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