001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.resource; 018 019import java.util.Collection; 020import java.util.LinkedHashSet; 021import java.util.Set; 022import java.util.regex.Matcher; 023import java.util.regex.Pattern; 024 025import org.apache.wicket.WicketRuntimeException; 026import org.apache.wicket.css.ICssCompressor; 027import org.apache.wicket.request.Url; 028import org.apache.wicket.request.cycle.RequestCycle; 029import org.apache.wicket.request.resource.PackageResourceReference; 030import org.apache.wicket.util.image.ImageUtil; 031 032/** 033 * This compressor is used to replace URLs within CSS files with URLs created from 034 * PackageResourceReferences that belongs to their corresponding resources (e.g images).The scope of 035 * the CSS file is used to create the PackageResourceReferences. The compress method is not 036 * compressing any content, but replacing the URLs with Wicket representatives.<br> 037 * <br> 038 * Usage: 039 * 040 * <pre> 041 * this.getResourceSettings().setCssCompressor(new CssUrlReplacer()); 042 * </pre> 043 * 044 * @since 6.20.0 045 * @author Tobias Soloschenko 046 */ 047public class CssUrlReplacer implements IScopeAwareTextResourceProcessor, ICssCompressor 048{ 049 // The pattern to find URLs in CSS resources 050 private static final Pattern URL_PATTERN = Pattern 051 .compile("url\\([ ]*['|\"]?([^ ]*?)['|\"]?[ ]*\\)"); 052 053 /** 054 * Used to be append to CSS URLs (background-image: url('Beer.gif?embedBase64');). The 055 * CssUrlReplacer embeds the base64 content instead of using an URL. 056 */ 057 public static final String EMBED_BASE64 = "embedBase64"; 058 059 private final Set<String> excludes = new LinkedHashSet<>(); 060 061 /** 062 * Creates a css url replacer 063 */ 064 public CssUrlReplacer() 065 { 066 } 067 068 /** 069 * Creates a css url replacer 070 * 071 * @param excludes 072 * css file names to be excluded 073 */ 074 public CssUrlReplacer(Collection<String> excludes) 075 { 076 this.excludes.addAll(excludes); 077 } 078 079 /** 080 * Replaces the URLs of CSS resources with Wicket representatives. 081 */ 082 @Override 083 public String process(String input, Class<?> scope, String name) 084 { 085 // filter out the excluded css files 086 for (String excludeName : excludes) 087 { 088 if(name.endsWith(excludeName)){ 089 return input; 090 } 091 } 092 RequestCycle cycle = RequestCycle.get(); 093 Url cssUrl = Url.parse(name); 094 Matcher matcher = URL_PATTERN.matcher(input); 095 StringBuffer output = new StringBuffer(); 096 097 while (matcher.find()) 098 { 099 Url imageCandidateUrl = Url.parse(matcher.group(1)); 100 CharSequence processedUrl; 101 boolean embedded = false; 102 103 if (imageCandidateUrl.isFull()) 104 { 105 processedUrl = imageCandidateUrl.toString(Url.StringMode.FULL); 106 } 107 else if (imageCandidateUrl.isContextAbsolute()) 108 { 109 processedUrl = imageCandidateUrl.toString(); 110 } 111 else if (imageCandidateUrl.isDataUrl()) 112 { 113 embedded = true; 114 processedUrl = imageCandidateUrl.toString(); 115 } 116 else 117 { 118 // relativize against the url for the containing CSS file 119 Url cssUrlCopy = new Url(cssUrl); 120 cssUrlCopy.resolveRelative(imageCandidateUrl); 121 122 // if the image should be processed as URL or base64 embedded 123 if (cssUrlCopy.getQueryString() != null 124 && cssUrlCopy.getQueryString().contains(EMBED_BASE64)) 125 { 126 embedded = true; 127 PackageResourceReference imageReference = new PackageResourceReference(scope, 128 cssUrlCopy.toString().replace("?" + EMBED_BASE64, "")); 129 try 130 { 131 processedUrl = ImageUtil.createBase64EncodedImage(imageReference, true); 132 } 133 catch (Exception e) 134 { 135 throw new WicketRuntimeException( 136 "Error while embedding an image into the css: " + imageReference, e); 137 } 138 } 139 else 140 { 141 PackageResourceReference imageReference = new PackageResourceReference(scope, 142 cssUrlCopy.toString()); 143 processedUrl = cycle.urlFor(imageReference, null); 144 } 145 146 } 147 148 // embedded data urls don't need single quotes, but regular urls do: 149 matcher.appendReplacement(output, 150 embedded ? "url(" + processedUrl + ")" : "url('" + processedUrl + "')"); 151 } 152 matcher.appendTail(output); 153 return output.toString(); 154 } 155 156 @Override 157 public String compress(String original) 158 { 159 throw new UnsupportedOperationException( 160 CssUrlReplacer.class.getSimpleName() + ".process() should be used instead!"); 161 } 162 163 /** 164 * Gets excluded css file names 165 * 166 * @return a list with css file names to be excluded 167 */ 168 public Collection<String> getExcludes() 169 { 170 return excludes; 171 } 172 173 /** 174 * Sets a list of css file names to be excluded 175 * 176 * @param excludes 177 * a list with css file names to be excluded 178 */ 179 public void setExcludes(Collection<String> excludes) 180 { 181 this.excludes.clear(); 182 this.excludes.addAll(excludes); 183 } 184}