/* * Copyright 2012 Jason Miller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jj.css; import static jj.application.AppLocation.*; import static jj.server.ServerLocation.*; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import javax.inject.Inject; import javax.inject.Provider; import jj.logging.LoggedEvent; import org.mozilla.javascript.BaseFunction; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; import jj.http.server.LoadedResource; import jj.http.server.ServableResourceConfiguration; import jj.http.server.resource.StaticResource; import jj.resource.AbstractResource; import jj.resource.NoSuchResourceException; import jj.resource.ResourceNotViableException; import jj.script.Global; import jj.script.RhinoContext; import jj.script.module.ScriptResource; import jj.util.SHA1Helper; import org.slf4j.Logger; /** * <p> * a virtual resource representing either a static css * stylesheet, or a stylesheet that has run through less * processing. * * <p> * The stylesheet represented by this resource will be processed to have * all internal URIs replaced with long-term cacheable URIs. This might * get put into configuration to disable it. * * @author jason * */ @ServableResourceConfiguration( routeContributor = StylesheetResourceRouteContributor.class, processorConfig = StylesheetResourceRouteProcessorConfiguration.class ) public class StylesheetResource extends AbstractResource<Void> implements LoadedResource { static final String LESS_SCRIPT = "less-rhino-1.7.3.js"; private final ByteBuf bytes; private final String sha1; private final Path path; private final long size; private final LessConfiguration lessConfiguration; @Inject StylesheetResource( final Dependencies dependencies, final Provider<RhinoContext> contextProvider, final @Global ScriptableObject global, final CssReferenceVersionProcessor processor, final LessConfiguration lessConfiguration ) { super(dependencies); this.lessConfiguration = lessConfiguration; // is there a static css file? StaticResource css = resourceFinder.loadResource(StaticResource.class, Public, name()); String result; if (css == null) { // for whatever reason, i'm getting obsessed with optimal string processing String lessName = new StringBuilder(name().length() + 1) .append(name()) .replace(name().length() - 3, name().length() - 2, "le") .toString(); // this is just to check for existence of the resource, it will get loaded from // the script execution and hooked into the dependency system then LessResource lessSheet = resourceFinder.loadResource(LessResource.class, Private, lessName); if (lessSheet == null) { throw new NoSuchResourceException(StylesheetResource.class, name()); } path = lessSheet.path(); result = processLessScript(contextProvider, global, lessName); if (result == null) { throw new ResourceNotViableException(path, "could not process " + lessName); } } else { path = css.path(); css.addDependent(this); try { result = new String(Files.readAllBytes(path), css.charset()); } catch (IOException ioe) { throw new ResourceNotViableException(path, ioe); } } result = processor.fixUris(result, this); sha1 = SHA1Helper.keyFor(result); bytes = Unpooled.copiedBuffer(result, charset()); size = bytes.readableBytes(); } private String processLessScript(final Provider<RhinoContext> contextProvider, final ScriptableObject global, String lessName) { publisher.publish(new StartingLessProcessing(lessName)); try (RhinoContext context = contextProvider.get().withoutContinuations()) { // turn on optimizations before loading this! ScriptResource lessScript = resourceFinder.loadResource(ScriptResource.class, Assets, LESS_SCRIPT); assert lessScript != null : "less script not found! build failure!"; lessScript.addDependent(this); ScriptableObject local = context.newObject(global); local.setPrototype(global); local.setParentScope(null); local.defineProperty("readFile", new ReadFileFunction(), ScriptableObject.EMPTY); local.defineProperty("name", new NameFunction(), ScriptableObject.EMPTY); local.defineProperty("lessLog", new LessLogFunction(), ScriptableObject.EMPTY); context.executeScript(lessScript.script(), local); Function runLess = (Function)local.get("runLess", local); Object result = context.callFunction(runLess, local, local, lessName, lessConfiguration); if (result == Scriptable.NOT_FOUND) { return null; } return String.valueOf(result); } finally { NameFunction.name.set(null); publisher.publish(new FinishedLessProcessing(lessName)); } } @Override protected String extension() { return "css"; } @Override public String sha1() { return sha1; } @Override public long size() { return size; } Path path() { return path; } @Override public String contentType() { return settings.contentType(); } @Override public boolean compressible() { return settings.compressible(); } @Override public Charset charset() { return settings.charset(); } @Override public ByteBuf bytes() { return Unpooled.wrappedBuffer(bytes); } @Override public boolean needsReplacing() throws IOException { // always replaced by whatever our resources are return false; } @Override protected boolean removeOnReload() { return false; } // the tiny API to connect the less script to the resource system private class ReadFileFunction extends BaseFunction { private static final long serialVersionUID = -1L; @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { String resourceName = String.valueOf(args[0]); publisher.publish(new LoadingLessResource(resourceName)); LessResource lr = resourceFinder.loadResource(LessResource.class, Public.and(Private), resourceName); if (lr != null) { lr.addDependent(StylesheetResource.this); return lr.contents(); } publisher.publish(new LessResourceNotFound(resourceName)); return ""; } } private static class NameFunction extends BaseFunction { private static final long serialVersionUID = -1L; private static ThreadLocal<String> name = new ThreadLocal<>(); public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { if (args.length == 1) { name.set(String.valueOf(args[0])); } return name.get() == null ? Undefined.instance : name.get(); } } private class LessLogFunction extends BaseFunction { private static final long serialVersionUID = -1L; public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { if (args.length == 1) { publisher.publish(new LessLog(args[0])); } return Undefined.instance; } } @LessLogger private static class LessLog extends LoggedEvent { private final Object arg; LessLog(Object arg) { this.arg = arg; } @Override public void describeTo(Logger logger) { logger.debug("{}", arg); } } }