001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.ksb.messaging.serviceconnectors;
017
018import java.io.IOException;
019import java.net.SocketException;
020import java.net.SocketTimeoutException;
021import java.net.URL;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.Map;
025import java.util.Properties;
026
027import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
028import org.apache.commons.httpclient.HostConfiguration;
029import org.apache.commons.httpclient.HttpClient;
030import org.apache.commons.httpclient.HttpMethod;
031import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
032import org.apache.commons.httpclient.cookie.CookiePolicy;
033import org.apache.commons.httpclient.params.HttpClientParams;
034import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
035import org.apache.commons.httpclient.params.HttpConnectionParams;
036import org.apache.commons.httpclient.params.HttpMethodParams;
037import org.apache.commons.httpclient.params.HttpParams;
038import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
039import org.apache.commons.lang.StringUtils;
040import org.apache.log4j.Logger;
041import org.kuali.rice.core.api.config.property.ConfigContext;
042import org.kuali.rice.ksb.api.bus.support.JavaServiceConfiguration;
043import org.kuali.rice.ksb.messaging.HttpClientHelper;
044import org.kuali.rice.ksb.messaging.KSBHttpInvokerProxyFactoryBean;
045import org.kuali.rice.ksb.messaging.KSBHttpInvokerRequestExecutor;
046import org.kuali.rice.ksb.security.httpinvoker.AuthenticationCommonsHttpInvokerRequestExecutor;
047
048
049/**
050 * @author Kuali Rice Team (rice.collab@kuali.org)
051 * @since 0.9
052 */
053public 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}