All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glassfish.grizzly.http.CompressionConfig Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2013, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.grizzly.http;

import java.util.Arrays;
import java.util.Collections;
import java.util.Set;

import org.glassfish.grizzly.http.util.DataChunk;
import org.glassfish.grizzly.http.util.Header;
import org.glassfish.grizzly.http.util.HttpUtils;
import org.glassfish.grizzly.utils.ArraySet;

/**
 * Compression configuration class.
 */
public final class CompressionConfig {

    /**
     * Common CompressionMode interface. It is a temporary solution used to deprecate CompressionLevel enum in http-server
     * module.
     *
     * This interface is intended to be used in Grizzly core methods only. Please don't use this interface directly in your
     * code. Please use {@link CompressionMode} enum in all the Grizzly core methods, that expect CompressionModeI.
     *
     * @see CompressionMode
     */
    public interface CompressionModeI {
    }

    public enum CompressionMode implements CompressionModeI {

        OFF, ON, FORCE;

        /**
         * Returns the {@link CompressionMode} based on the string representation.
         */
        public static CompressionMode fromString(final String mode) {
            if ("on".equalsIgnoreCase(mode)) {
                return CompressionMode.ON;
            } else if ("force".equalsIgnoreCase(mode)) {
                return CompressionMode.FORCE;
            } else if ("off".equalsIgnoreCase(mode)) {
                return CompressionMode.OFF;
            }

            throw new IllegalArgumentException("Compression mode is not recognized. Supported modes: " + Arrays.toString(CompressionMode.values()));
        }
    }

    // compression mode
    private CompressionMode compressionMode;
    // the min size of the entities, which will be compressed
    private int compressionMinSize;
    // mime types of the enitties, which will be compressed
    private final ArraySet compressibleMimeTypes = new ArraySet<>(String.class);
    // the user-agents, for which the payload will never be compressed
    private final ArraySet noCompressionUserAgents = new ArraySet<>(String.class);
    // Allow decompression of incoming data
    private boolean decompressionEnabled;

    public CompressionConfig() {
        compressionMode = CompressionMode.OFF;
    }

    /**
     * The copy constructor. The newly constructed CompressionConfig object will have the same settings as the source one,
     * but actual values will be independent, so changes to one CompressionConfig object will not affect the other one.
     */
    @SuppressWarnings("IncompleteCopyConstructor")
    public CompressionConfig(final CompressionConfig compression) {
        set(compression);
    }

    public CompressionConfig(final CompressionMode compressionMode, final int compressionMinSize, final Set compressibleMimeTypes,
            final Set noCompressionUserAgents) {
        this(compressionMode, compressionMinSize, compressibleMimeTypes, noCompressionUserAgents, false);
    }

    public CompressionConfig(final CompressionMode compressionMode, final int compressionMinSize, final Set compressibleMimeTypes,
            final Set noCompressionUserAgents, final boolean decompressionEnabled) {
        setCompressionMode(compressionMode);
        setCompressionMinSize(compressionMinSize);
        setCompressibleMimeTypes(compressibleMimeTypes);
        setNoCompressionUserAgents(noCompressionUserAgents);
        setDecompressionEnabled(decompressionEnabled);
    }

    /**
     * Copies the source CompressionConfig object value into this object. As the result this CompressionConfig object will
     * have the same settings as the source one, but actual values will be independent, so changes to one CompressionConfig
     * object will not affect the other one.
     */
    public void set(final CompressionConfig compression) {
        compressionMode = compression.compressionMode;
        compressionMinSize = compression.compressionMinSize;
        setCompressibleMimeTypes(compression.compressibleMimeTypes);
        setNoCompressionUserAgents(compression.noCompressionUserAgents);
        decompressionEnabled = compression.isDecompressionEnabled();
    }

    /**
     * Returns the {@link CompressionMode}.
     */
    public CompressionMode getCompressionMode() {
        return compressionMode;
    }

    /**
     * Sets the {@link CompressionMode}.
     */
    public void setCompressionMode(final CompressionMode mode) {
        this.compressionMode = mode != null ? mode : CompressionMode.OFF;
    }

    /**
     * Returns the minimum size of an entity, which will be compressed.
     */
    public int getCompressionMinSize() {
        return compressionMinSize;
    }

    /**
     * Sets the minimum size of an entity, which will be compressed.
     */
    public void setCompressionMinSize(int compressionMinSize) {
        this.compressionMinSize = compressionMinSize;
    }

