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 jdomDoc) throws 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 element) throws 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<?> resourceClass) throws 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> disjunctionMap) throws 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<String> is an instantiation of List<X>
512 * and List<X> implements Collection<X>, 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[i] instanceof Class) {
560 tvMap.put(formalTypeParams[i], (Class<?>)actualTypeParams[i]);
561 }
562 else if(actualTypeParams[i] instanceof 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 }
|