// $Id: InvDatasetImpl.java,v 1.14 2004/12/14 15:41:00 caron Exp $
/*
 * Copyright 2002 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 thredds.datatype.*;

import java.util.*;
import java.net.URI;

/**
 * Concrete implementation of a thredds Dataset, for reading and writing from XML.
 * @see InvDataset
 *
 * @author john caron
 * @version $Revision: 1.14 $ $Date: 2004/12/14 15:41:00 $
 */

public class InvDatasetImpl extends InvDataset {

  private String urlPath;
  private String alias;
  private double size = 0.0;

  private ArrayList accessLocal = new ArrayList();
  private ArrayList servicesLocal = new ArrayList();
  protected ThreddsMetadata tm = new ThreddsMetadata( false); // all local metadata kept here
  protected ThreddsMetadata tmi = new ThreddsMetadata( true); // all local inherited metadata kept here (cat6 only)

  // validation
  protected StringBuffer log = new StringBuffer();
  // filter
  protected boolean mark = false;
  // debug
  private boolean debugInherit = false;


  /**
   * Copy constructor. All nested elements are reparented to this, so copy object
   *  should not be used anymore.
   * @param copy take info from this.
   *
  protected InvDatasetImpl( InvDatasetImpl copy) {
    super( copy.parent, copy.name);
    this.id = copy.id;
    this.authorityNameSet = copy.authorityNameSet;
    //this.serviceNameSet = copy.serviceName;
    this.catalog = copy.catalog;

    this.dataType = copy.dataType;
    this.urlPath = copy.urlPath;
    this.alias = copy.alias;

    this.datasets = new ArrayList( copy.datasets);
    this.docs = new ArrayList( copy.docs);
    this.metadata = new ArrayList( copy.metadata);

    this.accessLocal = new ArrayList( copy.accessLocal);
    this.services = new ArrayList( copy.services);
    //this.propertiesLocal = new ArrayList( copy.propertiesLocal);

    // reparent nested elements
    java.util.Iterator iter = datasets.iterator();
    while (iter.hasNext()) {
      InvDatasetImpl nested = (InvDatasetImpl) iter.next();
      nested.setParent( this);
    }

    iter = accessLocal.iterator();
    while (iter.hasNext()) {
      InvAccess nested = (InvAccess) iter.next();
      nested.dataset = this;
    }

    /* iter = metadata.iterator();
    while (iter.hasNext()) {
      InvMetadata nested = (InvMetadata) iter.next();
      nested.setParentDataset( this);
    }

    LOOK
    iter = docs.iterator();
    while (iter.hasNext()) {
      InvDocumentation nested = (InvDocumentation) iter.next();
      nested.dataset = this;
    } */

    /* iter = services.iterator();
    while (iter.hasNext()) {
      InvService nested = (InvService) iter.next();
      nested.parent = this;
    }

    finish();
  } */

  /**
   * Constructor from Catalog XML info. You must call finish() before this object
   * is ready to be used. We had to do it this way so that nested service elements could be added
   * through addService(), before we define on the default service.
   *
   * @param parent : parent dataset
   * @param name : display name of dataset
   * @param dataType : DataType name (may be null)
   * @param serviceName : default service (may be null)
   * @param urlPath : URL = server.getURLbase() + urlPath
   */
  public InvDatasetImpl( InvDatasetImpl parent, String name, DataType dataType, String serviceName, String urlPath) {
    super( parent, name);

    tm.setDataType( dataType);
    tm.setServiceName( serviceName);
    this.urlPath = urlPath;
  }

  /*** Finish constructing after all elements have been added.
   * This does the inheritence thing
   * This can be called again if new elements are added.
   * @return true if successful.
   **/
  public boolean finish() {
    boolean ok = true;
    java.util.Iterator iter;
    if (debugInherit) System.out.println("Now finish "+getName()+" id= "+getID());

    // deal with the thredds metadata elements and inheritence
    /* authorityName = tm.getAuthority();
    dataType = tm.getDataType();
    if (tm.getServiceName() != null) {
      defaultService = findService(tm.getServiceName());
      if (debugInherit) System.out.println("finish serviceNameSet="+tm.getServiceName()+"->"+defaultService);
    } */

    dataFormatType = null;
    gc = null;
    tc = null;
    docs = new ArrayList();
    metadata = new ArrayList();
    properties = new ArrayList();

    creators = new ArrayList();
    contributors = new ArrayList();
    dates = new ArrayList();
    keywords = new ArrayList();
    projects = new ArrayList();
    publishers = new ArrayList();
    variables = new ArrayList();

    transferMetadata( tm, true); // add local metadata
    transferMetadata( tmi, true); // add local inherited metadata
    transferFromParents( (InvDatasetImpl) getParent()); // add inherited metadata from parents

    // build the expanded access list
    access = new ArrayList();

    // add access element if urlPath is specified
    if ((urlPath != null) && (getServiceDefault() != null)) {
      InvAccessImpl a = new InvAccessImpl(this, urlPath, getServiceDefault());
      a.setSize( size);
      a.finish();
      addExpandedAccess(a);
    }

    // add local access elements
    iter = accessLocal.iterator();
    while (iter.hasNext()) {
      InvAccessImpl a = (InvAccessImpl) iter.next();
      a.finish();
      addExpandedAccess( a );
    }

    // recurse into child datasets.
    if (!(this instanceof InvCatalogRef)) {
      java.util.Iterator dsIter = this.getDatasets().iterator();
      while (dsIter.hasNext()) {
        InvDatasetImpl curDs = (InvDatasetImpl) dsIter.next();
        ok &= curDs.finish();
      }
    }

    return ok;
  }

