    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}