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.sass; 017 018import java.util.ArrayList; 019import java.util.Arrays; 020import java.util.HashSet; 021import java.util.LinkedHashSet; 022import java.util.List; 023import java.util.Map; 024import java.util.Objects; 025import java.util.Set; 026import java.util.concurrent.ConcurrentHashMap; 027import java.util.function.Consumer; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.commons.lang3.StringUtils; 034import org.apache.commons.lang3.tuple.Pair; 035import org.apache.excalibur.source.Source; 036import org.apache.excalibur.source.SourceResolver; 037 038import org.ametys.plugins.core.ui.minimize.HashCache.UriData; 039import org.ametys.plugins.core.ui.resources.ResourceDependenciesList; 040import org.ametys.plugins.core.ui.resources.css.sass.SassImportHelper.SassImportInfo; 041import org.ametys.plugins.core.ui.util.RequestAttributesHelper; 042import org.ametys.runtime.plugin.component.AbstractLogEnabled; 043 044/** 045 * Dependencies list for sass files compiled into css 046 * Uncompiled sass files don't depend on anything and are managed by the DefaultResourceDependenciesList 047 */ 048public class SassDependenciesList extends AbstractLogEnabled implements ResourceDependenciesList, Serviceable 049{ 050 /** List of sass extensions */ 051 protected static final String[] __SASS_EXTENSION = new String[] {".scss", ".sass"}; 052 053 /** RequestAttributesHelper */ 054 protected RequestAttributesHelper _requestAttributesHelper; 055 056 /** The source resolver */ 057 protected SourceResolver _sourceResolver; 058 059 /** The Sass import helper */ 060 protected SassImportHelper _sassImportHelper; 061 062 /* Dependencies cache */ 063 private Map<String, Pair<List<String>, Long>> _dependenciesCache = new ConcurrentHashMap<>(); 064 065 @Override 066 public void service(ServiceManager manager) throws ServiceException 067 { 068 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 069 _requestAttributesHelper = (RequestAttributesHelper) manager.lookup(RequestAttributesHelper.ROLE); 070 _sassImportHelper = (SassImportHelper) manager.lookup(SassImportHelper.ROLE); 071 } 072 073 @Override 074 public boolean isSupported(String uri) 075 { 076 if (StringUtils.endsWith(uri, ".css")) 077 { 078 return getSassImportInfo(uri) != null; 079 } 080 081 return false; 082 } 083 084 @Override 085 public int getPriority() 086 { 087 return 200; 088 } 089 090 @Override 091 public Set<UriData> getDependenciesList(String uri, Map<String, String> data) 092 { 093 SassImportInfo fileSource = null; 094 095 try 096 { 097 fileSource = getSassImportInfo(uri); 098 099 if (fileSource != null) 100 { 101 String media = data.getOrDefault("media", null); 102 return getDependenciesList(uri, media, fileSource.getSource(), fileSource.getLastModified(), true); 103 } 104 } 105 finally 106 { 107 if (fileSource != null) 108 { 109 _sourceResolver.release(fileSource.getSource()); 110 } 111 } 112 113 return new HashSet<>(); 114 } 115 116 /** 117 * Get the Sass import informations from the URI 118 * @param uri The URI 119 * @return The information 120 */ 121 protected SassImportInfo getSassImportInfo(String uri) 122 { 123 String uriWithoutExtension = StringUtils.removeEnd(uri, ".css"); 124 return _sassImportHelper.findExistingImportSource(Arrays.stream(__SASS_EXTENSION).map(ext -> uriWithoutExtension + ext).collect(Collectors.toList())); 125 } 126 127 private Set<UriData> getDependenciesList(SassImportInfo sassImportInfo, String media) 128 { 129 return getDependenciesList(sassImportInfo, media, false); 130 } 131 132 private Set<UriData> getDependenciesList(SassImportInfo sassImportInfo, String media, boolean firstLevel) 133 { 134 return getDependenciesList(sassImportInfo.getUri(), media, sassImportInfo.getSource(), sassImportInfo.getLastModified(), firstLevel); 135 } 136 137 private Set<UriData> getDependenciesList(String uri, String media, Source source, long lastModified, boolean firstLevel) 138 { 139 Set<UriData> dependencies = new LinkedHashSet<>(); 140 UriData fileInfos = new UriData(uri, firstLevel); 141 fileInfos.setLastModified(lastModified); 142 fileInfos.setMedia(media); 143 dependencies.add(fileInfos); 144 145 Pair<List<String>, Long> dependencyFromCache = _dependenciesCache.get(uri); 146 147 if (dependencyFromCache == null || dependencyFromCache.getRight() != lastModified) 148 { 149 // cache is outdated 150 List<String> dependenciesCache = new ArrayList<>(); 151 152 Consumer<UriData> addDependencyToCache = cssDependency -> 153 { 154 if (!dependenciesCache.contains(cssDependency.getUri())) 155 { 156 dependenciesCache.add(cssDependency.getUri()); 157 dependencies.add(cssDependency); 158 } 159 }; 160 161 _sassImportHelper.getDependenciesList(source).values() 162 .stream() 163 .map(sassImport -> getDependenciesList(sassImport, media)) 164 .flatMap(Set::stream) 165 .forEach(addDependencyToCache); 166 167 _dependenciesCache.put(uri, Pair.of(dependenciesCache, lastModified)); 168 } 169 else 170 { 171 // cache is up to date, lookup dependencies of each entry from the cache to build the full list 172 dependencyFromCache.getLeft() 173 .stream() 174 .map(uriFromCache -> _sassImportHelper.getImportSource(uriFromCache)) 175 .filter(Objects::nonNull) 176 .map(sassImport -> getDependenciesList(sassImport, media)) 177 .forEach(subDependencies -> dependencies.addAll(subDependencies)); 178 } 179 180 return dependencies; 181 } 182}