// Copyright 2017 Google Inc. All Rights Reserved.
//
// 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.google.api.ads.common.lib.soap.axis;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Map.Entry;
import java.util.zip.GZIPOutputStream;
import javax.xml.soap.SOAPException;
import org.apache.axis.AxisFault;
import org.apache.axis.Constants;
import org.apache.axis.Message;
import org.apache.axis.MessageContext;
import org.apache.axis.handlers.BasicHandler;
import org.apache.axis.transport.http.HTTPConstants;
import org.apache.axis.utils.Messages;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Axis handler implementation that handles compression. */
public class HttpHandler extends BasicHandler implements HttpRequestInitializer {
private static final Logger logger = LoggerFactory.getLogger(HttpHandler.class);
private final HttpRequestFactory requestFactory;
private final InputStreamEventListener inputStreamEventListener;
/** The default transport used for everything except tests. */
private static final HttpTransport defaultHttpTransport = new NetHttpTransport();
public HttpHandler() {
this(defaultHttpTransport, null);
}
@VisibleForTesting
HttpHandler(HttpTransport transport, InputStreamEventListener inputStreamEventListener) {
super();
this.requestFactory = transport.createRequestFactory(this);
this.inputStreamEventListener = inputStreamEventListener;
}
@Override
public void invoke(MessageContext msgContext) throws AxisFault {
if (msgContext == null) {
throw AxisFault.makeFault(new NullPointerException("Null message context"));
}
// Catch any exception thrown and wrap it in an AxisFault, per the contract of Handler.invoke.
try {
HttpResponse response = null;
// Create the request.
HttpRequest postRequest = createHttpRequest(msgContext);
// Execute the request.
response = postRequest.execute();
// Translate the HTTP response to an Axis message on the message context.
msgContext.setResponseMessage(createResponseMessage(response));
} catch (Exception e) {
throw AxisFault.makeFault(e);
}
}
/** Sets attributes of the request that are common to all requests for this handler. */
@Override
public void initialize(HttpRequest httpRequest) throws IOException {
// Do not throw if execute fails, since Axis will handle unmarshalling the
// fault.
httpRequest.setThrowExceptionOnExecuteError(false);
// For consistency with the default Axis HTTPSender and CommonsHTTPSender, do not
// follow redirects.
httpRequest.setFollowRedirects(false);
// Retry should be handled by the client.
httpRequest.setNumberOfRetries(0);
}
/**
* Creates an HTTP request based on the message context.
*
* @param msgContext the Axis message context
* @return a new {@link HttpRequest} with content and headers populated
*/
private HttpRequest createHttpRequest(MessageContext msgContext)
throws SOAPException, IOException {
Message requestMessage =
Preconditions.checkNotNull(
msgContext.getRequestMessage(), "Null request message on message context");
// Construct the output stream.
String contentType = requestMessage.getContentType(msgContext.getSOAPConstants());
ByteArrayOutputStream bos;
int contentLength = Long.valueOf(requestMessage.getContentLength()).intValue();
if (contentLength > 0) {
bos = new ByteArrayOutputStream(contentLength);
} else {
bos = new ByteArrayOutputStream();
}
if (msgContext.isPropertyTrue(HTTPConstants.MC_GZIP_REQUEST)) {
logger.debug("Compressing request");
try (GZIPOutputStream gzipOs = new GZIPOutputStream(bos)) {
requestMessage.writeTo(gzipOs);
}
} else {
logger.debug("Not compressing request");
requestMessage.writeTo(bos);
}
HttpRequest httpRequest =
requestFactory.buildPostRequest(
new GenericUrl(msgContext.getStrProp(MessageContext.TRANS_URL)),
new ByteArrayContent(contentType, bos.toByteArray()));
int timeoutMillis = msgContext.getTimeout();
if (timeoutMillis >= 0) {
logger.debug("Setting read and connect timeout to {} millis", timeoutMillis);
// These are not the same, but MessageContext has only one definition of timeout.
httpRequest.setReadTimeout(timeoutMillis);
httpRequest.setConnectTimeout(timeoutMillis);
}
// Copy the request headers from the message context to the post request.
setHttpRequestHeaders(msgContext, httpRequest);
return httpRequest;
}
/** Sets HTTP request headers based on the Axis message context. */
private void setHttpRequestHeaders(MessageContext msgContext, HttpRequest httpRequest) {
@SuppressWarnings("unchecked")
Map<Object, Object> requestHeaders =
(Map<Object, Object>) msgContext.getProperty(HTTPConstants.REQUEST_HEADERS);
if (requestHeaders != null) {
for (Entry<Object, Object> headerEntry : requestHeaders.entrySet()) {
Object headerKey = headerEntry.getKey();
if (headerKey == null) {
continue;
}
String headerName = headerKey.toString().trim();
Object headerValue = headerEntry.getValue();
if (HTTPConstants.HEADER_AUTHORIZATION.equals(headerName)
&& (headerValue instanceof String)) {
// HttpRequest expects the Authorization header to be a list of values,
// so handle the case where it is simply a string.
httpRequest.getHeaders().setAuthorization((String) headerValue);
} else {
httpRequest.getHeaders().set(headerName, headerValue);
}
}
}
if (msgContext.isPropertyTrue(HTTPConstants.MC_GZIP_REQUEST)) {
httpRequest.getHeaders().setContentEncoding(HTTPConstants.COMPRESSION_GZIP);
}
}
/**
* Returns a new Axis Message based on the contents of the HTTP response.
*
* @param httpResponse the HTTP response
* @return an Axis Message for the HTTP response
* @throws IOException if unable to retrieve the HTTP response's contents
* @throws AxisFault if the HTTP response's status or contents indicate an unexpected error, such
* as a 405.
*/
private Message createResponseMessage(HttpResponse httpResponse) throws IOException, AxisFault {
int statusCode = httpResponse.getStatusCode();
String contentType = httpResponse.getContentType();
// The conditions below duplicate the logic in CommonsHTTPSender and HTTPSender.
boolean shouldParseResponse =
(statusCode > 199 && statusCode < 300)
|| (contentType != null
&& !contentType.equals("text/html")
&& statusCode > 499
&& statusCode < 600);
// Wrap the content input stream in a notifying stream so the stream event listener will be
// notified when it is closed.
InputStream responseInputStream =
new NotifyingInputStream(httpResponse.getContent(), inputStreamEventListener);
if (!shouldParseResponse) {
// The contents are not an XML response, so throw an AxisFault with
// the HTTP status code and message details.
String statusMessage = httpResponse.getStatusMessage();
AxisFault axisFault =
new AxisFault("HTTP", "(" + statusCode + ")" + statusMessage, null, null);
byte[] contentBytes;
try (InputStream stream = responseInputStream) {
contentBytes = ByteStreams.toByteArray(stream);
}
axisFault.setFaultDetailString(
Messages.getMessage(
"return01", Integer.toString(statusCode), new String(contentBytes, UTF_8)));
axisFault.addFaultDetail(
Constants.QNAME_FAULTDETAIL_HTTPERRORCODE, Integer.toString(statusCode));
throw axisFault;
}
// Response is an XML response. Do not consume and close the stream in this case, since that
// will happen later when the response is deserialized by Axis (as confirmed by unit tests for
// this class).
Message responseMessage =
new Message(
responseInputStream, false, contentType, httpResponse.getHeaders().getLocation());
responseMessage.setMessageType(Message.RESPONSE);
return responseMessage;
}
/**
* Lifecycle listener for an input stream. Used in tests to verify that input streams created by
* this handler are properly closed.
*/
@VisibleForTesting
static interface InputStreamEventListener {
void afterCreate();
void afterClose();
}
/** Wrapper for an input stream that triggers lifecycle events. */
private static class NotifyingInputStream extends FilterInputStream {
private final InputStreamEventListener inputStreamEventListener;
public NotifyingInputStream(
InputStream inputStream, InputStreamEventListener inputStreamEventListener) {
super(inputStream);
this.inputStreamEventListener = inputStreamEventListener;
if (this.inputStreamEventListener != null) {
this.inputStreamEventListener.afterCreate();
}
}
@Override
public void close() throws IOException {
if (inputStreamEventListener != null) {
inputStreamEventListener.afterClose();
}
super.close();
}
}
}