/*------------------------------------------------------------------------------------
Copyright (c)2000 TogetherSoft Corporation. Patents pending. All rights reserved.
------------------------------------------------------------------------------------*/

package com.togethersoft.modules.xmleditor.xmltree;

import java.util.*;
import java.io.*;
import java.net.*;
import com.togethersoft.modules.xmleditor.dtd.*;
import com.togethersoft.modules.xmleditor.dtd.impl.*;
import com.togethersoft.modules.xmleditor.utils.*;

import org.w3c.dom.*;
import org.xml.sax.*;
import com.ibm.xml.parser.*;
import com.ibm.xml.parsers.*;
import com.togethersoft.together.util.xml.*;
import com.togethersoft.together.util.message.Informator;
import com.togethersoft.together.util.message.DefaultInformatorImpl;
import com.togethersoft.openapi.util.ResourceUtil;


public class XMLTreeManager
{
  private DTDDocumentType  m_documentType;
  private String  m_rootElementTagName;

  private String m_xmlVersion = new String();
  private String m_xmlEncoding = new String();
  private String m_xmlPublicID = new String();
  private String m_xmlSystemID = new String();

  private String m_dtdConfigFileName = null;

  /**
   * @supplierCardinality 0..1
   */
  private ElementInst  m_rootElementInst = null;

  private boolean m_modified = false;
  private boolean m_xmlParserFatalError = false;

  //--------------------------------------------------------------------
  public XMLTreeManager()
  {
  }

  //--------------------------------------------------------------------
  public void initialize (DTDDocumentType documentType, String rootElementTagName)
    throws UnknownXMLElementException
  {
    m_documentType = documentType;
    m_rootElementTagName = rootElementTagName;

    // create root element instance
    m_rootElementInst = createElementInst (rootElementTagName, null);
  }

  //--------------------------------------------------------------------
  public void setVersion (String version)
  {
    m_xmlVersion = (version != null) ? version.trim() : new String();
  }

  //--------------------------------------------------------------------
  public void setEncoding (String encoding)
  {
    m_xmlEncoding = (encoding != null) ? encoding.trim() : new String();
  }

  //--------------------------------------------------------------------
  public void setPublicID (String publicID)
  {
    m_xmlPublicID = (publicID != null) ? publicID.trim() : new String();
  }

  //--------------------------------------------------------------------
  public void setSystemID (String systemID)
  {
    m_xmlSystemID = (systemID != null) ? systemID.trim() : new String();
  }

  //--------------------------------------------------------------------
  public void setDTDConfigFileName (String dtdConfigFileName)
  {
    m_dtdConfigFileName = dtdConfigFileName;
  }

  //--------------------------------------------------------------------
  private void makeDTDConfigFileName (String dtdFileName)
  {
    int extPos = dtdFileName.lastIndexOf ('.');
    String dtdConfigFileName = ((extPos > 0) ? dtdFileName.substring (0, extPos) : dtdFileName) + ".config";
    setDTDConfigFileName (dtdConfigFileName);
  }

  //--------------------------------------------------------------------
  public ElementInst getRootElementInst()
  {
    return m_rootElementInst;
  }

  //--------------------------------------------------------------------
  public ElementInst createElementInst (String tagName, ElementInst parentElementInst)
      throws UnknownXMLElementException
  {
    DTDElementDef elementDef = m_documentType.findElementDef (tagName);
    if (elementDef == null)
      throw new UnknownXMLElementException (tagName);

    return new ElementInst (this, elementDef, parentElementInst);
  }

  //--------------------------------------------------------------------
  public void loadFromDOMDocument (Document domDocument) throws UnknownXMLElementException
  {
    DocumentType doctype = domDocument.getDoctype();

    Element domRootElement = domDocument.getDocumentElement();
    m_rootElementTagName = domRootElement.getTagName();
    m_rootElementInst = createElementInst (m_rootElementTagName, null);
    m_rootElementInst.loadFromDOMElement (domRootElement);
  }

  //--------------------------------------------------------------------
  public void storeToDOMDocument (Document domDocument)
  {
    if (m_rootElementInst == null)
      return;

    // create root element
    Element domRootElement = domDocument.createElement (m_rootElementTagName);
    domDocument.appendChild (domRootElement);

    m_rootElementInst.storeToDOMElement (domRootElement);
  }

  //--------------------------------------------------------------------
  public void load (String xmlFileName) throws IllegalArgumentException
  {
    load (null, xmlFileName);
  }

