/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE.txt for license information.
 */

package thredds.core;

import com.coverity.security.Escape;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import thredds.featurecollection.FeatureCollectionCache;
import thredds.featurecollection.InvDatasetFeatureCollection;
import thredds.server.admin.DebugCommands;
import thredds.server.catalog.DatasetScan;
import thredds.server.catalog.FeatureCollectionRef;
import thredds.server.catalog.tracker.DatasetTracker;
import thredds.servlet.DatasetSource;
import thredds.servlet.ServletUtil;
import thredds.servlet.restrict.Authorizer;
import thredds.util.TdsPathUtils;
import ucar.nc2.NetcdfFile;
import ucar.nc2.constants.FeatureType;
import ucar.nc2.dataset.DatasetUrl;
import ucar.nc2.dataset.NetcdfDataset;
import ucar.nc2.dt.grid.GridDataset;
import ucar.nc2.ft.FeatureDatasetFactoryManager;
import ucar.nc2.ft.FeatureDatasetPoint;
import ucar.nc2.ft2.coverage.CoverageDatasetFactory;
import ucar.nc2.ft2.coverage.FeatureDatasetCoverage;
import ucar.nc2.ft2.simpgeometry.SimpleGeometryFeatureDataset;
import ucar.nc2.ft2.coverage.CoverageCollection;
import ucar.nc2.ncml.NcMLReader;
import ucar.nc2.util.Optional;
import ucar.nc2.util.cache.FileFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Formatter;

/**
 * Provides an API to obtain the various Dataset objects, given the request Path.
 * <p>
 * Need to rethink return type - using null to mean many things
 *
 * @author caron
 * @since 1/23/2015
 *
 * This needs revision to separate out the url path stuff from the more general
 * file stuff.
 */
