// $Id: InvCatalogFactory10.java,v 1.8 2004/12/15 00:11:45 caron Exp $
/*
 * Copyright 1997-2004 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.parser.jdom;

import thredds.catalog.*;
import thredds.datatype.*;

import org.jdom.*;
import org.jdom.input.*;
import org.jdom.output.*;

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

/**
 * Inventory Catalog parser, version 1.0.
 * Reads InvCatalog.xml files, constructs object representation.
 *
 * @author John Caron
 * @version $Id: InvCatalogFactory10.java,v 1.8 2004/12/15 00:11:45 caron Exp $
 */

public class InvCatalogFactory10 implements InvCatalogConvertIF, MetadataConverterIF  {

  private static final Namespace defNS = Namespace.getNamespace(JaxpFactory.CATALOG_NAMESPACE_10);
  private static final Namespace xlinkNS = Namespace.getNamespace("xlink", JaxpFactory.XLINK_NAMESPACE);

  private InvCatalogFactory factory = null;
  private DOMBuilder builder = new DOMBuilder();
  private DOMOutputter domOut = null;

  private boolean debugMetadataRead = false;

  public InvCatalogImpl parseXML( InvCatalogFactory fac, org.w3c.dom.Document domDoc, URI uri) {
    this.factory = fac;

    // convert to JDOM document
    Document doc = builder.build(domDoc);

    if (InvCatalogFactory.showParsedXML) {
      XMLOutputter xmlOut = new XMLOutputter();
      System.out.println ("*** catalog/showParsedXML = \n"+xmlOut.outputString(doc)+"\n*******");
    }

    InvCatalogImpl catalog = readCatalog( doc.getRootElement(), uri);

    return catalog;
  }


  private HashMap metadataHash = new HashMap(10);
  public void registerMetadataConverter(MetadataType type, MetadataConverterIF converter) {
    metadataHash.put(type, converter);
  }

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

  protected InvAccessImpl readAccess( InvDatasetImpl dataset, Element accessElem) {
    String urlPath = accessElem.getAttributeValue("urlPath");
    String serviceName = accessElem.getAttributeValue("serviceName");
    String dataFormat = accessElem.getAttributeValue("dataFormat");

    return new InvAccessImpl( dataset, urlPath, serviceName, null, dataFormat, readDataSize( accessElem));
  }

  protected InvCatalogImpl readCatalog( Element catalogElem, URI baseURI) {
    String name = catalogElem.getAttributeValue("name");
    String expires = catalogElem.getAttributeValue("expires");
    String version = catalogElem.getAttributeValue("version");
    InvCatalogImpl catalog = new InvCatalogImpl( name, version, makeDateType(expires, null, null), baseURI);

    // read top-level services
    java.util.List sList = catalogElem.getChildren("service", defNS);
    for (int j=0; j< sList.size(); j++) {
      InvService s = readService((Element) sList.get(j), baseURI);
      catalog.addService(s);
    }

    // read top-level properties
    java.util.List pList = catalogElem.getChildren("property", defNS);
    for (int j=0; j< pList.size(); j++) {
      InvProperty s = readProperty((Element) pList.get(j));
      catalog.addProperty(s);
    }

     // look for top-level dataset and catalogRefs elements (keep them in order)
    java.util.List allChildren = catalogElem.getChildren();
    for (int j=0; j< allChildren.size(); j++) {
      Element e = (Element) allChildren.get(j);
      if (e.getName().equals("dataset")) {
        catalog.addDataset( readDataset( catalog, null, e, baseURI));
      } else if (e.getName().equals("catalogRef")) {
        catalog.addDataset( readCatalogRef( catalog, null, e, baseURI));
      }
    }

    return catalog;
  }

  protected InvCatalogRef readCatalogRef( InvCatalog cat, InvDatasetImpl parent, Element catRefElem, URI baseURI) {
    String title = catRefElem.getAttributeValue("title", xlinkNS);
    String href = catRefElem.getAttributeValue("href", xlinkNS);

    InvCatalogRef catRef = new InvCatalogRef( parent, title, href);

    // look for documentation
    java.util.List docList = catRefElem.getChildren("documentation", defNS);
    for (int j=0; j< docList.size(); j++) {
      InvDocumentation doc = readDocumentation(cat, (Element) docList.get(j));
      catRef.addDocumentation( doc);
     }

    return catRef;
  }

  protected ThreddsMetadata.Contributor readContributor(Element elem) {
    if (elem == null) return null;
    return new ThreddsMetadata.Contributor( elem.getText(), elem.getAttributeValue("role"));
  }

  protected ThreddsMetadata.Vocab readControlledVocabulary(Element elem) {
    if (elem == null) return null;
    return new ThreddsMetadata.Vocab( elem.getText(), elem.getAttributeValue("vocabulary"));
  }

