001/**
002 * Copyright 2005-2015 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 org.apache.log4j.Logger;
019import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
020import org.kuali.rice.core.api.util.ClassLoaderUtils;
021import org.kuali.rice.core.api.util.ContextClassLoaderProxy;
022import org.kuali.rice.core.api.util.reflect.BaseTargetedInvocationHandler;
023import org.kuali.rice.ksb.api.KsbApiServiceLocator;
024import org.kuali.rice.ksb.api.bus.Endpoint;
025import org.kuali.rice.ksb.api.bus.ServiceConfiguration;
026
027import java.lang.reflect.Method;
028import java.lang.reflect.Proxy;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Set;
032
033
034
035public class BusClientFailureProxy extends BaseTargetedInvocationHandler<Object> {
036
037        private static final Logger LOG = Logger.getLogger(BusClientFailureProxy.class);
038
039    static final String SERVICE_REMOVAL_EXCEPTIONS_BEAN = "rice.ksb.serviceRemovalExceptions";
040    static final String SERVICE_REMOVAL_RESPONSE_CODES_BEAN = "rice.ksb.serviceRemovalResponseCodes";
041
042        private final Object failoverLock = new Object();
043        
044        private ServiceConfiguration serviceConfiguration;
045
046        private BusClientFailureProxy(Object target, ServiceConfiguration serviceConfiguration) {
047                super(target);
048                this.serviceConfiguration = serviceConfiguration;
049        }
050
051        public static Object wrap(Object target, ServiceConfiguration serviceConfiguration) {
052                return Proxy.newProxyInstance(ClassLoaderUtils.getDefaultClassLoader(), ContextClassLoaderProxy.getInterfacesToProxy(target), new BusClientFailureProxy(target, serviceConfiguration));
053        }
054
055        protected Object invokeInternal(Object proxyObject, Method method, Object[] params) throws Throwable {
056                Set<ServiceConfiguration> servicesTried = null;
057                
058                do {
059                        try {
060                                return method.invoke(getTarget(), params);
061                        } catch (Throwable throwable) {                 
062                                if (isServiceRemovalException(throwable)) {
063                                        synchronized (failoverLock) {
064                        LOG.error("Exception caught accessing remote service " + this.serviceConfiguration.getServiceName() + " at " + this.serviceConfiguration.getEndpointUrl(), throwable);
065                        if (servicesTried == null) {
066                                                        servicesTried = new HashSet<ServiceConfiguration>();
067                                                        servicesTried.add(serviceConfiguration);
068                                                }
069                                                Object failoverService = null;
070                                                List<Endpoint> endpoints = KsbApiServiceLocator.getServiceBus().getEndpoints(serviceConfiguration.getServiceName(), serviceConfiguration.getApplicationId());
071                                                for (Endpoint endpoint : endpoints) {
072                                                        if (!servicesTried.contains(endpoint.getServiceConfiguration())) {
073                                                                failoverService = endpoint.getService();
074                                if(Proxy.isProxyClass(failoverService.getClass()) && Proxy.getInvocationHandler(failoverService) instanceof BusClientFailureProxy) {
075                                    failoverService = ((BusClientFailureProxy)Proxy.getInvocationHandler(failoverService)).getTarget();
076                                }
077                                                                servicesTried.add(endpoint.getServiceConfiguration());
078                                break; // KULRICE-8728: BusClientFailureProxy doesn't try all endpoint options
079                                                        }
080                                                }                                                                       
081                                                if (failoverService != null) {
082                            LOG.info("Refetched replacement service for service " + this.serviceConfiguration.getServiceName() + " at " + this.serviceConfiguration.getEndpointUrl());
083                            // as per KULRICE-4287, reassign target to the new service we just fetched, hopefully this one works better!
084                                                        setTarget(failoverService);
085                                                } else {
086                                                        LOG.error("Didn't find replacement service throwing exception");
087                                                        throw throwable;                                        
088                                                }
089                                        }
090                                } else {
091                                        throw throwable;
092                                }
093                        }
094                } while (true);
095        }
096
097        private static boolean isServiceRemovalException(Throwable throwable) {
098                LOG.info("Checking for Service Removal Exception: " + throwable.getClass().getName());
099                if (getServiceRemovalExceptions().contains(throwable.getClass())) {
100                        LOG.info("Found a Service Removal Exception: " + throwable.getClass().getName());
101                        return true;
102                } else if (throwable instanceof org.kuali.rice.ksb.messaging.HttpException) {
103                        org.kuali.rice.ksb.messaging.HttpException httpException = (org.kuali.rice.ksb.messaging.HttpException)throwable;
104                        if (getServiceRemovalResponseCodes().contains(httpException.getResponseCode())) {
105                                LOG.info("Found a Service Removal Exception because of a " + httpException.getResponseCode() + " " + throwable.getClass().getName());
106                                return true;
107                        }
108                } else if (throwable instanceof org.apache.cxf.transport.http.HTTPException) {
109                        org.apache.cxf.transport.http.HTTPException httpException = (org.apache.cxf.transport.http.HTTPException)throwable;
110                        if (getServiceRemovalResponseCodes().contains(httpException.getResponseCode())) {
111                                LOG.info("Found a Service Removal Exception because of a " + httpException.getResponseCode() + " " + throwable.getClass().getName());
112                                return true;
113                        }
114                }
115                if (throwable.getCause() != null) {
116                        LOG.info("Unwrapping Throwable cause to check for service removal exception from: " + throwable.getClass().getName());
117                        return isServiceRemovalException(throwable.getCause());
118                }
119                return false;
120        }
121
122    /**
123     * Lazy initialization holder class idiom for static fields, see Effective Java item 71
124     */
125    private static class ServiceRemovalExceptionsHolder {
126        static final List<Class<?>> serviceRemovalExceptions =
127                GlobalResourceLoader.getService(SERVICE_REMOVAL_EXCEPTIONS_BEAN);
128    }
129
130    /**
131     * Get the list of exception classes that are considered service removal exceptions.
132     *
133     * <p>These are the exceptions that, when thrown from a service proxy, indicate that we should fail over to the
134     * next available endpoint for that service and remove the failing endpoint from the bus.</p>
135     *
136     * <p>On first call, the bean specified by {@link #SERVICE_REMOVAL_EXCEPTIONS_BEAN} will be lazily assigned and
137     * used.</p>
138     */
139    private static List<Class<?>> getServiceRemovalExceptions() {
140        return ServiceRemovalExceptionsHolder.serviceRemovalExceptions;
141    }
142
143    /**
144     * Lazy initialization holder class idiom for static fields, see Effective Java item 71
145     */
146    private static class ServiceRemovalResponseCodesHolder {
147        static final List<Integer> serviceRemovalResponseCodes =
148                GlobalResourceLoader.getService(SERVICE_REMOVAL_RESPONSE_CODES_BEAN);
149    }
150
151    /**
152     * Get the list of HTTP response codes that indicate the need to fail over.
153     *
154     * <p>These are the response codes that, when detected within an exception thrown by a service proxy, indicate
155     * that we should fail over to the next available endpoint for that service and remove the failing endpoint from
156     * the bus.</p>
157     *
158     * <p>On first call, the bean specified by {@link #SERVICE_REMOVAL_RESPONSE_CODES_BEAN} will be lazily assigned and
159     * used.</p>
160     */
161    private static List<Integer> getServiceRemovalResponseCodes() {
162        return ServiceRemovalResponseCodesHolder.serviceRemovalResponseCodes;
163    }
164
165}