@Component
public class DatasetManager implements InitializingBean {
  static private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DatasetManager.class);
  static final boolean debugResourceControl = false;

  @Autowired
  private DataRootManager dataRootManager;

  @Autowired
  private FeatureCollectionCache featureCollectionCache;

  @Autowired
  @Qualifier("restrictedDatasetAuthorizer")
  private Authorizer restrictedDatasetAuthorizer;

  @Autowired
  private DebugCommands debugCommands;

  // injected by catalogInitializer, when catalogs are reread
  private DatasetTracker datasetTracker;

  // list of dataset sources. note we have to search this each call to getNetcdfFile - most requests (!)
  // possible change to one global hash table request
  private ArrayList<DatasetSource> datasetSources = new ArrayList<>();

  @Override
  public void afterPropertiesSet() throws Exception {
    TdsRequestedDataset.setDatasetManager(this);      // LOOK why not autowire this ?  maybe because static ??
    makeDebugActions();
  }

  public void setDatasetTracker(DatasetTracker datasetTracker) {
    if (this.datasetTracker != null)
      try {
        this.datasetTracker.close();
      } catch (IOException e) {
        log.error("Cant close datasetTracker ", e);
      }

    this.datasetTracker = datasetTracker;
  }

  public DatasetManager() {
  }

  public String getLocationFromRequestPath(String reqPath) {
    return dataRootManager.getLocationFromRequestPath(reqPath);
  }

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

  // used only for the case of Dataset (not DatasetScan) that have an NcML element inside.
  // This makes the NcML dataset the target of the server.
  private static class NcmlFileFactory implements FileFactory {
    private String ncml;

    NcmlFileFactory(String ncml) {
      this.ncml = ncml;
    }

    public NetcdfFile open(DatasetUrl durl, int buffer_size, ucar.nc2.util.CancelTask cancelTask, Object spiObject) throws IOException {
      return NcMLReader.readNcML(new StringReader(ncml), durl.trueurl, cancelTask);
    }
  }

  // return null means request has been handled, and calling routine should exit without further processing
  public NetcdfFile openNetcdfFile(HttpServletRequest req, HttpServletResponse res, String reqPath) throws IOException {
    if (log.isDebugEnabled()) log.debug("DatasetHandler wants " + reqPath);

    if (reqPath == null)
      return null;

    if (reqPath.startsWith("/"))
      reqPath = reqPath.substring(1);

    // see if its under resource control
    if (!resourceControlOk(req, res, reqPath))
      return null;

    // HEY LOOK datascan below has its own Ncml
    // look for a dataset (non scan, non fmrc) that has an ncml element
    String ncml = datasetTracker.findNcml(reqPath);
    if (ncml != null) {
      NetcdfFile ncfile = NetcdfDataset.acquireFile(new NcmlFileFactory(ncml), null, DatasetUrl.findDatasetUrl(reqPath), -1, null, null);
      if (ncfile == null) throw new FileNotFoundException(reqPath);
      return ncfile;
    }

    // look for a match
    DataRootManager.DataRootMatch match = dataRootManager.findDataRootMatch(reqPath);

    // look for an feature collection dataset
    if ((match != null) && (match.dataRoot.getFeatureCollection() != null)) {
      FeatureCollectionRef featCollection = match.dataRoot.getFeatureCollection();
      if (log.isDebugEnabled()) log.debug("  -- DatasetHandler found FeatureCollection= " + featCollection);
      InvDatasetFeatureCollection fc = featureCollectionCache.get(featCollection);
      NetcdfFile ncfile = fc.getNetcdfDataset(match.remaining);
      if (ncfile == null) throw new FileNotFoundException(reqPath);
      return ncfile;
    }

    // might be a pluggable DatasetSource:
    NetcdfFile ncfile = null;
    for (DatasetSource datasetSource : datasetSources) {   // LOOK linear
      if (datasetSource.isMine(req)) {
        ncfile = datasetSource.getNetcdfFile(req, res);
        if (ncfile != null) return ncfile;
      }
    }

    // common case - its a file
    if (match != null) {
      org.jdom2.Element netcdfElem = null; // find ncml if it exists
      if (match.dataRoot != null) {
        DatasetScan dscan = match.dataRoot.getDatasetScan();
        // if (dscan == null) dscan = match.dataRoot.getDatasetRootProxy();  // no ncml possible in getDatasetRootProxy
        if (dscan != null)
          netcdfElem = dscan.getNcmlElement();
      }

      String location = dataRootManager.getLocationFromRequestPath(reqPath);
      if (location == null)
        throw new FileNotFoundException(reqPath);

      // if theres an ncml element, open it directly through NcMLReader, therefore not being cached.
      // this is safer given all the trouble we have with ncml and caching.
      if (netcdfElem != null) {
        String ncmlLocation = "DatasetScan#" + location; // LOOK some descriptive name
        NetcdfDataset ncd = NcMLReader.readNcML(ncmlLocation, netcdfElem, "file:" + location, null);
        //new NcMLReader().readNetcdf(reqPath, ncd, ncd, netcdfElem, null);
        //if (log.isDebugEnabled()) log.debug("  -- DatasetHandler found DataRoot NcML = " + ds);
        return ncd;
      }

      DatasetUrl durl = DatasetUrl.findDatasetUrl(location);
      ncfile = NetcdfDataset.acquireFile(durl, null);
    }

    if (ncfile == null) throw new FileNotFoundException(reqPath);
    return ncfile;
  }

  /**
   * Open a file as a GridDataset, using getNetcdfFile(), so that it gets wrapped in NcML if needed.
   */
  // return null means request has been handled, and calling routine should exit without further processing
  public GridDataset openGridDataset(HttpServletRequest req, HttpServletResponse res, String reqPath) throws IOException {
    // first look for a feature collection
    DataRootManager.DataRootMatch match = dataRootManager.findDataRootMatch(reqPath);
    if ((match != null) && (match.dataRoot.getFeatureCollection() != null)) {
      // see if its under resource control
      if (!resourceAuthorized(req, res, match.dataRoot.getRestrict()))
        return null;

      FeatureCollectionRef featCollection = match.dataRoot.getFeatureCollection();
      if (log.isDebugEnabled()) log.debug("  -- DatasetHandler found FeatureCollection= " + featCollection);

      InvDatasetFeatureCollection fc = featureCollectionCache.get(featCollection);
      GridDataset gds = fc.getGridDataset(match.remaining);
      if (gds == null) throw new FileNotFoundException(reqPath);
      return gds;
    }

    // fetch it as a NetcdfFile; this deals with possible NcML
    NetcdfFile ncfile = openNetcdfFile(req, res, reqPath);
    if (ncfile == null) return null;

    NetcdfDataset ncd = null;
    try {
      // Convert to NetcdfDataset
      ncd = NetcdfDataset.wrap(ncfile, NetcdfDataset.getDefaultEnhanceMode());
      return new ucar.nc2.dt.grid.GridDataset(ncd);

    } catch (Throwable t) {
      if (ncd == null)
        ncfile.close();
      else
        ncd.close();

      if (t instanceof IOException)
        throw (IOException) t;

      String msg = ncd == null ? "Problem wrapping NetcdfFile in NetcdfDataset" : "Problem creating GridDataset from NetcdfDataset";
      log.error("openGridDataset(): " + msg, t);
      throw new IOException(msg + t.getMessage());
    }
  }

  // return null means request has been handled, and calling routine should exit without further processing
  public FeatureDatasetPoint openPointDataset(HttpServletRequest req, HttpServletResponse res, String reqPath) throws IOException {
    // first look for a feature collection
    DataRootManager.DataRootMatch match = dataRootManager.findDataRootMatch(reqPath);
    if ((match != null) && (match.dataRoot.getFeatureCollection() != null)) {
      // see if its under resource control
      if (!resourceAuthorized(req, res, match.dataRoot.getRestrict()))
        return null;

      FeatureCollectionRef featCollection = match.dataRoot.getFeatureCollection();
      if (log.isDebugEnabled()) log.debug("  -- DatasetHandler found FeatureCollection= " + featCollection);

      InvDatasetFeatureCollection fc = featureCollectionCache.get(featCollection);
      FeatureDatasetPoint fd = fc.getPointDataset(match.remaining);
      if (fd == null) throw new IllegalArgumentException("Not a Point Dataset " + fc.getName());
      return fd;
    }

    // fetch it as a NetcdfFile; this deals with possible NcML
    NetcdfFile ncfile = openNetcdfFile(req, res, reqPath);
    if (ncfile == null) return null;

    Formatter errlog = new Formatter();
    NetcdfDataset ncd = null;
    try {
      ncd = NetcdfDataset.wrap(ncfile, NetcdfDataset.getDefaultEnhanceMode());
      return (FeatureDatasetPoint) FeatureDatasetFactoryManager.wrap(FeatureType.ANY_POINT, ncd, null, errlog);

    } catch (Throwable t) {
      if (ncd == null)
        ncfile.close();
      else
        ncd.close();

      if (t instanceof IOException)
        throw (IOException) t;

      String msg = ncd == null ? "Problem wrapping NetcdfFile in NetcdfDataset; " : "Problem calling FeatureDatasetFactoryManager; ";
      msg += errlog.toString();
      log.error("openGridDataset(): " + msg, t);
      throw new IOException(msg + t.getMessage());
    }
  }

  // return null means request has been handled, and calling routine should exit without further processing
  public CoverageCollection openCoverageDataset(HttpServletRequest req, HttpServletResponse res, String reqPath) throws IOException {
    if (reqPath == null)
      return null;

    if (reqPath.startsWith("/"))
      reqPath = reqPath.substring(1);

    // see if its under resource control
    if (!resourceControlOk(req, res, reqPath))
      return null;

    DataRootManager.DataRootMatch match = dataRootManager.findDataRootMatch(reqPath);

    // first look for a feature collection
    if ((match != null) && (match.dataRoot.getFeatureCollection() != null)) {
      FeatureCollectionRef featCollection = match.dataRoot.getFeatureCollection();
      if (log.isDebugEnabled()) log.debug("  -- DatasetHandler found FeatureCollection= " + featCollection);

      InvDatasetFeatureCollection fc = featureCollectionCache.get(featCollection);
      CoverageCollection gds = fc.getGridCoverage(match.remaining);
      if (gds == null) throw new FileNotFoundException(reqPath);
      return gds;
    }

    // otherwise assume its a local file

    // try to open as a FeatureDatasetCoverage. This allows GRIB to be handle specially
    String location = getLocationFromRequestPath(reqPath);
    if (location != null) {
      Optional<FeatureDatasetCoverage> opt = CoverageDatasetFactory.openCoverageDataset(location);
      if (!opt.isPresent())
        throw new FileNotFoundException("Not a Grid Dataset " + reqPath + " err=" + opt.getErrorMessage());

      if (log.isDebugEnabled()) log.debug("  -- DatasetHandler found FeatureCollection from file= " + location);
      return opt.get().getSingleCoverageCollection(); // LOOK doesnt have to be single, then what is the URL?

    }

    // if ncml, must handle special, otherwise we're out of options for opening
    // a coverage collection.

    String ncml = datasetTracker.findNcml(reqPath);
    if (ncml != null) {
      Optional<FeatureDatasetCoverage> opt = CoverageDatasetFactory.openNcmlString(ncml);
      if (!opt.isPresent())
        throw new FileNotFoundException("NcML is not a Grid Dataset " + reqPath + " err=" + opt.getErrorMessage());

      if (log.isDebugEnabled()) log.debug("  -- DatasetHandler found FeatureCollection from NcML");
      return opt.get().getSingleCoverageCollection();
    }

    return null;
  }
  public SimpleGeometryFeatureDataset openSimpleGeometryDataset(HttpServletRequest req, HttpServletResponse res, String reqPath) throws IOException {
    // first look for a feature collection
    DataRootManager.DataRootMatch match = dataRootManager.findDataRootMatch(reqPath);
    if ((match != null) && (match.dataRoot.getFeatureCollection() != null)) {
      // see if its under resource control
      if (!resourceAuthorized(req, res, match.dataRoot.getRestrict()))
        return null;

      FeatureCollectionRef featCollection = match.dataRoot.getFeatureCollection();
      if (log.isDebugEnabled()) log.debug("  -- DatasetHandler found FeatureCollection= " + featCollection);

      InvDatasetFeatureCollection fc = featureCollectionCache.get(featCollection);
      SimpleGeometryFeatureDataset fd = fc.getSimpleGeometryDataset(match.remaining);
      if (fd == null) throw new IllegalArgumentException("Not a Simple Geometry Dataset " + fc.getName());
      return fd;
    }

    // fetch it as a NetcdfFile; this deals with possible NcML
    NetcdfFile ncfile = openNetcdfFile(req, res, reqPath);
    if (ncfile == null) return null;

    Formatter errlog = new Formatter();
    NetcdfDataset ncd = null;
    try {
      ncd = NetcdfDataset.wrap(ncfile, NetcdfDataset.getDefaultEnhanceMode());
      return (SimpleGeometryFeatureDataset) FeatureDatasetFactoryManager.wrap(FeatureType.SIMPLE_GEOMETRY, ncd, null, errlog);

    } catch (Throwable t) {
      if (ncd == null)
        ncfile.close();
      else
        ncd.close();

      if (t instanceof IOException)
        throw (IOException) t;

      String msg = ncd == null ? "Problem wrapping NetcdfFile in NetcdfDataset; " : "Problem calling FeatureDatasetFactoryManager; ";
      msg += errlog.toString();
      log.error("openSimpleGeometryDataset(): " + msg, t);
      throw new IOException(msg + t.getMessage());
    }
  }
  /////////////////////////////////////////////////////////////////
  // Resource control

  /**
   * Check if this is making a request for a restricted dataset, and if so, if its allowed.
   *
   * @param req     the request
   * @param res     the response
   * @param reqPath the request path; if null, use req.getPathInfo()
   * @return true if ok to proceed. If false, the appropriate error or redirect message has been sent, the caller only needs to return.
   */
  public boolean resourceControlOk(HttpServletRequest req, HttpServletResponse res, String reqPath) {
    if (null == reqPath)
      reqPath = TdsPathUtils.extractPath(req, null);

    // see if its under resource control
    String rc = null;
    DataRootManager.DataRootMatch match = dataRootManager.findDataRootMatch(reqPath);
    if (match != null) {
      rc = match.dataRoot.getRestrict(); // datasetScan, featCollection are restricted at the dataRoot
    }

    if (rc == null) {
      rc = datasetTracker.findResourceControl(reqPath); // regular datasets tracked here
    }

    return resourceAuthorized(req, res, rc);
  }

  private boolean resourceAuthorized(HttpServletRequest req, HttpServletResponse res, String rc) {
    if (rc == null) return true;
    if (debugResourceControl) System.out.println("DatasetHandler request has resource control =" + rc + "\n"
            + ServletUtil.showRequestHeaders(req) + ServletUtil.showSecurity(req, rc));

    // Principal p = req.getUserPrincipal(); // debug

    try {
      if (!restrictedDatasetAuthorizer.authorize(req, res, rc)) {
        return false;
      }
    } catch (Exception e) {
      throw new RuntimeException(e.getMessage());
    }

    if (debugResourceControl) System.out.println("ResourceControl granted = " + rc);

    return true;
  }

  /////////////////////////////////////////////////////////
  // DatasetSource

  public void registerDatasetSource(String className) {
    Class vClass;
    try {
      vClass = DatasetManager.class.getClassLoader().loadClass(className);
    } catch (ClassNotFoundException e) {
      log.error("Attempt to load DatasetSource class " + className + " not found");
      return;
    }

    if (!(DatasetSource.class.isAssignableFrom(vClass))) {
      log.error("Attempt to load class " + className + " does not implement " + DatasetSource.class.getName());
      return;
    }

    // create instance of the class
    Object instance;
    try {
      instance = vClass.newInstance();
    } catch (InstantiationException e) {
      log.error("Attempt to load Viewer class " + className + " cannot instantiate, probably need default Constructor.");
      return;
    } catch (IllegalAccessException e) {
      log.error("Attempt to load Viewer class " + className + " is not accessible.");
      return;
    }

    registerDatasetSource((DatasetSource) instance);
  }

  public void registerDatasetSource(DatasetSource v) {
    datasetSources.add(v);
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////
  // debugging
  public InvDatasetFeatureCollection openFeatureCollection(FeatureCollectionRef ftCollection) throws IOException {
    return featureCollectionCache.get(ftCollection);
  }

  void makeDebugActions() {
    DebugCommands.Category debugHandler = debugCommands.findCategory("Catalogs");
    DebugCommands.Action act;

    act = new DebugCommands.Action("showDatasetTrackerDB", "Show DatasetTracker database") {
      public void doAction(DebugCommands.Event e) {
        Formatter f = new Formatter();
        datasetTracker.showDB(f);
        e.pw.println(Escape.html(f.toString()));
      }
    };
    debugHandler.addAction(act);

  }

}


