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