001 /**
002 * Copyright 2005-2013 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 }