/////////////////////////////////////////////////////////////////////////////
// This file is part of the "Java-DAP" project, a Java implementation
// of the OPeNDAP Data Access Protocol.
//
// Copyright (c) 2010, OPeNDAP, Inc.
// Copyright (c) 2002,2003 OPeNDAP, Inc.
//
// Author: James Gallagher <jgallagher@opendap.org>
//
// All rights reserved.
//
// Redistribution and use in source and binary forms,
// with or without modification, are permitted provided
// that the following conditions are met:
//
// - Redistributions of source code must retain the above copyright
//   notice, this list of conditions and the following disclaimer.
//
// - Redistributions in binary form must reproduce the above copyright
//   notice, this list of conditions and the following disclaimer in the
//   documentation and/or other materials provided with the distribution.
//
// - Neither the name of the OPeNDAP nor the names of its contributors may
//   be used to endorse or promote products derived from this software
//   without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/////////////////////////////////////////////////////////////////////////////

package opendap.servers;

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

import opendap.dap.BaseType;

/**
 * Represents a clause which invokes a function that returns a BaseType.
 *
 * @author joew
 * @see ClauseFactory
 */
public class BTFunctionClause
        extends AbstractClause
        implements SubClause {

    /**
     * Creates a new BTFunctionClause.
     *
     * @param function The function invoked by the clause
     * @param children A list of SubClauses, to be given as arguments
     *                 to the function. If all the arguments are constant, the function
     *                 clause will be flagged as constant, and evaluated immediately.
     * @throws DAP2ServerSideException Thrown if either 1) the function does not
     *                        accept the arguments given, or 2) the
     *                        clause is constant, and the attempt to evaluate it fails.
     */
    protected BTFunctionClause(BTFunction function,
                               List children)
            throws DAP2ServerSideException {

        function.checkArgs(children);
        this.function = function;
        this.children = children;
        this.constant = true;
        Iterator it = children.iterator();
        while (it.hasNext()) {
            SubClause current = (SubClause) it.next();
            current.setParent(this);
            if (!current.isConstant()) {
                constant = false;
            }
        }
        value = function.getReturnType(children);
        if (constant) {
            evaluate();
        }
    }

    public Clause getParent() {
        return parent;
    }

    public BaseType getValue() {
        return value;
    }

    public BaseType evaluate()
            throws DAP2ServerSideException {

        if (!constant || !defined) {
            value = function.evaluate(children);
            defined = true;
        }
        return value;
    }

    public void setParent(Clause parent) {
        this.parent = parent;
    }

    /**
     * Returns the server-side function invoked by this clause
     */
    public BTFunction getFunction() {
        return function;
    }

    /**
     * Prints the original string representation of this clause.
     * For use in debugging.
     */
    public void printConstraint(PrintWriter os)
    {
        os.print(function.getName()+"(");
        Iterator it = children.iterator();
	boolean first = true;
        while (it.hasNext()) {
	    ValueClause vc = (ValueClause)it.next();
            if(!first) os.print(",");
	    vc.printConstraint(os);
	    first = false;
        }
        os.print(")");
    }

    protected Clause parent;

    protected BTFunction function;

    protected BaseType value;

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package ucar.nc2.iosp.gempak;

import ucar.ma2.*;

import ucar.nc2.*;
import ucar.nc2.constants.AxisType;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.CF;
import ucar.nc2.constants._Coordinate;

import ucar.unidata.io.RandomAccessFile;

import java.io.IOException;

import java.nio.ByteBuffer;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * An IOSP for Gempak Sounding (SN) data.
 *
 * @author dmurray
 */
public class GempakSoundingIOSP extends GempakStationFileIOSP {
  /*
  dmlib.txt:
                   Sounding (SN) Library

  The sounding library subroutines allow the programmer to access
  GEMPAK upper-air data files.  These files contain meteorological
  observations from many locations for different times.  The
  library contains modules which create and open files and read or
  write data to these files.

  There are two types of GEMPAK sounding files:  merged and unmerged.
  Merged files may contain an arbitrary set of parameters which
  report at every level.  Unmerged files store mandatory and
  significant data separately in the following parts with the
  given parameters:

    TTAA  mandatory data below 100 mb  PRES TEMP DWPT DRCT SPED HGHT
    TTBB  sig temp data below 100 mb   PRES TEMP DWPT
    PPBB  sig wind data below 100 mb   HGHT DRCT SPED or
                                       PRES DRCT SPED
    TTCC  mandatory data above 100 mb  PRES TEMP DWPT DRCT SPED HGHT
    TTDD  sig temp data above 100 mb   PRES TEMP DWPT
    PPDD  sig wind data above 100 mb   HGHT DRCT SPED or
                                       PRES DRCT SPED

  When wind data appear on pressure surfaces, the first pressure is
  set to the negative of its value as a flag.

  Data that are to be written to an unmerged file must be in the
  specified order.  When data are returned from an unmerged file,
  data from all the parts will be merged.  Interpolation will be
  used to fill in the significant data levels.

  Merged data files can be created using SN_CREF or SN_CRFP;
  unmerged files can be created using SN_CRUA.  SN_OPNF will open
  either file type.  SN_RDAT will read data from all files;
  unmerged data will be returned as a merged data set.  SN_RTYP can
  be called to determine whether each level is mandatory, significant
  temperature, or significant wind level data.  SN_MAND can
  be called to request that only mandatory data below 100 mb be
  returned when SN_RDAT is called.  SN_WDAT writes to merged files;
  SN_WPRT writes to unmerged files.

  The subroutines to create or open a sounding file return a file
  number which must be used in later subroutines to access the file.

  The file GEMPRM.PRM contains the maximum values for array
  dimensions when using GEMPAK subroutines.  A copy of this file has
  been included in the appendix for easy reference.  MMFILE is the
  maximum number of files that can be open.  LLMXTM is the maximum
  number of times that can be saved in a GEMPAK5 file. The maximum
  number of stations is LLSTFL and the maximum number of parameters
  is MMPARM.

  After a file is opened, both the time and station must be selected
  before data can be read or written.  There are two groups of
  subroutines that perform this function.

  If data from many stations are to be accessed for a particular
  time, the time can be set using SN_STIM.  The stations to be
  selected may be defined using LC_SARE or LC_UARE, which select
  stations using the GEMPAK variable, AREA.  The LC subroutines may
  be called before or after SN_STIM.  Stations within the area are
  returned using SN_SNXT.

  If data for many times at a particular station are required, the
  station may be selected using SN_TSTN.  The time may then be
  defined using SN_TTIM.  Alternatively, times may be returned using
  SN_TNXT.

  All GEMPAK files contain information about the station in station
  headers.  The station header names, contents, and the data types
  returned from the SN library are:

          STID    Station identifier            CHARACTER*4
          STNM    Station number                INTEGER
          SLAT    Station latitude              REAL
          SLON    Station longitude             REAL
          SELV    Station elevation in meters   REAL
          STAT    State                         CHARACTER*2
          COUN    Country                       CHARACTER*2
    STD2	Extended station ID	      CHARACTER*4

  Only SLAT and SLON are required for sounding files.  The other
  header variables are optional.

  The subroutines SN_FTIM and SN_FSTN can be used to find a time
  and station in a data set.  They will execute faster than the
  subroutines above, but can only be used with files where the times
  are in rows and the stations are in columns (or vice versa).  They were
  designed to be used in real-time data ingest applications and should
  not be used for normal applications which use general sounding
  files.

  Some examples of subroutine sequences for accessing the data follow.

  A sequence of subroutines to retrieve sounding data for many
  stations at one time is:

          Initialize GEMPAK                       (IN_BDTA)

    Open the file				(SN_OPNF)
    Define the time				(SN_STIM)
    Define the area search			(LC_SARE)
    Loop:
       Get the next station			(SN_SNXT)
       Read the data			(SN_RDAT)
    End loop
    Close the file				(SN_CLOS)

  A sequence of subroutines to retrieve sounding data for many times
  at one station is:

    Open the sounding file			(SN_OPNF)
    Get times in file                       (SN_GTIM)
    Get times to use                        (TI_FIND)
    Set the station				(SN_TSTN)
    Loop:
       Get the next time			(SN_TTIM)
       Read the data			(SN_RDAT)
    End loop
    Close the file				(SN_CLOS)

  ================================================================================

  Also, check out Don's GEMPAK Support reply to Patrick Marsh
  about the GEMPAK data format:

  http://www.unidata.ucar.edu/support/help/MailArchives/gempak/msg05580.html

  There is some documention in the GEMPAK distribution in the
  gempak/txt/gemlib directory.

  In particular, dmlib.txt explains the basic DM (Data Management)
  structure.  The other files explain specific implementations
  for grids (gdlib.txt), surface (sflib.txt) and sounding (snlib.txt).

  =====

    Another thing is this from an email Michal sent me:

  SNLIST functions to look at

  snlpdt.f
  https://github.com/Unidata/gempak/blob/master/gempak/source/programs/sn/snlist/snlpdt.f

  starting on line 133 is IF/then for different listing routines, SNLWMN, SNLWTM, SNLWSG, etc.

  snlpdt called SN_RPRT to read mandatory level data (merged data uses SN_RDAT)
  https://github.com/Unidata/gempak/blob/master/gempak/source/snlib/snrprt.f

  DM_RDTR and DM_RFLT are used to read data from DM files

  https://github.com/Unidata/gempak/blob/master/gempak/source/gemlib/dm/dmrdtr.f
  https://github.com/Unidata/gempak/blob/master/gempak/source/gemlib/dm/dmrflt.f

 */

  /**
   * static for shared dimension of length 4
   */
  protected final static Dimension DIM_MAXMERGELEVELS = new Dimension("maxMergeLevels", 50, true);

  /**
   * Make the station reader for this type
   *
   * @return a GempakSoundingFileReader
   */
  protected AbstractGempakStationFileReader makeStationReader() {
    return new GempakSoundingFileReader();
  }

  /**
   * Is this a valid file?
   *
   * @param raf RandomAccessFile to check
   * @return true if a valid Gempak grid file
   * @throws IOException problem reading file
   */
  @Override
  public boolean isValidFile(RandomAccessFile raf) throws IOException {
    if (!super.isValidFile(raf)) {
      return false;
    }
    // TODO:  handle other types of surface files
    return gemreader.getFileSubType().equals(GempakSoundingFileReader.MERGED) ||
            gemreader.getFileSubType().equals(GempakSoundingFileReader.UNMERGED);
  }

  /**
   * Get the file type id
   *
   * @return the file type id
   */
  @Override
  public String getFileTypeId() {
    return "GempakSounding";
  }

  /**
   * Get the file type description
   *
   * @return the file type description
   */
  @Override
  public String getFileTypeDescription() {
    return "GEMPAK Sounding Obs Data";
  }

  /**
   * Get the CF feature type
   *
   * @return the feature type
   */
  public String getCFFeatureType() {
    return CF.FeatureType.timeSeriesProfile.toString();
  }

  /**
   * Read the data for the variable
   *
   * @param v2      Variable to read
   * @param section section infomation
   * @return Array of data
   * @throws IOException           problem reading from file
   * @throws InvalidRangeException invalid Range
   */
  public Array readData(Variable v2, Section section) throws IOException, InvalidRangeException {
    if (gemreader == null) {
      throw new IllegalStateException("reader not initialized");
    }
    return readSoundingData(v2, section, gemreader.getFileSubType().equals(GempakSoundingFileReader.MERGED));
  }

  /**
   * Read in the data for the variable.  In this case, it should be
   * a Structure.  The section should be rank 2 (station, time).
   *
   * @param v2       variable to read
   * @param section  section of the variable
   * @param isMerged flag for merged data or not
   * @return the array of data
   * @throws IOException problem reading the file
   */
  private Array readSoundingData(Variable v2, Section section, boolean isMerged) throws IOException {

    Array array = null;
    if (v2 instanceof Structure) {

      Range stationRange = section.getRange(0);
      Range timeRange = section.getRange(1);
      int size = stationRange.length() * timeRange.length();

      Structure pdata = (Structure) v2;
      StructureMembers members = pdata.makeStructureMembers();
      ArrayStructureBB.setOffsets(members);
      ArrayStructureBB abb = new ArrayStructureBB(members, new int[]{size});
      ByteBuffer buf = abb.getByteBuffer();

      for (int stnIdx : stationRange) {
        for (int timeIdx : timeRange) {
          List<String> parts = (isMerged) ? ((GempakSoundingFileReader) gemreader).getMergedParts()
                  : ((GempakSoundingFileReader) gemreader).getUnmergedParts();
          boolean allMissing = true;

          for (String part : parts) {
            List<GempakParameter> params = gemreader.getParameters(part);
            GempakFileReader.RData vals = gemreader.DM_RDTR(timeIdx + 1, stnIdx + 1, part);
            ArraySequence aseq;
            Sequence seq = (Sequence) pdata.findVariable(part);
            if (vals == null) {
              aseq = makeEmptySequence(seq);
            } else {
              allMissing = false;
              aseq = makeArraySequence(seq, params, vals.data);
            }
            int index = abb.addObjectToHeap(aseq);
            buf.putInt(index);
          }
          buf.put((byte) (allMissing ? 1 : 0));
        }
      }
      array = abb;
    }
    return array;
  }

  /**
   * Create an empty ArraySequence for missing data
   *
   * @param seq the Sequence variable
   * @return the empty sequence
   */
  private ArraySequence makeEmptySequence(Sequence seq) {
    StructureMembers members = seq.makeStructureMembers();
    return new ArraySequence(members, new EmptyStructureDataIterator(), -1);
  }

  /**
   * Create an ArraySequence to hold the data
   *
   * @param seq    the Sequence variable
   * @param params the list of all GempakParameters possible in that sequence
   * @param values the values that were read
   * @return the ArraySequence
   */
  private ArraySequence makeArraySequence(Sequence seq, List<GempakParameter> params, float[] values) {

    if (values == null) {
      return makeEmptySequence(seq);
    }

    int numLevels = values.length / params.size();
    StructureMembers members = seq.makeStructureMembers();
    int offset = ArrayStructureBB.setOffsets(members);

    int size = offset * numLevels;
    byte[] bytes = new byte[size];
    ByteBuffer buf = ByteBuffer.wrap(bytes);
    ArrayStructureBB abb = new ArrayStructureBB(members, new int[]{numLevels}, buf, 0);
    int var = 0;
    for (int i = 0; i < numLevels; i++) {
      for (GempakParameter param : params) {
        if (members.findMember(param.getName()) != null) {
          buf.putFloat(values[var]);
        }
        var++;
      }
    }
    return new ArraySequence(members, new SequenceIterator(numLevels, abb), numLevels);
  }

  /**
   * Build the netCDF file
   *
   * @throws IOException problem reading the file
   */
  protected void fillNCFile() throws IOException {
    String fileType = gemreader.getFileSubType();
    buildFile(fileType.equals(GempakSoundingFileReader.MERGED));
  }

  /**
   * Build a standard station structure
   *
   * @param isMerged true if this is a merged file
   */
  private void buildFile(boolean isMerged) {

    // Build station list
    List<GempakStation> stations = gemreader.getStations();
    Dimension station = new Dimension("station", stations.size(), true);
    ncfile.addDimension(null, station);
    ncfile.addDimension(null, DIM_LEN8);
    ncfile.addDimension(null, DIM_LEN4);
    ncfile.addDimension(null, DIM_LEN2);
    List<Variable> stationVars = makeStationVars(stations, station);
    // loop through and add to ncfile
    for (Variable stnVar : stationVars) {
      ncfile.addVariable(null, stnVar);
    }

    // Build variable list (var(station,time))
    // time
    List<Date> timeList = gemreader.getDates();
    int numTimes = timeList.size();
    Dimension times = new Dimension(TIME_VAR, numTimes, true);
    ncfile.addDimension(null, times);
    Array varArray;
    Variable timeVar = new Variable(ncfile, null, null, TIME_VAR, DataType.DOUBLE, TIME_VAR);
    timeVar.addAttribute(new Attribute(CDM.UNITS, "seconds since 1970-01-01 00:00:00"));
    timeVar.addAttribute(new Attribute("long_name", TIME_VAR));
    varArray = new ArrayDouble.D1(numTimes);
    int i = 0;
    for (Date date : timeList) {
      ((ArrayDouble.D1) varArray).set(i, date.getTime() / 1000.d);
      i++;
    }
    timeVar.setCachedData(varArray, false);
    ncfile.addVariable(null, timeVar);

    // build the data structure
    List<Dimension> stationTime = new ArrayList<>();
    stationTime.add(station);
    stationTime.add(times);
    String structName = (isMerged) ? GempakSoundingFileReader.MERGED : GempakSoundingFileReader.UNMERGED;
    structName = structName + "Sounding";
    Structure sVar = new Structure(ncfile, null, null, structName);
    sVar.setDimensions(stationTime);
    sVar.addAttribute(new Attribute(CF.COORDINATES, "time SLAT SLON SELV"));
    List<String> sequenceNames;
    if (isMerged) {
      sequenceNames = new ArrayList<>();
      sequenceNames.add(GempakSoundingFileReader.SNDT);
    } else {
      sequenceNames = ((GempakSoundingFileReader) gemreader).getUnmergedParts();
    }
    for (String seqName : sequenceNames) {
      Sequence paramData = makeSequence(sVar, seqName, false);
      if (paramData == null) {
        continue;
      }
      sVar.addMemberVariable(paramData);
    }
    sVar.addMemberVariable(makeMissingVariable());
    ncfile.addAttribute(null, new Attribute("CF:featureType", CF.FeatureType.timeSeriesProfile.toString()));
    ncfile.addVariable(null, sVar);
  }

  /**
   * Make a Sequence for the part
   *
   * @param parent         parent structure
   * @param partName       partname
   * @param includeMissing true to include the missing variable
   * @return a Structure
   */
  protected Sequence makeSequence(Structure parent, String partName, boolean includeMissing) {
    List<GempakParameter> params = gemreader.getParameters(partName);
    if (params == null) {
      return null;
    }
    Sequence sVar = new Sequence(ncfile, null, parent, partName);
    sVar.setDimensions("");
    for (GempakParameter param : params) {
      Variable v = makeParamVariable(param, null);
      addVerticalCoordAttribute(v);
      sVar.addMemberVariable(v);
    }
    if (includeMissing) {
      sVar.addMemberVariable(makeMissingVariable());
    }
    return sVar;
  }

  /**
   * Add the vertical coordinate variables if necessary
   *
   * @param v the variable
   */
  private void addVerticalCoordAttribute(Variable v) {

    GempakSoundingFileReader gsfr = (GempakSoundingFileReader) gemreader;
    int vertType = gsfr.getVerticalCoordinate();
    String pName = v.getFullName();
    if (gemreader.getFileSubType().equals(GempakSoundingFileReader.MERGED)) {
      if ((vertType == GempakSoundingFileReader.PRES_COORD) && pName.equals("PRES")) {
        v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Pressure.name()));
      } else if ((vertType == GempakSoundingFileReader.HGHT_COORD) && (pName.equals("HGHT") || pName.equals("MHGT") || pName.equals("DHGT"))) {
        v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Height.name()));
      }
    } else if (pName.equals("PRES")) {
      v.addAttribute(new Attribute(_Coordinate.AxisType, AxisType.Pressure.name()));
    }
  }

  /**
   * An empty sequence iterator
   *
   * @author Unidata Development Team
   */
  static class EmptyStructureDataIterator implements StructureDataIterator {

    /**
     * Do we have more?
     *
     * @return false
     * @throws IOException problem with read
     */
    @Override
    public boolean hasNext() throws IOException {
      return false;
    }

    /**
     * Get the next data
     *
     * @return null
     * @throws IOException problem with read
     */
    @Override
    public StructureData next() throws IOException {
      return null;
    }

    /**
     * Reset the iterator
     *
     * @return this
     */
    @Override
    public StructureDataIterator reset() {
      return this;
    }

    /**
     * Get the current record number
     *
     * @return -1
     */
    @Override
    public int getCurrentRecno() {
      return -1;
    }

  }

  /**
   * A data iterator for a sequence
   *
   * @author Unidata Development Team
   */
  static private class SequenceIterator implements StructureDataIterator {
    private ArrayStructure abb;
    private StructureDataIterator siter;

    /**
     * Create a new iterator for the ArrayStructure
     *
     * @param count the number of records
     * @param abb   the backing store
     */
    SequenceIterator(int count, ArrayStructure abb) {
      // this.count = count;
      this.abb = abb;
    }

    /**
     * Do we have more?
     *
     * @return true if we do
     * @throws IOException problem with read
     */
    @Override
    public boolean hasNext() throws IOException {
      if (siter == null) {
        siter = abb.getStructureDataIterator();
      }
      return siter.hasNext();
    }

    /**
     * Get the next StructureData
     *
     * @return the next StructureData
     * @throws IOException problem with read
     */
    @Override
    public StructureData next() throws IOException {
      return siter.next();
    }

    /**
     * Set the buffer size
     *
     * @param bytes the buffer size
     */
    @Override
    public void setBufferSize(int bytes) {
      siter.setBufferSize(bytes);
    }

    /**
     * Reset the iterator
     *
     * @return this
     */
    @Override
    public StructureDataIterator reset() {
      siter = null;
      return this;
    }

    /**
     * Get the current record number
     *
     * @return the current record number
     */
    @Override
    public int getCurrentRecno() {
      return siter.getCurrentRecno();
    }

    @Override
    public void close() {
      siter.close();
    }

  }

}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */

package thredds.catalog;

import org.jdom2.*;
import org.jdom2.input.SAXBuilder;

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

import ucar.nc2.constants.CDM;
import ucar.nc2.util.IO;
import thredds.util.PathAliasReplacement;
import thredds.catalog.parser.jdom.InvCatalogFactory10;

/**
 * Reads an XML document and constructs thredds.catalog object.
 *

 *
Example of normal use:

 *

 *

 * InvCatalogFactory factory = new InvCatalogFactory("default", validate);
 * InvCatalogImpl catalog = (InvCatalogImpl) factory.readXML( catalogURI);
 * StringBuilder buff = new StringBuilder();
 * if (!catalog.check( buff)) {
 *   javax.swing.JOptionPane.showMessageDialog(this, "Invalid catalog <"+ catalogURI+">\n"+
 *   buff.toString());
 * }
 *

 *

 *
To write out a catalog to XML:

 *

 *

 * // write out catalog to String
 * try {
 *   System.out.println("\Catalog in XML=\n" + factory.writeXML( catalog));
 * } catch (IOException e) {
 *   e.printStackTrace();
 * }
 *

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

 * // write out catalog to a stream, catch exceptions
 * try {
 * BufferedOutputStream os = new BufferedOutputStream (new FileOutputStream(filename));
 * factory.writeXML( catalog, os);
 * os.close();
 * } catch (IOException e) {
 * e.printStackTrace();
 * }
 *

 *

 * Implementation details:  Uses JDOM to read XML documents.
 * Uses a pluggable InvCatalogConvertIF to transform the JDOM tree to the thredds.catalog objects.
 * The converters are registered based on the namespace used. We are supporting:

 *
 the older "0.6" spec, namespace "http://www.unidata.ucar.edu/thredds"
 *
 the current 1.0/1.1 spec, namespace "http://www.unidata.ucar.edu/namespaces/thredds/InvCatalog/v1.0"
 *

 * The schemas are read from a local resource, see XMLEntityResolver
 *
 * @author John Caron
 */

