/** * Copyright 2014 Opower, Inc. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ package com.opower.rest.client.generator.core; import com.google.common.base.Predicate; import com.google.common.base.Throwables; import com.google.common.io.ByteStreams; import com.opower.rest.client.generator.util.CaseInsensitiveMap; import com.opower.rest.client.generator.util.GenericType; import com.opower.rest.client.generator.util.HttpHeaderNames; import com.opower.rest.client.generator.util.HttpResponseCodes; import javax.ws.rs.core.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.Providers; /** * Base class for ClientResponses. * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * */ @SuppressWarnings("unchecked") public class BaseClientResponse extends ClientResponse { private static final Logger LOG = LoggerFactory.getLogger(BaseClientResponse.class); protected Providers providers; protected String attributeExceptionsTo; protected CaseInsensitiveMap<String> headers = new CaseInsensitiveMap<>(); protected String alternateMediaType; protected Class<?> returnType; protected Type genericReturnType; protected Annotation[] annotations = {}; protected int status; protected boolean wasReleased; protected Object unmarshaledEntity; // These can only be set by an interceptor protected Exception exception; protected BaseClientResponseStreamFactory streamFactory; protected ClientExecutor executor; private final Predicate<Integer> errorStatusCriteria; /** * Create an instance with the given StreamFactory and ClientExecutor. * @param streamFactory the StreamFactory to use * @param executor the ClientExecutor to use * @param errorStatusCriteria */ public BaseClientResponse(BaseClientResponseStreamFactory streamFactory, ClientExecutor executor, Predicate<Integer> errorStatusCriteria) { this.streamFactory = streamFactory; this.executor = executor; this.errorStatusCriteria = errorStatusCriteria; } /** * Create an instance with the given StreamFactory. * @param streamFactory the StreamFactory to use * @param errorStatusCriteria */ public BaseClientResponse(BaseClientResponseStreamFactory streamFactory, Predicate<Integer> errorStatusCriteria) { this.streamFactory = streamFactory; this.errorStatusCriteria = errorStatusCriteria; } /** * Store entity within a byte array input stream because we want to release the connection * if a ClientResponseFailure is thrown. Copy status and headers, but ignore * all type information stored in the ClientResponse. * * @param copy the ClientResponse to copy * @return the copy of the ClientResponse without the type info */ public static ClientResponse copyFromError(ClientResponse copy) { BaseClientResponse base = (BaseClientResponse) copy; InputStream is = null; if (copy.getHeaders().containsKey(HttpHeaderNames.CONTENT_TYPE)) { try { is = base.streamFactory.getInputStream(); byte[] bytes = ByteStreams.toByteArray(is); is = new ByteArrayInputStream(bytes); } catch (IOException e) { LOG.warn("unable to get headers from copy of client response because of ", e); } } final InputStream theIs = is; BaseClientResponse tmp = new BaseClientResponse(new BaseClientResponseStreamFactory() { public InputStream getInputStream() throws IOException { return theIs; } public void performReleaseConnection() { } }, base.errorStatusCriteria); tmp.executor = base.executor; tmp.status = base.status; tmp.providers = base.providers; tmp.headers = new CaseInsensitiveMap<>(); tmp.headers.putAll(base.headers); return tmp; } public void setStatus(int status) { this.status = status; } public void setHeaders(CaseInsensitiveMap<String> headers) { this.headers = headers; } public void setProviders(Providers providers) { this.providers = providers; } public void setReturnType(Class<?> returnType) { this.returnType = returnType; } public void setGenericReturnType(Type genericReturnType) { this.genericReturnType = genericReturnType; } public void setAnnotations(Annotation[] annotations) { this.annotations = annotations; } public void setAttributeExceptionsTo(String attributeExceptionsTo) { this.attributeExceptionsTo = attributeExceptionsTo; } public Exception getException() { return this.exception; } public void setException(Exception exception) { this.exception = exception; } public Annotation[] getAnnotations() { return this.annotations; } /** * Get the value of the Response header for the specified key. * @param headerKey the header to get * @return The header for the specified key */ public String getResponseHeader(String headerKey) { if (this.headers == null) { return null; } return this.headers.getFirst(headerKey); } public void setAlternateMediaType(String alternateMediaType) { this.alternateMediaType = alternateMediaType; } public BaseClientResponseStreamFactory getStreamFactory() { return this.streamFactory; } @Override public boolean resetStream() { try { this.streamFactory.getInputStream().reset(); return true; } catch (Exception e) { LOG.debug("couldn't reset stream."); return false; } } @Override public Object getEntity() { if (this.returnType == null) { throw new RuntimeException( "No type information to extract entity with, use other getEntity() methods"); } // this seems like the best we can do. You get the InputStream from the response. if (Response.class.isAssignableFrom(this.returnType)) { return getEntity(InputStream.class, null, this.annotations); } else { return getEntity(this.returnType, this.genericReturnType, this.annotations); } } @Override public <T2> T2 getEntity(Class<T2> type) { return getEntity(type, null); } @Override public <T2> T2 getEntity(Class<T2> type, Type genericType) { return getEntity(type, genericType, getAnnotations(type, genericType)); } private <T2> Annotation[] getAnnotations(Class<T2> type, Type genericType) { return (this.returnType == type && this.genericReturnType == genericType) ? this.annotations : null; } @Override public <T2> T2 getEntity(Class<T2> type, Type genericType, Annotation[] anns) { if (this.exception != null) { throw new RuntimeException("Unable to unmarshall response for " + this.attributeExceptionsTo, this.exception); } if (this.unmarshaledEntity != null && !type.isInstance(this.unmarshaledEntity)) { throw new RuntimeException("The entity was already read, and it was of type " + this.unmarshaledEntity.getClass()); } if (this.unmarshaledEntity == null) { if (this.status == HttpResponseCodes.SC_NO_CONTENT) { return null; } this.unmarshaledEntity = readFrom(type, genericType, getMediaType(), anns); // only release connection if we actually unmarshalled something and if the object is *NOT* an InputStream // If it is an input stream, the user may be doing their own stream processing. if (this.unmarshaledEntity != null && !InputStream.class.isInstance(this.unmarshaledEntity)) { releaseConnection(); } } @SuppressWarnings("unchecked") T2 entityToReturn = (T2)this.unmarshaledEntity; return entityToReturn; } /** * Get the MediaType from the Response headers. * @return the MediaType as found in the Response headers */ protected MediaType getMediaType() { String mediaType = getResponseHeader(HttpHeaderNames.CONTENT_TYPE); if (mediaType == null) { mediaType = this.alternateMediaType; } return mediaType == null ? MediaType.WILDCARD_TYPE : MediaType.valueOf(mediaType); } /** * Read the Response returning the specified type. * @param type The type to return * @param genericType The generic type info if needed * @param media The MediaType to use * @param annotations The relevant annotations for the associated request * @param <T2> The type of the object that will be returned * @return The response converted to the appropriate type */ protected <T2> Object readFrom(Class<T2> type, Type genericType, MediaType media, Annotation[] annotations) { Type useGeneric = genericType == null ? type : genericType; Class<?> useType = type; MessageBodyReader reader1 = this.providers.getMessageBodyReader(useType, useGeneric, this.annotations, media); if (reader1 == null) { throw createResponseFailure(String.format( "Unable to find a MessageBodyReader of content-type %s and type %s", media, genericType)); } try { InputStream is = this.streamFactory.getInputStream(); if (is == null) { throw new ClientResponseFailure("Input stream was empty, there is no entity", this); } return reader1.readFrom(useType, useGeneric, this.annotations, media, getHeaders(), is); } catch (Exception e) { throw Throwables.propagate(e); } } @Override public <T2> T2 getEntity(GenericType<T2> genericType) { return getEntity(genericType.getType(), genericType.getGenericType()); } @Override public <T2> T2 getEntity(GenericType<T2> genericType, Annotation[] ann) { return getEntity(genericType.getType(), genericType.getGenericType(), ann); } public MultivaluedMap<String, String> getHeaders() { return this.headers; } @Override public MultivaluedMap<String, Object> getMetadata() { // hack to cast from <String, String> to <String, Object> return (MultivaluedMap) this.headers; } @Override public int getStatus() { return this.status; } /** * Check the status code of the response to see if it falls in the range of failures. */ public void checkFailureStatus() { if (this.errorStatusCriteria.apply(this.status)) { throw createResponseFailure(String.format("Error status %d %s returned", this.status, getResponseStatus())); } } public boolean isSuccessful() { return !this.errorStatusCriteria.apply(this.getStatus()); } public ClientResponseFailure createResponseFailure(String message) { return createResponseFailure(message, null); } public ClientResponseFailure createResponseFailure(String message, Exception e) { setException(e); this.returnType = byte[].class; this.genericReturnType = null; this.annotations = null; return new ClientResponseFailure(message, e, this); } @Override public Status getResponseStatus() { return Status.fromStatusCode(getStatus()); } public final void releaseConnection() { if (!wasReleased) { if (streamFactory != null) streamFactory.performReleaseConnection(); wasReleased = true; } } @Override protected final void finalize() throws Throwable { releaseConnection(); } /** * Factory for managing the InputStream from Responses. */ public interface BaseClientResponseStreamFactory { /** * Get the InputStream from the Response. The closing of the stream will be carefully managed. * @return the InputStream from the Response. * @throws IOException related to the InputStream */ InputStream getInputStream() throws IOException; /** * Release the connection associated with the Response. */ void performReleaseConnection(); } }