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) 089 { 090 return _getFileDependencies(uri, data.getOrDefault("media", null), true); 091 } 092 093 private Set<UriData> _getFileDependencies(String cssUri, String media, boolean firstLevel) 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 Long validity = Optional.ofNullable(_dependenciesCache.get(cssUri)) 112 .map(Pair::getRight) 113 .orElse(null); 114 115 if (validity == null || validity != fileLastModified) 116 { 117 // cache is outdated 118 List<String> dependenciesCache = new ArrayList<>(); 119 120 for (UriData cssDependency : _getCssFileDependencies(cssUri, fileSource)) 121 { 122 if (!dependenciesCache.contains(cssDependency.getUri())) 123 { 124 dependenciesCache.add(cssDependency.getUri()); 125 dependencies.add(cssDependency); 126 } 127 } 128 129 _dependenciesCache.put(cssUri, Pair.of(dependenciesCache, fileLastModified)); 130 } 131 else 132 { 133 // cache is up to date 134 Pair<List<String>, Long> dependenciesFromCache = _dependenciesCache.get(cssUri); 135 if (dependenciesFromCache != null) 136 { 137 for (String dependencyCached : dependenciesFromCache.getLeft()) 138 { 139 dependencies.addAll(_getFileDependencies(dependencyCached, null, false)); 140 } 141 } 142 } 143 } 144 finally 145 { 146 _sourceResolver.release(fileSource); 147 } 148 149 return dependencies; 150 } 151 152 private Source _getFileSource(String cssUri, Map<String, Object> resolveParameters) 153 { 154 try 155 { 156 String uriToResolve = cssUri; 157 URI uri = new URI(cssUri); 158 if (!uri.isAbsolute()) 159 { 160 uriToResolve = "cocoon:/" + uriToResolve; 161 } 162 163 _requestAttributesHelper.removeRequestAttributes(); 164 165 return _sourceResolver.resolveURI(uriToResolve, null, resolveParameters); 166 } 167 catch (Exception e) 168 { 169 throw new IllegalArgumentException("Unable to resolve the dependencies of specified uri", e); 170 } 171 } 172 173 private List<UriData> _getCssFileDependencies(String uri, Source cssSource) throws IllegalArgumentException 174 { 175 List<UriData> cssDependencies = new ArrayList<>(); 176 177 String fileContent; 178 try (InputStream is = cssSource.getInputStream()) 179 { 180 fileContent = IOUtils.toString(is, StandardCharsets.UTF_8); 181 } 182 catch (IOException e) 183 { 184 getLogger().error("Unable to retrieve css file dependencies for '" + uri + "'", e); 185 return Collections.EMPTY_LIST; 186 } 187 188 String contextPath = _proxiedContextPathProvider.getContextPath(); 189 190 Matcher urlMatcher = CSSFileHelper.IMPORT_PATTERN.matcher(fileContent); 191 192 while (urlMatcher.find()) 193 { 194 String cssUrl = urlMatcher.group(1); 195 196 Matcher externalMatcher = __EXTERNAL_URL.matcher(cssUrl); 197 198 if (!externalMatcher.find()) 199 { 200 try 201 { 202 cssUrl = StringUtils.removeStart(cssUrl, contextPath); 203 URI cssUri = new URI(cssUrl); 204 if (!cssUri.isAbsolute() && !cssUrl.startsWith("/")) 205 { 206 cssUri = new URI(FilenameUtils.getFullPath(uri) + cssUrl); 207 } 208 cssDependencies.addAll(_getFileDependencies(cssUri.normalize().toString(), null, false)); 209 } 210 catch (URISyntaxException e) 211 { 212 // Invalid URI inside a file, but should not be blocking 213 getLogger().warn("Invalid URI in a file, could not calculate dependancies for file : " + uri , e); 214 } 215 } 216 } 217 218 return cssDependencies; 219 } 220 221}