// $Id: InvCatalogFactory.java,v 1.16 2004/12/15 00:11:45 caron Exp $
/*
 * Copyright 1997-2000 Unidata Program Center/University Corporation for
 * Atmospheric Research, P.O. Box 3000, Boulder, CO 80307,
 * support@unidata.ucar.edu.
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

package thredds.catalog;

import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.*;

import java.io.*;
import java.net.*;
import java.util.*;

/**
 * Reads an XML document and constructs thredds.catalog object.
 *
 * <h3>Example of normal use:</h3>
 *
 * <pre>
    InvCatalogFactory factory = new InvCatalogFactory("default", validate);
    InvCatalogImpl catalog = (InvCatalogImpl) factory.readXML( catalogURI);
    StringBuffer buff = new StringBuffer();
    if (!catalog.check( buff)) {
      javax.swing.JOptionPane.showMessageDialog(this, "Invalid catalog <"+ catalogURI+">\n"+
        buff.toString());
    }
   </pre>
  *
  * <h3>To write out a catalog to XML:</h3>
  *
  <pre>
    // write out catalog to String
    try {
      System.out.println("\Catalog in XML=\n" + factory.writeXML( catalog));
    } catch (IOException e) {
      e.printStackTrace();
    }

    // write out catalog to a file
    if (!factory.writeXML( catalog, filename))
      System.out.println("Catalog failed to write to file=" + filename);

    // write out catalog to a stream, catch exceptions
    try {
      BufferedOutputStream os = new BufferedOutputStream (new FileOutputStream(filename));
      factory.writeXML( catalog, os);
      os.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
  </pre>
 *
 * Implementation details: Uses JAXP to load an XML Parser and construct a DOM tree.
 * Uses a pluggable "converter" to transform the DOM to the thredds.catalog objects.
 * The location of a catalog DTD with "InvCatalog.0.6.dtd" in its systemId will actually be read
 * <ol> <li> as a resource, eg from catalog.jar
 * <li> from "http://www.unidata.ucar.edu/projects/THREDDS/xml/InvCatalog.0.6.dtd".
 * </ol>
 * @author John Caron
 * @version $Id: InvCatalogFactory.java,v 1.16 2004/12/15 00:11:45 caron Exp $
 */

public class InvCatalogFactory {
  public static boolean debugURL = false, debugOpen = false, debugVersion = false;
  public static boolean showParsedXML = false, showStackTrace = false;

  public static boolean debugXML = false, debugDBurl = false;
  public static boolean debugXMLopen = false, showCatalogXML = false;

  /**
   * Get new Factory for reading and writing catalogs.
   * For multithreading, get seperate InvCatalogFactory for each thread.
   * @param validate : do XML validation or not.
   */
  public static InvCatalogFactory getDefaultFactory(boolean validate) {
    return new InvCatalogFactory("default", validate);
  }

  ////////////////////////////////////////////////////////////////////////////////////////////////////

  public String getName() { return ( this.name ); }
  private String name; // , version;
  private DocumentBuilder builder; // JAXP parser
  private InvCatalogConvertIF defaultConverter;

  private HashMap converters = new HashMap(10);
  private HashMap metadataConverters = new HashMap(10);
  private StringBuffer warnMessages;
  private StringBuffer errMessages;
  private StringBuffer fatalMessages;


  /** Constructor.
   * Can use this to read as many catalogs as you want, but should only
   * use in single thread.
   * @param name : optional name to keep track of factories
   * @param validate : do XML validation or not.
   */
  public InvCatalogFactory(String name, boolean validate) {
    this.name = name;
    JaxpFactory jaxp = new JaxpFactory(validate);
    builder = jaxp.getDocumentBuilder();
    warnMessages = jaxp.getErrorMessages();
    errMessages = jaxp.getErrorMessages();
    fatalMessages = jaxp.getFatalMessages();
    setDefaults();
  }

  private void setDefaults() {
    try {
      Class fac6 = Class.forName("thredds.catalog.parser.jdom.InvCatalogFactory6");
      Object fac6o = fac6.newInstance();
      registerCatalogConverter( JaxpFactory.CATALOG_NAMESPACE_06, (InvCatalogConvertIF) fac6o);

      Class fac1 = Class.forName("thredds.catalog.parser.jdom.InvCatalogFactory10");
      Object fac1o = fac1.newInstance();
      defaultConverter = (InvCatalogConvertIF) fac1o;
      registerCatalogConverter( JaxpFactory.CATALOG_NAMESPACE_10, (InvCatalogConvertIF) fac1o);

      // registerMetadataConverter( JaxpFactory.CATALOG_NAMESPACE_10, (MetadataConverterIF) fac1o);
      // registerMetadataConverter( MetadataType.THREDDS, new ThreddsMetadata.Parser());
    } catch ( ClassNotFoundException e) {
      throw new RuntimeException("InvCatalogFactory: no implementing class found: "+e.getMessage());
    } catch ( InstantiationException e) {
      throw new RuntimeException("InvCatalogFactory: instantition failed: "+e.getMessage());
    } catch ( IllegalAccessException e) {
      throw new RuntimeException("InvCatalogFactory: access failed: "+e.getMessage());
    }
  }

