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    }