    // read a dataset element
  protected InvDatasetImpl readDataset( InvCatalogImpl catalog, InvDatasetImpl parent, Element dsElem, URI base) {

      // deal with aliases
    String name = dsElem.getAttributeValue("name");
    String alias = dsElem.getAttributeValue("alias");
    if (alias != null) {
      InvDatasetImpl ds = (InvDatasetImpl) catalog.findDatasetByID( alias);
      if (ds == null)
        factory.appendErr(" ** Parse error: dataset named "+name+" has illegal alias = "+alias+"\n");
      return new InvDatasetImplProxy(name, ds);
    }

     // read attributes
    String authority = dsElem.getAttributeValue("authority");
    String collectionTypeName = dsElem.getAttributeValue("collectionType");
    String dataTypeName = dsElem.getAttributeValue("dataType");
    String harvest = dsElem.getAttributeValue("harvest");
    String id = dsElem.getAttributeValue("ID");
    String serviceName = dsElem.getAttributeValue("serviceName");
    String urlPath = dsElem.getAttributeValue("urlPath");

    DataType dataType = null;
    if (dataTypeName != null) {
      dataType = DataType.getType( dataTypeName);
      if (dataType == null) {
        dataType = new DataType( dataTypeName);
        factory.appendWarning(" ** warning: non-standard data type = "+dataTypeName+"\n");
      }
    }

    InvDatasetImpl dataset = new InvDatasetImpl( parent, name, dataType, serviceName, urlPath);

    if (authority != null) dataset.setAuthority( authority);
    if (id != null) dataset.setID( id);
    if (harvest != null) dataset.setHarvest( harvest.equalsIgnoreCase("true"));

    if (collectionTypeName != null) {
      CollectionType collectionType = CollectionType.getType( collectionTypeName);
      if (collectionType == null) {
        collectionType = new CollectionType( collectionTypeName);
        factory.appendWarning(" ** warning: non-standard collection type = "+collectionTypeName+"\n");
      }
      dataset.setCollectionType( collectionType);
    }

    catalog.addDatasetByID( dataset); // need to do immed for alias processing

        // look for services
    java.util.List serviceList = dsElem.getChildren("service", defNS);
    for (int j=0; j< serviceList.size(); j++) {
      InvService s = readService( (Element) serviceList.get(j), base);
      dataset.addService( s);
    }

    // look for thredds metadata (not inherited)
    ThreddsMetadata tmg = dataset.getLocalMetadata();
    readThreddsMetadata( catalog, dataset, dsElem, tmg);

      // look for access elements
    java.util.List aList = dsElem.getChildren("access", defNS);
    for (int j=0; j< aList.size(); j++) {
      InvAccessImpl a = readAccess( dataset, (Element) aList.get(j));
      dataset.addAccess( a);
     }

     // look for nested dataset and catalogRefs elements (keep them in order)
    java.util.List allChildren = dsElem.getChildren();
    for (int j=0; j< allChildren.size(); j++) {
      Element e = (Element) allChildren.get(j);
      if (e.getName().equals("dataset")) {
        InvDatasetImpl ds = readDataset( catalog, dataset, e, base);
        if (ds != null)
          dataset.addDataset( ds);
      } else if (e.getName().equals("catalogRef")) {
        InvDatasetImpl ds = readCatalogRef( catalog, dataset, e, base);
        dataset.addDataset( ds);
      }
     }

    if (InvCatalogFactory.debugXML) System.out.println (" Dataset added: "+ dataset.dump());
    return dataset;
  }

  protected DateType readDate(Element elem) {
    if (elem == null) return null;
    String format =  elem.getAttributeValue("format");
    String type =  elem.getAttributeValue("type");
    return makeDateType( elem.getText(), format, type);
  }

  protected DateType makeDateType(String text, String format, String type) {
    if (text == null) return null;
    try {
      return new DateType( text, format, type);
    } catch (java.text.ParseException e) {
      factory.appendErr(" ** Parse error: Bad date format = "+text+"\n");
      return null;
    }
  }

  protected TimeDuration readDuration(Element elem) {
    if (elem == null) return null;
    String text = null;
    try {
      text = elem.getText();
      return new TimeDuration( text);
    } catch (java.text.ParseException e) {
      factory.appendErr(" ** Parse error: Bad duration format = "+text+"\n");
      return null;
    }
  }

  protected InvDocumentation readDocumentation( InvCatalog cat, Element s) {
    String href = s.getAttributeValue("href", xlinkNS);
    String title = s.getAttributeValue("title", xlinkNS);
    String type = s.getAttributeValue("type"); // not XLink type
    String content = s.getTextNormalize();

    URI uri = null;
    if (href != null) {
      try {
        uri = cat.resolveUri(href);
      } catch (Exception e) {
        factory.appendErr(" ** Invalid documentation href = "+href+" "+e.getMessage()+"\n");
      }
    }

    InvDocumentation doc = new InvDocumentation( href, uri, title, type, content);

    // LOOK XTML ?? !!

    if (InvCatalogFactory.debugXML) System.out.println (" Documentation added: "+ doc);
    return doc;
  }

  protected double readDouble(Element elem) {
    if (elem == null) return Double.NaN;
    String text = elem.getText();
    try {
      return Double.parseDouble( text);
    } catch (NumberFormatException e) {
      factory.appendErr(" ** Parse error: Bad double format = "+text+"\n");
      return Double.NaN;
    }
  }

  protected ThreddsMetadata.GeospatialCoverage readGeospatialCoverage( Element gcElem) {
    if (gcElem == null) return null;

    String zpositive = gcElem.getAttributeValue("zpositive");

    ThreddsMetadata.Range northsouth = readGeospatialRange( gcElem.getChild("northsouth", defNS));
    ThreddsMetadata.Range eastwest = readGeospatialRange( gcElem.getChild("eastwest", defNS));
    ThreddsMetadata.Range updown = readGeospatialRange( gcElem.getChild("updown", defNS));

    // look for names
    ArrayList names = new ArrayList();
    java.util.List list = gcElem.getChildren("name", defNS);
    for (int j=0; j< list.size(); j++) {
      ThreddsMetadata.Vocab name = readControlledVocabulary((Element) list.get(j));
      names.add(name);
    }

    ThreddsMetadata.GeospatialCoverage gc = new ThreddsMetadata.GeospatialCoverage(
        eastwest, northsouth, updown, names, zpositive);
    return gc;
  }

  protected ThreddsMetadata.Range readGeospatialRange( Element spElem) {
    if (spElem == null) return null;

    double start = readDouble( spElem.getChild("start", defNS));
    double size = readDouble( spElem.getChild("size", defNS));
    double resolution = readDouble( spElem.getChild("resolution", defNS));
    String units = spElem.getAttributeValue("units");

    return new ThreddsMetadata.Range( start, size, resolution, units);
  }