  /**
   * Look for InvMetadata elements in the parent that need to be added to the public metadata of this dataset.
   * Recurse up through all ancestors.
   * @param parent
   */
  private void transferFromParents( InvDatasetImpl parent) {
    if (parent == null) return;
    if (debugInherit) System.out.println(" inheritFromParent= "+parent.getID());

    transferMetadata( parent.getLocalMetadataInherited(), true);

    // look through local metadata, find inherited InvMetadata elements
    ThreddsMetadata tmd = parent.getLocalMetadata();
    Iterator iter = tmd.getMetadata().iterator();
    while (iter.hasNext()) {
      InvMetadata meta = (InvMetadata) iter.next();
      if (meta.isInherited()) {
        if (!meta.isThreddsMetadata()) {
          metadata.add( meta);
        } else {
          if (debugInherit) System.out.println("  inheritMetadata Element "+tmd.isInherited()+" "+
                                               meta.isInherited());
          meta.finish(); // make sure XLink is read in.
          transferMetadata(meta.getThreddsMetadata(), false);
          metadata.add( meta);
        }
      }
    }

    transferFromParents( (InvDatasetImpl) parent.getParent());
  }

  /**
   * take all elements from tmg and add to the public metadata of this dataset.
   * for InvMetadata elements, only add if inheritAll || InvMetadata.isInherited().
   * @param tmd
   * @param inheritAll
   */
  private void transferMetadata( ThreddsMetadata tmd, boolean inheritAll) {
    if (tmd == null) return;

    if (authorityName == null)
      authorityName = tmd.getAuthority();
    if (dataType == null || dataType == DataType.NONE)
      dataType = tmd.getDataType();
    if (dataFormatType == null || dataFormatType == DataFormatType.NONE)
      dataFormatType = tmd.getDataFormatType();

    if (defaultService == null)
      defaultService = findService(tmd.getServiceName());

    if (gc == null) {
      ThreddsMetadata.GeospatialCoverage tgc = tmd.getGeospatialCoverage();
      if ((tgc != null) && !tgc.isEmpty())
        gc = tgc;
    }

    if (tc == null) {
      ThreddsMetadata.TimeCoverage ttc = tmd.getTimeCoverage();
      if ((ttc != null) && !ttc.isEmpty()) {
        // System.out.println(" tc assigned = "+ttc);
        tc = ttc;
      }
    }

    if (tc == null)
      tc = tmd.getTimeCoverage();

    Iterator iter = tmd.getProperties().iterator();
    while (iter.hasNext()) {
      Object item = iter.next();
      if( !properties.contains( item)) { // dont add properties with same name
        if (debugInherit) System.out.println("  add Property "+item+" to "+getID());
        properties.add(item);
      }
    }

    creators.addAll( tmd.getCreators());
    contributors.addAll( tmd.getContributors());
    dates.addAll( tmd.getDates());
    docs.addAll( tmd.getDocumentation());
    keywords.addAll( tmd.getKeywords());
    projects.addAll( tmd.getProjects());
    publishers.addAll( tmd.getPublishers());
    variables.addAll( tmd.getVariables());

    iter = tmd.getMetadata().iterator();
    while (iter.hasNext()) {
      InvMetadata meta = (InvMetadata) iter.next();
      if (meta.isInherited() || inheritAll) {
        if (!meta.isThreddsMetadata()) {
          metadata.add( meta);
        } else {
          if (debugInherit) System.out.println("  inheritMetadata Element "+tmd.isInherited()+" "+
                                               meta.isInherited());
          meta.finish(); // make sure XLink is read in.
          transferMetadata(meta.getThreddsMetadata(), inheritAll);
          metadata.add( meta);
        }
      }
    }

  }

  private void addExpandedAccess( InvAccessImpl a) {
    InvService service = (InvService) a.getService();
    if (null == service) {
      a.check( log, false); // illegal; get error message
      return;
    }

    if ( service.getServiceType() == ServiceType.COMPOUND) {
      // if its a compound service, expand it
      java.util.List serviceList = service.getServices();
      for (int i = 0; i < serviceList.size(); i++) {
        InvService nestedService = (InvService) serviceList.get(i);
        InvAccessImpl nestedAccess = new InvAccessImpl( this, a.getUrlPath(), nestedService);
        addExpandedAccess( nestedAccess); // i guess it could recurse
      }
    } else {
      access.add( a);
    }
  }

  /**
   * Construct an InvDatasetImpl which refers to a urlPath.
   * This is used to create a standalone InvDatasetImpl, outside of an InvCatalog.
   * An "anonymous" InvServerImpl is created and attached to the InvDataset.
   *
   * @param urlPath : construct URL from this path
   * @param dtype : data type
   * @param stype : ServiceType
   */
  public InvDatasetImpl( String urlPath, DataType dtype, ServiceType stype) {
    super( null, "local file");
    tm.setDataType( dataType);
    tm.setServiceName( "anon");
    this.urlPath = urlPath;

    // create anonomous service
    addService(new InvService( tm.getServiceName(), stype.toString(), "", "", null));

    finish();
  }

  public InvDatasetImpl( InvDataset parent, String name) {
    super(parent, name);
  }

  ////////////////////////////////////////////////////////
  // get/set local properties

  /** Get alias for this Dataset, if it exists */
  public String getAlias() { return alias; }
  /** Set alias for this Dataset */
  public void setAlias(String alias) {
    this.alias = alias;
    hashCode = 0;
  }

  /** Set the containing catalog; use only for top level dataset. */
  public void setCatalog( InvCatalog catalog) {
    this.catalog = catalog;
    hashCode = 0;
  }

  /** Get urlPath for this Dataset */
  public String getUrlPath() { return urlPath; }
  /** Set the urlPath for this InvDatasetImpl */
  public void setUrlPath( String urlPath) {
    this.urlPath = urlPath;
    hashCode = 0;
  }

  /** Get authorityName set local to this Dataset */
  //public String getAuthorityNameSet() { return tm.getAuthority(); }
  /** Set authorityName for this Dataset */
  public void setAuthority(String authorityName) {
    tm.setAuthority(authorityName);
    hashCode = 0;
  }

  /** Return the serviceName set local to this InvDatasetImpl
  public String getServiceNameSet() { return tm.getServiceName(); }
  /** Set the serviceName for this InvDatasetImpl
  public void setServiceName( String serviceName) {
    tm.setServiceName( serviceName);
    hashCode = 0;
  }

  /** Return the datatype set local to this InvDatasetImpl
  public DataType getDataTypeSet() { return tm.getDataType(); }
  /** Set the datatype for this InvDatasetImpl
  public void setDataType( DataType dataType) {
    tm.setDataType(dataType);
    hashCode = 0;
  }

  /** Set the DataFormatType
  public void setDataFormatType(DataFormatType type) {
    tm.setDataFormatType( type);
    hashCode = 0;
  } */

