CreoleAnnotationHandler.java
001 /*
002  * CreoleAnnotationHandler.java
003  
004  * Copyright (c) 1995-2013, 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 software,
008  * licenced under the GNU Library General Public License, Version 2, June 1991
009  * (in the distribution as file licence.html, and also available at
010  * http://gate.ac.uk/gate/licence.html).
011  
012  * Ian Roberts, 27/Jul/2008
013  
014  * $Id: CreoleAnnotationHandler.java 17530 2014-03-04 15:57:43Z markagreenwood $
015  */
016 
017 package gate.creole;
018 
019 import gate.Gate;
020 import gate.Gate.DirectoryInfo;
021 import gate.Gate.ResourceInfo;
022 import gate.Resource;
023 import gate.creole.metadata.AutoInstance;
024 import gate.creole.metadata.AutoInstanceParam;
025 import gate.creole.metadata.CreoleParameter;
026 import gate.creole.metadata.CreoleResource;
027 import gate.creole.metadata.GuiType;
028 import gate.creole.metadata.HiddenCreoleParameter;
029 import gate.creole.metadata.Optional;
030 import gate.creole.metadata.RunTime;
031 import gate.util.GateClassLoader;
032 import gate.util.GateException;
033 import gate.util.ant.ExpandIvy;
034 
035 import java.beans.BeanInfo;
036 import java.beans.IntrospectionException;
037 import java.beans.Introspector;
038 import java.beans.PropertyDescriptor;
039 import java.io.IOException;
040 import java.lang.reflect.AnnotatedElement;
041 import java.lang.reflect.Field;
042 import java.lang.reflect.Method;
043 import java.lang.reflect.ParameterizedType;
044 import java.lang.reflect.Type;
045 import java.lang.reflect.TypeVariable;
046 import java.net.MalformedURLException;
047 import java.net.URL;
048 import java.util.Collection;
049 import java.util.HashMap;
050 import java.util.List;
051 import java.util.Map;
052 
053 import org.apache.ivy.Ivy;
054 import org.apache.ivy.core.LogOptions;
055 import org.apache.ivy.core.report.ArtifactDownloadReport;
056 import org.apache.ivy.core.report.ResolveReport;
057 import org.apache.ivy.core.resolve.ResolveOptions;
058 import org.apache.ivy.util.filter.FilterHelper;
059 import org.apache.log4j.Logger;
060 import org.jdom.Document;
061 import org.jdom.Element;
062 import org.jdom.xpath.XPath;
063 
064 /**
065  * Class to take a creole.xml file (as a JDOM tree) and add elements
066  * corresponding to the CREOLE annotations on the RESOURCE classes it declares.
067  */
068 public class CreoleAnnotationHandler {
069 
070   private static final Logger log = Logger
071       .getLogger(CreoleAnnotationHandler.class);
072 
073   private URL creoleFileUrl;
074 
075   /**
076    * Create an annotation handler for the given creole.xml file.
077    
078    @param creoleFileUrl
079    *          location of the creole.xml file.
080    */
081   public CreoleAnnotationHandler(URL creoleFileUrl) {
082     this.creoleFileUrl = creoleFileUrl;
083   }
084 
085   /**
086    * Extract all JAR elements from the given JDOM document and add the jars they
087    * reference to the GateClassLoader.
088    
089    @param jdomDoc
090    *          JDOM document representing a parsed creole.xml file.
091    */
092   public void addJarsToClassLoader(GateClassLoader gcl, Document jdomDoc)
093       throws IOException {
094     addJarsToClassLoader(gcl, jdomDoc.getRootElement());
095     addIvyDependencies(gcl, jdomDoc);
096   }
097 
098   /**
099    * Extracts all the the REQUIRES elements from the given JDOM document,
100    * expands the URLs and then attempts to load the specified plugin
101    
102    @param jdomDoc JDOM document representing a parsed creole.xml file.
103    */
104   public void loadRequiredPlugins(Document jdomDocthrows IOException {
105     try {
106       XPath jarXPath =
107           XPath
108               .newInstance("//*[translate(local-name(), 'requires', 'REQUIRES') = 'REQUIRES']");
109       @SuppressWarnings("unchecked")
110       List<Element> requires = jarXPath.selectNodes(jdomDoc);
111 
112       String relativePathMarker = "$relpath$";
113       String gatehomePathMarker = "$gatehome$";
114       String gatepluginsPathMarker = "$gateplugins$";
115 
116       for(Element required : requires) {
117         URL url = null;
118         String urlString = required.getTextTrim();
119         if(urlString.startsWith(relativePathMarker)) {
120           url =
121               new URL(creoleFileUrl, urlString.substring(relativePathMarker
122                   .length()));
123         else if(urlString.startsWith(gatehomePathMarker)) {
124           URL gatehome = Gate.getGateHome().toURI().toURL();
125           url =
126               new URL(gatehome,
127                   urlString.substring(gatehomePathMarker.length()));
128         else if(urlString.startsWith(gatepluginsPathMarker)) {
129           URL gateplugins = Gate.getPluginsHome().toURI().toURL();
130           url =
131               new URL(gateplugins, urlString.substring(gatepluginsPathMarker
132                   .length()));
133         else {
134           url = new URL(creoleFileUrl, urlString);
135         }
136 
137         Gate.getCreoleRegister().registerDirectories(url);
138       }
139     catch(Exception e) {
140       throw new IOException("Unable to load required plugin", e);
141     }
142   }
143 
144   /**
145    * Extract all the IVY elements from the given JDOM document and then add all
146    * the jars resulting from ivy's dependency resolution to the GateClassLoader.
147    
148    @param creoleDoc
149    *          JDOM document representing a parsed creole.xml file.
150    */
151   private void addIvyDependencies(GateClassLoader gcl, Document creoleDoc)
152       throws IOException {
153 
154     try {
155       List<Element> ivyElts = ExpandIvy.getIvyElements(creoleDoc);
156 
157       if(ivyElts.size() 0) {
158 
159         Ivy ivy = ExpandIvy.getIvy(ExpandIvy.getSettingsURL());
160 
161         ResolveOptions resolveOptions = new ResolveOptions();
162         resolveOptions.setArtifactFilter(FilterHelper
163             .getArtifactTypeFilter(new String[]{"jar"}));
164         resolveOptions.setLog(LogOptions.LOG_QUIET);
165 
166         for(Element e : ivyElts) {
167           URL url = new URL(creoleFileUrl, ExpandIvy.getIvyPath(e));
168 
169           ResolveReport report = ivy.resolve(url, resolveOptions);
170           if(report.getAllProblemMessages().size() 0)
171             throw new Exception("Unable to resolve all IVY dependencies");
172 
173           for(ArtifactDownloadReport dlReport : report.getAllArtifactsReports()) {
174             gcl.addURL(dlReport.getLocalFile().toURI().toURL());
175           }
176 
177         }
178       }
179     catch(Exception ex) {
180       throw new IOException("Error using Ivy to add required dependencies", ex);
181     }
182   }
183 
184   /**
185    * Recursively search the given element for JAR entries and add these jars to
186    * the GateClassLoader
187    
188    @param jdomElt
189    *          JDOM element representing a creole.xml file
190    */
191   @SuppressWarnings("unchecked")
192   private void addJarsToClassLoader(GateClassLoader gcl, Element jdomElt)
193       throws MalformedURLException {
194     if("JAR".equals(jdomElt.getName())) {
195       URL url = new URL(creoleFileUrl, jdomElt.getTextTrim());
196       try {
197         java.io.InputStream s = url.openStream();
198         s.close();
199         gcl.addURL(url);
200       catch(IOException e) {
201         log.debug("Unable to add JAR "
202             + url
203             " specified in creole file "
204             + creoleFileUrl
205             " to class loader, hopefully the required classes are already on the classpath.");
206       }
207     else {
208       for(Element child : (List<Element>)jdomElt.getChildren()) {
209         addJarsToClassLoader(gcl, child);
210       }
211     }
212   }
213 
214   /**
215    * Fetches the directory information for this handler's creole plugin and adds
216    * additional RESOURCE elements to the given JDOM document so that it contains
217    * a RESOURCE for every resource type defined in the plugin's directory info.
218    
219    @param jdomDoc
220    *          JDOM document which should be the parsed creole.xml that this
221    *          handler was configured for.
222    */
223   public void createResourceElementsForDirInfo(Document jdomDoc)
224       throws MalformedURLException {
225     Element jdomElt = jdomDoc.getRootElement();
226     URL directoryUrl = new URL(creoleFileUrl, ".");
227     DirectoryInfo dirInfo = Gate.getDirectoryInfo(directoryUrl);
228     if(dirInfo != null) {
229       Map<String, Element> resourceElements = new HashMap<String, Element>();
230       findResourceElements(resourceElements, jdomElt);
231       for(ResourceInfo resInfo : dirInfo
232           .getResourceInfoList()) {
233         if(!resourceElements.containsKey(resInfo.getResourceClassName())) {
234           // no existing RESOURCE element for this resource type (so it
235           // was
236           // auto-discovered from a <JAR SCAN="true">), so add a minimal
237           // RESOURCE element which will be filled in by the annotation
238           // processor.
239           jdomElt.addContent(new Element("RESOURCE").addContent(new Element(
240               "CLASS").setText(resInfo.getResourceClassName())));
241         }
242       }
243     }
244 
245   }
246 
247   @SuppressWarnings("unchecked")
248   private void findResourceElements(Map<String, Element> map, Element elt) {
249     if(elt.getName().equals("RESOURCE")) {
250       String className = elt.getChildTextTrim("CLASS");
251       if(className != null) {
252         map.put(className, elt);
253       }
254     else {
255       for(Element child : (List<Element>)elt.getChildren()) {
256         findResourceElements(map, child);
257       }
258     }
259   }
260 
261   /**
262    * Processes annotations for resource classes named in the given creole.xml
263    * document, adding the relevant XML elements to the document as appropriate.
264    
265    @param jdomDoc
266    *          the parsed creole.xml file
267    */
268   public void processAnnotations(Document jdomDocthrows GateException {
269     processAnnotations(jdomDoc.getRootElement());
270   }
271 
272   /**
273    * Process annotations for the given element. If the element is a RESOURCE it
274    * is processed, otherwise the method calls itself recursively for all the
275    * children of the given element.
276    
277    @param element
278    *          the element to process.
279    */
280   @SuppressWarnings("unchecked")
281   private void processAnnotations(Element elementthrows GateException {
282     if("RESOURCE".equals(element.getName())) {
283       processAnnotationsForResource(element);
284     else {
285       for(Element child : (List<Element>)element.getChildren()) {
286         processAnnotations(child);
287       }
288     }
289   }
290 
291   /**
292    * Process the given RESOURCE element, adding extra elements to it based on
293    * the annotations on the resource class.
294    
295    @param element
296    *          the RESOURCE element to process.
297    */
298   private void processAnnotationsForResource(Element element)
299       throws GateException {
300     String className = element.getChildTextTrim("CLASS");
301     if(className == null) { throw new GateException(
302         "\"CLASS\" element not found for resource in " + creoleFileUrl)}
303     Class<?> resourceClass = null;
304     try {
305       resourceClass = Gate.getClassLoader().loadClass(className);
306     catch(ClassNotFoundException e) {
307       log.debug("Couldn't load class " + className + " for resource in "
308           + creoleFileUrl, e);
309       throw new GateException("Couldn't load class " + className
310           " for resource in " + creoleFileUrl);
311     }
312 
313     processCreoleResourceAnnotations(element, resourceClass);
314   }
315 
316   @SuppressWarnings("unchecked")
317   public void processCreoleResourceAnnotations(Element element,
318       Class<?> resourceClassthrows GateException {
319     CreoleResource creoleResourceAnnot =
320         resourceClass.getAnnotation(CreoleResource.class);
321     if(creoleResourceAnnot != null) {
322       // first process the top-level data and autoinstances
323       processResourceData(resourceClass, element);
324 
325       // now deal with parameters
326       // build a map holding the element corresponding to each parameter
327       Map<String, Element> parameterMap = new HashMap<String, Element>();
328       Map<String, Element> disjunctionMap = new HashMap<String, Element>();
329       for(Element paramElt : (List<Element>)element.getChildren("PARAMETER")) {
330         parameterMap.put(paramElt.getAttributeValue("NAME"), paramElt);
331       }
332       for(Element disjunctionElt : (List<Element>)element.getChildren("OR")) {
333         String disjunctionId = disjunctionElt.getAttributeValue("ID");
334         if(disjunctionId != null) {
335           disjunctionMap.put(disjunctionId, disjunctionElt);
336         }
337         for(Element paramElt : (List<Element>)disjunctionElt
338             .getChildren("PARAMETER")) {
339           parameterMap.put(paramElt.getAttributeValue("NAME"), paramElt);
340         }
341       }
342 
343       processParameters(resourceClass, element, parameterMap, disjunctionMap);
344     }
345   }
346 
347   /**
348    * Process the {@link CreoleResource} data for this class. This method first
349    * extracts the non-inheritable data (PRIVATE, MAIN_VIEWER, NAME and TOOL),
350    * then calls {@link #processInheritableResourceData} to process the
351    * inheritable data, then deals with any specified {@link AutoInstance}s.
352    
353    @param resourceClass
354    *          the resource class to process, which must be annotated with
355    *          {@link CreoleResource}.
356    @param element
357    *          the RESOURCE element to which data should be added.
358    */
359   private void processResourceData(Class<?> resourceClass, Element element) {
360     CreoleResource cr = resourceClass.getAnnotation(CreoleResource.class);
361     if(cr.isPrivate() && element.getChild("PRIVATE"== null) {
362       element.addContent(new Element("PRIVATE"));
363     }
364     if(cr.mainViewer() && element.getChild("MAIN_VIEWER"== null) {
365       element.addContent(new Element("MAIN_VIEWER"));
366     }
367     if(cr.tool() && element.getChild("TOOL"== null) {
368       element.addContent(new Element("TOOL"));
369     }
370     // NAME is the name given in the annotation, or the simple name of
371     // the class if omitted
372     addElement(element, ("".equals(cr.name()))
373         ? resourceClass.getSimpleName()
374         : cr.name()"NAME");
375     processInheritableResourceData(resourceClass, element);
376     processAutoInstances(cr, element);
377   }
378 
379   /**
380    * Recursive method to process the {@link CreoleResource} elements that can be
381    * inherited from superclasses and interfaces (everything except the PRIVATE
382    * and MAIN_VIEWER flags, the NAME and the AUTOINSTANCEs). Once data has been
383    * extracted from the current class the method calls itself recursively for
384    * the superclass and any implemented interfaces. For any given attribute, the
385    * first value specified wins (i.e. the one on the most specific class).
386    
387    @param clazz
388    *          the class to process
389    @param element
390    *          the RESOURCE element to which data should be added.
391    */
392   private void processInheritableResourceData(Class<?> clazz, Element element) {
393     CreoleResource cr = clazz.getAnnotation(CreoleResource.class);
394     if(cr != null) {
395       addElement(element, cr.comment()"COMMENT");
396       addElement(element, cr.helpURL()"HELPURL");
397       addElement(element, cr.interfaceName()"INTERFACE");
398       addElement(element, cr.icon()"ICON");
399       if(cr.guiType() != GuiType.NONE && element.getChild("GUI"== null) {
400         Element guiElement =
401             new Element("GUI").setAttribute("TYPE", cr.guiType().toString());
402         element.addContent(guiElement);
403         addElement(guiElement, cr.resourceDisplayed()"RESOURCE_DISPLAYED");
404       }
405       addElement(element, cr.annotationTypeDisplayed(),
406           "ANNOTATION_TYPE_DISPLAYED");
407     }
408 
409     Class<?> superclass = clazz.getSuperclass();
410     if(superclass != null) {
411       processInheritableResourceData(superclass, element);
412     }
413 
414     for(Class<?> intf : clazz.getInterfaces()) {
415       processInheritableResourceData(intf, element);
416     }
417   }
418 
419   /**
420    * Adds an element with the given name and text value to the parent element,
421    * but only if no such child element already exists and the value is not the
422    * empty string.
423    
424    @param parent
425    *          the parent element
426    @param value
427    *          the text value for the new child
428    @param elementName
429    *          the name of the new child element
430    */
431   private void addElement(Element parent, String value, String elementName) {
432     if(!"".equals(value&& parent.getChild(elementName== null) {
433       parent.addContent(new Element(elementName).setText(value));
434     }
435   }
436 
437   /**
438    * Process the {@link AutoInstance} annotations contained in the given
439    {@link CreoleResource} and add the corresponding
440    * AUTOINSTANCE/HIDDEN-AUTOINSTANCE elements to the given parent.
441    
442    @param cr
443    *          the {@link CreoleResource} annotation
444    @param element
445    *          the parent element
446    */
447   private void processAutoInstances(CreoleResource cr, Element element) {
448     for(AutoInstance ai : cr.autoinstances()) {
449       Element aiElt = null;
450       if(ai.hidden()) {
451         aiElt = new Element("HIDDEN-AUTOINSTANCE");
452       else {
453         aiElt = new Element("AUTOINSTANCE");
454       }
455       element.addContent(aiElt);
456       for(AutoInstanceParam param : ai.parameters()) {
457         aiElt.addContent(new Element("PARAM")
458             .setAttribute("NAME", param.name()).setAttribute("VALUE",
459                 param.value()));
460       }
461     }
462   }
463 
464   /**
465    * Process any {@link CreoleParameter} and {@link HiddenCreoleParameter}
466    * annotations on set methods of the given class and set up the corresponding
467    * PARAMETER elements.
468    
469    @param resourceClass
470    *          the resource class to process
471    @param resourceElement
472    *          the RESOURCE element to which the PARAMETERs are to be added
473    @param parameterMap
474    *          a map from parameter names to the PARAMETER elements that define
475    *          them. This is used as we combine information from the original
476    *          creole.xml, the parameter annotation on the target method and the
477    *          annotations on the same method of its superclasses and interfaces.
478    *          Parameter names that have been hidden by a
479    *          {@link HiddenCreoleParameter} annotation are explicitly mapped to
480    *          <code>null</code> in this map.
481    @param disjunctionMap
482    *          a map from disjunction IDs to the OR elements that define them.
483    *          Disjunctive parameters are handled by specifying a disjunction ID
484    *          on the {@link CreoleParameter} annotations - parameters with the
485    *          same disjunction ID are grouped under the same OR element.
486    */
487   private void processParameters(Class<?> resourceClass,
488       Element resourceElement, Map<String, Element> parameterMap,
489       Map<String, Element> disjunctionMapthrows GateException {
490 
491     BeanInfo bi;
492     try {
493       bi = Introspector.getBeanInfo(resourceClass);
494     catch(IntrospectionException e) {
495       throw new GateException("Failed to introspect " + resourceClass, e);
496     }
497 
498     for(Method method : resourceClass.getDeclaredMethods()) {
499       processElement(method, bi, resourceElement, parameterMap, disjunctionMap);
500     }
501 
502     for(Field field : resourceClass.getDeclaredFields()) {
503       processElement(field, bi, resourceElement, parameterMap, disjunctionMap);
504     }
505 
506     // go up the tree
507     Class<?> superclass = resourceClass.getSuperclass();
508     if(superclass != null) {
509       processParameters(superclass, resourceElement, parameterMap,
510           disjunctionMap);
511     }
512 
513     for(Class<?> intf : resourceClass.getInterfaces()) {
514       processParameters(intf, resourceElement, parameterMap, disjunctionMap);
515     }
516   }
517 
518   /**
519    * Processes parameter annotations on a single element. Can support both Field
520    * and Method elements.
521    
522    @param element
523    *          The Method of Field from which to discern parameters
524    @param bi
525    *          the BeanInfo for the corresponding class.
526    @param resourceElement
527    *          the RESOURCE element to which the PARAMETERs are to be added
528    @param parameterMap
529    *          a map from parameter names to the PARAMETER elements that define
530    *          them. This is used as we combine information from the original
531    *          creole.xml, the parameter annotation on the target method and the
532    *          annotations on the same method of its superclasses and interfaces.
533    *          Parameter names that have been hidden by a
534    *          {@link HiddenCreoleParameter} annotation are explicitly mapped to
535    *          <code>null</code> in this map.
536    @param disjunctionMap
537    *          a map from disjunction IDs to the OR elements that define them.
538    *          Disjunctive parameters are handled by specifying a disjunction ID
539    *          on the {@link CreoleParameter} annotations - parameters with the
540    *          same disjunction ID are grouped under the same OR element.
541    */
542   private void processElement(AnnotatedElement element, BeanInfo bi,
543       Element resourceElement, Map<String, Element> parameterMap,
544       Map<String, Element> disjunctionMapthrows GateException {
545     CreoleParameter paramAnnot = element.getAnnotation(CreoleParameter.class);
546     HiddenCreoleParameter hiddenParamAnnot =
547         element.getAnnotation(HiddenCreoleParameter.class);
548     String paramName; // Extracted name of this parameter
549     Class<?> paramType;
550     Type genericParamType; // The type of this parameter.
551     if(paramAnnot != null || hiddenParamAnnot != null) {
552       // Enforce constraints relevant for this type of element
553       if(element.getClass().equals(java.lang.reflect.Field.class)) {
554         java.lang.reflect.Field field = (java.lang.reflect.Field)element;
555         paramName = field.getName();
556         genericParamType = field.getGenericType();
557         paramType = field.getType();
558         PropertyDescriptor paramDescriptor = null;
559         for(PropertyDescriptor pd : bi.getPropertyDescriptors()) {
560           if(paramName.equals(pd.getName())) {
561             paramDescriptor = pd;
562             break;
563           }
564         }
565 
566         if(paramDescriptor == null) {
567           throw new GateException("CREOLE parameter annotation found on field "
568               + field
569               " but no corresponding JavaBean accessor methods exist.");
570         else if(paramDescriptor.getReadMethod() == null
571             || paramDescriptor.getWriteMethod() == null) { throw new GateException(
572             "CREOLE parameter annotation found on field "
573                 + field
574                 " but getter or setter is missing.  CREOLE parameters require both.")}
575       else if(element.getClass().equals(Method.class)) {
576         Method method = (Method)element;
577         // Extract the parameter name from the BeanInfo
578         PropertyDescriptor paramDescriptor = null;
579         for(PropertyDescriptor pd : bi.getPropertyDescriptors()) {
580           if(method.equals(pd.getWriteMethod())) {
581             paramDescriptor = pd;
582             break;
583           }
584         }
585 
586         if(paramDescriptor == null) { throw new GateException(
587             "CREOLE parameter annotation found on " + method
588                 " but this method is not a Java Bean property setter.")}
589         paramName = paramDescriptor.getName();
590         // And the type is that of the first argument
591         genericParamType = method.getGenericParameterTypes()[0];
592         paramType = method.getParameterTypes()[0];
593       else {
594         throw new GateException("CREOLE parameter annotation found on "
595             + element + " but can only be placed on Method or Field");
596       }
597 
598       // Hidden parameters can be added straight to the map.
599       if(hiddenParamAnnot != null && !parameterMap.containsKey(paramName)) {
600         parameterMap.put(paramName, null);
601       }
602 
603       // Visible parameters need converting to JDOM Elements
604       if(paramAnnot != null) {
605         Element paramElt = null;
606         if(parameterMap.containsKey(paramName)) {
607           // Use existing annotation if there is such a thing.
608           paramElt = parameterMap.get(paramName);
609         else {
610           // Otherwise create on - type depends on whether it is disjunctive or
611           // not.
612           paramElt = new Element("PARAMETER").setAttribute("NAME", paramName);
613 
614           if(!"".equals(paramAnnot.disjunction())) {
615             // Disjunctive parameters (cannot both be set) need special markup.
616             Element disjunctionElt =
617                 disjunctionMap.get(paramAnnot.disjunction());
618             if(disjunctionElt == null) {
619               disjunctionElt = new Element("OR");
620               resourceElement.addContent(disjunctionElt);
621               disjunctionMap.put(paramAnnot.disjunction(), disjunctionElt);
622             }
623             disjunctionElt.addContent(paramElt);
624           else {
625             resourceElement.addContent(paramElt);
626           }
627           parameterMap.put(paramName, paramElt);
628         }
629 
630         if(paramElt != null) {
631           // paramElt is a valid element for the current parameter,
632           // which has not been masked by a @HiddenCreoleParameter
633           if(paramElt.getTextTrim().length() == 0) {
634             // The text of the element should be the the type
635             paramElt.setText(paramType.getName());
636             // for collections (but not GATE Resources) we also try to determine
637             // the item type.
638             if((!Resource.class.isAssignableFrom(paramType))
639                 && Collection.class.isAssignableFrom(paramType)) {
640               determineCollectionElementType(element, genericParamType,
641                   paramElt);
642             }
643           }
644 
645           // other attributes
646           addAttribute(paramElt, paramAnnot.comment()"""COMMENT");
647           addAttribute(paramElt, paramAnnot.suffixes()"""SUFFIXES");
648           addAttribute(paramElt, paramAnnot.defaultValue(),
649               CreoleParameter.NO_DEFAULT_VALUE, "DEFAULT");
650           addAttribute(paramElt, String.valueOf(paramAnnot.priority()),
651               String.valueOf(CreoleParameter.DEFAULT_PRIORITY)"PRIORITY");
652 
653           // runtime and optional are based on marker annotations
654           String runtimeParam = "";
655           if(element.isAnnotationPresent(RunTime.class)) {
656             runtimeParam =
657                 String.valueOf(element.getAnnotation(RunTime.class).value());
658           }
659           addAttribute(paramElt, runtimeParam, """RUNTIME");
660 
661           String optionalParam = "";
662           if(element.isAnnotationPresent(Optional.class)) {
663             optionalParam =
664                 String.valueOf(element.getAnnotation(Optional.class).value());
665           }
666           addAttribute(paramElt, optionalParam, """OPTIONAL");
667         }
668       }
669     }
670   }
671 
672   /**
673    * Given a single-argument method whose parameter is a {@link Collection}, use
674    * the method's generic type information to determine the collection element
675    * type and store it as the ITEM_CLASS_NAME attribute of the given Element.
676    
677    @param method
678    *          the setter method
679    @param paramElt
680    *          the PARAMETER element
681    */
682   private void determineCollectionElementType(AnnotatedElement method,
683       Type paramType, Element paramElt) {
684     if(paramElt.getAttributeValue("ITEM_CLASS_NAME"== null) {
685       Class<?> elementType;
686       CreoleParameter paramAnnot = method.getAnnotation(CreoleParameter.class);
687       if(paramAnnot != null
688           && paramAnnot.collectionElementType() != CreoleParameter.NoElementType.class) {
689         elementType = paramAnnot.collectionElementType();
690       else {
691         elementType = findCollectionElementType(paramType);
692       }
693       if(elementType != null) {
694         paramElt.setAttribute("ITEM_CLASS_NAME", elementType.getName());
695       }
696     }
697   }
698 
699   /**
700    * Find the collection element type for the given type.
701    
702    @param type
703    *          the type to use. To be able to find the element type, this must be
704    *          a Class that is assignable from Collection or a ParameterizedType
705    *          whose raw type is assignable from Collection.
706    @return the Class representing the collection element type, or
707    *         <code>null</code> if this cannot be determined
708    */
709   private Class<?> findCollectionElementType(Type type) {
710     return findCollectionElementType(type,
711         new HashMap<TypeVariable<?>, Class<?>>());
712   }
713 
714   /**
715    * Recursive method to find the collection element type for the given type.
716    
717    @param type
718    *          the type to use
719    @param tvMap
720    *          map from type variables to the classes they are ultimately bound
721    *          to. The reflection APIs can tell us that List&lt;String&gt; is an
722    *          instantiation of List&lt;X&gt; and List&lt;X&gt; implements
723    *          Collection&lt;X&gt;, but we have to keep track of the fact that X
724    *          maps to String ourselves.
725    @return the collection element type, or <code>null</code> if it cannot be
726    *         determined.
727    */
728   private Class<?> findCollectionElementType(Type type,
729       Map<TypeVariable<?>, Class<?>> tvMap) {
730     Class<?> rawClass = null;
731     if(type instanceof Class) {
732       // we have a non-parameterized type, but it might be a raw class
733       // that extends a parameterized one (e.g. CustomCollection extends
734       // Set<Integer>) so we still need to look up
735       // the class tree
736       rawClass = (Class<?>)type;
737     else if(type instanceof ParameterizedType) {
738       Type rawType = ((ParameterizedType)type).getRawType();
739       // if we've reached Collection<T>, look at the tvMap to find what
740       // T maps to and return that
741       if(rawType == Collection.class) {
742         Type collectionTypeArgument =
743             ((ParameterizedType)type).getActualTypeArguments()[0];
744         if(collectionTypeArgument instanceof Class<?>) {
745           // e.g. Collection<String>
746           return (Class<?>)collectionTypeArgument;
747         else if(collectionTypeArgument instanceof TypeVariable<?>) {
748           // e.g. Collection<X>
749           return tvMap.get(collectionTypeArgument);
750         else {
751           // e.g. Collection<? extends Widget> or Collection<T[]>- we
752           // can't handle this in creole.xml so give up
753           return null;
754         }
755       }
756 
757       // we haven't reached Collection here, so add the type variable
758       // mappings to the tvMap before we look up the tree
759       if(rawType instanceof Class) {
760         rawClass = (Class<?>)rawType;
761         Type[] actualTypeParams =
762             ((ParameterizedType)type).getActualTypeArguments();
763         TypeVariable<?>[] formalTypeParams =
764             ((Class<?>)rawType).getTypeParameters();
765         for(int i = 0; i < actualTypeParams.length; i++) {
766           if(actualTypeParams[iinstanceof Class) {
767             tvMap.put(formalTypeParams[i](Class<?>)actualTypeParams[i]);
768           else if(actualTypeParams[iinstanceof TypeVariable) {
769             tvMap.put(formalTypeParams[i], tvMap.get(actualTypeParams[i]));
770           }
771         }
772       }
773     }
774 
775     // process the superclass, if there is one, and any implemented
776     // interfaces
777     if(rawClass != null) {
778       Type superclass = rawClass.getGenericSuperclass();
779       if(type != null) {
780         Class<?> componentType = findCollectionElementType(superclass, tvMap);
781         if(componentType != null) { return componentType; }
782       }
783 
784       for(Type intf : rawClass.getGenericInterfaces()) {
785         Class<?> componentType = findCollectionElementType(intf, tvMap);
786         if(componentType != null) { return componentType; }
787       }
788     }
789 
790     return null;
791   }
792 
793   /**
794    * Add an attribute with the given value to the given element, but only if the
795    * element does not already have the attribute, and the value is not equal to
796    * the given default value.
797    
798    @param paramElt
799    *          the element
800    @param value
801    *          the attribute value (which will be converted to a string)
802    @param defaultValue
803    *          if value.equals(defaultValue) we do not add the attribute.
804    @param attrName
805    *          the name of the attribute to add.
806    */
807   private void addAttribute(Element paramElt, Object value,
808       Object defaultValue, String attrName) {
809     if(!defaultValue.equals(value&& paramElt.getAttribute(attrName== null) {
810       paramElt.setAttribute(attrName, value.toString());
811     }
812   }
813 }