public class InvCatalogFactory {
  static private org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(InvCatalogFactory.class);

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

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

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

  public static InvCatalogConvertIF getDefaultConverter() {
    return new thredds.catalog.parser.jdom.InvCatalogFactory10();
  }

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

  /**
   * An InvCatalogFactory may have a name, in case you need to manage several of them.
   *
   * @return name of factory
   */
  public String getName() {
    return (this.name);
  }

  private String name;
  private InvCatalogConvertIF defaultConverter;
  private SAXBuilder saxBuilder;
  private StringBuilder warnMessages, errMessages, fatalMessages;

  private Map<String, InvCatalogConvertIF> converters = new HashMap<String, InvCatalogConvertIF>(10);
  private Map<String, MetadataConverterIF> metadataConverters = new HashMap<String, MetadataConverterIF>(10);

  /**
   * Constructor.
   * Can use this to read as many catalogs as you want, but only use in single thread.
   *
   * @param name     : optional name to keep track of factories
   * @param validate : do XML validation or not.
   */
  public InvCatalogFactory(String name, boolean validate) {
    this.name = name;

    XMLEntityResolver xml = new XMLEntityResolver(validate);
    saxBuilder = xml.getSAXBuilder();
    warnMessages = xml.getWarningMessages();
    errMessages = xml.getErrorMessages();
    fatalMessages = xml.getFatalMessages();

    setDefaults();
  }

  private List<PathAliasReplacement> dataRootLocAliasExpanders = Collections.emptyList();

  public void setDataRootLocationAliasExpanders( List<PathAliasReplacement> dataRootLocAliasExpanders )
  {
    if ( dataRootLocAliasExpanders == null )
      this.dataRootLocAliasExpanders = Collections.emptyList();
    else
      this.dataRootLocAliasExpanders = new ArrayList<PathAliasReplacement>( dataRootLocAliasExpanders );

    for ( InvCatalogConvertIF catConv : this.converters.values())
    {
      // LOOK! ToDo Should be more generic (add setter to InvCatalogConvertIF?)
      if ( catConv instanceof InvCatalogFactory10 )
        ((InvCatalogFactory10) catConv).setDataRootLocationAliasExpanders( this.dataRootLocAliasExpanders );
    }
  }

  public List<PathAliasReplacement> getDataRootLocationAliasExpanders()
  {
    return Collections.unmodifiableList( this.dataRootLocAliasExpanders );
  }

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

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

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

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

  /**
   * Find the InvCatalogConvertIF registered for this namespace.
   * @param namespace : find InvCatalogConvertIF for this namespace
   * @return the InvCatalogConvertIF registered for this namespace, or null
   */
  public InvCatalogConvertIF getCatalogConverter(String namespace) {
    return converters.get(namespace);
  }

  /*
   * Find the InvCatalogConvertIF registered for this namespace, and set it into the catalog.
   * @param cat set InvCatalogConvertIF on this catalog
   * @param namespace find InvCatalogConvertIF for this namespace
   *
  public void setCatalogConverter(InvCatalogImpl cat, String namespace) {
    cat.setCatalogConverter(getCatalogConverter(namespace));
  } */

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

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