  ////////////////////////////////////////////////////////
  // setters for public properties (in InvDataset)

  /** Set collectionType */
  public void setCollectionType( CollectionType collectionType) {
    this.collectionType = collectionType;
    hashCode = 0;
  }

  /** Set harvest */
  public void setHarvest( boolean harvest) {
    this.harvest = harvest;
    hashCode = 0;
  }

  /** Set the ID for this Dataset */
  public void setID(String id) {
    this.id = id;
    hashCode = 0;
  }

  /** Set name of this Dataset. */
  public void setName( String name) {
    this.name = name;
    hashCode = 0;
  }

  /** Set the parent dataset. */
  public void setParent(InvDatasetImpl parent) {
    this.parent = parent;
    hashCode = 0;
  }

  ////////////////////////////////////////////////////////
  // setters for public properties that are inherited - these go into the local metadata object

  // LOOK these are probably wrong
  public void setGeospatialCoverage(ThreddsMetadata.GeospatialCoverage gc) {
    tm.setGeospatialCoverage( gc);
    hashCode = 0;
  }

  public void setTimeCoverage(ThreddsMetadata.TimeCoverage tc) {
    tm.setTimeCoverage(tc);
    hashCode = 0;
  }

  // LOOK these are wrong
  public void setContributors(ArrayList a) {
    List dest = tm.getContributors();
    for (Iterator iter = a.iterator(); iter.hasNext(); ) {
      ThreddsMetadata.Contributor item = (ThreddsMetadata.Contributor)iter.next();
      if (!dest.contains(item))
        dest.add(item);
    }
    hashCode = 0;
  }

  public void setKeywords(ArrayList a) {
    List dest = tm.getKeywords();
    for (Iterator iter = a.iterator(); iter.hasNext(); ) {
      ThreddsMetadata.Vocab item = (ThreddsMetadata.Vocab)iter.next();
      if (!dest.contains(item))
        dest.add(item);
    }
    hashCode = 0;
  }

  public void setProjects(ArrayList a) {
    List dest = tm.getProjects();
    for (Iterator iter = a.iterator(); iter.hasNext(); ) {
      ThreddsMetadata.Vocab item = (ThreddsMetadata.Vocab)iter.next();
      if (!dest.contains(item))
        dest.add(item);
    }
    hashCode = 0;
  }

  public void setPublishers(ArrayList a) {
    List dest = tm.getPublishers();
    for (Iterator iter = a.iterator(); iter.hasNext(); ) {
      ThreddsMetadata.Source item = (ThreddsMetadata.Source) iter.next();
      if (!dest.contains(item))
        dest.add(item);
    }
    hashCode = 0;
  }

  //////////////////////////////////////////////////////////////////////////////
  // add/remove/get/find local elements

  /*** Add InvAccess element to this dataset.*/
  public void addAccess( InvAccess a) {
    accessLocal.add( a);
    hashCode = 0;
  }
  /** Get the non-expanded access elements. */
  public java.util.List getAccessLocal() { return accessLocal; }


  /*** Add a nested dataset. */
  public void addDataset( InvDatasetImpl ds) {
    ds.setParent( this);
    datasets.add( ds);
    hashCode = 0;
  }

  /**
   * Remove the given dataset element from this dataset if it is in the dataset.
   * @param ds - the dataset element to be removed
   * @return true if this dataset contained the given dataset element.
   */
  public boolean removeDataset(InvDatasetImpl ds) {
    if (this.datasets.remove(ds)) {
      ds.setParent(null);
      return (true);
    }
    return (false);
  }

  /*** Add documentation element to this dataset. */
  public void addDocumentation( InvDocumentation doc) {
    tm.addDocumentation( doc);
    hashCode = 0;
  }

  /** Add a property to this dataset */
  public void addProperty( InvProperty p) {
    tm.addProperty( p);
    hashCode = 0;
  }

  /**
   * Add a service to this dataset.
   * @deprecated put services in catalog
   */
  public void addService( InvService service) {
    // System.out.println("--add dataset service= "+service.getName());
    servicesLocal.add( service);
    services.add( service);
    // add nested servers
    java.util.List serviceList = service.getServices();
    for (int k=0; k< serviceList.size(); k++) {
      InvService nested = (InvService) serviceList.get(k);
      services.add( nested);
      // System.out.println("--add expanded service= "+nested.getName());
    }
    hashCode = 0;
  }

  /**
   * Remove a service from this dataset.
   * @deprecated put services in catalog
   */
  public void removeService( InvService service) {
    servicesLocal.remove( service);
    services.remove( service);
    // remove nested servers
    java.util.List serviceList = service.getServices();
    for (int k=0; k< serviceList.size(); k++) {
      InvService nested = (InvService) serviceList.get(k);
      services.remove( nested);
    }
   }

  /**
   * Get services attached specifically to this dataset.
   * @return List of type InvService. May be empty, but not null.
   */
  public java.util.List getServicesLocal() { return servicesLocal; }

  /**
   * Set the list of services attached specifically to this dataset.
   * Discard any previous servies.
   * @param s list of services.
   */
  public void setServicesLocal( java.util.ArrayList s) {
    this.services = new ArrayList();
    this.servicesLocal = new ArrayList();

    for (int i=0; i<s.size(); i++) {
      InvService elem = (InvService) s.get(i);
      addService( elem);
    }
    hashCode = 0;
  }

  /* all local metadata stored here */
  public ThreddsMetadata getLocalMetadata() { return tm; }
  public void setLocalMetadata(ThreddsMetadata tm) {
    // look this is wrong.
    // need to copy fields into it !!
    // possible only the one that are different from default !!!
    // like stored defaults !! ha ha !
    this.tm = tm;
    hashCode = 0;
  }

  /* used only by Cat6 */
  public ThreddsMetadata getLocalMetadataInherited() { return tmi; }


