101d65943562fca5c2b8fdce77d729d6b2582a85
[mir.git] / source / mir / xml / XmlConfigurator.java
1 package mir.xml;
2
3 import java.io.*;
4 import java.util.*;
5 import java.lang.reflect.*;
6 import org.xml.sax.helpers.DefaultHandler;
7 import org.xml.sax.*;
8 import javax.xml.parsers.ParserConfigurationException;
9 import javax.xml.parsers.SAXParser;
10 import javax.xml.parsers.SAXParserFactory;
11
12
13 import mir.misc.ConfigException;
14 import mir.misc.Location;
15
16 /**
17  * Configures a based on
18  * a XML config file.
19  *
20  * Based and inspired by ant's XmlConfigurator.java
21  * It's a simplified version of the ant parser with
22  * the addition of calling set* methods for defined
23  * classes as well as the inclusion of a method to
24  * add parameters (nested tags) that are required.
25  * (the addRequired method) in the config file.
26  * that part is from tomcat.
27  *
28  * much code is stolen from ant ProjectHelper.java.
29  *
30  * @author -mh <heckmann@hbe.ca>
31  * @version 2001.10.21
32  */
33
34 public class XmlConfigurator {
35
36     private static SAXParserFactory parserFactory = null;
37
38     private SAXParser saxParser;
39     //private Project project;
40     private File configFile;
41     private File configFileParent;
42     private Locator locator;
43
44     private SaxContext saxContext;
45
46     XmlMatch requiredXmlMatch[]=new XmlMatch[256]; //maximum amount of rules
47     int requiredXmlMatchCount=0;
48     boolean matched[] = new boolean[256];
49     int matchedCount=0;
50
51     XmlMatch mustComeFirstMatch[]=new XmlMatch[256]; //maximum amount of rules
52     int comeFirstMatchCount=0;
53
54     Property comesFirstArr[]=new Property[128];
55     int comesFirstCount=0;
56
57     Property propertyArr[]=new Property[128];
58     int propertyCount=0;
59
60     private static XmlConfigurator instance = new XmlConfigurator();
61     public static XmlConfigurator getInstance() { return instance; }
62
63     /**
64      * Configures the Project with the contents of the specified XML file.
65      */
66     public void configure(File configFile) throws ConfigException {
67         setConfigFile(configFile);
68         parse();
69     }
70
71     /**
72      * konstruktor. private so no one calls "new" on us.
73      */
74     private XmlConfigurator() {}
75
76
77     /**
78      * Constructs a new Ant parser for the specified XML file.
79      */
80     private void setConfigFile(File configFile) {
81         this.configFile = new File(configFile.getAbsolutePath());
82         configFileParent = new File(this.configFile.getParent());
83         saxContext = new SaxContext();
84     }
85
86     /**
87      * Parses the config file.
88      */
89     private void parse() throws ConfigException {
90         FileInputStream inputStream = null;
91         InputSource inputSource = null;
92         
93         try {
94             SAXParserFactory factory = SAXParserFactory.newInstance();
95             saxParser = factory.newSAXParser();
96                 
97             String uri = "file:" + configFile.getAbsolutePath().replace('\\', '/');
98             for (int index = uri.indexOf('#'); index != -1; index = uri.indexOf('#')) {
99                 uri = uri.substring(0, index) + "%23" + uri.substring(index+1);
100             }
101             
102             inputStream = new FileInputStream(configFile);
103             inputSource = new InputSource(inputStream);
104             inputSource.setSystemId(uri);
105             saxParser.parse(inputSource, new RootHandler());
106             if(matchedCount < requiredXmlMatchCount) {
107                 for( int i=0; i<requiredXmlMatchCount; i++) {
108                     if( !matched[i] )
109                         throw new ConfigException("Error parsing config file, missing required element: "+requiredXmlMatch[i].toString());
110                 }
111             }
112             try {
113                 for(int i=0; i<comesFirstCount;i++) {
114                     comesFirstArr[i].set();
115                 }
116                 for(int i=0; i<propertyCount;i++) {
117                     propertyArr[i].set();
118                     System.out.println("about to set: "+i);
119                 }
120             } catch (Exception e) {
121                 throw new SAXParseException(e.toString(), locator);
122             }
123 }
124         catch(ParserConfigurationException exc) {
125             throw new ConfigException("Parser has not been configured correctly", exc);
126         }
127         catch(SAXParseException exc) {
128             Location location =
129                 new Location(configFile.toString(), exc.getLineNumber(), exc.getColumnNumber());
130
131             Throwable t = exc.getException();
132             if (t instanceof ConfigException) {
133                 ConfigException be = (ConfigException) t;
134                 if (be.getLocation() == Location.UNKNOWN_LOCATION) {
135                     be.setLocation(location);
136                 }
137                 throw be;
138             }
139             
140             throw new ConfigException(exc.getMessage(), t, location);
141         }
142         catch(SAXException exc) {
143             Throwable t = exc.getException();
144             if (t instanceof ConfigException) {
145                 throw (ConfigException) t;
146             }
147             throw new ConfigException(exc.getMessage(), t);
148         }
149         catch(FileNotFoundException exc) {
150             throw new ConfigException(exc);
151         }
152         catch(IOException exc) {
153             throw new ConfigException("Error reading config file", exc);
154         }
155         finally {
156             if (inputStream != null) {
157                 try {
158                     inputStream.close();
159                 }
160                 catch (IOException ioe) {
161                     // ignore this
162                 }
163             }
164         }
165     }
166
167     /**
168      * The common superclass for all sax event handlers in Ant. Basically
169      * throws an exception in each method, so subclasses should override
170      * what they can handle.
171      *
172      * Each type of xml element (task, target, etc) in ant will
173      * have its own subclass of AbstractHandler.
174      *
175      * In the constructor, this class    takes over the handling of sax
176      * events from the parent handler, and returns
177      * control back to the parent in the endElement method.
178      */
179     private class AbstractHandler extends DefaultHandler {
180         protected ContentHandler parentHandler;
181
182         public AbstractHandler(ContentHandler parentHandler) {
183             this.parentHandler = parentHandler;
184
185             // Start handling SAX events
186             try {
187                 saxParser.getXMLReader().setContentHandler(this);
188             } catch (SAXException e) {
189                 throw new ConfigException("Error getting XMLReader",e);
190             }
191                 
192         }
193
194         public void startElement(String uri, String tag, String qName, Attributes attrs) throws SAXParseException {
195             throw new SAXParseException("Unexpected element \"" + tag + "\"", locator);
196         }
197
198         public void characters(char[] buf, int start, int end) throws SAXParseException {
199             String s = new String(buf, start, end).trim();
200
201             if (s.length() > 0) {
202                 throw new SAXParseException("Unexpected text \"" + s + "\"", locator);
203             }
204         }
205
206         /**
207          * Called when this element and all elements nested into it have been
208          * handeled.
209          */
210         protected void finished() {}
211
212         public void endElement(String uri, String tag, String qName) throws SAXException {
213             finished();
214             // Let parent resume handling SAX events
215             saxParser.getXMLReader().setContentHandler(parentHandler);
216         }
217     }
218
219     /**
220      * Handler for the root element. It's only child must be the "mir" element.
221      */
222     private class RootHandler extends DefaultHandler {
223
224         /**
225          * resolve file: URIs as relative to the build file.
226          */
227         public InputSource resolveEntity(String publicId,
228                                          String systemId) {
229         
230         
231             if (systemId.startsWith("file:")) {
232                 String path = systemId.substring(5);
233                 int index = path.indexOf("file:");
234                 
235                 // we only have to handle these for backward compatibility
236                 // since they are in the FAQ.
237                 while (index != -1) {
238                     path = path.substring(0, index) + path.substring(index + 5);
239                     index = path.indexOf("file:");
240                 }
241                 
242                 String entitySystemId = path;
243                 index = path.indexOf("%23");
244                 // convert these to #
245                 while (index != -1) {
246                     path = path.substring(0, index) + "#" + path.substring(index + 3);
247                     index = path.indexOf("%23");
248                 }
249
250                 File file = new File(path);
251                 if (!file.isAbsolute()) {
252                     file = new File(configFileParent, path);
253                 }
254                 
255                 try {
256                     InputSource inputSource = new InputSource(new FileInputStream(file));
257                     inputSource.setSystemId("file:" + entitySystemId);
258                     return inputSource;
259                 } catch (FileNotFoundException fne) {
260                     System.out.println(file.getAbsolutePath()+" could not be found");
261                 }
262             }
263             // use default if not file or file not found
264             return null;
265         }
266
267         public void startElement(String uri, String tag, String qName, Attributes attrs) throws SAXParseException {
268             if (tag.equals("mir")) {
269                 new MirHandler(this).init(tag, attrs);
270             } else {
271                 throw new SAXParseException("Config file is not of expected XML type", locator);
272             }
273         }
274
275         public void setDocumentLocator(Locator locator) {
276             XmlConfigurator.this.locator = locator;
277         }
278     }
279
280     /**
281      * Handler for the top level "project" element.
282      */
283     private class MirHandler extends AbstractHandler {
284         public MirHandler(ContentHandler parentHandler) {
285             super(parentHandler);
286         }
287
288         public void init(String tag, Attributes attrs) throws SAXParseException {
289             String name = null;
290
291             for (int i = 0; i < attrs.getLength(); i++) {
292                 String key = attrs.getLocalName(i);
293                 String value = attrs.getValue(i);
294
295                 if (key.equals("name")) {
296                     name = value;
297                 } else {
298                     throw new SAXParseException("Unexpected attribute \"" + attrs.getLocalName(i) + "\"", locator);
299                 }
300             }
301
302             if (name == null) {
303                 throw new SAXParseException("The default attribute of \"name\" is required", 
304                                             locator);
305             }
306             
307             saxContext.push(tag);
308             matchedCount += checkRequiredTag(saxContext);
309
310             //MirConfig.setName(name);
311
312         }
313
314         public void startElement(String uri, String name, String qName, Attributes attrs) throws SAXParseException {
315             if (name.equals("class")) {
316                 handleClassdef(name, attrs);
317             } else {
318                 throw new SAXParseException("Unexpected element \"" + name + "\"", locator);
319             }
320         }
321
322         public void finished() {
323             System.out.println("COUNT "+saxContext.getTagCount()+" TAG "+saxContext.getTag(saxContext.getTagCount()-1));
324             saxContext.pop();
325         }
326
327         private void handleClassdef(String name, Attributes attrs) throws SAXParseException {
328             (new ClassHandler(this)).init(name, attrs);
329         }
330
331     }
332
333     /**
334      * Handler for "class" elements.
335      */
336     private class ClassHandler extends AbstractHandler {
337
338         Class classN;
339
340         public ClassHandler(ContentHandler parentHandler) {
341             super(parentHandler);
342         }
343
344         public void init(String tag, Attributes attrs) throws SAXParseException {
345             String name = null;
346
347             for (int i = 0; i < attrs.getLength(); i++) {
348                 String key = attrs.getLocalName(i);
349                 String value = attrs.getValue(i);
350
351                 if (key.equals("name")) {
352                     name = value;
353                 } else {
354                     throw new SAXParseException("Unexpected attribute \"" + key + "\"", locator);
355                 }
356             }
357
358             if (name == null) {
359                 throw new SAXParseException("class element appears without a \"name\" attribute", locator);
360             }
361
362             saxContext.push(tag+":"+name);
363             matchedCount += checkRequiredTag(saxContext);
364             try {
365                 classN=Class.forName(name, false, this.getClass().getClassLoader());
366             } catch (ClassNotFoundException e) {
367                 throw new ConfigException("Error invoking class: \""+name+
368                     "\"",e);
369             }
370
371         }
372
373         public void startElement(String uri, String name, String qName, Attributes attrs) throws SAXParseException {
374             if (name.equals("property")) {
375                 handleProperties(name, attrs);
376             } else {
377                 throw new SAXParseException("Unexpected element \"" + name + "\"", locator);
378             }
379         }
380
381         public void finished() {
382             System.out.println("COUNT "+saxContext.getTagCount()+" TAG "+saxContext.getTag(saxContext.getTagCount()-1));
383             System.out.println("COUNT "+saxContext.getTagCount());
384             saxContext.pop();
385         }
386
387         private void handleProperties(String name, Attributes attrs) throws SAXParseException {
388             (new PropertiesHandler(this, classN )).init(name, attrs);
389         }
390
391     }
392
393     /**
394      * Handler for all property elements.
395      */
396     private class PropertiesHandler extends AbstractHandler {
397         private Class classN;
398
399         public PropertiesHandler(ContentHandler parentHandler, Class classN) {
400             super(parentHandler);
401
402             this.classN = classN;
403         }
404
405         public void init(String tag, Attributes attrs) throws SAXParseException {
406             String name=null;
407             String value=null;
408
409             for (int i = 0; i < attrs.getLength(); i++) {
410                 String key = attrs.getLocalName(i);
411                 String v = attrs.getValue(i);
412
413                 if (key.equals("name")) {
414                     name = v;
415                 } else if (key.equals("value")) {
416                     value = v; 
417                 } else {
418                     throw new SAXParseException("Unexpected attribute \"" + key + "\"", locator);
419                 }
420             }
421
422             if (name == null) {
423                 throw new SAXParseException("property element appears without a \"name\" attribute", locator);
424             }
425             if (value == null) {
426                 throw new SAXParseException("property element appears without a \"value\" attribute", locator);
427             }
428             saxContext.push(tag+":"+name);
429             matchedCount += checkRequiredTag(saxContext);
430
431             //finally add it to the lists
432             //to be processed later
433             if (checkComesFirstTag(saxContext)) {
434                 comesFirstArr[comesFirstCount]=new Property(classN, name, value);
435                 comesFirstCount++;
436             } else {
437                 propertyArr[propertyCount]=new Property(classN, name, value);
438                 propertyCount++;
439             }
440         }
441
442         protected void finished() {
443             System.out.println("COUNT "+saxContext.getTagCount()+" TAG "+saxContext.getTag(saxContext.getTagCount()-1));
444             System.out.println("COUNT "+saxContext.getTagCount());
445             saxContext.pop();
446         }
447
448     }
449
450     public void addComesFirstTag(String xmlPath) {
451         mustComeFirstMatch[comeFirstMatchCount]=new XmlMatch(xmlPath);
452         comeFirstMatchCount++;
453     }
454
455     private boolean checkComesFirstTag(SaxContext ctx) {
456         for( int i=0; i<comeFirstMatchCount; i++ ) {
457             if( mustComeFirstMatch[i].match(ctx) ) {
458                 return true;
459             }
460         }
461         return false;
462     }
463
464     public void addRequiredTag(String xmlPath) {
465         requiredXmlMatch[requiredXmlMatchCount]=new XmlMatch(xmlPath);
466         matched[requiredXmlMatchCount]=false;
467         requiredXmlMatchCount++;
468     }
469
470     private int checkRequiredTag(SaxContext ctx) {
471         int matchCount=0;
472         for( int i=0; i<requiredXmlMatchCount; i++ ) {
473             if( requiredXmlMatch[i].match(ctx) ) {
474                 matched[i]=true;
475                 matchCount++;
476             }
477         }
478         return matchCount;
479     }
480
481
482     private static String capitalize(String name) {
483         if (name == null || name.length() == 0) {
484             return name;
485         }
486         char chars[] = name.toCharArray();
487         chars[0] = Character.toUpperCase(chars[0]);
488         return new String(chars);
489     }
490
491     private class Property {
492         Class classN;
493         String name;
494         String value;
495
496         public Property( Class classN, String name, String value) {
497             this.classN=classN;
498             this.name=name;
499             this.value=value;
500         }
501             
502         /** Find a method with the right name
503          * If found, call the method ( if param is int or boolean we'll convert 
504          * value to the right type before) - that means you can have setDebug(1).
505          */
506         public void set() throws Exception {
507             
508             String setter= "set" +capitalize(name);
509
510             try {
511                 Method methods[]=classN.getMethods();
512                 Method setPropertyMethod=null;
513
514                 // First, the ideal case - a setFoo( String ) method
515                 for( int i=0; i< methods.length; i++ ) {
516                     Class paramT[]=methods[i].getParameterTypes();
517                     if( setter.equals( methods[i].getName() ) &&
518                         paramT.length == 1 &&
519                         "java.lang.String".equals( paramT[0].getName())) {
520
521                         methods[i].invoke( null, new Object[] { value } );
522                         return;
523                     }
524                 } //end for
525
526                 // Try a setFoo ( int ), (float) or ( boolean )
527                 for( int i=0; i< methods.length; i++ ) {
528                     boolean ok=true;
529                     if( setter.equals( methods[i].getName() ) &&
530                         methods[i].getParameterTypes().length == 1) {
531
532                         // match - find the type and invoke it
533                         Class paramType=methods[i].getParameterTypes()[0];
534                         Object params[]=new Object[1];
535                         if ("java.lang.Integer".equals( paramType.getName()) ||
536                             "int".equals( paramType.getName())) {
537                             try {
538                                 params[0]=new Integer(value);
539                             } catch( NumberFormatException ex ) {ok=false;}
540                         } else if ("java.lang.Float".equals( paramType.getName()) ||
541                             "float".equals( paramType.getName())) {
542                             try {
543                                 params[0]=new Float(value);
544                             } catch( NumberFormatException ex ) {ok=false;}
545                         } else if ("java.lang.Boolean".equals( paramType.getName()) ||
546                             "boolean".equals( paramType.getName())) {
547                             params[0]=new Boolean(value);
548                         } else {
549                             throw new Exception("Unknown type " + paramType.getName() + "for property \""+name+"\"with value \""+value+"\"");
550                         }
551
552                         if( ok ) {
553                             System.out.println("XXX: " + methods[i] + " " + classN + " " + params[0] );
554                             methods[i].invoke( null, params );
555                             return; 
556                         } //end if
557                     } //end if setter
558                 } //end for
559
560                 //if we got this far it means we were not successful in setting the
561                 //property
562                 throw new Exception("Count not find method \""+setter+"\" in Class \""+classN.getName()+"\" in order to set property \""+name+"\"");
563
564             } catch( SecurityException ex1 ) {
565                 throw new Exception("SecurityException for " + classN.getName() + " " +  name + "="  + value  +")" );
566                 //if( ctx.getDebug() > 1 ) ex1.printStackTrace();
567             } catch (IllegalAccessException iae) {
568                 throw new Exception("IllegalAccessException for " + classN.getName() + " " +  name + "="  + value  +")" );
569                 //if( ctx.getDebug() > 1 ) iae.printStackTrace();
570             } catch (InvocationTargetException ie) {
571                 throw new Exception("InvocationTargetException for " + classN.getName() + " " +  name + "="  + value  +")" );
572                 //if( ctx.getDebug() > 1 ) ie.printStackTrace();
573             }
574         }
575     }
576
577 }