  //--------------------------------------------------------------------
  public void load (InputStream is, String xmlFileName) throws IllegalArgumentException
  {
    Document domDocument;
    Informator informator = new DefaultInformatorImpl();

    m_dtdConfigFileName = null;

    XMLEntityResolver entityResolver = getEntityResolver (informator);

    TXDOMParser domParser = getTXDOMParser (xmlFileName, entityResolver, informator);
    if (domParser == null)
    {
      String msg = "Unable to open XML parser for " + xmlFileName;
      informator.printError (msg);

      throw new IllegalArgumentException (msg);
    }

    m_xmlParserFatalError = false;

    try
    {
      if (is == null)
        is = new FileInputStream (xmlFileName);

      String encoding = getEncoding();
      encoding = (encoding != null) ? encoding.trim() : new String();

      InputStreamReader ir = (encoding.length() != 0) ? new InputStreamReader (is, encoding)
                                                      : new InputStreamReader (is);
      domParser.parse (new InputSource (ir));
      is.close();

      domDocument = domParser.getDocument();
    }
    catch (Exception e)
    {
      String msg = "XML Parser Exception: " + e + " in " + xmlFileName;
      informator.printError (msg);
      e.printStackTrace();

      throw new IllegalArgumentException (msg);
    }

    if (m_xmlParserFatalError)
    {
      String msg = "XML Parser Fatal Error in " + xmlFileName;
      throw new IllegalArgumentException (msg);
    }

    // load DocumentType

    try
    {
      TXDocument txDocument = (TXDocument) domDocument;
      setVersion (txDocument.getVersion());
      setEncoding (txDocument.getEncoding());

      m_documentType = DTDDocumentType.newInstance();
      ((DTDDocumentTypeImpl) m_documentType).load (txDocument.getDTD());

      // if created DocumentType contains no element definitions
      // then suppose there was no DTD-file specified in opened XML-file;
      // in this case ask for an alternetive DocumentType

      if (m_documentType.elementDefs().size() == 0)
      {
        DTDDocumentType altDocumentType = getAltDocumentType();
        if (altDocumentType != null)
          m_documentType = altDocumentType;
      }
      else
      {
        // load <DTD>.config file

        String dtdConfigFileName = getDTDConfigFileName();
        if (dtdConfigFileName != null)
        {
          try
          {
            InputStream dtdConfigStream = new FileInputStream (dtdConfigFileName);
            m_documentType.loadConfig (dtdConfigStream);
          }
          catch (FileNotFoundException e) { }
        }
      }
    }
    catch (Exception e)
    {
      String msg = "Exception: " + e + " while opening DTD for " + xmlFileName;
      informator.printError (msg);

      if ((e instanceof RuntimeException) && ! (e instanceof IllegalArgumentException))
        DebugTools.showException (null, e);

      throw new IllegalArgumentException (msg);
    }

    try
    {
      loadFromDOMDocument (domDocument);
    }
    catch (UnknownXMLElementException e)
    {
      throw new IllegalArgumentException (e.getMessage());
    }

    m_modified = false;
  }

  //--------------------------------------------------------------------
  private TXDOMParser getTXDOMParser (String xmlFileName, EntityResolver entityResolver, Informator informator)
  {
    TXDOMParser domParser = null;
    try
    {
      domParser = new TXDOMParser();

      // Register own error handler (to print parser error messages into Together message window)
      domParser.setErrorHandler (new XMLErrorHandler (xmlFileName, informator) {
        public void fatalError (SAXParseException exc)
        {
          m_xmlParserFatalError = true;
          super.fatalError (exc);
        }
      });

      // Specify immediate expanding of nodes, no deferred processing
      // domParser.setNodeExpansion (TXDOMParser.FULL);

      domParser.setEntityResolver (entityResolver);
    }
    catch (Exception e)
    {
      informator.printError ("Exception occured " + e.getMessage());
      e.printStackTrace();
    }

    return domParser;
  }

  //--------------------------------------------------------------------
  public void store (PrintWriter writer)
  {
    Informator informator = new DefaultInformatorImpl();
    Document domDocument = XMLUtils.getDocumentInstance (informator);
    if (domDocument == null)
      return;

    storeToDOMDocument (domDocument);

    XMLWriter xmlWriter = new XMLWriter (writer);
    xmlWriter.setPrologue (getXmlProlog());
    xmlWriter.printNode (domDocument);
    xmlWriter.close();
  }

  //--------------------------------------------------------------------
  public void updateTogetherModel()
  {
    if (m_rootElementInst != null)
      m_rootElementInst.updateTogetherModel();
  }

  //--------------------------------------------------------------------
  public void setModified (boolean modified)
  {
    m_modified = modified;
  }

  //--------------------------------------------------------------------
  public boolean isModified()
  {
    return m_modified;
  }

  //--------------------------------------------------------------------
  protected XMLEntityResolver getEntityResolver (Informator informator)
  {
    return new DefaultEntityResolver (this, informator);
  }

