View Javadoc

1   /*
2    * Copyright 2006-2011 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.kuali.rice.kew.config;
18  
19  
20  import java.io.IOException;
21  import java.net.SocketException;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.Map;
26  import java.util.Properties;
27  
28  import javax.xml.namespace.QName;
29  
30  import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
31  import org.apache.commons.httpclient.HostConfiguration;
32  import org.apache.commons.httpclient.HttpClient;
33  import org.apache.commons.httpclient.HttpMethod;
34  import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
35  import org.apache.commons.httpclient.cookie.CookiePolicy;
36  import org.apache.commons.httpclient.params.HttpClientParams;
37  import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
38  import org.apache.commons.httpclient.params.HttpMethodParams;
39  import org.apache.commons.httpclient.params.HttpParams;
40  import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
41  import org.apache.commons.lang.StringUtils;
42  import org.kuali.rice.core.api.config.CoreConfigHelper;
43  import org.kuali.rice.core.api.config.property.ConfigContext;
44  import org.kuali.rice.core.framework.resourceloader.BaseResourceLoader;
45  import org.kuali.rice.kew.util.KEWConstants;
46  import org.kuali.rice.kim.api.group.GroupService;
47  import org.kuali.rice.kim.api.identity.IdentityService;
48  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
49  import org.kuali.rice.ksb.messaging.HttpClientHelper;
50  import org.kuali.rice.ksb.messaging.KSBHttpInvokerRequestExecutor;
51  import org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean;
52  
53  
54  /**
55   * Initializes and loads webservice resources for the Embedded plugin.
56   * Currently, the only 2 services which are exposed are the utility service and
57   * the document actions service.
58   *
59   * @author Kuali Rice Team (rice.collab@kuali.org)
60   */
61  public class ThinClientResourceLoader extends BaseResourceLoader {
62  		private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ThinClientResourceLoader.class);
63  
64      	private static final String DEFAULT_MAX_CONNECTIONS = "40";
65      	private static final String DEFAULT_CONNECTION_TIMEOUT = "60000";
66      	private static final String DEFAULT_CONNECTION_MANAGER_TIMEOUT = "60000";
67      	public static final String MAX_CONNECTIONS = "kew." + HttpConnectionManagerParams.MAX_TOTAL_CONNECTIONS; // kew.http.connection-manager.max-total
68      	public static final String CONNECTION_TIMEOUT = "kew." + HttpConnectionManagerParams.CONNECTION_TIMEOUT; // kew.http.connection.timeout
69      	public static final String CONNECTION_MANAGER_TIMEOUT = "kew." + HttpClientParams.CONNECTION_MANAGER_TIMEOUT; // kew.http.connection-manager.timeout
70      	public static final String DOCUMENT_ENDPOINT = "workflowdocument.javaservice.endpoint";
71      	public static final String SECURE_DOCUMENT_ENDPOINT = "secure.workflowdocument.javaservice.endpoint";
72      	public static final String UTILITY_ENDPOINT = "workflowutility.javaservice.endpoint";
73      	public static final String SECURE_UTILITY_ENDPOINT = "secure.workflowutility.javaservice.endpoint";
74      	public static final String IDENTITY_ENDPOINT = "identity.javaservice.endpoint"; 
75      	public static final String SECURE_IDENTITY_ENDPOINT = "secure.identity.javaservice.endpoint"; 
76      	public static final String GROUP_ENDPOINT = "group.javaservice.endpoint"; 
77      	public static final String SECURE_GROUP_ENDPOINT = "secure.group.javaservice.endpoint"; 
78      	
79      	
80      	private static final String IDLE_CONNECTION_THREAD_INTERVAL_PROPERTY = "ksb.thinClient.idleConnectionThreadInterval";
81      	private static final String IDLE_CONNECTION_TIMEOUT_PROPERTY = "ksb.thinClient.idleConnectionTimeout";
82      	private static final String DEFAULT_IDLE_CONNECTION_THREAD_INTERVAL = "7500";
83      	private static final String DEFAULT_IDLE_CONNECTION_TIMEOUT = "5000";
84      	private static final String RETRY_SOCKET_EXCEPTION_PROPERTY = "ksb.thinClient.retrySocketException";
85      	
86      	private Map<String, Object> services = Collections.synchronizedMap(new HashMap<String, Object>());
87  
88      	private IdleConnectionTimeoutThread ictt;
89      	
90  	public ThinClientResourceLoader() {
91  		super(new QName(CoreConfigHelper.getApplicationId(), "ThinClientResourceLoader"));
92  		ictt = new IdleConnectionTimeoutThread();
93  	}
94  
95  	@Override
96  	public void start() throws Exception {
97  		super.start();
98  		initializeHttpClientParams();
99  		runIdleConnectionTimeout();
100 		//springLifecycle.start();
101 	}
102 
103 
104 
105 	@Override
106 	public void stop() throws Exception {
107 		super.stop();
108 		if (ictt != null) {
109 		    ictt.shutdown();
110 		}
111 		//springLifecycle.stop();
112 	}
113 
114 	public Object getService(QName serviceQName) {
115 	    String serviceName = serviceQName.getLocalPart();
116 	    	Object cachedService = services.get(serviceName);
117 	    	if (cachedService != null) {
118 	    	    return cachedService;
119 	    	}
120 		if (serviceName.equals(KEWConstants.WORKFLOW_UTILITY_SERVICE)) {
121 		    throw new UnsupportedOperationException("Reimplement me! - see KULRICE-5061");
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(KimApiServiceLocator.KIM_IDENTITY_SERVICE)) {
130 			IdentityService identityService = getIdentityService();
131 			services.put(serviceName, identityService);
132 			return identityService;
133 		} else if (serviceName.equals(KimApiServiceLocator.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 }