  /**
   * Remove the given InvMetadata from the set of metadata local to this dataset.
   * @param metadata
   * @return true if an InvMetadata is removed, false otherwise.
   */
  public boolean removeLocalMetadata( InvMetadata metadata)
  {
    InvDatasetImpl parentDataset = ( (InvDatasetImpl) metadata.getParentDataset());
    List localMdata = parentDataset.getLocalMetadata().getMetadata();
    if ( localMdata.contains( metadata))
    {
       if ( localMdata.remove( metadata) )
       {
         hashCode = 0; // Need to recalculate the hash code.
         return( true);
       }
    }
    return( false);
  }

  /** Filtering */
  protected boolean getMark() { return mark; }
  protected void setMark( boolean mark) { this.mark = mark; }

  /** User properties */
  public Object getUserProperty( Object key) {
    if (userMap == null) return null;
    return userMap.get( key);
  }
  public void setUserProperty( Object key, Object value) {
    if (userMap == null) userMap = new HashMap();
    userMap.put( key, value);
  }
  private HashMap userMap = null;

  ////////////////////////////////////////////////////////
  // convenience routines

  /** Resolve reletive URIs, using the catalog's URI.
   * @param uriString any uri, reletive or absolute
   * @return resolved uri string, or null on error
   * use getParentCatalog().resolveUri();
  public String resolveUri( String uriString) {
    if (catalog != null)
      return catalog.resolveUri( uriString);
    if (parent != null)
      return parent.resolveUri( uriString);
    return uriString;
  } */

  // erd 2002/10/28 Added findServiceName()
  // Similar to getDefaultService()???
  /** Return the serviceName for this InvDatasetImpl (local or inherited).
   *  use getDefaultService().getName();
  public String findServiceName() {
    if (this.serviceName == null) {
      if ( this.parent != null)
        return( this.parent.findServiceName());
    }
    return( this.serviceName);
  } */

  // erd 2002/10/28 Added this method (probably replaces getServicesAvailable()).
  /** Make sure the list of InvAccess objects is complete.
   *
   * The access list should contain all "standard" access elements plus any
   * proxy access elements necessary. Proxy access elements are created when:
   * 1) a dataset specifies an access method by urlPath and inherited
   * serviceName;
   * 2) An access method points to a COMPOUND service, each non-COMPOUND
   * service in the service hierarchy corresponds to a proxy access.
   *
  public void validateAccessMethods()
  {
// 1) see InvCatalogFactory6.makeDataset()
// That is where need to check for duplicates in any compound services, etc
// [done]
// 2) Don't forget to check for bad duplicates (e.g., if someone sets the
// dataset urlPath, runs validateAccessMethods(), and does so again.

    InvAccessImpl curAccess = null;

    // Find all proxy access methods and all compound access methods.
    java.util.List duplicateAccesses = new ArrayList();
    java.util.List compoundAccesses = new ArrayList();
    java.util.Iterator accessIter = this.getAccess().iterator();
    while ( accessIter.hasNext())
    {
      curAccess = (InvAccessImpl) accessIter.next();
      if ( curAccess.getService().getServiceType() == ServiceType.COMPOUND)
      {
        compoundAccesses.add( curAccess);
      }
      else if ( curAccess.isDuplicate())
      {
        duplicateAccesses.add( curAccess);
      }
    }

    // Make sure defaultService is defined if serviceName is locally defined.
    //if ( this.serviceName != null && this.defaultService == null)
    //  this.defaultService = findService( this.serviceName);

    // Is an access method defined by urlPath and serviceName?
    if ( this.getUrlPath() != null)
    {
      InvService tmpService =
        (InvService) this.findService( this.findServiceName());
      if ( tmpService != null)
      {
        // Is this service a COMPOUND service?
        if ( tmpService.getServiceType() != ServiceType.COMPOUND)
        {
          // Not COMPOUND. Does a matching duplicate already exist? If not
          // create an access and add to this datasets access list.
          addDuplicateAccess( duplicateAccesses, this.getUrlPath(), tmpService);
        }
        else
        {
          // @todo Deal with case where serviceType is COMPOUND.
          System.err.println( "InvDatasetImpl:validateAccessMethods() - " +
                              "skipping a COMPOUND service.\n");
        }

      }
    }

    // Check for access methods with COMPOUND services and whether there are
    // existing duplicate access methods for them.
    if ( compoundAccesses.size() > 0)
    {
      java.util.List nonCompoundServices = null;

      // Step through COMPOUND access list.
      java.util.Iterator aIter = compoundAccesses.iterator();
      while ( aIter.hasNext())
      {
        curAccess = (InvAccessImpl) aIter.next();

        // Get expanded list of all non-COMPOUND services in compound service.
        nonCompoundServices = this.expandCompoundService( curAccess.getService());

        // Loop through services and possibly add access for each.
        InvService curService = null;
        java.util.Iterator sIter = nonCompoundServices.iterator();
        while ( sIter.hasNext())
        {
          curService = (InvService) sIter.next();
          addDuplicateAccess( duplicateAccesses, this.getUrlPath(), curService);
        }

        nonCompoundServices.clear();
      } // END - loop through COMPOUND access methods

    }

    // If any access methods left in duplicate list, they are bad.
    // Remove them.
    if ( duplicateAccesses.size() > 0)
    {
      // @todo Remove bad access methods from this dataset.
      System.err.println( "InvDatasetImpl:validateAccessMethods() - " +
                          "leaving invalid access methods.\n");
    }

    // Validate any child datasets.
    InvDatasetImpl curDs = null;
    java.util.Iterator dsIter = this.getDatasets().iterator();
    while ( dsIter.hasNext())
    {
      curDs = (InvDatasetImpl) dsIter.next();
      curDs.validateAccessMethods();
    }

    return;
  }

  /** Helper method for validateAccessMethods()
  private boolean addDuplicateAccess( java.util.List duplicateAccesses,
                                      String urlPath, InvService service)
  {
    InvAccessImpl curAccess = null;
    InvAccessImpl newAccess = null;

    // Check whether proxy access already exists. If not, create one.
    if ( duplicateAccesses.size() > 0)
    {
      java.util.Iterator iter = duplicateAccesses.iterator();
      while ( iter.hasNext())
      {
        curAccess = (InvAccessImpl) iter.next();
        if ( service.getName().equals( curAccess.getService().getName()) &&
             urlPath.equals( curAccess.getUrlPath()))
        {
          // Duplicate already exists, remove from duplicate list.
          iter.remove();
          return( false);
        }
      } // END - loop through duplicate access objects.
    }

    // Create a proxy access and add.
    newAccess = new InvAccessImpl( this, this.getUrlPath(), service);
    newAccess.setDuplicate( true);
    this.addAccess( newAccess);

    return( true);
  }

  // erd 2002/10/28 Added this method
  /** Helper method for validateAccessMethods() to recurse through
   *  COMPOUND service and return list of non-COMPOUND services.
   *  @return List of InvService objects
   *
  private java.util.List expandCompoundService( InvService service) {
    java.util.List returnList = new ArrayList();

    // Loop through all services in given compound service.
    java.util.List serviceList = service.getServices();
    for (int i = 0; i < serviceList.size(); i++)  {
      InvService curService = (InvService) serviceList.get( i);

      // If COMPOUND, recurse.
      if ( curService.getServiceType() == ServiceType.COMPOUND) {
        returnList.addAll( this.expandCompoundService( curService));
      }
      // Otherwise, add to list.
      else {
        returnList.add( curService);
      }
    }
    return( returnList);
  } */

