001/* 002 * Copyright 2018 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 */ 016package org.ametys.plugins.core.ui.resources.css; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.net.URI; 021import java.net.URISyntaxException; 022import java.nio.charset.StandardCharsets; 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Optional; 030import java.util.Set; 031import java.util.concurrent.ConcurrentHashMap; 032import java.util.regex.Matcher; 033import java.util.regex.Pattern; 034 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.commons.io.FilenameUtils; 039import org.apache.commons.io.IOUtils; 040import org.apache.commons.lang3.StringUtils; 041import org.apache.commons.lang3.tuple.Pair; 042import org.apache.excalibur.source.Source; 043import org.apache.excalibur.source.SourceResolver; 044 045import org.ametys.core.resources.ProxiedContextPathProvider; 046import org.ametys.core.resources.ResourceReader; 047import org.ametys.plugins.core.ui.minimize.HashCache.UriData; 048import org.ametys.plugins.core.ui.resources.ResourceDependenciesList; 049import org.ametys.plugins.core.ui.util.RequestAttributesHelper; 050import org.ametys.runtime.plugin.component.AbstractLogEnabled; 051 052/** 053 * Dependencies list for css files 054 */ 055public class CssDependenciesList extends AbstractLogEnabled implements ResourceDependenciesList, Serviceable 056{ 057 private static final Pattern __EXTERNAL_URL = Pattern.compile("^(http[s]?://[^/]+)(/.*)?$"); 058 059 /** RequestAttributesHelper */ 060 protected RequestAttributesHelper _requestAttributesHelper; 061 062 /* Dependencies cache */ 063 private Map<String, Pair<List<String>, Long>> _dependenciesCache = new ConcurrentHashMap<>(); 064 065 private SourceResolver _sourceResolver; 066 067 private ProxiedContextPathProvider _proxiedContextPathProvider; 068 069 public void service(ServiceManager manager) throws ServiceException 070 { 071 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 072 _requestAttributesHelper = (RequestAttributesHelper) manager.lookup(RequestAttributesHelper.ROLE); 073 _proxiedContextPathProvider = (ProxiedContextPathProvider) manager.lookup(ProxiedContextPathProvider.ROLE); 074 } 075 076 @Override 077 public boolean isSupported(String uri) 078 { 079 return StringUtils.endsWith(uri, ".css"); 080 } 081 082 @Override 083 public int getPriority() 084 { 085 return 100; 086 } 087 088 public Set<UriData> getDependenciesList(String uri, Map<String, String> data, boolean onlyFirstLevel) 089 { 090 return _getFileDependencies(uri, data.getOrDefault("media", null), true, onlyFirstLevel); 091 } 092 093 private Set<UriData> _getFileDependencies(String cssUri, String media, boolean firstLevel, boolean onlyFirstLevel) throws IllegalArgumentException 094 { 095 Set<UriData> dependencies = new LinkedHashSet<>(); 096 097 Source fileSource = null; 098 Map<String, Object> resolveParameters = new HashMap<>(); 099 100 try 101 { 102 fileSource = _getFileSource(cssUri, resolveParameters); 103 104 long fileLastModified = resolveParameters.get(ResourceReader.LAST_MODIFIED) != null ? (long) resolveParameters.get("lastModified") : -1; 105 106 UriData fileInfos = new UriData(cssUri, firstLevel); 107 fileInfos.setLastModified(fileLastModified); 108 fileInfos.setMedia(media); 109 dependencies.add(fileInfos); 110 111 if (onlyFirstLevel) 112 { 113 return dependencies; 114 } 115 116 Long validity = Optional.ofNullable(_dependenciesCache.get(cssUri)) 117 .map(Pair::getRight) 118 .orElse(null); 119 120 if (validity == null || validity != fileLastModified) 121 { 122 // cache is outdated 123 List<String> dependenciesCache = new ArrayList<>(); 124 125 for (UriData cssDependency : _getCssFileDependencies(cssUri, fileSource)) 126 { 127 if (!dependenciesCache.contains(cssDependency.getUri())) 128 { 129 dependenciesCache.add(cssDependency.getUri()); 130 dependencies.add(cssDependency); 131 } 132 } 133 134 _dependenciesCache.put(cssUri, Pair.of(dependenciesCache, fileLastModified)); 135 } 136 else 137 { 138 // cache is up to date 139 Pair<List<String>, Long> dependenciesFromCache = _dependenciesCache.get(cssUri); 140 if (dependenciesFromCache != null) 141 { 142 for (String dependencyCached : dependenciesFromCache.getLeft()) 143 { 144 dependencies.addAll(_getFileDependencies(dependencyCached, null, false, false)); 145 } 146 } 147 } 148 } 149 finally 150 { 151 _sourceResolver.release(fileSource); 152 } 153 154 return dependencies; 155 } 156 157 private Source _getFileSource(String cssUri, Map<String, Object> resolveParameters) 158 { 159 try 160 { 161 String uriToResolve = cssUri; 162 URI uri = new URI(cssUri); 163 if (!uri.isAbsolute()) 164 { 165 uriToResolve = "cocoon:/" + uriToResolve; 166 } 167 168 _requestAttributesHelper.removeRequestAttributes(); 169 170 return _sourceResolver.resolveURI(uriToResolve, null, resolveParameters); 171 } 172 catch (Exception e) 173 { 174 throw new IllegalArgumentException("Unable to resolve the dependencies of specified uri", e); 175 } 176 } 177 178 private List<UriData> _getCssFileDependencies(String uri, Source cssSource) throws IllegalArgumentException 179 { 180 List<UriData> cssDependencies = new ArrayList<>(); 181 182 String fileContent; 183 try (InputStream is = cssSource.getInputStream()) 184 { 185 fileContent = IOUtils.toString(is, StandardCharsets.UTF_8); 186 } 187 catch (IOException e) 188 { 189 getLogger().error("Unable to retrieve css file dependencies for '" + uri + "'", e); 190 return Collections.EMPTY_LIST; 191 } 192 193 String contextPath = _proxiedContextPathProvider.getContextPath(); 194 195 Matcher urlMatcher = CSSFileHelper.IMPORT_PATTERN.matcher(fileContent); 196 197 while (urlMatcher.find()) 198 { 199 String cssUrl = urlMatcher.group(1); 200 201 Matcher externalMatcher = __EXTERNAL_URL.matcher(cssUrl); 202 203 if (!externalMatcher.find()) 204 { 205 try 206 { 207 cssUrl = StringUtils.removeStart(cssUrl, contextPath); 208 URI cssUri = new URI(cssUrl); 209 if (!cssUri.isAbsolute() && !cssUrl.startsWith("/")) 210 { 211 cssUri = new URI(FilenameUtils.getFullPath(uri) + cssUrl); 212 } 213 cssDependencies.addAll(_getFileDependencies(cssUri.normalize().toString(), null, false, false)); 214 } 215 catch (URISyntaxException e) 216 { 217 // Invalid URI inside a file, but should not be blocking 218 getLogger().warn("Invalid URI in a file, could not calculate dependancies for file : " + uri , e); 219 } 220 } 221 } 222 223 return cssDependencies; 224 } 225}