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