/* * EuroCarbDB, a framework for carbohydrate bioinformatics * * Copyright (c) 2006-2009, Eurocarb project, or third-party contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * A copy of this license accompanies this distribution in the file LICENSE.txt. * * This program 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. * * Last commit: $Rev: 1231 $ by $Author: glycoslave $ on $Date:: 2009-06-19 #$ */ package org.eurocarbdb.util; import java.util.Map; import java.util.HashMap; import java.lang.reflect.Method; import java.lang.reflect.InvocationTargetException; import org.apache.log4j.Logger; /** *<p> * Implementation of the <em>Visitor pattern</em>, which uses reflection * rather than a Visitable interface to perform the double-dispatch * that is necessary to traverse arbitrarily complex data structures (which * is the point of the Visitor pattern). *</p> *<p> * To use this class to traverse a data structure, add a version of the * method <code>public void accept(XXX x)</code> for each class/interface * XXX that you are interested in visiting. Traversal is initiated * by calling {@link #visit visit(Object)} on the top-level element. Similarly, * each accept method is free to visit other objects from itself as * required, and Visitor classes can of course have state and be sub-classed * in the usual manner. *</p> *<h3>Example</h3> * *<pre> class A {} class B extends A {} interface I extends SomeInterface {} class MyVisitor extends Visitor { public void accept( A a ) { // do something with A... } public void accept( B b ) { // do something with B... } public void accept( I i ) { // do something with I... } public void accept( Collection c ) { for ( Object item : collection) visit( item ); } public static void main( String[] args ) { Object complex_data_structure = ... Visitor v = new MyVisitor(); v.visit( complex_data_structure ); } } *</pre> *<p> * Note that the call to <tt>visit</tt> follows the normal Java rules * for method selection - if an <tt>accept</tt> method for a given object's * class cannot be found, then an <tt>accept</tt> method for its superclass * will be used, or if there are no accept methods for any superclass, then * accept methods for the implemented interfaces of that object will match, * then their superinterfaces, etc. *</p> *<p> * Note also that <tt>accept</tt> methods MUST be <tt>public</tt>, or they * will not get visited. The classes that declare accept methods however, * do not need to be public. *</p> * *<h3>Performance</h3> *<p> * Here are some performance metrics taken from the unit test of this class * comparing performance of this class versus a tradition Visitable Visitor * implementation: *<pre> * timing 100 iteration(s): * reflection visitor took 2 msec * traditional visitor took 1 msec * * timing 10000 iteration(s): * reflection visitor took 25 msec * traditional visitor took 15 msec *</pre> * So, it's approximately 2 times slower, but for all intents and purposes, * the performance difference is negligible compared to the work done in * accept(Xxx) methods, and the reflection-based Visitor (ie: this class) * has the tremendous benefit that every class that requires visiting * does not need to implement a Visitable interface nor some arbitrary * <code>accept( Visitor v ) { v.visit(this); }</code> method. *</p> *<p> * Finally, a single visitor can be used to traverse many data structures, * (provided any recorded state is reset as appropriate). *</p> * * @see <a href="http://en.wikipedia.org/wiki/Visitor_pattern">Visitor pattern</a> * @author mjh */ public abstract class Visitor { /** Inheritable logging handle. */ protected static final Logger log = Logger.getLogger( Visitor.class ); /** Controls compile-time addition/removal of debugging calls */ private static final boolean DEBUG = false; /** Lookup cache of visit method references for various classes. * This is to avoid the high cost of using reflection for method discovery. */ private final Map<Class<?>,Method> cache = new HashMap<Class<?>,Method>(); /** * This is the main method for dynamic dispatch to other * <tt>accept(XXX x)</tt> methods based on the class of <tt>x</tt>. */ public final void visit( Object object_to_visit ) { if ( object_to_visit == null ) return; Class<?> c = object_to_visit.getClass(); // lookup accept method in cache first, saves using reflection more // than what we have to. a method value of null means there's no // suitable accept method to handle the given Object argument. Method m = null; if ( cache.containsKey( c ) ) { m = cache.get( c ); } else { // find a suitable accept method. // returned method can be null, which means 'no method'. m = getVisitMethodForClass( c ); if ( DEBUG ) log.debug("caching " + m + " for " + c ); synchronized ( cache ) { cache.put( c, m ); } // allows us to call accept methods in non-public classes. if ( m != null ) m.setAccessible( true ); } if ( m == null ) { if ( DEBUG ) log.debug("no matching accept methods for " + c + ", skipping" ); return; } try { if ( DEBUG ) log.debug("invoking " + m ); m.invoke( this, object_to_visit ); } // this should only occur if subclass implementors are silly // enough to make their accept methods private or package private // and therefore inaccessible. catch ( IllegalAccessException ex ) { log.warn( "Caught IllegalAccessException while invoking " + m.toString() + ". This most likely cause is that either the class in which " + "this method is declared, or the method itself, is not public." , ex ); throw new RuntimeException( ex ); } // this exception is received if the method invoked throws an // exception -- report it catch ( InvocationTargetException ex ) { log.warn( "Caught InvocationTargetException while invoking " + m.toString() , ex ); throw new RuntimeException( ex ); } } /** * Recurses through all superclasses & interfaces of passed Class * looking for a suitable accept( object of Class c ) method. * This method uses reflection and is, consequently, slow as shit. * Calls to it are minimised by caching the return value in * {@link #cache}. * @return a suitable accept method for objects of the given Class, * or null if no suitable method could be found. */ private final Method getVisitMethodForClass( Class c ) { if ( DEBUG ) log.debug("looking for accept method for class " + c.getSimpleName() + ")" ); Method m = null; try { m = this.getClass().getMethod( "accept", c ); if ( DEBUG ) log.debug("found method: " + m ); return m; } catch ( NoSuchMethodException ignore ) {} // search for a visit method that handles superclasses of c, recursively if ( ! c.isInterface() ) { Class s = c.getSuperclass(); if ( s != null && s != Object.class ) { if ( DEBUG ) log.debug("trying superclass " + s.getSimpleName() ); m = getVisitMethodForClass( s ); if ( m != null ) return m; } } // search for a visit method that handles interfaces of c, recursively for ( Class s : c.getInterfaces() ) { if ( DEBUG ) log.debug("trying interface " + s ); m = getVisitMethodForClass( s ); if ( m != null ) return m; } try { m = this.getClass().getDeclaredMethod( "accept", Object.class ); if ( DEBUG ) log.debug("found accept( Object ): " + m ); return m; } catch ( NoSuchMethodException ignore ) {} return null; } // this isn't any faster than recursive method... /* private final Method getVisitMethodForClass( Class<?> c ) { if ( DEBUG ) log.debug("looking for accept method for " + c.getSimpleName() ); Method m; try { m = this.getClass().getMethod( "accept", c ); if ( DEBUG ) log.debug("found accept( " + m.getParameterTypes()[0].getSimpleName() + ")" ); return m; } catch ( NoSuchMethodException ignore ) {} // look for method: accept( superclass ) for ( Class<?> s = c.getSuperclass(); s != Object.class; s = s.getSuperclass() ) { if ( DEBUG ) log.debug("searching superclass " + s.getSimpleName() ); try { m = this.getClass().getMethod( "accept", s ); if ( DEBUG ) log.debug("found accept( " + m.getParameterTypes()[0].getSimpleName() + ")" ); return m; } catch ( NoSuchMethodException ignore ) {} } // look for method: accept( interface ) for ( Class<?> i : c.getInterfaces() ) { if ( DEBUG ) log.debug("searching interface " + i.getSimpleName() ); try { m = this.getClass().getMethod( "accept", i ); if ( DEBUG ) log.debug("found accept( " + m.getParameterTypes()[0].getSimpleName() + ")" ); return m; } catch ( NoSuchMethodException ignore ) {} } // finally, look for method: accept( Object ) try { m = this.getClass().getMethod( "accept", Object.class ); if ( DEBUG ) log.debug("found method for Object.class" ); return m; } catch ( NoSuchMethodException ignore ) {} return null; } */ // even with extra caching, this is slowest of all... /* private final Method getVisitMethodForClass2( Class<?> c ) { if ( DEBUG ) log.debug("looking for accept method for " + c.getSimpleName() ); if ( cache.containsKey( c ) ) return cache.get( c ); Method m; // try // { // m = this.getClass().getDeclaredMethod( "accept", c ); // if ( DEBUG ) log.debug("found accept( " + m.getParameterTypes()[0].getSimpleName() + ")" ); // cache( c, m ); // return m; // } // catch ( NoSuchMethodException ignore ) {} // look for method: accept( superclass ) for ( Class<?> s = c.getSuperclass(); s != Object.class; s = s.getSuperclass() ) { if ( cache.containsKey( s ) ) return cache.get( s ); if ( DEBUG ) log.debug("searching superclass " + s.getSimpleName() ); try { m = this.getClass().getDeclaredMethod( "accept", s ); if ( DEBUG ) log.debug("found accept( " + m.getParameterTypes()[0].getSimpleName() + ")" ); cache( c, m ); cache( s, m ); return m; } catch ( NoSuchMethodException ignore ) {} } // look for method: accept( interface ) for ( Class<?> i : c.getInterfaces() ) { if ( cache.containsKey( i ) ) return cache.get( i ); if ( DEBUG ) log.debug("searching interface " + i.getSimpleName() ); try { m = this.getClass().getDeclaredMethod( "accept", i ); if ( DEBUG ) log.debug("found accept( " + m.getParameterTypes()[0].getSimpleName() + ")" ); cache( c, m ); cache( i, m ); return m; } catch ( NoSuchMethodException ignore ) {} } // finally, look for method: accept( Object ) if ( cache.containsKey( Object.class ) ) return cache.get( Object.class ); try { m = this.getClass().getDeclaredMethod( "accept", Object.class ); if ( DEBUG ) log.debug("found method for Object.class" ); cache( Object.class, m ); return m; } catch ( NoSuchMethodException ignore ) { cache( Object.class, null ); } cache( c, null ); return null; } */ } // end class Visitor