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;
017
018import java.io.InterruptedIOException;
019import java.lang.reflect.Method;
020import java.lang.reflect.Proxy;
021import java.net.ConnectException;
022import java.net.NoRouteToHostException;
023import java.net.SocketTimeoutException;
024import java.net.UnknownHostException;
025import java.util.ArrayList;
026import java.util.HashSet;
027import java.util.List;
028import java.util.Set;
029
030import org.apache.commons.httpclient.ConnectTimeoutException;
031import org.apache.commons.httpclient.ConnectionPoolTimeoutException;
032import org.apache.commons.httpclient.NoHttpResponseException;
033import org.apache.log4j.Logger;
034import org.kuali.rice.core.api.util.ClassLoaderUtils;
035import org.kuali.rice.core.api.util.ContextClassLoaderProxy;
036import org.kuali.rice.core.api.util.reflect.BaseTargetedInvocationHandler;
037import org.kuali.rice.ksb.api.KsbApiServiceLocator;
038import org.kuali.rice.ksb.api.bus.Endpoint;
039import org.kuali.rice.ksb.api.bus.ServiceConfiguration;
040
041
042
043public class BusClientFailureProxy extends BaseTargetedInvocationHandler {
044
045        private static final Logger LOG = Logger.getLogger(BusClientFailureProxy.class);
046
047        private final Object failoverLock = new Object();
048        
049        private ServiceConfiguration serviceConfiguration;
050
051        // exceptions that will cause this Proxy to remove the service from the bus
052        private static List<Class<?>> serviceRemovalExceptions = new ArrayList<Class<?>>();
053        private static List<Integer> serviceRemovalResponseCodes = new ArrayList<Integer>();
054
055        static {
056                serviceRemovalExceptions.add(NoHttpResponseException.class);
057                serviceRemovalExceptions.add(InterruptedIOException.class);
058                serviceRemovalExceptions.add(UnknownHostException.class);
059                serviceRemovalExceptions.add(NoRouteToHostException.class);
060                serviceRemovalExceptions.add(ConnectTimeoutException.class);
061                serviceRemovalExceptions.add(ConnectionPoolTimeoutException.class);
062                serviceRemovalExceptions.add(ConnectException.class);
063        serviceRemovalExceptions.add(SocketTimeoutException.class);
064    }
065        
066        static {
067            serviceRemovalResponseCodes.add(new Integer(404));
068        serviceRemovalResponseCodes.add(new Integer(503));
069        }
070        
071        private BusClientFailureProxy(Object target, ServiceConfiguration serviceConfiguration) {
072                super(target);
073                this.serviceConfiguration = serviceConfiguration;
074        }
075
076        public static Object wrap(Object target, ServiceConfiguration serviceConfiguration) {
077                return Proxy.newProxyInstance(ClassLoaderUtils.getDefaultClassLoader(), ContextClassLoaderProxy.getInterfacesToProxy(target), new BusClientFailureProxy(target, serviceConfiguration));
078        }
079
080        protected Object invokeInternal(Object proxyObject, Method method, Object[] params) throws Throwable {
081                Set<ServiceConfiguration> servicesTried = null;
082                
083                do {
084                        try {
085                                return method.invoke(getTarget(), params);
086                        } catch (Throwable throwable) {                 
087                                if (isServiceRemovalException(throwable)) {
088                                        synchronized (failoverLock) {
089                        LOG.error("Exception caught accessing remote service " + this.serviceConfiguration.getServiceName() + " at " + this.serviceConfiguration.getEndpointUrl(), throwable);
090                        if (servicesTried == null) {
091                                                        servicesTried = new HashSet<ServiceConfiguration>();
092                                                        servicesTried.add(serviceConfiguration);
093                                                }
094                                                Object failoverService = null;
095                                                List<Endpoint> endpoints = KsbApiServiceLocator.getServiceBus().getEndpoints(serviceConfiguration.getServiceName(), serviceConfiguration.getApplicationId());
096                                                for (Endpoint endpoint : endpoints) {
097                                                        if (!servicesTried.contains(endpoint.getServiceConfiguration())) {
098                                                                failoverService = endpoint.getService();
099                                if(Proxy.isProxyClass(failoverService.getClass()) && Proxy.getInvocationHandler(failoverService) instanceof BusClientFailureProxy) {
100                                    failoverService = ((BusClientFailureProxy)Proxy.getInvocationHandler(failoverService)).getTarget();
101                                }
102                                                                servicesTried.add(endpoint.getServiceConfiguration());
103                                break; // KULRICE-8728: BusClientFailureProxy doesn't try all endpoint options
104                                                        }
105                                                }                                                                       
106                                                if (failoverService != null) {
107                            LOG.info("Refetched replacement service for service " + this.serviceConfiguration.getServiceName() + " at " + this.serviceConfiguration.getEndpointUrl());
108                            // as per KULRICE-4287, reassign target to the new service we just fetched, hopefully this one works better!
109                                                        setTarget(failoverService);
110                                                } else {
111                                                        LOG.error("Didn't find replacement service throwing exception");
112                                                        throw throwable;                                        
113                                                }
114                                        }
115                                } else {
116                                        throw throwable;
117                                }
118                        }
119                } while (true);
120        }
121
122        private static boolean isServiceRemovalException(Throwable throwable) {
123                LOG.info("Checking for Service Removal Exception: " + throwable.getClass().getName());
124                if (serviceRemovalExceptions.contains(throwable.getClass())) {
125                        LOG.info("Found a Service Removal Exception: " + throwable.getClass().getName());
126                        return true;
127                } else if (throwable instanceof org.kuali.rice.ksb.messaging.HttpException) {
128                        org.kuali.rice.ksb.messaging.HttpException httpException = (org.kuali.rice.ksb.messaging.HttpException)throwable;
129                        if (serviceRemovalResponseCodes.contains(httpException.getResponseCode())) {
130                                LOG.info("Found a Service Removal Exception because of a " + httpException.getResponseCode() + " " + throwable.getClass().getName());
131                                return true;
132                        }
133                } else if (throwable instanceof org.apache.cxf.transport.http.HTTPException) {
134                        org.apache.cxf.transport.http.HTTPException httpException = (org.apache.cxf.transport.http.HTTPException)throwable;
135                        if (serviceRemovalResponseCodes.contains(httpException.getResponseCode())) {
136                                LOG.info("Found a Service Removal Exception because of a " + httpException.getResponseCode() + " " + throwable.getClass().getName());
137                                return true;
138                        }
139                }
140                if (throwable.getCause() != null) {
141                        LOG.info("Unwrapping Throwable cause to check for service removal exception from: " + throwable.getClass().getName());
142                        return isServiceRemovalException(throwable.getCause());
143                }
144                return false;
145        }
146
147}