/* * ALMA - Atacama Large Millimiter Array * (c) European Southern Observatory, 2002 * Copyright by ESO (in the framework of the ALMA collaboration), * All rights reserved * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA */ package alma.acs.container; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.omg.CORBA.DATA_CONVERSION; import org.omg.CORBA.SystemException; import org.omg.CORBA.UserException; import alma.ACS.ACSComponentOperations; import alma.JavaContainerError.wrappers.AcsJContainerEx; import alma.acs.component.dynwrapper.DynWrapperException; import alma.acs.container.corba.CorbaNullFinder; import alma.acs.exceptions.CorbaExceptionConverter; import alma.acs.logging.AcsLogLevel; import alma.acs.monitoring.DynamicInterceptor; import alma.acs.monitoring.DynamicInterceptor.InterceptionHandler; import alma.acs.monitoring.DynamicInterceptor.InterceptionHandlerFactory; import alma.acs.util.StopWatch; /** * Seals the container to make it a "tight container", as opposed to an "open container". * There's one sealant instance per component instance. The sealant is placed between the CORBA POATie * on the one side, and the component implementation class or the component interface translator * (if present) on the other side. The sealant therefore intercepts all functional calls made * to the component. * <p> * This sealant class is not only used for components, but also for offshoots from that component, * if they follow the tie-approach. Since ACS 9.0 it uses {@link DynamicInterceptor}. * * @author hsommer */ public class ContainerSealant { /** * If this property is enabled, the container will check the return value and out/inout parameters * for illegal null values that will cause exceptions during subsequent corba marshalling. */ public static final String CHECK_NULLS_CORBA_OUT_PROPERTYNAME = "alma.acs.container.check_nulls_corba_out"; /** * Creates a ContainerSealant and uses it as the invocation handler for the returned dynamic proxy * which implements <code>corbaInterface</code>. * * @param corbaInterface the interface that the created sealant will implement; * this should be the component's or offshoot's xxxOperations interface. * @param component the component/offshoot implementation class, or any translator class * in front of the component implementation. * <code>componentImpl</code> must implement <code>corbaInterface</code> * so that the sealant can forward calls to the component. <br> * Note about usage of java generics: Ideally this would be declared "T" instead of "Object", * but did not get it to work with that... * @param name the component instance name (used for logging) * @param isOffShoot true if the <code>component</code> object is actually an offshoot of a component * @param logger logger to be used by this class * @param componentContextCL classloader used for {@link Thread#setContextClassLoader(java.lang.ClassLoader setContextClassLoader)} * before component method gets invoked. (after the call, the old context CL will be restored.) * @param methodNamesExcludedFromInvocationLogging * @return an instance of <code>corbaInterface</code> that intercepts calls to * <code>componentImpl</code> and forwards them if restrictions allow this. * @throws ContainerException if the given <code>component</code> Object does not implement the given <code>corbaInterface</code>. */ public static <T> T createContainerSealant(Class<T> corbaInterface, Object component, String name, boolean isOffShoot, Logger logger, ClassLoader componentContextCL, String[] methodNamesExcludedFromInvocationLogging ) throws AcsJContainerEx { if (!corbaInterface.isInstance(component)) { AcsJContainerEx ex = new AcsJContainerEx(); ex.setContextInfo("sealant factory: component " + component.getClass().getName() + " must implement the sealant interface " + corbaInterface.getClass().getName()); throw ex; } InterceptionHandlerFactory interceptionHandlerFactory = new ComponentInterceptionHandlerFactory(name, isOffShoot, logger, methodNamesExcludedFromInvocationLogging); return DynamicInterceptor.createDynamicInterceptor(corbaInterface, component, logger, componentContextCL, interceptionHandlerFactory); } ////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////// Callbacks for Dynamic Interceptor ////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// /** * The handler created for every call to a component or offshoot (with lightweight implementation). * Will log method invocation and return. */ private static class ComponentInterceptionHandler implements DynamicInterceptor.InterceptionHandler { private final String name; private final Logger logger; private final Set<String> methodNamesExcludedFromInvocationLogging; private StopWatch methodInvWatch; /** * The level at which we log the intercepted call. */ private final Level logLevel; /** * Whether the call should be logged (because of level or suppressed logging for the given interface method). */ private boolean isLoggable = false; /** * The method that was called */ private Method method; ComponentInterceptionHandler(String name, boolean isOffShoot, Logger logger, Set<String> methodNamesExcludedFromInvocationLogging) { this.name = name; this.logger = logger; this.methodNamesExcludedFromInvocationLogging = methodNamesExcludedFromInvocationLogging; // TODO: Now that component interception logs have been demoted from DEBUG to DELOUSE, // shouldn't we also use DELOUSE for offshoot interception logs? Historically it is TRACE // but also at that time we did not have the DELOUSE level available. this.logLevel = ( isOffShoot ? AcsLogLevel.TRACE : AcsLogLevel.DELOUSE ); } @Override public boolean callReceived(Method method, Object[] args) { this.method = method; isLoggable = ( !isExcludedFromInvocationLogging(method.getName()) && logger.isLoggable(logLevel) ); if (isLoggable) { String qualMethodName = name + "#" + method.getName(); methodInvWatch = new StopWatch(logger); logger.log(logLevel, "intercepted a call to '" + qualMethodName + "'."); } return true; } @Override public Object callFinished(Object retVal, Object[] args, Throwable realThr) throws Throwable { // log invocation time String qualMethodName = name + "#" + method.getName(); if (isLoggable) { logger.log(logLevel, "returning from " + qualMethodName + " after " + methodInvWatch.getLapTimeMillis() + " ms."); } if (realThr != null) { if (realThr instanceof UserException) { // Any thrown Corba user exception should be declared in the IDL, but we better check! boolean declared = false; Class<?>[] declaredExceptions = method.getExceptionTypes(); for (int i = 0; i < declaredExceptions.length; i++) { if (declaredExceptions[i] == realThr.getClass()) { declared = true; break; } } String msg = (declared ? "checked exception " : "unchecked user exception (should have been declared in IDL!) "); msg += "was thrown in functional method '" + qualMethodName + "':"; Level exLogLevel = (declared ? logLevel : Level.WARNING); // log the exception including an optional embedded ErrorTrace (as chain of AcsJxxx exceptions), see COMP-3654 Throwable thrExpanded = CorbaExceptionConverter.convertHiddenErrorTrace(realThr); logger.log(exLogLevel, msg, thrExpanded); throw realThr; } else if (realThr instanceof DynWrapperException) { String msg = "the dynamic xml entity translator failed with the functional method '" + qualMethodName + "': "; logger.log(Level.SEVERE, msg, realThr); // we slightly extend the foreseen usage of this CORBA system exception, which according to the spec should be // "raised if an ORB cannot convert the representation of data as marshaled into its native representation or vice-versa" throw new DATA_CONVERSION(msg + realThr.toString()); } else { logger.log(Level.WARNING, "Unexpected exception was thrown in functional method '" + qualMethodName + "': ", realThr); if (realThr instanceof SystemException) { throw realThr; } else { // Wrapping our ex with CORBA.UNKNOWN changes the subsequent log from jacorb from Emergency to Info level. // The client would in any case see an UNKNOWN ex. See http://jira.alma.cl/browse/COMP-1846. throw new org.omg.CORBA.UNKNOWN(realThr.toString()); } } } if (Boolean.getBoolean(CHECK_NULLS_CORBA_OUT_PROPERTYNAME)) { try { // check return value Class<?> clzzRet = method.getReturnType(); if (!Void.TYPE.equals(clzzRet) && !CorbaNullFinder.isIDLInterfaceClass(clzzRet)) { CorbaNullFinder finder = new CorbaNullFinder(retVal); if (finder.hasErrors()) { List<String> errors = finder.getErrors(); StringBuilder sb = new StringBuilder(); for (String errorline : errors) { sb.append(errorline).append("\n"); } logger.warning("Illegal null value returned by method " + method.getName() + ":\n" + sb.toString()); } } // check out or inout parameters Class<?>[] argsClasses = method.getParameterTypes(); StringBuilder sb = new StringBuilder(); for (int argIndex = 0; argIndex < argsClasses.length; argIndex++) { Class<?> clzzOutParam = argsClasses[argIndex]; if (clzzOutParam.getSimpleName().endsWith("Holder") && !CorbaNullFinder.isIDLInterfaceClass(clzzOutParam) ) { CorbaNullFinder finder = new CorbaNullFinder(args[argIndex]); if (finder.hasErrors()) { List<String> errors = finder.getErrors(); sb.append(" Parameter " + clzzOutParam.getSimpleName() + ": \n"); for (String errorline : errors) { sb.append(" ").append(errorline).append("\n"); } } } } String paramErrors = sb.toString(); if (!paramErrors.isEmpty()) { logger.warning("Illegal null value in out parameter(s) of method " + method.getName() + ":\n" + paramErrors); } } catch (Exception ex) { logger.log(Level.FINE, "Failed to check returned data for illegal nulls.", ex); } } return retVal; } /** * Checks if an invoked method is known to be excluded from logging. * This is always true for {@link ACSComponentOperations#componentState() componentState}, * and for all methods that are set for no-logging. */ private boolean isExcludedFromInvocationLogging(String methodName) { return (methodNamesExcludedFromInvocationLogging.contains(methodName)); } } /** * */ private static class ComponentInterceptionHandlerFactory implements DynamicInterceptor.InterceptionHandlerFactory { private final String name; private final boolean isOffShoot; private final Logger logger; private final Set<String> methodNamesExcludedFromInvocationLogging; ComponentInterceptionHandlerFactory(String name, boolean isOffShoot, Logger logger, String[] excludedMethods) { this.name = name; this.isOffShoot = isOffShoot; this.logger = logger; methodNamesExcludedFromInvocationLogging = new HashSet<String>(2); setExcludedMethods(excludedMethods); } /** * Processes and stores the excludedMethods (taken out from constructor) */ private void setExcludedMethods(String[] excludedMethods) { // see comment for method 'isExcludedFromInvocationLogging' methodNamesExcludedFromInvocationLogging.add("componentState"); if (excludedMethods != null) { try { if (isOffShoot) { // strip off component name from qualified offshoot name String actualInterfaceName = name.substring(name.lastIndexOf('/')+1); // we must filter out the qualified method names that apply to our given offshoot class // (there may be methods for other offshoots produced by the same component) for (int i = 0; i < excludedMethods.length; i++) { String qualExclMethodName = excludedMethods[i]; if (qualExclMethodName.startsWith("OFFSHOOT::")) { int methodNamePosition = qualExclMethodName.indexOf('#') + 1; if (methodNamePosition > 0) { String interfaceName = qualExclMethodName.substring("OFFSHOOT::".length(), methodNamePosition -1); if (interfaceName.equals(actualInterfaceName)) { String methodName = qualExclMethodName.substring(methodNamePosition); methodNamesExcludedFromInvocationLogging.add(methodName); } } } } } else { // for components, we get the list of method names directly and don't have to check if they apply this.methodNamesExcludedFromInvocationLogging.addAll(Arrays.asList(excludedMethods)); } if (logger.isLoggable(Level.FINE) && !methodNamesExcludedFromInvocationLogging.isEmpty()) { StringBuffer buff = new StringBuffer(100); buff.append("Container will not log invocations of the following methods of "); buff.append(isOffShoot ? "offshoot '" : "component '"); buff.append(name).append("' :"); for (Iterator<String> iter = methodNamesExcludedFromInvocationLogging.iterator(); iter.hasNext();) { String methodName = iter.next(); buff.append(methodName); if (iter.hasNext()) { buff.append(", "); } } logger.fine(buff.toString()); } } catch (Exception ex) { // logging a warning is enough (error will just result in unwanted future logging and thus does not warrant a failure here). logger.log(Level.WARNING, "failed to exclude certain methods of '" + name + "' from future automatic invocation logging.", ex); } } } @Override public InterceptionHandler createInterceptionHandler() { return new ComponentInterceptionHandler(name, isOffShoot, logger, methodNamesExcludedFromInvocationLogging); } } }