  /**
   * Register converters for creating InvCatalogs from specific catalog XML namespaces.
   * This allows the user to add to or override the way catalogs are made.
   *
   * @param namespace : namespace of catalog; acts as the version
   * @param converter : use this factory for this version
   */
  public void registerCatalogConverter(String namespace, InvCatalogConvertIF converter) {
    converters.put( namespace, converter);
  }

  public InvCatalogConvertIF getCatalogConverter(String namespace) {
    return (InvCatalogConvertIF) converters.get(namespace);
  }

  public void setCatalogConverter(InvCatalogImpl cat, String namespace) {
    cat.setCatalogConverter( getCatalogConverter(namespace));
  }

  /**
   * Register metadata converters for reading metadata objects of a certain type or namespace.
   * This allows allows extensible metadata processing.
   *
   * @param key : namespace or metadata type string
   * @param converter : use this MetadataConverterIF for the given key
   * @see InvMetadata
   */
  public void registerMetadataConverter(String key, MetadataConverterIF converter) {
    metadataConverters.put( key, converter);
  }

  /**
   * This allows the possibility of reading a catalog in another thread. The default
   *  implementation does not do that, but a subclass may override and implement.
   *  If the catalog is read successfully, it is passed on to the callback.
   * @param uriString : read this catalog.
   * @param callback : call this if successfully read.
   *
   * @see CatalogSetCallback
   * @see thredds.catalog.ui.CatalogFactoryCancellable
   */
  public void readXMLasynch( String uriString, CatalogSetCallback callback) {
    InvCatalogImpl cat = readXML( uriString);
    callback.setCatalog( cat);
  }

  /**
   * Create an InvCatalog from an XML document at a named URL.
   *   Failures and exceptions are handled
   *   by causing validate() to fail. Therefore, be sure to call validate() before trying
   *   to use the InvCatalog object.
   *
   * @param uriString : the URI name that the XML doc is at.
   * @return an InvCatalogImpl object
   */
  public InvCatalogImpl readXML( String uriString) {
    URI uri = null;
    try {
      uri = new URI( uriString);
    } catch (URISyntaxException e) {
      InvCatalogImpl cat = new InvCatalogImpl( uriString, null, null);
      cat.appendErrorMessage( "**Fatal:  InvCatalogFactory.readXML URISyntaxException on URL ("+
        uriString+") "+e.getMessage()+"\n", true);
      return cat;
    }

    // get ready for XML parsing
    warnMessages.setLength(0);
    errMessages.setLength(0);
    fatalMessages.setLength(0);

    Document doc = null;
    try {
      doc = builder.parse(uriString);
    } catch (Exception e) {
      InvCatalogImpl cat = new InvCatalogImpl( uriString, null, null);
      cat.appendErrorMessage( "**Fatal:  InvCatalogFactory.readXML failed"
        +"\n Exception= "+e.getClass().getName()+" "+e.getMessage()
        +"\n fatalMessages= " +fatalMessages.toString()
        +"\n errMessages= " +errMessages.toString()
        +"\n warnMessages= " +warnMessages.toString()+"\n", true);
      return cat;
    }

    if (fatalMessages.length() > 0) {
      InvCatalogImpl cat = new InvCatalogImpl( uriString, null, null);
      cat.appendErrorMessage( "**Fatal:  InvCatalogFactory.readXML XML Fatal error(s) =\n"+
        fatalMessages.toString()+"\n", true);
      return cat;
    }

    return readXML( doc, uri);
  }