    /**
     * Returns the read-only set of the mime-types, which are allowed to be compressed. Empty set means *all* mime-types are
     * allowed to be compressed.
     *
     * @deprecated Use {@link #getCompressibleMimeTypes()}
     */
    @Deprecated
    public Set getCompressableMimeTypes() {
        return getCompressibleMimeTypes();
    }

    /**
     * Sets the set of the mime-types, which are allowed to be compressed. Empty set means *all* mime-types are allowed to
     * be compressed.
     *
     * Please note that CompressionConfig object will copy the source Set content, so further changes made on the source Set
     * will not affect CompressionConfig object state.
     *
     * @deprecated Use {@link #setCompressibleMimeTypes(Set)}
     */
    @Deprecated
    public void setCompressableMimeTypes(final Set compressibleMimeTypes) {
        setCompressibleMimeTypes(compressibleMimeTypes);
    }

    /**
     * Sets the set of the mime-types, which are allowed to be compressed. Empty set means *all* mime-types are allowed to
     * be compressed.
     *
     * Please note that CompressionConfig object will copy the source Set content, so further changes made on the source Set
     * will not affect CompressionConfig object state.
     *
     * @deprecated Use {@link #setCompressibleMimeTypes(String...)}
     */
    @Deprecated
    public void setCompressableMimeTypes(final String... compressibleMimeTypes) {
        setCompressibleMimeTypes(compressibleMimeTypes);
    }

    /**
     * Returns the read-only set of the mime-types, which are allowed to be compressed. Empty set means *all* mime-types are
     * allowed to be compressed.
     *
     * @since 2.3.29
     */
    public Set getCompressibleMimeTypes() {
        return Collections.unmodifiableSet(compressibleMimeTypes);
    }

    /**
     * Sets the set of the mime-types, which are allowed to be compressed. Empty set means *all* mime-types are allowed to
     * be compressed.
     *
     * Please note that CompressionConfig object will copy the source Set content, so further changes made on the source Set
     * will not affect CompressionConfig object state.
     *
     * @since 2.3.29
     */
    public void setCompressibleMimeTypes(final Set compressibleMimeTypes) {
        this.compressibleMimeTypes.clear();

        if (compressibleMimeTypes != null && !compressibleMimeTypes.isEmpty()) {
            this.compressibleMimeTypes.addAll(compressibleMimeTypes);
        }
    }

    /**
     * Sets the set of the mime-types, which are allowed to be compressed. Empty set means *all* mime-types are allowed to
     * be compressed.
     *
     * Please note that CompressionConfig object will copy the source Set content, so further changes made on the source Set
     * will not affect CompressionConfig object state.
     *
     * @since 2.3.29
     */
    public void setCompressibleMimeTypes(final String... compressibleMimeTypes) {
        this.compressibleMimeTypes.clear();

        if (compressibleMimeTypes.length > 0) {
            this.compressibleMimeTypes.addAll(compressibleMimeTypes);
        }
    }

    /**
     * Returns the read-only set of the user-agents, which will be always responded with uncompressed are.
     *
     * Empty set means that compressed data could be sent to *all* user-agents.
     */
    public Set getNoCompressionUserAgents() {
        return Collections.unmodifiableSet(noCompressionUserAgents);
    }

    /**
     * Sets the set of the user-agents, which will be always responded with uncompressed data. Empty set means that
     * compressed data could be sent to *all* user-agents.
     *
     * Please note that CompressionConfig object will copy the source Set content, so further changes made on the source Set
     * will not affect CompressionConfig object state.
     */
    public void setNoCompressionUserAgents(final Set noCompressionUserAgents) {
        this.noCompressionUserAgents.clear();

        if (noCompressionUserAgents != null && !noCompressionUserAgents.isEmpty()) {
            this.noCompressionUserAgents.addAll(noCompressionUserAgents);
        }
    }

    /**
     * Sets the set of the user-agents, which will be always responded with uncompressed data. Empty set means that
     * compressed data could be sent to *all* user-agents.
     *
     * Please note that CompressionConfig object will copy the source Set content, so further changes made on the source Set
     * will not affect CompressionConfig object state.
     */
    public void setNoCompressionUserAgents(final String... noCompressionUserAgents) {
        this.noCompressionUserAgents.clear();

        if (noCompressionUserAgents.length > 0) {
            this.noCompressionUserAgents.addAll(noCompressionUserAgents);
        }
    }

