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.css.less;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.io.UnsupportedEncodingException;
023import java.net.URI;
024import java.net.URISyntaxException;
025import java.nio.charset.StandardCharsets;
026import java.util.ArrayList;
027import java.util.List;
028import java.util.Map;
029import java.util.regex.Matcher;
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 org.ametys.plugins.core.ui.resources.AbstractCompiledResourceHandler;
040import org.ametys.plugins.core.ui.resources.css.CSSFileHelper;
041
042import com.github.sommeri.less4j.Less4jException;
043import com.github.sommeri.less4j.LessCompiler.CompilationResult;
044import com.github.sommeri.less4j.LessSource;
045import com.github.sommeri.less4j.core.DefaultLessCompiler;
046
047/**
048 * Reader for LESS files, compile them on the fly into CSS files.
049 */
050public class LessResourceHandler extends AbstractCompiledResourceHandler
051{
052    private DefaultLessCompiler _defaultLessCompiler;
053    
054    /**
055     * Constructor with an already resolved {@link Source}.
056     * @param source the source
057     */
058    public LessResourceHandler(Source source)
059    {
060        super(source);
061    }
062    
063    @Override
064    public Source setup(String location, Map objectModel, Parameters par, boolean readForDownload) throws ProcessingException, IOException
065    {
066        Source source = super.setup(location, objectModel, par, readForDownload);
067        
068        _defaultLessCompiler = new DefaultLessCompiler();
069        
070        return source;
071    }
072        
073    @Override
074    public void generate(OutputStream out) throws IOException, ProcessingException
075    {
076        CompilationResult result = null;
077        
078        try (InputStream is = _source.getInputStream())
079        {
080            String lessContent = IOUtils.toString(is, StandardCharsets.UTF_8);
081            AmetysLessSource stringSource = new AmetysLessSource(_resolver, lessContent, new URI(_source.getURI()));
082            result = _defaultLessCompiler.compile(stringSource);
083        }
084        catch (Less4jException e)
085        {
086            throw new ProcessingException("Unable to compile the LESS file : " + _source.getURI(), e);
087        }
088        catch (URISyntaxException e)
089        {
090            throw new ProcessingException("Unable to process LESS File, invalid uri : " + _source.getURI(), e);
091        }
092        
093        String resultString = result == null ? null : result.getCss();
094        IOUtils.write(resultString, out, StandardCharsets.UTF_8);
095    }
096    
097    @Override
098    protected List<String> getDependenciesList(Source inputSource)
099    {
100        List<String> result = new ArrayList<>();
101        
102        try (InputStream is = inputSource.getInputStream())
103        {
104            String content = IOUtils.toString(is, StandardCharsets.UTF_8);
105            
106            Matcher matcher = CSSFileHelper.IMPORT_PATTERN.matcher(content);
107            
108            while (matcher.find())
109            {
110                String cssUrl = matcher.group(1);
111                
112                if (!StringUtils.contains(cssUrl, "http://") && !StringUtils.contains(cssUrl, "https://")) 
113                {
114                    if (!StringUtils.endsWith(cssUrl, ".css") && !StringUtils.endsWith(cssUrl, ".less"))
115                    {
116                        cssUrl += ".less";
117                    }
118                    
119                    result.add(cssUrl);
120                }
121            }
122        }
123        catch (IOException e)
124        {
125            getLogger().warn("Invalid content when listing dependencies for file " + inputSource.getURI(), e);
126        }
127        
128        return result;
129    }
130
131    @Override
132    public String getMimeType()
133    {
134        return "text/css";
135    }
136    
137    /**
138     * LessSource definition for Ametys Resources
139     */
140    private static class AmetysLessSource extends LessSource
141    {
142        private String _lessContent;
143        private String _name;
144        private URI _sourceUri;
145        private SourceResolver _sResolver;
146    
147        /**
148         * Default constructor for Ametys less source
149         * @param sourceResolver The Source Resolver
150         * @param lessContent The content of the less source
151         * @param uri The uri of the less source
152         */
153        public AmetysLessSource(SourceResolver sourceResolver, String lessContent, URI uri)
154        {
155            _sResolver = sourceResolver;
156            _lessContent = lessContent;
157            _sourceUri = uri;
158        }
159    
160        @Override
161        public LessSource relativeSource(String relativePath) throws FileNotFound, CannotReadFile, StringSourceException
162        {
163            Source importSource = null;
164            try
165            {
166                URI relativeSourceUri = new URI(relativePath);
167                if (!relativeSourceUri.isAbsolute())
168                {
169                    relativeSourceUri = new URI(FilenameUtils.getFullPath(_sourceUri.toString()) + relativePath);
170                }
171                
172                importSource = _sResolver.resolveURI(relativeSourceUri.toString());
173    
174                String importText = IOUtils.toString(importSource.getInputStream(), "UTF-8");
175                return new AmetysLessSource(_sResolver, importText, relativeSourceUri);
176            }
177            catch (Exception e)
178            {
179                throw new RuntimeException("Unable to process LESS File : " + _sourceUri + ", invalid import : " + relativePath, e);
180            }
181            finally
182            {
183                _sResolver.release(importSource);
184            }
185        }
186    
187        @Override
188        public String getContent() throws FileNotFound, CannotReadFile
189        {
190            return _lessContent;
191        }
192    
193        @Override
194        public byte[] getBytes() throws FileNotFound, CannotReadFile
195        {
196            try
197            {
198                return _lessContent.getBytes("UTF-8");
199            }
200            catch (UnsupportedEncodingException e)
201            {
202                throw new CannotReadFile();
203            }
204        }
205        
206        @Override
207        public URI getURI()
208        {
209            return _sourceUri;
210        }
211        
212        @Override
213        public String getName()
214        {
215            return _name;
216        }
217    }
218}