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 }