  /**
   * Get all metadata elements for this InvDataset. These may have been specified
   * in the dataset or an enclosing parent element.
   * @return List of InvMetadata objects. List may be empty but not null.
   *
  public java.util.List getMetadataInherited() {
    ArrayList result = new ArrayList(metadata);
    InvDatasetImpl p = parent;
    while (p != null) {
      result.addAll( p.getMetadata());
      p = (InvDatasetImpl) p.getParent();
    }
    return result;
  }

  /**
   * Get all metadata elements of the specified type. These may have been specified
   * in the dataset or an enclosing parent element.
   * @return List of InvMetadata objects. List may be empty but not null.
   *
  public java.util.List getMetadataInherited( thredds.catalog.MetadataType type) {
    ArrayList result = new ArrayList();
    java.util.List allMetadata = getMetadataInherited();
    for (int i=0; i<allMetadata.size(); i++) {
      InvMetadataImpl m = (InvMetadataImpl) allMetadata.get(i);
      if (m.getMetadataType() == type)
        result.add(m);
    }
    return result;
  } */

  public String toString() { return getName(); }

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


  static public void writeHtmlDescription(StringBuffer buff, InvDatasetImpl ds,
    boolean complete, boolean datasetEvents, boolean catrefEvents) {
    if (ds == null) return;

    if (complete) {
      buff.append("<html>");
      buff.append("<head>");
      buff.append("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=iso-8859-1\">");
      buff.append("</head>");
      buff.append("<body>\n");
    }

    buff.append("<h2>Dataset: "+ds.getName()+"</h2>\n<ul>\n");
    if ((ds.getDataFormatType() != null) && (ds.getDataFormatType() != DataFormatType.NONE))
      buff.append(" <li><em>Data format: </em>"+ds.getDataFormatType()+"\n");
    if ((ds.getDataType() != null) && (ds.getDataType() != DataType.NONE))
      buff.append(" <li><em>Data type: </em>"+ds.getDataType()+"\n");
    if ((ds.getCollectionType() != null) && (ds.getCollectionType() != CollectionType.NONE))
      buff.append(" <li><em>Collection type: </em>"+ds.getCollectionType()+"\n");
    if (ds.isHarvest())
      buff.append(" <li><em>Harvest: </em>"+ds.isHarvest()+"\n");
    if (ds.getAuthority() != null)
      buff.append(" <li><em>Naming Authority: </em>"+ds.getAuthority()+"\n");
    if (ds.getID() != null)
      buff.append(" <li><em>ID: </em>"+ds.getID()+"\n");
    if (ds instanceof InvCatalogRef) {
      InvCatalogRef catref = (InvCatalogRef) ds;
      String href = resolve( ds, catref.getXlinkHref());
      if (catrefEvents) href = "catref:"+href;
      buff.append(" <li><em>CatalogRef: </em>"+makeHref( href, href)+"\n");
    }

    buff.append("</ul>\n");

    java.util.List docs = ds.getDocumentation();
    if (docs.size() > 0) {
      buff.append("<h3>Documentation:</h3>\n<ul>\n");
      for (int i=0; i<docs.size(); i++) {
        InvDocumentation doc = (InvDocumentation) docs.get(i);
        String type = (doc.getType() == null) ? "" : "<strong>"+doc.getType() +":</strong> ";
        String inline = doc.getInlineContent();
        if ((inline != null) && (inline.length() > 0))
          buff.append(" <li>"+type+inline + "\n");
        if (doc.hasXlink()) {
          URI uri = doc.getURI();
          buff.append(" <li>"+type+makeHrefResolve( ds, uri.toString(), doc.getXlinkTitle())+"</a>\n");
        }
      }
      buff.append("</ul>");
    }

    java.util.List access = ds.getAccess();
    if (access.size() > 0) {
      buff.append("<h3>Access:</h3>\n<ol>\n");
      for (int i=0; i<access.size(); i++) {
        InvAccess a = (InvAccess) access.get(i);
        InvService s = a.getService();
        String urlString = a.getStandardUrlName();
        if (datasetEvents) urlString = "dataset:"+urlString;
        buff.append(" <li> <b>"+s.getServiceType() +":</b> "+makeHref(urlString, a.getStandardUrlName())+"\n");
      }
      buff.append("</ol>\n");
    }

    java.util.List list = ds.getContributors();
    if (list.size() > 0) {
      buff.append("<h3>Contributors:</h3>\n<ul>\n");
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Contributor t = (ThreddsMetadata.Contributor) list.get(i);
        String role = (t.getRole() == null) ? "" : "<strong> ("+t.getRole() +")</strong> ";
        buff.append(" <li>"+t.getName()+role+"\n");
      }
      buff.append("</ul>");
    }