  protected InvMetadata readMetadata( InvCatalog catalog, InvDatasetImpl dataset, Element mdataElement) {
    // there are 6 cases to deal with: threddsNamespace vs not & inline vs Xlink & hasConverter or not
    // (the hasConverter only applies when its not threddsNamespace, giving 6 cases)
    // this factory is the converter for threddsNamespace metadata
    //  and also handles non-threddsNamespace when there is no converter, in which case it just
    //   propagates the inline dom elements

    // figure out the namespace
    Namespace namespace;
    List inlineElements = mdataElement.getChildren();
    if (inlineElements.size() > 0) // look at the namespace of the children, if they exist
      namespace = ((Element) inlineElements.get( 0)).getNamespace();
    else
      namespace = mdataElement.getNamespace(); // will be thredds

    String mtype = mdataElement.getAttributeValue("metadataType");
    String href = mdataElement.getAttributeValue("href", xlinkNS);
    String title = mdataElement.getAttributeValue("title", xlinkNS);
    String inheritedS = mdataElement.getAttributeValue("inherited");
    boolean inherited = (inheritedS != null) && inheritedS.equalsIgnoreCase("true");

    boolean isThreddsNamespace = ((mtype == null) || mtype.equalsIgnoreCase("THREDDS")) &&
        namespace.getURI().equals(JaxpFactory.CATALOG_NAMESPACE_10);

    // see if theres a converter for it.
    MetadataConverterIF metaConverter = factory.getMetadataConverter( namespace.getURI());
    if (metaConverter == null) metaConverter = factory.getMetadataConverter( mtype);
    if (metaConverter != null) {
      if (debugMetadataRead) System.out.println("found factory for metadata type = "+mtype+" namespace = "+
        namespace+"="+metaConverter.getClass().getName());

      // see if theres any inline content
      Object contentObj = null;
      if (inlineElements.size() > 0) {
        contentObj = metaConverter.readMetadataContent( dataset, this.toDOM( mdataElement));
        return new InvMetadata( dataset, mtype, namespace.getURI(), namespace.getPrefix(),
                              inherited, false, metaConverter, contentObj);

      } else { // otherwise it  must be an Xlink; defer reading
        return new InvMetadata(dataset, href, title, mtype, namespace.getURI(),
          namespace.getPrefix(), inherited, false, metaConverter);
      }
    }

    // the case where its not ThreddsMetadata, but theres no converter
    if (!isThreddsNamespace) {
      if (inlineElements.size() > 0) {
        // just hold onto the jdom elements as the "content" LOOK should be DOM?
        return new InvMetadata( dataset, mtype, namespace.getURI(), namespace.getPrefix(),
                              inherited, false, this, mdataElement);

      } else { // otherwise it must be an Xlink, never read
        return new InvMetadata(dataset, href, title, mtype, namespace.getURI(),
          namespace.getPrefix(), inherited, false, null);
      }

    }

    // the case where its ThreddsMetadata
    if (inlineElements.size() > 0) {
      ThreddsMetadata tmg = new ThreddsMetadata(false);
      readThreddsMetadata( catalog, dataset, mdataElement, tmg);
      return new InvMetadata( dataset, mtype, namespace.getURI(), namespace.getPrefix(),
                            inherited, true, this, tmg);

    } else { // otherwise it  must be an Xlink; defer reading
      return new InvMetadata(dataset, href, title, mtype, namespace.getURI(),
        namespace.getPrefix(), inherited, true, this);
    }

  }

  /* MetadataConverterIF */
  public Object readMetadataContent( InvDataset dataset, org.w3c.dom.Element mdataElement) {
    InvMetadata m = readMetadata( dataset.getParentCatalog(), (InvDatasetImpl) dataset, toJDOM( mdataElement));
    return m.getThreddsMetadata();
  }

     // this is only called for ThredddsMetadata
  public Object readMetadataContentFromURL( InvDataset dataset, java.net.URI uri) throws java.io.IOException {
    // open and read the referenced catalog XML
    if (debugMetadataRead) System.out.println(" readMetadataContentFromURL = " + uri);
    org.w3c.dom.Element mdataElement = factory.readOtherXML( uri);
    if (mdataElement == null) {
      factory.appendErr(" ** failed to read thredds metadata at = "+uri+" for dataset"+dataset.getName()+"\n");
      return null;
    }

    Object contentObject = readMetadataContent( dataset, mdataElement);
    if (debugMetadataRead) System.out.println(" convert to " + contentObject.getClass().getName());
    return contentObject;
  }

  // dummy LOOK
  public boolean validateMetadataContent(Object contentObject, StringBuffer out) { return true; }

  public void addMetadataContent( org.w3c.dom.Element mdataElement, Object contentObject) { }

  protected InvProperty readProperty( Element s) {
    String name = s.getAttributeValue("name");
    String value = s.getAttributeValue("value");
    return new InvProperty( name, value);
  }

  protected ThreddsMetadata.Source readSource(Element elem) {
    if (elem == null) return null;
    ThreddsMetadata.Vocab name = readControlledVocabulary( elem.getChild("name", defNS));
    Element contact = elem.getChild("contact", defNS);
    if (contact == null) {
      factory.appendErr(" ** Parse error: Missing contact element in = "+elem.getName()+"\n");
      return null;
    }
    return new ThreddsMetadata.Source( name, contact.getAttributeValue("url"), contact.getAttributeValue("email"));
  }

