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    }