001/* ========================================================================
002 * JCommon : a free general purpose class library for the Java(tm) platform
003 * ========================================================================
004 *
005 * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
006 * 
007 * Project Info:  http://www.jfree.org/jcommon/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it 
010 * under the terms of the GNU Lesser General Public License as published by 
011 * the Free Software Foundation; either version 2.1 of the License, or 
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but 
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022 * USA.  
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 * 
027 * ------------------------
028 * AbstractModelReader.java
029 * ------------------------
030 * (C)opyright 2003-2005, by Thomas Morgner and Contributors.
031 *
032 * Original Author:  Thomas Morgner;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *
035 * $Id: AbstractModelReader.java,v 1.8 2005/10/18 13:33:53 mungady Exp $
036 *
037 * Changes
038 * -------
039 * 12-Nov-2003 : Initial version
040 * 25-Nov-2003 : Updated header (DG);
041 *
042 */
043
044package org.jfree.xml.util;
045
046import java.io.BufferedInputStream;
047import java.io.InputStream;
048import java.net.URL;
049import java.util.Stack;
050
051import javax.xml.parsers.SAXParser;
052import javax.xml.parsers.SAXParserFactory;
053
054import org.jfree.util.Log;
055import org.jfree.util.ObjectUtilities;
056import org.jfree.xml.CommentHandler;
057import org.jfree.xml.ElementDefinitionException;
058import org.xml.sax.Attributes;
059import org.xml.sax.InputSource;
060import org.xml.sax.SAXException;
061import org.xml.sax.XMLReader;
062import org.xml.sax.helpers.DefaultHandler;
063
064/**
065 * Loads the class model from an previously written xml file set.
066 * This class provides abstract methods which get called during the parsing
067 * (similiar to the SAX parsing, but slightly easier to code).
068 *
069 * This will need a rewrite in the future, when the structure is finished.
070 */
071public abstract class AbstractModelReader {
072
073    /** The 'START' state. */
074    private static final int STATE_START = 0;
075    
076    /** The 'IN_OBJECT' state. */
077    private static final int IN_OBJECT = 1;
078
079    /** The 'IGNORE_OBJECT' state. */
080    private static final int IGNORE_OBJECT = 2;
081    
082    /** The 'MAPPING' state. */
083    private static final int MAPPING_STATE = 3;
084    
085    /** The 'CONSTRUCTOR' state. */
086    private static final int CONSTRUCTOR_STATE = 4;
087
088    /**
089     * The SAX2 callback implementation used for parsing the model xml files.
090     */
091    private class SAXModelHandler extends DefaultHandler {
092
093        /** The resource URL. */
094        private URL resource;
095
096        /** The current state. */
097        private int state;
098        
099        /** Open comments. */
100        private Stack openComments;
101        
102        /** Flag to track includes. */
103        private boolean isInclude;
104
105        /**
106         * Creates a new SAX handler for parsing the model.
107         *
108         * @param resource  the resource URL.
109         * @param isInclude  an include?
110         */
111        public SAXModelHandler(final URL resource, final boolean isInclude) {
112            if (resource == null) {
113                throw new NullPointerException();
114            }
115            this.resource = resource;
116            this.openComments = new Stack();
117            this.isInclude = isInclude;
118        }
119
120        /**
121         * Receive notification of the start of an element.
122         *
123         * @param uri The Namespace URI, or the empty string if the
124         *        element has no Namespace URI or if Namespace
125         *        processing is not being performed.
126         * @param localName The local name (without prefix), or the
127         *        empty string if Namespace processing is not being
128         *        performed.
129         * @param qName The qualified name (with prefix), or the
130         *        empty string if qualified names are not available.
131         * @param attributes The attributes attached to the element.  If
132         *        there are no attributes, it shall be an empty
133         *        Attributes object.
134         * @exception SAXException Any SAX exception, possibly
135         *            wrapping another exception.
136         * 
137         * @see org.xml.sax.ContentHandler#startElement
138         */
139        public void startElement(final String uri, final String localName,
140                                 final String qName, final Attributes attributes)
141            throws SAXException {
142
143            setOpenComment(getCommentHandler().getComments());
144            this.openComments.push(getOpenComment());
145            setCloseComment(null);
146
147            try {
148
149                if (!this.isInclude && qName.equals(ClassModelTags.OBJECTS_TAG)) {
150                    //Log.debug ("Open Comments: " + openComment);
151                    startRootDocument();
152                    return;
153                }
154
155                if (getState() == STATE_START) {
156                    startRootElement(qName, attributes);
157                }
158                else if (getState() == IGNORE_OBJECT) {
159                    return;
160                }
161                else if (getState() == IN_OBJECT) {
162                    startObjectElement(qName, attributes);
163                }
164                else if (getState() == MAPPING_STATE) {
165                    if (!qName.equals(ClassModelTags.TYPE_TAG)) {
166                        throw new SAXException("Expected 'type' tag");
167                    }
168                    final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
169                    final String target = attributes.getValue(ClassModelTags.CLASS_ATTR);
170                    handleMultiplexMapping(name, target);
171                }
172                else if (getState() == CONSTRUCTOR_STATE) {
173                    if (!qName.equals(ClassModelTags.PARAMETER_TAG)) {
174                        throw new SAXException("Expected 'parameter' tag");
175                    }
176                    final String parameterClass = attributes.getValue(ClassModelTags.CLASS_ATTR);
177                    final String tagName = attributes.getValue(ClassModelTags.PROPERTY_ATTR); // optional
178                    handleConstructorDefinition(tagName, parameterClass);
179                }
180            }
181            catch (ObjectDescriptionException e) {
182                throw new SAXException(e);
183            }
184            finally {
185                getCommentHandler().clearComments();
186            }
187        }
188
189        /**
190         * Receive notification of the end of an element.
191         *
192         * @param uri The Namespace URI, or the empty string if the
193         *        element has no Namespace URI or if Namespace
194         *        processing is not being performed.
195         * @param localName The local name (without prefix), or the
196         *        empty string if Namespace processing is not being
197         *        performed.
198         * @param qName The qualified name (with prefix), or the
199         *        empty string if qualified names are not available.
200         * @exception SAXException Any SAX exception, possibly
201         *            wrapping another exception.
202         * @see org.xml.sax.ContentHandler#endElement
203         */
204        public void endElement(final String uri, final String localName, final String qName)
205            throws SAXException {
206
207            setOpenComment((String[]) this.openComments.pop());
208            setCloseComment(getCommentHandler().getComments());
209
210            try {
211                if (!this.isInclude && qName.equals(ClassModelTags.OBJECTS_TAG)) {
212                    endRootDocument();
213                    return;
214                }
215
216                if (qName.equals(ClassModelTags.OBJECT_TAG)) {
217                    if (getState() != IGNORE_OBJECT) {
218                        endObjectDefinition();
219                    }
220                    setState(STATE_START);
221                }
222                else if (qName.equals(ClassModelTags.MAPPING_TAG)) {
223                    setState(STATE_START);
224                    endMultiplexMapping();
225                }
226                else if (qName.equals(ClassModelTags.CONSTRUCTOR_TAG)) {
227                    if (getState() != IGNORE_OBJECT) {
228                        setState(IN_OBJECT);
229                    }
230                }
231            }
232            catch (ObjectDescriptionException e) {
233                throw new SAXException(e);
234            }
235            finally {
236                getCommentHandler().clearComments();
237            }
238        }
239
240        /**
241         * Handles the start of an element within an object definition.
242         *
243         * @param qName The qualified name (with prefix), or the
244         *        empty string if qualified names are not available.
245         * @param attributes The attributes attached to the element.  If
246         *        there are no attributes, it shall be an empty
247         *        Attributes object.
248         * @throws ObjectDescriptionException if an error occured while
249         *        handling this tag
250         */
251        private void startObjectElement(final String qName, final Attributes attributes)
252            throws ObjectDescriptionException {
253            if (qName.equals(ClassModelTags.CONSTRUCTOR_TAG)) {
254                setState(CONSTRUCTOR_STATE);
255            }
256            else if (qName.equals(ClassModelTags.LOOKUP_PROPERTY_TAG)) {
257                final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
258                final String lookupKey = attributes.getValue(ClassModelTags.LOOKUP_ATTR);
259                handleLookupDefinition(name, lookupKey);
260            }
261            else if (qName.equals(ClassModelTags.IGNORED_PROPERTY_TAG)) {
262                final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
263                handleIgnoredProperty(name);
264            }
265            else if (qName.equals(ClassModelTags.ELEMENT_PROPERTY_TAG)) {
266                final String elementAtt = attributes.getValue(ClassModelTags.ELEMENT_ATTR);
267                final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
268                handleElementDefinition(name, elementAtt);
269            }
270            else if (qName.equals(ClassModelTags.ATTRIBUTE_PROPERTY_TAG)) {
271                final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
272                final String attribName = attributes.getValue(ClassModelTags.ATTRIBUTE_ATTR);
273                final String handler = attributes.getValue(ClassModelTags.ATTRIBUTE_HANDLER_ATTR);
274                handleAttributeDefinition(name, attribName, handler);
275            }
276        }
277
278        /**
279         * Handles the include or object tag.
280         *
281         * @param qName The qualified name (with prefix), or the
282         *        empty string if qualified names are not available.
283         * @param attributes The attributes attached to the element.  If
284         *        there are no attributes, it shall be an empty
285         *        Attributes object.
286         * @throws SAXException if an parser error occured
287         * @throws ObjectDescriptionException if an object model related
288         *        error occured.
289         */
290        private void startRootElement(final String qName, final Attributes attributes)
291            throws SAXException, ObjectDescriptionException {
292
293            if (qName.equals(ClassModelTags.INCLUDE_TAG)) {
294                if (this.isInclude) {
295                    Log.warn("Ignored nested include tag.");
296                    return;
297                }
298                final String src = attributes.getValue(ClassModelTags.SOURCE_ATTR);
299                try {
300                    final URL url = new URL(this.resource, src);
301                    startIncludeHandling(url);
302                    parseXmlDocument(url, true);
303                    endIncludeHandling();
304                }
305                catch (Exception ioe) {
306                    throw new ElementDefinitionException
307                        (ioe, "Unable to include file from " + src);
308                }
309            }
310            else if (qName.equals(ClassModelTags.OBJECT_TAG)) {
311                setState(IN_OBJECT);
312                final String className = attributes.getValue(ClassModelTags.CLASS_ATTR);
313                String register = attributes.getValue(ClassModelTags.REGISTER_NAMES_ATTR);
314                if (register != null && register.length() == 0) {
315                    register = null;
316                }
317                final boolean ignored = "true".equals(attributes.getValue(ClassModelTags.IGNORE_ATTR));
318                if (!startObjectDefinition(className, register, ignored)) {
319                    setState(IGNORE_OBJECT);
320                }
321            }
322            else if (qName.equals(ClassModelTags.MANUAL_TAG)) {
323                final String className = attributes.getValue(ClassModelTags.CLASS_ATTR);
324                final String readHandler = attributes.getValue(ClassModelTags.READ_HANDLER_ATTR);
325                final String writeHandler = attributes.getValue(ClassModelTags.WRITE_HANDLER_ATTR);
326                handleManualMapping(className, readHandler, writeHandler);
327            }
328            else if (qName.equals(ClassModelTags.MAPPING_TAG)) {
329                setState(MAPPING_STATE);
330                final String typeAttr = attributes.getValue(ClassModelTags.TYPE_ATTR);
331                final String baseClass = attributes.getValue(ClassModelTags.BASE_CLASS_ATTR);
332                startMultiplexMapping(baseClass, typeAttr);
333            }
334        }
335
336        /**
337         * Returns the current state.
338         *
339         * @return the state.
340         */
341        private int getState() {
342            return this.state;
343        }
344
345        /**
346         * Sets the current state.
347         *
348         * @param state  the state.
349         */
350        private void setState(final int state) {
351            this.state = state;
352        }
353    }
354
355    /** The comment handler. */
356    private CommentHandler commentHandler;
357    
358    /** The close comments. */
359    private String[] closeComment;
360    
361    /** The open comments. */
362    private String[] openComment;
363
364    /**
365     * Default Constructor.
366     */
367    public AbstractModelReader() {
368        this.commentHandler = new CommentHandler();
369    }
370
371    /**
372     * Returns the comment handler.
373     * 
374     * @return The comment handler.
375     */
376    protected CommentHandler getCommentHandler() {
377        return this.commentHandler;
378    }
379
380    /**
381     * Returns the close comment. 
382     * 
383     * @return The close comment.
384     */
385    protected String[] getCloseComment() {
386        return this.closeComment;
387    }
388
389    /**
390     * Returns the open comment. 
391     * 
392     * @return The open comment.
393     */
394    protected String[] getOpenComment() {
395        return this.openComment;
396    }
397
398    /**
399     * Sets the close comment.
400     * 
401     * @param closeComment  the close comment.
402     */
403    protected void setCloseComment(final String[] closeComment) {
404        this.closeComment = closeComment;
405    }
406
407    /**
408     * Sets the open comment.
409     * 
410     * @param openComment  the open comment.
411     */
412    protected void setOpenComment(final String[] openComment) {
413        this.openComment = openComment;
414    }
415
416    /**
417     * Parses an XML document at the given URL.
418     * 
419     * @param resource  the document URL.
420     * 
421     * @throws ObjectDescriptionException ??
422     */
423    protected void parseXml(final URL resource) throws ObjectDescriptionException {
424        parseXmlDocument(resource, false);
425    }
426
427    /**
428     * Parses the given specification and loads all includes specified in the files.
429     * This implementation does not check for loops in the include files.
430     *
431     * @param resource  the url of the xml specification.
432     * @param isInclude  an include?
433     * 
434     * @throws org.jfree.xml.util.ObjectDescriptionException if an error occured which prevented the
435     * loading of the specifications.
436     */
437    protected void parseXmlDocument(final URL resource, final boolean isInclude)
438        throws ObjectDescriptionException {
439        
440        try {
441            final InputStream in = new BufferedInputStream(resource.openStream());
442            final SAXParserFactory factory = SAXParserFactory.newInstance();
443            final SAXParser saxParser = factory.newSAXParser();
444            final XMLReader reader = saxParser.getXMLReader();
445
446            final SAXModelHandler handler = new SAXModelHandler(resource, isInclude);
447            try {
448                reader.setProperty
449                    ("http://xml.org/sax/properties/lexical-handler",
450                        getCommentHandler());
451            }
452            catch (SAXException se) {
453                Log.debug("Comments are not supported by this SAX implementation.");
454            }
455            reader.setContentHandler(handler);
456            reader.setDTDHandler(handler);
457            reader.setErrorHandler(handler);
458            reader.parse(new InputSource(in));
459            in.close();
460        }
461        catch (Exception e) {
462            // unable to init
463            Log.warn("Unable to load factory specifications", e);
464            throw new ObjectDescriptionException("Unable to load object factory specs.", e);
465        }
466    }
467
468    /**
469     * Start the root document.
470     */
471    protected void startRootDocument() {
472        // nothing required
473    }
474
475    /**
476     * End the root document.
477     */
478    protected void endRootDocument() {
479        // nothing required
480    }
481
482    /**
483     * Start handling an include.
484     * 
485     * @param resource  the URL.
486     */
487    protected void startIncludeHandling(final URL resource) {
488        // nothing required
489    }
490
491    /**
492     * End handling an include.
493     */
494    protected void endIncludeHandling() {
495        // nothing required
496    }
497
498    /**
499     * Callback method for ignored properties. Such properties get marked so that
500     * the information regarding these properties won't get lost.
501     *
502     * @param name the name of the ignored property.
503     */
504    protected void handleIgnoredProperty(final String name) {
505        // nothing required
506    }
507
508    /**
509     * Handles a manual mapping definition. The manual mapping maps specific
510     * read and write handlers to a given base class. Manual mappings always
511     * override any other definition.
512     *
513     * @param className the base class name
514     * @param readHandler the class name of the read handler
515     * @param writeHandler the class name of the write handler
516     * @return true, if the mapping was accepted, false otherwise.
517     * @throws ObjectDescriptionException if an unexpected error occured.
518     */
519    protected abstract boolean handleManualMapping(String className, String readHandler, 
520        String writeHandler) throws ObjectDescriptionException;
521
522    /**
523     * Starts a object definition. The object definition collects all properties of
524     * an bean-class and defines, which constructor should be used when creating the
525     * class.
526     *
527     * @param className the class name of the defined object
528     * @param register the (optional) register name, to lookup and reference the object
529     * later.
530     * @param ignored  ??.
531     * 
532     * @return true, if the definition was accepted, false otherwise.
533     * @throws ObjectDescriptionException if an unexpected error occured.
534     */
535    protected abstract boolean startObjectDefinition(String className, String register,
536        boolean ignored) throws ObjectDescriptionException;
537
538    /**
539     * Handles an attribute definition. This method gets called after the object definition
540     * was started. The method will be called for every defined attribute property.
541     *
542     * @param name the name of the property
543     * @param attribName the xml-attribute name to use later.
544     * @param handlerClass the attribute handler class.
545     * @throws ObjectDescriptionException if an error occured.
546     */
547    protected abstract void handleAttributeDefinition(String name, String attribName,
548                                                      String handlerClass)
549        throws ObjectDescriptionException;
550
551    /**
552     * Handles an element definition. This method gets called after the object definition
553     * was started. The method will be called for every defined element property. Element
554     * properties are used to describe complex objects.
555     *
556     * @param name the name of the property
557     * @param element the xml-tag name for the child element.
558     * @throws ObjectDescriptionException if an error occurs.
559     */
560    protected abstract void handleElementDefinition(String name, String element) 
561        throws ObjectDescriptionException;
562
563    /**
564     * Handles an lookup definition. This method gets called after the object definition
565     * was started. The method will be called for every defined lookup property. Lookup properties
566     * reference previously created object using the object's registry name.
567     *
568     * @param name the property name of the base object
569     * @param lookupKey the register key of the referenced object
570     * @throws ObjectDescriptionException if an error occured.
571     */
572    protected abstract void handleLookupDefinition(String name, String lookupKey) 
573        throws ObjectDescriptionException;
574
575    /**
576     * Finializes the object definition.
577     *
578     * @throws ObjectDescriptionException if an error occures.
579     */
580    protected abstract void endObjectDefinition() throws ObjectDescriptionException;
581
582    /**
583     * Starts a multiplex mapping. Multiplex mappings are used to define polymorphic
584     * argument handlers. The mapper will collect all derived classes of the given
585     * base class and will select the corresponding mapping based on the given type
586     * attribute.
587     *
588     * @param className the base class name
589     * @param typeAttr the xml-attribute name containing the mapping key
590     */
591    protected abstract void startMultiplexMapping(String className, String typeAttr);
592
593    /**
594     * Defines an entry for the multiplex mapping. The new entry will be activated
595     * when the base mappers type attribute contains this <code>typename</code> and
596     * will resolve to the handler for the given classname.
597     *
598     * @param typeName the type value for this mapping.
599     * @param className the class name to which this mapping resolves.
600     * @throws ObjectDescriptionException if an error occurs.
601     */
602    protected abstract void handleMultiplexMapping(String typeName, String className) 
603        throws ObjectDescriptionException;
604
605    /**
606     * Finializes the multiplexer mapping.
607     *
608     * @throws ObjectDescriptionException if an error occurs.
609     */
610    protected abstract void endMultiplexMapping() throws ObjectDescriptionException;
611
612    /**
613     * Handles a constructor definition. Only one constructor can be defined for
614     * a certain object type. The constructor will be filled using the given properties.
615     *
616     * @param propertyName the property name of the referenced local property
617     * @param parameterClass the parameter class for the parameter.
618     * @throws ObjectDescriptionException if an error occured.
619     */
620    protected abstract void handleConstructorDefinition(String propertyName, String parameterClass)
621        throws ObjectDescriptionException;
622
623    /**
624     * Loads the given class, and ignores all exceptions which may occur
625     * during the loading. If the class was invalid, null is returned instead.
626     *
627     * @param className the name of the class to be loaded.
628     * @return the class or null.
629     */
630    protected Class loadClass(final String className) {
631        if (className == null) {
632            return null;
633        }
634        if (className.startsWith("::")) {
635            return BasicTypeSupport.getClassRepresentation(className);
636        }
637        try {
638            return ObjectUtilities.getClassLoader(getClass()).loadClass(className);
639        }
640        catch (Exception e) {
641            // ignore buggy classes for now ..
642            Log.warn("Unable to load class", e);
643            return null;
644        }
645    }
646
647}