1   /*
2    *  CreoleXmlHandler.java
3    *
4    *  Copyright (c) 1998-2005, The University of Sheffield.
5    *
6    *  This file is part of GATE (see http://gate.ac.uk/), and is free
7    *  software, licenced under the GNU Library General Public License,
8    *  Version 2, June 1991 (in the distribution as file licence.html,
9    *  and also available at http://gate.ac.uk/gate/licence.html).
10   *
11   *  Hamish Cunningham, 1/Sept/2000
12   *
13   *  $Id: CreoleXmlHandler.java,v 1.45 2005/10/20 11:09:11 ian_roberts Exp $
14   */
15  
16  package gate.creole;
17  
18  import java.net.MalformedURLException;
19  import java.net.URL;
20  import java.util.*;
21  
22  import org.xml.sax.*;
23  import org.xml.sax.helpers.DefaultHandler;
24  
25  import gate.*;
26  import gate.util.*;
27  import gate.xml.SimpleErrorHandler;
28  
29  /** This is a SAX handler for processing <CODE>creole.xml</CODE> files.
30    * It would have been better to write it using DOM or JDOM but....
31    * Resource data objects are created and added to the CREOLE register.
32    * URLs for resource JAR files are added to the GATE class loader.
33    */
34  public class CreoleXmlHandler extends DefaultHandler {
35  
36    /** A stack to stuff PCDATA onto for reading back at element ends.
37     *  (Probably redundant to have a stack as we only push one item
38     *  onto it. Probably. Ok, so I should check, but a) it works, b)
39     *  I'm bald already and c) life is short.)
40     */
41    private Stack contentStack = new Stack();
42  
43    /** The current resource data object */
44    private ResourceData resourceData;
45  
46    /** The current parameter list */
47    private ParameterList currentParamList = new ParameterList();
48  
49    /** The current parameter disjunction */
50    private List currentParamDisjunction = new ArrayList();
51  
52    /** The current parameter */
53    private Parameter currentParam;
54  
55    /** The current element's attribute list */
56    private Attributes currentAttributes;
57  
58    /** Debug flag */
59    private static final boolean DEBUG = false;
60  
61    /** The source URL of the directory file being parsed. */
62    private URL sourceUrl;
63    
64    /**
65     * The URL to the creole.xml file being parsed.
66     */
67    private URL creoleFileUrl;
68  
69    /** This object indicates what to do when the parser encounts an error*/
70    private SimpleErrorHandler _seh = new SimpleErrorHandler();
71  
72    /** This field represents the params map required for autoinstantiation
73      * Its a map from param name to param value.
74      */
75    private FeatureMap currentAutoinstanceParams = null;
76  
77    /** This field holds autoinstanceParams describing the resource that
78      * needs to be instantiated
79      */
80    private List currentAutoinstances = null;
81  
82  
83    /** This is used to capture all data within two tags before calling the actual characters method */
84    private StringBuffer contentBuffer = new StringBuffer("");
85  
86    /** This is a variable that shows if characters have been read */
87    private boolean readCharacterStatus = false;
88  
89    /** Construction */
90    public CreoleXmlHandler(CreoleRegister register, URL directoryUrl, 
91            URL creoleFileUrl) {
92      this.register = register;
93      this.sourceUrl = directoryUrl;
94      this.creoleFileUrl = creoleFileUrl;
95      currentParam = new Parameter(this.creoleFileUrl);
96    } // construction
97  
98    /** The register object that we add ResourceData objects to during parsing.
99      */
100   private CreoleRegister register;
101 
102   /** Called when the SAX parser encounts the beginning of the XML document */
103   public void startDocument() throws GateSaxException {
104     if(DEBUG) Out.prln("start document");
105   } // startDocument
106 
107   /** Called when the SAX parser encounts the end of the XML document */
108   public void endDocument() throws GateSaxException {
109     if(DEBUG) Out.prln("end document");
110     if(! contentStack.isEmpty()) {
111       StringBuffer errorMessage =
112         new StringBuffer("document ended but element stack not empty:");
113       while(! contentStack.isEmpty())
114         errorMessage.append(Strings.getNl()+"  "+(String) contentStack.pop());
115       throw new GateSaxException(errorMessage.toString());
116     }
117   } // endDocument
118 
119   /** A verboase method for Attributes*/
120   private String attributes2String(Attributes atts){
121     StringBuffer strBuf = new StringBuffer("");
122     if (atts == null) return strBuf.toString();
123     for (int i = 0; i < atts.getLength(); i++) {
124      String attName  = atts.getQName(i);
125      String attValue = atts.getValue(i);
126      strBuf.append(" ");
127      strBuf.append(attName);
128      strBuf.append("=");
129      strBuf.append(attValue);
130     }// End for
131     return strBuf.toString();
132   }// attributes2String()
133 
134   /** Called when the SAX parser encounts the beginning of an XML element */
135   public void startElement (String uri, String qName, String elementName,
136                                                              Attributes atts) throws SAXException {
137 
138     // call characterActions
139     if(readCharacterStatus) {
140       readCharacterStatus = false;
141       charactersAction(new String(contentBuffer).toCharArray(),0,contentBuffer.length());
142     }
143 
144     if(DEBUG) {
145       Out.pr("startElement: ");
146       Out.println(
147         elementName + " " +
148         attributes2String(atts)
149       );
150     }
151 
152     // create a new ResourceData when it's a RESOURCE element
153     if(elementName.toUpperCase().equals("RESOURCE")) {
154       resourceData = new ResourceData();
155       resourceData.setFeatures(Factory.newFeatureMap());
156       currentAutoinstances = new ArrayList();
157     }// End if RESOURCE
158 
159     // record the attributes of this element
160     currentAttributes = atts;
161 
162     // When an AUTOINSTANCE element is found a params FeatureMap will
163     // be prepared in order for the resource to be instantiated
164     if (elementName.toUpperCase().equals("AUTOINSTANCE")){
165       currentAutoinstanceParams = Factory.newFeatureMap();
166     }// End if AUTOINSTANCE
167 
168     // When a PARAN start element is found, the parameter would be instantiated
169     // with a value and added to the autoinstanceParams
170     if (elementName.toUpperCase().equals("PARAM")){
171       // The autoinstanceParams should always be != null because of the fact
172       // that PARAM is incuded into an AUTOINSTANCE element.
173       // IF a AUTOINSTANCE starting element would be missing then the
174       // parser would signal this later....
175       if (currentAutoinstanceParams == null)
176         currentAutoinstanceParams = Factory.newFeatureMap();
177       // Take the param's name and value
178       String paramName = currentAttributes.getValue("NAME");
179       String paramStrValue = currentAttributes.getValue("VALUE");
180       if (paramName == null)
181         throw new GateRuntimeException ("Found in creole.xml a PARAM element" +
182         " for resource "+ resourceData.getClassName()+ " without a NAME"+
183         " attribute. Check the file and try again.");
184       if (paramStrValue == null)
185         throw new GateRuntimeException("Found in creole.xml a PARAM element"+
186         " for resource "+ resourceData.getClassName()+ " without a VALUE"+
187         " attribute. Check the file and try again.");
188       // Add the paramname and its value to the autoinstanceParams
189       currentAutoinstanceParams.put(paramName,paramStrValue);
190     }// End if PARAM
191 
192     // process attributes of parameter and GUI elements
193     if(elementName.toUpperCase().equals("PARAMETER")) {
194       if(DEBUG) {
195         for(int i=0, len=currentAttributes.getLength(); i<len; i++) {
196           Out.prln(currentAttributes.getLocalName(i));
197           Out.prln(currentAttributes.getValue(i));
198         }// End for
199       }// End if
200       currentParam.comment = currentAttributes.getValue("COMMENT");
201       currentParam.defaultValueString = currentAttributes.getValue("DEFAULT");
202       currentParam.optional =
203         Boolean.valueOf(currentAttributes.getValue("OPTIONAL")).booleanValue();
204       currentParam.name = currentAttributes.getValue("NAME");
205       currentParam.runtime =
206         Boolean.valueOf(currentAttributes.getValue("RUNTIME")).booleanValue();
207       currentParam.itemClassName =
208                                 currentAttributes.getValue("ITEM_CLASS_NAME");
209       // read the suffixes and transform them to a Set of Strings
210       String suffixes = currentAttributes.getValue("SUFFIXES");
211       Set suffiexesSet = null;
212       if (suffixes != null){
213         suffiexesSet = new HashSet();
214         StringTokenizer strTokenizer = new StringTokenizer(suffixes,";");
215         while(strTokenizer.hasMoreTokens()){
216            suffiexesSet.add(strTokenizer.nextToken());
217         }// End while
218       }// End if
219       currentParam.suffixes = suffiexesSet;
220     }else if(elementName.toUpperCase().equals("GUI")){
221       String typeValue = currentAttributes.getValue("TYPE");
222       if (typeValue != null){
223         if (typeValue.toUpperCase().equals("LARGE"))
224           resourceData.setGuiType(ResourceData.LARGE_GUI);
225         if (typeValue.toUpperCase().equals("SMALL"))
226           resourceData.setGuiType(ResourceData.SMALL_GUI);
227       }// End if
228     }// End if
229 
230     // if there are any parameters awaiting addition to the list, add them
231     // (note that they're not disjunctive or previous "/OR" would have got 'em)
232     if(elementName.toUpperCase().equals("OR")) {
233       if(! currentParamDisjunction.isEmpty()) {
234         currentParamList.addAll(currentParamDisjunction);
235         currentParamDisjunction = new ArrayList();
236       }// End if
237     }// End if
238   } // startElement()
239 
240   /** Utility function to throw exceptions on stack errors. */
241   private void checkStack(String methodName, String elementName)
242   throws GateSaxException {
243     if(contentStack.isEmpty())
244       throw new GateSaxException(
245         methodName + " called for element " + elementName + " with empty stack"
246       );
247   } // checkStack
248 
249   /** Called when the SAX parser encounts the end of an XML element.
250     * This is where ResourceData objects get values set, and where
251     * they are added to the CreoleRegister when we parsed their complete
252     * metadata entries.
253     */
254   public void endElement (String uri, String qName, String elementName)
255                                                     throws GateSaxException, SAXException {
256     // call characterActions
257     if(readCharacterStatus) {
258       readCharacterStatus = false;
259       charactersAction(new String(contentBuffer).toCharArray(),0,contentBuffer.length());
260     }
261 
262     if(DEBUG) Out.prln("endElement: " + elementName);
263 
264     //////////////////////////////////////////////////////////////////
265     if(elementName.toUpperCase().equals("RESOURCE")) {
266       // check for validity of the resource data
267       if(! resourceData.isValid())
268         throw new GateSaxException(
269           "Invalid resource data: " + resourceData.getValidityMessage()
270         );
271 
272       //set the URL to the creole.xml file on the resource data object
273       resourceData.setXmlFileUrl(creoleFileUrl);
274       // add the new resource data object to the creole register
275       register.put(resourceData.getClassName(), resourceData);
276       // if the resource is auto-loading, try and load it
277       if(resourceData.isAutoLoading())
278         try {
279           Class resClass = resourceData.getResourceClass();
280 //          Resource res = Factory.createResource(
281 //              resourceData.getClassName(), Factory.newFeatureMap()
282 //          );
283 //          resourceData.makeInstantiationPersistant(res);
284         } catch(ClassNotFoundException e) {
285           throw new GateSaxException(
286             "Couldn't load autoloading resource: " +
287             resourceData.getName() + "; problem was: " + e
288           );
289         }// End try
290 
291       // if there are any parameters awaiting addition to the list, add them
292       // (note that they're not disjunctive or the "/OR" would have got them)
293       if(! currentParamDisjunction.isEmpty()) {
294         currentParamList.addAll(currentParamDisjunction);
295         currentParamDisjunction = new ArrayList();
296       }// End if
297 
298       // add the parameter list to the resource (and reinitialise it)
299       resourceData.setParameterList(currentParamList);
300       currentParamList = new ParameterList();
301 
302       if(DEBUG) Out.println("added: " + resourceData);
303       // Iterate through autoinstances and try to instanciate them
304       if ( currentAutoinstances != null && !currentAutoinstances.isEmpty()){
305         Iterator iter = currentAutoinstances.iterator();
306         while (iter.hasNext()){
307           FeatureMap autoinstanceParams = (FeatureMap) iter.next();
308           iter.remove();
309           // Try to create the resource.
310           try {
311             Resource res = Factory.createResource(
312                               resourceData.getClassName(), autoinstanceParams);
313             resourceData.makeInstantiationPersistant(res);
314           } catch(ResourceInstantiationException e) {
315             throw new GateSaxException(
316               "Couldn't auto-instantiate resource: " +
317               resourceData.getName() + "; problem was: " + e
318             );
319           }// End try
320         }// End while
321       }// End if
322       currentAutoinstances = null;
323     // End RESOURCE processing
324     //////////////////////////////////////////////////////////////////
325     } else if(elementName.toUpperCase().equals("AUTOINSTANCE")) {
326       //checkStack("endElement", "AUTOINSTANCE");
327       // Cache the auto-instance into the autoins
328       if (currentAutoinstanceParams != null)
329         currentAutoinstances.add(currentAutoinstanceParams);
330     // End AUTOINSTANCE processing
331     //////////////////////////////////////////////////////////////////
332     } else if(elementName.toUpperCase().equals("PARAM")) {
333     // End PARAM processing
334     //////////////////////////////////////////////////////////////////
335     } else if(elementName.toUpperCase().equals("NAME")) {
336       checkStack("endElement", "NAME");
337       resourceData.setName((String) contentStack.pop());
338     // End NAME processing
339     //////////////////////////////////////////////////////////////////
340     } else if(elementName.toUpperCase().equals("JAR")) {
341       checkStack("endElement", "JAR");
342 
343       // add jar file name
344       String jarFileName = (String) contentStack.pop();
345       if(resourceData != null) {
346         resourceData.setJarFileName(jarFileName);
347       }
348 
349       // add jar file URL if there is one
350       if(sourceUrl != null) {
351         String sourceUrlName = sourceUrl.toExternalForm();
352         String separator = "/";
353 
354         if(sourceUrlName.endsWith(separator))
355           separator = "";
356         URL jarFileUrl = null;
357 
358         try {
359           jarFileUrl = new URL(sourceUrlName + separator + jarFileName);
360           if(resourceData != null) {
361             resourceData.setJarFileUrl(jarFileUrl);
362           }
363 
364           // add the jar URL to the class loader
365           if(DEBUG) Out.prln("adding URL to classloader: " + jarFileUrl);
366           Gate.getClassLoader().addURL(jarFileUrl);
367         } catch(MalformedURLException e) {
368           throw new GateSaxException("bad URL " + jarFileUrl + e);
369         }// End try
370       }// End if
371     // End JAR processing
372     //////////////////////////////////////////////////////////////////
373     } else if(elementName.toUpperCase().equals("CLASS")) {
374       checkStack("endElement", "CLASS");
375       resourceData.setClassName((String) contentStack.pop());
376     // End CLASS processing
377     //////////////////////////////////////////////////////////////////
378     } else if(elementName.toUpperCase().equals("COMMENT")) {
379       checkStack("endElement", "COMMENT");
380       resourceData.setComment((String) contentStack.pop());
381     // End COMMENT processing
382     //////////////////////////////////////////////////////////////////
383     } else if(elementName.toUpperCase().equals("INTERFACE")) {
384       checkStack("endElement", "INTERFACE");
385       resourceData.setInterfaceName((String) contentStack.pop());
386     // End INTERFACE processing
387     //////////////////////////////////////////////////////////////////
388     } else if(elementName.toUpperCase().equals("ICON")) {
389       checkStack("endElement", "ICON");
390       resourceData.setIcon((String) contentStack.pop());
391     // End ICON processing
392     //////////////////////////////////////////////////////////////////
393     } else if(elementName.toUpperCase().equals("OR")) {
394       currentParamList.add(currentParamDisjunction);
395       currentParamDisjunction = new ArrayList();
396     // End OR processing
397     //////////////////////////////////////////////////////////////////
398     } else if(elementName.toUpperCase().equals("PARAMETER")) {
399       checkStack("endElement", "PARAMETER");
400       currentParam.typeName = (String) contentStack.pop();
401       currentParamDisjunction.add(currentParam);
402       if(DEBUG)
403         Out.prln("added param: " + currentParam);
404       currentParam = new Parameter(creoleFileUrl);
405     // End PARAMETER processing
406     //////////////////////////////////////////////////////////////////
407     } else if(elementName.toUpperCase().equals("AUTOLOAD")) {
408       resourceData.setAutoLoading(true);
409     // End AUTOLOAD processing
410     //////////////////////////////////////////////////////////////////
411     } else if(elementName.toUpperCase().equals("PRIVATE")) {
412       resourceData.setPrivate(true);
413     // End PRIVATE processing
414     //////////////////////////////////////////////////////////////////
415     } else if(elementName.toUpperCase().equals("TOOL")) {
416       resourceData.setTool(true);
417     // End TOOL processing
418     //////////////////////////////////////////////////////////////////
419     } else if(elementName.toUpperCase().equals("MAIN_VIEWER")) {
420       resourceData.setIsMainView(true);
421     // End MAIN_VIEWER processing
422     //////////////////////////////////////////////////////////////////
423     } else if(elementName.toUpperCase().equals("RESOURCE_DISPLAYED")){
424       checkStack("endElement", "RESOURCE_DISPLAYED");
425       String resourceDisplayed = (String) contentStack.pop();
426       resourceData.setResourceDisplayed(resourceDisplayed);
427       Class resourceDisplayedClass = null;
428       try{
429         resourceDisplayedClass = Gate.getClassLoader().
430                                  loadClass(resourceDisplayed);
431       } catch (ClassNotFoundException ex){
432         throw new GateRuntimeException(
433           "Couldn't get resource class from the resource name :" +
434           resourceDisplayed + " " +ex );
435       }// End try
436     // End RESOURCE_DISPLAYED processing
437     //////////////////////////////////////////////////////////////////
438     } else if(elementName.toUpperCase().equals("ANNOTATION_TYPE_DISPLAYED")){
439       checkStack("endElement", "ANNOTATION_TYPE_DISPLAYED");
440       resourceData.setAnnotationTypeDisplayed((String) contentStack.pop());
441     // End ANNOTATION_TYPE_DISPLAYED processing
442     //////////////////////////////////////////////////////////////////
443     } else if(elementName.toUpperCase().equals("GUI")) {
444 
445     // End GUI processing
446     //////////////////////////////////////////////////////////////////
447     } else if(elementName.toUpperCase().equals("CREOLE")) {
448     // End CREOLE processing
449     //////////////////////////////////////////////////////////////////
450     } else if(elementName.toUpperCase().equals("CREOLE-DIRECTORY")) {
451     // End CREOLE-DIRECTORY processing
452     //////////////////////////////////////////////////////////////////
453     } else { // arbitrary elements get added as features of the resource data
454       if(resourceData != null)
455         resourceData.getFeatures().put(
456           elementName.toUpperCase(),
457           ((contentStack.isEmpty()) ? null : (String) contentStack.pop())
458         );
459     }
460     //////////////////////////////////////////////////////////////////
461 
462   } // endElement
463 
464   /** Called when the SAX parser encounts text (PCDATA) in the XML doc */
465   public void characters(char [] text,int start,int length) throws SAXException {
466     if(!readCharacterStatus) {
467       contentBuffer = new StringBuffer(new String(text,start,length));
468     } else {
469       contentBuffer.append(new String(text,start,length));
470     }
471     readCharacterStatus = true;
472   }
473 
474   /**
475    * This method is called when all characters between specific tags have been read completely
476    */
477   public void charactersAction(char[] text, int start, int length)
478   throws SAXException {
479     // Get the trimmed text between elements
480     String content = new String(text, start, length).trim();
481     // If the entire text is empty or is made from whitespaces then we simply
482     // return
483     if (content.length() == 0) return;
484     contentStack.push(content);
485     if(DEBUG) Out.println(content);
486   } // characters
487 
488   /** Called when the SAX parser encounts white space */
489   public void ignorableWhitespace(char ch[], int start, int length)
490   throws SAXException {
491   } // ignorableWhitespace
492 
493   /** Called for parse errors. */
494   public void error(SAXParseException ex) throws SAXException {
495     _seh.error(ex);
496   } // error
497 
498   /** Called for fatal errors. */
499   public void fatalError(SAXParseException ex) throws SAXException {
500     _seh.fatalError(ex);
501   } // fatalError
502 
503   /** Called for warnings. */
504   public void warning(SAXParseException ex) throws SAXException {
505     _seh.warning(ex);
506   } // warning
507 
508 } // CreoleXmlHandler
509