View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.servlet.resource;
18  
19  import java.io.IOException;
20  import java.io.StringWriter;
21  import java.nio.charset.Charset;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Set;
27  import javax.servlet.http.HttpServletRequest;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  
32  import org.springframework.core.io.Resource;
33  import org.springframework.util.FileCopyUtils;
34  import org.springframework.util.StringUtils;
35  
36  /**
37   * A {@link ResourceTransformer} implementation that modifies links in a CSS
38   * file to match the public URL paths that should be exposed to clients (e.g.
39   * with an MD5 content-based hash inserted in the URL).
40   *
41   * <p>The implementation looks for links in CSS {@code @import} statements and
42   * also inside CSS {@code url()} functions. All links are then passed through the
43   * {@link ResourceResolverChain} and resolved relative to the location of the
44   * containing CSS file. If successfully resolved, the link is modified, otherwise
45   * the original link is preserved.
46   *
47   * @author Rossen Stoyanchev
48   * @since 4.1
49   */
50  public class CssLinkResourceTransformer extends ResourceTransformerSupport {
51  
52  	private static final Log logger = LogFactory.getLog(CssLinkResourceTransformer.class);
53  
54  	private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
55  
56  
57  	private final List<CssLinkParser> linkParsers = new ArrayList<CssLinkParser>();
58  
59  
60  	public CssLinkResourceTransformer() {
61  		this.linkParsers.add(new ImportStatementCssLinkParser());
62  		this.linkParsers.add(new UrlFunctionCssLinkParser());
63  	}
64  
65  
66  	@Override
67  	public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain)
68  			throws IOException {
69  
70  		resource = transformerChain.transform(request, resource);
71  
72  		String filename = resource.getFilename();
73  		if (!"css".equals(StringUtils.getFilenameExtension(filename))) {
74  			return resource;
75  		}
76  
77  		if (logger.isTraceEnabled()) {
78  			logger.trace("Transforming resource: " + resource);
79  		}
80  
81  		byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
82  		String content = new String(bytes, DEFAULT_CHARSET);
83  
84  		Set<CssLinkInfo> infos = new HashSet<CssLinkInfo>(5);
85  		for (CssLinkParser parser : this.linkParsers) {
86  			parser.parseLink(content, infos);
87  		}
88  
89  		if (infos.isEmpty()) {
90  			if (logger.isTraceEnabled()) {
91  				logger.trace("No links found.");
92  			}
93  			return resource;
94  		}
95  
96  		List<CssLinkInfo> sortedInfos = new ArrayList<CssLinkInfo>(infos);
97  		Collections.sort(sortedInfos);
98  
99  		int index = 0;
100 		StringWriter writer = new StringWriter();
101 		for (CssLinkInfo info : sortedInfos) {
102 			writer.write(content.substring(index, info.getStart()));
103 			String link = content.substring(info.getStart(), info.getEnd());
104 			String newLink = null;
105 			if (!hasScheme(link)) {
106 				newLink = resolveUrlPath(link, request, resource, transformerChain);
107 			}
108 			if (logger.isTraceEnabled()) {
109 				if (newLink != null && !link.equals(newLink)) {
110 					logger.trace("Link modified: " + newLink + " (original: " + link + ")");
111 				}
112 				else {
113 					logger.trace("Link not modified: " + link);
114 				}
115 			}
116 			writer.write(newLink != null ? newLink : link);
117 			index = info.getEnd();
118 		}
119 		writer.write(content.substring(index));
120 
121 		return new TransformedResource(resource, writer.toString().getBytes(DEFAULT_CHARSET));
122 	}
123 
124 	private boolean hasScheme(String link) {
125 		int schemeIndex = link.indexOf(":");
126 		return (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/"))
127 				|| link.indexOf("//") == 0;
128 	}
129 
130 
131 	protected static interface CssLinkParser {
132 
133 		void parseLink(String content, Set<CssLinkInfo> linkInfos);
134 
135 	}
136 
137 	protected static abstract class AbstractCssLinkParser implements CssLinkParser {
138 
139 		/**
140 		 * Return the keyword to use to search for links.
141 		 */
142 		protected abstract String getKeyword();
143 
144 		@Override
145 		public void parseLink(String content, Set<CssLinkInfo> linkInfos) {
146 			int index = 0;
147 			do {
148 				index = content.indexOf(getKeyword(), index);
149 				if (index == -1) {
150 					break;
151 				}
152 				index = skipWhitespace(content, index + getKeyword().length());
153 				if (content.charAt(index) == '\'') {
154 					index = addLink(index, "'", content, linkInfos);
155 				}
156 				else if (content.charAt(index) == '"') {
157 					index = addLink(index, "\"", content, linkInfos);
158 				}
159 				else {
160 					index = extractLink(index, content, linkInfos);
161 
162 				}
163 			}
164 			while (true);
165 		}
166 
167 		private int skipWhitespace(String content, int index) {
168 			while (true) {
169 				if (Character.isWhitespace(content.charAt(index))) {
170 					index++;
171 					continue;
172 				}
173 				return index;
174 			}
175 		}
176 
177 		protected int addLink(int index, String endKey, String content, Set<CssLinkInfo> linkInfos) {
178 			int start = index + 1;
179 			int end = content.indexOf(endKey, start);
180 			linkInfos.add(new CssLinkInfo(start, end));
181 			return end + endKey.length();
182 		}
183 
184 		/**
185 		 * Invoked after a keyword match, after whitespaces removed, and when
186 		 * the next char is neither a single nor double quote.
187 		 */
188 		protected abstract int extractLink(int index, String content, Set<CssLinkInfo> linkInfos);
189 
190 	}
191 
192 	private static class ImportStatementCssLinkParser extends AbstractCssLinkParser {
193 
194 		@Override
195 		protected String getKeyword() {
196 			return "@import";
197 		}
198 
199 		@Override
200 		protected int extractLink(int index, String content, Set<CssLinkInfo> linkInfos) {
201 			if (content.substring(index, index + 4).equals("url(")) {
202 				// Ignore, UrlLinkParser will take care
203 			}
204 			else if (logger.isErrorEnabled()) {
205 				logger.error("Unexpected syntax for @import link at index " + index);
206 			}
207 			return index;
208 		}
209 	}
210 
211 	private static class UrlFunctionCssLinkParser extends AbstractCssLinkParser {
212 
213 		@Override
214 		protected String getKeyword() {
215 			return "url(";
216 		}
217 
218 		@Override
219 		protected int extractLink(int index, String content, Set<CssLinkInfo> linkInfos) {
220 			// A url() function without unquoted
221 			return addLink(index - 1, ")", content, linkInfos);
222 		}
223 	}
224 
225 
226 	private static class CssLinkInfo implements Comparable<CssLinkInfo> {
227 
228 		private final int start;
229 
230 		private final int end;
231 
232 
233 		private CssLinkInfo(int start, int end) {
234 			this.start = start;
235 			this.end = end;
236 		}
237 
238 		public int getStart() {
239 			return this.start;
240 		}
241 
242 		public int getEnd() {
243 			return this.end;
244 		}
245 
246 		@Override
247 		public int compareTo(CssLinkInfo other) {
248 			return (this.start < other.start ? -1 : (this.start == other.start ? 0 : 1));
249 		}
250 
251 		@Override
252 		public boolean equals(Object obj) {
253 			if (this == obj) {
254 				return true;
255 			}
256 			if (obj != null && obj instanceof CssLinkInfo) {
257 				CssLinkInfo other = (CssLinkInfo) obj;
258 				return (this.start == other.start && this.end == other.end);
259 			}
260 			return false;
261 		}
262 
263 		@Override
264 		public int hashCode() {
265 			return this.start * 31 + this.end;
266 		}
267 	}
268 
269 }