/*
* 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.LinkerOrder.Order;
import com.google.gwt.core.ext.linker.Shardable;
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.Deflater;
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) {
{
def.setLevel(Deflater.BEST_COMPRESSION);
}
};
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();
}
}
}