    list = ds.getKeywords();
    if (list.size() > 0) {
      buff.append("<h3>Keywords:</h3>\n<ul>\n");
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Vocab t = (ThreddsMetadata.Vocab) list.get(i);
        String vocab = (t.getVocabulary() == null) ? "" : " <strong>("+t.getVocabulary() +")</strong> ";
        buff.append(" <li>"+t.getText()+vocab+"\n");
      }
      buff.append("</ul>");
    }

    list = ds.getDates();
    if (list.size() > 0) {
      buff.append("<h3>Dates:</h3>\n<ul>\n");
      for (int i=0; i<list.size(); i++) {
        DateType d = (DateType) list.get(i);
        String type = (d.getType() == null) ? "" : " <strong>("+d.getType() +")</strong> ";
        buff.append(" <li>"+d.getText()+type+"\n");
      }
      buff.append("</ul>");
    }

    list = ds.getProjects();
    if (list.size() > 0) {
      buff.append("<h3>Projects:</h3>\n<ul>\n");
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Vocab t = (ThreddsMetadata.Vocab) list.get(i);
        String vocab = (t.getVocabulary() == null) ? "" : " <strong>("+t.getVocabulary() +")</strong> ";
        buff.append(" <li>"+t.getText()+vocab+"\n");
      }
      buff.append("</ul>");
    }

    list = ds.getCreators();
    if (list.size() > 0) {
      buff.append("<h3>Creators:</h3>\n<ul>\n");
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Source t = (ThreddsMetadata.Source) list.get(i);
        buff.append(" <li><strong>"+t.getName()+"</strong><ul>\n");
        buff.append(" <li><em>email: </em>"+t.getEmail()+"\n");
        if (t.getUrl() != null) {
          buff.append(" <li> <em>"+makeHrefResolve(ds, t.getUrl(), null)+"</em>\n");
        }
        buff.append(" </ul>\n");
      }
      buff.append("</ul>");
    }

    list = ds.getPublishers();
    if (list.size() > 0) {
      buff.append("<h3>Publishers:</h3>\n<ul>\n");
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Source t = (ThreddsMetadata.Source) list.get(i);
        buff.append(" <li><strong>"+t.getName()+"</strong><ul>\n");
        buff.append(" <li><em>email: </em>"+t.getEmail()+"\n");
        if (t.getUrl() != null) {
          buff.append(" <li> <em>"+makeHrefResolve(ds, t.getUrl(), null)+"</em>\n");
        }
        buff.append(" </ul>\n");
      }
      buff.append("</ul>");
    }

    list = ds.getVariables();
    if (list.size() > 0) {
      buff.append("<h3>Variables:</h3>\n<ul>\n");
      for (int i=0; i<list.size(); i++) {
        ThreddsMetadata.Variables t = (ThreddsMetadata.Variables) list.get(i);

        if (t.getVocabUri() != null) {
          URI uri = t.getVocabUri();
          buff.append(" <li>"+makeHrefResolve(ds, uri.toString(), t.getVocabulary())+"</a>");
        } else {
          buff.append(" <li>"+t.getVocabulary());
        }
        buff.append(" <em>vocabulary:</em> <ul>\n");

        java.util.List vlist = t.getVariableList();
        if (vlist.size() > 0) {
          for (int j=0; j<vlist.size(); j++) {
            ThreddsMetadata.Variable v = (ThreddsMetadata.Variable) vlist.get(j);
            String units = (v.getUnits() == null || v.getUnits().length() == 0 ) ? "" : " ("+v.getUnits() +") ";
            buff.append(" <li><strong>"+v.getName()+"</strong> = "+v.getVocabularyName()+units+"\n");
          }
          buff.append(" </ul>\n");
        }
        buff.append("</ul>");
      }
      buff.append("</ul>");
    }

    ThreddsMetadata.GeospatialCoverage gc = ds.getGeospatialCoverage();
    if ((gc != null) && !gc.isEmpty()) {
      buff.append("<h3>GeospatialCoverage:</h3>\n<ul>\n");
      if (gc.isGlobal())
        buff.append(" <li><em> Global </em></ul>\n");
      else {
        buff.append(" <li><em> Longitude: </em> "+rangeString( gc.getEastWestRange())+"\n");
        buff.append(" <li><em> Latitude: </em> "+rangeString( gc.getNorthSouthRange())+"\n");
        if (gc.getUpDownRange() != null)
          buff.append(" <li><em> Altitude: </em> "+rangeString( gc.getUpDownRange())+
                      " (positive is <strong>"+gc.getZPositive()+")</strong>\n");

        java.util.List nlist = gc.getNames();
        if ((nlist != null) && (nlist.size() > 0)) {
          buff.append(" <li><em>  Names: </em> <ul>\n");
          for (int i = 0; i < nlist.size(); i++) {
             ThreddsMetadata.Vocab elem = (ThreddsMetadata.Vocab) nlist.get(i);
             buff.append(" <li>"+elem.getText()+"\n");
          }
          buff.append(" </ul>\n");
        }
        buff.append(" </ul>\n");
      }
    }

    ThreddsMetadata.TimeCoverage tc = ds.getTimeCoverage();
    if ((tc != null) && !tc.isEmpty()) {
      buff.append("<h3>TimeCoverage:</h3>\n<ul>\n");
      DateType start = tc.getStart();
      if ((start != null) && !start.isBlank())
        buff.append(" <li><em>  Start: </em> "+start+"\n");
      DateType end = tc.getEnd();
      if ((end != null) && !end.isBlank())
        buff.append(" <li><em>  End: </em> "+end+"\n");
      TimeDuration duration = tc.getDuration();
      if ((duration != null) && !duration.isBlank())
        buff.append(" <li><em>  Duration: </em> "+duration+"\n");
      TimeDuration resolution = tc.getResolution();
      if ((resolution != null) && !resolution.isBlank())
        buff.append(" <li><em>  Resolution: </em> "+resolution+"\n");
      buff.append(" </ul>\n");
    }

    java.util.List metadata = ds.getMetadata();
    boolean gotSomeMetadata = false;
    for (int i=0; i<metadata.size(); i++) {
      InvMetadata m = (InvMetadata) metadata.get(i);
      if (m.hasXlink()) gotSomeMetadata = true;
    }

    if (gotSomeMetadata) {
      buff.append("<h3>Metadata:</h3>\n<ul>\n");
      for (int i=0; i<metadata.size(); i++) {
        InvMetadata m = (InvMetadata) metadata.get(i);
        String type = (m.getMetadataType() == null) ? "" : m.getMetadataType();
        if (m.hasXlink()) {
          String title = (m.getXlinkTitle() == null) ? "Type "+type : m.getXlinkTitle();
          buff.append(" <li> "+makeHrefResolve( ds, m.getXlinkHref().toString(), title)+"\n");
        } //else {
          //buff.append(" <li> <pre>"+m.getMetadataType()+" "+m.getContentObject()+"</pre>\n");
        //}
      }
      buff.append("</ul>");
    }

    java.util.List props = ds.getProperties();
    if (props.size() > 0) {
      buff.append("<h3>Properties:</h3>\n<ul>\n");
      for (int i=0; i<props.size(); i++) {
        InvProperty p = (InvProperty) props.get(i);
        if (p.getName().equals("attachments"))
          buff.append(" <li>"+makeHrefResolve(ds, p.getValue(), p.getName())+"\n");
        else
          buff.append(" <li>"+p.getName()+" = \"" + p.getValue()+"\"\n");
      }
      buff.append("</ul>");
    }

    if (complete) buff.append("</body></html>");
  }

  static private String rangeString( ThreddsMetadata.Range r) {
    if (r == null) return "";
    String units = (r.getUnits() == null) ? "" : " "+r.getUnits() ;
    String resolution = r.hasResolution() ? "" : " Resolution=" + r.getResolution() ;
    return r.getStart()+" to "+(r.getStart()+r.getSize()) + resolution + units;
  }

  /**
   * resolve reletive URLS against the catalog URL.
   * @param ds use ds parent catalog, if it exists
   * @param href URL to resolve
   * @return resolved URL
   */
  static public String resolve( InvDataset ds, String href) {
    InvCatalog cat = ds.getParentCatalog();
    if (cat != null) {
      try {
        java.net.URI uri = cat.resolveUri(href);
        href = uri.toString();
      }
      catch (java.net.URISyntaxException e) {
        System.err.println("InvDatasetImpl.writeHtml: error parsing URL= " + href);
      }
    }
    return href;
  }

  static private String makeHref( String href, String title) {
    if (title == null) title = href;
    return "<a href='"+href+"'>"+title+"</a>";
  }

  static private String makeHrefResolve( InvDatasetImpl ds, String href, String title) {
    if (title == null) title = href;
    href = resolve( ds, href);
    return makeHref( href, title);
  }

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

  /** debugging info */
  public String dump() { return dump(0); }

  String dump(int n) {
    StringBuffer buff = new StringBuffer(100);

    buff.append(indent(n));
    buff.append("Dataset name:<"+getName());
    if (dataType != null)
      buff.append("> dataType:<"+dataType);
    if (urlPath != null)
      buff.append("> urlPath:<"+urlPath);
    if (defaultService != null)
      buff.append("> defaultService <"+defaultService);
    buff.append("> uID:<"+getUniqueID());
    buff.append(">\n");

    List svcs = getServicesLocal();
    if (svcs.size() > 0) {
      String indent = indent(n+2);
      buff.append(indent);
      buff.append("Services:\n");
      for (int i=0; i<svcs.size(); i++) {
        InvService s = (InvService) svcs.get(i);
        buff.append( s.dump(n+4));
      }
    }

    if (access.size() > 0) {
      String indent = indent(n+2);
      buff.append(indent);
      if (access.size() == 1) {
        buff.append("Access: "+access.get(0)+"\n");
      } else if (access.size() > 1) {
        buff.append("Access:\n");
        for (int i=0; i<access.size(); i++) {
          InvAccess a = (InvAccessImpl) access.get(i);
          buff.append( indent(n+4) + a + "\n");
        }
      }
    }

    if (docs.size() > 0) {
      String indent = indent(n+2);
      buff.append(indent);
      buff.append("Docs:\n");
      for (int i=0; i<docs.size(); i++) {
        InvDocumentation doc = (InvDocumentation) docs.get(i);
        buff.append( indent(n+4) + doc + "\n");
      }
    }

    if (metadata.size() > 0) {
      String indent = indent(n+2);
      buff.append(indent);
      buff.append("Metadata:\n");
      for (int i=0; i<metadata.size(); i++) {
        InvMetadata m = (InvMetadata) metadata.get(i);
        buff.append( indent(n+4) + m +"\n");
      }
    }

    List props = properties;
    if (props.size() > 0) {
      String indent = indent(n+2);
      buff.append(indent);
      buff.append("Properties:\n");
      for (int i=0; i<props.size(); i++) {
        InvProperty p = (InvProperty) props.get(i);
        buff.append( indent(n+4) + p + "\n");
      }
    }

    if (datasets.size() > 0) {
      String indent = indent(n+2);
      buff.append(indent);
      buff.append("Datasets:\n");
      for (int i=0; i<datasets.size(); i++) {
        InvDatasetImpl ds = (InvDatasetImpl) datasets.get(i);
        buff.append( ds.dump(n+4));
      }
    }
    return buff.toString();
  }

  static String indent(int n) {
    StringBuffer blanks = new StringBuffer(n);
    for (int i=0; i<n; i++)
      blanks.append(" ");
    return blanks.toString();
  }

  boolean check(StringBuffer out, boolean show) {
    boolean isValid = true;

    if (log.length() > 0) {
      out.append( log);
    }

    /* check that the serviceName is valid
    if (serviceName != null) {
      if (null == findService(serviceName)) {
        out.append("**Dataset ("+getFullName()+"): has unknown service named ("+serviceName+")\n");
        isValid = false;
      }
    } */

    for (int i=0; i<access.size(); i++) {
      InvAccessImpl a = (InvAccessImpl) access.get(i);
      isValid &= a.check( out, show);
    }

    for (int i=0; i<datasets.size(); i++) {
      InvDatasetImpl ds = (InvDatasetImpl) datasets.get(i);
      isValid &= ds.check( out, show);
    }

    List mdata = getMetadata();
    for (int i=0; i<mdata.size(); i++) {
      InvMetadata m = (InvMetadata) mdata.get(i);
      m.check( out);
    }

    List services = getServicesLocal();
    for (int i=0; i<services.size(); i++) {
      InvService s = (InvService) services.get(i);
      isValid &= s.check( out);
    }

    if (hasAccess() && (getDataType() == null)) {
      out.append("**Warning: Dataset ("+getFullName()+"): is selectable but no data type declared in it or in a parent element\n");
    }

    if (!hasAccess() && !hasNestedDatasets()) {
      out.append("**Warning: Dataset ("+getFullName()+"): is not selectable and does not have nested datasets\n");
    }

    if (show) System.out.println("  dataset "+name+" valid = "+isValid);

    return isValid;
  }

  /** InvDatasetImpl elements with same values are equal. */
   public boolean equals(Object o) {
     if (this == o) return true;
     if (!(o instanceof InvDatasetImpl)) return false;
     return o.hashCode() == this.hashCode();
  }
  /** Override Object.hashCode() to implement equals. */
  public int hashCode() {
    if (hashCode == 0) {
      int result = 17;
      result = 37*result + getName().hashCode();
      result = 37*result + getServicesLocal().hashCode();
      result = 37*result + getDatasets().hashCode();
      result = 37*result + getAccessLocal().hashCode();
      result = 37*result + (isHarvest() ? 1 : 0);

      if (null != getCollectionType())
        result = 37*result + getCollectionType().hashCode();

      result = 37*result + getDocumentation().hashCode();
      result = 37*result + getProperties().hashCode();
      result = 37*result + getMetadata().hashCode();

      result = 37*result + getCreators().hashCode();
      result = 37*result + getContributors().hashCode();
      result = 37*result + getDates().hashCode();
      result = 37*result + getKeywords().hashCode();
      result = 37*result + getProjects().hashCode();
      result = 37*result + getPublishers().hashCode();
      result = 37*result + getVariables().hashCode();

      if (null != getID())
        result = 37*result + getID().hashCode();
      if (null != getAlias())
        result = 37*result + getAlias().hashCode();
      if (null != getAuthority())
        result = 37*result + getAuthority().hashCode();
      if (null != getDataType())
        result = 37*result + getDataType().hashCode();
      if (null != getDataFormatType())
        result = 37*result + getDataFormatType().hashCode();
      if (null != getServiceDefault())
        result = 37*result + getServiceDefault().hashCode();
      if (null != getUrlPath())
        result = 37*result + getUrlPath().hashCode();

      if (null != getGeospatialCoverage())
        result = 37*result + getGeospatialCoverage().hashCode();

      if (null != getTimeCoverage())
        result = 37*result + getTimeCoverage().hashCode(); // */

      hashCode = result;
    }
    return hashCode;
  }
  private volatile int hashCode = 0; // Bloch, item 8 - lazily initialize hash value


  /** test */
  public static void main(String[] args)
  {
    InvDatasetImpl topDs = new InvDatasetImpl( null, "topDs", DataType.getType("Grid"), "myService", "myUrlPath/");
    InvService myS = new InvService( "myService", ServiceType.DODS.toString(),
      "http://motherlode.ucar.edu/cgi-bin/dods/nph-dods", "", null);
    topDs.addService( myS);
    topDs.getLocalMetadata().setServiceName( "myService");
    InvDatasetImpl childDs = new InvDatasetImpl( null, "childDs", null, null, "myUrlPath/");
    topDs.addDataset( childDs);
    InvService ts = childDs.findService( "myService");

    System.out.println( "InvDatasetImpl.main(): " + childDs.getAccess( ServiceType.DODS).toString());
  }

}

