001 /** 002 * Copyright 2005-2012 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.rice.ksb.messaging.serviceconnectors; 017 018 import java.io.IOException; 019 import java.net.SocketException; 020 import java.net.SocketTimeoutException; 021 import java.net.URL; 022 import java.util.HashMap; 023 import java.util.Iterator; 024 import java.util.Map; 025 import java.util.Properties; 026 027 import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler; 028 import org.apache.commons.httpclient.HostConfiguration; 029 import org.apache.commons.httpclient.HttpClient; 030 import org.apache.commons.httpclient.HttpMethod; 031 import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; 032 import org.apache.commons.httpclient.cookie.CookiePolicy; 033 import org.apache.commons.httpclient.params.HttpClientParams; 034 import org.apache.commons.httpclient.params.HttpConnectionManagerParams; 035 import org.apache.commons.httpclient.params.HttpConnectionParams; 036 import org.apache.commons.httpclient.params.HttpMethodParams; 037 import org.apache.commons.httpclient.params.HttpParams; 038 import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread; 039 import org.apache.commons.lang.StringUtils; 040 import org.apache.log4j.Logger; 041 import org.kuali.rice.core.api.config.property.ConfigContext; 042 import org.kuali.rice.ksb.api.bus.support.JavaServiceConfiguration; 043 import org.kuali.rice.ksb.messaging.HttpClientHelper; 044 import org.kuali.rice.ksb.messaging.KSBHttpInvokerProxyFactoryBean; 045 import org.kuali.rice.ksb.messaging.KSBHttpInvokerRequestExecutor; 046 import org.kuali.rice.ksb.security.httpinvoker.AuthenticationCommonsHttpInvokerRequestExecutor; 047 048 049 /** 050 * @author Kuali Rice Team (rice.collab@kuali.org) 051 * @since 0.9 052 */ 053 public class HttpInvokerConnector extends AbstractServiceConnector { 054 055 private static final Logger LOG = Logger.getLogger(HttpInvokerConnector.class); 056 057 private HttpClientParams httpClientParams; 058 059 private boolean httpClientInitialized = false; 060 061 private static final String IDLE_CONNECTION_THREAD_INTERVAL_PROPERTY = "ksb.thinClient.idleConnectionThreadInterval"; 062 private static final String IDLE_CONNECTION_TIMEOUT_PROPERTY = "ksb.thinClient.idleConnectionTimeout"; 063 private static final String DEFAULT_IDLE_CONNECTION_THREAD_INTERVAL = "7500"; 064 private static final String DEFAULT_IDLE_CONNECTION_TIMEOUT = "5000"; 065 private static final String RETRY_SOCKET_EXCEPTION_PROPERTY = "ksb.thinClient.retrySocketException"; 066 067 private static IdleConnectionTimeoutThread ictt; 068 069 public HttpInvokerConnector(final JavaServiceConfiguration serviceConfiguration, final URL alternateEndpointUrl) { 070 super(serviceConfiguration, alternateEndpointUrl); 071 initializeHttpClientParams(); 072 } 073 074 @Override 075 public JavaServiceConfiguration getServiceConfiguration() { 076 return (JavaServiceConfiguration) super.getServiceConfiguration(); 077 } 078 079 public Object getService() { 080 LOG.debug("Getting connector for endpoint " + getActualEndpointUrl()); 081 KSBHttpInvokerProxyFactoryBean client = new KSBHttpInvokerProxyFactoryBean(); 082 client.setServiceUrl(getActualEndpointUrl().toExternalForm()); 083 client.setServiceConfiguration(getServiceConfiguration()); 084 085 KSBHttpInvokerRequestExecutor executor; 086 087 if (getCredentialsSource() != null) { 088 executor = new AuthenticationCommonsHttpInvokerRequestExecutor(getHttpClient(), getCredentialsSource(), getServiceConfiguration()); 089 } else { 090 executor = new KSBHttpInvokerRequestExecutor(getHttpClient()); 091 } 092 executor.setSecure(getServiceConfiguration().getBusSecurity()); 093 client.setHttpInvokerRequestExecutor(executor); 094 client.afterPropertiesSet(); 095 return getServiceProxyWithFailureMode(client.getObject(), getServiceConfiguration()); 096 } 097 098 /** 099 * Creates a commons HttpClient for service invocation. Config parameters 100 * that start with http.* are used to configure the client. 101 * 102 * TODO we need to add support for other invocation protocols and 103 * implementations, but for now... 104 */ 105 public HttpClient getHttpClient() { 106 return new HttpClient(this.httpClientParams); 107 } 108 109 protected void initializeHttpClientParams() { 110 synchronized (HttpInvokerConnector.class) { 111 if (! this.httpClientInitialized) { 112 this.httpClientParams = new HttpClientParams(); 113 configureDefaultHttpClientParams(this.httpClientParams); 114 Properties configProps = ConfigContext.getCurrentContextConfig().getProperties(); 115 for (Iterator<Object> iterator = configProps.keySet().iterator(); iterator.hasNext();) { 116 String paramName = (String) iterator.next(); 117 if (paramName.startsWith("http.")) { 118 HttpClientHelper.setParameter(this.httpClientParams, paramName, (String) configProps.get(paramName)); 119 } 120 } 121 runIdleConnectionTimeout(); 122 this.httpClientInitialized = true; 123 } 124 } 125 } 126 127 protected void configureDefaultHttpClientParams(HttpParams params) { 128 params.setParameter(HttpClientParams.CONNECTION_MANAGER_CLASS, MultiThreadedHttpConnectionManager.class); 129 params.setParameter(HttpMethodParams.COOKIE_POLICY, CookiePolicy.RFC_2109); 130 params.setLongParameter(HttpClientParams.CONNECTION_MANAGER_TIMEOUT, 10000); 131 Map<HostConfiguration, Integer> maxHostConnectionsMap = new HashMap<HostConfiguration, Integer>(); 132 maxHostConnectionsMap.put(HostConfiguration.ANY_HOST_CONFIGURATION, new Integer(20)); 133 params.setParameter(HttpConnectionManagerParams.MAX_HOST_CONNECTIONS, maxHostConnectionsMap); 134 params.setIntParameter(HttpConnectionManagerParams.MAX_TOTAL_CONNECTIONS, 20); 135 params.setIntParameter(HttpConnectionParams.CONNECTION_TIMEOUT, 10000); 136 params.setIntParameter(HttpConnectionParams.SO_TIMEOUT, 2*60*1000); 137 138 139 boolean retrySocketException = new Boolean(ConfigContext.getCurrentContextConfig().getProperty(RETRY_SOCKET_EXCEPTION_PROPERTY)); 140 if (retrySocketException) { 141 LOG.info("Installing custom HTTP retry handler to retry requests in face of SocketExceptions"); 142 params.setParameter(HttpMethodParams.RETRY_HANDLER, new CustomHttpMethodRetryHandler()); 143 } 144 145 146 } 147 148 149 150 /** 151 * Idle connection timeout thread added as a part of the fix for ensuring that 152 * threads that timed out need to be cleaned or and send back to the pool so that 153 * other clients can use it. 154 * 155 */ 156 private void runIdleConnectionTimeout() { 157 if (ictt != null) { 158 String timeoutInterval = ConfigContext.getCurrentContextConfig().getProperty(IDLE_CONNECTION_THREAD_INTERVAL_PROPERTY); 159 if (StringUtils.isBlank(timeoutInterval)) { 160 timeoutInterval = DEFAULT_IDLE_CONNECTION_THREAD_INTERVAL; 161 } 162 String connectionTimeout = ConfigContext.getCurrentContextConfig().getProperty(IDLE_CONNECTION_TIMEOUT_PROPERTY); 163 if (StringUtils.isBlank(connectionTimeout)) { 164 connectionTimeout = DEFAULT_IDLE_CONNECTION_TIMEOUT; 165 } 166 167 ictt.addConnectionManager(getHttpClient().getHttpConnectionManager()); 168 ictt.setTimeoutInterval(new Integer(timeoutInterval)); 169 ictt.setConnectionTimeout(new Integer(connectionTimeout)); 170 //start the thread 171 ictt.start(); 172 } 173 } 174 175 public static void shutdownIdleConnectionTimeout() { 176 if (ictt != null) { 177 try { 178 ictt.shutdown(); 179 } catch (Exception e) { 180 LOG.error("Failed to shutdown idle connection thread.", e); 181 } 182 } 183 } 184 185 private static final class CustomHttpMethodRetryHandler extends DefaultHttpMethodRetryHandler { 186 187 private static final int MAX_RETRIES = 1; 188 189 public CustomHttpMethodRetryHandler() { 190 super(MAX_RETRIES, true); 191 } 192 193 @Override 194 public boolean retryMethod(HttpMethod method, IOException exception, int executionCount) { 195 boolean shouldRetry = super.retryMethod(method, exception, executionCount); 196 if (!shouldRetry && executionCount < MAX_RETRIES) { 197 if (exception instanceof SocketException) { 198 LOG.warn("Retrying request because of SocketException!", exception); 199 shouldRetry = true; 200 } else if (exception instanceof SocketTimeoutException) { 201 LOG.warn("Retrying request because of SocketTimeoutException!", exception); 202 shouldRetry = true; 203 } 204 } 205 return shouldRetry; 206 } 207 208 } 209 210 211 212 }