  /**
   * Create an InvCatalog from an XML document at a named URI.
   * Failures and exceptions are handled
   * by causing validate() to fail. Therefore, be sure to call validate() before trying
   * to use the InvCatalog object.
   *
   * @param uriString : the URI name that the XML doc is at. May start with a "file:"
   * @return an InvCatalogImpl object
   */
  public InvCatalogImpl readXML(String uriString) {

    URI uri;
    try {
      uri = new URI(uriString);
    } catch (URISyntaxException e) {
      InvCatalogImpl cat = new InvCatalogImpl(uriString, null, null);
      cat.appendErrorMessage("**Fatal:  InvCatalogFactory.readXML URISyntaxException on URL (" +
          uriString + ") " + e.getMessage() + "\n", true);
      return cat;
    }

    /* if (uriString.startsWith("file:")) {
      String filename = uriString.substring(5);
      File f = new File(filename);
      if (f.exists()) {
        try {
          return readXML(new FileInputStream(f), uri);

        } catch (Exception e) {
          InvCatalogImpl cat = new InvCatalogImpl(uriString, null, null);
          cat.appendErrorMessage("**Fatal:  InvCatalogFactory.readXML error (" +
              uriString + ") " + e.getMessage() + "\n", true);
        }
      }
    }  */

    return readXML(uri);
  }

  /**
   * Read an InvCatalog from an a URI.
   * Failures and exceptions are handled
   * by causing validate() to fail. Therefore, be sure to call validate() before trying
   * to use the InvCatalog object.
   *
   * @param uri : the URI of the document, used for resolving reletive references.
   * @return an InvCatalogImpl object
   */
  public InvCatalogImpl readXML(URI uri) {
    // get ready for XML parsing
    warnMessages.setLength(0);
    errMessages.setLength(0);
    fatalMessages.setLength(0);

    org.jdom2.Document jdomDoc;
    InputStream is = null;
    try {
      jdomDoc = saxBuilder.build(uri.toURL());
//      HttpUriResolver httpUriResolver = HttpUriResolver.newDefaultUriResolver();
//      String s = httpUriResolver.getString( url );
//      //StringReader
//      is = new BufferedInputStream( httpUriResolver.getInputStream( url ), 1000000 );
//      jdomDoc = saxBuilder.build( is );
    } catch (Exception e) {
      InvCatalogImpl cat = new InvCatalogImpl(uri.toString(), null, null);
      cat.appendErrorMessage("**Fatal:  InvCatalogFactory.readXML failed"
          + "\n Exception= " + e.getClass().getName() + " " + e.getMessage()
          + "\n fatalMessages= " + fatalMessages.toString()
          + "\n errMessages= " + errMessages.toString()
          + "\n warnMessages= " + warnMessages.toString() + "\n", true);
      return cat;
    }
    finally
    {
      if ( is != null) try
      {
        is.close();
      }
      catch ( IOException e )
      {
        log.warn( "Failed to close input stream [" + uri.toString() + "]." );
      }
    }

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

    return readXML(jdomDoc, uri);
  }

  /**
   * Create an InvCatalog by reading catalog XML from a String.
   *
   * Failures and exceptions are handled by causing validate() to
   * fail. Therefore, be sure to call validate() before trying to use
   * the InvCatalog object.
   *
   * @param catAsString : the String from which to read the catalog.
   * @param baseUri : the base URI of the document, used for resolving reletive references.
   * @return an InvCatalogImpl object
   */
  public InvCatalogImpl readXML( String catAsString, URI baseUri )
  {
    return readXML( new StringReader( catAsString ), baseUri );
  }

  /**
   * Create an InvCatalog by reading catalog XML from a StringReader.
   *
   * Failures and exceptions are handled by causing validate() to
   * fail. Therefore, be sure to call validate() before trying to use
   * the InvCatalog object.
   *
   * @param catAsStringReader : the StreamReader from which to read the catalog.
   * @param baseUri : the base URI of the document, used for resolving reletive references.
   * @return an InvCatalogImpl object
   */
  public InvCatalogImpl readXML( StringReader catAsStringReader, URI baseUri )
  {
    XMLEntityResolver resolver = new XMLEntityResolver( false );
    SAXBuilder builder = resolver.getSAXBuilder();

    Document inDoc;
    try
    {
      inDoc = builder.build( catAsStringReader );
    }
    catch ( Exception e )
    {
      InvCatalogImpl cat = new InvCatalogImpl( baseUri.toString(), null, null );
      cat.appendErrorMessage( "**Fatal:  InvCatalogFactory.readXML(String catAsString, URI url) failed:"
                              + "\n  Exception= " + e.getClass().getName() + " " + e.getMessage()
                              + "\n  fatalMessages= " + fatalMessages.toString()
                              + "\n  errMessages= " + errMessages.toString()
                              + "\n  warnMessages= " + warnMessages.toString() + "\n", true );
      return cat;
    }

    return readXML( inDoc, baseUri );
  }

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

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

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

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

    return readXML(jdomDoc, uri);
  }

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

    // decide on converter based on namespace
    Element root = jdomDoc.getRootElement();
    if (!root.getName().equalsIgnoreCase("catalog")) {
      throw new IllegalArgumentException("not a catalog");
    }
    String namespace = root.getNamespaceURI();
    InvCatalogConvertIF fac = converters.get(namespace);
    if (fac == null) {
      fac = defaultConverter; // LOOK
      if (debugVersion)
        System.out.println("use default converter " + fac.getClass().getName() + "; no namespace " + namespace);
    } else if (debugVersion)
      System.out.println("use converter " + fac.getClass().getName() + " based on namespace " + namespace);

    InvCatalogImpl cat = fac.parseXML(this, jdomDoc, uri);
    cat.setCreateFrom(uri.toString());
    // cat.setCatalogFactory(this);
    //cat.setCatalogConverter(fac);
    cat.finish();

    /* if (showCatalogXML) {
      System.out.println("*** catalog/showCatalogXML");
      try {
        writeXML(cat, System.out);
      }
      catch (IOException ex) {
        log.warn("Error writing catalog for debugging", ex);
      }
    }  */

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

  /* public org.w3c.dom.Element readOtherXML( URI url) {

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

    return doc.getDocumentElement();
  } */

  /*
   * Write the catalog as an XML document to the specified stream.
   *
   * @param catalog write this catalog
   * @param os      write to this OutputStream
   * @throws IOException on an error.
   *
  public void writeXML(InvCatalogImpl catalog, OutputStream os) throws IOException {
    InvCatalogConvertIF fac = catalog.getCatalogConverter();
    if (fac == null) fac = defaultConverter;
    fac.writeXML(catalog, os);
  }

  /*
   * Write the catalog as an XML document to the specified stream.
   *
   * @param catalog write this catalog
   * @param os      write to this OutputStream
   * @param raw set true for "server side" catalogs, false is default, shows "client side" catalogs
   * @throws IOException on an error.
   *
  public void writeXML(InvCatalogImpl catalog, OutputStream os, boolean raw) throws IOException {
    InvCatalogConvertIF fac = catalog.getCatalogConverter();
    if (fac == null) fac = defaultConverter;
    fac.writeXML(catalog, os, raw);
  }

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

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

  /**
   * Write the InvCatalogImpl to the OutputStream as a InvCatalog 1.0 document.
   *
   * @param catalog - the catalog to be written
   * @param os      - the OutputStream to write to
   * @param raw  if true, write raw, vs converted
   * @throws IOException           when the OutputStream can't be written
   * @throws IllegalStateException when the factory doesn't know how to write a 1.0 document.
   */
  public void writeXML(InvCatalogImpl catalog, OutputStream os, boolean raw) throws IOException {
    InvCatalogConvertIF converter = this.getCatalogConverter(XMLEntityResolver.CATALOG_NAMESPACE_10);
    converter.writeXML(catalog, os, raw);
  }

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

  /*
   * Write the catalog as an XML document to the specified filename.
   *
   * @param catalog  write this catalog
   * @param filename write to this filename
   * @throws IOException on an error.
   */
  public void writeXML(InvCatalogImpl catalog, String filename) throws IOException {
    BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(filename));
    writeXML(catalog, os, false);
    os.close();
  }

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

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

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

  /**
   * append an error message. Used by the InvCatalogConvertIF
   * @param err append this error message
   */
  public void appendErr(String err) {
    errMessages.append(err);
  }

  /**
   * append a fatal error message
   * @param err append this error message
   */
  public void appendFatalErr(String err) {
    fatalMessages.append(err);
  }

  /**
   * append a warning message
   * @param err append this error message
   */
  public void appendWarning(String err) {
    warnMessages.append(err);
  }

  /**
   * Find the MetadataConverterIF registered for this key
   * @param key search on this key
   * @return  MetadataConverterIF else null
   */
  public MetadataConverterIF getMetadataConverter(String key) {
    if (key == null) return null;
    return metadataConverters.get(key);
  }

  ///////////////////////////////////////////////////////////////////////////////////////////////////////
  // testing

  private static InvCatalogImpl doOne(InvCatalogFactory fac, String urlString, boolean show) {
    System.out.println("***read " + urlString);
    if (show) System.out.println(" original catalog=\n" + IO.readURLcontents(urlString));
    try {
      InvCatalogImpl cat = fac.readXML(new URI(urlString));
      StringBuilder buff = new StringBuilder();
      boolean isValid = cat.check(buff, false);
      System.out.println("catalog <" + cat.getName() + "> " + (isValid ? "is" : "is not") + " valid");
      System.out.println(" validation output=\n" + buff);
      // if (show) System.out.println(" parsed catalog=\n" + fac.writeXML(cat));
      //System.out.println(" -----\n"+cat.dump());
      return cat;
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }

  }

  /**
   * testing
   */
  public static void main(String[] args) throws Exception {
    InvCatalogFactory catFactory = InvCatalogFactory.getDefaultFactory(false);
    //doOne(catFactory, "http://www.unidata.ucar.edu/georesources/idvcatalog.xml", true);
    doOne(catFactory, "file:C:/data/work/maurer/atm_mod.xml", true);
  }

}

