package fr.openwide.core.wicket.more.css.scss.service;
import io.bit3.jsass.CompilationException;
import io.bit3.jsass.Compiler;
import io.bit3.jsass.Options;
import io.bit3.jsass.Output;
import io.bit3.jsass.OutputStyle;
import io.bit3.jsass.context.Context;
import io.bit3.jsass.context.StringContext;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.wicket.util.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.io.ClassPathResource;
import com.google.common.collect.Maps;
import fr.openwide.core.jpa.exception.ServiceException;
import fr.openwide.core.wicket.more.config.spring.WicketMoreServiceConfig;
import fr.openwide.core.wicket.more.css.scss.model.ScssStylesheetInformation;
/**
* @see WicketMoreServiceConfig
*/
// TODO SCSS : @Service("scssService")
public class ScssServiceImpl implements IScssService {
private static final Logger LOGGER = LoggerFactory.getLogger(ScssServiceImpl.class);
private static final Pattern SCOPE_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]*$");
private static final Map<String, Class<?>> SCOPES = Maps.newHashMapWithExpectedSize(3);
@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 = "scssService.compiledStylesheets",
key = "T(fr.openwide.core.wicket.more.css.scss.service.ScssServiceImpl).getCacheKey(#scope, #path)",
beforeInvocation = true,
condition= "#checkCacheEntryUpToDate && !(caches.?[name=='scssService.compiledStylesheets'][0]?.get(T(fr.openwide.core.wicket.more.css.scss.service.ScssServiceImpl).getCacheKey(#scope, #path))?.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 = "scssService.compiledStylesheets",
key = "T(fr.openwide.core.wicket.more.css.scss.service.ScssServiceImpl).getCacheKey(#scope, #path)")
public ScssStylesheetInformation getCompiledStylesheet(Class<?> scope, String path, boolean checkCacheEntryUpToDate)
throws ServiceException {
String scssPath = getFullPath(scope, path);
try {
JSassScopeAwareImporter importer = new JSassScopeAwareImporter(SCOPES);
importer.addSourceUri(scssPath);
Compiler compiler = new Compiler();
Options options = new Options();
options.setOutputStyle(OutputStyle.EXPANDED);
options.setIndent("\t");
options.getImporters().add(importer);
ClassPathResource scssCpr = new ClassPathResource(scssPath);
Context fileContext = new StringContext(IOUtils.toString(scssCpr.getInputStream()), new URI("classpath", "/" + scssPath, null), null, options);
Output output = compiler.compile(fileContext);
// Write result
ScssStylesheetInformation compiledStylesheet = new ScssStylesheetInformation(scssPath, output.getCss());
for (String sourceUri : importer.getSourceUris()) {
ClassPathResource cpr = new ClassPathResource(sourceUri);
compiledStylesheet.addImportedStylesheet(new ScssStylesheetInformation(sourceUri, cpr.lastModified()));
}
return compiledStylesheet;
} catch (RuntimeException | IOException | URISyntaxException | CompilationException e) {
throw new ServiceException(String.format("Error compiling %1$s", scssPath), e);
}
}
@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(Class<?> scope, String path) {
StringBuilder cacheKeyBuilder = new StringBuilder();
cacheKeyBuilder.append(getFullPath(scope, path));
return cacheKeyBuilder.toString();
}
public static String getFullPath(Class<?> scope, String path) {
StringBuilder fullPath = new StringBuilder();
if (scope != null) {
fullPath.append(scope.getPackage().getName().replace(".", "/")).append("/");
}
fullPath.append(path);
return fullPath.toString();
}
}