  protected InvService readService( Element s, URI baseURI) {
    String name = s.getAttributeValue("name");
    String type = s.getAttributeValue("serviceType");
    String serviceBase = s.getAttributeValue("base");
    String suffix = s.getAttributeValue("suffix");
    String desc = s.getAttributeValue("desc");

    InvService service = new InvService( name, type, serviceBase, suffix, desc);

    java.util.List propertyList = s.getChildren("property", defNS);
    for (int j=0; j< propertyList.size(); j++) {
      InvProperty p = readProperty( (Element) propertyList.get(j));
      service.addProperty( p);
     }

    // nested services
    java.util.List serviceList = s.getChildren("service", defNS);
    for (int j=0; j< serviceList.size(); j++) {
      InvService ss = readService( (Element) serviceList.get(j), baseURI);
      service.addService( ss);
     }

    if (InvCatalogFactory.debugXML) System.out.println (" Service added: "+ service);
    return service;
  }

  protected double readDataSize(Element parent) {
    Element elem = parent.getChild("dataSize", defNS);
    if (elem == null) return Double.NaN;

    double size;
    String sizeS = elem.getText();
    try {
      size = Double.parseDouble( sizeS);
    } catch (NumberFormatException e) {
      factory.appendErr(" ** Parse error: Bad double format in size element = "+sizeS+"\n");
      return Double.NaN;
    }

    String units = elem.getAttributeValue("units");
    char c = Character.toUpperCase(units.charAt(0));
    if (c == 'K') size *= 1000;
    else if (c == 'M') size *= 1000 * 1000;
    else if (c == 'G') size *= 1000 * 1000 * 1000;
    else if (c == 'T') size *= 1000 * 1000 * 1000 * 1000;
    else if (c == 'P') size *= 1000 * 1000 * 1000 * 1000 * 1000;
    return size;
  }

  protected ThreddsMetadata.TimeCoverage readTimeCoverage( Element tElem) {
    if (tElem == null) return null;

    DateType start = readDate( tElem.getChild("start", defNS));
    DateType end = readDate( tElem.getChild("end", defNS));
    TimeDuration duration = readDuration( tElem.getChild("duration", defNS));
    TimeDuration resolution = readDuration( tElem.getChild("resolution", defNS));

    ThreddsMetadata.TimeCoverage tc = new ThreddsMetadata.TimeCoverage( start, end, duration, resolution);
    return tc;
  }

  protected void readThreddsMetadata( InvCatalog catalog, InvDatasetImpl dataset,
      Element parent, ThreddsMetadata tmg) {
    List list;

     // look for creators - kind of a Source
    list = parent.getChildren("creator", defNS);
    for (int j=0; j< list.size(); j++) {
      tmg.addCreator( readSource( (Element) list.get(j)));
    }

     // look for contributors
    list = parent.getChildren("contributor", defNS);
    for (int j=0; j< list.size(); j++) {
      tmg.addContributor( readContributor( (Element) list.get(j)));
    }

     // look for dates
    list = parent.getChildren("date", defNS);
    for (int j=0; j< list.size(); j++) {
      DateType d = readDate( (Element) list.get(j));
      tmg.addDate( d);
     }

     // look for documentation
    list = parent.getChildren("documentation", defNS);
    for (int j=0; j< list.size(); j++) {
      InvDocumentation doc = readDocumentation( catalog, (Element) list.get(j));
      tmg.addDocumentation( doc);
     }

     // look for keywords - kind of a controlled vocabulary
    list = parent.getChildren("keyword", defNS);
    for (int j=0; j< list.size(); j++) {
      tmg.addKeyword( readControlledVocabulary( (Element) list.get(j)));
    }

    // look for metadata
    java.util.List mList = parent.getChildren("metadata", defNS);
    for (int j=0; j< mList.size(); j++) {
      InvMetadata m = readMetadata( catalog, dataset, (Element) mList.get(j));
      if (m != null) {
          tmg.addMetadata(m);
      }
    }

     // look for projects - kind of a controlled vocabulary
    list = parent.getChildren("project", defNS);
    for (int j=0; j< list.size(); j++) {
      tmg.addProject( readControlledVocabulary( (Element) list.get(j)));
    }

     // look for properties
    list = parent.getChildren("property", defNS);
    for (int j=0; j< list.size(); j++) {
      InvProperty p = readProperty( (Element) list.get(j));
      tmg.addProperty( p);
     }

     // look for publishers - kind of a Source
    list = parent.getChildren("publisher", defNS);
    for (int j=0; j< list.size(); j++) {
      tmg.addPublisher( readSource( (Element) list.get(j)));
    }

     // look for variables
    list = parent.getChildren("variables", defNS);
    for (int j=0; j< list.size(); j++) {
      ThreddsMetadata.Variables vars = readVariables( catalog, dataset, (Element) list.get(j));
      tmg.addVariables( vars);
     }

    // can only be one each of these kinds
    ThreddsMetadata.GeospatialCoverage gc = readGeospatialCoverage( parent.getChild("geospatialCoverage", defNS));
    if (gc != null) tmg.setGeospatialCoverage( gc);

    ThreddsMetadata.TimeCoverage tc = readTimeCoverage( parent.getChild("timeCoverage", defNS));
    if (tc != null) tmg.setTimeCoverage( tc);

    Element serviceNameElem = parent.getChild("serviceName", defNS);
    if (serviceNameElem != null) tmg.setServiceName( serviceNameElem.getText());

    Element authElem = parent.getChild("authority", defNS);
    if (authElem != null) tmg.setAuthority( authElem.getText());

    Element dataTypeElem = parent.getChild("dataType", defNS);
    if (dataTypeElem != null) {
      String dataTypeName = dataTypeElem.getText();
      if ((dataTypeName != null) && (dataTypeName.length() > 0)) {
        DataType dataType = DataType.getType( dataTypeName);
        if (dataType == null) {
          dataType = new DataType( dataTypeName);
          factory.appendWarning(" ** warning: non-standard data type = "+dataTypeName+"\n");
        }
        tmg.setDataType( dataType);
      }
    }

    Element dataFormatElem = parent.getChild("dataFormat", defNS);
    if (dataFormatElem != null) {
      String dataFormatTypeName = dataFormatElem.getText();
      if ((dataFormatTypeName != null) && (dataFormatTypeName.length() > 0)) {
        DataFormatType dataFormatType = DataFormatType.getType( dataFormatTypeName);
        if (dataFormatType == null) {
          dataFormatType = new DataFormatType( dataFormatTypeName);
          factory.appendWarning(" ** warning: non-standard data type = "+dataFormatTypeName+"\n");
        }
        tmg.setDataFormatType( dataFormatType);
      }
    }

    double size = readDataSize(parent);
    if ( !Double.isNaN( size))
      tmg.setDataSize( size);
  }

