/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2010 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package com.sun.jersey.api; import com.sun.jersey.core.header.OutBoundHeaders; import com.sun.jersey.core.spi.factory.ResponseImpl; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.URI; import java.util.Date; import java.util.List; import java.util.Locale; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.EntityTag; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.StatusType; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.Variant; /** * Defines the contract between a returned instance and the runtime when * an application needs to provide metadata to the runtime. * <p> * JResponse is a type safe alternative to {@link Response} that preserves the * type information of response entity thus it is not necessary to utilize * {@link GenericEntity}. It provides equivalent functonality to * {@link Response}. * <p> * JResponse may be extended in combination with {@link AJResponseBuilder} * specialization when building responses. * <p> * Several methods have parameters of type URI, {@link UriBuilder} provides * convenient methods to create such values as does * {@link <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/net/URI.html#create(java.lang.String)">URI.create()</a>}. * * @param <E> The entity type * @see JResponseBuilder * @see Response * @author Paul.Sandoz@Sun.Com */ public class JResponse<E> { private final StatusType statusType; private final E entity; private final OutBoundHeaders headers; /** * Construct given a status type, entity and metadata. * * @param statusType the status type * @param headers the metadata, it is the callers responsibility to copy * the metadata if necessary. * @param entity the entity */ public JResponse(StatusType statusType, OutBoundHeaders headers, E entity) { this.statusType = statusType; this.entity = entity; this.headers = headers; } /** * Construct given a status, entity and metadata. * * @param status the status * @param headers the metadata, it is the callers responsibility to copy * the metadata if necessary. * @param entity the entity */ public JResponse(int status, OutBoundHeaders headers, E entity) { this(ResponseImpl.toStatusType(status), headers, entity); } /** * Construct a shallow copy. The metadata map will be copied but not the * key/value references. * * @param that the JResponse to copy from. */ public JResponse(JResponse<E> that) { this(that.statusType, that.headers != null ? new OutBoundHeaders(that.headers) : null, that.entity); } /** * Construct from a {@link AJResponseBuilder}. * * @param b the builder. */ protected JResponse(AJResponseBuilder<E, ?> b) { this.statusType = b.getStatusType(); this.entity = b.getEntity(); this.headers = b.getMetadata(); } /** * Convert to a {@link Response} compatible instance. * * @return the {@link Response} compatible instance. */ public JResponseAsResponse toResponse() { return new JResponseAsResponse(this); } /** * Convert to a {@link Response} compatible instance. * * @param type the entity type * @return the {@link Response} compatible instance. */ public JResponseAsResponse toResponse(Type type) { return new JResponseAsResponse(this, type); } /** * Get the status type associated with the response. * * @return the response status type. */ public StatusType getStatusType() { return statusType; } /** * Get the status code associated with the response. * * @return the response status code. */ public int getStatus() { return statusType.getStatusCode(); } /** * Get metadata associated with the response as a map. The returned map * may be subsequently modified by the JAX-RS runtime. Values will be * serialized using a {@link javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate} * if one is available via * {@link javax.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} * for the class of the value or using the values {@code toString} method if a * header delegate is not available. * * @return response metadata as a map */ public OutBoundHeaders getMetadata() { return headers; } /** * Get the response entity. The response will be serialized using a * MessageBodyWriter for the class and type the entity <code>E</code>. * * @return the response entity. * @see javax.ws.rs.ext.MessageBodyWriter */ public E getEntity() { return entity; } /** * Get the type of the entity. * * @return the type of the entity. */ public Type getType() { return getSuperclassTypeParameter(getClass()); } private static Type getSuperclassTypeParameter(Class<?> subclass) { Type superclass = subclass.getGenericSuperclass(); if (!(superclass instanceof ParameterizedType)) { return Object.class; } ParameterizedType parameterized = (ParameterizedType) superclass; return parameterized.getActualTypeArguments()[0]; } /** * Create a new {@link JResponseBuilder} by performing a shallow copy of an * existing {@link Response}. The returned builder has its own metadata map but * entries are simply references to the keys and values contained in the * supplied Response metadata map. * * @param <E> The entity type * @param response a Response from which the status code, entity and metadata * will be copied * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> fromResponse(Response response) { JResponseBuilder b = status(response.getStatus()); b.entity(response.getEntity()); for (String headerName: response.getMetadata().keySet()) { List<Object> headerValues = response.getMetadata().get(headerName); for (Object headerValue: headerValues) { b.header(headerName, headerValue); } } return b; } /** * Create a new {@link JResponseBuilder} by performing a shallow copy of an * existing {@link JResponse}. The returned builder has its own metadata map but * entries are simply references to the keys and values contained in the * supplied Response metadata map. * * @param <E> The entity type * @param response a JResponse from which the status code, entity and metadata * will be copied * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> fromResponse(JResponse<E> response) { JResponseBuilder<E> b = status(response.getStatus()); b.entity(response.getEntity()); for (String headerName: response.getMetadata().keySet()) { List<Object> headerValues = response.getMetadata().get(headerName); for (Object headerValue: headerValues) { b.header(headerName, headerValue); } } return b; } /** * Create a new {@link JResponseBuilder} with the supplied status. * * @param <E> The entity type * @param status the response status * @return a new JResponseBuilder * @throws IllegalArgumentException if status is null */ public static <E> JResponseBuilder<E> status(StatusType status) { JResponseBuilder<E> b = new JResponseBuilder<E>(); b.status(status); return b; } /** * Create a new {@link JResponseBuilder} with the supplied status. * * @param <E> The entity type * @param status the response status * @return a new JResponseBuilder * @throws IllegalArgumentException if status is null */ public static <E> JResponseBuilder<E> status(Response.Status status) { return status((StatusType)status); } /** * Create a new {@link JResponseBuilder} with the supplied status. * * @param <E> The entity type * @param status the response status * @return a new JResponseBuilder * @throws IllegalArgumentException if status is less than 100 or greater * than 599. */ public static <E> JResponseBuilder<E> status(int status) { JResponseBuilder<E> b = new JResponseBuilder<E>(); b.status(status); return b; } /** * Create a new {@link JResponseBuilder} with an OK status. * * @param <E> The entity type * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> ok() { JResponseBuilder b = status(Status.OK); return b; } /** * Create a new {@link JResponseBuilder} that contains a representation. * * @param <E> The entity type * @param entity the representation entity data * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> ok(E entity) { JResponseBuilder<E> b = ok(); b.entity(entity); return b; } /** * Create a new {@link JResponseBuilder} that contains a representation. * * @param <E> The entity type * @param entity the representation entity data * @param type the media type of the entity * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> ok(E entity, MediaType type) { JResponseBuilder<E> b = ok(); b.entity(entity); b.type(type); return b; } /** * Create a new {@link JResponseBuilder} that contains a representation. * * @param <E> The entity type * @param entity the representation entity data * @param type the media type of the entity * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> ok(E entity, String type) { JResponseBuilder<E> b = ok(); b.entity(entity); b.type(type); return b; } /** * Create a new {@link JResponseBuilder} that contains a representation. * * @param <E> The entity type * @param entity the representation entity data * @param variant representation metadata * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> ok(E entity, Variant variant) { JResponseBuilder<E> b = ok(); b.entity(entity); b.variant(variant); return b; } /** * Create a new {@link JResponseBuilder} with an server error status. * * @param <E> The entity type * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> serverError() { JResponseBuilder<E> b = status(Status.INTERNAL_SERVER_ERROR); return b; } /** * Create a new {@link JResponseBuilder} for a created resource, set the * location header using the supplied value. * * @param <E> The entity type * @param location the URI of the new resource. If a relative URI is * supplied it will be converted into an absolute URI by resolving it * relative to the request URI (see {@link UriInfo#getRequestUri}). * @return a new JResponseBuilder * @throws java.lang.IllegalArgumentException if location is null */ public static <E> JResponseBuilder<E> created(URI location) { JResponseBuilder<E> b = JResponse.<E>status(Status.CREATED).location(location); return b; } /** * Create a new {@link JResponseBuilder} for an empty response. * * @param <E> The entity type * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> noContent() { JResponseBuilder<E> b = status(Status.NO_CONTENT); return b; } /** * Create a new {@link JResponseBuilder} with a not-modified status. * * @param <E> The entity type * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> notModified() { JResponseBuilder<E> b = status(Status.NOT_MODIFIED); return b; } /** * Create a new {@link JResponseBuilder} with a not-modified status. * * @param <E> The entity type * @param tag a tag for the unmodified entity * @return a new JResponseBuilder * @throws java.lang.IllegalArgumentException if tag is null */ public static <E> JResponseBuilder<E> notModified(EntityTag tag) { JResponseBuilder<E> b = notModified(); b.tag(tag); return b; } /** * Create a new {@link JResponseBuilder} with a not-modified status * and a strong entity tag. This is a shortcut * for <code>notModified(new EntityTag(<i>value</i>))</code>. * * @param <E> The entity type * @param tag the string content of a strong entity tag. The JAX-RS * runtime will quote the supplied value when creating the header. * @return a new JResponseBuilder * @throws java.lang.IllegalArgumentException if tag is null */ public static <E> JResponseBuilder<E> notModified(String tag) { JResponseBuilder b = notModified(); b.tag(tag); return b; } /** * Create a new {@link JResponseBuilder} for a redirection. Used in the * redirect-after-POST (aka POST/redirect/GET) pattern. * * @param <E> The entity type * @param location the redirection URI. If a relative URI is * supplied it will be converted into an absolute URI by resolving it * relative to the base URI of the application (see * {@link UriInfo#getBaseUri}). * @return a new JResponseBuilder * @throws java.lang.IllegalArgumentException if location is null */ public static <E> JResponseBuilder<E> seeOther(URI location) { JResponseBuilder<E> b = JResponse.<E>status(Status.SEE_OTHER).location(location); return b; } /** * Create a new {@link JResponseBuilder} for a temporary redirection. * * @param <E> The entity type * @param location the redirection URI. If a relative URI is * supplied it will be converted into an absolute URI by resolving it * relative to the base URI of the application (see * {@link UriInfo#getBaseUri}). * @return a new JResponseBuilder * @throws java.lang.IllegalArgumentException if location is null */ public static <E> JResponseBuilder<E> temporaryRedirect(URI location) { JResponseBuilder<E> b = JResponse.<E>status(Status.TEMPORARY_REDIRECT).location(location); return b; } /** * Create a new {@link JResponseBuilder} for a not acceptable response. * * @param <E> The entity type * @param variants list of variants that were available, a null value is * equivalent to an empty list. * @return a new JResponseBuilder */ public static <E> JResponseBuilder<E> notAcceptable(List<Variant> variants) { JResponseBuilder<E> b = JResponse.<E>status(Status.NOT_ACCEPTABLE).variants(variants); return b; } /** * A class used to build {@link JResponse} instances that contain metadata * instead of or in addition to an entity. An initial instance may be * obtained via static methods of the {@link JResponse} class, instance * methods provide the ability to set metadata. E.g. to create a response * that indicates the creation of a new resource: * <pre>@POST * JResponse addWidget(...) { * Widget w = ... * URI widgetId = UriBuilder.fromResource(Widget.class)... * return JResponse.created(widgetId).build(); * }</pre> * <p> * Several methods have parameters of type URI, {@link UriBuilder} * provides convenient methods to create such values as does * <code>URI.create()</code>. * <p> * Where multiple variants of the same method are provided, the type of * the supplied parameter is retained in the metadata of the built * {@link JResponse}. * * @param <E> The entity type */ public static final class JResponseBuilder<E> extends AJResponseBuilder<E, JResponseBuilder<E>> { /** * Default constructor. */ public JResponseBuilder() {} /** * Construct a shallow copy. The metadata map will be copied but not the * key/value references. * * @param that the JResponseBuilder to copy from. */ public JResponseBuilder(JResponseBuilder<E> that) { super(that); } /** * Create a shallow copy preserving state. The metadata map will be * copied but not the key/value references. * * @return the copy. */ @Override public JResponseBuilder<E> clone() { return new JResponseBuilder<E>(this); } /** * Create a {@link JResponse} instance from the current JResponseBuilder. * The builder is reset to a blank state equivalent to calling * {@link JResponse#ok() }. * * @return a JResponse instance */ public JResponse<E> build() { JResponse<E> r = new JResponse<E>(this); reset(); return r; } } /** * An abstract response builder that may be utilized to extend * response building and the construction of {@link JResponse} * instances. * * @param <E> The entity type * @param <B> The builder type */ public static abstract class AJResponseBuilder<E, B extends AJResponseBuilder> { /** * The status type. */ protected StatusType statusType = Status.NO_CONTENT; /** * The response metadata. */ protected OutBoundHeaders headers; /** * The entity. */ protected E entity; /** * Default constructor. */ protected AJResponseBuilder() {} /** * Construct a shallow copy. The metadata map will be copied but not the * key/value references. * * @param that the AJResponseBuilder to copy from. */ protected AJResponseBuilder(AJResponseBuilder<E, ?> that) { this.statusType = that.statusType; this.entity = that.entity; if (that.headers != null) { this.headers = new OutBoundHeaders(that.headers); } else { this.headers = null; } } /** * Reset to the default state. */ protected void reset() { statusType = Status.NO_CONTENT; entity = null; headers = null; } /** * Get the status type associated with the response. * * @return the response status type. */ protected StatusType getStatusType() { return statusType; } /** * Get the status code associated with the response. * * @return the response status code. */ protected int getStatus() { return statusType.getStatusCode(); } /** * Get the metadata associated with the response. * * @return response metadata as a map */ protected OutBoundHeaders getMetadata() { if (headers == null) headers = new OutBoundHeaders(); return headers; } /** * Get the response entity. * * @return the response entity. */ protected E getEntity() { return entity; } /** * Set the status. * * @param status the response status * @return the updated instance * @throws IllegalArgumentException if status is less than 100 or greater * than 599. */ public B status(int status) { return status(ResponseImpl.toStatusType(status)); } /** * Set the status. * * @param status the response status * @return the updated instance. * @throws IllegalArgumentException if status is null */ public B status(StatusType status) { if (status == null) throw new IllegalArgumentException(); this.statusType = status; return (B)this; }; /** * Set the status. * * @param status the response status * @return the updated instance. * @throws IllegalArgumentException if status is null */ public B status(Status status) { return status((StatusType)status); }; /** * Set the entity. * * @param entity the response entity * @return the updated instance */ public B entity(E entity) { this.entity = entity; return (B)this; } /** * Set the response media type. * * @param type the media type of the response entity, if null any * existing value for type will be removed * @return the updated instance */ public B type(MediaType type) { headerSingle(HttpHeaders.CONTENT_TYPE, type); return (B)this; } /** * Set the response media type. * * @param type the media type of the response entity, if null any * existing value for type will be removed * @return the updated instance * @throws IllegalArgumentException if type cannot be parsed */ public B type(String type) { return type(type == null ? null : MediaType.valueOf(type)); } /** * Set representation metadata. Equivalent to setting the values of * content type, content language, and content encoding separately using * the values of the variant properties. * * @param variant metadata of the response entity, a null value is * equivalent to a variant with all null properties. * @return the updated instance */ public B variant(Variant variant) { if (variant == null) { type((MediaType)null); language((String)null); encoding(null); return (B)this; } type(variant.getMediaType()); // TODO set charset language(variant.getLanguage()); encoding(variant.getEncoding()); return (B)this; } /** * Add a Vary header that lists the available variants. * * @param variants a list of available representation variants, a null * value will remove an existing value for vary. * @return the updated instance */ public B variants(List<Variant> variants) { if (variants == null) { header(HttpHeaders.VARY, null); return (B)this; } if (variants.isEmpty()) return (B)this; MediaType accept = variants.get(0).getMediaType(); boolean vAccept = false; Locale acceptLanguage = variants.get(0).getLanguage(); boolean vAcceptLanguage = false; String acceptEncoding = variants.get(0).getEncoding(); boolean vAcceptEncoding = false; for (Variant v : variants) { vAccept |= !vAccept && vary(v.getMediaType(), accept); vAcceptLanguage |= !vAcceptLanguage && vary(v.getLanguage(), acceptLanguage); vAcceptEncoding |= !vAcceptEncoding && vary(v.getEncoding(), acceptEncoding); } StringBuilder vary = new StringBuilder(); append(vary, vAccept, HttpHeaders.ACCEPT); append(vary, vAcceptLanguage, HttpHeaders.ACCEPT_LANGUAGE); append(vary, vAcceptEncoding, HttpHeaders.ACCEPT_ENCODING); if (vary.length() > 0) header(HttpHeaders.VARY, vary.toString()); return (B)this; } private boolean vary(MediaType v, MediaType vary) { return v != null && !v.equals(vary); } private boolean vary(Locale v, Locale vary) { return v != null && !v.equals(vary); } private boolean vary(String v, String vary) { return v != null && !v.equalsIgnoreCase(vary); } private void append(StringBuilder sb, boolean v, String s) { if (v) { if (sb.length() > 0) sb.append(','); sb.append(s); } } /** * Set the language. * * @param language the language of the response entity, if null any * existing value for language will be removed * @return the updated instance */ public B language(String language) { headerSingle(HttpHeaders.CONTENT_LANGUAGE, language); return (B)this; } /** * Set the language. * * @param language the language of the response entity, if null any * existing value for type will be removed * @return the updated instance */ public B language(Locale language) { headerSingle(HttpHeaders.CONTENT_LANGUAGE, language); return (B)this; } /** * Set the location. * * @param location the location. If a relative URI is * supplied it will be converted into an absolute URI by resolving it * relative to the base URI of the application (see * {@link UriInfo#getBaseUri}). If null any * existing value for location will be removed. * @return the updated instance. */ public B location(URI location) { headerSingle(HttpHeaders.LOCATION, location); return (B)this; } /** * Set the content location. * * @param location the content location. Relative or absolute URIs * may be used for the value of content location. If null any * existing value for content location will be removed. * @return the updated instance */ public B contentLocation(URI location) { headerSingle(HttpHeaders.CONTENT_LOCATION, location); return (B)this; } /** * Set the content encoding. * * @param encoding the content encoding of the response entity, if null * any existing value for type will be removed * @return the updated instance */ public B encoding(String encoding) { headerSingle(HttpHeaders.CONTENT_ENCODING, encoding); return (B)this; } /** * Set an entity tag. * * @param tag the entity tag, if null any * existing entity tag value will be removed. * @return the updated instance */ public B tag(EntityTag tag) { headerSingle(HttpHeaders.ETAG, tag); return (B)this; } /** * Set a strong entity tag. This is a shortcut * for <code>tag(new EntityTag(<i>value</i>))</code>. * * @param tag the string content of a strong entity tag. The JAX-RS * runtime will quote the supplied value when creating the header. If * null any existing entity tag value will be removed. * @return the updated instance */ public B tag(String tag) { return tag(tag == null ? null : new EntityTag(tag)); } /** * Set the last modified date. * * @param lastModified the last modified date, if null any existing * last modified value will be removed. * @return the updated instance */ public B lastModified(Date lastModified) { headerSingle(HttpHeaders.LAST_MODIFIED, lastModified); return (B)this; } /** * Set the cache control. * * @param cacheControl the cache control directives, if null removes any * existing cache control directives. * @return the updated instance */ public B cacheControl(CacheControl cacheControl) { headerSingle(HttpHeaders.CACHE_CONTROL, cacheControl); return (B)this; } /** * Set the expires date. * * @param expires the expiration date, if null removes any existing * expires value. * @return the updated instance */ public B expires(Date expires) { headerSingle(HttpHeaders.EXPIRES, expires); return (B)this; } /** * Add cookies. * * @param cookies new cookies that will accompany the response. A null * value will remove all cookies, including those added via the * {@link #header(java.lang.String, java.lang.Object)} method. * @return the updated instance */ public B cookie(NewCookie... cookies) { if (cookies != null) { for (NewCookie cookie : cookies) header(HttpHeaders.SET_COOKIE, cookie); } else { header(HttpHeaders.SET_COOKIE, null); } return (B)this; } /** * Add a header. * * @param name the name of the header * @param value the value of the header, the header will be serialized * using a {@link javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if * one is available via * {@link javax.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} * for the class of {@code value} or using its {@code toString} method if a * header delegate is not available. If {@code value} is null then all * current headers of the same name will be removed. * @return the updated instance. */ public B header(String name, Object value) { return header(name, value, false); } /** * Add a header or replace an existing header. * * @param name the name of the header * @param value the value of the header, the header will be serialized * using a {@link javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if * one is available via * {@link javax.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} * for the class of {@code value} or using its {@code toString} method if a * header delegate is not available. If {@code value} is null then all * current headers of the same name will be removed. * @return the updated instance. */ public B headerSingle(String name, Object value) { return header(name, value, true); } /** * Add a header. * * @param name the name of the header * @param value the value of the header, the header will be serialized * using a {@link javax.ws.rs.ext.RuntimeDelegate.HeaderDelegate} if * one is available via * {@link javax.ws.rs.ext.RuntimeDelegate#createHeaderDelegate(java.lang.Class)} * for the class of {@code value} or using its {@code toString} method if a * header delegate is not available. If {@code value} is null then all * current headers of the same name will be removed. * @param single if true then replace the header if it exists, otherwise * add the header. * @return the updated instance. */ public B header(String name, Object value, boolean single) { if (value != null) { if (single) { getMetadata().putSingle(name, value); } else { getMetadata().add(name, value); } } else { getMetadata().remove(name); } return (B)this; } } }