/* * Copyright 2010 Google Inc. * * 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 com.google.gwt.precompress.linker; import com.google.gwt.core.ext.LinkerContext; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.core.ext.linker.AbstractLinker; import com.google.gwt.core.ext.linker.ArtifactSet; import com.google.gwt.core.ext.linker.ConfigurationProperty; import com.google.gwt.core.ext.linker.EmittedArtifact; import com.google.gwt.core.ext.linker.EmittedArtifact.Visibility; import com.google.gwt.core.ext.linker.LinkerOrder; import com.google.gwt.core.ext.linker.Shardable; import com.google.gwt.core.ext.linker.LinkerOrder.Order; import com.google.gwt.dev.util.collect.HashSet; import com.google.gwt.util.regexfilter.RegexFilter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Set; import java.util.zip.GZIPOutputStream; /** * <p> * A linker that precompresses the public artifacts that it sees. That way, a * web server that uses gzip transfer encoding can use the precompressed files * instead of having to compress them on the fly. * * <p> * To use this linker, add the following to your module definition: * * <pre> * <inherits name="com.google.gwt.precompress.Precompress"/> * </pre> * * <p> * The files to precompress are specified by the configuration property * <code>precompress.path.regexes</code>. By default, the uncompressed artifacts * are left in the artifact set. If the configuration property * <code>precompress.leave.originals</code> is set to <code>false</code>, * however, then the uncompressed version is removed. */ @Shardable @LinkerOrder(Order.POST) public class PrecompressLinker extends AbstractLinker { private static class PrecompressFilter extends RegexFilter { public PrecompressFilter(TreeLogger logger, List<String> regexes) throws UnableToCompleteException { super(logger, regexes); } @Override protected boolean acceptByDefault() { return false; } @Override protected boolean entriesArePositiveByDefault() { return true; } } /** * Buffer size to use when streaming data from artifacts and through * {@link GZIPOutputStream}. */ private static final int BUF_SIZE = 10000; private static final String PROP_LEAVE_ORIGINALS = "precompress.leave.originals"; private static final String PROP_PATH_REGEXES = "precompress.path.regexes"; private static ConfigurationProperty findProperty( TreeLogger logger, Iterable<com.google.gwt.core.ext.linker.ConfigurationProperty> properties, String propName) throws UnableToCompleteException { for (ConfigurationProperty prop : properties) { if (prop.getName().equals(propName)) { return prop; } } logger.log(TreeLogger.ERROR, "Could not find configuration property " + propName); throw new UnableToCompleteException(); } @Override public String getDescription() { return "PrecompressLinker"; } @Override public ArtifactSet link(TreeLogger logger, LinkerContext context, ArtifactSet artifacts, boolean onePermutation) throws UnableToCompleteException { ConfigurationProperty leaveOriginalsProp = findProperty(logger, context.getConfigurationProperties(), PROP_LEAVE_ORIGINALS); boolean leaveOriginals = Boolean.valueOf(leaveOriginalsProp.getValues().get( 0)); PrecompressFilter filter = new PrecompressFilter(logger.branch( TreeLogger.TRACE, "Analyzing the path patterns"), findProperty(logger, context.getConfigurationProperties(), PROP_PATH_REGEXES).getValues()); // Record the list of all paths for later lookup Set<String> allPaths = new HashSet<String>(); for (EmittedArtifact art : artifacts.find(EmittedArtifact.class)) { allPaths.add(art.getPartialPath()); } try { // Buffer for streaming data to be compressed byte[] buf = new byte[BUF_SIZE]; ArtifactSet updated = new ArtifactSet(artifacts); for (EmittedArtifact art : artifacts.find(EmittedArtifact.class)) { if (art.getVisibility() != Visibility.Public) { // only compress things that will be served to the client continue; } if (art.getPartialPath().endsWith(".gz")) { // Already a compressed artifact continue; } if (allPaths.contains(art.getPartialPath() + ".gz")) { // It's already been compressed continue; } if (!filter.isIncluded(logger.branch(TreeLogger.TRACE, "Checking the path patterns"), art.getPartialPath())) { continue; } TreeLogger compressBranch = logger.branch(TreeLogger.TRACE, "Compressing " + art.getPartialPath()); InputStream originalBytes = art.getContents(compressBranch); ByteArrayOutputStream compressedBytes = new ByteArrayOutputStream(); GZIPOutputStream gzip = new GZIPOutputStream(compressedBytes); int originalLength = 0; int n; while ((n = originalBytes.read(buf)) > 0) { originalLength += n; gzip.write(buf, 0, n); } gzip.close(); byte[] compressed = compressedBytes.toByteArray(); if (compressed.length < originalLength) { updated.add(emitBytes(compressBranch, compressed, art.getPartialPath() + ".gz")); if (!leaveOriginals) { updated.remove(art); } } } return updated; } catch (IOException e) { logger.log(TreeLogger.ERROR, "Unexpected exception", e); throw new UnableToCompleteException(); } } }