CreoleAnnotationHandler.java
001 /*
002  *  CreoleAnnotationHandler.java
003  *
004  *  Copyright (c) 1995-2010, The University of Sheffield. See the file
005  *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
006  *
007  *  This file is part of GATE (see http://gate.ac.uk/), and is free
008  *  software, licenced under the GNU Library General Public License,
009  *  Version 2, June 1991 (in the distribution as file licence.html,
010  *  and also available at http://gate.ac.uk/gate/licence.html).
011  *
012  *  Ian Roberts, 27/Jul/2008
013  *
014  *  $Id: CreoleAnnotationHandler.java 13384 2011-01-31 14:02:28Z ian_roberts $
015  */
016 
017 package gate.creole;
018 
019 import gate.Gate;
020 import gate.Gate.DirectoryInfo;
021 import gate.Gate.ResourceInfo;
022 import gate.creole.metadata.AutoInstance;
023 import gate.creole.metadata.AutoInstanceParam;
024 import gate.creole.metadata.CreoleParameter;
025 import gate.creole.metadata.CreoleResource;
026 import gate.creole.metadata.GuiType;
027 import gate.creole.metadata.HiddenCreoleParameter;
028 import gate.creole.metadata.Optional;
029 import gate.creole.metadata.RunTime;
030 import gate.util.GateException;
031 import gate.util.Err;
032 
033 import java.lang.reflect.Method;
034 import java.lang.reflect.ParameterizedType;
035 import java.lang.reflect.Type;
036 import java.lang.reflect.TypeVariable;
037 import java.net.MalformedURLException;
038 import java.net.URL;
039 import java.io.IOException;
040 import java.util.Collection;
041 import java.util.HashMap;
042 import java.util.List;
043 import java.util.Map;
044 
045 import org.jdom.Document;
046 import org.jdom.Element;
047 
048 /**
049  * Class to take a creole.xml file (as a JDOM tree) and add elements
050  * corresponding to the CREOLE annotations on the RESOURCE classes it
051  * declares.
052  */
053 public class CreoleAnnotationHandler {
054 
055   private URL creoleFileUrl;
056   
057   /**
058    * Create an annotation handler for the given creole.xml file.
059    *
060    @param creoleFileUrl location of the creole.xml file.
061    */
062   public CreoleAnnotationHandler(URL creoleFileUrl) {
063     this.creoleFileUrl = creoleFileUrl;
064   }
065 
066   /**
067    * Extract all JAR elements from the given JDOM document and add the
068    * jars they reference to the GateClassLoader.
069    *
070    @param jdomDoc JDOM document representing a parsed creole.xml file.
071    */
072   public void addJarsToClassLoader(Document jdomDoc)
073           throws MalformedURLException {
074     addJarsToClassLoader(jdomDoc.getRootElement());
075   }
076 
077   /**
078    * Recursively search the given element for JAR entries and add these
079    * jars to the GateClassLoader
080    *
081    @param jdomElt JDOM element representing a creole.xml file
082    */
083   private void addJarsToClassLoader(Element jdomElt)
084           throws MalformedURLException {
085     if("JAR".equals(jdomElt.getName())) {
086       URL url = new URL(creoleFileUrl, jdomElt.getTextTrim());
087       try {
088         java.io.InputStream s = url.openStream();
089         s.close();
090         Gate.getClassLoader().addURL(url);
091       catch(IOException e) {
092         Err.println("Error opening JAR "+url+" specified in creole file "+ creoleFileUrl +": "+e);
093       }
094     }
095     else {
096       for(Element child : (List<Element>)jdomElt.getChildren()) {
097         addJarsToClassLoader(child);
098       }
099     }
100   }
101 
102   /**
103    * Fetches the directory information for this handler's creole plugin
104    * and adds additional RESOURCE elements to the given JDOM document so
105    * that it contains a RESOURCE for every resource type defined in the
106    * plugin's directory info.
107    *
108    @param jdomDoc JDOM document which should be the parsed creole.xml
109    *          that this handler was configured for.
110    */
111   public void createResourceElementsForDirInfo(Document jdomDoc)
112           throws MalformedURLException {
113     Element jdomElt = jdomDoc.getRootElement();
114     URL directoryUrl = new URL(creoleFileUrl, ".");
115     DirectoryInfo dirInfo = Gate.getDirectoryInfo(directoryUrl);
116     if(dirInfo != null) {
117       Map<String, Element> resourceElements = new HashMap<String, Element>();
118       findResourceElements(resourceElements, jdomElt);
119       for(ResourceInfo resInfo : (List<ResourceInfo>)dirInfo
120               .getResourceInfoList()) {
121         if(!resourceElements.containsKey(resInfo.getResourceClassName())) {
122           // no existing RESOURCE element for this resource type (so it
123           // was
124           // auto-discovered from a <JAR SCAN="true">), so add a minimal
125           // RESOURCE element which will be filled in by the annotation
126           // processor.
127           jdomElt.addContent(new Element("RESOURCE").addContent(new Element(
128                   "CLASS").setText(resInfo.getResourceClassName())));
129         }
130       }
131     }
132 
133   }
134 
135   private void findResourceElements(Map<String, Element> map, Element elt) {
136     if(elt.getName().equals("RESOURCE")) {
137       String className = elt.getChildTextTrim("CLASS");
138       if(className != null) {
139         map.put(className, elt);
140       }
141     }
142     else {
143       for(Element child : (List<Element>)elt.getChildren()) {
144         findResourceElements(map, child);
145       }
146     }
147   }
148 
149   /**
150    * Processes annotations for resource classes named in the given
151    * creole.xml document, adding the relevant XML elements to the
152    * document as appropriate.
153    *
154    @param jdomDoc the parsed creole.xml file
155    */
156   public void processAnnotations(Document jdomDocthrows GateException {
157     processAnnotations(jdomDoc.getRootElement());
158   }
159 
160   /**
161    * Process annotations for the given element. If the element is a
162    * RESOURCE it is processed, otherwise the method calls itself
163    * recursively for all the children of the given element.
164    *
165    @param element the element to process.
166    */
167   private void processAnnotations(Element elementthrows GateException {
168     if("RESOURCE".equals(element.getName())) {
169       processAnnotationsForResource(element);
170     }
171     else {
172       for(Element child : (List<Element>)element.getChildren()) {
173         processAnnotations(child);
174       }
175     }
176   }
177 
178   /**
179    * Process the given RESOURCE element, adding extra elements to it
180    * based on the annotations on the resource class.
181    *
182    @param element the RESOURCE element to process.
183    */
184   private void processAnnotationsForResource(Element element)
185           throws GateException {
186     String className = element.getChildTextTrim("CLASS");
187     if(className == null) {
188       throw new GateException("\"CLASS\" element not found for resource in "
189               + creoleFileUrl);
190     }
191     Class<?> resourceClass = null;
192     try {
193       resourceClass = Gate.getClassLoader().loadClass(className);
194     }
195     catch(ClassNotFoundException e) {
196       e.printStackTrace();
197       throw new GateException("Couldn't load class " + className
198               " for resource in " + creoleFileUrl);
199     }
200 
201     processCreoleResourceAnnotations(element, resourceClass);
202   }
203 
204   @SuppressWarnings("unchecked")
205   public void processCreoleResourceAnnotations(Element element,
206           Class<?> resourceClassthrows GateException {
207     CreoleResource creoleResourceAnnot = resourceClass
208             .getAnnotation(CreoleResource.class);
209     if(creoleResourceAnnot != null) {
210       // first process the top-level data and autoinstances
211       processResourceData(resourceClass, element);
212 
213       // now deal with parameters
214       // build a map holding the element corresponding to each parameter
215       Map<String, Element> parameterMap = new HashMap<String, Element>();
216       Map<String, Element> disjunctionMap = new HashMap<String, Element>();
217       for(Element paramElt : (List<Element>)element.getChildren("PARAMETER")) {
218         parameterMap.put(paramElt.getAttributeValue("NAME"), paramElt);
219       }
220       for(Element disjunctionElt : (List<Element>)element.getChildren("OR")) {
221         String disjunctionId = disjunctionElt.getAttributeValue("ID");
222         if(disjunctionId != null) {
223           disjunctionMap.put(disjunctionId, disjunctionElt);
224         }
225         for(Element paramElt : (List<Element>)disjunctionElt
226                 .getChildren("PARAMETER")) {
227           parameterMap.put(paramElt.getAttributeValue("NAME"), paramElt);
228         }
229       }
230 
231       processParameters(resourceClass, element, parameterMap, disjunctionMap);
232     }
233   }
234 
235   /**
236    * Process the {@link CreoleResource} data for this class. This method
237    * first extracts the non-inheritable data (PRIVATE, MAIN_VIEWER,
238    * NAME and TOOL), then calls {@link #processInheritableResourceData}
239    * to process the inheritable data, then deals with any specified
240    {@link AutoInstance}s.
241    *
242    @param resourceClass the resource class to process, which must be
243    *          annotated with {@link CreoleResource}.
244    @param element the RESOURCE element to which data should be added.
245    */
246   private void processResourceData(Class<?> resourceClass, Element element) {
247     CreoleResource cr = resourceClass.getAnnotation(CreoleResource.class);
248     if(cr.isPrivate() && element.getChild("PRIVATE"== null) {
249       element.addContent(new Element("PRIVATE"));
250     }
251     if(cr.mainViewer() && element.getChild("MAIN_VIEWER"== null) {
252       element.addContent(new Element("MAIN_VIEWER"));
253     }
254     if(cr.tool() && element.getChild("TOOL"== null) {
255       element.addContent(new Element("TOOL"));
256     }
257     // NAME is the name given in the annotation, or the simple name of
258     // the class if omitted
259     addElement(element, ("".equals(cr.name()))
260             ? resourceClass.getSimpleName()
261             : cr.name()"NAME");
262     processInheritableResourceData(resourceClass, element);
263     processAutoInstances(cr, element);
264   }
265 
266   /**
267    * Recursive method to process the {@link CreoleResource} elements
268    * that can be inherited from superclasses and interfaces (everything
269    * except the PRIVATE and MAIN_VIEWER flags, the NAME and the
270    * AUTOINSTANCEs). Once data has been extracted from the current class
271    * the method calls itself recursively for the superclass and any
272    * implemented interfaces. For any given attribute, the first value
273    * specified wins (i.e. the one on the most specific class).
274    *
275    @param clazz the class to process
276    @param element the RESOURCE element to which data should be added.
277    */
278   private void processInheritableResourceData(Class<?> clazz, Element element) {
279     CreoleResource cr = clazz.getAnnotation(CreoleResource.class);
280     if(cr != null) {
281       addElement(element, cr.comment()"COMMENT");
282       addElement(element, cr.helpURL()"HELPURL");
283       addElement(element, cr.interfaceName()"INTERFACE");
284       addElement(element, cr.icon()"ICON");
285       if(cr.guiType() != GuiType.NONE && element.getChild("GUI"== null) {
286         Element guiElement = new Element("GUI").setAttribute("TYPE", cr
287                 .guiType().toString());
288         element.addContent(guiElement);
289         addElement(guiElement, cr.resourceDisplayed()"RESOURCE_DISPLAYED");
290       }
291       addElement(element, cr.annotationTypeDisplayed(),
292               "ANNOTATION_TYPE_DISPLAYED");
293     }
294 
295     Class<?> superclass = clazz.getSuperclass();
296     if(superclass != null) {
297       processInheritableResourceData(superclass, element);
298     }
299 
300     for(Class<?> intf : clazz.getInterfaces()) {
301       processInheritableResourceData(intf, element);
302     }
303   }
304 
305   /**
306    * Adds an element with the given name and text value to the parent
307    * element, but only if no such child element already exists and the
308    * value is not the empty string.
309    *
310    @param parent the parent element
311    @param value the text value for the new child
312    @param elementName the name of the new child element
313    */
314   private void addElement(Element parent, String value, String elementName) {
315     if(!"".equals(value&& parent.getChild(elementName== null) {
316       parent.addContent(new Element(elementName).setText(value));
317     }
318   }
319 
320   /**
321    * Process the {@link AutoInstance} annotations contained in the given
322    {@link CreoleResource} and add the corresponding
323    * AUTOINSTANCE/HIDDEN-AUTOINSTANCE elements to the given parent.
324    *
325    @param cr the {@link CreoleResource} annotation
326    @param element the parent element
327    */
328   private void processAutoInstances(CreoleResource cr, Element element) {
329     for(AutoInstance ai : cr.autoinstances()) {
330       Element aiElt = null;
331       if(ai.hidden()) {
332         aiElt = new Element("HIDDEN-AUTOINSTANCE");
333       }
334       else {
335         aiElt = new Element("AUTOINSTANCE");
336       }
337       element.addContent(aiElt);
338       for(AutoInstanceParam param : ai.parameters()) {
339         aiElt.addContent(new Element("PARAM")
340                 .setAttribute("NAME", param.name()).setAttribute("VALUE",
341                         param.value()));
342       }
343     }
344   }
345 
346   /**
347    * Process any {@link CreoleParameter} and
348    {@link HiddenCreoleParameter} annotations on set methods of the
349    * given class and set up the corresponding PARAMETER elements.
350    *
351    @param resourceClass the resource class to process
352    @param resourceElement the RESOURCE element to which the PARAMETERs
353    *          are to be added
354    @param parameterMap a map from parameter names to the PARAMETER
355    *          elements that define them. This is used as we combine
356    *          information from the original creole.xml, the parameter
357    *          annotation on the target method and the annotations on the
358    *          same method of its superclasses and interfaces. Parameter
359    *          names that have been hidden by a
360    *          {@link HiddenCreoleParameter} annotation are explicitly
361    *          mapped to <code>null</code> in this map.
362    @param disjunctionMap a map from disjunction IDs to the OR elements
363    *          that define them. Disjunctive parameters are handled by
364    *          specifying a disjunction ID on the {@link CreoleParameter}
365    *          annotations - parameters with the same disjunction ID are
366    *          grouped under the same OR element.
367    */
368   private void processParameters(Class<?> resourceClass,
369           Element resourceElement, Map<String, Element> parameterMap,
370           Map<String, Element> disjunctionMapthrows GateException {
371     for(Method method : resourceClass.getDeclaredMethods()) {
372       CreoleParameter paramAnnot = method.getAnnotation(CreoleParameter.class);
373       HiddenCreoleParameter hiddenParamAnnot = method
374               .getAnnotation(HiddenCreoleParameter.class);
375       if(paramAnnot != null || hiddenParamAnnot != null) {
376         if(!method.getName().startsWith("set"|| method.getName().length() 4
377                 || method.getParameterTypes().length != 1) {
378           throw new GateException("Creole parameter annotation found on "
379                   + method
380                   ", but only setter methods may have this annotation.");
381         }
382         // extract the parameter name from the method name
383         String paramName = Character.toLowerCase(method.getName().charAt(3))
384                 + method.getName().substring(4);
385         if(hiddenParamAnnot != null && !parameterMap.containsKey(paramName)) {
386           parameterMap.put(paramName, null);
387         }
388         if(paramAnnot != null) {
389           Element paramElt = null;
390           if(parameterMap.containsKey(paramName)) {
391             paramElt = parameterMap.get(paramName);
392           }
393           else {
394             paramElt = new Element("PARAMETER").setAttribute("NAME", paramName);
395             if(!"".equals(paramAnnot.disjunction())) {
396               Element disjunctionElt = disjunctionMap.get(paramAnnot
397                       .disjunction());
398               if(disjunctionElt == null) {
399                 disjunctionElt = new Element("OR");
400                 resourceElement.addContent(disjunctionElt);
401                 disjunctionMap.put(paramAnnot.disjunction(), disjunctionElt);
402               }
403               disjunctionElt.addContent(paramElt);
404             }
405             else {
406               resourceElement.addContent(paramElt);
407             }
408             parameterMap.put(paramName, paramElt);
409           }
410 
411           if(paramElt != null) {
412             // here we have a valid element for the current parameter,
413             // which has not been masked by a @HiddenCreoleParameter
414             if(paramElt.getTextTrim().length() == 0) {
415               // need to determine the type
416               paramElt.setText(method.getParameterTypes()[0].getName());
417               if(Collection.class
418                       .isAssignableFrom(method.getParameterTypes()[0])) {
419                 determineCollectionElementType(method, paramElt);
420               }
421             }
422 
423             // other attributes
424             addAttribute(paramElt, paramAnnot.comment()"""COMMENT");
425             addAttribute(paramElt, paramAnnot.suffixes()"""SUFFIXES");
426             addAttribute(paramElt, paramAnnot.defaultValue(),
427                     CreoleParameter.NO_DEFAULT_VALUE, "DEFAULT");
428             addAttribute(paramElt, String.valueOf(paramAnnot.priority()),
429                     String.valueOf(CreoleParameter.DEFAULT_PRIORITY)"PRIORITY");
430 
431             // runtime and optional are based on marker annotations
432             String runtimeParam = "";
433             if(method.isAnnotationPresent(RunTime.class)) {
434               runtimeParam = String.valueOf(method.getAnnotation(RunTime.class)
435                       .value());
436             }
437             addAttribute(paramElt, runtimeParam, """RUNTIME");
438 
439             String optionalParam = "";
440             if(method.isAnnotationPresent(Optional.class)) {
441               optionalParam = String.valueOf(method.getAnnotation(
442                       Optional.class).value());
443             }
444             addAttribute(paramElt, optionalParam, """OPTIONAL");
445           }
446         }
447       }
448     }
449 
450     // go up the tree
451     Class<?> superclass = resourceClass.getSuperclass();
452     if(superclass != null) {
453       processParameters(superclass, resourceElement, parameterMap,
454               disjunctionMap);
455     }
456 
457     for(Class<?> intf : resourceClass.getInterfaces()) {
458       processParameters(intf, resourceElement, parameterMap, disjunctionMap);
459     }
460   }
461 
462   /**
463    * Given a single-argument method whose parameter is a
464    {@link Collection}, use the method's generic type information to
465    * determine the collection element type and store it as the
466    * ITEM_CLASS_NAME attribute of the given Element.
467    *
468    @param method the setter method
469    @param paramElt the PARAMETER element
470    */
471   private void determineCollectionElementType(Method method, Element paramElt) {
472     if(paramElt.getAttributeValue("ITEM_CLASS_NAME"== null) {
473       Class<?> elementType;
474       CreoleParameter paramAnnot = method.getAnnotation(CreoleParameter.class);
475       if(paramAnnot != null
476               && paramAnnot.collectionElementType() != CreoleParameter.NoElementType.class) {
477         elementType = paramAnnot.collectionElementType();
478       }
479       else {
480         Type paramType = method.getGenericParameterTypes()[0];
481         elementType = findCollectionElementType(paramType);
482       }
483       if(elementType != null) {
484         paramElt.setAttribute("ITEM_CLASS_NAME", elementType.getName());
485       }
486     }
487   }
488 
489   /**
490    * Find the collection element type for the given type.
491    *
492    @param type the type to use. To be able to find the element type,
493    *          this must be a Class that is assignable from Collection or
494    *          a ParameterizedType whose raw type is assignable from
495    *          Collection.
496    @return the Class representing the collection element type, or
497    *         <code>null</code> if this cannot be determined
498    */
499   private Class<?> findCollectionElementType(Type type) {
500     return findCollectionElementType(type,
501             new HashMap<TypeVariable<?>, Class<?>>());
502   }
503 
504   /**
505    * Recursive method to find the collection element type for the given
506    * type.
507    *
508    @param type the type to use
509    @param tvMap map from type variables to the classes they are
510    *          ultimately bound to. The reflection APIs can tell us that
511    *          List&lt;String&gt; is an instantiation of List&lt;X&gt;
512    *          and List&lt;X&gt; implements Collection&lt;X&gt;, but we
513    *          have to keep track of the fact that X maps to String
514    *          ourselves.
515    @return the collection element type, or <code>null</code> if it
516    *         cannot be determined.
517    */
518   private Class<?> findCollectionElementType(Type type,
519           Map<TypeVariable<?>, Class<?>> tvMap) {
520     Class<?> rawClass = null;
521     if(type instanceof Class) {
522       // we have a non-parameterized type, but it might be a raw class
523       // that extends a parameterized one (e.g. CustomCollection extends
524       // Set<Integer>) so we still need to look up
525       // the class tree
526       rawClass = (Class<?>)type;
527     }
528     else if(type instanceof ParameterizedType) {
529       Type rawType = ((ParameterizedType)type).getRawType();
530       // if we've reached Collection<T>, look at the tvMap to find what
531       // T maps to and return that
532       if(rawType == Collection.class) {
533         Type collectionTypeArgument = ((ParameterizedType)type)
534                 .getActualTypeArguments()[0];
535         if(collectionTypeArgument instanceof Class<?>) {
536           // e.g. Collection<String>
537           return (Class<?>)collectionTypeArgument;
538         }
539         else if(collectionTypeArgument instanceof TypeVariable<?>) {
540           // e.g. Collection<X>
541           return tvMap.get(collectionTypeArgument);
542         }
543         else {
544           // e.g. Collection<? extends Widget> or Collection<T[]>- we
545           // can't handle this in creole.xml so give up
546           return null;
547         }
548       }
549 
550       // we haven't reached Collection here, so add the type variable
551       // mappings to the tvMap before we look up the tree
552       if(rawType instanceof Class) {
553         rawClass = (Class<?>)rawType;
554         Type[] actualTypeParams = ((ParameterizedType)type)
555                 .getActualTypeArguments();
556         TypeVariable<?>[] formalTypeParams = ((Class<?>)rawType)
557                 .getTypeParameters();
558         for(int i = 0; i < actualTypeParams.length; i++) {
559           if(actualTypeParams[iinstanceof Class) {
560             tvMap.put(formalTypeParams[i](Class<?>)actualTypeParams[i]);
561           }
562           else if(actualTypeParams[iinstanceof TypeVariable) {
563             tvMap.put(formalTypeParams[i], tvMap.get(actualTypeParams[i]));
564           }
565         }
566       }
567     }
568 
569     // process the superclass, if there is one, and any implemented
570     // interfaces
571     if(rawClass != null) {
572       Type superclass = rawClass.getGenericSuperclass();
573       if(type != null) {
574         Class<?> componentType = findCollectionElementType(superclass, tvMap);
575         if(componentType != null) {
576           return componentType;
577         }
578       }
579 
580       for(Type intf : rawClass.getGenericInterfaces()) {
581         Class<?> componentType = findCollectionElementType(intf, tvMap);
582         if(componentType != null) {
583           return componentType;
584         }
585       }
586     }
587 
588     return null;
589   }
590 
591   /**
592    * Add an attribute with the given value to the given element, but
593    * only if the element does not already have the attribute, and the
594    * value is not equal to the given default value.
595    *
596    @param paramElt the element
597    @param value the attribute value (which will be converted to a
598    *          string)
599    @param defaultValue if value.equals(defaultValue) we do not add the
600    *          attribute.
601    @param attrName the name of the attribute to add.
602    */
603   private void addAttribute(Element paramElt, Object value,
604           Object defaultValue, String attrName) {
605     if(!defaultValue.equals(value&& paramElt.getAttribute(attrName== null) {
606       paramElt.setAttribute(attrName, value.toString());
607     }
608   }
609 }