  /**
   * Create an InvCatalog from an InputStream.
   *   Failures and exceptions are handled
   *   by causing validate() to fail. Therefore, be sure to call validate() before trying
   *   to use the InvCatalog object.
   *
   * @param docIs : the InputStream to read from
   * @return an InvCatalogImpl object
   */
  public InvCatalogImpl readXML( InputStream docIs, URI uri) {

    // get ready for XML parsing
    warnMessages.setLength(0);
    errMessages.setLength(0);
    fatalMessages.setLength(0);

    Document doc = null;
    try {
      doc = builder.parse(docIs);
    } catch (Exception e) {
      InvCatalogImpl cat = new InvCatalogImpl( uri.toString(), null, uri);
      cat.appendErrorMessage( "**Fatal:  InvCatalogFactory.readXML failed"
        +"\n Exception= "+e.getClass().getName()+" "+e.getMessage()
        +"\n fatalMessages= " +fatalMessages.toString()
        +"\n errMessages= " +errMessages.toString()
        +"\n warnMessages= " +warnMessages.toString()+"\n", true);
      return cat;
    }

    if (fatalMessages.length() > 0) {
      InvCatalogImpl cat = new InvCatalogImpl( uri.toString(), null, uri);
      cat.appendErrorMessage( "**Fatal:  InvCatalogFactory.readXML XML Fatal error(s) =\n"+
        fatalMessages.toString()+"\n", true);
      return cat;
    }

    return readXML( doc, uri);
  }

  /**
   * Create an InvCatalog from an a DOM tree.
   *   Failures and exceptions are handled
   *   by causing validate() to fail. Therefore, be sure to call validate() before trying
   *   to use the InvCatalog object.
   *
   * @param uri : the URI of the document, used for resolving reletive references.
   * @return an InvCatalogImpl object
   */
  public InvCatalogImpl readXML( org.w3c.dom.Document doc, URI uri) {

    // decide on converter based on namespace
    Element root = doc.getDocumentElement();
    String namespace = root.getNamespaceURI();
    InvCatalogConvertIF fac = (InvCatalogConvertIF) converters.get( namespace);
    if (fac == null) {
      fac = defaultConverter; // LOOK
      if (debugVersion) System.out.println("use default converter "+fac.getClass().getName()+"; no namespace "+namespace);
    } else
      if (debugVersion) System.out.println("use converter "+fac.getClass().getName()+" based on namespace "+namespace);

    // convert to object model
    InvCatalogImpl cat = fac.parseXML( this, doc, uri);
    cat.setCreateFrom( uri.toString());
    cat.setCatalogFactory( this);
    cat.setCatalogConverter( fac);
    cat.finish();

    if (showCatalogXML) {
      System.out.println ("*** catalog/showCatalogXML");
      try { writeXML(cat, System.out); }
      catch (IOException ex) { }
    }

    if (fatalMessages.length() > 0)
      cat.appendErrorMessage(fatalMessages.toString(), true); // makes it invalid
    if (errMessages.length() > 0)
      cat.appendErrorMessage(errMessages.toString(), false); // doesnt make it invalid
    if (errMessages.length() > 0)
      cat.appendErrorMessage(warnMessages.toString(), false); // doesnt make it invalid
    return cat;
  }

  public org.w3c.dom.Element readOtherXML( URI uri) {

    Document doc = null;
    try {
      doc = builder.parse(uri.toString());
    } catch (Exception e) {
      errMessages.append( "**Error:  InvCatalogFactory.readOtherXML failed on "+ uri+
        "\n Exception= "+e.getClass().getName()+" "+e.getMessage()+"\n");
      return null;
    }

    return doc.getDocumentElement();
  }

  /**
   * Write the catalog as an XML document to a String.
   * @param catalog write this catalog
   * @return string containing XML representation
   * @throws IOException
   */
  public String writeXML(InvCatalogImpl catalog) throws IOException {
    ByteArrayOutputStream os = new ByteArrayOutputStream(10000);
    writeXML( catalog, os);
    return os.toString();
  }

  /**
   * Write the catalog as an XML document to the specified stream.
   *
   * @param catalog write this catalog
   * @param os write to this OutputStream
   * @throws IOException on an error.
   */
  public void writeXML(InvCatalogImpl catalog, OutputStream os) throws IOException {
    InvCatalogConvertIF fac = catalog.getCatalogConverter();
    if (fac == null) fac = defaultConverter;
    fac.writeXML( catalog, os);
  }
  /**
   * Write the catalog as an XML document to the specified filename.
   *
   * @param catalog write this catalog
   * @param filename write to this filename
   * @return true if success
   */
  public boolean writeXML(InvCatalogImpl catalog, String filename) {
    try {
      BufferedOutputStream os = new BufferedOutputStream (new FileOutputStream(filename));
      writeXML( catalog, os);
      os.close();
    } catch (IOException e) {
      e.printStackTrace();
      return false;
    }
    return true;
  }

