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

org.glassfish.jersey.message.internal.OutboundMessageContext Maven / Gradle / Ivy

Go to download

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.

There is a newer version: 3.1.6
Show newest version
/*
 * 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.message.internal;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URI;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.EntityTag;
import jakarta.ws.rs.core.GenericEntity;
import jakarta.ws.rs.core.GenericType;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Link;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.NewCookie;

import org.glassfish.jersey.CommonProperties;
import org.glassfish.jersey.internal.RuntimeDelegateDecorator;
import org.glassfish.jersey.internal.LocalizationMessages;
import org.glassfish.jersey.internal.util.ReflectionHelper;

/**
 * Base outbound message context implementation.
 *
 * @author Marek Potociar
 */
public class OutboundMessageContext {
    private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0];
    private static final List WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST =
            Collections.singletonList(MediaTypes.WILDCARD_ACCEPTABLE_TYPE);

    private final MultivaluedMap headers;
    private final CommittingOutputStream committingOutputStream;
    private Configuration configuration;

    private Object entity;
    private GenericType entityType;
    private Annotation[] entityAnnotations = EMPTY_ANNOTATIONS;
    private OutputStream entityStream;



    /**
     * The callback interface which is used to get the terminal output stream into which the entity should be
     * written and to inform the implementation about the entity size.
     */
    public static interface StreamProvider {
        /**
         * Get the output stream. This method will be called after all the
         * {@link jakarta.ws.rs.ext.WriterInterceptor writer interceptors} are called and written entity is buffered
         * into the buffer or the buffer exceeds.
         *
         * @param contentLength the size of the buffered entity or -1 if the entity exceeded the maximum buffer
         *                      size or if the buffering is disabled.
         * @return the adapted output stream into which the serialized entity should be written. May return null
         * which will cause ignoring the written entity (in that case the entity will
         * still be written by {@link jakarta.ws.rs.ext.MessageBodyWriter message body writers}
         * but the output will be ignored).
         * @throws java.io.IOException in case of an IO error.
         */
        public OutputStream getOutputStream(int contentLength) throws IOException;
    }

    /**
     * Create new outbound message context.
     * @param configuration the client/server {@link Configuration}. If {@code null}, the default behaviour is expected.
     */
    public OutboundMessageContext(Configuration configuration) {
        this.configuration = configuration;
        this.headers = HeaderUtils.createOutbound();
        this.committingOutputStream = new CommittingOutputStream();
        this.entityStream = committingOutputStream;
    }

    /**
     * Create new outbound message context copying the content
     * of another context.
     *
     * @param original the original outbound message context.
     */
    public OutboundMessageContext(OutboundMessageContext original) {
        this.headers = HeaderUtils.createOutbound();
        this.headers.putAll(original.headers);
        this.committingOutputStream = new CommittingOutputStream();
        this.entityStream = committingOutputStream;

        this.entity = original.entity;
        this.entityType = original.entityType;
        this.entityAnnotations = original.entityAnnotations;
        this.configuration = original.configuration;
    }

    /**
     * Create new outbound message context.
     *
     * @see #OutboundMessageContext(Configuration)
     */
    @Deprecated
    public OutboundMessageContext() {
        this ((Configuration) null);
    }

    /**
     * Replace all headers.
     *
     * @param headers new headers.
     */
    public void replaceHeaders(MultivaluedMap headers) {
        getHeaders().clear();
        if (headers != null) {
            getHeaders().putAll(headers);
        }
    }

    /**
     * Get a multi-valued map representing outbound message headers with their values converted
     * to strings.
     *
     * @return multi-valued map of outbound message header names to their string-converted values.
     */
    public MultivaluedMap getStringHeaders() {
        return HeaderUtils.asStringHeaders(headers, configuration);
    }

    /**
     * Get a message header as a single string value.
     * 

* Each single header value is converted to String using a * {@link jakarta.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if one is available * via {@link jakarta.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} * for the header value class or using its {@code toString} method if a header * delegate is not available. * * @param name the message header. * @return the message header value. If the message header is not present then * {@code null} is returned. If the message header is present but has no * value then the empty string is returned. If the message header is present * more than once then the values of joined together and separated by a ',' * character. */ public String getHeaderString(String name) { return HeaderUtils.asHeaderString(headers.get(name), RuntimeDelegateDecorator.configured(configuration)); } /** * Get a single typed header value. * * @param header value type. * @param name header name. * @param valueType header value class. * @param converter from string conversion function. Is expected to throw {@link ProcessingException} * if conversion fails. * @param convertNull if {@code true} this method calls the provided converter even for {@code null}. Otherwise this * method returns the {@code null} without calling the converter. * @return value of the header, or (possibly converted) {@code null} if not present. */ private T singleHeader(String name, Class valueType, Function converter, boolean convertNull) { final List values = headers.get(name); if (values == null || values.isEmpty()) { return convertNull ? converter.apply(null) : null; } if (values.size() > 1) { throw new HeaderValueException( LocalizationMessages.TOO_MANY_HEADER_VALUES(name, values.toString()), HeaderValueException.Context.OUTBOUND); } Object value = values.get(0); if (value == null) { return convertNull ? converter.apply(null) : null; } if (valueType.isInstance(value)) { return valueType.cast(value); } else { try { return converter.apply(HeaderUtils.asString(value, null)); } catch (ProcessingException ex) { throw exception(name, value, ex); } } } private static HeaderValueException exception(final String headerName, Object headerValue, Exception e) { return new HeaderValueException(LocalizationMessages.UNABLE_TO_PARSE_HEADER_VALUE(headerName, headerValue), e, HeaderValueException.Context.OUTBOUND); } /** * Get the mutable message headers multivalued map. * * @return mutable multivalued map of message headers. */ public MultivaluedMap getHeaders() { return headers; } /** * Get message date. * * @return the message date, otherwise {@code null} if not present. */ public Date getDate() { return singleHeader(HttpHeaders.DATE, Date.class, input -> { try { return HttpHeaderReader.readDate(input); } catch (ParseException e) { throw new ProcessingException(e); } }, false); } /** * Get the language of the entity. * * @return the language of the entity or {@code null} if not specified */ public Locale getLanguage() { return singleHeader(HttpHeaders.CONTENT_LANGUAGE, Locale.class, input -> { try { return new LanguageTag(input).getAsLocale(); } catch (ParseException e) { throw new ProcessingException(e); } }, false); } /** * Get the media type of the entity. * * @return the media type or {@code null} if not specified (e.g. there's no * message entity). */ public MediaType getMediaType() { return singleHeader(HttpHeaders.CONTENT_TYPE, MediaType.class, RuntimeDelegateDecorator.configured(configuration) .createHeaderDelegate(MediaType.class)::fromString, false); } /** * Get a list of media types that are acceptable for the message. * * @return a read-only list of requested message media types sorted according * to their q-value, with highest preference first. */ @SuppressWarnings("unchecked") public List getAcceptableMediaTypes() { final List values = headers.get(HttpHeaders.ACCEPT); if (values == null || values.isEmpty()) { return WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST; } final List result = new ArrayList<>(values.size()); boolean conversionApplied = false; for (final Object value : values) { try { if (value instanceof MediaType) { final AcceptableMediaType _value = AcceptableMediaType.valueOf((MediaType) value); conversionApplied = _value != value; // true if value was not an instance of AcceptableMediaType already result.add(_value); } else { conversionApplied = true; result.addAll(HttpHeaderReader.readAcceptMediaType(HeaderUtils.asString(value, configuration))); } } catch (java.text.ParseException e) { throw exception(HttpHeaders.ACCEPT, value, e); } } if (conversionApplied) { // cache converted headers.put(HttpHeaders.ACCEPT, result.stream() .map((Function) mediaType -> mediaType) .collect(Collectors.toList())); } return Collections.unmodifiableList(result); } /** * Get a list of languages that are acceptable for the message. * * @return a read-only list of acceptable languages sorted according * to their q-value, with highest preference first. */ public List getAcceptableLanguages() { final List values = headers.get(HttpHeaders.ACCEPT_LANGUAGE); if (values == null || values.isEmpty()) { return Collections.singletonList(new AcceptableLanguageTag("*", null).getAsLocale()); } final List result = new ArrayList(values.size()); boolean conversionApplied = false; for (final Object value : values) { if (value instanceof Locale) { result.add((Locale) value); } else { conversionApplied = true; try { result.addAll(HttpHeaderReader.readAcceptLanguage(HeaderUtils.asString(value, configuration)) .stream() .map(LanguageTag::getAsLocale) .collect(Collectors.toList())); } catch (java.text.ParseException e) { throw exception(HttpHeaders.ACCEPT_LANGUAGE, value, e); } } } if (conversionApplied) { // cache converted headers.put(HttpHeaders.ACCEPT_LANGUAGE, result.stream() .map((Function) locale -> locale) .collect(Collectors.toList())); } return Collections.unmodifiableList(result); } /** * Get any cookies that accompanied the message. * * @return a read-only map of cookie name (String) to {@link jakarta.ws.rs.core.Cookie}. */ public Map getRequestCookies() { final List cookies = headers.get(HttpHeaders.COOKIE); if (cookies == null || cookies.isEmpty()) { return Collections.emptyMap(); } Map result = new HashMap(); for (String cookie : HeaderUtils.asStringList(cookies, configuration)) { if (cookie != null) { result.putAll(HttpHeaderReader.readCookies(cookie)); } } return result; } /** * Get the allowed HTTP methods from the Allow HTTP header. * * @return the allowed HTTP methods, all methods will returned as upper case * strings. */ public Set getAllowedMethods() { final String allowed = getHeaderString(HttpHeaders.ALLOW); if (allowed == null || allowed.isEmpty()) { return Collections.emptySet(); } try { return new HashSet(HttpHeaderReader.readStringList(allowed)); } catch (java.text.ParseException e) { throw exception(HttpHeaders.ALLOW, allowed, e); } } /** * Get Content-Length value. *

* Note: {@link #getLengthLong() getLengthLong()} * should be preferred over this method, since it returns a {@code long} * instead and is therefore more portable.

* * @return Content-Length as a postive integer if present and valid number, {@code -1} if negative number. * @throws ProcessingException when {@link Integer#parseInt(String)} (String)} throws {@link NumberFormatException}. */ public int getLength() { return singleHeader(HttpHeaders.CONTENT_LENGTH, Integer.class, input -> { try { if (input != null && !input.isEmpty()) { int i = Integer.parseInt(input); if (i >= 0) { return i; } } return -1; } catch (NumberFormatException ex) { throw new ProcessingException(ex); } }, true); } /** * Get Content-Length value. * * @return Content-Length as a positive long if present and valid number, {@code -1} if negative number. * @throws ProcessingException when {@link Long#parseLong(String)} throws {@link NumberFormatException}. */ public long getLengthLong() { return singleHeader(HttpHeaders.CONTENT_LENGTH, Long.class, input -> { try { if (input != null && !input.isEmpty()) { long l = Long.parseLong(input); if (l >= 0) { return l; } } return -1L; } catch (NumberFormatException ex) { throw new ProcessingException(ex); } }, true); } /** * Get any new cookies set on the message message. * * @return a read-only map of cookie name (String) to a {@link jakarta.ws.rs.core.NewCookie new cookie}. */ public Map getResponseCookies() { List cookies = headers.get(HttpHeaders.SET_COOKIE); if (cookies == null || cookies.isEmpty()) { return Collections.emptyMap(); } Map result = new HashMap(); for (String cookie : HeaderUtils.asStringList(cookies, configuration)) { if (cookie != null) { NewCookie newCookie = HttpHeaderReader.readNewCookie(cookie); String cookieName = newCookie.getName(); if (result.containsKey(cookieName)) { result.put(cookieName, HeaderUtils.getPreferredCookie(result.get(cookieName), newCookie)); } else { result.put(cookieName, newCookie); } } } return result; } /** * Get the entity tag. * * @return the entity tag, otherwise {@code null} if not present. */ public EntityTag getEntityTag() { return singleHeader(HttpHeaders.ETAG, EntityTag.class, new Function() { @Override public EntityTag apply(String value) { try { return value == null ? null : EntityTag.valueOf(value); } catch (IllegalArgumentException ex) { throw new ProcessingException(ex); } } }, false); } /** * Get the last modified date. * * @return the last modified date, otherwise {@code null} if not present. */ public Date getLastModified() { return singleHeader(HttpHeaders.LAST_MODIFIED, Date.class, new Function() { @Override public Date apply(String input) { try { return HttpHeaderReader.readDate(input); } catch (ParseException e) { throw new ProcessingException(e); } } }, false); } /** * Get the location. * * @return the location URI, otherwise {@code null} if not present. */ public URI getLocation() { return singleHeader(HttpHeaders.LOCATION, URI.class, new Function() { @Override public URI apply(String value) { try { return value == null ? null : URI.create(value); } catch (IllegalArgumentException ex) { throw new ProcessingException(ex); } } }, false); } /** * Get the links attached to the message as header. * * @return links, may return empty {@link java.util.Set} if no links are present. Never * returns {@code null}. */ public Set getLinks() { List values = headers.get(HttpHeaders.LINK); if (values == null || values.isEmpty()) { return Collections.emptySet(); } final Set result = new HashSet(values.size()); boolean conversionApplied = false; for (final Object value : values) { if (value instanceof Link) { result.add((Link) value); } else { conversionApplied = true; try { result.add(Link.valueOf(HeaderUtils.asString(value, configuration))); } catch (IllegalArgumentException e) { throw exception(HttpHeaders.LINK, value, e); } } } if (conversionApplied) { // cache converted headers.put(HttpHeaders.LINK, result.stream() .map((Function) link -> link) .collect(Collectors.toList())); } return Collections.unmodifiableSet(result); } /** * Check if link for relation exists. * * @param relation link relation. * @return {@code true} if the for the relation link exists, {@code false} * otherwise. */ public boolean hasLink(String relation) { for (Link link : getLinks()) { List relations = LinkProvider.getLinkRelations(link.getRel()); if (relations != null && relations.contains(relation)) { return true; } } return false; } /** * Get the link for the relation. * * @param relation link relation. * @return the link for the relation, otherwise {@code null} if not present. */ public Link getLink(String relation) { for (Link link : getLinks()) { List relations = LinkProvider.getLinkRelations(link.getRel()); if (relations != null && relations.contains(relation)) { return link; } } return null; } /** * Convenience method that returns a {@link jakarta.ws.rs.core.Link.Builder Link.Builder} * for the relation. * * @param relation link relation. * @return the link builder for the relation, otherwise {@code null} if not * present. */ public Link.Builder getLinkBuilder(String relation) { Link link = getLink(relation); if (link == null) { return null; } return Link.fromLink(link); } // Message entity /** * Check if there is an entity available in the message. *

* The method returns {@code true} if the entity is present, returns * {@code false} otherwise. * * @return {@code true} if there is an entity present in the message, * {@code false} otherwise. */ public boolean hasEntity() { return entity != null; } /** * Get the message entity Java instance. *

* Returns {@code null} if the message does not contain an entity. * * @return the message entity or {@code null} if message does not contain an * entity body. */ public Object getEntity() { return entity; } /** * Set a new message message entity. * * @param entity entity object. * @see jakarta.ws.rs.ext.MessageBodyWriter */ public void setEntity(Object entity) { setEntity(entity, ReflectionHelper.genericTypeFor(entity)); } /** * Set a new message message entity. * * @param entity entity object. * @param annotations annotations attached to the entity. * @see jakarta.ws.rs.ext.MessageBodyWriter */ public void setEntity(Object entity, Annotation[] annotations) { setEntity(entity, ReflectionHelper.genericTypeFor(entity)); setEntityAnnotations(annotations); } /** * Set a new message message entity. * * @param entity entity object. * @param type entity generic type information. * @see jakarta.ws.rs.ext.MessageBodyWriter */ private void setEntity(Object entity, GenericType type) { if (entity instanceof GenericEntity) { this.entity = ((GenericEntity) entity).getEntity(); } else { this.entity = entity; } // ignoring overridden generic entity type information this.entityType = type; } /** * Set a new message message entity. * * @param entity entity object. * @param type declared entity class. * @param annotations annotations attached to the entity. * @see jakarta.ws.rs.ext.MessageBodyWriter */ public void setEntity(Object entity, Type type, Annotation[] annotations) { setEntity(entity, new GenericType(type)); setEntityAnnotations(annotations); } /** * Set a new message message entity. * * @param entity entity object. * @param annotations annotations attached to the entity. * @param mediaType entity media type. * @see jakarta.ws.rs.ext.MessageBodyWriter */ public void setEntity(Object entity, Annotation[] annotations, MediaType mediaType) { setEntity(entity, annotations); setMediaType(mediaType); } /** * Set the message content media type. * * @param mediaType message content media type. */ public void setMediaType(MediaType mediaType) { this.headers.putSingle(HttpHeaders.CONTENT_TYPE, mediaType); } /** * Get the raw message entity type information. * * @return raw message entity type information. */ public Class getEntityClass() { return entityType == null ? null : entityType.getRawType(); } /** * Get the message entity type information. * * @return message entity type. */ public Type getEntityType() { return entityType == null ? null : entityType.getType(); } /** * Set the message entity type information. *

* This method overrides any computed or previously set entity type information. * * @param type overriding message entity type. */ public void setEntityType(Type type) { this.entityType = new GenericType(type); } /** * Get the annotations attached to the entity. * * @return entity annotations. */ public Annotation[] getEntityAnnotations() { return entityAnnotations.clone(); } /** * Set the annotations attached to the entity. * * @param annotations entity annotations. */ public void setEntityAnnotations(Annotation[] annotations) { this.entityAnnotations = (annotations == null) ? EMPTY_ANNOTATIONS : annotations; } /** * Get the entity output stream. * * @return entity output stream. */ public OutputStream getEntityStream() { return entityStream; } /** * Set a new entity output stream. * * @param outputStream new entity output stream. */ public void setEntityStream(OutputStream outputStream) { this.entityStream = outputStream; } /** * Enable a buffering of serialized entity. The buffering will be configured from configuration. The property * determining the size of the buffer is {@link CommonProperties#OUTBOUND_CONTENT_LENGTH_BUFFER}. *

* The buffering functionality is by default disabled and could be enabled by calling this method. In this case * this method must be called before first bytes are written to the {@link #getEntityStream() entity stream}. * * @param configuration runtime configuration. */ public void enableBuffering(Configuration configuration) { final Integer bufferSize = CommonProperties.getValue(configuration.getProperties(), configuration.getRuntimeType(), CommonProperties.OUTBOUND_CONTENT_LENGTH_BUFFER, Integer.class); if (bufferSize != null) { committingOutputStream.enableBuffering(bufferSize); } else { committingOutputStream.enableBuffering(); } } /** * Set a stream provider callback. *

* This method must be called before first bytes are written to the {@link #getEntityStream() entity stream}. * * @param streamProvider non-{@code null} output stream provider. */ public void setStreamProvider(StreamProvider streamProvider) { committingOutputStream.setStreamProvider(streamProvider); } /** * Commits the {@link #getEntityStream() entity stream} if it wasn't already committed. * * @throws IOException in case of the IO error. */ public void commitStream() throws IOException { if (!committingOutputStream.isCommitted()) { entityStream.flush(); if (!committingOutputStream.isCommitted()) { committingOutputStream.commit(); committingOutputStream.flush(); } } } /** * Returns {@code true} if the entity stream has been committed. * * @return {@code true} if the entity stream has been committed. Otherwise returns {@code false}. */ public boolean isCommitted() { return committingOutputStream.isCommitted(); } /** * Closes the context. Flushes and closes the entity stream. */ public void close() { if (hasEntity()) { try { final OutputStream es = getEntityStream(); es.flush(); es.close(); } catch (IOException e) { // Happens when the client closed connection before receiving the full response. // This is OK and not interesting in vast majority of the cases // hence the log level set to FINE to make sure it does not flood the log unnecessarily // (especially for clients disconnecting from SSE listening, which is very common). Logger.getLogger(OutboundMessageContext.class.getName()).log(Level.FINE, e.getMessage(), e); } finally { // In case some of the output stream wrapper does not delegate close() call we // close the root stream manually to make sure it commits the data. if (!committingOutputStream.isClosed()) { try { committingOutputStream.close(); } catch (IOException e) { // Just log the exception Logger.getLogger(OutboundMessageContext.class.getName()).log(Level.FINE, e.getMessage(), e); } } } } } void setConfiguration(Configuration configuration) { this.configuration = configuration; } /** * The related client/server side {@link Configuration}. Can be {@code null}. * @return {@link Configuration} the configuration */ public Configuration getConfiguration() { return configuration; } }