  //--------------------------------------------------------------------
  /**
   * Returns full name of <DTD>.config file or null if config not specified.
   * 
   * By default this method returns the name of config-file derived from the name of used DTD-file.
   * However, this method can be redefined in derived classes.
   */
  protected String getDTDConfigFileName()
  {
    return m_dtdConfigFileName;
  }

  //--------------------------------------------------------------------
  /**
   * Returns full name of the alternative DTD-file that will be used with the XML being opened.
   * If the XML contains reference to a different DTD then alternative DTD will be used instead of it. 
   * If no alternative DTD specified the method should return null.
   */
  protected String getAltDTDFileName()
  {
    return (String) (Object) null;
  }

  //--------------------------------------------------------------------
  /**
   * This method is called when the opened XML document has no reference to DTD file. In this case
   * the method can create alternative DocumentType which will be used by XML Editor for this XML file.
   * @return object of alternative DocumentType which should be fully specified (i.e. additional DTD
   *         configuration should loaded in it);
   *         or null if no alternative DocumentType specified
   */
  protected DTDDocumentType getAltDocumentType() throws Exception
  {
    String altDtdFileName = getAltDTDFileName();

    if (altDtdFileName != null)
    {
      try
      {
        DTDDocumentType altDocumentType = DTDDocumentType.newInstance();
        altDocumentType.load (altDtdFileName);

        makeDTDConfigFileName (altDtdFileName);

        String dtdConfigFileName = getDTDConfigFileName();
        if (dtdConfigFileName != null)
        {
          try
          {
            InputStream dtdConfigStream = new FileInputStream (dtdConfigFileName);
            altDocumentType.loadConfig (dtdConfigStream);
          }
          catch (FileNotFoundException e) { }
        }

        return altDocumentType;
      }
      catch (Exception e)
      {
        throw e;
      }
      catch (Throwable e)
      {
        DebugTools.showException (null, e);
        throw new Exception (e.toString() + "-- fatal error");
      }
    }

    return null;
  }

  //--------------------------------------------------------------------
  protected String getXmlProlog()
  {
    String xmlProlog = "<?xml version=\"" + ((m_xmlVersion.length() > 0) ? m_xmlVersion : "1.0") + '\"';

    if (m_xmlEncoding.length() > 0)
      xmlProlog += " encoding=\"" + m_xmlEncoding + '\"';

    xmlProlog += "?>\n\n";
    xmlProlog += "<!DOCTYPE " + m_rootElementTagName;

    if (m_xmlPublicID.length() > 0)
    {
      xmlProlog += " PUBLIC \'" + m_xmlPublicID + '\'';

      if (m_xmlSystemID.length() > 0)
        xmlProlog += " \'" + m_xmlSystemID + '\'';
    }
    else
    {
      if (m_xmlSystemID.length() > 0)
        xmlProlog += " SYSTEM \"" + m_xmlSystemID + '\"';
    }

    xmlProlog += '>';

    return xmlProlog;
  }

  //--------------------------------------------------------------------
  protected String getEncoding()
  {
    return new String();
  }

  //--------------------------------------------------------------------
  public static class DefaultEntityResolver extends XMLEntityResolver
  {
    protected XMLTreeManager m_xmlTreeManager;

    public DefaultEntityResolver (XMLTreeManager xmlTreeManager, Informator informator)
    {
      super (informator);
      m_xmlTreeManager = xmlTreeManager;
    }

    public InputSource resolveEntity(String publicID, String systemID) throws SAXException, IOException
    {
      m_xmlTreeManager.setPublicID (publicID);
      m_xmlTreeManager.setSystemID (systemID);

      if (systemID != null && systemID.length() > 0)
      {
        try
        {
          String dtdFileName = (new URL (systemID)).getFile();
          dtdFileName = dtdFileName.replace ('/', File.separatorChar);
          setConfigFileName (dtdFileName);
        }
        catch (Exception e) { }
      }

      return super.resolveEntity (publicID, systemID);
    }

    public InputSource getResource (String resourceName)
    {
      // if it was an alternative DTD-file specified then primarely try to use it

      String altDtdFileName = m_xmlTreeManager.getAltDTDFileName();
      if (altDtdFileName != null)
      {
        try
        {
          FileInputStream fis = new FileInputStream (altDtdFileName);
          InputStreamReader ir = new InputStreamReader (fis);
          InputSource is = new InputSource (ir);

          setConfigFileName (altDtdFileName);
          return is;
        }
        catch (IOException e) {  }
      }

      InputSource is = super.getResource (resourceName);
      if (is != null)
      {
        File resourceFile = ResourceUtil.getResource (getClass(), resourceName);
        if (resourceFile != null)
          setConfigFileName (resourceFile.getPath());
      }

      return is;
    }

    private void setConfigFileName (String dtdFileName)
    {
      m_xmlTreeManager.makeDTDConfigFileName (dtdFileName);
    }
  }

  //--------------------------------------------------------------------
}
