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.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.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Optional;
028import java.util.regex.Matcher;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.avalon.framework.service.Serviceable;
034import org.apache.commons.io.FilenameUtils;
035import org.apache.commons.io.IOUtils;
036import org.apache.commons.lang3.StringUtils;
037import org.apache.excalibur.source.Source;
038import org.apache.excalibur.source.SourceResolver;
039import org.apache.excalibur.source.TraversableSource;
040
041import org.ametys.core.resources.ResourceReader;
042import org.ametys.plugins.core.ui.resources.css.CSSFileHelper;
043import org.ametys.plugins.core.ui.resources.css.JSASSResourceURIExtensionPoint;
044import org.ametys.runtime.plugin.component.AbstractLogEnabled;
045
046/**
047 * Helper component for sass contents
048 */
049public class SassImportHelper extends AbstractLogEnabled implements Serviceable, Component
050{
051    /** The avalon ROLE */
052    public static final String ROLE = SassImportHelper.class.getName();
053    
054    /** The source resolver */
055    protected SourceResolver _resolver;
056
057    /** JsassResourceURIExtensionPoint */
058    protected JSASSResourceURIExtensionPoint _jsassResourceURIExtensionPoint;
059
060
061    public void service(ServiceManager manager) throws ServiceException
062    {
063        _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
064        _jsassResourceURIExtensionPoint = (JSASSResourceURIExtensionPoint) manager.lookup(JSASSResourceURIExtensionPoint.ROLE);
065    }
066    
067    
068    /**
069     * Get the list of direct dependencies of a sass source
070     * @param inputSource The sass source
071     * @return The list of dependencies, mapped by URI
072     */
073    public Map<String, SassImportInfo> getDependenciesList(Source inputSource)
074    {
075        Map<String, SassImportInfo> result = new HashMap<>();
076        
077        try (InputStream is = inputSource.getInputStream())
078        {
079            String previous = inputSource.getURI();
080            String content = IOUtils.toString(is, StandardCharsets.UTF_8);
081            
082            Matcher matcher = CSSFileHelper.IMPORT_PATTERN.matcher(content);
083            
084            while (matcher.find())
085            {
086                String cssUrl = matcher.group(1);
087                
088                if (!StringUtils.contains(cssUrl, "http://") && !StringUtils.contains(cssUrl, "https://")) 
089                {
090                    URI currentUri = new URI(cssUrl);
091                    if (!currentUri.isAbsolute())
092                    {
093                        currentUri = new URI(FilenameUtils.getFullPath(previous.toString()) + cssUrl);
094                    }
095            
096                    SassImportInfo importSource = getImportSource(currentUri.toString());
097                    if (importSource != null)
098                    {
099                        result.put(importSource.getUri(), importSource);
100                    }
101                }
102            }
103        }
104        catch (IOException | URISyntaxException e)
105        {
106            getLogger().warn("Invalid " + inputSource.getURI(), e);
107        }
108        
109        return result;
110    }
111    
112
113    /**
114     * Get the Sass source from the current Uri
115     * @param currentUri The URI
116     * @return The real URI, the Sass source and its last modified
117     */
118    public SassImportInfo getImportSource(String currentUri)
119    {
120        List<String> uriMatching = new ArrayList<>();
121        uriMatching.add(currentUri);
122        
123        // extension is optional
124        uriMatching.add(currentUri + ".scss");
125        uriMatching.add(currentUri + ".sass");
126        uriMatching.add(currentUri + ".css");
127        
128        // add an underscore prefix to the file name for sass partial imports
129        String name = FilenameUtils.getName(currentUri);
130        String partialUri = currentUri.substring(0, currentUri.length() - name.length()) + "_" + name;
131        uriMatching.add(partialUri);
132        uriMatching.add(partialUri + ".scss");
133        uriMatching.add(partialUri + ".sass");
134        uriMatching.add(partialUri + ".css");
135        
136        return findExistingImportSource(uriMatching);
137    }
138
139    /**
140     * Find the first existing import source from a list of URIs to try
141     * @param uriMatching The list of URIs to try
142     * @return the first matching source, or null if no source matches
143     */
144    public SassImportInfo findExistingImportSource(List<String> uriMatching)
145    {
146        for (String uri : uriMatching)
147        {
148            try
149            {
150                Map<String, Object> resolveParameters = new HashMap<>();
151                String absoluteUri = new URI(uri).isAbsolute() ? uri : _jsassResourceURIExtensionPoint.resolvePath(uri);
152                Source importSource = _resolver.resolveURI(absoluteUri, null, resolveParameters);
153                if (importSource.exists() && !(importSource instanceof TraversableSource && ((TraversableSource) importSource).isCollection()))
154                {
155                    SassImportInfo sassImportInfo = new SassImportInfo(uri, importSource);
156                    sassImportInfo.setLastModified(resolveParameters.containsKey(ResourceReader.LAST_MODIFIED) ? (long) resolveParameters.get("lastModified") : importSource.getLastModified());
157                    return sassImportInfo;
158                }
159            }
160            catch (IOException | URISyntaxException e)
161            {
162                // source does not exists. Do nothing
163            }
164        }
165        
166        return null;
167    }
168    
169    /**
170     * Informations about sass import, such as real URI and real last modified
171     */
172    public static class SassImportInfo
173    {
174        private String _uri;
175        private Source _source;
176        private Long _lastModified;
177        
178        /**
179         * Default constructor for the sass import info
180         * @param uri The import real URI
181         * @param source The import source
182         */
183        SassImportInfo(String uri, Source source)
184        {
185            _uri = uri;
186            _source = source;
187        }
188        
189        /**
190         * Get the real uri of the import, with the sass extension
191         * @return The real uri
192         */
193        public String getUri()
194        {
195            return _uri;
196        }
197        
198        /**
199         * Get the source of the import
200         * @return The source
201         */
202        public Source getSource()
203        {
204            return _source;
205        }
206        
207        /**
208         * Set the last modified time of the import
209         * @param lastModified The last modified
210         */
211        public void setLastModified(Long lastModified)
212        {
213            _lastModified = lastModified;
214        }
215        
216        /**
217         * Get the last modified of the import
218         * @return The last modified
219         */
220        public long getLastModified()
221        {
222            return Optional.ofNullable(_lastModified).orElse(_source.getLastModified());
223        }
224    }
225
226}