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}