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(DEBUG) Out.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(DEBUG) Out.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()+" "+(String) contentStack.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 == null) return 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 atts) throws 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(DEBUG) Out.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(DEBUG) Out.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 = (FeatureMap) iter.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((String) contentStack.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 = (String) contentStack.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((String) contentStack.pop());
413 // End CLASS processing
414 //////////////////////////////////////////////////////////////////
415 } else if(elementName.toUpperCase().equals("COMMENT")) {
416 checkStack("endElement", "COMMENT");
417 resourceData.setComment((String) contentStack.pop());
418 // End COMMENT processing
419 //////////////////////////////////////////////////////////////////
420 } else if(elementName.toUpperCase().equals("HELPURL")) {
421 checkStack("endElement", "HELPURL");
422 resourceData.setHelpURL((String) contentStack.pop());
423 // End HELPURL processing
424 //////////////////////////////////////////////////////////////////
425 } else if(elementName.toUpperCase().equals("INTERFACE")) {
426 checkStack("endElement", "INTERFACE");
427 resourceData.setInterfaceName((String) contentStack.pop());
428 // End INTERFACE processing
429 //////////////////////////////////////////////////////////////////
430 } else if(elementName.toUpperCase().equals("ICON")) {
431 checkStack("endElement", "ICON");
432 resourceData.setIcon((String) contentStack.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 = (String) contentStack.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 != null) priority = 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 = (String) contentStack.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((String) contentStack.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 : (String) contentStack.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 length) throws 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() == 0) return;
543 contentStack.push(content);
544 if(DEBUG) Out.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 ex) throws SAXException {
566 _seh.error(ex);
567 } // error
568
569 /** Called for fatal errors. */
570 public void fatalError(SAXParseException ex) throws SAXException {
571 _seh.fatalError(ex);
572 } // fatalError
573
574 /** Called for warnings. */
575 public void warning(SAXParseException ex) throws SAXException {
576 _seh.warning(ex);
577 } // warning
578
579 } // CreoleXmlHandler
|