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.core.minimize.css.sass;
017
018import java.io.IOException;
019import java.net.URI;
020import java.net.URISyntaxException;
021import java.net.URL;
022import java.nio.charset.StandardCharsets;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Optional;
028
029import org.apache.commons.io.FilenameUtils;
030import org.apache.commons.io.IOUtils;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.excalibur.source.Source;
033import org.apache.excalibur.source.SourceNotFoundException;
034import org.apache.excalibur.source.SourceResolver;
035import org.apache.excalibur.source.TraversableSource;
036
037import org.ametys.core.cocoon.source.ResourceSource;
038import org.ametys.plugins.core.ui.resources.css.CSSFileHelper;
039import org.ametys.plugins.core.ui.resources.css.JSASSResourceURIExtensionPoint;
040
041import io.bit3.jsass.importer.Import;
042import io.bit3.jsass.importer.Importer;
043
044/**
045 * Sass Importer which can resolve Ametys resources
046 */
047public class AmetysScssImporter implements Importer 
048{
049    private SourceResolver _resolver;
050    private JSASSResourceURIExtensionPoint _jsassResourceURIExtensionPoint;
051    private String _internalContextPath;
052    private String _externalContextPath;
053
054    /**
055     * Default constructor for the Ametys SassImporter.
056     * @param internalContextPath The internal context path
057     * @param externalContextPath The external context path
058     * @param resolver The source resolver
059     * @param jsassResourceURIExtensionPoint The JSASS resourceUri extension point 
060     */
061    public AmetysScssImporter(String internalContextPath, String externalContextPath, SourceResolver resolver, JSASSResourceURIExtensionPoint jsassResourceURIExtensionPoint)
062    {
063        _internalContextPath = internalContextPath;
064        _externalContextPath = externalContextPath;
065        _resolver = resolver;
066        _jsassResourceURIExtensionPoint = jsassResourceURIExtensionPoint;
067    }
068    
069    public Collection<Import> apply(String url, Import previous)
070    {
071        String previousUri = Optional.of(previous)
072                                     .map(Import::getAbsoluteUri)
073                                     .map(URI::toString)
074                                     .filter(uri -> !uri.endsWith(".css"))
075                                     .orElse(null);
076        
077        if (previousUri == null || url.endsWith(".css") || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//"))
078        {
079            // returning null keeps the original @import mention
080            return null;
081        }
082        
083        List<Import> list = new LinkedList<>();
084        
085        try
086        {
087            boolean isUrlAbsolute = new URI(url).isAbsolute();
088            String currentUri = isUrlAbsolute || url.startsWith("/") ? url : StringUtils.removeStart("/" + new URI(FilenameUtils.getFullPath(previousUri) + url).normalize(), _externalContextPath);
089            
090            Source importSource = _getImportSource(currentUri, _resolver);
091            String importSourcePath = StringUtils.substringBefore(importSource.getURI(), "?");
092
093            if (importSourcePath.endsWith(".scss") 
094                    || importSourcePath.endsWith(".sass") 
095                    || importSourcePath.endsWith(".css") && !currentUri.endsWith(".css")) // An url ending at the origin by .css should stay an import at the end (while a .css added automatically should be resolved)
096            {
097                String importText = IOUtils.toString(importSource.getInputStream(), StandardCharsets.UTF_8);
098                
099                if (importSourcePath.endsWith(".css"))
100                {
101                    String contextualizedUri = isUrlAbsolute ? _jsassResourceURIExtensionPoint.resolve(url) : url;
102                    importText = CSSFileHelper.replaceRelativeUri(importText, contextualizedUri, _jsassResourceURIExtensionPoint, _internalContextPath, _externalContextPath);
103                    currentUri = StringUtils.appendIfMissing(currentUri, ".css");
104                }
105                
106                String uri = StringUtils.removeStart(StringUtils.prependIfMissing(_jsassResourceURIExtensionPoint.resolve(currentUri), _externalContextPath), "/");
107                list.add(new Import(uri, StringUtils.appendIfMissing(uri, "." + FilenameUtils.getExtension(importSourcePath)), importText));
108            }
109            else
110            {
111                // returning null keeps the original @import mention
112                return null;
113            }
114        }
115        catch (URISyntaxException | IOException e) 
116        {
117            throw new RuntimeException(e);
118        }
119
120        return list;
121    }
122    
123    private Source _getImportSource(String currentPath, SourceResolver resolver) throws URISyntaxException, IOException
124    {
125        List<String> pathMatching = new ArrayList<>();
126        pathMatching.add(currentPath);
127        
128        // extension is optional
129        pathMatching.add(currentPath + ".scss");
130        pathMatching.add(currentPath + ".sass");
131        pathMatching.add(currentPath + ".css");
132        
133        // add an underscore prefix to the file name for sass partial imports
134        String name = FilenameUtils.getName(currentPath);
135        String partialPath = currentPath.substring(0, currentPath.length() - name.length()) + "_" + name;
136        pathMatching.add(partialPath);
137        pathMatching.add(partialPath + ".scss");
138        pathMatching.add(partialPath + ".sass");
139        pathMatching.add(partialPath + ".css");
140        
141        for (String uri : pathMatching)
142        {
143            try
144            {
145                Source importSource = resolver.resolveURI(_jsassResourceURIExtensionPoint.resolvePath(uri));
146                if (_isSourceValid(importSource))
147                {
148                    return importSource;
149                }
150            }
151            catch (SourceNotFoundException e)
152            {
153                // source does not exists. Do nothing
154            }
155        }
156        
157        throw new URISyntaxException(currentPath, "Unable to resolve SASS import, no matching source found");
158    }
159    
160    private boolean _isSourceValid(Source source)
161    {
162        if (!source.exists())
163        {
164            return false;
165        }
166        
167        // ignore folders
168        if (source instanceof TraversableSource && ((TraversableSource) source).isCollection())
169        {
170            return false;
171        }
172        
173        // Folders inside a zip entry (such as a jar file) are not traversable
174        if (source instanceof ResourceSource)
175        {
176            URL location = ((ResourceSource) source).getLocation();
177            if (location != null && StringUtils.endsWith(location.toString(), "/"))
178            {
179                return false;
180            }
181        }
182        
183        return true;
184    }
185}
186