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