/*
 *
Default catalog parsing

 *
 * InvCatalogFactory allows parsers that handle different catalog versions to be registered
 *  by the application. These factories may also use different XML parsers. Usually you call
 *  InvCatalogFactory.getDefaultFactory(), which already has the default factories for catalog
 *  versions. The default factories validate the catalog XML, and require jdom.jar and a validating
 *  parser like the one that ships with jdk1.4.
 *
 *
Nonvalidating catalog parsing

 *  To use a nonvalidating, small parser that does not need jdom.jar or any external parser do the
 *  following before you make any other InvCatalogFactory calls:
 *

    InvCatalogFactory myFactory = new InvCatalogFactory();
    myFactory.registerCatalogFactory("0.6", new thredds.catalog.parser.nano.Catalog6());
    InvCatalogFactory.setDefaultFactory( myFactory);
 *

 *
 *
Custom catalog parsing

 *  You may also instantiate an InvCatalogFactory and register your
 *  own factories, for example:
 *

    InvCatalogFactory myFactory = new InvCatalogFactory();
    myFactory.registerCatalogFactory("0.6", new MyInvCatalogFactory());
    InvCatalogFactory.setDefaultFactory( myFactory);

 *
 *
Custom metadata parsing

 * InvCatalogFactory also allows you to register parsers for specialized metadata elements that
 * appear in the catalog.xml. These parsers are called "metadata factories" and must implement
 * the InvMetadataFactoryIF interface.
 * Example:
 *

    InvCatalogFactory catFactory = InvCatalogFactory.getDefaultFactory();
    catFactory.registerMetadataFactory(MetadataType.WMS, new myWMSFactory());

 * In this example, any metadata element of type MetadataType.WMS will be passed
 * to your factory when the catalog is parsed. myWMSFactory will turn it into an
 * Object, which will be available through the InvMetadata.getContentObject() call.

 */


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.units;

/**
 * Provides support for a concrete implementation of derived units.
 *
 * @author Steven R. Emmerson
 */
// @Immutable
public class DerivedUnitImpl extends UnitImpl implements DerivedUnit, DerivableUnit {
    private static final long           serialVersionUID = 1L;

    /**
     * The dimensionless derived unit.
     */
    public static final DerivedUnitImpl DIMENSIONLESS    = new DerivedUnitImpl();

    /**
     * The dimension of this derived unit.
     *
     * @serial
     */
    private /* final */ UnitDimension     dimension;

    /**
     * Constructs a dimensionless derived unit from nothing.
     */
    protected DerivedUnitImpl() {
        // dimensionless derived unit
        this(new UnitDimension(), dimensionlessID());
    }

    /**
     * Returns the identifiers associated with the dimensionless, derived unit.
     *
     * @return The identifiers of the dimensionless, derived unit.
     */
    private static UnitName dimensionlessID() {
        UnitName id;
        try {
            id = UnitName.newUnitName("1", "1", "1");
        }
        catch (final NameException e) {
            id = null;
        }
        return id;
    }

    /**
     * Constructs from a unit dimension. This is a trusted constructor for use
     * by subclasses only.
     *
     * @param dimension
     *            The unit dimension.
     */
    protected DerivedUnitImpl(final UnitDimension dimension) {
        this(dimension, null);
    }

    /**
     * Constructs from identifiers. This is a trusted constructor for use by
     * subclasses only.
     *
     * @param id
     *            The identifiers for the unit.
     */
    protected DerivedUnitImpl(final UnitName id) {
        this(null, id);
    }

    /**
     * Constructs from a unit dimension and identifiers. This is a trusted
     * constructor for use by subclasses only.
     *
     * @param dimension
     *            The unit dimension.
     * @param id
     *            The identifiers for the unit.
     */
    protected DerivedUnitImpl(final UnitDimension dimension, final UnitName id) {
        super(id);
        this.dimension = dimension;
    }

    /**
     * Sets the unit dimension of this derived unit. This is a trusted method
     * for use by subclasses only and should be called only once immediately
     * after construction of this superinstance.
     *
     * @param dimension
     *            The unit dimension.
     */
    protected void setDimension(final UnitDimension dimension) {
        this.dimension = dimension;
    }

    /**
     * Returns the unit dimension of this derived unit.
     *
     * @return The unit dimension of this derived unit.
     */
    public final UnitDimension getDimension() {
        return dimension;
    }

    /**
     * Returns the quantity dimension of this derived unit.
     *
     * @return The quantity dimension of this derived unit.
     */
    public final QuantityDimension getQuantityDimension() {
        return getDimension().getQuantityDimension();
    }

    /*
     * From DerivedUnit:
     */

    /**
     * Indicates if this derived unit is the reciprocal of another derived unit
     * (e.g. "second" and "hertz").
     *
     * @param that
     *            The other, derived unit.
     */
    public final boolean isReciprocalOf(final DerivedUnit that) {
        return dimension.isReciprocalOf(that.getDimension());
    }

    /*
     * From UnitImpl:
     */

    /**
     * Returns the derived unit that is convertible with this unit. Obviously,
     * the method returns this derived unit.
     *
     * @return this.
     */
    public final DerivedUnit getDerivedUnit() {
        return this;
    }

    /**
     * Clones the derived unit changing the identifiers.
     *
     * @param id
     *            The identifiers for the new unit.
     * @return The new unit.
     */
    public final Unit clone(final UnitName id) {
        return new DerivedUnitImpl(dimension, id);
    }

    /**
     * Multiplies this derived unit by another.
     *
     * @param that
     *            The other unit.
     * @return The product of the two units.
     * @throws MultiplyException
     *             Can't multiply these units.
     */
    @Override
    protected Unit myMultiplyBy(final Unit that) throws MultiplyException {
        Unit result;
        if (dimension.getRank() == 0) {
            result = that;
        }
        else {
            if (!(that instanceof DerivedUnit)) {
                result = that.multiplyBy(this);
            }
            else {
                final UnitDimension thatDimension = ((DerivedUnit) that)
                        .getDimension();
                result = thatDimension.getRank() == 0
                        ? this
                        : new DerivedUnitImpl(dimension
                                .multiplyBy(thatDimension));
            }
        }
        return result;
    }

    /**
     * Divides this derived unit by another.
     *
     * @param that
     *            The other unit.
     * @return The quotient of the two units.
     * @throws OperationException
     *             Can't divide these units.
     */
    @Override
    protected Unit myDivideBy(final Unit that) throws OperationException {
        Unit result;
        if (dimension.getRank() == 0) {
            result = that.raiseTo(-1);
        }
        else {
            if (!(that instanceof DerivedUnit)) {
                result = that.divideInto(this);
            }
            else {
                final UnitDimension thatDimension = ((DerivedUnit) that)
                        .getDimension();
                result = thatDimension.getRank() == 0
                        ? this
                        : new DerivedUnitImpl(dimension.divideBy(thatDimension));
            }
        }
        return result;
    }

    /**
     * Divides this derived unit into another.
     *
     * @param that
     *            The other unit.
     * @return The quotient of the two units.
     * @throws OperationException
     *             Can't divide these units.
     */
    @Override
    protected Unit myDivideInto(final Unit that) throws OperationException {
        return that.divideBy(this);
    }

    /**
     * Raises this derived unit to a power.
     *
     * @param power
     *            The power.
     * @return This derived unit raised to the given power.
     */
    @Override
    protected Unit myRaiseTo(final int power) {
        return power == 1
                ? this
                : new DerivedUnitImpl(dimension.raiseTo(power));
    }

    /**
     * Converts a numerical value from this unit to the derived unit. Obviously,
     * the numerical value is unchanged.
     *
     * @param amount
     *            The numerical values in this unit.
     * @return The numerical value in the derived unit.
     */
    public final float toDerivedUnit(final float amount) {
        return amount;
    }

    /**
     * Converts a numerical value from this unit to the derived unit. Obviously,
     * the numerical value is unchanged.
     *
     * @param amount
     *            The numerical values in this unit.
     * @return The numerical value in the derived unit.
     */
    public final double toDerivedUnit(final double amount) {
        return amount;
    }

    /**
     * Converts numerical values from this unit to the derived unit. Obviously,
     * the numerical values are unchanged.
     *
     * @param input
     *            The numerical values in this unit.
     * @param output
     *            The numerical values in the derived unit. May be the same
     *            array as input.
     * @return output.
     */
    public final float[] toDerivedUnit(final float[] input, final float[] output) {
        if (input != output) {
            System.arraycopy(input, 0, output, 0, input.length);
        }
        return output;
    }