/**
 * $Log: InvDatasetImpl.java,v $
 * Revision 1.14  2004/12/14 15:41:00  caron
 * *** empty log message ***
 *
 * Revision 1.13  2004/06/09 00:27:25  caron
 * version 2.0a release; cleanup javadoc
 *
 * Revision 1.12  2004/06/04 22:28:44  caron
 * convertTover1; get ver 06 inheritence right
 *
 * Revision 1.11  2004/05/21 16:31:05  edavis
 * Add removeLocalMetadata(InvMetadata) method for use by CatalogGen.
 *
 * Revision 1.10  2004/05/21 05:57:31  caron
 * release 2.0b
 *
 * Revision 1.9  2004/05/11 23:30:27  caron
 * release 2.0a
 *
 * Revision 1.8  2004/03/23 18:58:40  caron
 * add user properties hash
 *
 * Revision 1.7  2004/02/20 00:49:50  caron
 * 1.3 changes
 *
 * Revision 1.6  2003/05/29 21:21:52  john
 * getMetadataInherited() resolveUri()
 *
 * Revision 1.5  2003/03/17 20:04:02  john
 * compound service
 *
 * Revision 1.4  2002/12/13 00:36:25  caron
 * pass 2 of thredds build environ
 *
 * Revision 1.3  2002/11/26 00:05:55  caron
 * merge2 ethan's changes
 *
 * Revision 1.2  2002/11/25 22:43:55  caron
 * merge ethan's changes
 *
 * Revision 1.10  2002/11/19 21:25:17  edavis
 * Changes for CatalogGen release 0.6:
 * 1) Added setServiceName( String), getServiceName(), and findServiceName();
 * 2) added setName( String) and setUrlPath( String);
 * 3) added validateAccessMethods() and two helper methods,
 * addDuplicateAccess( java.util.List, String, InvService) and
 * expandCompoundService( InvService);
 * 4) changed getMetadata() and added getInheretedMetadata() and
 * getInheretedMetadata( MetadataType);
 * 5) added removeDataset( InvDatasetImpl) and removeMetadata( InvMetadata).
 *
 * Revision 1.9  2002/10/18 18:20:43  caron
 * thredds server
 *
 * Revision 1.8  2002/09/18 16:24:11  caron
 * version 0.6 release
 *
 * Revision 1.7  2002/07/02 20:42:54  caron
 * add writes
 *
 * Revision 1.6  2002/07/01 23:35:01  caron
 * release 0.6
 *
 * Revision 1.5  2002/06/28 21:28:19  caron
 * create vresion 6 object model
 *
 *
 */