001 /*
002 * Copyright 2005-2007 The Kuali Foundation
003 *
004 *
005 * Licensed under the Educational Community License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 * http://www.opensource.org/licenses/ecl2.php
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.kuali.rice.kew.config;
018
019
020 import java.io.IOException;
021 import java.net.SocketException;
022 import java.util.Collections;
023 import java.util.HashMap;
024 import java.util.Iterator;
025 import java.util.Map;
026 import java.util.Properties;
027
028 import javax.xml.namespace.QName;
029
030 import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
031 import org.apache.commons.httpclient.HostConfiguration;
032 import org.apache.commons.httpclient.HttpClient;
033 import org.apache.commons.httpclient.HttpMethod;
034 import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
035 import org.apache.commons.httpclient.cookie.CookiePolicy;
036 import org.apache.commons.httpclient.params.HttpClientParams;
037 import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
038 import org.apache.commons.httpclient.params.HttpMethodParams;
039 import org.apache.commons.httpclient.params.HttpParams;
040 import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
041 import org.apache.commons.lang.StringUtils;
042 import org.kuali.rice.core.config.ConfigContext;
043 import org.kuali.rice.core.resourceloader.BaseResourceLoader;
044 import org.kuali.rice.kew.service.WorkflowDocumentActions;
045 import org.kuali.rice.kew.service.WorkflowUtility;
046 import org.kuali.rice.kew.util.KEWConstants;
047 import org.kuali.rice.kim.service.GroupService;
048 import org.kuali.rice.kim.service.IdentityService;
049 import org.kuali.rice.kim.service.KIMServiceLocator;
050 import org.kuali.rice.ksb.messaging.HttpClientHelper;
051 import org.kuali.rice.ksb.messaging.KSBHttpInvokerRequestExecutor;
052 import org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean;
053
054
055 /**
056 * Initializes and loads webservice resources for the Embedded plugin.
057 * Currently, the only 2 services which are exposed are the utility service and
058 * the document actions service.
059 *
060 * @author Kuali Rice Team (rice.collab@kuali.org)
061 */
062 public class ThinClientResourceLoader extends BaseResourceLoader {
063 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ThinClientResourceLoader.class);
064
065 private static final String DEFAULT_MAX_CONNECTIONS = "40";
066 private static final String DEFAULT_CONNECTION_TIMEOUT = "60000";
067 private static final String DEFAULT_CONNECTION_MANAGER_TIMEOUT = "60000";
068 public static final String MAX_CONNECTIONS = "kew." + HttpConnectionManagerParams.MAX_TOTAL_CONNECTIONS; // kew.http.connection-manager.max-total
069 public static final String CONNECTION_TIMEOUT = "kew." + HttpConnectionManagerParams.CONNECTION_TIMEOUT; // kew.http.connection.timeout
070 public static final String CONNECTION_MANAGER_TIMEOUT = "kew." + HttpClientParams.CONNECTION_MANAGER_TIMEOUT; // kew.http.connection-manager.timeout
071 public static final String DOCUMENT_ENDPOINT = "workflowdocument.javaservice.endpoint";
072 public static final String SECURE_DOCUMENT_ENDPOINT = "secure.workflowdocument.javaservice.endpoint";
073 public static final String UTILITY_ENDPOINT = "workflowutility.javaservice.endpoint";
074 public static final String SECURE_UTILITY_ENDPOINT = "secure.workflowutility.javaservice.endpoint";
075 public static final String IDENTITY_ENDPOINT = "identity.javaservice.endpoint";
076 public static final String SECURE_IDENTITY_ENDPOINT = "secure.identity.javaservice.endpoint";
077 public static final String GROUP_ENDPOINT = "group.javaservice.endpoint";
078 public static final String SECURE_GROUP_ENDPOINT = "secure.group.javaservice.endpoint";
079
080
081 private static final String IDLE_CONNECTION_THREAD_INTERVAL_PROPERTY = "ksb.thinClient.idleConnectionThreadInterval";
082 private static final String IDLE_CONNECTION_TIMEOUT_PROPERTY = "ksb.thinClient.idleConnectionTimeout";
083 private static final String DEFAULT_IDLE_CONNECTION_THREAD_INTERVAL = "7500";
084 private static final String DEFAULT_IDLE_CONNECTION_TIMEOUT = "5000";
085 private static final String RETRY_SOCKET_EXCEPTION_PROPERTY = "ksb.thinClient.retrySocketException";
086
087 private Map<String, Object> services = Collections.synchronizedMap(new HashMap<String, Object>());
088
089 private IdleConnectionTimeoutThread ictt;
090
091 public ThinClientResourceLoader() {
092 super(new QName(ConfigContext.getCurrentContextConfig().getServiceNamespace(), "ThinClientResourceLoader"));
093 ictt = new IdleConnectionTimeoutThread();
094 }
095
096 @Override
097 public void start() throws Exception {
098 super.start();
099 initializeHttpClientParams();
100 runIdleConnectionTimeout();
101 //springLifecycle.start();
102 }
103
104
105
106 @Override
107 public void stop() throws Exception {
108 super.stop();
109 if (ictt != null) {
110 ictt.shutdown();
111 }
112 //springLifecycle.stop();
113 }
114
115 public Object getService(QName serviceQName) {
116 String serviceName = serviceQName.getLocalPart();
117 Object cachedService = services.get(serviceName);
118 if (cachedService != null) {
119 return cachedService;
120 }
121 if (serviceName.equals(KEWConstants.WORKFLOW_UTILITY_SERVICE)) {
122 WorkflowUtility utility = getWorkflowUtility();
123 services.put(serviceName, utility);
124 return utility;
125 } else if (serviceName.equals(KEWConstants.WORKFLOW_DOCUMENT_ACTIONS_SERVICE)) {
126 WorkflowDocumentActions documentActions = getWorkflowDocument();
127 services.put(serviceName, documentActions);
128 return documentActions;
129 } else if (serviceName.equals(KIMServiceLocator.KIM_IDENTITY_SERVICE)) {
130 IdentityService identityService = getIdentityService();
131 services.put(serviceName, identityService);
132 return identityService;
133 } else if (serviceName.equals(KIMServiceLocator.KIM_GROUP_SERVICE)) {
134 GroupService groupService = getGroupService();
135 services.put(serviceName, groupService);
136 return groupService;
137 }
138 return null;
139 }
140
141 public WorkflowUtility getWorkflowUtility() {
142 return (WorkflowUtility)getServiceProxy(WorkflowUtility.class, UTILITY_ENDPOINT, SECURE_UTILITY_ENDPOINT);
143 }
144
145 public WorkflowDocumentActions getWorkflowDocument() {
146 return (WorkflowDocumentActions)getServiceProxy(WorkflowDocumentActions.class, DOCUMENT_ENDPOINT, SECURE_DOCUMENT_ENDPOINT);
147 }
148
149 public IdentityService getIdentityService() {
150 return (IdentityService)getServiceProxy(IdentityService.class, IDENTITY_ENDPOINT, SECURE_IDENTITY_ENDPOINT);
151 }
152
153 public GroupService getGroupService() {
154 return (GroupService)getServiceProxy(GroupService.class, GROUP_ENDPOINT, SECURE_GROUP_ENDPOINT);
155 }
156
157 protected Object getServiceProxy(Class serviceInterface, String endpointParam, String secureEndpointParam) {
158 HttpInvokerProxyFactoryBean proxyFactory = new HttpInvokerProxyFactoryBean();
159 String serviceUrl = ConfigContext.getCurrentContextConfig().getProperty(endpointParam);
160 if (StringUtils.isEmpty(serviceUrl)) {
161 throw new IllegalArgumentException("The " + endpointParam + " configuration parameter was not defined but is required.");
162 }
163 proxyFactory.setServiceUrl(serviceUrl);
164 proxyFactory.setServiceInterface(serviceInterface);
165 String secureProp = ConfigContext.getCurrentContextConfig().getProperty(secureEndpointParam);
166 Boolean secureIt = null;
167 secureIt = secureProp == null || Boolean.valueOf(secureProp);
168 KSBHttpInvokerRequestExecutor executor = new KSBHttpInvokerRequestExecutor(getHttpClient());
169 executor.setSecure(secureIt);
170 proxyFactory.setHttpInvokerRequestExecutor(executor);
171 proxyFactory.afterPropertiesSet();
172 return proxyFactory.getObject();
173 }
174
175 /*
176 * the below code copied from RemoteResourceServiceLocator
177 */
178
179 private HttpClientParams httpClientParams;
180
181 /**
182 * Creates a commons HttpClient for service invocation. Config parameters
183 * that start with http.* are used to configure the client.
184 *
185 * TODO we need to add support for other invocation protocols and
186 * implementations, but for now...
187 */
188 protected HttpClient getHttpClient() {
189 return new HttpClient(httpClientParams);
190 }
191
192 protected void initializeHttpClientParams() {
193 httpClientParams = new HttpClientParams();
194 configureDefaultHttpClientParams(httpClientParams);
195 Properties configProps = ConfigContext.getCurrentContextConfig().getProperties();
196 for (Iterator iterator = configProps.keySet().iterator(); iterator.hasNext();) {
197 String paramName = (String) iterator.next();
198 if (paramName.startsWith("http.")) {
199 HttpClientHelper.setParameter(httpClientParams, paramName, (String) configProps.get(paramName));
200 }
201 }
202
203 String maxConnectionsValue = configProps.getProperty(MAX_CONNECTIONS);
204 if (!StringUtils.isEmpty(maxConnectionsValue)) {
205 Integer maxConnections = new Integer(maxConnectionsValue);
206 Map<HostConfiguration, Integer> maxHostConnectionsMap = new HashMap<HostConfiguration, Integer>();
207 maxHostConnectionsMap.put(HostConfiguration.ANY_HOST_CONFIGURATION, maxConnections);
208 httpClientParams.setParameter(HttpConnectionManagerParams.MAX_HOST_CONNECTIONS, maxHostConnectionsMap);
209 httpClientParams.setIntParameter(HttpConnectionManagerParams.MAX_TOTAL_CONNECTIONS, maxConnections);
210 }
211
212 String connectionManagerTimeoutValue = configProps.getProperty(CONNECTION_MANAGER_TIMEOUT);
213 if (!StringUtils.isEmpty(connectionManagerTimeoutValue)) {
214 httpClientParams.setLongParameter(HttpClientParams.CONNECTION_MANAGER_TIMEOUT, new Long(connectionManagerTimeoutValue));
215 }
216
217 String connectionTimeoutValue = configProps.getProperty(CONNECTION_TIMEOUT);
218 if (!StringUtils.isEmpty(connectionTimeoutValue)) {
219 httpClientParams.setIntParameter(HttpConnectionManagerParams.CONNECTION_TIMEOUT, new Integer(connectionTimeoutValue));
220 }
221 }
222
223 protected void configureDefaultHttpClientParams(HttpParams params) {
224 params.setParameter(HttpClientParams.CONNECTION_MANAGER_CLASS, MultiThreadedHttpConnectionManager.class);
225 params.setParameter(HttpMethodParams.COOKIE_POLICY, CookiePolicy.RFC_2109);
226 params.setLongParameter(HttpClientParams.CONNECTION_MANAGER_TIMEOUT, new Long(DEFAULT_CONNECTION_MANAGER_TIMEOUT));
227 Map<HostConfiguration, Integer> maxHostConnectionsMap = new HashMap<HostConfiguration, Integer>();
228 maxHostConnectionsMap.put(HostConfiguration.ANY_HOST_CONFIGURATION, new Integer(DEFAULT_MAX_CONNECTIONS));
229 params.setParameter(HttpConnectionManagerParams.MAX_HOST_CONNECTIONS, maxHostConnectionsMap);
230 params.setIntParameter(HttpConnectionManagerParams.MAX_TOTAL_CONNECTIONS, new Integer(DEFAULT_MAX_CONNECTIONS));
231 params.setIntParameter(HttpConnectionManagerParams.CONNECTION_TIMEOUT, new Integer(DEFAULT_CONNECTION_TIMEOUT));
232
233 boolean retrySocketException = new Boolean(ConfigContext.getCurrentContextConfig().getProperty(RETRY_SOCKET_EXCEPTION_PROPERTY));
234 if (retrySocketException) {
235 LOG.info("Installing custom HTTP retry handler to retry requests in face of SocketExceptions");
236 params.setParameter(HttpMethodParams.RETRY_HANDLER, new CustomHttpMethodRetryHandler());
237 }
238 }
239
240 /**
241 * Idle connection timeout thread added as a part of the fix for ensuring that
242 * threads that timed out need to be cleaned or and send back to the pool so that
243 * other clients can use it.
244 *
245 */
246 private void runIdleConnectionTimeout() {
247 if (ictt != null) {
248 String timeoutInterval = ConfigContext.getCurrentContextConfig().getProperty(IDLE_CONNECTION_THREAD_INTERVAL_PROPERTY);
249 if (StringUtils.isBlank(timeoutInterval)) {
250 timeoutInterval = DEFAULT_IDLE_CONNECTION_THREAD_INTERVAL;
251 }
252 String connectionTimeout = ConfigContext.getCurrentContextConfig().getProperty(IDLE_CONNECTION_TIMEOUT_PROPERTY);
253 if (StringUtils.isBlank(connectionTimeout)) {
254 connectionTimeout = DEFAULT_IDLE_CONNECTION_TIMEOUT;
255 }
256
257 ictt.addConnectionManager(getHttpClient().getHttpConnectionManager());
258 ictt.setTimeoutInterval(new Integer(timeoutInterval));
259 ictt.setConnectionTimeout(new Integer(connectionTimeout));
260 //start the thread
261 ictt.start();
262 }
263 }
264
265 private static final class CustomHttpMethodRetryHandler extends DefaultHttpMethodRetryHandler {
266
267 @Override
268 public boolean retryMethod(HttpMethod method, IOException exception, int executionCount) {
269 boolean shouldRetry = super.retryMethod(method, exception, executionCount);
270 if (!shouldRetry && exception instanceof SocketException) {
271 LOG.warn("Retrying request because of SocketException!", exception);
272 shouldRetry = true;
273 }
274 return shouldRetry;
275 }
276
277 }
278
279 }