    /**
     * Converts numerical values from this unit to the derived unit. Obviously,
     * the numerical values are unchanged.
     *
     * @param input
     *            The numerical values in this unit.
     * @param output
     *            The numerical values in the derived unit. May be the same
     *            array as input.
     * @return output.
     */
    public final double[] toDerivedUnit(final double[] input,
            final double[] output) {
        if (input != output) {
            System.arraycopy(input, 0, output, 0, input.length);
        }
        return output;
    }

    /**
     * Converts a numerical value to this unit from the derived unit. Obviously,
     * the numerical value is unchanged.
     *
     * @param amount
     *            The numerical values in the derived unit.
     * @return The numerical value in this unit.
     */
    public final float fromDerivedUnit(final float amount) {
        return amount;
    }

    /**
     * Converts a numerical value to this unit from the derived unit. Obviously,
     * the numerical value is unchanged.
     *
     * @param amount
     *            The numerical values in the derived unit.
     * @return The numerical value in this unit.
     */
    public final double fromDerivedUnit(final double amount) {
        return amount;
    }

    /**
     * Converts numerical values to this unit from the derived unit. Obviously,
     * the numerical values are unchanged.
     *
     * @param input
     *            The numerical values in the derived unit.
     * @param output
     *            The numerical values in this unit. May be the same array as
     *            input.
     * @return output.
     */
    public final float[] fromDerivedUnit(final float[] input,
            final float[] output) {
        return toDerivedUnit(input, output);
    }

    /**
     * Converts numerical values to this unit from the derived unit. Obviously,
     * the numerical values are unchanged.
     *
     * @param input
     *            The numerical values in the derived unit.
     * @param output
     *            The numerical values in this unit. May be the same array as
     *            input.
     * @return output.
     */
    public final double[] fromDerivedUnit(final double[] input,
            final double[] output) {
        return toDerivedUnit(input, output);
    }

    /**
     * Indicates if values in this unit are convertible with another unit.
     *
     * @param that
     *            The other unit.
     * @return true if and only if values in this unit are
     *         convertible to values in
     *				that.
     */
    @Override
    public final boolean isCompatible(final Unit that) {
        final DerivedUnit unit = that.getDerivedUnit();
        return equals(unit) || isReciprocalOf(unit);
    }

    /**
     * Indicates if this derived unit is semantically identical to an object.
     *
     * @param object
     *            The object
     * @return true if and only if this derived unit is
     *         semantically identical to
     *				object.
     */
    @Override
    public boolean equals(final Object object) {
        if (this == object) {
            return true;
        }
        if (!(object instanceof DerivedUnit)) {
            return false;
        }
        final DerivedUnit that = (DerivedUnit) object;
        return (this instanceof BaseUnit && that instanceof BaseUnit)
                ? false
                : dimension.equals(that.getDimension());
    }

    /**
     * Returns the hash code of this instance.
     *
     * @return The hash code of this instance.
     */
    @Override
    public int hashCode() {
        return this instanceof BaseUnit
                ? System.identityHashCode(this)
                : dimension.hashCode();
    }

    /**
     * Indicates if this derived unit is dimensionless.
     *
     * @return true if and only if this derived unit is
     *         dimensionless.
     */
    public boolean isDimensionless() {
        return dimension.isDimensionless();
    }

    /**
     * Returns a string representation of this unit. If the symbol or name is
     * available, then that is returned; otherwise, the corresponding expression
     * in base units is returned.
     *
     * @return The string expression for this derived unit.
     */
    @Override
    public String toString() {
        final String string = super.toString(); // get symbol or name
        return string != null
                ? string
                : getCanonicalString();
    }

    /**
     * Returns the canonical string representation of the unit.
     *
     * @return The canonical string representation.
     */
    public String getCanonicalString() {
        return dimension.toString();
    }

    /**
     * Tests this class.
     */
    public static void main(final String[] args) throws Exception {
        final BaseUnit second = BaseUnit.getOrCreate(UnitName.newUnitName(
                "second", null, "s"), BaseQuantity.TIME);
        System.out.println("second = \"" + second + '"');
        final BaseUnit meter = BaseUnit.getOrCreate(UnitName.newUnitName(
                "meter", null, "m"), BaseQuantity.LENGTH);
        System.out.println("meter = \"" + meter + '"');
        final DerivedUnitImpl meterSecond = (DerivedUnitImpl) meter
                .myMultiplyBy(second);
        System.out.println("meterSecond = \"" + meterSecond + '"');
        final DerivedUnitImpl meterPerSecond = (DerivedUnitImpl) meter
                .myDivideBy(second);
        System.out.println("meterPerSecond = \"" + meterPerSecond + '"');
        final DerivedUnitImpl secondPerMeter = (DerivedUnitImpl) second
                .myDivideBy(meter);
        System.out.println("secondPerMeter = \"" + secondPerMeter + '"');
        System.out.println("meterPerSecond.isReciprocalOf(secondPerMeter)="
                + meterPerSecond.isReciprocalOf(secondPerMeter));
        System.out.println("meter.toDerivedUnit(1.0)="
                + meter.toDerivedUnit(1.0));
        System.out
                .println("meter.toDerivedUnit(new double[] {1,2,3}, new double[3])[1]="
                        + meter.toDerivedUnit(new double[] { 1, 2, 3 },
                                new double[3])[1]);
        System.out.println("meter.fromDerivedUnit(1.0)="
                + meter.fromDerivedUnit(1.0));
        System.out
                .println("meter.fromDerivedUnit(new double[] {1,2,3}, new double[3])[2]="
                        + meter.fromDerivedUnit(new double[] { 1, 2, 3 },
                                new double[3])[2]);
        System.out.println("meter.isCompatible(meter)="
                + meter.isCompatible(meter));
        System.out.println("meter.isCompatible(second)="
                + meter.isCompatible(second));
        System.out.println("meter.equals(meter)=" + meter.equals(meter));
        System.out.println("meter.equals(second)=" + meter.equals(second));
        System.out
                .println("meter.isDimensionless()=" + meter.isDimensionless());
        final Unit sPerS = second.myDivideBy(second);
        System.out.println("sPerS = \"" + sPerS + '"');
        System.out
                .println("sPerS.isDimensionless()=" + sPerS.isDimensionless());
        meterPerSecond.raiseTo(2);
        meter.myDivideBy(meterPerSecond);
    }
}


package thredds.server.wfs;

import ucar.nc2.ft2.simpgeometry.*;

/**
 * Takes in a Simple Geometry, determines which kind it is, and writes the corresponding xml
 *
 * @author Stanley Kaymen
 *
 */
public class GMLFeatureWriter {

    /**
     * Checks the type of the Simple Geom and calls the appropriate method to build the xml
     *
     * @param geom the Simple Geom
     * @return the xml
     */
    public String writeFeature(SimpleGeometry geom) {

        if (geom instanceof Point) return writePoint((Point)geom);
        else if (geom instanceof Line) return writeLine((Line)geom);
        else if (geom instanceof Polygon) return writePolygon((Polygon)geom);
        else return null;

    }

    /**
     * Takes in a point and writes its xml
     *
     * @param point the point
     * @return the xml
     */
    private String writePoint(Point point) {

        String xml = "";
        xml += "<gml:Point srsName=\"http://www.opengis.net/gml/srs/epsg.xml@900913\" srsDimension=\"2\">"
                + "<gml:pos>" + point.getX() + " " + point.getY() +"</gml:pos>"
                + "</gml:Point>";

        return xml;
    }

    /**
     * Takes in a line and iterates through all its points, writing the posList to xml
     *
     * @param line the line
     * @return the xml
     */
    private String writeLine(Line line) {

        String xml = "";
        xml += "<gml:LineString><gml:posList>";

        for (Point point: line.getPoints()) {
            xml += point.getX() + " " + point.getY() + " ";
        }

        xml += "</gml:posList></gml:LineString>";

        return xml;
    }

    /**
     * Takes in a polygon, checks whether it is an interior or exterior ring, and writes the corresponding xml.
     * Iterates through all linked polygons
     *
     * @param polygon the polygon
     * @return the xml
     */
    private String writePolygon(Polygon poly) {

        String xml = "";
        xml += "<gml:Polygon>";

        Polygon polygon = poly;

    //    while (polygon != null) {

            if (!polygon.getInteriorRing()) {
                xml += "<gml:exterior><gml:LinearRing><gml:posList>";

                for (Point point : polygon.getPoints()) {
                    xml += point.getX() + " " + point.getY() + " ";
                }

                xml += "</gml:posList></gml:LinearRing></gml:exterior>";

            }

            else {
                xml += "<gml:interior><gml:LinearRing><gml:posList>";

                for (Point point : polygon.getPoints()) {
                    xml += point.getX() + " " + point.getY() + " ";
                }

                xml += "</gml:posList></gml:LinearRing></gml:interior>";
            }

      //      polygon = polygon.getNext();
       // }

        xml += "</gml:Polygon>";
        return xml;
    }
}