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}