  protected ThreddsMetadata.Variable readVariable( Element varElem) {
    if (varElem == null) return null;

    String name = varElem.getAttributeValue("name");
    String vocabulary_name = varElem.getAttributeValue("vocabulary_name");
    String units = varElem.getAttributeValue("units");

    return new ThreddsMetadata.Variable( name, vocabulary_name, units);
  }


  protected ThreddsMetadata.Variables readVariables( InvCatalog cat, InvDataset ds, Element varsElem) {
    if (varsElem == null) return null;

    String vocab = varsElem.getAttributeValue("vocabulary");
    String vocabHref = varsElem.getAttributeValue("href", xlinkNS);

    URI vocabUri = null;
    if (vocabHref != null) {
      try {
        vocabUri = cat.resolveUri(vocabHref);
      } catch (Exception e) {
        factory.appendErr(" ** Invalid Variables vocabulary URI = "+vocabHref+" "+e.getMessage()+"\n");
      }
    }

    java.util.List vlist = varsElem.getChildren("variable", defNS);

    String mapHref = null;
    URI mapUri = null;
    Element map = varsElem.getChild("variableMap", defNS);
    if (map != null) {
      mapHref = map.getAttributeValue("href", xlinkNS);
      try {
        mapUri = cat.resolveUri(mapHref);
      } catch (Exception e) {
        factory.appendErr(" ** Invalid Variables map URI = "+mapHref+" "+e.getMessage()+"\n");
      }
    }

    if ((mapUri != null) && vlist.size() > 0) { // cant do both
      factory.appendErr(" ** Catalog error: cant have variableMap and variable in same element (dataset = "+
                        ds.getName()+"\n");
      mapUri = null;
    }

    ThreddsMetadata.Variables variables = new ThreddsMetadata.Variables( vocab, vocabHref, vocabUri, mapHref, mapUri);

    for (int j=0; j< vlist.size(); j++) {
      ThreddsMetadata.Variable v = readVariable( (Element) vlist.get(j));
      variables.addVariable( v);
    }

    // read in variable map LOOK: would like to defer
    if (mapUri != null) {
      org.w3c.dom.Element domElement = factory.readOtherXML( mapUri);
      if (domElement != null) {
        Element varsElement = toJDOM(domElement);
        List list = varsElement.getChildren("variable", defNS);
        for (int j = 0; j < list.size(); j++) {
          ThreddsMetadata.Variable v = readVariable( (Element) list.get(j));
          variables.addVariable(v);
        }
      }

    }

    return variables;
  }


  /************************************************************************/
  // Writing XML from objects

  /**
   * Write the catalog as an XML document to the specified stream.
   *
   * @param catalog write this catalog
   * @param os write to this OutputStream
   * @throws IOException
   */
  public void writeXML(InvCatalogImpl catalog, OutputStream os) throws IOException {
    // Output the document, use standard formatter
    XMLOutputter fmt = new XMLOutputter();
    fmt.setNewlines(true);
    fmt.setIndent("  ");
    fmt.output(writeCatalog(catalog), os);
  }

  public Document writeCatalog(InvCatalogImpl cat) {
    Element rootElem = new Element("catalog", defNS);
    Document doc = new Document(rootElem);

    // attributes
    if (cat.getName() != null)
      rootElem.setAttribute("name", cat.getName());
    //rootElem.setAttribute("version", "1.0");
    rootElem.addNamespaceDeclaration(xlinkNS);
    if (cat.getExpires() != null)
      rootElem.setAttribute("expires", cat.getExpires().toString());

    // services
    Iterator iter = cat.getServices().iterator();
    while ( iter.hasNext()) {
      InvService service = (InvService) iter.next();
      rootElem.addContent( writeService( service));
    }

    // properties
    iter = cat.getProperties().iterator();
    while ( iter.hasNext()) {
      InvProperty p = (InvProperty) iter.next();
      rootElem.addContent( writeProperty( p));
    }

    // datasets
    iter = cat.getDatasets().iterator();
    while ( iter.hasNext()) {
      InvDatasetImpl ds = (InvDatasetImpl) iter.next();
      rootElem.addContent( writeDataset( null, ds));
    }

    return doc;
  }

  private Element writeAccess( InvAccessImpl access) {
    Element accessElem = new Element("access", defNS);
    accessElem.setAttribute("urlPath", access.getUrlPath());
    if (access.getServiceName() != null)
      accessElem.setAttribute("serviceName", access.getServiceName());
    if (access.getDataFormatType() != null)
      accessElem.setAttribute("dataFormat", access.getDataFormatType().toString());

    if (access.hasDataSize())
      accessElem.addContent( writeSize( access.getDataSize()));

    return accessElem;
  }

  private Element writeCatalogRef( InvCatalogRef catRef) {
    Element catrefElem = new Element("catalogRef", defNS);
    catrefElem.setAttribute("href", catRef.getXlinkHref(), xlinkNS);
    catrefElem.setAttribute("title", catRef.getName(), xlinkNS);

    List list = catRef.getDocumentation();
    for (int j=0; j< list.size(); j++) {
      InvDocumentation doc = (InvDocumentation) list.get(j);
      catrefElem.addContent( writeDocumentation(doc, "documentation"));
    }

    return catrefElem;
  }

