1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
38
39
40
41
42
43
44
45
46
47
48
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
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
186
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
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
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 }