001/* 002 * Copyright 2002-2016 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.servlet.resource; 018 019import java.io.IOException; 020import java.io.StringWriter; 021import java.nio.charset.Charset; 022import java.util.ArrayList; 023import java.util.Collections; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Set; 027import javax.servlet.http.HttpServletRequest; 028 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031 032import org.springframework.core.io.Resource; 033import org.springframework.util.FileCopyUtils; 034import org.springframework.util.StringUtils; 035 036/** 037 * A {@link ResourceTransformer} implementation that modifies links in a CSS 038 * file to match the public URL paths that should be exposed to clients (e.g. 039 * with an MD5 content-based hash inserted in the URL). 040 * 041 * <p>The implementation looks for links in CSS {@code @import} statements and 042 * also inside CSS {@code url()} functions. All links are then passed through the 043 * {@link ResourceResolverChain} and resolved relative to the location of the 044 * containing CSS file. If successfully resolved, the link is modified, otherwise 045 * the original link is preserved. 046 * 047 * @author Rossen Stoyanchev 048 * @since 4.1 049 */ 050public class CssLinkResourceTransformer extends ResourceTransformerSupport { 051 052 private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 053 054 private static final Log logger = LogFactory.getLog(CssLinkResourceTransformer.class); 055 056 private final List<CssLinkParser> linkParsers = new ArrayList<CssLinkParser>(2); 057 058 059 public CssLinkResourceTransformer() { 060 this.linkParsers.add(new ImportStatementCssLinkParser()); 061 this.linkParsers.add(new UrlFunctionCssLinkParser()); 062 } 063 064 065 @Override 066 public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain) 067 throws IOException { 068 069 resource = transformerChain.transform(request, resource); 070 071 String filename = resource.getFilename(); 072 if (!"css".equals(StringUtils.getFilenameExtension(filename))) { 073 return resource; 074 } 075 076 if (logger.isTraceEnabled()) { 077 logger.trace("Transforming resource: " + resource); 078 } 079 080 byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream()); 081 String content = new String(bytes, DEFAULT_CHARSET); 082 083 Set<CssLinkInfo> infos = new HashSet<CssLinkInfo>(8); 084 for (CssLinkParser parser : this.linkParsers) { 085 parser.parseLink(content, infos); 086 } 087 088 if (infos.isEmpty()) { 089 if (logger.isTraceEnabled()) { 090 logger.trace("No links found."); 091 } 092 return resource; 093 } 094 095 List<CssLinkInfo> sortedInfos = new ArrayList<CssLinkInfo>(infos); 096 Collections.sort(sortedInfos); 097 098 int index = 0; 099 StringWriter writer = new StringWriter(); 100 for (CssLinkInfo info : sortedInfos) { 101 writer.write(content.substring(index, info.getStart())); 102 String link = content.substring(info.getStart(), info.getEnd()); 103 String newLink = null; 104 if (!hasScheme(link)) { 105 newLink = resolveUrlPath(link, request, resource, transformerChain); 106 } 107 if (logger.isTraceEnabled()) { 108 if (newLink != null && !link.equals(newLink)) { 109 logger.trace("Link modified: " + newLink + " (original: " + link + ")"); 110 } 111 else { 112 logger.trace("Link not modified: " + link); 113 } 114 } 115 writer.write(newLink != null ? newLink : link); 116 index = info.getEnd(); 117 } 118 writer.write(content.substring(index)); 119 120 return new TransformedResource(resource, writer.toString().getBytes(DEFAULT_CHARSET)); 121 } 122 123 private boolean hasScheme(String link) { 124 int schemeIndex = link.indexOf(':'); 125 return (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/")) || link.indexOf("//") == 0; 126 } 127 128 129 protected interface CssLinkParser { 130 131 void parseLink(String content, Set<CssLinkInfo> linkInfos); 132 } 133 134 135 protected static abstract class AbstractCssLinkParser implements CssLinkParser { 136 137 /** 138 * Return the keyword to use to search for links. 139 */ 140 protected abstract String getKeyword(); 141 142 @Override 143 public void parseLink(String content, Set<CssLinkInfo> linkInfos) { 144 int index = 0; 145 do { 146 index = content.indexOf(getKeyword(), index); 147 if (index == -1) { 148 break; 149 } 150 index = skipWhitespace(content, index + getKeyword().length()); 151 if (content.charAt(index) == '\'') { 152 index = addLink(index, "'", content, linkInfos); 153 } 154 else if (content.charAt(index) == '"') { 155 index = addLink(index, "\"", content, linkInfos); 156 } 157 else { 158 index = extractLink(index, content, linkInfos); 159 160 } 161 } 162 while (true); 163 } 164 165 private int skipWhitespace(String content, int index) { 166 while (true) { 167 if (Character.isWhitespace(content.charAt(index))) { 168 index++; 169 continue; 170 } 171 return index; 172 } 173 } 174 175 protected int addLink(int index, String endKey, String content, Set<CssLinkInfo> linkInfos) { 176 int start = index + 1; 177 int end = content.indexOf(endKey, start); 178 linkInfos.add(new CssLinkInfo(start, end)); 179 return end + endKey.length(); 180 } 181 182 /** 183 * Invoked after a keyword match, after whitespaces removed, and when 184 * the next char is neither a single nor double quote. 185 */ 186 protected abstract int extractLink(int index, String content, Set<CssLinkInfo> linkInfos); 187 188 } 189 190 191 private static class ImportStatementCssLinkParser extends AbstractCssLinkParser { 192 193 @Override 194 protected String getKeyword() { 195 return "@import"; 196 } 197 198 @Override 199 protected int extractLink(int index, String content, Set<CssLinkInfo> linkInfos) { 200 if (content.substring(index, index + 4).equals("url(")) { 201 // Ignore, UrlLinkParser will take care 202 } 203 else if (logger.isErrorEnabled()) { 204 logger.error("Unexpected syntax for @import link at index " + index); 205 } 206 return index; 207 } 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 public CssLinkInfo(int start, int end) { 233 this.start = start; 234 this.end = end; 235 } 236 237 public int getStart() { 238 return this.start; 239 } 240 241 public int getEnd() { 242 return this.end; 243 } 244 245 @Override 246 public int compareTo(CssLinkInfo other) { 247 return (this.start < other.start ? -1 : (this.start == other.start ? 0 : 1)); 248 } 249 250 @Override 251 public boolean equals(Object obj) { 252 if (this == obj) { 253 return true; 254 } 255 if (obj != null && obj instanceof CssLinkInfo) { 256 CssLinkInfo other = (CssLinkInfo) obj; 257 return (this.start == other.start && this.end == other.end); 258 } 259 return false; 260 } 261 262 @Override 263 public int hashCode() { 264 return this.start * 31 + this.end; 265 } 266 } 267 268}