  protected Element writeContributor(ThreddsMetadata.Contributor c) {
    Element elem = new Element("contributor", defNS);
    if (c.getRole() != null)
      elem.setAttribute("role", c.getRole());
    elem.setText( c.getName());
    return elem;
  }

  private Element writeControlledVocabulary( ThreddsMetadata.Vocab v, String name) {
    Element elem = new Element(name, defNS);
    if (v.getVocabulary() != null)
      elem.setAttribute("vocabulary", v.getVocabulary());
    elem.addContent(v.getText());
    return elem;
  }

  private Element writeDataset( InvDatasetImpl p, InvDatasetImpl ds) {
    Element dsElem = new Element("dataset", defNS);

    if (ds instanceof InvDatasetImplProxy) {
      dsElem.setAttribute("name", ((InvDatasetImplProxy)ds).getAliasName());
      dsElem.setAttribute("alias", ds.getID());
      return dsElem;
    }
    dsElem.setAttribute("name", ds.getName());

    // other attributes, note the others get made into an element
    if ((ds.getCollectionType() != null) && (ds.getCollectionType() != CollectionType.NONE))
      dsElem.setAttribute("collectionType", ds.getCollectionType().toString());
    if (ds.isHarvest())
      dsElem.setAttribute("harvest", "true");
    if (ds.getID() != null)
      dsElem.setAttribute("ID", ds.getID());
    if (ds.getUrlPath() != null)
      dsElem.setAttribute("urlPath", ds.getUrlPath());

    // services (local only)
    Iterator services = ds.getServicesLocal().iterator();
    while ( services.hasNext()) {
      InvService service = (InvService) services.next();
      dsElem.addContent( writeService( service));
    }

    // thredds metadata LOOK
    writeThreddsMetadata( dsElem, ds.getLocalMetadata());
    writeCat6InheritedMetadata( dsElem, ds.getLocalMetadataInherited());

    // access  (local only)
    Iterator access = ds.getAccessLocal().iterator();
    while ( access.hasNext()) {
      InvAccessImpl a = (InvAccessImpl) access.next();
      dsElem.addContent( writeAccess( a));
    }

    // nested datasets
    Iterator datasets = ds.getDatasets().iterator();
    while ( datasets.hasNext()) {
      InvDatasetImpl nested = (InvDatasetImpl) datasets.next();
      if (nested instanceof InvCatalogRef)
        dsElem.addContent( writeCatalogRef( (InvCatalogRef) nested));
      else
        dsElem.addContent( writeDataset( ds, nested));
    }

    return dsElem;
  }

  protected Element writeDate(String name, DateType date) {
    Element dateElem = new Element(name, defNS);
    dateElem.addContent(date.getText());
    if (date.getType() != null)
      dateElem.setAttribute("type", date.getType());
    if (date.getFormat() != null)
      dateElem.setAttribute("format", date.getFormat());

    return dateElem;
  }

  private Element writeDocumentation( InvDocumentation doc, String name) {
    Element docElem = new Element(name, defNS);
    if (doc.getType() != null)
      docElem.setAttribute("type", doc.getType());

    if (doc.hasXlink()) {
      docElem.setAttribute("href", doc.getXlinkHref(), xlinkNS);
      if (!doc.getXlinkTitle().equals( doc.getURI().toString()))
        docElem.setAttribute("title", doc.getXlinkTitle(), xlinkNS);
    }

    String inline = doc.getInlineContent();
    if (inline != null)
      docElem.addContent(inline);
    return docElem;
  }

  protected Element writeGeospatialCoverage( ThreddsMetadata.GeospatialCoverage gc) {
    Element elem = new Element("geospatialCoverage", defNS);
    if (gc.getZPositive().equals("down"))
      elem.setAttribute( "zpositive", gc.getZPositive());

    writeGeospatialRange( elem, new Element("northsouth", defNS), gc.getNorthSouthRange());
    writeGeospatialRange( elem, new Element("eastwest", defNS), gc.getEastWestRange());
    if (gc.getUpDownRange() != null)
      writeGeospatialRange( elem, new Element("updown", defNS), gc.getUpDownRange());

    // serialize isGlobal
    java.util.List names = gc.getNames();
    ThreddsMetadata.Vocab global = new ThreddsMetadata.Vocab("global", null);
    if (gc.isGlobal() && !names.contains(global)) {
      names.add(global);
    } else if (!gc.isGlobal() && names.contains(global)) {
      names.remove(global);
    }

    for (int j = 0; j < names.size(); j++) {
      ThreddsMetadata.Vocab name = (ThreddsMetadata.Vocab) names.get(j);
      elem.addContent(writeControlledVocabulary(name, "name"));
    }

    return elem;
  }

  private void writeGeospatialRange(Element parent, Element elem, ThreddsMetadata.Range r) {
    if (r == null) return;

    elem.addContent( new Element("start", defNS).setText( Double.toString(r.getStart())));
    elem.addContent( new Element("size", defNS).setText( Double.toString(r.getSize())));
    if (r.hasResolution())
      elem.addContent( new Element("resolution", defNS).setText( Double.toString(r.getResolution())));
    if (r.getUnits() != null)
      elem.addContent( new Element("units", defNS).setText( r.getUnits()));

    parent.addContent( elem);
  }

  private Element writeMetadata( InvMetadata mdata) {
    Element mdataElem = new Element("metadata", defNS);
    if (mdata.getMetadataType() != null)
      mdataElem.setAttribute("metadataType", mdata.getMetadataType());
    if (mdata.isInherited())
      mdataElem.setAttribute("inherited", "true");

    String ns = mdata.getNamespaceURI();
    if ((ns != null) && !ns.equals(JaxpFactory.CATALOG_NAMESPACE_10)) {
      Namespace mdataNS = Namespace.getNamespace(mdata.getNamespacePrefix(), ns);
      mdataElem.addNamespaceDeclaration(mdataNS);
    }

    if (mdata.hasXlink()) {
      mdataElem.setAttribute("href", mdata.getXlinkHref().toString(), xlinkNS);
      if (mdata.getXlinkTitle() != null)
        mdataElem.setAttribute("title", mdata.getXlinkTitle(), xlinkNS);

    } else if (mdata.getThreddsMetadata() != null) {
      writeThreddsMetadata( mdataElem, mdata.getThreddsMetadata());

    } else {
        // inline non-thredds case
      MetadataConverterIF converter = mdata.getConverter();
      if ((converter != null) && mdata.getContentObject() != null) {
        if (mdata.getContentObject() instanceof Element) { // special case
          Element mdataOrg = (Element) mdata.getContentObject();
          List children = mdataOrg.getChildren();
          for (int i=0; i<children.size(); i++) {
            Element child = (Element) children.get(i);
            mdataElem.addContent( (Element) child.clone());
          }
        } else {
          org.w3c.dom.Element dome = toDOM(mdataElem);
          converter.addMetadataContent(dome, mdata.getContentObject());
          mdataElem = toJDOM(dome);
          mdataElem.detach();
        }
      }
    }

    return mdataElem;
  }

  private Element writeProperty( InvProperty prop) {
    Element propElem = new Element("property", defNS);
    propElem.setAttribute("name", prop.getName());
    propElem.setAttribute("value", prop.getValue());
    return propElem;
  }

  protected Element writeSource( String elementName, ThreddsMetadata.Source p) {
    Element elem = new Element(elementName, defNS);

    elem.addContent( writeControlledVocabulary( p.getNameVocab() , "name"));

    Element contact = new Element("contact", defNS);
    if (p.getUrl() != null)
      contact.setAttribute("url", p.getUrl());
    if (p.getEmail() != null)
      contact.setAttribute("email", p.getEmail());
    elem.addContent( contact);

    return elem;
  }


  private Element writeService( InvService service) {
    Element serviceElem = new Element("service", defNS);
    serviceElem.setAttribute("name", service.getName());
    serviceElem.setAttribute("serviceType", service.getServiceType().toString());
    serviceElem.setAttribute("base", service.getBase());
    if ((service.getSuffix() != null) && (service.getSuffix().length() > 0))
      serviceElem.setAttribute("suffix", service.getSuffix());

    // properties
    Iterator props = service.getProperties().iterator();
    while ( props.hasNext()) {
      InvProperty p = (InvProperty) props.next();
      serviceElem.addContent( writeProperty( p));
    }

    // services
    Iterator services = service.getServices().iterator();
    while ( services.hasNext()) {
      InvService nested = (InvService) services.next();
      serviceElem.addContent( writeService( nested));
    }

    return serviceElem;
  }

  private Element writeSize( double size) {
    Element sizeElem = new Element("dataSize", defNS);
    String unit = null;
    if (size > 1.0e15) {
      unit = "Pbytes";
      size *= 1.0e-15;
    } else if (size > 1.0e12) {
      unit = "Tbytes";
      size *= 1.0e-12;
    } else if (size > 1.0e9) {
      unit = "Gbytes";
      size *= 1.0e-9;
    } else if (size > 1.0e6) {
      unit = "Mbytes";
      size *= 1.0e-6;
    } else if (size > 1.0e3) {
      unit = "Kbytes";
      size *= 1.0e-3;
    } else  {
      unit = "bytes";
    }

    sizeElem.setAttribute("units", unit);
    sizeElem.setText(Double.toString(size));

    return sizeElem;
  }

  protected void writeCat6InheritedMetadata( Element elem, ThreddsMetadata tmi) {
    if ((tmi.getDataType() == null) && (tmi.getServiceName() == null) &&
        (tmi.getAuthority() == null) && ( tmi.getProperties().size() == 0))
      return;

    Element mdataElem = new Element("metadata", defNS);
    mdataElem.setAttribute("inherited", "true");
    writeThreddsMetadata( mdataElem, tmi);
    elem.addContent( mdataElem);
  }

