CreoleXmlHandler.java
001 /*
002  *  CreoleXmlHandler.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  *  Hamish Cunningham, 1/Sept/2000
013  *
014  *  $Id: CreoleXmlHandler.java 13384 2011-01-31 14:02:28Z ian_roberts $
015  */
016 
017 package gate.creole;
018 
019 import java.net.MalformedURLException;
020 import java.net.URL;
021 import java.util.*;
022 
023 import javax.swing.text.html.HTMLDocument.HTMLReader.HiddenAction;
024 
025 import org.xml.sax.*;
026 import org.xml.sax.helpers.DefaultHandler;
027 
028 import gate.*;
029 import gate.util.*;
030 import gate.xml.SimpleErrorHandler;
031 
032 /** This is a SAX handler for processing <CODE>creole.xml</CODE> files.
033   * It would have been better to write it using DOM or JDOM but....
034   * Resource data objects are created and added to the CREOLE register.
035   * URLs for resource JAR files are added to the GATE class loader.
036   */
037 public class CreoleXmlHandler extends DefaultHandler {
038 
039   /** A stack to stuff PCDATA onto for reading back at element ends.
040    *  (Probably redundant to have a stack as we only push one item
041    *  onto it. Probably. Ok, so I should check, but a) it works, b)
042    *  I'm bald already and c) life is short.)
043    */
044   private Stack contentStack = new Stack();
045 
046   /** The current resource data object */
047   private ResourceData resourceData;
048 
049   /** The current parameter list */
050   private ParameterList currentParamList = new ParameterList();
051 
052   /**
053    * The current parameter disjunction.  This is a map where each key
054    * is a "priority" and the value is a list of parameters tagged with
055    * that priority.  This map is flattened into a single list when
056    * all the parameters in a single disjunction have been processed,
057    * such that parameters with a smaller priority value are listed
058    * ahead of those with a larger value, and those with no explicit
059    * priority are listed last of all.  Parameters at the same priority
060    * are listed in document order.  This is not so useful when writing
061    * creole.xml files by hand but is necessary to ensure a predictable
062    * order when using CreoleParameter annotations.  The GATE developer
063    * GUI offers the first listed (i.e. highest priority) parameter for
064    * each disjunction as the default option in the resource parameters
065    * dialog box. 
066    */
067   private SortedMap<Integer, List<Parameter>> currentParamDisjunction =
068     new TreeMap<Integer, List<Parameter>>();
069 
070   /** The current parameter */
071   private Parameter currentParam;
072 
073   /** The current element's attribute list */
074   private Attributes currentAttributes;
075 
076   /** Debug flag */
077   private static final boolean DEBUG = false;
078 
079   /** The source URL of the directory file being parsed. */
080   private URL sourceUrl;
081   
082   /**
083    * The URL to the creole.xml file being parsed.
084    */
085   private URL creoleFileUrl;
086 
087   /** This object indicates what to do when the parser encounts an error*/
088   private SimpleErrorHandler _seh = new SimpleErrorHandler();
089 
090   /** This field represents the params map required for autoinstantiation
091     * Its a map from param name to param value.
092     */
093   private FeatureMap currentAutoinstanceParams = null;
094 
095   /** This field holds autoinstanceParams describing the resource that
096     * needs to be instantiated
097     */
098   private List currentAutoinstances = null;
099 
100 
101   /** This is used to capture all data within two tags before calling the actual characters method */
102   private StringBuffer contentBuffer = new StringBuffer("");
103 
104   /** This is a variable that shows if characters have been read */
105   private boolean readCharacterStatus = false;
106 
107   /** Construction */
108   public CreoleXmlHandler(CreoleRegister register, URL directoryUrl, 
109           URL creoleFileUrl) {
110     this.register = register;
111     this.sourceUrl = directoryUrl;
112     this.creoleFileUrl = creoleFileUrl;
113     currentParam = new Parameter(this.creoleFileUrl);
114   // construction
115 
116   /** The register object that we add ResourceData objects to during parsing.
117     */
118   private CreoleRegister register;
119 
120   /** Called when the SAX parser encounts the beginning of the XML document */
121   public void startDocument() throws GateSaxException {
122     if(DEBUGOut.prln("start document");
123   // startDocument
124 
125   /** Called when the SAX parser encounts the end of the XML document */
126   public void endDocument() throws GateSaxException {
127     if(DEBUGOut.prln("end document");
128     if(! contentStack.isEmpty()) {
129       StringBuffer errorMessage =
130         new StringBuffer("document ended but element stack not empty:");
131       while(! contentStack.isEmpty())
132         errorMessage.append(Strings.getNl()+"  "+(StringcontentStack.pop());
133       throw new GateSaxException(errorMessage.toString());
134     }
135   // endDocument
136 
137   /** A verboase method for Attributes*/
138   private String attributes2String(Attributes atts){
139     StringBuffer strBuf = new StringBuffer("");
140     if (atts == nullreturn strBuf.toString();
141     for (int i = 0; i < atts.getLength(); i++) {
142      String attName  = atts.getQName(i);
143      String attValue = atts.getValue(i);
144      strBuf.append(" ");
145      strBuf.append(attName);
146      strBuf.append("=");
147      strBuf.append(attValue);
148     }// End for
149     return strBuf.toString();
150   }// attributes2String()
151 
152   /** Called when the SAX parser encounts the beginning of an XML element */
153   public void startElement (String uri, String qName, String elementName,
154                                                              Attributes attsthrows SAXException {
155 
156     // call characterActions
157     if(readCharacterStatus) {
158       readCharacterStatus = false;
159       charactersAction(new String(contentBuffer).toCharArray(),0,contentBuffer.length());
160     }
161 
162     if(DEBUG) {
163       Out.pr("startElement: ");
164       Out.println(
165         elementName + " " +
166         attributes2String(atts)
167       );
168     }
169 
170     // create a new ResourceData when it's a RESOURCE element
171     if(elementName.toUpperCase().equals("RESOURCE")) {
172       resourceData = new ResourceData();
173       resourceData.setFeatures(Factory.newFeatureMap());
174       currentAutoinstances = new ArrayList();
175     }// End if RESOURCE
176 
177     // record the attributes of this element
178     currentAttributes = atts;
179 
180     // When an AUTOINSTANCE element is found a params FeatureMap will
181     // be prepared in order for the resource to be instantiated
182     if (elementName.toUpperCase().equals("AUTOINSTANCE")){
183       currentAutoinstanceParams = Factory.newFeatureMap();
184     }// End if AUTOINSTANCE
185 
186     //When an HIDDEN-AUTOINSTANCE element is found a params FeatureMap will
187     // be prepared in order for the resource to be instantiated
188     if (elementName.toUpperCase().equals("HIDDEN-AUTOINSTANCE")){
189       currentAutoinstanceParams = Factory.newFeatureMap();
190       Gate.setHiddenAttribute(currentAutoinstanceParams, true);
191     }// End if AUTOINSTANCE
192     
193     // When a PARAN start element is found, the parameter would be instantiated
194     // with a value and added to the autoinstanceParams
195     if (elementName.toUpperCase().equals("PARAM")){
196       // The autoinstanceParams should always be != null because of the fact
197       // that PARAM is incuded into an AUTOINSTANCE element.
198       // IF a AUTOINSTANCE starting element would be missing then the
199       // parser would signal this later....
200       if (currentAutoinstanceParams == null)
201         currentAutoinstanceParams = Factory.newFeatureMap();
202       // Take the param's name and value
203       String paramName = currentAttributes.getValue("NAME");
204       String paramStrValue = currentAttributes.getValue("VALUE");
205       if (paramName == null)
206         throw new GateRuntimeException ("Found in creole.xml a PARAM element" +
207         " for resource "+ resourceData.getClassName()" without a NAME"+
208         " attribute. Check the file and try again.");
209       if (paramStrValue == null)
210         throw new GateRuntimeException("Found in creole.xml a PARAM element"+
211         " for resource "+ resourceData.getClassName()" without a VALUE"+
212         " attribute. Check the file and try again.");
213       // Add the paramname and its value to the autoinstanceParams
214       currentAutoinstanceParams.put(paramName,paramStrValue);
215     }// End if PARAM
216 
217     // process attributes of parameter and GUI elements
218     if(elementName.toUpperCase().equals("PARAMETER")) {
219       if(DEBUG) {
220         for(int i=0, len=currentAttributes.getLength(); i<len; i++) {
221           Out.prln(currentAttributes.getLocalName(i));
222           Out.prln(currentAttributes.getValue(i));
223         }// End for
224       }// End if
225       currentParam.comment = currentAttributes.getValue("COMMENT");
226       currentParam.helpURL = currentAttributes.getValue("HELPURL");
227       currentParam.defaultValueString = currentAttributes.getValue("DEFAULT");
228       currentParam.optional =
229         Boolean.valueOf(currentAttributes.getValue("OPTIONAL")).booleanValue();
230       currentParam.name = currentAttributes.getValue("NAME");
231       currentParam.runtime =
232         Boolean.valueOf(currentAttributes.getValue("RUNTIME")).booleanValue();
233       currentParam.itemClassName =
234                                 currentAttributes.getValue("ITEM_CLASS_NAME");
235       // read the suffixes and transform them to a Set of Strings
236       String suffixes = currentAttributes.getValue("SUFFIXES");
237       Set suffiexesSet = null;
238       if (suffixes != null){
239         suffiexesSet = new HashSet();
240         StringTokenizer strTokenizer = new StringTokenizer(suffixes,";");
241         while(strTokenizer.hasMoreTokens()){
242            suffiexesSet.add(strTokenizer.nextToken());
243         }// End while
244       }// End if
245       currentParam.suffixes = suffiexesSet;
246     }else if(elementName.toUpperCase().equals("GUI")){
247       String typeValue = currentAttributes.getValue("TYPE");
248       if (typeValue != null){
249         if (typeValue.toUpperCase().equals("LARGE"))
250           resourceData.setGuiType(ResourceData.LARGE_GUI);
251         if (typeValue.toUpperCase().equals("SMALL"))
252           resourceData.setGuiType(ResourceData.SMALL_GUI);
253       }// End if
254     }// End if
255 
256     // if there are any parameters awaiting addition to the list, add them
257     // (note that they're not disjunctive or previous "/OR" would have got 'em)
258     if(elementName.toUpperCase().equals("OR")) {
259       if(! currentParamDisjunction.isEmpty()) {
260         currentParamList.addAll(currentFlattenedDisjunction());
261         currentParamDisjunction.clear();
262       }// End if
263     }// End if
264   // startElement()
265 
266   /** Utility function to throw exceptions on stack errors. */
267   private void checkStack(String methodName, String elementName)
268   throws GateSaxException {
269     if(contentStack.isEmpty())
270       throw new GateSaxException(
271         methodName + " called for element " + elementName + " with empty stack"
272       );
273   // checkStack
274 
275   /** Called when the SAX parser encounts the end of an XML element.
276     * This is where ResourceData objects get values set, and where
277     * they are added to the CreoleRegister when we parsed their complete
278     * metadata entries.
279     */
280   public void endElement (String uri, String qName, String elementName)
281                                                     throws GateSaxException, SAXException {
282     // call characterActions
283     if(readCharacterStatus) {
284       readCharacterStatus = false;
285       charactersAction(new String(contentBuffer).toCharArray(),0,contentBuffer.length());
286     }
287 
288     if(DEBUGOut.prln("endElement: " + elementName);
289 
290     //////////////////////////////////////////////////////////////////
291     if(elementName.toUpperCase().equals("RESOURCE")) {
292       // check for validity of the resource data
293       if(! resourceData.isValid())
294         throw new GateSaxException(
295           "Invalid resource data: " + resourceData.getValidityMessage()
296         );
297 
298       //set the URL to the creole.xml file on the resource data object
299       resourceData.setXmlFileUrl(creoleFileUrl);
300       // add the new resource data object to the creole register
301       register.put(resourceData.getClassName(), resourceData);
302       // if the resource is auto-loading, try and load it
303       if(resourceData.isAutoLoading())
304         try {
305           Class resClass = resourceData.getResourceClass();
306 //          Resource res = Factory.createResource(
307 //              resourceData.getClassName(), Factory.newFeatureMap()
308 //          );
309 //          resourceData.makeInstantiationPersistant(res);
310         catch(ClassNotFoundException e) {
311           throw new GateSaxException(
312             "Couldn't load autoloading resource: " +
313             resourceData.getName() "; problem was: " + e
314           );
315         }// End try
316 
317       // if there are any parameters awaiting addition to the list, add them
318       // (note that they're not disjunctive or the "/OR" would have got them)
319       if(! currentParamDisjunction.isEmpty()) {
320         currentParamList.addAll(currentFlattenedDisjunction());
321         currentParamDisjunction.clear();
322       }// End if
323 
324       // add the parameter list to the resource (and reinitialise it)
325       resourceData.setParameterList(currentParamList);
326       currentParamList = new ParameterList();
327 
328       if(DEBUGOut.println("added: " + resourceData);
329       // Iterate through autoinstances and try to instanciate them
330       if currentAutoinstances != null && !currentAutoinstances.isEmpty()){
331         Iterator iter = currentAutoinstances.iterator();
332         while (iter.hasNext()){
333           FeatureMap autoinstanceParams = (FeatureMapiter.next();
334           iter.remove();
335           FeatureMap autoinstanceFeatures = null;
336           //if the hidden attribute was set in the parameters, create a feature 
337           //map and move the hidden attribute there.
338           if(Gate.getHiddenAttribute(autoinstanceParams)){
339             autoinstanceFeatures = Factory.newFeatureMap();
340             Gate.setHiddenAttribute(autoinstanceFeatures, true);
341             autoinstanceParams.remove(GateConstants.HIDDEN_FEATURE_KEY);
342           }
343           // Try to create the resource.
344           try {
345             // Resource res = 
346             Factory.createResource(
347                               resourceData.getClassName(), autoinstanceParams, 
348                               autoinstanceFeatures);
349             //resourceData.makeInstantiationPersistant(res);
350             // all resource instantiations are persistent
351           catch(ResourceInstantiationException e) {
352             throw new GateSaxException(
353               "Couldn't auto-instantiate resource: " +
354               resourceData.getName() "; problem was: " + e
355             );
356           }// End try
357         }// End while
358       }// End if
359       currentAutoinstances = null;
360     // End RESOURCE processing
361     //////////////////////////////////////////////////////////////////
362     else if(elementName.toUpperCase().equals("AUTOINSTANCE"||
363             elementName.toUpperCase().equals("HIDDEN-AUTOINSTANCE")) {
364       //checkStack("endElement", "AUTOINSTANCE");
365       // Cache the auto-instance into the autoins
366       if (currentAutoinstanceParams != null)
367         currentAutoinstances.add(currentAutoinstanceParams);
368     // End AUTOINSTANCE processing
369     //////////////////////////////////////////////////////////////////
370     else if(elementName.toUpperCase().equals("PARAM")) {
371     // End PARAM processing
372     //////////////////////////////////////////////////////////////////
373     else if(elementName.toUpperCase().equals("NAME")) {
374       checkStack("endElement""NAME");
375       resourceData.setName((StringcontentStack.pop());
376     // End NAME processing
377     //////////////////////////////////////////////////////////////////
378     else if(elementName.toUpperCase().equals("JAR")) {
379       checkStack("endElement""JAR");
380 
381       // add jar file name
382       String jarFileName = (StringcontentStack.pop();
383       if(resourceData != null) {
384         resourceData.setJarFileName(jarFileName);
385       }
386 
387       // add jar file URL if there is one
388       if(sourceUrl != null) {
389         String sourceUrlName = sourceUrl.toExternalForm();
390         String separator = "/";
391 
392         if(sourceUrlName.endsWith(separator))
393           separator = "";
394         URL jarFileUrl = null;
395 
396         try {
397           jarFileUrl = new URL(sourceUrlName + separator + jarFileName);
398           if(resourceData != null) {
399             resourceData.setJarFileUrl(jarFileUrl);
400           }
401 
402           // We no longer need to add the jar URL to the class loader, as this
403           // is done before the SAX parse
404         catch(MalformedURLException e) {
405           throw new GateSaxException("bad URL " + jarFileUrl + e);
406         }// End try
407       }// End if
408     // End JAR processing
409     //////////////////////////////////////////////////////////////////
410     else if(elementName.toUpperCase().equals("CLASS")) {
411       checkStack("endElement""CLASS");
412       resourceData.setClassName((StringcontentStack.pop());
413     // End CLASS processing
414     //////////////////////////////////////////////////////////////////
415     else if(elementName.toUpperCase().equals("COMMENT")) {
416       checkStack("endElement""COMMENT");
417       resourceData.setComment((StringcontentStack.pop());
418     // End COMMENT processing
419     //////////////////////////////////////////////////////////////////
420     else if(elementName.toUpperCase().equals("HELPURL")) {
421       checkStack("endElement""HELPURL");
422       resourceData.setHelpURL((StringcontentStack.pop());
423     // End HELPURL processing
424     //////////////////////////////////////////////////////////////////
425     else if(elementName.toUpperCase().equals("INTERFACE")) {
426       checkStack("endElement""INTERFACE");
427       resourceData.setInterfaceName((StringcontentStack.pop());
428     // End INTERFACE processing
429     //////////////////////////////////////////////////////////////////
430     else if(elementName.toUpperCase().equals("ICON")) {
431       checkStack("endElement""ICON");
432       resourceData.setIcon((StringcontentStack.pop());
433     // End ICON processing
434     //////////////////////////////////////////////////////////////////
435     else if(elementName.toUpperCase().equals("OR")) {
436       currentParamList.add(currentFlattenedDisjunction());
437       currentParamDisjunction.clear();
438     // End OR processing
439     //////////////////////////////////////////////////////////////////
440     else if(elementName.toUpperCase().equals("PARAMETER")) {
441       checkStack("endElement""PARAMETER");
442       currentParam.typeName = (StringcontentStack.pop();
443       String priorityStr = currentAttributes.getValue("PRIORITY");
444       // if no priority specified, assume lowest (i.e. parameters with an
445       // explicit priority come ahead of those without).
446       Integer priority = Integer.MAX_VALUE;
447       try {
448         if(priorityStr != nullpriority = Integer.valueOf(priorityStr);
449       }
450       catch(NumberFormatException nfe) {
451         throw new GateRuntimeException ("Found in creole.xml a PARAM element" +
452                 " for resource "+ resourceData.getClassName()" with a non-numeric"+
453                 " PRIORITY attribute. Check the file and try again.");
454       }
455       List<Parameter> paramList = currentParamDisjunction.get(priority);
456       if(paramList == null) {
457         paramList = new ArrayList<Parameter>();
458         currentParamDisjunction.put(priority, paramList);
459       }
460       paramList.add(currentParam);
461       if(DEBUG)
462         Out.prln("added param: " + currentParam);
463       currentParam = new Parameter(creoleFileUrl);
464     // End PARAMETER processing
465     //////////////////////////////////////////////////////////////////
466     else if(elementName.toUpperCase().equals("AUTOLOAD")) {
467       resourceData.setAutoLoading(true);
468     // End AUTOLOAD processing
469     //////////////////////////////////////////////////////////////////
470     else if(elementName.toUpperCase().equals("PRIVATE")) {
471       resourceData.setPrivate(true);
472     // End PRIVATE processing
473     //////////////////////////////////////////////////////////////////
474     else if(elementName.toUpperCase().equals("TOOL")) {
475       resourceData.setTool(true);
476     // End TOOL processing
477     //////////////////////////////////////////////////////////////////
478     else if(elementName.toUpperCase().equals("MAIN_VIEWER")) {
479       resourceData.setIsMainView(true);
480     // End MAIN_VIEWER processing
481     //////////////////////////////////////////////////////////////////
482     else if(elementName.toUpperCase().equals("RESOURCE_DISPLAYED")){
483       checkStack("endElement""RESOURCE_DISPLAYED");
484       String resourceDisplayed = (StringcontentStack.pop();
485       resourceData.setResourceDisplayed(resourceDisplayed);
486       Class resourceDisplayedClass = null;
487       try{
488         resourceDisplayedClass = Gate.getClassLoader().
489                                  loadClass(resourceDisplayed);
490       catch (ClassNotFoundException ex){
491         throw new GateRuntimeException(
492           "Couldn't get resource class from the resource name :" +
493           resourceDisplayed + " " +ex );
494       }// End try
495     // End RESOURCE_DISPLAYED processing
496     //////////////////////////////////////////////////////////////////
497     else if(elementName.toUpperCase().equals("ANNOTATION_TYPE_DISPLAYED")){
498       checkStack("endElement""ANNOTATION_TYPE_DISPLAYED");
499       resourceData.setAnnotationTypeDisplayed((StringcontentStack.pop());
500     // End ANNOTATION_TYPE_DISPLAYED processing
501     //////////////////////////////////////////////////////////////////
502     else if(elementName.toUpperCase().equals("GUI")) {
503 
504     // End GUI processing
505     //////////////////////////////////////////////////////////////////
506     else if(elementName.toUpperCase().equals("CREOLE")) {
507     // End CREOLE processing
508     //////////////////////////////////////////////////////////////////
509     else if(elementName.toUpperCase().equals("CREOLE-DIRECTORY")) {
510     // End CREOLE-DIRECTORY processing
511     //////////////////////////////////////////////////////////////////
512     else // arbitrary elements get added as features of the resource data
513       if(resourceData != null)
514         resourceData.getFeatures().put(
515           elementName.toUpperCase(),
516           ((contentStack.isEmpty()) null (StringcontentStack.pop())
517         );
518     }
519     //////////////////////////////////////////////////////////////////
520 
521   // endElement
522 
523   /** Called when the SAX parser encounts text (PCDATA) in the XML doc */
524   public void characters(char [] text,int start,int lengththrows SAXException {
525     if(!readCharacterStatus) {
526       contentBuffer = new StringBuffer(new String(text,start,length));
527     else {
528       contentBuffer.append(new String(text,start,length));
529     }
530     readCharacterStatus = true;
531   }
532 
533   /**
534    * This method is called when all characters between specific tags have been read completely
535    */
536   public void charactersAction(char[] text, int start, int length)
537   throws SAXException {
538     // Get the trimmed text between elements
539     String content = new String(text, start, length).trim();
540     // If the entire text is empty or is made from whitespaces then we simply
541     // return
542     if (content.length() == 0return;
543     contentStack.push(content);
544     if(DEBUGOut.println(content);
545   // characters
546   
547   /**
548    * Flatten the currentParamDisjunction map into a single list
549    * ordered by priority.
550    */
551   protected List<Parameter> currentFlattenedDisjunction() {
552     List<Parameter> listToReturn = new ArrayList<Parameter>();
553     for(List<Parameter> l : currentParamDisjunction.values()) {
554       listToReturn.addAll(l);
555     }
556     return listToReturn;
557   }
558 
559   /** Called when the SAX parser encounts white space */
560   public void ignorableWhitespace(char ch[]int start, int length)
561   throws SAXException {
562   // ignorableWhitespace
563 
564   /** Called for parse errors. */
565   public void error(SAXParseException exthrows SAXException {
566     _seh.error(ex);
567   // error
568 
569   /** Called for fatal errors. */
570   public void fatalError(SAXParseException exthrows SAXException {
571     _seh.fatalError(ex);
572   // fatalError
573 
574   /** Called for warnings. */
575   public void warning(SAXParseException exthrows SAXException {
576     _seh.warning(ex);
577   // warning
578 
579 // CreoleXmlHandler