content: /* ========================================================================
   * JCommon : a free general purpose class library for the Java(tm) platform
   * ========================================================================
   *
   * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
   * 
   * Project Info:  http://www.jfree.org/jcommon/index.html
   *
   * This library is free software; you can redistribute it and/or modify it 
   * under the terms of the GNU Lesser General Public License as published by 
   * the Free Software Foundation; either version 2.1 of the License, or 
   * (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful, but 
   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
   * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
   * License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
   * USA.  
   *
   * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
   * in the United States and other countries.]
   * 
   * ------------------------
   * AbstractModelReader.java
   * ------------------------
   * (C)opyright 2003-2005, by Thomas Morgner and Contributors.
   *
   * Original Author:  Thomas Morgner;
   * Contributor(s):   David Gilbert (for Object Refinery Limited);
   *
   * $Id: AbstractModelReader.java,v 1.8 2005/10/18 13:33:53 mungady Exp $
   *
   * Changes
   * -------
   * 12-Nov-2003 : Initial version
   * 25-Nov-2003 : Updated header (DG);
   *
   */
  
  package org.jfree.xml.util;
  
  import java.io.BufferedInputStream;
  import java.io.InputStream;
  import java.net.URL;
  import java.util.Stack;
  
  import javax.xml.parsers.SAXParser;
  import javax.xml.parsers.SAXParserFactory;
  
  import org.jfree.util.Log;
  import org.jfree.util.ObjectUtilities;
  import org.jfree.xml.CommentHandler;
  import org.jfree.xml.ElementDefinitionException;
  import org.xml.sax.Attributes;
  import org.xml.sax.InputSource;
  import org.xml.sax.SAXException;
  import org.xml.sax.XMLReader;
  import org.xml.sax.helpers.DefaultHandler;
  
  /**
   * Loads the class model from an previously written xml file set.
   * This class provides abstract methods which get called during the parsing
   * (similiar to the SAX parsing, but slightly easier to code).
   *
   * This will need a rewrite in the future, when the structure is finished.
   */
  public abstract class AbstractModelReader {
  
      /** The 'START' state. */
      private static final int STATE_START = 0;
      
      /** The 'IN_OBJECT' state. */
      private static final int IN_OBJECT = 1;
  
      /** The 'IGNORE_OBJECT' state. */
      private static final int IGNORE_OBJECT = 2;
      
      /** The 'MAPPING' state. */
      private static final int MAPPING_STATE = 3;
      
      /** The 'CONSTRUCTOR' state. */
      private static final int CONSTRUCTOR_STATE = 4;
  
      /**
       * The SAX2 callback implementation used for parsing the model xml files.
       */
      private class SAXModelHandler extends DefaultHandler {
  
          /** The resource URL. */
          private URL resource;
  
          /** The current state. */
          private int state;
          
          /** Open comments. */
          private Stack openComments;
          
          /** Flag to track includes. */
          private boolean isInclude;
  
          /**
           * Creates a new SAX handler for parsing the model.
           *
           * @param resource  the resource URL.
           * @param isInclude  an include?
           */
          public SAXModelHandler(final URL resource, final boolean isInclude) {
              if (resource == null) {
                  throw new NullPointerException();
              }
              this.resource = resource;
              this.openComments = new Stack();
              this.isInclude = isInclude;
          }
  
          /**
           * Receive notification of the start of an element.
           *
           * @param uri The Namespace URI, or the empty string if the
           *        element has no Namespace URI or if Namespace
           *        processing is not being performed.
           * @param localName The local name (without prefix), or the
           *        empty string if Namespace processing is not being
           *        performed.
           * @param qName The qualified name (with prefix), or the
           *        empty string if qualified names are not available.
           * @param attributes The attributes attached to the element.  If
           *        there are no attributes, it shall be an empty
           *        Attributes object.
           * @exception SAXException Any SAX exception, possibly
           *            wrapping another exception.
           * 
           * @see org.xml.sax.ContentHandler#startElement
           */
          public void startElement(final String uri, final String localName,
                                   final String qName, final Attributes attributes)
              throws SAXException {
  
              setOpenComment(getCommentHandler().getComments());
              this.openComments.push(getOpenComment());
              setCloseComment(null);
  
              try {
  
                  if (!this.isInclude && qName.equals(ClassModelTags.OBJECTS_TAG)) {
                      //Log.debug ("Open Comments: " + openComment);
                      startRootDocument();
                      return;
                  }
  
                  if (getState() == STATE_START) {
                      startRootElement(qName, attributes);
                  }
                  else if (getState() == IGNORE_OBJECT) {
                      return;
                  }
                  else if (getState() == IN_OBJECT) {
                      startObjectElement(qName, attributes);
                  }
                  else if (getState() == MAPPING_STATE) {
                      if (!qName.equals(ClassModelTags.TYPE_TAG)) {
                          throw new SAXException("Expected 'type' tag");
                      }
                      final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
                      final String target = attributes.getValue(ClassModelTags.CLASS_ATTR);
                      handleMultiplexMapping(name, target);
                  }
                  else if (getState() == CONSTRUCTOR_STATE) {
                      if (!qName.equals(ClassModelTags.PARAMETER_TAG)) {
                          throw new SAXException("Expected 'parameter' tag");
                      }
                      final String parameterClass = attributes.getValue(ClassModelTags.CLASS_ATTR);
                      final String tagName = attributes.getValue(ClassModelTags.PROPERTY_ATTR); // optional
                      handleConstructorDefinition(tagName, parameterClass);
                  }
              }
              catch (ObjectDescriptionException e) {
                  throw new SAXException(e);
              }
              finally {
                  getCommentHandler().clearComments();
              }
          }
  
          /**
           * Receive notification of the end of an element.
           *
           * @param uri The Namespace URI, or the empty string if the
           *        element has no Namespace URI or if Namespace
           *        processing is not being performed.
           * @param localName The local name (without prefix), or the
           *        empty string if Namespace processing is not being
           *        performed.
           * @param qName The qualified name (with prefix), or the
           *        empty string if qualified names are not available.
           * @exception SAXException Any SAX exception, possibly
           *            wrapping another exception.
           * @see org.xml.sax.ContentHandler#endElement
           */
          public void endElement(final String uri, final String localName, final String qName)
              throws SAXException {
  
              setOpenComment((String[]) this.openComments.pop());
              setCloseComment(getCommentHandler().getComments());
  
              try {
                  if (!this.isInclude && qName.equals(ClassModelTags.OBJECTS_TAG)) {
                      endRootDocument();
                      return;
                  }
  
                  if (qName.equals(ClassModelTags.OBJECT_TAG)) {
                      if (getState() != IGNORE_OBJECT) {
                          endObjectDefinition();
                      }
                      setState(STATE_START);
                  }
                  else if (qName.equals(ClassModelTags.MAPPING_TAG)) {
                      setState(STATE_START);
                      endMultiplexMapping();
                  }
                  else if (qName.equals(ClassModelTags.CONSTRUCTOR_TAG)) {
                      if (getState() != IGNORE_OBJECT) {
                          setState(IN_OBJECT);
                      }
                  }
              }
              catch (ObjectDescriptionException e) {
                  throw new SAXException(e);
              }
              finally {
                  getCommentHandler().clearComments();
              }
          }
  
          /**
           * Handles the start of an element within an object definition.
           *
           * @param qName The qualified name (with prefix), or the
           *        empty string if qualified names are not available.
           * @param attributes The attributes attached to the element.  If
           *        there are no attributes, it shall be an empty
           *        Attributes object.
           * @throws ObjectDescriptionException if an error occured while
           *        handling this tag
           */
          private void startObjectElement(final String qName, final Attributes attributes)
              throws ObjectDescriptionException {
              if (qName.equals(ClassModelTags.CONSTRUCTOR_TAG)) {
                  setState(CONSTRUCTOR_STATE);
              }
              else if (qName.equals(ClassModelTags.LOOKUP_PROPERTY_TAG)) {
                  final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
                  final String lookupKey = attributes.getValue(ClassModelTags.LOOKUP_ATTR);
                  handleLookupDefinition(name, lookupKey);
              }
              else if (qName.equals(ClassModelTags.IGNORED_PROPERTY_TAG)) {
                  final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
                  handleIgnoredProperty(name);
              }
              else if (qName.equals(ClassModelTags.ELEMENT_PROPERTY_TAG)) {
                  final String elementAtt = attributes.getValue(ClassModelTags.ELEMENT_ATTR);
                  final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
                  handleElementDefinition(name, elementAtt);
              }
              else if (qName.equals(ClassModelTags.ATTRIBUTE_PROPERTY_TAG)) {
                  final String name = attributes.getValue(ClassModelTags.NAME_ATTR);
                  final String attribName = attributes.getValue(ClassModelTags.ATTRIBUTE_ATTR);
                  final String handler = attributes.getValue(ClassModelTags.ATTRIBUTE_HANDLER_ATTR);
                  handleAttributeDefinition(name, attribName, handler);
              }
          }
  
          /**
           * Handles the include or object tag.
           *
           * @param qName The qualified name (with prefix), or the
           *        empty string if qualified names are not available.
           * @param attributes The attributes attached to the element.  If
           *        there are no attributes, it shall be an empty
           *        Attributes object.
           * @throws SAXException if an parser error occured
           * @throws ObjectDescriptionException if an object model related
           *        error occured.
           */
          private void startRootElement(final String qName, final Attributes attributes)
              throws SAXException, ObjectDescriptionException {
  
              if (qName.equals(ClassModelTags.INCLUDE_TAG)) {
                  if (this.isInclude) {
                      Log.warn("Ignored nested include tag.");
                      return;
                  }
                  final String src = attributes.getValue(ClassModelTags.SOURCE_ATTR);
                  try {
                      final URL url = new URL(this.resource, src);
                      startIncludeHandling(url);
                      parseXmlDocument(url, true);
                      endIncludeHandling();
                  }
                  catch (Exception ioe) {
                      throw new ElementDefinitionException
                          (ioe, "Unable to include file from " + src);
                  }
              }
              else if (qName.equals(ClassModelTags.OBJECT_TAG)) {
                  setState(IN_OBJECT);
                  final String className = attributes.getValue(ClassModelTags.CLASS_ATTR);
                  String register = attributes.getValue(ClassModelTags.REGISTER_NAMES_ATTR);
                  if (register != null && register.length() == 0) {
                      register = null;
                  }
                  final boolean ignored = "true".equals(attributes.getValue(ClassModelTags.IGNORE_ATTR));
                  if (!startObjectDefinition(className, register, ignored)) {
                      setState(IGNORE_OBJECT);
                  }
              }
              else if (qName.equals(ClassModelTags.MANUAL_TAG)) {
                  final String className = attributes.getValue(ClassModelTags.CLASS_ATTR);
                  final String readHandler = attributes.getValue(ClassModelTags.READ_HANDLER_ATTR);
                  final String writeHandler = attributes.getValue(ClassModelTags.WRITE_HANDLER_ATTR);
                  handleManualMapping(className, readHandler, writeHandler);
              }
              else if (qName.equals(ClassModelTags.MAPPING_TAG)) {
                  setState(MAPPING_STATE);
                  final String typeAttr = attributes.getValue(ClassModelTags.TYPE_ATTR);
                  final String baseClass = attributes.getValue(ClassModelTags.BASE_CLASS_ATTR);
                  startMultiplexMapping(baseClass, typeAttr);
              }
          }
  
          /**
           * Returns the current state.
           *
           * @return the state.
           */
          private int getState() {
              return this.state;
          }
  
          /**
           * Sets the current state.
           *
           * @param state  the state.
           */
          private void setState(final int state) {
              this.state = state;
          }
      }
  
      /** The comment handler. */
      private CommentHandler commentHandler;
      
      /** The close comments. */
      private String[] closeComment;
      
      /** The open comments. */
      private String[] openComment;
  
      /**
       * Default Constructor.
       */
      public AbstractModelReader() {
          this.commentHandler = new CommentHandler();
      }
  
      /**
       * Returns the comment handler.
       * 
       * @return The comment handler.
       */
      protected CommentHandler getCommentHandler() {
          return this.commentHandler;
      }
  
      /**
       * Returns the close comment. 
       * 
       * @return The close comment.
       */
      protected String[] getCloseComment() {
          return this.closeComment;
      }
  
      /**
       * Returns the open comment. 
       * 
       * @return The open comment.
       */
      protected String[] getOpenComment() {
          return this.openComment;
      }
  
      /**
       * Sets the close comment.
       * 
       * @param closeComment  the close comment.
       */
      protected void setCloseComment(final String[] closeComment) {
          this.closeComment = closeComment;
      }
  
      /**
       * Sets the open comment.
       * 
       * @param openComment  the open comment.
       */
      protected void setOpenComment(final String[] openComment) {
          this.openComment = openComment;
      }
  
      /**
       * Parses an XML document at the given URL.
       * 
       * @param resource  the document URL.
       * 
       * @throws ObjectDescriptionException ??
       */
      protected void parseXml(final URL resource) throws ObjectDescriptionException {
          parseXmlDocument(resource, false);
      }
  
      /**
       * Parses the given specification and loads all includes specified in the files.
       * This implementation does not check for loops in the include files.
       *
       * @param resource  the url of the xml specification.
       * @param isInclude  an include?
       * 
       * @throws org.jfree.xml.util.ObjectDescriptionException if an error occured which prevented the
       * loading of the specifications.
       */
      protected void parseXmlDocument(final URL resource, final boolean isInclude)
          throws ObjectDescriptionException {
          
          try {
              final InputStream in = new BufferedInputStream(resource.openStream());
              final SAXParserFactory factory = SAXParserFactory.newInstance();
              final SAXParser saxParser = factory.newSAXParser();
              final XMLReader reader = saxParser.getXMLReader();
  
              final SAXModelHandler handler = new SAXModelHandler(resource, isInclude);
              try {
                  reader.setProperty
                      ("http://xml.org/sax/properties/lexical-handler",
                          getCommentHandler());
              }
              catch (SAXException se) {
                  Log.debug("Comments are not supported by this SAX implementation.");
              }
              reader.setContentHandler(handler);
              reader.setDTDHandler(handler);
              reader.setErrorHandler(handler);
              reader.parse(new InputSource(in));
              in.close();
          }
          catch (Exception e) {
              // unable to init
              Log.warn("Unable to load factory specifications", e);
              throw new ObjectDescriptionException("Unable to load object factory specs.", e);
          }
      }
  
      /**
       * Start the root document.
       */
      protected void startRootDocument() {
          // nothing required
      }
  
      /**
       * End the root document.
       */
      protected void endRootDocument() {
          // nothing required
      }
  
      /**
       * Start handling an include.
       * 
       * @param resource  the URL.
       */
      protected void startIncludeHandling(final URL resource) {
          // nothing required
      }
  
      /**
       * End handling an include.
       */
      protected void endIncludeHandling() {
          // nothing required
      }
  
      /**
       * Callback method for ignored properties. Such properties get marked so that
       * the information regarding these properties won't get lost.
       *
       * @param name the name of the ignored property.
       */
      protected void handleIgnoredProperty(final String name) {
          // nothing required
      }
  
      /**
       * Handles a manual mapping definition. The manual mapping maps specific
       * read and write handlers to a given base class. Manual mappings always
       * override any other definition.
       *
       * @param className the base class name
       * @param readHandler the class name of the read handler
       * @param writeHandler the class name of the write handler
       * @return true, if the mapping was accepted, false otherwise.
       * @throws ObjectDescriptionException if an unexpected error occured.
       */
      protected abstract boolean handleManualMapping(String className, String readHandler, 
          String writeHandler) throws ObjectDescriptionException;
  
      /**
       * Starts a object definition. The object definition collects all properties of
       * an bean-class and defines, which constructor should be used when creating the
       * class.
       *
       * @param className the class name of the defined object
       * @param register the (optional) register name, to lookup and reference the object
       * later.
       * @param ignored  ??.
       * 
       * @return true, if the definition was accepted, false otherwise.
       * @throws ObjectDescriptionException if an unexpected error occured.
       */
      protected abstract boolean startObjectDefinition(String className, String register,
          boolean ignored) throws ObjectDescriptionException;
  
      /**
       * Handles an attribute definition. This method gets called after the object definition
       * was started. The method will be called for every defined attribute property.
       *
       * @param name the name of the property
       * @param attribName the xml-attribute name to use later.
       * @param handlerClass the attribute handler class.
       * @throws ObjectDescriptionException if an error occured.
       */
      protected abstract void handleAttributeDefinition(String name, String attribName,
                                                        String handlerClass)
          throws ObjectDescriptionException;
  
      /**
       * Handles an element definition. This method gets called after the object definition
       * was started. The method will be called for every defined element property. Element
       * properties are used to describe complex objects.
       *
       * @param name the name of the property
       * @param element the xml-tag name for the child element.
       * @throws ObjectDescriptionException if an error occurs.
       */
      protected abstract void handleElementDefinition(String name, String element) 
          throws ObjectDescriptionException;
  
      /**
       * Handles an lookup definition. This method gets called after the object definition
       * was started. The method will be called for every defined lookup property. Lookup properties
       * reference previously created object using the object's registry name.
       *
       * @param name the property name of the base object
       * @param lookupKey the register key of the referenced object
       * @throws ObjectDescriptionException if an error occured.
       */
      protected abstract void handleLookupDefinition(String name, String lookupKey) 
          throws ObjectDescriptionException;
  
      /**
       * Finializes the object definition.
       *
       * @throws ObjectDescriptionException if an error occures.
       */
      protected abstract void endObjectDefinition() throws ObjectDescriptionException;
  
      /**
       * Starts a multiplex mapping. Multiplex mappings are used to define polymorphic
       * argument handlers. The mapper will collect all derived classes of the given
       * base class and will select the corresponding mapping based on the given type
       * attribute.
       *
       * @param className the base class name
       * @param typeAttr the xml-attribute name containing the mapping key
       */
      protected abstract void startMultiplexMapping(String className, String typeAttr);
  
      /**
       * Defines an entry for the multiplex mapping. The new entry will be activated
       * when the base mappers type attribute contains this <code>typename</code> and
       * will resolve to the handler for the given classname.
       *
       * @param typeName the type value for this mapping.
       * @param className the class name to which this mapping resolves.
       * @throws ObjectDescriptionException if an error occurs.
       */
      protected abstract void handleMultiplexMapping(String typeName, String className) 
          throws ObjectDescriptionException;
  
      /**
       * Finializes the multiplexer mapping.
       *
       * @throws ObjectDescriptionException if an error occurs.
       */
      protected abstract void endMultiplexMapping() throws ObjectDescriptionException;
  
      /**
       * Handles a constructor definition. Only one constructor can be defined for
       * a certain object type. The constructor will be filled using the given properties.
       *
       * @param propertyName the property name of the referenced local property
       * @param parameterClass the parameter class for the parameter.
       * @throws ObjectDescriptionException if an error occured.
       */
      protected abstract void handleConstructorDefinition(String propertyName, String parameterClass)
          throws ObjectDescriptionException;
  
      /**
       * Loads the given class, and ignores all exceptions which may occur
       * during the loading. If the class was invalid, null is returned instead.
       *
       * @param className the name of the class to be loaded.
       * @return the class or null.
       */
      protected Class loadClass(final String className) {
          if (className == null) {
              return null;
          }
          if (className.startsWith("::")) {
              return BasicTypeSupport.getClassRepresentation(className);
          }
          try {
              return ObjectUtilities.getClassLoader(getClass()).loadClass(className);
          }
          catch (Exception e) {
              // ignore buggy classes for now ..
              Log.warn("Unable to load class", e);
              return null;
          }
      }
  
  }
  