package fr.openwide.core.wicket.more.css.lesscss.service; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.io.FilenameUtils; import org.apache.wicket.util.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import com.github.sommeri.less4j.Less4jException; import com.github.sommeri.less4j.LessCompiler.CompilationResult; import com.github.sommeri.less4j.LessCompiler.Configuration; import com.github.sommeri.less4j.LessCompiler.Problem; import com.github.sommeri.less4j.core.ThreadUnsafeLessCompiler; import com.google.common.collect.Maps; import fr.openwide.core.jpa.exception.ServiceException; import fr.openwide.core.spring.property.service.IPropertyService; import fr.openwide.core.spring.util.StringUtils; import fr.openwide.core.wicket.more.config.spring.WicketMoreServiceConfig; import fr.openwide.core.wicket.more.css.lesscss.model.LessCssStylesheetInformation; /** * @see WicketMoreServiceConfig */ @Service("lessCssService") public class LessCssServiceImpl implements ILessCssService { private static final Logger LOGGER = LoggerFactory.getLogger(LessCssServiceImpl.class); private static final Pattern LESSCSS_IMPORT_PATTERN = Pattern.compile("^\\p{Blank}*@import\\p{Blank}+\"([^\"]+)\"\\p{Blank}*;", Pattern.MULTILINE); private static final Pattern LESSCSS_IMPORT_SCOPE_PATTERN = Pattern.compile("^@\\{scope-([a-zA-Z0-9_-]*)\\}(.*)$"); private static final Pattern SCOPE_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]*$"); private static final Map<String, Class<?>> SCOPES = Maps.newHashMapWithExpectedSize(10); /** * required = false pour les tests unitaires */ @Autowired(required = false) private IPropertyService propertyService; @Override // If checkCacheInvalidation is true and, before invocation, a cached value exists and is not up to date, we evict the cache entry. @CacheEvict(value = "lessCssService.compiledStylesheets", key = "T(fr.openwide.core.wicket.more.css.lesscss.service.LessCssServiceImpl).getCacheKey(#lessInformation)", beforeInvocation = true, condition= "#checkCacheEntryUpToDate && !(caches.?[name=='lessCssService.compiledStylesheets'][0]?.get(T(fr.openwide.core.wicket.more.css.lesscss.service.LessCssServiceImpl).getCacheKey(#lessInformation))?.get()?.isUpToDate() ?: false)" ) // THEN, we check if a cached value exists. If it does, it is returned ; if not, the method is called. @Cacheable(value = "lessCssService.compiledStylesheets", key = "T(fr.openwide.core.wicket.more.css.lesscss.service.LessCssServiceImpl).getCacheKey(#lessInformation)") public LessCssStylesheetInformation getCompiledStylesheet(LessCssStylesheetInformation lessInformation, boolean checkCacheEntryUpToDate) throws ServiceException { prepareRawStylesheet(lessInformation); try { Configuration configuration = new Configuration(); if (propertyService != null && propertyService.isConfigurationTypeDevelopment()) { // on insère inline une source map // -> utile seulement si on a les outils adéquats pour l'exploiter configuration.getSourceMapConfiguration().setInline(true); } else { // on n'insère aucune information sur l'emplacement des fichiers configuration.getSourceMapConfiguration().setLinkSourceMap(false); } CompilationResult compilationResult = new ThreadUnsafeLessCompiler().compile(lessInformation.getSource(), configuration); LessCssStylesheetInformation compiledStylesheet = new LessCssStylesheetInformation( lessInformation, compilationResult.getCss() ); List<Problem> warnings = compilationResult.getWarnings(); if (!CollectionUtils.isEmpty(warnings)) { for (Problem warning : warnings) { LOGGER.warn(formatLess4jProblem(warning)); } } return compiledStylesheet; } catch (Less4jException e) { List<Problem> errors = e.getErrors(); if (!CollectionUtils.isEmpty(errors)) { for (Problem error : errors) { LOGGER.error(formatLess4jProblem(error)); } } throw new ServiceException(String.format("Error compiling %1$s (scope: %2$s)", lessInformation.getName(), lessInformation.getScope()), e); } } private void prepareRawStylesheet(LessCssStylesheetInformation lessSource) throws ServiceException { Matcher matcher = LESSCSS_IMPORT_PATTERN.matcher(lessSource.getSource()); ClassPathResource importedResource; while (matcher.find()) { Class<?> scope; String importedResourceFilename; String importUrl = matcher.group(1); Matcher scopeMatcher = LESSCSS_IMPORT_SCOPE_PATTERN.matcher(importUrl); if (scopeMatcher.matches()) { Class<?> referencedScope = SCOPES.get(scopeMatcher.group(1)); if (referencedScope != null) { scope = referencedScope; } else { throw new IllegalStateException(String.format("Scope %1$s is not supported", scopeMatcher.group(1))); } importedResourceFilename = scopeMatcher.group(2); } else { // Defaults to importing file's scope scope = lessSource.getScope(); importedResourceFilename = getRelativeToScopePath(lessSource.getName(), matcher.group(1)); } InputStream inputStream = null; try { importedResource = new ClassPathResource(importedResourceFilename, scope); inputStream = importedResource.getURL().openStream(); LessCssStylesheetInformation importedStylesheet = new LessCssStylesheetInformation(scope, importedResourceFilename, IOUtils.toString(inputStream), importedResource.lastModified()); prepareRawStylesheet(importedStylesheet); lessSource.addImportedStylesheet(importedStylesheet); lessSource.setSource(StringUtils.replace(lessSource.getSource(), matcher.group(), importedStylesheet.getSource())); } catch (RuntimeException | IOException e) { throw new ServiceException(String.format("Error reading lesscss source for %1$s in %2$s (scope: %3$s)", importedResourceFilename, lessSource.getName(), scope), e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { LOGGER.error(String.format("Error closing the resource stream for: %1$s", importedResourceFilename)); } } } } } private String getRelativeToScopePath(String sourceFile, String importFilename) { String contextPath = FilenameUtils.getFullPath(sourceFile); String relativeToScopeFilename; if (StringUtils.hasLength(contextPath)) { relativeToScopeFilename = FilenameUtils.concat(contextPath, importFilename); } else { relativeToScopeFilename = importFilename; } return relativeToScopeFilename; } @Override public void registerImportScope(String scopeName, Class<?> scope) { if (SCOPES.containsKey(scopeName)) { LOGGER.warn(String.format("Scope %1$s already registered: ignored", scopeName)); return; } Matcher matcher = SCOPE_NAME_PATTERN.matcher(scopeName); if (!matcher.matches()) { LOGGER.error(String.format("Scope name %1$s invalid (%2$s): ignored", scopeName, SCOPE_NAME_PATTERN.toString())); return; } SCOPES.put(scopeName, scope); } public static String getCacheKey(LessCssStylesheetInformation resourceInformation) { StringBuilder cacheKeyBuilder = new StringBuilder(); cacheKeyBuilder.append(resourceInformation.getScope().getName()); cacheKeyBuilder.append("-"); cacheKeyBuilder.append(resourceInformation.getName()); return cacheKeyBuilder.toString(); } public static String formatLess4jProblem(Problem problem) { StringBuilder sb = new StringBuilder(); sb.append(problem.getMessage()); sb.append(" at line ").append(problem.getLine()); sb.append(" at character ").append(problem.getCharacter()); return sb.toString(); } }