Java tutorial
/* * ***** BEGIN LICENSE BLOCK ***** * Zimbra Collaboration Suite Server * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software Foundation, * version 2 of the License. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * You should have received a copy of the GNU General Public License along with this program. * If not, see <https://www.gnu.org/licenses/>. * ***** END LICENSE BLOCK ***** */ /* * SoapHttpTransport.java */ package com.zimbra.common.soap; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletResponse; import org.apache.commons.httpclient.Cookie; import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpState; import org.apache.commons.httpclient.HttpVersion; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.cookie.CookiePolicy; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.apache.commons.httpclient.params.HttpMethodParams; import org.dom4j.DocumentException; import org.dom4j.ElementHandler; import org.dom4j.io.SAXReader; import org.xml.sax.SAXException; import com.zimbra.common.httpclient.HttpClientUtil; import com.zimbra.common.httpclient.HttpProxyConfig; import com.zimbra.common.localconfig.LC; import com.zimbra.common.net.ProxyHostConfiguration; import com.zimbra.common.service.ServiceException; import com.zimbra.common.util.ByteUtil; import com.zimbra.common.util.RemoteIP; import com.zimbra.common.util.ZimbraCookie; import com.zimbra.common.util.ZimbraHttpConnectionManager; import com.zimbra.common.util.ZimbraLog; public class SoapHttpTransport extends SoapTransport { private HttpClient mClient = ZimbraHttpConnectionManager.getInternalHttpConnMgr().getDefaultHttpClient(); private Map<String, String> mCustomHeaders; private ProxyHostConfiguration mHostConfig = null; private HttpDebugListener mHttpDebugListener; private boolean mKeepAlive = defaultKeepAlive; private int mRetryCount = defaultRetryCount; private int mTimeout = defaultTimeout; private final String mUri; private static boolean defaultKeepAlive = LC.httpclient_soaphttptransport_keepalive_connections.booleanValue(); private static int defaultRetryCount = LC.httpclient_soaphttptransport_retry_count.intValue(); private static int defaultTimeout = LC.httpclient_soaphttptransport_so_timeout.intValue(); public interface HttpDebugListener { public void sendSoapMessage(PostMethod postMethod, Element envelope, HttpState httpState); public void receiveSoapMessage(PostMethod postMethod, Element envelope); } @Override public String toString() { return "SoapHTTPTransport(uri=" + mUri + ")"; } /** * Create a new SoapHttpTransport object for the specified URI * * @param uri the origin server URL */ public SoapHttpTransport(String uri) { super(); mUri = uri; mHostConfig = HttpProxyConfig.getProxyConfig(mClient.getHostConfiguration(), uri); } public void setHttpDebugListener(HttpDebugListener listener) { mHttpDebugListener = listener; } public HttpDebugListener getHttpDebugListener() { return mHttpDebugListener; } /** * Frees any resources such as connection pool held by this transport. */ public void shutdown() { if (mClient != null && mClient != ZimbraHttpConnectionManager.getInternalHttpConnMgr().getDefaultHttpClient()) { mClient.getHttpConnectionManager().closeIdleConnections(0); mClient = null; mHostConfig = null; } } public Map<String, String> getCustomHeaders() { if (mCustomHeaders == null) mCustomHeaders = new HashMap<String, String>(); return mCustomHeaders; } /** * Whether to use HTTP keep-alive connections * * <p> Default value is <code>true</code>. */ public void setKeepAlive(boolean keepAlive) { mKeepAlive = keepAlive; } /** * The number of times the invoke method retries * * <p> Default value is <code>1</code>. */ public void setRetryCount(int newRetryCount) { mRetryCount = newRetryCount < 0 ? defaultRetryCount : newRetryCount; } /** * Get the mRetryCount value. */ public int getRetryCount() { return mRetryCount; } /** * Sets the number of milliseconds to wait when reading data * during a invoke call. */ @Override public void setTimeout(int newTimeout) { mTimeout = newTimeout < 0 ? defaultTimeout : newTimeout; } /** * Get the mTimeout value in milliseconds. The default is specified by * the <tt>httpclient_soaphttptransport_so_timeout</tt> localconfig variable. */ public int getTimeout() { return mTimeout; } /** * Gets the URI */ public String getURI() { return mUri; } @Override public Element invoke(Element document, boolean raw, boolean noSession, String requestedAccountId, String changeToken, String tokenType) throws IOException, HttpException, ServiceException { return invoke(document, raw, noSession, requestedAccountId, changeToken, tokenType, null); } public Element invoke(Element document, boolean raw, boolean noSession, String requestedAccountId, String changeToken, String tokenType, ResponseHandler respHandler) throws IOException, HttpException, ServiceException { PostMethod method = null; try { // Assemble post method. Append document name, so that the request // type is written to the access log. String uri, query; int i = mUri.indexOf('?'); if (i >= 0) { uri = mUri.substring(0, i); query = mUri.substring(i); } else { uri = mUri; query = ""; } if (!uri.endsWith("/")) uri += '/'; uri += getDocumentName(document); method = new PostMethod(uri + query); // Set user agent if it's specified. String agentName = getUserAgentName(); if (agentName != null) { String agentVersion = getUserAgentVersion(); if (agentVersion != null) agentName += " " + agentVersion; method.setRequestHeader(new Header("User-Agent", agentName)); } // the content-type charset will determine encoding used // when we set the request body method.setRequestHeader("Content-Type", getRequestProtocol().getContentType()); if (getClientIp() != null) { method.setRequestHeader(RemoteIP.X_ORIGINATING_IP_HEADER, getClientIp()); if (ZimbraLog.misc.isDebugEnabled()) { ZimbraLog.misc.debug("set remote IP header [%s] to [%s]", RemoteIP.X_ORIGINATING_IP_HEADER, getClientIp()); } } Element soapReq = generateSoapMessage(document, raw, noSession, requestedAccountId, changeToken, tokenType); String soapMessage = SoapProtocol.toString(soapReq, getPrettyPrint()); HttpMethodParams params = method.getParams(); method.setRequestEntity(new StringRequestEntity(soapMessage, null, "UTF-8")); if (getRequestProtocol().hasSOAPActionHeader()) method.setRequestHeader("SOAPAction", mUri); if (mCustomHeaders != null) { for (Map.Entry<String, String> entry : mCustomHeaders.entrySet()) method.setRequestHeader(entry.getKey(), entry.getValue()); } String host = method.getURI().getHost(); HttpState state = HttpClientUtil.newHttpState(getAuthToken(), host, this.isAdmin()); String trustedToken = getTrustedToken(); if (trustedToken != null) { state.addCookie( new Cookie(host, ZimbraCookie.COOKIE_ZM_TRUST_TOKEN, trustedToken, "/", null, false)); } params.setCookiePolicy(state.getCookies().length == 0 ? CookiePolicy.IGNORE_COOKIES : CookiePolicy.BROWSER_COMPATIBILITY); params.setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(mRetryCount - 1, true)); params.setSoTimeout(mTimeout); params.setVersion(HttpVersion.HTTP_1_1); method.setRequestHeader("Connection", mKeepAlive ? "Keep-alive" : "Close"); if (mHostConfig != null && mHostConfig.getUsername() != null && mHostConfig.getPassword() != null) { state.setProxyCredentials(new AuthScope(null, -1), new UsernamePasswordCredentials(mHostConfig.getUsername(), mHostConfig.getPassword())); } if (mHttpDebugListener != null) { mHttpDebugListener.sendSoapMessage(method, soapReq, state); } int responseCode = mClient.executeMethod(mHostConfig, method, state); // SOAP allows for "200" on success and "500" on failure; // real server issues will probably be "503" or "404" if (responseCode != HttpServletResponse.SC_OK && responseCode != HttpServletResponse.SC_INTERNAL_SERVER_ERROR) throw ServiceException.PROXY_ERROR(method.getStatusLine().toString(), uri); // Read the response body. Use the stream API instead of the byte[] // version to avoid HTTPClient whining about a large response. InputStreamReader reader = new InputStreamReader(method.getResponseBodyAsStream(), SoapProtocol.getCharset()); String responseStr = ""; try { if (respHandler != null) { respHandler.process(reader); return null; } else { responseStr = ByteUtil.getContent(reader, (int) method.getResponseContentLength(), false); Element soapResp = parseSoapResponse(responseStr, raw); if (mHttpDebugListener != null) { mHttpDebugListener.receiveSoapMessage(method, soapResp); } return soapResp; } } catch (SoapFaultException x) { // attach request/response to the exception and rethrow x.setFaultRequest(soapMessage); x.setFaultResponse(responseStr.substring(0, Math.min(10240, responseStr.length()))); throw x; } } finally { // Release the connection to the connection manager if (method != null) method.releaseConnection(); // really not necessary if running in the server because the reaper thread // of our connection manager will take care it. // if called from CLI, all connections will be closed when the CLI // exits. Leave it here anyway. if (!mKeepAlive) mClient.getHttpConnectionManager().closeIdleConnections(0); } } /** * Returns the document name. If the given document is an <tt>Envelope</tt> * element, returns the name of the first child of the <tt>Body</tt> subelement. */ private String getDocumentName(Element document) { if (document == null || document.getName() == null) { return null; } String name = document.getName(); if (name.equals("Envelope")) { Element body = document.getOptionalElement("Body"); if (body != null) { List<Element> children = body.listElements(); if (children.size() > 0) { name = children.get(0).getName(); } } } return name; } public static interface ResponseHandler { void process(Reader src) throws ServiceException; } /** * Use SAXReader to parse large soap response. caller must provide list of handlers, which are <path, handler> * pairs. To reduce memory usage, a handler may call Element.detach() in ElementHandler.onEnd() to prune off * processed elements. */ public static final class SAXResponseHandler implements ResponseHandler { private final Map<String, ElementHandler> handlers; public SAXResponseHandler(Map<String, ElementHandler> handlers) { this.handlers = handlers; } @Override public void process(Reader src) throws ServiceException { SAXReader reader; try { // This code is slower than less safe code used prior to 8.7 but appears to only be used // in ZimbraOffline. Submitted Bugzilla Bug 104175 suggesting investigating replacing use of this // code with more modern, higher performing code. reader = W3cDomUtil.getDom4jSAXReaderWhichUsesSecureProcessing(); for (Map.Entry<String, ElementHandler> entry : handlers.entrySet()) { reader.addHandler(entry.getKey(), entry.getValue()); } reader.read(src); } catch (XmlParseException | SAXException | DocumentException e) { ZimbraLog.misc.debug("Problem processing XML", e); throw ServiceException.SAX_READER_ERROR(e.getMessage(), e.getCause()); } } } }