org.glassfish.jersey.server.filter.EncodingFilter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jaxrs-ri Show documentation
Show all versions of jaxrs-ri Show documentation
A bundle project producing JAX-RS RI bundles. The primary artifact is an "all-in-one" OSGi-fied JAX-RS RI bundle
(jaxrs-ri.jar).
Attached to that are two compressed JAX-RS RI archives. The first archive (jaxrs-ri.zip) consists of binary RI bits and
contains the API jar (under "api" directory), RI libraries (under "lib" directory) as well as all external
RI dependencies (under "ext" directory). The secondary archive (jaxrs-ri-src.zip) contains buildable JAX-RS RI source
bundle and contains the API jar (under "api" directory), RI sources (under "src" directory) as well as all external
RI dependencies (under "ext" directory). The second archive also contains "build.xml" ANT script that builds the RI
sources. To build the JAX-RS RI simply unzip the archive, cd to the created jaxrs-ri directory and invoke "ant" from
the command line.
/*
* Copyright (c) 2012, 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.jersey.server.filter;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import jakarta.ws.rs.NotAcceptableException;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import org.glassfish.jersey.internal.inject.InjectionManager;
import org.glassfish.jersey.message.internal.HttpHeaderReader;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.spi.ContentEncoder;
/**
* Container filter that supports encoding-based content negotiation. The filter examines what
* content encodings are supported by the container (by looking up all the
* {@link org.glassfish.jersey.spi.ContentEncoder encoders}) and decides what encoding should be chosen
* based on the encodings listed in the Accept-Encoding request header and their associated quality values.
* If none of the acceptable encodings is supported and identity encoding is explicitly forbidden by the client,
* the filter generates {@link jakarta.ws.rs.core.Response.Status#NOT_ACCEPTABLE} response.
*
* The filter also ensures Accept-Encoding is added to the Vary header, for proper interaction with web caches.
*
*
* @author Martin Matula
*/
@Priority(Priorities.HEADER_DECORATOR)
public final class EncodingFilter implements ContainerResponseFilter {
// name for the identity encoding
private static final String IDENTITY_ENCODING = "identity";
@Inject
private InjectionManager injectionManager;
// sorted set to keep the order same for different invocations of the app
private volatile SortedSet supportedEncodings = null;
/**
* Enables this filter along with the provided {@link org.glassfish.jersey.spi.ContentEncoder encoders}
* for the supplied {@link ResourceConfig}.
*
* @param rc Resource config this filter should be enabled for.
* @param encoders content encoders.
*/
@SafeVarargs
public static void enableFor(ResourceConfig rc, Class... encoders) {
rc.registerClasses(encoders).registerClasses(EncodingFilter.class);
}
@Override
public void filter(ContainerRequestContext request, ContainerResponseContext response) throws IOException {
if (!response.hasEntity()) {
return;
}
// add Accept-Encoding to Vary header
List varyHeader = response.getStringHeaders().get(HttpHeaders.VARY);
if (varyHeader == null || !varyHeader.contains(HttpHeaders.ACCEPT_ENCODING)) {
response.getHeaders().add(HttpHeaders.VARY, HttpHeaders.ACCEPT_ENCODING);
}
// if Content-Encoding is already set, don't do anything
if (response.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING) != null) {
return;
}
// retrieve the list of accepted encodings
List acceptEncoding = request.getHeaders().get(HttpHeaders.ACCEPT_ENCODING);
// if empty, don't do anything
if (acceptEncoding == null || acceptEncoding.isEmpty()) {
return;
}
// convert encodings from String to Encoding objects
List encodings = new ArrayList<>();
for (String input : acceptEncoding) {
if (input.isEmpty()) {
continue;
}
String[] tokens = input.split(",");
for (String token : tokens) {
try {
ContentEncoding encoding = ContentEncoding.fromString(token);
encodings.add(encoding);
} catch (ParseException e) {
// ignore the encoding that could not parse
// but log the exception
Logger.getLogger(EncodingFilter.class.getName()).log(Level.WARNING, e.getLocalizedMessage(), e);
}
}
}
// sort based on quality parameter
Collections.sort(encodings);
// make sure IDENTITY_ENCODING is at the end (since it accepted if not explicitly forbidden
// in the Accept-Content header by assigning q=0
encodings.add(new ContentEncoding(IDENTITY_ENCODING, -1));
// get a copy of supported encoding (we'll be modifying this set, hence the copy)
SortedSet acceptedEncodings = new TreeSet<>(getSupportedEncodings());
// indicates that we can pick any of the encodings that remained in the acceptedEncodings set
boolean anyRemaining = false;
// final resulting value of the Content-Encoding header to be set
String contentEncoding = null;
// iterate through the accepted encodings, starting with the highest quality one
for (ContentEncoding encoding : encodings) {
if (encoding.q == 0) {
// ok, we are down at 0 quality
if ("*".equals(encoding.name)) {
// no other encoding is acceptable
break;
}
// all the 0 quality encodings need to be removed from the accepted ones (these are explicitly
// forbidden by the client)
acceptedEncodings.remove(encoding.name);
} else {
if ("*".equals(encoding.name)) {
// any remaining encoding (after filtering out q=0) will be acceptable
anyRemaining = true;
} else {
if (acceptedEncodings.contains(encoding.name)) {
// found an acceptable one -> we are done
contentEncoding = encoding.name;
break;
}
}
}
}
if (contentEncoding == null) {
// haven't found any explicit acceptable encoding, let's see if we can just pick any of the remaining ones
// (if there are any left)
if (anyRemaining && !acceptedEncodings.isEmpty()) {
contentEncoding = acceptedEncodings.first();
} else {
// no acceptable encoding can be sent -> return NOT ACCEPTABLE status code back to the client
throw new NotAcceptableException();
}
}
// finally set the header - but no need to set for identity encoding
if (!IDENTITY_ENCODING.equals(contentEncoding)) {
response.getHeaders().putSingle(HttpHeaders.CONTENT_ENCODING, contentEncoding);
}
}
// representation of a single Content-Encoding header value
private static class ContentEncoding implements Comparable {
public final String name;
public final int q;
public ContentEncoding(String encoding, int q) {
this.name = encoding;
this.q = q;
}
public static ContentEncoding fromString(String input) throws ParseException {
HttpHeaderReader reader = HttpHeaderReader.newInstance(input);
// Skip any white space
reader.hasNext();
return new ContentEncoding(reader.nextToken().toString(), HttpHeaderReader.readQualityFactorParameter(reader));
}
@Override
public int hashCode() {
return 41 * name.hashCode() + q;
}
@Override
public boolean equals(Object obj) {
return obj == this || obj != null && (obj instanceof ContentEncoding) && name.equals(((ContentEncoding) obj).name)
&& q == ((ContentEncoding) obj).q;
}
@Override
public int compareTo(ContentEncoding o) {
// higher q goes first (i.e. descending order)
return Integer.compare(o.q, q);
}
}
/**
* Returns a (lexically) sorted set of supported encodings.
* @return sorted set of supported encodings.
*/
SortedSet getSupportedEncodings() {
// no need for synchronization - in case of a race condition, the property
// may be set twice, but it does not break anything
if (supportedEncodings == null) {
SortedSet se = new TreeSet<>();
List encoders = injectionManager.getAllInstances(ContentEncoder.class);
for (ContentEncoder encoder : encoders) {
se.addAll(encoder.getSupportedEncodings());
}
se.add(IDENTITY_ENCODING);
supportedEncodings = se;
}
return supportedEncodings;
}
}