    /**
     * @return true if decompression of incoming data is enabled.
     *
     * @since 2.3.29
     */
    public boolean isDecompressionEnabled() {
        return decompressionEnabled;
    }

    /**
     * If set to true content-encoding header for incoming data will be respected and data will be decompressed
     * and the decompressed stream will be passed on.
     *
     * @since 2.3.29
     */
    public void setDecompressionEnabled(boolean decompressionEnabled) {
        this.decompressionEnabled = decompressionEnabled;
    }

    /**
     * Returns true if a client, based on its {@link HttpRequestPacket}, could be responded with compressed data,
     * or false otherwise.
     *
     * @param compressionConfig {@link CompressionConfig}
     * @param request client-side {@link HttpRequestPacket}
     * @param aliases compression algorithm aliases (to match with Accept-Encoding header)
     * @return true if a client, based on its {@link HttpRequestPacket}, could be responded with compressed data,
     * or false otherwise
     */
    public static boolean isClientSupportCompression(final CompressionConfig compressionConfig, final HttpRequestPacket request, final String[] aliases) {
        final CompressionMode mode = compressionConfig.getCompressionMode();

        switch (mode) {
        case OFF:
            return false;
        default:
            // Compress only since HTTP 1.1
            if (org.glassfish.grizzly.http.Protocol.HTTP_1_1 != request.getProtocol()) {
                return false;
            }

            if (!isClientSupportContentEncoding(request, aliases)) {
                return false;
            }

            // If force mode, always compress (test purposes only)
            return mode == CompressionMode.FORCE || compressionConfig.checkUserAgent(request);

        }
    }

    /**
     * Returns true if based on this configuration user-agent, specified in the {@link HttpRequestPacket}, can
     * receive compressed data.
     */
    public boolean checkUserAgent(final HttpRequestPacket request) {
        // Check for incompatible Browser
        if (!noCompressionUserAgents.isEmpty()) {
            final DataChunk userAgentValueDC = request.getHeaders().getValue(Header.UserAgent);
            if (userAgentValueDC != null && indexOf(noCompressionUserAgents.getArray(), userAgentValueDC) != -1) {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns true if the resource with the given content-type could be compressed, or false otherwise.
     */
    public boolean checkMimeType(final String contentType) {
        return compressibleMimeTypes.isEmpty() || indexOfStartsWith(compressibleMimeTypes.getArray(), contentType) != -1;

    }

    private static boolean isClientSupportContentEncoding(HttpRequestPacket request, final String[] aliases) {
        // Check if browser support gzip encoding
        final DataChunk acceptEncodingDC = request.getHeaders().getValue(Header.AcceptEncoding);
        if (acceptEncodingDC == null) {
            return false;
        }
        String alias = null;
        int idx = -1;
        for (int i = 0, len = aliases.length; i < len; i++) {
            alias = aliases[i];
            idx = acceptEncodingDC.indexOf(alias, 0);
            if (idx != -1) {
                break;
            }
        }

        if (idx == -1) {
            return false;
        }

        assert alias != null;

        // we only care about q=0/q=0.0. If present, the user-agent
        // doesn't support this particular compression.
        int qvalueStart = acceptEncodingDC.indexOf(';', idx + alias.length());
        if (qvalueStart != -1) {
            qvalueStart = acceptEncodingDC.indexOf('=', qvalueStart);
            final int commaIdx = acceptEncodingDC.indexOf(',', qvalueStart);
            final int qvalueEnd = commaIdx != -1 ? commaIdx : acceptEncodingDC.getLength();
            if (HttpUtils.convertQValueToFloat(acceptEncodingDC, qvalueStart + 1, qvalueEnd) == 0.0f) {
                return false;
            }
        }

        return true;
    }

    private static int indexOf(String[] aliases, DataChunk dc) {
        if (dc == null || dc.isNull()) {
            return -1;
        }
        for (int i = 0; i < aliases.length; i++) {
            final String alias = aliases[i];
            if (dc.indexOf(alias, 0) != -1) {
                return i;
            }
        }
        return -1;
    }

    private static int indexOfStartsWith(String[] aliases, String s) {
        if (s == null || s.length() == 0) {
            return -1;
        }
        for (int i = 0; i < aliases.length; i++) {
            final String alias = aliases[i];
            if (s.startsWith(alias)) {
                return i;
            }
        }
        return -1;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy