static { FILE_UTILS = FileUtils.getFileUtils(); AntClassLoader.pathMap = Collections.synchronizedMap(new HashMap<String, String>()); AntClassLoader.subClassToLoad = null; CONSTRUCTOR_ARGS = new Class[] {ClassLoader.class, Project.class, Path.class, Boolean.TYPE}; if (JavaEnvUtils.isAtLeastJavaVersion("1.5")) { try { AntClassLoader.subClassToLoad = Class.forName("org.apache.tools.ant.loader.AntClassLoader5"); } catch (ClassNotFoundException ex) { } } }
/** * Implementations of this abstract class can add compression of a particular type to a given {@link * OutputStream}. They each return a {@link CompressingOutputStream}, which is just a thin wrapper * on top of an {@link OutputStream} that adds the ability to "finish" a stream (see {@link * CompressingOutputStream}). * * <p> * * <p>This class contains implementations based on several popular compression algorithms, such as * gzip. For example, the gzip implementation can decorate an {@link OutputStream} using an instance * of {@link GZIPOutputStream} and in that way add gzip compression to the stream. * * @author Sean Owen */ abstract class CompressingStreamFactory { /** "No encoding" content type: "identity". */ static final String NO_ENCODING = "identity"; private static final Logger LOG = LoggerFactory.getLogger(CompressingStreamFactory.class); /** Implementation based on {@link GZIPOutputStream} and {@link GZIPInputStream}. */ private static final CompressingStreamFactory GZIP_CSF = new GZIPCompressingStreamFactory(); /** Implementation based on {@link ZipOutputStream} and {@link ZipInputStream}. */ private static final CompressingStreamFactory ZIP_CSF = new ZipCompressingStreamFactory(); /** Implementation based on {@link DeflaterOutputStream}. */ private static final CompressingStreamFactory DEFLATE_CSF = new DeflateCompressingStreamFactory(); private static final String GZIP_ENCODING = "gzip"; private static final String X_GZIP_ENCODING = "x-gzip"; private static final String DEFLATE_ENCODING = "deflate"; private static final String COMPRESS_ENCODING = "compress"; private static final String X_COMPRESS_ENCODING = "x-compress"; static final List<String> ALL_COMPRESSION_ENCODINGS = Collections.unmodifiableList( Arrays.asList( GZIP_ENCODING, DEFLATE_ENCODING, COMPRESS_ENCODING, X_GZIP_ENCODING, X_COMPRESS_ENCODING)); /** "Any encoding" content type: the "*" wildcard. */ private static final String ANY_ENCODING = "*"; /** Ordered list of preferred encodings, from most to least preferred */ private static final List<String> SUPPORTED_ENCODINGS; /** * Cache mapping previously seen "Accept-Encoding" header Strings to an appropriate instance of * {@link CompressingStreamFactory}. */ private static final Map<String, String> BEST_ENCODING_CACHE = Collections.synchronizedMap(new HashMap<String, String>(101)); /** Maps content type String to appropriate implementation of {@link CompressingStreamFactory}. */ private static final Map<String, CompressingStreamFactory> FACTORY_MAP; private static final Pattern COMMA = Pattern.compile(","); static { List<String> temp = new ArrayList<>(6); temp.add(GZIP_ENCODING); temp.add(DEFLATE_ENCODING); temp.add(COMPRESS_ENCODING); temp.add(X_GZIP_ENCODING); temp.add(X_COMPRESS_ENCODING); temp.add(NO_ENCODING); SUPPORTED_ENCODINGS = Collections.unmodifiableList(temp); } static { Map<String, CompressingStreamFactory> temp = new HashMap<>(11); temp.put(GZIP_ENCODING, GZIP_CSF); temp.put(X_GZIP_ENCODING, GZIP_CSF); temp.put(COMPRESS_ENCODING, ZIP_CSF); temp.put(X_COMPRESS_ENCODING, ZIP_CSF); temp.put(DEFLATE_ENCODING, DEFLATE_CSF); FACTORY_MAP = Collections.unmodifiableMap(temp); } private static OutputStream maybeWrapStatsOutputStream( OutputStream outputStream, CompressingFilterContext context, CompressingFilterStats.StatsField field) { assert outputStream != null; OutputStream result; if (context.isStatsEnabled()) { CompressingFilterStats stats = context.getStats(); CompressingFilterStats.OutputStatsCallback callbackOutput = stats.getOutputStatsCallback(field); result = new StatsOutputStream(outputStream, callbackOutput); } else { result = outputStream; } return result; } private static InputStream maybeWrapStatsInputStream( InputStream inputStream, CompressingFilterContext context, CompressingFilterStats.StatsField field) { assert inputStream != null; InputStream result; if (context.isStatsEnabled()) { CompressingFilterStats stats = context.getStats(); CompressingFilterStats.InputStatsCallback callbackInput = stats.getInputStatsCallback(field); result = new StatsInputStream(inputStream, callbackInput); } else { result = inputStream; } return result; } private static boolean isSupportedResponseContentEncoding(String contentEncoding) { return NO_ENCODING.equals(contentEncoding) || FACTORY_MAP.containsKey(contentEncoding); } static boolean isSupportedRequestContentEncoding(String contentEncoding) { return NO_ENCODING.equals(contentEncoding) || FACTORY_MAP.containsKey(contentEncoding); } /** * Returns the instance associated to the given content encoding. * * @param contentEncoding content encoding (e.g. "gzip") * @return instance for content encoding */ static CompressingStreamFactory getFactoryForContentEncoding(String contentEncoding) { assert FACTORY_MAP.containsKey(contentEncoding); return FACTORY_MAP.get(contentEncoding); } /** * Determines best content encoding for the response, based on the request -- in particular, based * on its "Accept-Encoding" header. * * @param httpRequest request * @return best content encoding */ static String getBestContentEncoding(HttpServletRequest httpRequest) { String forcedEncoding = (String) httpRequest.getAttribute(CompressingFilter.FORCE_ENCODING_KEY); String bestEncoding; if (forcedEncoding != null) { bestEncoding = forcedEncoding; } else { String acceptEncodingHeader = httpRequest.getHeader(CompressingHttpServletResponse.ACCEPT_ENCODING_HEADER); if (acceptEncodingHeader == null) { bestEncoding = NO_ENCODING; } else { bestEncoding = BEST_ENCODING_CACHE.get(acceptEncodingHeader); if (bestEncoding == null) { // No cached value; must parse header to determine best encoding // I don't synchronize on bestEncodingCache; it's not worth it to avoid the rare case // where // two thread get in here and both parse the header. It's only a tiny bit of extra work, // and // avoids the synchronization overhead. if (acceptEncodingHeader.indexOf((int) ',') >= 0) { // multiple encodings are accepted bestEncoding = selectBestEncoding(acceptEncodingHeader); } else { // one encoding is accepted bestEncoding = parseBestEncoding(acceptEncodingHeader); } BEST_ENCODING_CACHE.put(acceptEncodingHeader, bestEncoding); } } } // User-specified encoding might not be supported if (!isSupportedResponseContentEncoding(bestEncoding)) { bestEncoding = NO_ENCODING; } return bestEncoding; } private static String parseBestEncoding(String acceptEncodingHeader) { ContentEncodingQ contentEncodingQ = parseContentEncodingQ(acceptEncodingHeader); String contentEncoding = contentEncodingQ.getContentEncoding(); if (contentEncodingQ.getQ() > 0.0) { if (ANY_ENCODING.equals(contentEncoding)) { return SUPPORTED_ENCODINGS.get(0); } else if (SUPPORTED_ENCODINGS.contains(contentEncoding)) { return contentEncoding; } } return NO_ENCODING; } @SuppressWarnings("squid:S1244") private static String selectBestEncoding(String acceptEncodingHeader) { // multiple encodings are accepted; determine best one Collection<String> bestEncodings = new HashSet<>(3); double bestQ = 0.0; Collection<String> unacceptableEncodings = new HashSet<>(3); boolean willAcceptAnything = false; for (String token : COMMA.split(acceptEncodingHeader)) { ContentEncodingQ contentEncodingQ = parseContentEncodingQ(token); String contentEncoding = contentEncodingQ.getContentEncoding(); double q = contentEncodingQ.getQ(); if (ANY_ENCODING.equals(contentEncoding)) { willAcceptAnything = q > 0.0; } else if (SUPPORTED_ENCODINGS.contains(contentEncoding)) { if (q > 0.0) { // This is a header quality comparison. // So it is safe to suppress warning squid:S1244 if (q == bestQ) { bestEncodings.add(contentEncoding); } else if (q > bestQ) { bestQ = q; bestEncodings.clear(); bestEncodings.add(contentEncoding); } } else { unacceptableEncodings.add(contentEncoding); } } } if (bestEncodings.isEmpty()) { // nothing was acceptable to us if (willAcceptAnything) { if (unacceptableEncodings.isEmpty()) { return SUPPORTED_ENCODINGS.get(0); } else { for (String encoding : SUPPORTED_ENCODINGS) { if (!unacceptableEncodings.contains(encoding)) { return encoding; } } } } } else { for (String encoding : SUPPORTED_ENCODINGS) { if (bestEncodings.contains(encoding)) { return encoding; } } } return NO_ENCODING; } private static ContentEncodingQ parseContentEncodingQ(String contentEncodingString) { double q = 1.0; int qvalueStartIndex = contentEncodingString.indexOf((int) ';'); String contentEncoding; if (qvalueStartIndex >= 0) { contentEncoding = contentEncodingString.substring(0, qvalueStartIndex).trim(); String qvalueString = contentEncodingString.substring(qvalueStartIndex + 1).trim(); if (qvalueString.startsWith("q=")) { try { q = Double.parseDouble(qvalueString.substring(2)); } catch (NumberFormatException ignored) { // That's bad -- browser sent an invalid number. All we can do is ignore it, and // pretend that no q value was specified, so that it effectively defaults to 1.0 LOG.trace("Couldn't parse a Double from {}.", qvalueString.substring(2), ignored); } } } else { contentEncoding = contentEncodingString.trim(); } return new ContentEncodingQ(contentEncoding, q); } abstract CompressingOutputStream getCompressingStream( OutputStream servletOutputStream, CompressingFilterContext context) throws IOException; abstract CompressingInputStream getCompressingStream( InputStream servletInputStream, CompressingFilterContext context) throws IOException; private static final class ContentEncodingQ { private final String contentEncoding; private final double q; private ContentEncodingQ(String contentEncoding, double q) { assert contentEncoding != null && contentEncoding.length() > 0; this.contentEncoding = contentEncoding; this.q = q; } String getContentEncoding() { return contentEncoding; } double getQ() { return q; } @Override public String toString() { return contentEncoding + ";q=" + q; } } private static class GZIPCompressingStreamFactory extends CompressingStreamFactory { @Override CompressingOutputStream getCompressingStream( final OutputStream outputStream, final CompressingFilterContext context) throws IOException { return new CompressingOutputStream() { private final DeflaterOutputStream gzipOutputStream = new GZIPOutputStream( CompressingStreamFactory.maybeWrapStatsOutputStream( outputStream, context, CompressingFilterStats.StatsField.RESPONSE_COMPRESSED_BYTES)); private final OutputStream statsOutputStream = CompressingStreamFactory.maybeWrapStatsOutputStream( gzipOutputStream, context, CompressingFilterStats.StatsField.RESPONSE_INPUT_BYTES); @Override public OutputStream getCompressingOutputStream() { return statsOutputStream; } @Override public void finish() throws IOException { gzipOutputStream.finish(); } }; } @Override CompressingInputStream getCompressingStream( final InputStream inputStream, final CompressingFilterContext context) { return new CompressingInputStream() { @Override public InputStream getCompressingInputStream() throws IOException { return CompressingStreamFactory.maybeWrapStatsInputStream( new GZIPInputStream( CompressingStreamFactory.maybeWrapStatsInputStream( inputStream, context, CompressingFilterStats.StatsField.REQUEST_COMPRESSED_BYTES)), context, CompressingFilterStats.StatsField.REQUEST_INPUT_BYTES); } }; } } private static class ZipCompressingStreamFactory extends CompressingStreamFactory { @Override CompressingOutputStream getCompressingStream( final OutputStream outputStream, final CompressingFilterContext context) { return new CompressingOutputStream() { private final DeflaterOutputStream zipOutputStream = new ZipOutputStream( CompressingStreamFactory.maybeWrapStatsOutputStream( outputStream, context, CompressingFilterStats.StatsField.RESPONSE_COMPRESSED_BYTES)); private final OutputStream statsOutputStream = CompressingStreamFactory.maybeWrapStatsOutputStream( zipOutputStream, context, CompressingFilterStats.StatsField.RESPONSE_INPUT_BYTES); @Override public OutputStream getCompressingOutputStream() { return statsOutputStream; } @Override public void finish() throws IOException { zipOutputStream.finish(); } }; } @Override CompressingInputStream getCompressingStream( final InputStream inputStream, final CompressingFilterContext context) { return new CompressingInputStream() { @Override public InputStream getCompressingInputStream() { return CompressingStreamFactory.maybeWrapStatsInputStream( new ZipInputStream( CompressingStreamFactory.maybeWrapStatsInputStream( inputStream, context, CompressingFilterStats.StatsField.REQUEST_COMPRESSED_BYTES)), context, CompressingFilterStats.StatsField.REQUEST_INPUT_BYTES); } }; } } private static class DeflateCompressingStreamFactory extends CompressingStreamFactory { @Override CompressingOutputStream getCompressingStream( final OutputStream outputStream, final CompressingFilterContext context) { return new CompressingOutputStream() { private final DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream( CompressingStreamFactory.maybeWrapStatsOutputStream( outputStream, context, CompressingFilterStats.StatsField.RESPONSE_COMPRESSED_BYTES)); private final OutputStream statsOutputStream = CompressingStreamFactory.maybeWrapStatsOutputStream( deflaterOutputStream, context, CompressingFilterStats.StatsField.RESPONSE_INPUT_BYTES); @Override public OutputStream getCompressingOutputStream() { return statsOutputStream; } @Override public void finish() throws IOException { deflaterOutputStream.finish(); } }; } @Override CompressingInputStream getCompressingStream( final InputStream inputStream, final CompressingFilterContext context) { return new CompressingInputStream() { @Override public InputStream getCompressingInputStream() { return CompressingStreamFactory.maybeWrapStatsInputStream( new InflaterInputStream( CompressingStreamFactory.maybeWrapStatsInputStream( inputStream, context, CompressingFilterStats.StatsField.REQUEST_COMPRESSED_BYTES)), context, CompressingFilterStats.StatsField.REQUEST_INPUT_BYTES); } }; } } }