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}