blob: 162dedaa9630c12614c06d2742d2d2a62f096cf6 [file] [log] [blame]
/*
* Copyright (c) 2012, 2019 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.client;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Variant;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.WriterInterceptor;
import org.glassfish.jersey.client.internal.LocalizationMessages;
import org.glassfish.jersey.internal.MapPropertiesDelegate;
import org.glassfish.jersey.internal.PropertiesDelegate;
import org.glassfish.jersey.internal.guava.Preconditions;
import org.glassfish.jersey.internal.inject.InjectionManager;
import org.glassfish.jersey.internal.inject.InjectionManagerSupplier;
import org.glassfish.jersey.internal.util.ExceptionUtils;
import org.glassfish.jersey.internal.util.PropertiesHelper;
import org.glassfish.jersey.message.MessageBodyWorkers;
import org.glassfish.jersey.message.internal.HeaderUtils;
import org.glassfish.jersey.message.internal.OutboundMessageContext;
/**
* Jersey client request context.
*
* @author Marek Potociar
*/
public class ClientRequest extends OutboundMessageContext implements ClientRequestContext, HttpHeaders, InjectionManagerSupplier {
// Request-scoped configuration instance
private final ClientConfig clientConfig;
// Request-scoped properties delegate
private final PropertiesDelegate propertiesDelegate;
// Absolute request URI
private URI requestUri;
// Request method
private String httpMethod;
// Request filter chain execution aborting response
private Response abortResponse;
// Entity providers
private MessageBodyWorkers workers;
// Flag indicating whether the request is asynchronous
private boolean asynchronous;
// true if writeEntity() was already called
private boolean entityWritten;
// writer interceptors used to write the request
private Iterable<WriterInterceptor> writerInterceptors;
// reader interceptors used to write the request
private Iterable<ReaderInterceptor> readerInterceptors;
// do not add user-agent header (if not directly set) to the request.
private boolean ignoreUserAgent;
private static final Logger LOGGER = Logger.getLogger(ClientRequest.class.getName());
/**
* Create new Jersey client request context.
*
* @param requestUri request Uri.
* @param clientConfig request configuration.
* @param propertiesDelegate properties delegate.
*/
protected ClientRequest(
final URI requestUri, final ClientConfig clientConfig, final PropertiesDelegate propertiesDelegate) {
super(clientConfig.getConfiguration());
clientConfig.checkClient();
this.requestUri = requestUri;
this.clientConfig = clientConfig;
this.propertiesDelegate = propertiesDelegate;
}
/**
* Copy constructor.
*
* @param original original instance.
*/
public ClientRequest(final ClientRequest original) {
super(original);
this.requestUri = original.requestUri;
this.httpMethod = original.httpMethod;
this.workers = original.workers;
this.clientConfig = original.clientConfig.snapshot();
this.asynchronous = original.isAsynchronous();
this.readerInterceptors = original.readerInterceptors;
this.writerInterceptors = original.writerInterceptors;
this.propertiesDelegate = new MapPropertiesDelegate(original.propertiesDelegate);
this.ignoreUserAgent = original.ignoreUserAgent;
}
/**
* Resolve a property value for the specified property {@code name}.
*
* <p>
* The method returns the value of the property registered in the request-specific
* property bag, if available. If no property for the given property name is found
* in the request-specific property bag, the method looks at the properties stored
* in the {@link #getConfiguration() global client-runtime configuration} this request
* belongs to. If there is a value defined in the client-runtime configuration,
* it is returned, otherwise the method returns {@code null} if no such property is
* registered neither in the client runtime nor in the request-specific property bag.
* </p>
*
* @param name property name.
* @param type expected property class type.
* @param <T> property Java type.
* @return resolved property value or {@code null} if no such property is registered.
*/
public <T> T resolveProperty(final String name, final Class<T> type) {
return resolveProperty(name, null, type);
}
/**
* Resolve a property value for the specified property {@code name}.
*
* <p>
* The method returns the value of the property registered in the request-specific
* property bag, if available. If no property for the given property name is found
* in the request-specific property bag, the method looks at the properties stored
* in the {@link #getConfiguration() global client-runtime configuration} this request
* belongs to. If there is a value defined in the client-runtime configuration,
* it is returned, otherwise the method returns {@code defaultValue} if no such property is
* registered neither in the client runtime nor in the request-specific property bag.
* </p>
*
* @param name property name.
* @param defaultValue default value to return if the property is not registered.
* @param <T> property Java type.
* @return resolved property value or {@code defaultValue} if no such property is registered.
*/
@SuppressWarnings("unchecked")
public <T> T resolveProperty(final String name, final T defaultValue) {
return resolveProperty(name, defaultValue, (Class<T>) defaultValue.getClass());
}
private <T> T resolveProperty(final String name, Object defaultValue, final Class<T> type) {
// Check runtime configuration first
Object result = clientConfig.getProperty(name);
if (result != null) {
defaultValue = result;
}
// Check request properties next
result = propertiesDelegate.getProperty(name);
if (result == null) {
result = defaultValue;
}
return (result == null) ? null : PropertiesHelper.convertValue(result, type);
}
@Override
public Object getProperty(final String name) {
return propertiesDelegate.getProperty(name);
}
@Override
public Collection<String> getPropertyNames() {
return propertiesDelegate.getPropertyNames();
}
@Override
public void setProperty(final String name, final Object object) {
propertiesDelegate.setProperty(name, object);
}
@Override
public void removeProperty(final String name) {
propertiesDelegate.removeProperty(name);
}
/**
* Get the underlying properties delegate.
*
* @return underlying properties delegate.
*/
PropertiesDelegate getPropertiesDelegate() {
return propertiesDelegate;
}
/**
* Get the underlying client runtime.
*
* @return underlying client runtime.
*/
ClientRuntime getClientRuntime() {
return clientConfig.getRuntime();
}
@Override
public URI getUri() {
return requestUri;
}
@Override
public void setUri(final URI uri) {
this.requestUri = uri;
}
@Override
public String getMethod() {
return httpMethod;
}
@Override
public void setMethod(final String method) {
this.httpMethod = method;
}
@Override
public JerseyClient getClient() {
return clientConfig.getClient();
}
@Override
public void abortWith(final Response response) {
this.abortResponse = response;
}
/**
* Get the request filter chain aborting response if set, or {@code null} otherwise.
*
* @return request filter chain aborting response if set, or {@code null} otherwise.
*/
public Response getAbortResponse() {
return abortResponse;
}
@Override
public Configuration getConfiguration() {
return clientConfig.getRuntime().getConfig();
}
/**
* Get internal client configuration state.
*
* @return internal client configuration state.
*/
ClientConfig getClientConfig() {
return clientConfig;
}
@Override
public List<String> getRequestHeader(String name) {
return HeaderUtils.asStringList(getHeaders().get(name), clientConfig.getConfiguration());
}
@Override
public MultivaluedMap<String, String> getRequestHeaders() {
return HeaderUtils.asStringHeaders(getHeaders(), clientConfig.getConfiguration());
}
@Override
public Map<String, Cookie> getCookies() {
return super.getRequestCookies();
}
/**
* Get the message body workers associated with the request.
*
* @return message body workers.
*/
public MessageBodyWorkers getWorkers() {
return workers;
}
/**
* Set the message body workers associated with the request.
*
* @param workers message body workers.
*/
public void setWorkers(final MessageBodyWorkers workers) {
this.workers = workers;
}
/**
* Add new accepted types to the message headers.
*
* @param types accepted types to be added.
*/
public void accept(final MediaType... types) {
getHeaders().addAll(HttpHeaders.ACCEPT, (Object[]) types);
}
/**
* Add new accepted types to the message headers.
*
* @param types accepted types to be added.
*/
public void accept(final String... types) {
getHeaders().addAll(HttpHeaders.ACCEPT, (Object[]) types);
}
/**
* Add new accepted languages to the message headers.
*
* @param locales accepted languages to be added.
*/
public void acceptLanguage(final Locale... locales) {
getHeaders().addAll(HttpHeaders.ACCEPT_LANGUAGE, (Object[]) locales);
}
/**
* Add new accepted languages to the message headers.
*
* @param locales accepted languages to be added.
*/
public void acceptLanguage(final String... locales) {
getHeaders().addAll(HttpHeaders.ACCEPT_LANGUAGE, (Object[]) locales);
}
/**
* Add new cookie to the message headers.
*
* @param cookie cookie to be added.
*/
public void cookie(final Cookie cookie) {
getHeaders().add(HttpHeaders.COOKIE, cookie);
}
/**
* Add new cache control entry to the message headers.
*
* @param cacheControl cache control entry to be added.
*/
public void cacheControl(final CacheControl cacheControl) {
getHeaders().add(HttpHeaders.CACHE_CONTROL, cacheControl);
}
/**
* Set message encoding.
*
* @param encoding message encoding to be set.
*/
public void encoding(final String encoding) {
if (encoding == null) {
getHeaders().remove(HttpHeaders.CONTENT_ENCODING);
} else {
getHeaders().putSingle(HttpHeaders.CONTENT_ENCODING, encoding);
}
}
/**
* Set message language.
*
* @param language message language to be set.
*/
public void language(final String language) {
if (language == null) {
getHeaders().remove(HttpHeaders.CONTENT_LANGUAGE);
} else {
getHeaders().putSingle(HttpHeaders.CONTENT_LANGUAGE, language);
}
}
/**
* Set message language.
*
* @param language message language to be set.
*/
public void language(final Locale language) {
if (language == null) {
getHeaders().remove(HttpHeaders.CONTENT_LANGUAGE);
} else {
getHeaders().putSingle(HttpHeaders.CONTENT_LANGUAGE, language);
}
}
/**
* Set message content type.
*
* @param type message content type to be set.
*/
public void type(final MediaType type) {
setMediaType(type);
}
/**
* Set message content type.
*
* @param type message content type to be set.
*/
public void type(final String type) {
type(type == null ? null : MediaType.valueOf(type));
}
/**
* Set message content variant (type, language and encoding).
*
* @param variant message content content variant (type, language and encoding)
* to be set.
*/
public void variant(final Variant variant) {
if (variant == null) {
type((MediaType) null);
language((String) null);
encoding(null);
} else {
type(variant.getMediaType());
language(variant.getLanguage());
encoding(variant.getEncoding());
}
}
/**
* Returns true if the request is called asynchronously using {@link javax.ws.rs.client.AsyncInvoker}
*
* @return True if the request is asynchronous; false otherwise.
*/
public boolean isAsynchronous() {
return asynchronous;
}
/**
* Sets the flag indicating whether the request is called asynchronously using {@link javax.ws.rs.client.AsyncInvoker}.
*
* @param async True if the request is asynchronous; false otherwise.
*/
void setAsynchronous(final boolean async) {
asynchronous = async;
}
/**
* Enable a buffering of serialized entity. The buffering will be configured from runtime configuration
* associated with this request. The property determining the size of the buffer
* is {@link org.glassfish.jersey.CommonProperties#OUTBOUND_CONTENT_LENGTH_BUFFER}.
* <p/>
* 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}.
*
*/
public void enableBuffering() {
enableBuffering(getConfiguration());
}
/**
* Write (serialize) the entity set in this request into the {@link #getEntityStream() entity stream}. The method
* use {@link javax.ws.rs.ext.WriterInterceptor writer interceptors} and {@link javax.ws.rs.ext.MessageBodyWriter
* message body writer}.
* <p/>
* This method modifies the state of this request and therefore it can be called only once per request life cycle otherwise
* IllegalStateException is thrown.
* <p/>
* Note that {@link #setStreamProvider(org.glassfish.jersey.message.internal.OutboundMessageContext.StreamProvider)}
* and optionally {@link #enableBuffering()} must be called before calling this method.
*
* @throws IOException In the case of IO error.
*/
public void writeEntity() throws IOException {
Preconditions.checkState(!entityWritten, LocalizationMessages.REQUEST_ENTITY_ALREADY_WRITTEN());
entityWritten = true;
ensureMediaType();
final GenericType<?> entityType = new GenericType(getEntityType());
doWriteEntity(workers, entityType);
}
/**
* Added only to make the code testable.
*
* @param writeWorkers Message body workers instance used to write the entity.
* @param entityType entity type.
* @throws IOException when {@link MessageBodyWorkers#writeTo(Object, Class, Type, Annotation[], MediaType,
* MultivaluedMap, PropertiesDelegate, OutputStream, Iterable)} throws an {@link IOException}.
* This state is always regarded as connection failure.
*/
/* package */ void doWriteEntity(final MessageBodyWorkers writeWorkers, final GenericType<?> entityType) throws IOException {
OutputStream entityStream = null;
boolean connectionFailed = false;
boolean runtimeException = false;
try {
try {
entityStream = writeWorkers.writeTo(
getEntity(),
entityType.getRawType(),
entityType.getType(),
getEntityAnnotations(),
getMediaType(),
getHeaders(),
getPropertiesDelegate(),
getEntityStream(),
writerInterceptors);
setEntityStream(entityStream);
} catch (final IOException e) {
// JERSEY-2728 - treat SSLException as connection failure
connectionFailed = true;
throw e;
} catch (final RuntimeException e) {
runtimeException = true;
throw e;
}
} finally {
// in case we've seen the ConnectException, we won't try to close/commit stream as this would produce just
// another instance of ConnectException (which would be logged even if the previously thrown one is propagated)
// However, if another failure occurred, we still have to try to close and commit the stream - and if we experience
// another failure, there is a valid reason to log it
if (!connectionFailed) {
if (entityStream != null) {
try {
entityStream.close();
} catch (final IOException e) {
ExceptionUtils.conditionallyReThrow(e, !runtimeException, LOGGER,
LocalizationMessages.ERROR_CLOSING_OUTPUT_STREAM(), Level.FINE);
} catch (final RuntimeException e) {
ExceptionUtils.conditionallyReThrow(e, !runtimeException, LOGGER,
LocalizationMessages.ERROR_CLOSING_OUTPUT_STREAM(), Level.FINE);
}
}
try {
commitStream();
} catch (final IOException e) {
ExceptionUtils.conditionallyReThrow(e, !runtimeException, LOGGER,
LocalizationMessages.ERROR_COMMITTING_OUTPUT_STREAM(), Level.FINE);
} catch (final RuntimeException e) {
ExceptionUtils.conditionallyReThrow(e, !runtimeException, LOGGER,
LocalizationMessages.ERROR_COMMITTING_OUTPUT_STREAM(), Level.FINE);
}
}
}
}
private void ensureMediaType() {
if (getMediaType() == null) {
// Content-Type is not present choose a default type
final GenericType<?> entityType = new GenericType(getEntityType());
final List<MediaType> mediaTypes = workers.getMessageBodyWriterMediaTypes(
entityType.getRawType(), entityType.getType(), getEntityAnnotations());
setMediaType(getMediaType(mediaTypes));
}
}
private MediaType getMediaType(final List<MediaType> mediaTypes) {
if (mediaTypes.isEmpty()) {
return MediaType.APPLICATION_OCTET_STREAM_TYPE;
} else {
MediaType mediaType = mediaTypes.get(0);
if (mediaType.isWildcardType() || mediaType.isWildcardSubtype()) {
mediaType = MediaType.APPLICATION_OCTET_STREAM_TYPE;
}
return mediaType;
}
}
/**
* Set writer interceptors for this request.
* @param writerInterceptors Writer interceptors in the interceptor execution order.
*/
void setWriterInterceptors(final Iterable<WriterInterceptor> writerInterceptors) {
this.writerInterceptors = writerInterceptors;
}
/**
* Get writer interceptors of this request.
* @return Writer interceptors in the interceptor execution order.
*/
public Iterable<WriterInterceptor> getWriterInterceptors() {
return writerInterceptors;
}
/**
* Get reader interceptors of this request.
* @return Reader interceptors in the interceptor execution order.
*/
public Iterable<ReaderInterceptor> getReaderInterceptors() {
return readerInterceptors;
}
/**
* Set reader interceptors for this request.
* @param readerInterceptors Reader interceptors in the interceptor execution order.
*/
void setReaderInterceptors(final Iterable<ReaderInterceptor> readerInterceptors) {
this.readerInterceptors = readerInterceptors;
}
@Override
public InjectionManager getInjectionManager() {
return getClientRuntime().getInjectionManager();
}
/**
* Indicates whether the User-Agent header should be omitted if not directly set to the map of headers.
*
* @return {@code true} if the header should be omitted, {@code false} otherwise.
*/
public boolean ignoreUserAgent() {
return ignoreUserAgent;
}
/**
* Indicates whether the User-Agent header should be omitted if not directly set to the map of headers.
*
* @param ignore {@code true} if the header should be omitted, {@code false} otherwise.
*/
public void ignoreUserAgent(final boolean ignore) {
this.ignoreUserAgent = ignore;
}
}