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    
044    package org.jfree.xml.util;
045    
046    import java.io.BufferedInputStream;
047    import java.io.InputStream;
048    import java.net.URL;
049    import java.util.Stack;
050    
051    import javax.xml.parsers.SAXParser;
052    import javax.xml.parsers.SAXParserFactory;
053    
054    import org.jfree.util.Log;
055    import org.jfree.util.ObjectUtilities;
056    import org.jfree.xml.CommentHandler;
057    import org.jfree.xml.ElementDefinitionException;
058    import org.xml.sax.Attributes;
059    import org.xml.sax.InputSource;
060    import org.xml.sax.SAXException;
061    import org.xml.sax.XMLReader;
062    import 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     */
071    public 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    }