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}