  protected void writeThreddsMetadata( Element elem, ThreddsMetadata tmg) {
    java.util.List list;

    if (tmg.getServiceName() != null) {
      Element serviceNameElem = new Element("serviceName", defNS);
      serviceNameElem.setText(tmg.getServiceName());
      elem.addContent( serviceNameElem);
    }

    if (tmg.getAuthority() != null) {
      Element authElem = new Element("authority", defNS);
      authElem.setText(tmg.getAuthority());
      elem.addContent( authElem);
    }

    if ((tmg.getDataType() != null) && (tmg.getDataType() != DataType.NONE)) {
      Element dataTypeElem = new Element("dataType", defNS);
      dataTypeElem.setText(tmg.getDataType().toString());
      elem.addContent( dataTypeElem);
    }

    if ((tmg.getDataFormatType() != null) && (tmg.getDataFormatType() != DataFormatType.NONE)) {
      Element dataFormatElem = new Element("dataFormat", defNS);
      dataFormatElem.setText(tmg.getDataFormatType().toString());
      elem.addContent( dataFormatElem);
    }

    if ( tmg.hasDataSize())
      elem.addContent( writeSize( tmg.getDataSize()));

    list = tmg.getDocumentation();
    for (int j=0; j< list.size(); j++) {
      InvDocumentation doc = (InvDocumentation) list.get(j);
      elem.addContent( writeDocumentation(doc, "documentation"));
    }

    list = tmg.getContributors();
    for (int j=0; j< list.size(); j++) {
      ThreddsMetadata.Contributor c = (ThreddsMetadata.Contributor) list.get(j);
      elem.addContent( writeContributor(c));
    }

    list = tmg.getCreators();
    for (int j=0; j< list.size(); j++) {
      ThreddsMetadata.Source p = (ThreddsMetadata.Source) list.get(j);
      elem.addContent( writeSource("creator", p));
    }

    list = tmg.getKeywords();
    for (int j=0; j< list.size(); j++) {
      ThreddsMetadata.Vocab v = (ThreddsMetadata.Vocab) list.get(j);
      elem.addContent( writeControlledVocabulary(v, "keyword"));
    }

    list = tmg.getMetadata();
    for (int j=0; j< list.size(); j++) {
      InvMetadata m = (InvMetadata) list.get(j);
      elem.addContent( writeMetadata(m));
    }

    list = tmg.getProjects();
    for (int j=0; j< list.size(); j++) {
      ThreddsMetadata.Vocab v = (ThreddsMetadata.Vocab) list.get(j);
      elem.addContent( writeControlledVocabulary(v, "project"));
    }

    list = tmg.getProperties();
    for (int j=0; j< list.size(); j++) {
      InvProperty p = (InvProperty) list.get(j);
      elem.addContent( writeProperty(p));
    }

    list = tmg.getPublishers();
    for (int j=0; j< list.size(); j++) {
      ThreddsMetadata.Source p = (ThreddsMetadata.Source) list.get(j);
      elem.addContent( writeSource("publisher", p));
    }

    list = tmg.getDates();
    for (int j=0; j< list.size(); j++) {
      DateType d = (DateType) list.get(j);
      elem.addContent( writeDate("date", d));
    }

    ThreddsMetadata.GeospatialCoverage gc = tmg.getGeospatialCoverage();
    if ((gc != null) && !gc.isEmpty())
      elem.addContent( writeGeospatialCoverage( gc));

    ThreddsMetadata.TimeCoverage tc = tmg.getTimeCoverage();
    if ((tc != null) && !tc.isEmpty())
      elem.addContent( writeTimeCoverage( tc));

    list = tmg.getVariables();
    for (int j=0; j< list.size(); j++) {
      ThreddsMetadata.Variables v = (ThreddsMetadata.Variables) list.get(j);
      elem.addContent( writeVariables(v));
    }

  }

  protected Element writeTimeCoverage( ThreddsMetadata.TimeCoverage t) {
    Element elem = new Element("timeCoverage", defNS);

    DateType start = t.getStart();
    DateType end = t.getEnd();
    TimeDuration duration = t.getDuration();

    if ((start != null) && !start.isBlank()) {
      Element startElem = new Element("start", defNS);
      startElem.setText(start.toString());
      elem.addContent(startElem);
    }

    if ((end != null) && !end.isBlank()) {
      Element telem = new Element("end", defNS);
      telem.setText(end.toString());
      elem.addContent(telem);
    }

    if ((duration != null) && !duration.isBlank()) {
      Element telem = new Element("duration", defNS);
      telem.setText(duration.toString());
      elem.addContent(telem);
    }

    if ((t.getResolution() != null) && !t.getResolution().isBlank()) {
      Element telem = new Element("resolution", defNS);
      telem.setText(t.getResolution().toString());
      elem.addContent(telem);
    }

    return elem;
  }


  protected Element writeVariable( ThreddsMetadata.Variable v) {
    Element elem = new Element("variable", defNS);
    elem.setAttribute("name", v.getName());
    elem.setAttribute("vocabulary_name", v.getVocabularyName());
    if (v.getUnits() != null)
      elem.setAttribute("units", v.getUnits());
    return elem;
  }


  protected Element writeVariables( ThreddsMetadata.Variables vs) {
    Element elem = new Element("variables", defNS);
    if (vs.getVocabulary() != null)
      elem.setAttribute("vocabulary", vs.getVocabulary());
    if (vs.getVocabHref() != null)
      elem.setAttribute("href", vs.getVocabHref(), xlinkNS);

    if (vs.getMapHref() != null) { // variable map
      Element mapElem = new Element("variableMap", defNS);
      mapElem.setAttribute("href", vs.getMapHref(), xlinkNS);
      elem.addContent( mapElem);

    } else { // inline variables
      List list = vs.getVariableList();
      for (int j = 0; j < list.size(); j++) {
        ThreddsMetadata.Variable v = (ThreddsMetadata.Variable) list.get(j);
        elem.addContent(writeVariable(v));
      }
    }

    return elem;
  }

  public org.w3c.dom.Element toDOM( Element elem) {
    try {
      if (domOut == null) domOut = new DOMOutputter();
      return domOut.output(elem);
    } catch (JDOMException e) {
      System.out.println("InvCatalogFactory6.readMetadata.toDom error " + e);
      return null;
    }
  }

  public Element toJDOM( org.w3c.dom.Element domElement) {
    return builder.build(domElement);
  }

}

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

   Revision 1.7  2004/09/24 03:26:28  caron
   merge nj22

   Revision 1.6  2004/06/09 00:27:27  caron
   version 2.0a release; cleanup javadoc

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

   Revision 1.4  2004/06/04 00:51:54  caron
   release 2.0b

   Revision 1.3  2004/05/21 05:57:31  caron
   release 2.0b

   Revision 1.2  2004/05/20 22:48:09  edavis
   Modify readMetadata() to get the namespace from the first child element of
   the metadata element instead of the metadata element itself, convert the
   metadata content to a contentObject, and return a new InvMetadata with the
   contentObject.

   Revision 1.1  2004/05/11 23:30:29  caron
   release 2.0a

 */