  /**
   * Write the InvCatalogImpl as a InvCatalog 1.0 XML document to a String.
   *
   * @param catalog - the catalog to be written
   * @return a String containing the XML representation
   * @throws IOException when the OutputStream can't be written
   * @throws IllegalStateException when the factory doesn't know how to write a 1.0 document.
   */
  public String writeXML_1_0( InvCatalogImpl catalog )
          throws IOException
  {
    ByteArrayOutputStream os = new ByteArrayOutputStream( 10000 );
    writeXML_1_0( catalog, os );
    return os.toString();
  }

  /**
   * Write the InvCatalogImpl as a InvCatalog 0.6 XML document to a String.
   *
   * @param catalog - the catalog to be written
   * @return a String containing the XML representation
   * @throws IOException           when the OutputStream can't be written
   * @throws IllegalStateException when the factory doesn't know how to write a 0.6 document.
   */
  public String writeXML_0_6( InvCatalogImpl catalog )
          throws IOException
  {
    ByteArrayOutputStream os = new ByteArrayOutputStream( 10000 );
    writeXML_0_6( catalog, os );
    return os.toString();
  }

  /**
   * Write the InvCatalogImpl to the OutputStream as a InvCatalog 1.0 document.
   *
   * @param catalog - the catalog to be written
   * @param os      - the OutputStream to write to
   * @throws IOException           when the OutputStream can't be written
   * @throws IllegalStateException when the factory doesn't know how to write a 1.0 document.
   */
  public void writeXML_1_0( InvCatalogImpl catalog, OutputStream os )
          throws IOException
  {
    this.writeXML_ver( JaxpFactory.CATALOG_NAMESPACE_10, catalog, os );
  }

  /**
   * Write the InvCatalogImpl to the OutputStream as a InvCatalog 0.6 document.
   *
   * @param catalog - the catalog to be written
   * @param os      - the OutputStream to write to
   * @throws IOException           when the OutputStream can't be written
   * @throws IllegalStateException when the factory doesn't know how to write a 0.6 document.
   */
  public void writeXML_0_6( InvCatalogImpl catalog, OutputStream os )
          throws IOException
  {
    this.writeXML_ver( JaxpFactory.CATALOG_NAMESPACE_06, catalog, os );
  }

  /**
   * Write an InvCatalogImpl to an OutputStream as an InvCatalog document using the given namespace.
   *
   * @param namespace - the namespace of the version of InvCatalog document to be written
   * @param catalog - the catalog to be written
   * @param os      - the OutputStream to write to
   * @throws IOException           when the OutputStream can't be written
   * @throws IllegalStateException when the factory doesn't know how to write the version of document requested.
   */
  private void writeXML_ver( String namespace, InvCatalogImpl catalog, OutputStream os)
          throws IOException
  {
    InvCatalogConvertIF converter = this.getCatalogConverter( namespace );
    if ( converter == null )
    {
      String tmpMsg = "This Factory <" + this.getName() + "> does not have a converter for the requested namespace <" + namespace + ">.";
      throw new IllegalStateException( tmpMsg );
    }
    converter.writeXML( catalog, os );
  }

  public void appendErr( String err) { errMessages.append( err); }
  public void appendFatalErr( String err) { fatalMessages.append( err); }
  public void appendWarning( String err) { warnMessages.append( err); }

  public MetadataConverterIF getMetadataConverter( String key) {
    if (key == null) return null;
    return (MetadataConverterIF) metadataConverters.get( key);
  }

  /************************************************************************/

  private static void doOne( InvCatalogFactory fac, String url) {
    System.out.println("***read "+url);
    try {
        InvCatalogImpl cat = (InvCatalogImpl) fac.readXML(url);
        StringBuffer buff = new StringBuffer();
        boolean isValid = cat.check( buff, false);
        System.out.println("catalog <" + cat.getName()+ "> "+ (isValid ? "is" : "is not") + " valid");
        System.out.println(" validation output=\n" + buff);
        System.out.println(" catalog=\n" + fac.writeXML(cat));
      } catch (Exception e) {
        e.printStackTrace();
      }

  }

   /** testing */
  public static void main (String[] args) throws Exception {
    InvCatalogFactory catFactory = InvCatalogFactory.getDefaultFactory(true);
    //InvCatalogFactory catFactoryNo = InvCatalogFactory.getDefaultFactory(false);

    /* 0.6
    doOne(catFactory, "file:///C:/dev/thredds/catalog/test/data/InvCatalog.0.6.xml");
    doOne(catFactory, "file:///C:/dev/thredds/catalog/test/data/ParseFails.xml");
    doOne(catFactory, "file:///C:/dev/thredds/catalog/test/data/malFormed.xml"); // */
    //doOne(catFactory, "file:///C:/dev/thredds/server/resources/initialContent/catalog.xml");

    // 1.0
    doOne(catFactory, "file:///C:/dev/thredds/catalog/test/data/Example1.0.xml");
    /* doOne(catFactory, "file:///C:/dev/thredds/catalog/test/data/TestInherit.1.0.xml"); // */
    //doOne(catFactory, "file:///C:/dev/thredds/catalog/test/data/catalogDev.xml");
    //doOne(catFactory, "http://motherlode.ucar.edu:8088/thredds/catalog.xml"); // */
    //doOne(catFactoryNo, "http://motherlode.ucar.edu:8088/thredds/catalog.xml"); // */

  }

}

