View Javadoc
1   /* Copyright (2005-2008) Schibsted Søk AS
2    * This file is part of Sesat Commons.
3    *
4    *   Sesat Commons is free software: you can redistribute it and/or modify
5    *   it under the terms of the GNU Lesser General Public License as published by
6    *   the Free Software Foundation, either version 3 of the License, or
7    *   (at your option) any later version.
8    *
9    *   Sesat Commons is distributed in the hope that it will be useful,
10   *   but WITHOUT ANY WARRANTY; without even the implied warranty of
11   *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   *   GNU Lesser General Public License for more details.
13   *
14   *   You should have received a copy of the GNU Lesser General Public License
15   *   along with Sesat Commons.  If not, see <http://www.gnu.org/licenses/>.
16   *
17   * AbstractReflectionVisitor.java
18   *
19   * Created on 7 January 2006, 16:12
20   *
21   */
22  
23  package no.sesat.commons.visitor;
24  
25  import java.lang.ref.Reference;
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Method;
28  import java.util.Map;
29  import java.util.concurrent.ConcurrentHashMap;
30  
31  import no.sesat.commons.ref.ReferenceMap;
32  import org.apache.log4j.Logger;
33  
34  
35  /** A helper implementation of the Visitor pattern using java's reflection.
36   * This results in not having to add overloaded methods for each subclass of clause as this implementation will
37   * automatically find those overloaded methods without explicitly having to call them in each Clause class.
38   * This saves alot of work when adding new Clause subclasses.
39   *
40   * The overloaded method name is specified by VISIT_METHOD_IMPL.
41   *
42   * See http://www.javaworld.com/javaworld/javatips/jw-javatip98.html
43   *
44   * @version $Id: AbstractReflectionVisitor.java 1127 2009-01-21 16:16:08Z ssmiweve $
45   *
46   */
47  public abstract class AbstractReflectionVisitor implements Visitor {
48  
49      /** String specifying name of method used to overload by any class extending this. **/
50      public static final String VISIT_METHOD_IMPL = "visitImpl";
51  
52      private static final Logger LOG = Logger.getLogger(AbstractReflectionVisitor.class);
53  
54      private static final String ERR_CLAUSE_SUBTYPE_NOT_FOUND = "Current visitor implementation does not handle visiting "
55              + "non clause subtypes. Tried to visit object ";
56      private static final String ERR_FAILED_TO_VISIT = "Failed to visit object ";
57      private static final String ERR_FAILED_TO_FIND_VISIT_IMPL_OBJECT = "Failed to find method that exists in this class!!"
58              + "Was trying to visit object ";
59      private static final String DEBUG_LOOKING_AT = "Looking for method "
60              + VISIT_METHOD_IMPL + "(";
61      private static final String TRACE_KEEP_LOOKING = "keep looking";
62      private static final String RB = ")";
63  
64      private static final ReferenceMap<Class<? extends Visitor>,Map<Class<? extends Visitable>,Method>> CACHE =
65              new ReferenceMap<Class<? extends Visitor>,Map<Class<? extends Visitable>,Method>>(
66              ReferenceMap.Type.SOFT,
67              new ConcurrentHashMap<Class<? extends Visitor>,Reference<Map<Class<? extends Visitable>,Method>>>());
68  
69  
70      /** Creates a new instance of AbstractReflectionVisitor.
71       */
72      public AbstractReflectionVisitor() {
73      }
74  
75      /**
76       * Method implementing Visitor interface. Uses reflection to find the method with name VISIT_METHOD_IMPL with the
77       * closest match to the clause subclass.
78       *
79       * We CACHE the methods used by the visitor. This is done in a soft ReferenceMap since the skins can
80       * be reloaded, and then we would have had a memory leak.
81       *
82       * @param clause the clause we're visiting.
83       */
84      public void visit(final Visitable clause) {
85  
86          Map<Class<? extends Visitable>,Method> map = CACHE.get(getClass());
87  
88          if (null == map) {
89  
90              map = new ConcurrentHashMap<Class<? extends Visitable>,Method>();
91  
92              CACHE.put(getClass(), map);
93          }
94  
95          Method method = map.get(clause.getClass());
96          if (null == method) {
97              method = getMethod(clause.getClass());
98              method.setAccessible(true);
99              map.put(clause.getClass(), method);
100         }
101         assert method.equals(getMethod(clause.getClass()));
102         try {
103             method.invoke(this, new Object[] {clause});
104 
105         } catch (IllegalArgumentException ex) {
106             LOG.error(ERR_FAILED_TO_VISIT + clause, ex);
107 
108         } catch (InvocationTargetException ex) {
109             LOG.error(ERR_FAILED_TO_VISIT + clause, ex);
110             // IllegalArgumentException often mean an underlying exception.
111             //   If the underlying exception has a blank stacktrace it's most likely a sun bug
112             //   when running hotspot compiled methods http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4966410
113             //   The JVM flag -XX:-OmitStackTraceInFastThrow fixes it.
114             for (Throwable t = ex; t != null; t = t.getCause()) {
115                 LOG.error(t.getMessage(), t);
116             }
117 
118         } catch (IllegalAccessException ex) {
119             LOG.error(ERR_FAILED_TO_VISIT + clause, ex);
120         }
121     }
122 
123     /**
124      * Final fallback method. This means that the object being visited is not a Clause (or subclass of) object!
125      * This behaviour is not intendedly supported and this implementation throws an IllegalArgumentException!
126      * @param clause the clause we're visiting (that's not acutally a clause subtype ;)
127      */
128     protected void visitImpl(final Object clause) {
129         throw new IllegalArgumentException(ERR_CLAUSE_SUBTYPE_NOT_FOUND + clause.getClass().getName());
130     }
131 
132     private Method getMethod(final Class clauseClass) {
133         Method method = null;
134 
135         LOG.trace("getMethod(" + clauseClass.getName() + ")");
136 
137         // Try the superclasses
138         Class currClauseClass = clauseClass;
139         while (method == null && currClauseClass != Object.class) {
140             LOG.trace(DEBUG_LOOKING_AT + currClauseClass.getName() + RB);
141 
142                 method = getDeclaredMethod(currClauseClass);
143 
144             if (method == null) {
145                 currClauseClass = currClauseClass.getSuperclass();
146             }
147         }
148 
149         // Try the interfaces.
150         // Gets alittle bit tricky because we must not only search subinterfaces
151         //  but search both interfaces and superinterfaces of superclasses...
152         currClauseClass = clauseClass;
153         while (method == null && currClauseClass != Object.class) {
154 
155             method = getMethodFromInterface(currClauseClass);
156             currClauseClass = currClauseClass.getSuperclass();
157         }
158 
159         // fallback to visitImpl(Object)
160         if (method == null) {
161 
162             method = getDeclaredMethod(Object.class);
163 
164             if (method == null) {
165                 LOG.fatal(ERR_FAILED_TO_FIND_VISIT_IMPL_OBJECT + clauseClass.getName());
166             }
167 
168         }
169         LOG.trace("end getMethod(" + clauseClass.getName() + ")");
170         return method;
171     }
172 
173     /** The interfaces in this array will already be in a suitable order.
174         According to java reflection's getMethod contract this order will match the order listed in the
175         implements(/extends) definition of the Clause subclass.
176      **/
177     private Method getMethodFromInterface(final Class clauseClass) {
178 
179         Method method = null;
180 
181         LOG.trace("getMethodFromInterface(" + clauseClass.getName() + ")");
182 
183         final Class[] interfaces = clauseClass.getInterfaces();
184         for (int i = 0; i < interfaces.length && method == null; i++) {
185 
186             LOG.trace(DEBUG_LOOKING_AT + interfaces[i].getName() + RB);
187 
188 
189             method = getDeclaredMethod(interfaces[i]);
190 
191 
192             if (method == null) {
193                 // [RECURSION] Look for super interfaces
194                 method = getMethodFromInterface(interfaces[i]);
195             }  else  {
196                 // This is the most useful log statement in this file,
197                 //  but gets called too many times per request to be promoted to debug.
198                 LOG.trace("Found method accepting <" + interfaces[i].getSimpleName()
199                         + "> in " + method.getDeclaringClass().getSimpleName());
200             }
201         }
202 
203         LOG.trace("end getMethodFromInterface(" + clauseClass.getName() + ")");
204         return method;
205     }
206 
207     /** Because Class.getDeclaredMethod(..) behaves differently to getMethod(..) in
208      * that it does not look into superclasses we must manually look through the superclass
209      * heirarchy. We don't want to use getMethod(..) either because it will only return public methods,
210      * and we would like our visitImpl methods to remain private/protected.
211      **/
212     private Method getDeclaredMethod(final Class clauseClass) {
213 
214         for (Class cls = getClass();; cls = cls.getSuperclass()) {
215             if (cls != null) {
216                 try {
217                     return cls.getDeclaredMethod(VISIT_METHOD_IMPL, new Class[] {clauseClass});
218 
219                 } catch (NoSuchMethodException e) {
220                     LOG.trace(TRACE_KEEP_LOOKING);
221                 }
222             }  else  {
223                 return null;
224             }
225         }
226     }
227 
228 }