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.IOException;
020import java.io.InputStream;
021import java.io.UnsupportedEncodingException;
022import java.net.MalformedURLException;
023import java.net.URI;
024import java.net.URISyntaxException;
025import java.nio.charset.StandardCharsets;
026import java.util.ArrayList;
027import java.util.List;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import org.apache.avalon.framework.parameters.Parameters;
032import org.apache.cocoon.ProcessingException;
033import org.apache.commons.io.FilenameUtils;
034import org.apache.commons.io.IOUtils;
035import org.apache.commons.lang3.StringUtils;
036import org.apache.excalibur.source.Source;
037import org.apache.excalibur.source.SourceResolver;
038
039import com.github.sommeri.less4j.Less4jException;
040import com.github.sommeri.less4j.LessCompiler.CompilationResult;
041import com.github.sommeri.less4j.LessSource;
042import com.github.sommeri.less4j.core.DefaultLessCompiler;
043
044/**
045 * Reader for LESS files, compile them on the fly into CSS files.
046 */
047public class LessResourceHandler extends AbstractCompiledResourceHandler
048{
049    private static final Pattern __IMPORT_PATTERN = Pattern.compile("^@import\\b\\s*(?:(?:url)?\\(?\\s*[\"']?)([^)\"']*)[\"']?\\)?\\s*;?$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
050    private static final String[] __LESS_EXTENSION = new String[] {".less"};
051    
052    private DefaultLessCompiler _defaultLessCompiler;
053    
054    @Override
055    public int getPriority()
056    {
057        return MIN_PRIORITY + 1000;
058    }
059    
060    @Override
061    public boolean isSupported(String source)
062    {
063        if (!super.isSupported(source))
064        {
065            return false;
066        }
067        
068        String lcSource = StringUtils.lowerCase(source);
069        if (lcSource.endsWith(".css"))
070        {
071            String sourceWithExt = StringUtils.substringBeforeLast(source, ".css");
072            for (String ext : __LESS_EXTENSION)
073            {
074                Source src = null;
075                try
076                {
077                    src = _resolver.resolveURI(sourceWithExt + ext);
078                    if (src.exists())
079                    {
080                        return true;
081                    }
082                }
083                catch (IOException e)
084                {
085                    // Nothing
086                }
087                finally 
088                {
089                    _resolver.release(src);
090                }
091                
092            }
093            
094            // No .less file found
095            return false;
096        }
097        else
098        {
099            return true;
100        }
101        
102    }
103    
104    @Override
105    protected Source getCompiledSource(String location) throws MalformedURLException, IOException
106    {
107        if (location.toLowerCase().endsWith(".css"))
108        {
109            String sourceWithExt = StringUtils.substringBeforeLast(location, ".css");
110            for (String ext : __LESS_EXTENSION)
111            {
112                Source src = _resolver.resolveURI(sourceWithExt + ext);
113                if (src.exists())
114                {
115                    return src;
116                }
117            }
118        }
119        
120        return _resolver.resolveURI(location);
121    }
122    
123    @Override
124    public Source setup(String location, Parameters par) throws ProcessingException, IOException
125    {
126        Source source = super.setup(location, par);
127        
128        _defaultLessCompiler = new DefaultLessCompiler();
129        
130        return source;
131    }
132        
133    @Override
134    public String compileResource(Source source) throws IOException, ProcessingException
135    {
136        CompilationResult result = null;
137        
138        try (InputStream is = source.getInputStream())
139        {
140            String lessContent = IOUtils.toString(is, StandardCharsets.UTF_8);
141            AmetysLessSource stringSource = new AmetysLessSource(_resolver, lessContent, new URI(source.getURI()));
142            result = _defaultLessCompiler.compile(stringSource);
143        }
144        catch (Less4jException e)
145        {
146            throw new ProcessingException("Unable to compile the LESS file : " + source.getURI(), e);
147        }
148        catch (URISyntaxException e)
149        {
150            throw new ProcessingException("Unable to process LESS File, invalid uri : " + source.getURI(), e);
151        }
152        
153        return result == null ? null : result.getCss();
154    }
155    
156    @Override
157    protected List<String> getDependenciesList(Source inputSource)
158    {
159        List<String> result = new ArrayList<>();
160        
161        try (InputStream is = inputSource.getInputStream())
162        {
163            String content = IOUtils.toString(is, StandardCharsets.UTF_8);
164            
165            Matcher matcher = __IMPORT_PATTERN.matcher(content);
166            
167            while (matcher.find())
168            {
169                String cssUrl = matcher.group(1);
170                
171                if (!StringUtils.contains(cssUrl, "http://") && !StringUtils.contains(cssUrl, "https://")) 
172                {
173                    if (!StringUtils.endsWith(cssUrl, ".css") && !StringUtils.endsWith(cssUrl, ".less"))
174                    {
175                        cssUrl += ".less";
176                    }
177                    
178                    result.add(cssUrl);
179                }
180            }
181        }
182        catch (IOException e)
183        {
184            getLogger().warn("Invalid content when listing dependencies for file " + inputSource.getURI(), e);
185        }
186        
187        return result;
188    }
189
190
191    /**
192     * LessSource definition for Ametys Resources
193     */
194    private static class AmetysLessSource extends LessSource
195    {
196        private String _lessContent;
197        private String _name;
198        private URI _sourceUri;
199        private SourceResolver _sResolver;
200    
201        /**
202         * Default constructor for Ametys less source
203         * @param sourceResolver The Source Resolver
204         * @param lessContent The content of the less source
205         * @param uri The uri of the less source
206         */
207        public AmetysLessSource(SourceResolver sourceResolver, String lessContent, URI uri)
208        {
209            _sResolver = sourceResolver;
210            _lessContent = lessContent;
211            _sourceUri = uri;
212        }
213    
214        @Override
215        public LessSource relativeSource(String relativePath) throws FileNotFound, CannotReadFile, StringSourceException
216        {
217            try
218            {
219                URI relativeSourceUri = new URI(relativePath);
220                if (!relativeSourceUri.isAbsolute())
221                {
222                    relativeSourceUri = new URI(FilenameUtils.getFullPath(_sourceUri.toString()) + relativePath);
223                }
224                
225                // SASS files can be .sass or .scss
226                Source importSource = null;
227                
228                importSource = _sResolver.resolveURI(relativeSourceUri.toString());
229    
230                String importText = IOUtils.toString(importSource.getInputStream(), "UTF-8");
231                return new AmetysLessSource(_sResolver, importText, relativeSourceUri);
232            }
233            catch (Exception e)
234            {
235                throw new RuntimeException("Unable to process LESS File : " + _sourceUri + ", invalid import : " + relativePath, e);
236            }
237        }
238    
239        @Override
240        public String getContent() throws FileNotFound, CannotReadFile
241        {
242            return _lessContent;
243        }
244    
245        @Override
246        public byte[] getBytes() throws FileNotFound, CannotReadFile
247        {
248            try
249            {
250                return _lessContent.getBytes("UTF-8");
251            }
252            catch (UnsupportedEncodingException e)
253            {
254                throw new CannotReadFile();
255            }
256        }
257        
258        @Override
259        public URI getURI()
260        {
261            return _sourceUri;
262        }
263        
264        @Override
265        public String getName()
266        {
267            return _name;
268        }
269    
270    }
271
272    @Override
273    public String getMimeType(Source source, Parameters par)
274    {
275        return "text/css";
276    }
277
278}