001/* 002 * Copyright 2016 Anyware Services 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 * http://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.ametys.plugins.core.ui.resources; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.io.Serializable; 023import java.net.MalformedURLException; 024import java.net.URI; 025import java.net.URISyntaxException; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.parameters.Parameters; 032import org.apache.cocoon.ProcessingException; 033import org.apache.cocoon.ResourceNotFoundException; 034import org.apache.cocoon.components.source.SourceUtil; 035import org.apache.commons.io.FilenameUtils; 036import org.apache.commons.io.IOUtils; 037import org.apache.commons.lang3.StringUtils; 038import org.apache.excalibur.source.Source; 039import org.apache.excalibur.source.SourceException; 040import org.apache.excalibur.source.SourceValidity; 041import org.apache.excalibur.source.impl.validity.TimeStampValidity; 042 043import org.ametys.core.cocoon.AbstractResourceHandler; 044 045/** 046 * Abstract reader for resources compiled during runtime, such as SASS or LESS files compiled into CSS. 047 */ 048public abstract class AbstractCompiledResourceHandler extends AbstractResourceHandler implements Component 049{ 050 /* Dependencies cache */ 051 private static Map<String, Long> _dependenciesCacheValidity = new HashMap<>(); 052 private static Map<String, List<String>> _dependenciesCache = new HashMap<>(); 053 054 /** 055 * Compile the current resource, and returns its value. 056 * @param resource The current resource 057 * @return The compiled value 058 * @throws IOException If an IO error occurs 059 * @throws ProcessingException If a processing error occurs 060 */ 061 public abstract String compileResource(Source resource) throws IOException, ProcessingException; 062 063 /** 064 * Calculate the list of dependencies for the given source, for validity calculations. 065 * @param inputSource The source 066 * @return The list of uri 067 */ 068 protected abstract List<String> getDependenciesList(Source inputSource); 069 070 /** 071 * Get the compiled source uri 072 * @param location The requested location 073 * @return the source 074 * @throws MalformedURLException if location is malformed. 075 * @throws IOException If an IO error occurs 076 */ 077 protected abstract Source getCompiledSource(String location) throws MalformedURLException, IOException; 078 079 @Override 080 public boolean isSupported(String source) 081 { 082 if (!super.isSupported(source)) 083 { 084 return false; 085 } 086 087 // If the requested source ends with '.css', this handler supports it only if the CSS file does not exist. 088 String lcSource = StringUtils.lowerCase(source); 089 if (lcSource.endsWith(".css")) 090 { 091 Source src = null; 092 try 093 { 094 src = _resolver.resolveURI(source); 095 if (src.exists()) 096 { 097 return false; 098 } 099 } 100 catch (IOException e) 101 { 102 // Nothing 103 } 104 finally 105 { 106 _resolver.release(src); 107 } 108 } 109 110 return true; 111 } 112 113 @Override 114 public Source setup(String location, Parameters par) throws IOException, ProcessingException 115 { 116 try 117 { 118 Source source = getCompiledSource(location); 119 if (!source.exists()) 120 { 121 throw new ResourceNotFoundException("Resource not found for URI : " + location); 122 } 123 124 return source; 125 } 126 catch (SourceException e) 127 { 128 throw SourceUtil.handle("Error during resolving of '" + location + "'.", e); 129 } 130 } 131 132 @Override 133 public void generateResource(Source source, OutputStream out, Parameters parameters) throws IOException, ProcessingException 134 { 135 try (ByteArrayOutputStream os = new ByteArrayOutputStream()) 136 { 137 String compiledResource = compileResource(source); 138 IOUtils.write(compiledResource, out, "UTF-8"); 139 } 140 } 141 142 @Override 143 public Serializable getKey(Source source, Parameters parameters) 144 { 145 return _getDependenciesKeys(source, source.getURI(), FilenameUtils.normalize(source.getURI()), source.getLastModified(), new HashMap<String, String>()); 146 } 147 148 @Override 149 public SourceValidity getValidity(Source source, Parameters parameters) 150 { 151 Long lastModified = _getCalculatedLastModified(source, source.getURI(), source.getLastModified(), new HashMap<String, String>()); 152 return lastModified != null ? new TimeStampValidity(lastModified) : null; 153 } 154 155 private Long _getCalculatedLastModified(Source inputSource, String sourceUri, long lastModified, HashMap<String, String> knowDependencies) 156 { 157 long result = lastModified; 158 List<String> dependencies = _getDependencies(inputSource, sourceUri, lastModified); 159 160 for (String dependency : dependencies) 161 { 162 if (dependency != null && !StringUtils.startsWith(dependency, "http://") && !StringUtils.startsWith(dependency, "https://")) 163 { 164 try 165 { 166 String uriToResolve = _getDependencyURI(sourceUri, dependency); 167 168 HashMap<String, Object> params = new HashMap<>(); 169 Source dependencySource = _resolver.resolveURI(uriToResolve, null, params); 170 String fsURI = FilenameUtils.normalize(dependencySource.getURI()); 171 if (!knowDependencies.containsKey(fsURI)) 172 { 173 knowDependencies.put(fsURI, sourceUri); 174 175 Long calculatedLastModified = _getCalculatedLastModified(dependencySource, uriToResolve, dependencySource.getLastModified(), knowDependencies); 176 if (calculatedLastModified != null && calculatedLastModified > result) 177 { 178 result = calculatedLastModified; 179 } 180 } 181 } 182 catch (Exception e) 183 { 184 getLogger().warn("Unable to resolve the following uri : '" + dependency + "' while calculating dependencies for " + inputSource.getURI(), e); 185 return null; 186 } 187 } 188 } 189 190 return result; 191 } 192 193 private String _getDependenciesKeys(Source inputSource, String sourceUri, String fileURI, long lastModified, HashMap<String, String> knowDependencies) 194 { 195 String result = fileURI; 196 List<String> dependencies = _getDependencies(inputSource, sourceUri, lastModified); 197 198 for (String dependency : dependencies) 199 { 200 if (dependency != null && !StringUtils.startsWith(dependency, "http://") && !StringUtils.startsWith(dependency, "https://")) 201 { 202 try 203 { 204 String uriToResolve = _getDependencyURI(sourceUri, dependency); 205 206 HashMap<String, Object> params = new HashMap<>(); 207 Source dependencySource = _resolver.resolveURI(uriToResolve, null, params); 208 String fileDependencyURI = FilenameUtils.normalize(dependencySource.getURI()); 209 if (knowDependencies.containsKey(fileDependencyURI)) 210 { 211 getLogger().warn("A loop import was detected in a SASS file : '" + sourceUri + "' tried to import '" + fileDependencyURI 212 + "' but it was already previously imported by '" + knowDependencies.get(fileDependencyURI) + "'."); 213 } 214 else 215 { 216 knowDependencies.put(fileDependencyURI, sourceUri); 217 String dependenciesKeys = _getDependenciesKeys(dependencySource, uriToResolve, fileDependencyURI, dependencySource.getLastModified(), knowDependencies); 218 if (dependenciesKeys != null) 219 { 220 result = result + "#" + dependenciesKeys; 221 } 222 } 223 } 224 catch (Exception e) 225 { 226 getLogger().warn("Unable to resolve the following uri : '" + dependency + "' while calculating dependencies for " + inputSource.getURI(), e); 227 return null; 228 } 229 } 230 } 231 232 return result; 233 } 234 235 private String _getDependencyURI(String sourceUri, String dependency) throws URISyntaxException 236 { 237 URI uri = new URI(dependency); 238 String uriToResolve = uri.isAbsolute() ? dependency : FilenameUtils.getFullPath(sourceUri) + dependency; 239 240 // Don't normalize the schema part of the uri 241 String schema = StringUtils.contains(uriToResolve, "://") ? uriToResolve.substring(0, uriToResolve.indexOf("://") + 3) : ""; 242 uriToResolve = schema + FilenameUtils.normalize(StringUtils.removeStart(uriToResolve, schema)); 243 return uriToResolve; 244 } 245 246 private List<String> _getDependencies(Source inputSource, String sourceUri, long lastModified) 247 { 248 Long cachedValidity = _dependenciesCacheValidity.get(sourceUri); 249 250 List<String> dependencies; 251 252 if (cachedValidity == null || !cachedValidity.equals(lastModified)) 253 { 254 // Cache is out of date 255 dependencies = getDependenciesList(inputSource); 256 _dependenciesCacheValidity.put(sourceUri, lastModified); 257 _dependenciesCache.put(sourceUri, dependencies); 258 } 259 else 260 { 261 dependencies = _dependenciesCache.get(sourceUri); 262 } 263 return dependencies; 264 } 265 266}