// Copyright 2012 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.jaxws; import com.google.api.ads.common.lib.exception.ServiceException; import com.google.api.ads.common.lib.soap.RequestInfo; import com.google.api.ads.common.lib.soap.ResponseInfo; import com.google.api.ads.common.lib.soap.SoapCall; import com.google.api.ads.common.lib.soap.SoapCallReturn; import com.google.api.ads.common.lib.soap.SoapClientHandler; import com.google.api.ads.common.lib.soap.SoapClientHandlerInterface; import com.google.api.ads.common.lib.soap.SoapServiceDescriptor; import com.google.api.ads.common.lib.soap.compatability.JaxWsCompatible; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.xml.namespace.QName; import javax.xml.soap.SOAPElement; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPHeaderElement; import javax.xml.ws.BindingProvider; import javax.xml.ws.handler.Handler; import javax.xml.ws.handler.MessageContext; /** * SOAP Client Handler implementation for use with JAX-WS. */ public class JaxWsHandler extends SoapClientHandler<BindingProvider> { /** * Default connect timeout. */ private static final int CONNECT_TIMEOUT = 10 * 60 * 1000; private static final String PRODUCTION_REQUEST_TIMEOUT_KEY = "com.sun.xml.ws.request.timeout"; private static final String PRODUCTION_CONNECT_TIMEOUT_KEY = "com.sun.xml.ws.connect.timeout"; private static final String DEVEL_REQUEST_TIMEOUT_KEY = "com.sun.xml.internal.ws.request.timeout"; private static final String DEVEL_CONNECT_TIMEOUT_KEY = "com.sun.xml.internal.ws.connect.timeout"; private final JaxWsSoapContextHandlerFactory contextHandlerFactory; /** * Constructor. * * @param contextHandlerFactory a factory which produces context handlers */ @Inject protected JaxWsHandler(JaxWsSoapContextHandlerFactory contextHandlerFactory) { super(); this.contextHandlerFactory = contextHandlerFactory; } /** * Sets the endpoint address of the given SOAP client. * * @param soapClient the SOAP client to set the endpoint address for * @param endpointAddress the target endpoint address */ @Override public void setEndpointAddress(BindingProvider soapClient, String endpointAddress) { soapClient.getRequestContext().put( BindingProvider.ENDPOINT_ADDRESS_PROPERTY, endpointAddress); } /** * Returns a SOAP header from the given SOAP client, if it exists. * * @param soapClient the SOAP client to check for the given header * @param headerName the name of the header being looked for * @return the header element, if it exists */ @Override public Object getHeader(BindingProvider soapClient, String headerName) { for (SOAPElement addedHeader : getContextHandlerFromClient(soapClient).getAddedHeaders()) { if (addedHeader.getNodeName().equals(headerName)) { return addedHeader; } } return null; } /** * Clears all of the SOAP headers from the given SOAP client. * * @param soapClient the client to remove the headers from */ @Override public void clearHeaders(BindingProvider soapClient) { getContextHandlerFromClient(soapClient).clearHeaders(); soapClient.getRequestContext().put(MessageContext.HTTP_REQUEST_HEADERS, new HashMap<String, List<String>>()); } /** * @see SoapClientHandler#setHeader(Object, String, String, Object) */ @Override public void setHeader(BindingProvider soapClient, String namespace, String headerName, Object headerValue) { if (headerValue instanceof SOAPElement) { getContextHandlerFromClient(soapClient).addHeader(namespace, headerName, (SOAPElement) headerValue); } else { throw new ServiceException("Unexpected SOAP header given for JAX-WS binding. Given " + "object of class \"" + headerValue.getClass().toString() + "\" but expecting " + "object of class \"" + SOAPElement.class + "\".", null); } } /** * Adds a child text node named childName to the existing header named headerName. * * @param soapClient the binding provider * @param headerName the name of the existing header * @param childNamespace the namespace of the new child * @param childName the name of the new child * @param childValue the value of the new child * * @throws NullPointerException if no header exists named headerName */ public void setHeaderChildString(BindingProvider soapClient, final String headerName, String childNamespace, String childName, String childValue) { // Find the parent header SOAPElement SOAPElement parentHeader = (SOAPElement) getHeader(soapClient, headerName); Preconditions.checkNotNull(parentHeader, "No header element found with name: %s", headerName); // Add a SOAPElement for the child try { SOAPElement childElement = parentHeader.addChildElement(new QName(childNamespace, childName)); childElement.setTextContent(childValue); } catch (SOAPException e) { throw new ServiceException("Failed to set header for child " + childName, e); } } /** * @see SoapClientHandler#putAllHttpHeaders(Object, Map) */ @Override public void putAllHttpHeaders(BindingProvider soapClient, Map<String, String> headersMap) { @SuppressWarnings("unchecked") // HTTP Headers in JAXWS are always a map of // String to List of String. Map<String, List<String>> httpHeaders = (Map<String, List<String>>) soapClient .getRequestContext().get(MessageContext.HTTP_REQUEST_HEADERS); if (httpHeaders == null) { httpHeaders = Maps.newHashMap(); } for (String key : headersMap.keySet()) { httpHeaders.put(key, Lists.newArrayList(headersMap.get(key))); } soapClient.getRequestContext().put(MessageContext.HTTP_REQUEST_HEADERS, httpHeaders); } /** * Set whether SOAP requests should use compression. * * @param soapClient the client to set compression settings for * @param compress whether or not to use compression */ @Override public void setCompression(BindingProvider soapClient, boolean compress) { Map<String, String> headersMap = Maps.newHashMap(); if (compress) { headersMap.put("Accept-Encoding", "gzip"); headersMap.put("Content-Encoding", "gzip"); putAllHttpHeaders(soapClient, headersMap); } else { @SuppressWarnings("unchecked") // HTTP Headers in JAXWS are always a map of // String to List of String. Map<String, List<String>> httpHeaders = (Map<String, List<String>>) soapClient.getRequestContext().get( MessageContext.HTTP_REQUEST_HEADERS); if (httpHeaders != null) { httpHeaders.remove("Accept-Encoding"); httpHeaders.remove("Content-Encoding"); } } } /** * Creates a SOAP client using a SOAP service descriptor. * * @param soapServiceDescriptor the descriptor to use for creating a client * @return the SOAP client for this descriptor * @throws ServiceException thrown if the SOAP client cannot be created */ @Override public BindingProvider createSoapClient(SoapServiceDescriptor soapServiceDescriptor) throws ServiceException { try { if (soapServiceDescriptor instanceof JaxWsCompatible) { JaxWsCompatible jaxWsCompatibleService = (JaxWsCompatible) soapServiceDescriptor; Object portLocator = jaxWsCompatibleService.getServiceClass() .getConstructor(new Class[0]).newInstance(new Object[0]); String interfaceClassName = soapServiceDescriptor.getInterfaceClass().getSimpleName(); BindingProvider soapClient = (BindingProvider) portLocator.getClass() .getMethod("get" + interfaceClassName + "Port").invoke(portLocator); // Required for App Engine to avoid default 10s timeout for UrlFetch requests. setConnectTimeout(soapClient); @SuppressWarnings("rawtypes") // getHandlerChain returns a list of raw Handler. List<Handler> bindings = soapClient.getBinding().getHandlerChain(); bindings.add(contextHandlerFactory.getJaxWsSoapContextHandler()); soapClient.getBinding().setHandlerChain(bindings); return soapClient; } throw new ServiceException("Service [" + soapServiceDescriptor + "] is not compatible with JAX-WS", null); } catch (SecurityException e) { throw new ServiceException("Unexpected Exception.", e); } catch (NoSuchMethodException e) { throw new ServiceException("Unexpected Exception.", e); } catch (IllegalArgumentException e) { throw new ServiceException("Unexpected Exception.", e); } catch (IllegalAccessException e) { throw new ServiceException("Unexpected Exception.", e); } catch (InvocationTargetException e) { throw new ServiceException("Unexpected Exception.", e.getCause()); } catch (ClassNotFoundException e) { throw new ServiceException("Unexpected Exception.", e); } catch (InstantiationException e) { throw new ServiceException("Unexpected Exception.", e); } } /** * Sets properties into the message context to alter the timeout on App Engine. */ @Override public void setRequestTimeout(BindingProvider bindingProvider, int timeout) { // Production App Engine bindingProvider.getRequestContext().put(PRODUCTION_REQUEST_TIMEOUT_KEY, timeout); // Dev App Engine bindingProvider.getRequestContext().put(DEVEL_REQUEST_TIMEOUT_KEY, timeout); } private void setConnectTimeout(BindingProvider bindingProvider) { // Production App Engine bindingProvider.getRequestContext().put(PRODUCTION_CONNECT_TIMEOUT_KEY, CONNECT_TIMEOUT); // Dev App Engine bindingProvider.getRequestContext().put(DEVEL_CONNECT_TIMEOUT_KEY, CONNECT_TIMEOUT); } /** * Invoke a SOAP call. * * @param soapCall the call to make to a SOAP web service * @return information about the SOAP response */ @Override public SoapCallReturn invokeSoapCall(SoapCall<BindingProvider> soapCall) { BindingProvider webService = soapCall.getSoapClient(); SoapCallReturn.Builder builder = new SoapCallReturn.Builder(); synchronized (webService) { Object result = null; try { result = invoke(soapCall); } catch (InvocationTargetException e) { builder.withException(e.getTargetException()); } catch (Exception e) { builder.withException(e); } finally { JaxWsSoapContextHandler contextHandler = getContextHandlerFromClient(webService); builder.withRequestInfo(new RequestInfo.Builder() .withSoapRequestXml(contextHandler.getLastRequestXml()) .withMethodName(contextHandler.getLastOperationCalled()) .withServiceName(contextHandler.getLastServiceCalled()) .withUrl((String) webService.getRequestContext().get( BindingProvider.ENDPOINT_ADDRESS_PROPERTY)) .build()); builder.withResponseInfo( new ResponseInfo.Builder() .withSoapResponseXml(contextHandler.getLastResponseXml()) .withRequestId(contextHandler.getLastRequestId()) .build()); } return builder.withReturnValue(result).build(); } } /** * @see SoapClientHandlerInterface#getEndpointAddress(Object) */ @Override public String getEndpointAddress(BindingProvider soapClient) { return (String) soapClient.getRequestContext().get(BindingProvider.ENDPOINT_ADDRESS_PROPERTY); } /** * JAX-WS does not support use of this method. * * @see SoapClientHandlerInterface#createSoapHeaderElement(QName) */ @Override public SOAPHeaderElement createSoapHeaderElement(QName qName) { throw new UnsupportedOperationException(); } /** * Extracts the {@link JaxWsSoapContextHandler} object from a SOAP client's * handler chain. * * In the event that no {@code JaxWsSoapContextHandler} object could be found, * this method throw an {@code IllegalStateException}. * * @param soapClient the JAX-WS soap client whose handler is needed * @return the {@code JaxWsSoapContextHandler} handler in the given client's * handler chain */ private JaxWsSoapContextHandler getContextHandlerFromClient(BindingProvider soapClient) { @SuppressWarnings("rawtypes") // getHandlerChain returns a list of raw Handler. List<Handler> handlers = soapClient.getBinding().getHandlerChain(); for (Handler<?> handler : handlers) { if (handler instanceof JaxWsSoapContextHandler) { return (JaxWsSoapContextHandler) handler; } } throw new IllegalStateException("The SOAP client passed into the JaxWsHandler does not " + "have the necessary context handler on its binding chain."); } }