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.Serializable; 020import java.net.URI; 021import java.net.URISyntaxException; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import java.util.concurrent.ConcurrentHashMap; 026 027import org.apache.avalon.framework.component.Component; 028import org.apache.commons.io.FilenameUtils; 029import org.apache.commons.lang3.StringUtils; 030import org.apache.commons.lang3.tuple.Pair; 031import org.apache.excalibur.source.Source; 032import org.apache.excalibur.source.SourceValidity; 033import org.apache.excalibur.source.impl.validity.TimeStampValidity; 034 035import org.ametys.core.resources.DefaultResourceHandler; 036 037/** 038 * Abstract reader for resources compiled during runtime, such as SASS or LESS files compiled into CSS. 039 */ 040public abstract class AbstractCompiledResourceHandler extends DefaultResourceHandler implements Component 041{ 042 /* Dependencies cache */ 043 private static Map<String, Pair<List<String>, Long>> _dependenciesCache = new ConcurrentHashMap<>(); 044 045 /** 046 * Constructor with an already resolved {@link Source}. 047 * @param source the source 048 */ 049 public AbstractCompiledResourceHandler(Source source) 050 { 051 super(source); 052 } 053 054 /** 055 * Calculate the list of dependencies for the given source, for validity calculations. 056 * @param inputSource The source 057 * @return The list of uri 058 */ 059 protected abstract List<String> getDependenciesList(Source inputSource); 060 061 @Override 062 public Serializable getKey() 063 { 064 return _getDependenciesKeys(_source, _source.getURI(), FilenameUtils.normalize(_source.getURI()), _source.getLastModified(), new HashMap<String, String>()); 065 } 066 067 @Override 068 public SourceValidity getValidity() 069 { 070 Long lastModified = _getCalculatedLastModified(_source, _source.getURI(), _source.getLastModified(), new HashMap<String, String>()); 071 return lastModified != null ? new TimeStampValidity(lastModified) : null; 072 } 073 074 private Long _getCalculatedLastModified(Source inputSource, String sourceUri, long lastModified, HashMap<String, String> knowDependencies) 075 { 076 long result = lastModified; 077 List<String> dependencies = _getDependencies(inputSource, sourceUri, lastModified); 078 079 for (String dependency : dependencies) 080 { 081 if (dependency != null && !StringUtils.startsWith(dependency, "http://") && !StringUtils.startsWith(dependency, "https://")) 082 { 083 try 084 { 085 String uriToResolve = _getDependencyURI(sourceUri, dependency); 086 087 HashMap<String, Object> params = new HashMap<>(); 088 Source dependencySource = _resolver.resolveURI(uriToResolve, null, params); 089 String fsURI = FilenameUtils.normalize(dependencySource.getURI()); 090 if (!knowDependencies.containsKey(fsURI)) 091 { 092 knowDependencies.put(fsURI, sourceUri); 093 094 Long calculatedLastModified = _getCalculatedLastModified(dependencySource, uriToResolve, dependencySource.getLastModified(), knowDependencies); 095 if (calculatedLastModified != null && calculatedLastModified > result) 096 { 097 result = calculatedLastModified; 098 } 099 } 100 } 101 catch (Exception e) 102 { 103 getLogger().warn("Unable to resolve the following uri : '" + dependency + "' while calculating dependencies for " + inputSource.getURI(), e); 104 return null; 105 } 106 } 107 } 108 109 return result; 110 } 111 112 private String _getDependenciesKeys(Source inputSource, String sourceUri, String fileURI, long lastModified, HashMap<String, String> knowDependencies) 113 { 114 String result = fileURI; 115 List<String> dependencies = _getDependencies(inputSource, sourceUri, lastModified); 116 117 for (String dependency : dependencies) 118 { 119 if (dependency != null && !StringUtils.startsWith(dependency, "http://") && !StringUtils.startsWith(dependency, "https://")) 120 { 121 try 122 { 123 String uriToResolve = _getDependencyURI(sourceUri, dependency); 124 125 HashMap<String, Object> params = new HashMap<>(); 126 Source dependencySource = _resolver.resolveURI(uriToResolve, null, params); 127 String fileDependencyURI = FilenameUtils.normalize(dependencySource.getURI()); 128 if (knowDependencies.containsKey(fileDependencyURI)) 129 { 130 getLogger().warn("A loop import was detected in file : '" + sourceUri + "' that imports '" + fileDependencyURI 131 + "' but it was already previously imported by '" + knowDependencies.get(fileDependencyURI) + "'."); 132 } 133 else 134 { 135 knowDependencies.put(fileDependencyURI, sourceUri); 136 String dependenciesKeys = _getDependenciesKeys(dependencySource, uriToResolve, fileDependencyURI, dependencySource.getLastModified(), knowDependencies); 137 if (dependenciesKeys != null) 138 { 139 result = result + "*" + dependenciesKeys; 140 } 141 } 142 } 143 catch (Exception e) 144 { 145 getLogger().warn("Unable to resolve the following uri : '" + dependency + "' while calculating dependencies for " + inputSource.getURI(), e); 146 return null; 147 } 148 } 149 } 150 151 return result; 152 } 153 154 private String _getDependencyURI(String sourceUri, String dependency) throws URISyntaxException 155 { 156 URI uri = new URI(dependency); 157 String uriToResolve = uri.isAbsolute() ? dependency : FilenameUtils.getFullPath(sourceUri) + dependency; 158 159 // Don't normalize the schema part of the uri 160 String schema = StringUtils.contains(uriToResolve, "://") ? uriToResolve.substring(0, uriToResolve.indexOf("://") + 3) : ""; 161 uriToResolve = schema + FilenameUtils.normalize(StringUtils.removeStart(uriToResolve, schema)); 162 return uriToResolve; 163 } 164 165 private List<String> _getDependencies(Source inputSource, String sourceUri, long lastModified) 166 { 167 Pair<List<String>, Long> cachedDependencies = _dependenciesCache.get(sourceUri); 168 169 List<String> dependencies; 170 171 if (cachedDependencies == null || !cachedDependencies.getRight().equals(lastModified)) 172 { 173 // Cache is out of date 174 dependencies = getDependenciesList(inputSource); 175 _dependenciesCache.put(sourceUri, Pair.of(dependencies, lastModified)); 176 } 177 else 178 { 179 dependencies = cachedDependencies.getLeft(); 180 } 181 182 return dependencies; 183 } 184}