/*
 * <h3>Default catalog parsing</h3>
 *
 * InvCatalogFactory allows parsers that handle different catalog versions to be registered
 *  by the application. These factories may also use different XML parsers. Usually you call
 *  InvCatalogFactory.getDefaultFactory(), which already has the default factories for catalog
 *  versions. The default factories validate the catalog XML, and require jdom.jar and a validating
 *  parser like the one that ships with jdk1.4.
 *
 * <h3>Nonvalidating catalog parsing</h3>
 *  To use a nonvalidating, small parser that does not need jdom.jar or any external parser do the
 *  following before you make any other InvCatalogFactory calls:
 * <pre>
    InvCatalogFactory myFactory = new InvCatalogFactory();
    myFactory.registerCatalogFactory("0.6", new thredds.catalog.parser.nano.Catalog6());
    InvCatalogFactory.setDefaultFactory( myFactory);
 * </pre>
 *
 * <h3>Custom catalog parsing</h3>
 *  You may also instantiate an InvCatalogFactory and register your
 *  own factories, for example:
 * <pre>
    InvCatalogFactory myFactory = new InvCatalogFactory();
    myFactory.registerCatalogFactory("0.6", new MyInvCatalogFactory());
    InvCatalogFactory.setDefaultFactory( myFactory);
   </pre>
 *
 * <h3>Custom metadata parsing</h3>
 * InvCatalogFactory also allows you to register parsers for specialized metadata elements that
 * appear in the catalog.xml. These parsers are called "metadata factories" and must implement
 * the InvMetadataFactoryIF interface.
 * Example:
 * <pre>
    InvCatalogFactory catFactory = InvCatalogFactory.getDefaultFactory();
    catFactory.registerMetadataFactory(MetadataType.WMS, new myWMSFactory());
  </pre>
 * In this example, any metadata element of type MetadataType.WMS will be passed
 * to your factory when the catalog is parsed. myWMSFactory will turn it into an
 * Object, which will be available through the InvMetadata.getContentObject() call.

 */

/* Change History:
   $Log: InvCatalogFactory.java,v $
   Revision 1.16  2004/12/15 00:11:45  caron
   2.2.05

   Revision 1.15  2004/07/26 19:32:00  edavis
   Add a writeXML_1_0(InvCatalogImpl):String method and a writeXML_0_6(InvCatalogImpl):String method.

   Revision 1.14  2004/07/23 17:59:12  edavis
   Add a writeXML_1_0() method and a writeXML_0_6() method.

   Revision 1.13  2004/06/12 02:01:09  caron
   dqc 0.3

   Revision 1.12  2004/06/04 22:28:44  caron
   convertTover1; get ver 06 inheritence right

   Revision 1.11  2004/06/04 00:51:53  caron
   release 2.0b

   Revision 1.10  2004/05/11 23:30:27  caron
   release 2.0a

   Revision 1.9  2004/03/02 21:45:21  caron
   v 1.3.1

   Revision 1.8  2004/02/21 02:18:48  caron
   add java.net.Authenticator.setDefault()

   Revision 1.7  2004/02/20 00:49:50  caron
   1.3 changes

   Revision 1.5  2003/12/04 22:27:44  caron
   *** empty log message ***

   Revision 1.4  2003/05/29 21:20:15  john
   optional validation

   Revision 1.3  2003/01/31 18:31:03  john
   adde server changes

   Revision 1.2  2003/01/27 20:23:25  john
   notes

   Revision 1.1.1.1  2002/11/23 17:49:45  caron
   thredds reorg

   Revision 1.5  2002/10/18 18:20:41  caron
   thredds server

   Revision 1.4  2002/09/18 16:24:08  caron
   version 0.6 release

   Revision 1.3  2002/07/02 20:42:53  caron
   add writes

   Revision 1.2  2002/07/01 23:34:57  caron
   release 0.6

   Revision 1.1  2002/06/28 21:28:13  caron
   create vresion 6 object model

 */