001// Copyright 2013 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.internal.services.assets; 016 017import org.apache.tapestry5.Asset; 018import org.apache.tapestry5.ioc.IOOperation; 019import org.apache.tapestry5.ioc.OperationTracker; 020import org.apache.tapestry5.ioc.Resource; 021import org.apache.tapestry5.services.AssetSource; 022import org.apache.tapestry5.services.assets.*; 023 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.InputStreamReader; 027import java.util.regex.Matcher; 028import java.util.regex.Pattern; 029 030/** 031 * Rewrites the {@code url()} attributes inside a CSS (MIME type "text/css")) resource. 032 * Each {@code url} is expanded to a complete path; this allows for CSS aggregation, where the location of the 033 * CSS file will change (which would ordinarily break relative URLs), and for changing the relative directories of 034 * the CSS file and the image assets it may refer to (useful for incorporating a hash of the resource's content into 035 * the exposed URL). 036 * <p/> 037 * <p/> 038 * One potential problem with URL rewriting is the way that URLs for referenced resources are generated; we are 039 * somewhat banking on the fact that referenced resources are non-compressable images. 040 * 041 * @since 5.4 042 */ 043public class CSSURLRewriter extends DelegatingSRS 044{ 045 // Group 1 is the optional single or double quote (note the use of backtracking to match it) 046 // Group 2 is the text inside the quotes, or inside the parens if no quotes 047 // Group 3 is any query parmameters (see issue TAP5-2106) 048 private final Pattern urlPattern = Pattern.compile( 049 "url" + 050 "\\(" + // opening paren 051 "\\s*" + 052 "(['\"]?)" + // group 1: optional single or double quote 053 "(.+?)" + // group 2: the main part of the URL, up to the first '#' or '?' 054 "([\\#\\?].*?)?" + // group 3: Optional '#' or '?' to end of string 055 "\\1" + // optional closing single/double quote 056 "\\s*" + 057 "\\)"); // matching close paren 058 059 // Does it start with a '/' or what looks like a scheme ("http:")? 060 private final Pattern completeURLPattern = Pattern.compile("^[#/]|(\\p{Alpha}\\w*:)"); 061 062 private final OperationTracker tracker; 063 064 private final AssetSource assetSource; 065 066 private final AssetChecksumGenerator checksumGenerator; 067 068 public CSSURLRewriter(StreamableResourceSource delegate, OperationTracker tracker, AssetSource assetSource, AssetChecksumGenerator checksumGenerator) 069 { 070 super(delegate); 071 this.tracker = tracker; 072 this.assetSource = assetSource; 073 this.checksumGenerator = checksumGenerator; 074 } 075 076 @Override 077 public StreamableResource getStreamableResource(Resource baseResource, StreamableResourceProcessing processing, ResourceDependencies dependencies) throws IOException 078 { 079 StreamableResource base = delegate.getStreamableResource(baseResource, processing, dependencies); 080 081 if (base.getContentType().equals("text/css")) 082 { 083 return filter(base, baseResource); 084 } 085 086 return base; 087 } 088 089 private StreamableResource filter(final StreamableResource base, final Resource baseResource) throws IOException 090 { 091 return tracker.perform("Rewriting relative URLs in " + baseResource, 092 new IOOperation<StreamableResource>() 093 { 094 public StreamableResource perform() throws IOException 095 { 096 String baseString = readAsString(base); 097 098 String filtered = replaceURLs(baseString, baseResource); 099 100 if (filtered == null) 101 { 102 // No URLs were replaced so no need to create a new StreamableResource 103 return base; 104 } 105 106 BytestreamCache cache = new BytestreamCache(filtered.getBytes("UTF-8")); 107 108 return new StreamableResourceImpl(base.getDescription(), "text/css", 109 CompressionStatus.COMPRESSABLE, 110 base.getLastModified(), 111 cache, checksumGenerator); 112 } 113 }); 114 } 115 116 /** 117 * Replaces any relative URLs in the content for the resource and returns the content with 118 * the URLs expanded. 119 * 120 * @param input 121 * content of the resource 122 * @param baseResource 123 * resource used to resolve relative URLs 124 * @return replacement content, or null if no relative URLs in the content 125 */ 126 private String replaceURLs(String input, Resource baseResource) 127 { 128 boolean didReplace = false; 129 130 StringBuffer output = new StringBuffer(input.length()); 131 132 Matcher matcher = urlPattern.matcher(input); 133 134 while (matcher.find()) 135 { 136 String url = matcher.group(2); // the string inside the quotes 137 138 // When the URL starts with a slash, there's no need to rewrite it (this is actually rare in Tapestry 139 // as you want to use relative URLs to leverage the asset pipeline. 140 if (completeURLPattern.matcher(url).find()) 141 { 142 String queryParameters = matcher.group(3); 143 144 if (queryParameters != null) 145 { 146 url = url + queryParameters; 147 } 148 149 // This may normalize single quotes, or missing quotes, to double quotes, but is not 150 // considered a real change, since all such variations are valid. 151 appendReplacement(matcher, output, url); 152 continue; 153 } 154 155 Asset asset = assetSource.getAsset(baseResource, url, null); 156 157 String assetURL = asset.toClientURL(); 158 159 String queryParameters = matcher.group(3); 160 if (queryParameters != null) 161 { 162 assetURL += queryParameters; 163 } 164 165 appendReplacement(matcher, output, assetURL); 166 167 didReplace = true; 168 } 169 170 if (!didReplace) 171 { 172 return null; 173 } 174 175 matcher.appendTail(output); 176 177 return output.toString(); 178 } 179 180 private void appendReplacement(Matcher matcher, StringBuffer output, String assetURL) 181 { 182 matcher.appendReplacement(output, String.format("url(\"%s\")", assetURL)); 183 } 184 185 186 // TODO: I'm thinking there's an (internal) service that should be created to make this more reusable. 187 private String readAsString(StreamableResource resource) throws IOException 188 { 189 StringBuffer result = new StringBuffer(resource.getSize()); 190 char[] buffer = new char[5000]; 191 192 InputStream is = resource.openStream(); 193 194 InputStreamReader reader = new InputStreamReader(is, "UTF-8"); 195 196 try 197 { 198 199 while (true) 200 { 201 int length = reader.read(buffer); 202 203 if (length < 0) 204 { 205 break; 206 } 207 208 result.append(buffer, 0, length); 209 } 210 } finally 211 { 212 reader.close(); 213 is.close(); 214 } 215 216 return result.toString(); 217 } 218}