001/* 002 * Copyright 2018 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 */ 016package org.ametys.plugins.core.ui.resources.css.sass; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.net.URI; 021import java.net.URISyntaxException; 022import java.nio.charset.StandardCharsets; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Optional; 028import java.util.regex.Matcher; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.commons.io.FilenameUtils; 035import org.apache.commons.io.IOUtils; 036import org.apache.commons.lang3.StringUtils; 037import org.apache.excalibur.source.Source; 038import org.apache.excalibur.source.SourceResolver; 039import org.apache.excalibur.source.TraversableSource; 040 041import org.ametys.core.resources.ResourceReader; 042import org.ametys.plugins.core.ui.resources.css.CSSFileHelper; 043import org.ametys.plugins.core.ui.resources.css.JSASSResourceURIExtensionPoint; 044import org.ametys.runtime.plugin.component.AbstractLogEnabled; 045 046/** 047 * Helper component for sass contents 048 */ 049public class SassImportHelper extends AbstractLogEnabled implements Serviceable, Component 050{ 051 /** The avalon ROLE */ 052 public static final String ROLE = SassImportHelper.class.getName(); 053 054 /** The source resolver */ 055 protected SourceResolver _resolver; 056 057 /** JsassResourceURIExtensionPoint */ 058 protected JSASSResourceURIExtensionPoint _jsassResourceURIExtensionPoint; 059 060 061 public void service(ServiceManager manager) throws ServiceException 062 { 063 _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 064 _jsassResourceURIExtensionPoint = (JSASSResourceURIExtensionPoint) manager.lookup(JSASSResourceURIExtensionPoint.ROLE); 065 } 066 067 068 /** 069 * Get the list of direct dependencies of a sass source 070 * @param inputSource The sass source 071 * @return The list of dependencies, mapped by URI 072 */ 073 public Map<String, SassImportInfo> getDependenciesList(Source inputSource) 074 { 075 Map<String, SassImportInfo> result = new HashMap<>(); 076 077 try (InputStream is = inputSource.getInputStream()) 078 { 079 String previous = inputSource.getURI(); 080 String content = IOUtils.toString(is, StandardCharsets.UTF_8); 081 082 Matcher matcher = CSSFileHelper.IMPORT_PATTERN.matcher(content); 083 084 while (matcher.find()) 085 { 086 String cssUrl = matcher.group(1); 087 088 if (!StringUtils.contains(cssUrl, "http://") && !StringUtils.contains(cssUrl, "https://")) 089 { 090 URI currentUri = new URI(cssUrl); 091 if (!currentUri.isAbsolute()) 092 { 093 currentUri = new URI(FilenameUtils.getFullPath(previous.toString()) + cssUrl); 094 } 095 096 SassImportInfo importSource = getImportSource(currentUri.toString()); 097 if (importSource != null) 098 { 099 result.put(importSource.getUri(), importSource); 100 } 101 } 102 } 103 } 104 catch (IOException | URISyntaxException e) 105 { 106 getLogger().warn("Invalid " + inputSource.getURI(), e); 107 } 108 109 return result; 110 } 111 112 113 /** 114 * Get the Sass source from the current Uri 115 * @param currentUri The URI 116 * @return The real URI, the Sass source and its last modified 117 */ 118 public SassImportInfo getImportSource(String currentUri) 119 { 120 List<String> uriMatching = new ArrayList<>(); 121 uriMatching.add(currentUri); 122 123 // extension is optional 124 uriMatching.add(currentUri + ".scss"); 125 uriMatching.add(currentUri + ".sass"); 126 uriMatching.add(currentUri + ".css"); 127 128 // add an underscore prefix to the file name for sass partial imports 129 String name = FilenameUtils.getName(currentUri); 130 String partialUri = currentUri.substring(0, currentUri.length() - name.length()) + "_" + name; 131 uriMatching.add(partialUri); 132 uriMatching.add(partialUri + ".scss"); 133 uriMatching.add(partialUri + ".sass"); 134 uriMatching.add(partialUri + ".css"); 135 136 return findExistingImportSource(uriMatching); 137 } 138 139 /** 140 * Find the first existing import source from a list of URIs to try 141 * @param uriMatching The list of URIs to try 142 * @return the first matching source, or null if no source matches 143 */ 144 public SassImportInfo findExistingImportSource(List<String> uriMatching) 145 { 146 for (String uri : uriMatching) 147 { 148 try 149 { 150 Map<String, Object> resolveParameters = new HashMap<>(); 151 String absoluteUri = new URI(uri).isAbsolute() ? uri : _jsassResourceURIExtensionPoint.resolvePath(uri); 152 Source importSource = _resolver.resolveURI(absoluteUri, null, resolveParameters); 153 if (importSource.exists() && !(importSource instanceof TraversableSource && ((TraversableSource) importSource).isCollection())) 154 { 155 SassImportInfo sassImportInfo = new SassImportInfo(uri, importSource); 156 sassImportInfo.setLastModified(resolveParameters.containsKey(ResourceReader.LAST_MODIFIED) ? (long) resolveParameters.get("lastModified") : importSource.getLastModified()); 157 return sassImportInfo; 158 } 159 } 160 catch (IOException | URISyntaxException e) 161 { 162 // source does not exists. Do nothing 163 } 164 } 165 166 return null; 167 } 168 169 /** 170 * Informations about sass import, such as real URI and real last modified 171 */ 172 public static class SassImportInfo 173 { 174 private String _uri; 175 private Source _source; 176 private Long _lastModified; 177 178 /** 179 * Default constructor for the sass import info 180 * @param uri The import real URI 181 * @param source The import source 182 */ 183 SassImportInfo(String uri, Source source) 184 { 185 _uri = uri; 186 _source = source; 187 } 188 189 /** 190 * Get the real uri of the import, with the sass extension 191 * @return The real uri 192 */ 193 public String getUri() 194 { 195 return _uri; 196 } 197 198 /** 199 * Get the source of the import 200 * @return The source 201 */ 202 public Source getSource() 203 { 204 return _source; 205 } 206 207 /** 208 * Set the last modified time of the import 209 * @param lastModified The last modified 210 */ 211 public void setLastModified(Long lastModified) 212 { 213 _lastModified = lastModified; 214 } 215 216 /** 217 * Get the last modified of the import 218 * @return The last modified 219 */ 220 public long getLastModified() 221 { 222 return Optional.ofNullable(_lastModified).orElse(_source.getLastModified()); 223 } 224 } 225 226}