/////////////////////////////////////////////////////////////////////////////
// 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.io.*;

import opendap.dap.*;

/**
 * Holds a OPeNDAP Server Array value.
 *
 * @author ndp
 * @version $Revision: 15901 $
 * @see BaseType
 */
public abstract class SDArray extends DArray implements ServerArrayMethods, RelOps {
    private boolean Synthesized;
    private boolean ReadMe;

    /**
     * Constructs a new SDArray.
     */
    public SDArray() {
        super();
        Synthesized = false;
        ReadMe = false;

    }

    /**
     * Constructs a new SDArray with name n.
     *
     * @param n the name of the variable.
     */
    public SDArray(String n) {
        super(n);
        Synthesized = false;
        ReadMe = false;
    }

    /**
     * Write the variable's declaration in a C-style syntax. This
     * function is used to create textual representation of the Data
     * Descriptor Structure (DDS).  See The OPeNDAP User Manual for
     * information about this structure.
     *
     * @param os          The PrintWriter on which to print the
     *                    declaration.
     * @param space       Each line of the declaration will begin with the
     *                    characters in this string.  Usually used for leading spaces.
     * @param print_semi  a boolean value indicating whether to print a
     *                    semicolon at the end of the declaration.
     * @param constrained a boolean value indicating whether to print
     *                    the declartion dependent on the projection information. This
     *                    is only used by Server side code.
     * @see BaseType#printDecl(PrintWriter, String, boolean, boolean)
     */
    public void printDecl(PrintWriter os, String space, boolean print_semi,
                          boolean constrained) {
        if (constrained && !isProject())
            return;

        // BEWARE! Since printDecl()is (multiple) overloaded in BaseType and
        // all of the different signatures of printDecl() in BaseType lead to
        // one signature, we must be careful to override that SAME signature
        // here. That way all calls to printDecl() for this object lead to
        // this implementation.

        // Also, since printDecl()is (multiple) overloaded in BaseType and
        // all of the different signatures of printDecl() in BaseType lead to
        // the signature we are overriding here, we MUST call the printDecl
        // with the SAME signature THROUGH the super class reference
        // (assuming we want the super class functionality). If we do
        // otherwise, we will create an infinte call loop. OOPS!

        //os.println("SDArray.printDecl()");

        super.printDecl(os, space, print_semi, constrained);
    }

    /**
     * Prints the value of the variable, with its declaration.  This
     * function is primarily intended for debugging OPeNDAP applications and
     * text-based clients such as geturl.
     *

     *
 Important Note

     * This method overrides the BaseType method of the same name and
     * type signature and it significantly changes the behavior for all versions
     * of printVal() for this type:
     *  All the various versions of printVal() will only
     * print a value, or a value with declaration, if the variable is
     * in the projection.
     *

     *
In other words, if a call to
     * isProject() for a particular variable returns
     * true then printVal() will print a value
     * (or a declaration and a value).
     *

     *
If isProject() for a particular variable returns
     * false then printVal() is basically a No-Op.
     *

     *

     *
     * @param os           the PrintWriter on which to print the value.
     * @param space        this value is passed to the printDecl method,
     *                     and controls the leading spaces of the output.
     * @param print_decl_p a boolean value controlling whether the
     *                     variable declaration is printed as well as the value.
     * @see BaseType#printVal(PrintWriter, String, boolean)
     * @see ServerMethods#isProject()
     */
    public  void printVal(PrintWriter os, String space, boolean print_decl_p) {

        if (!isProject())
            return;

        PrimitiveVector pv = getPrimitiveVector();

        if (pv instanceof BaseTypePrimitiveVector) {

            BaseTypePrimitiveVector vals = (BaseTypePrimitiveVector) pv;

            if (print_decl_p) {
                printDecl(os, space, false, true);
                os.print(" = ");
            }

            os.print("{ ");
            //vals.printVal(os, "");

            ServerMethods sm;
            int len = vals.getLength();
            for (int i = 0; i < len - 1; i++) {
                sm = (ServerMethods) vals.getValue(i);
                if (sm.isProject()) {
                    ((BaseType) sm).printVal(os, "", false);
                    os.print(", ");
                }
            }
            // print last value, if any, without trailing comma
            if (len > 0) {
                sm = (ServerMethods) vals.getValue(len - 1);
                if (sm.isProject()) {
                    ((BaseType) sm).printVal(os, "", false);
                }
            }

            os.print("}");

            if (print_decl_p)
                os.println(";");
        } else {
            super.printVal(os, space, print_decl_p);
        }

    }

    /**
     * Given a size and a name, this function adds a dimension to the array.
     * For example, if the SDArray is already 10 elements long,
     * calling appendDim with a size of 5 will transform the
     * array into a 10x5 matrix. Calling it again with a size of 2 will
     * create a 10x5x2 array, and so on. This overloads
     * appendDim of DArray so that projection
     * information need for the server side implementation can be handled.
     *
     * @param size the size of the desired new dimension.
     * @param name the name of the new dimension.
     */
    public void appendDim(int size, String name) {
        // Add the new dimension to the core array
        super.appendDim(size, name);
    }

    /**
     * Add a dimension to the array. Same as appendDim(size,
     * null).
     *
     * @param size the size of the desired new dimension.
     * @see DArray#appendDim(int, String)
     */
    public final void appendDim(int size) {
        this.appendDim(size, null);
    }

    // --------------- Projection Interface

    /**
     * Set the state of this variable's projection. true means
     * that this variable is part of the current projection as defined by
     * the current constraint expression, otherwise the current projection
     * for this variable should be false.
     *
     * @param state true if the variable is part of the current
     *              projection, false otherwise.
     * @param all   This parameter has no effect for this type of variable.
     * @see CEEvaluator
     */
    public void setProject(boolean state, boolean all) {
        setProjected(state);
        PrimitiveVector vals = getPrimitiveVector();
        ((ServerMethods) (vals.getTemplate())).setProject(state, all);
    }

    // RelOps Interface

    /**
     * The RelOps interface defines how each type responds to relational
     * operators. Most (all?) types will not have sensible responses to all
     * of the relational operators (e.g. DArray won't know how to match a
     * regular expression but DString will). For those operators that are
     * nonsensical a class should throw InvalidOperatorException.
     */

    public boolean equal(BaseType bt) throws InvalidOperatorException, RegExpException, SBHException {
        throw new InvalidOperatorException("Equals (=) operator does not work with the type SDArray!");
    }

    public boolean not_equal(BaseType bt) throws InvalidOperatorException, RegExpException, SBHException {
        throw new InvalidOperatorException("Not Equals (!=) operator does not work with the type SDArray!");
    }

    public boolean greater(BaseType bt) throws InvalidOperatorException, RegExpException, SBHException {
        throw new InvalidOperatorException("Greater Than (>)operator does not work with the type SDArray!");
    }

    public boolean greater_eql(BaseType bt) throws InvalidOperatorException, RegExpException, SBHException {
        throw new InvalidOperatorException("GreaterThan or equals (<=) operator does not work with the type SDArray!");
    }

    public boolean less(BaseType bt) throws InvalidOperatorException, RegExpException, SBHException {
        throw new InvalidOperatorException("LessThan (<) operator does not work with the type SDArray!");
    }

    public boolean less_eql(BaseType bt) throws InvalidOperatorException, RegExpException, SBHException {
        throw new InvalidOperatorException("LessThan oe equals (<=) operator does not work with the type SDArray!");
    }

    public boolean regexp(BaseType bt) throws InvalidOperatorException, RegExpException, SBHException {
        throw new InvalidOperatorException("Regular Expression's don't work with the type SDArray!");
    }

    // FileIO Interface

    /**
     * Set the Synthesized property.
     *
     * @param state If true then the variable is considered a
     *              synthetic variable and no part of OPeNDAP will ever try to read it from a
     *              file, otherwise if false the variable is considered a
     *              normal variable whose value should be read using the
     *              read() method. By default this property is false.
     * @see #isSynthesized()
     * @see #read(String, Object)
     */
    public void setSynthesized(boolean state) {
        Synthesized = state;
    }

    /**
     * Get the value of the Synthesized property.
     *
     * @return true if this is a synthetic variable,
     *         false otherwise.
     */
    public boolean isSynthesized() {
        return (Synthesized);
    }

    /**
     * Set the Read property. A normal variable is read using the
     * read() method. Once read the Read property is
     * true. Use this function to manually set the property
     * value. By default this property is false.
     *
     * @param state true if the variable has been read,
     *              false otherwise.
     * @see #isRead()
     * @see #read(String, Object)
     */
    public void setRead(boolean state) {
        ReadMe = state;
    }

    /**
     * Get the value of the Read property.
     *
     * @return true if the variable has been read,
     *         false otherwise.
     * @see #read(String, Object)
     * @see #setRead(boolean)
     */
    public boolean isRead() {
        return (ReadMe);
    }

    /**
     * Read a value from the named dataset for this variable.
     *
     * @param datasetName String identifying the file or other data store
     *                    from which to read a vaue for this variable.
     * @param specialO    This Object is a goody that is used by Server implementations
     *                    to deliver important, and as yet unknown, stuff to the read method. If you
     *                    don't need it, make it a null.
     * @return true if more data remains to be read, otherwise
     *         false. This is an abtsract method that must be implemented
     *         as part of the installation/localization of a OPeNDAP server.
     * @throws IOException
     * @throws EOFException
     */
    public abstract boolean read(String datasetName, Object specialO) throws NoSuchVariableException, IOException, EOFException;

    /**
     *

     * Server-side serialization for OPeNDAP variables (sub-classes of
     * BaseType). This does not send the entire class as the Java
     * Serializable interface does, rather it sends only the
     * binary data values. Other software is responsible for sending variable
     * type information (see DDS).
     *

     * Writes data to a DataOutputStream. This method is used
     * on the server side of the OPeNDAP client/server connection, and possibly
     * by GUI clients which need to download OPeNDAP data, manipulate it, and
     * then re-save it as a binary file.
     *

     *
Caution:

     * When serializing arrays of sequences (children of DSequence) it is crucial
     * that it be handled with great care. Sequences have been implemented so that
     * only one instance (or row if you will) is retained in memory at a given time.
     * In order to correctly serialize an array of sequences the read() method for the
     * array must create an instance of the sequence for each member of the array, typically
     * by repeatedly cloning the template variable in the PrimitiveVector. The important next
     * step is to NOT attempt to read any data into the sequences from within the read()
     * method of the parent array. The sequence's data will get read, and constraint expressions
     * applied when the serialze() method of the array calls the serialize method of the
     * sequence. Good Luck!
     *
     * @param sink     a DataOutputStream to write to.
     * @param ce       a CEEvaluator containing constraint Clauses.
     * @param specialO a Object to be used by ServerMethods.read()
     * @throws IOException thrown on any OutputStream
     *                     exception.
     * @see BaseType
     * @see DDS
     * @see ServerDDS
     */
    public void serialize(String dataset, DataOutputStream sink, CEEvaluator ce, Object specialO)
            throws NoSuchVariableException, DAP2ServerSideException, IOException {
        PrimitiveVector vals = getPrimitiveVector();

        if (!isRead())
            read(dataset, specialO);

        if (vals.getTemplate() instanceof DSequence || ce.evalClauses(specialO)) {
            // Because arrays of primitive types (ie int32, float32, byte,
            // etc) are handled in the C++ core using the XDR package we must
            // write the length twice for those types. For BaseType vectors,
            // we should write it only once. This is in effect a work around
            // for a bug in the C++ core as the C++ core does not consume 2
            // length values for thge BaseType vectors. Bummer...
            int length = vals.getLength();
            sink.writeInt(length);

            // Gotta check for this to make sure that DConstructor types
            // (Especially SDSequence) get handled correctly!!!
            if (vals instanceof BaseTypePrimitiveVector) {
                for (int i = 0; i < length; i++) {
                    ServerMethods sm = (ServerMethods)
                            ((BaseTypePrimitiveVector) vals).getValue(i);
                    sm.serialize(dataset, sink, ce, specialO);
                }
            } else {
                // Because both XDR and OPeNDAP read the length, we must write
                // it twice.
                sink.writeInt(length);
                vals.externalize(sink);
            }
        }
    }

    // Array Projection Interface ----------------------------

    /**
     * Set the projection information for this dimension. The
     * DArrayDimension associated with the
     * dimension specified is retrieved and the
     * start stride and stop
     * parameters are passed to its setProjection() method.
     *
     * @param dimension The dimension that is to be modified.
     * @param start     The starting point for the projection of this
     *                  DArrayDimension.
     * @param stride    The size of the stride for the projection of this
     *                  DArrayDimension.
     * @param stop      The stopping point for the projection of this
     *                  DArrayDimension.
     * @see DArray
     * @see DArrayDimension
     */
    public void setProjection(int dimension, int start, int stride, int stop)
            throws InvalidDimensionException {
        DArrayDimension d = getDimension(dimension);
        d.setProjection(start, stride, stop);
    }

    /**
     * Gets the start value for the array projection. The parameter
     * dimension is checked against the instance of the
     * SDArray for bounds violation.
     */
    public int getStart(int dimension) throws InvalidDimensionException {
        DArrayDimension d = getDimension(dimension);
        return (d.getStart());
    }

    /**
     * Gets the stride value for the array projection. The parameter
     * dimension is checked against the instance of the
     * SDArray for bounds violation.
     */
    public int getStride(int dimension) throws InvalidDimensionException {
        DArrayDimension d = getDimension(dimension);
        return (d.getStride());
    }

    /**
     * Gets the stop value for the array projection. The parameter
     * dimension is checked against the instance of the
     * SDArray for bounds violation.
     */
    public int getStop(int dimension) throws InvalidDimensionException {
        DArrayDimension d = getDimension(dimension);
        return (d.getStop());
    }

    /**
     * Write the variable's declaration in XML. This
     * function is used to create the XML representation of the Data
     * Descriptor Structure (DDS).  See The OPeNDAP User Manual for
     * information about this structure.
     *
     * @param constrained a boolean value indicating whether to print
     *                    the declartion dependent on the projection information. This
     *                    is only used by Server side code.
     * @see DDS
     * @opendap.ddx.experimental
     */
    public void printXML(PrintWriter pw, String pad, boolean constrained) {

        // BEWARE! Since printXML()is (multiple) overloaded in BaseType
        // and all of the different signatures of printXML() in BaseType
        // lead to one signature, we must be careful to override that
        // SAME signature here. That way all calls to printDecl() for
        // this object lead to this implementation.

        // Also, since printXML()is (multiple) overloaded in BaseType
        // and all of the different signatures of printXML() in BaseType
        // lead to the signature we are overriding here, we MUST call
        // the printXML with the SAME signature THROUGH the super class
        // reference (assuming we want the super class functionality). If
        // we do otherwise, we will create an infinte call loop. OOPS!

        if (constrained && !isProject())
            return;

        super.printXML(pw, pad, constrained);
    }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
/**
 *  Class to contain a single field of data from a dbase file
 * @author Russ Rew
 */
package ucar.nc2.ui.gis.shapefile;

import ucar.nc2.constants.CDM;

import java.io.DataInputStream;

public class DbaseData {
  DbaseFieldDesc desc;
  int nrec;  /* number of records */

  /**
   * Character type data (String[]).
   */
  public final static int TYPE_CHAR = 0;
  /**
   * Data is an array of doubles (double[]).
   */
  public final static int TYPE_NUMERIC = 1;
  /**
   * Data is an array of booleans (boolean[]).
   */
  public final static int TYPE_BOOLEAN = 2;

  /* the various possible types */
  String[] character;
  double[] numeric;
  boolean[] logical;
  byte[] field;
  int type;

  DbaseData(DbaseFieldDesc desc, int nrec) {
    this.desc = desc;
    this.nrec = nrec;
    field = new byte[desc.FieldLength];
    switch (desc.Type) {
      case 'C':
      case 'D':
        character = new String[nrec];
        type = TYPE_CHAR;
        break;
      case 'N':
      case 'F':
        numeric = new double[nrec];
        type = TYPE_NUMERIC;
        break;
      case 'L':
        logical = new boolean[nrec];
        type = TYPE_BOOLEAN;
        break;
    }

  }

  /**
   * Method to return the type of data for the field
   *
   * @return One of TYPE_CHAR, TYPE_BOOLEAN, or TYPE_NUMERIC
   */
  public int getType() {
    return type;
  }

  /**
   * Method to read an entry from the data stream.  The stream is assumed to be
   * in the right spot for reading.  This method should be called from something
   * controlling the reading of the entire file.
   *
   * @see DbaseFile
   */
  int readRowN(DataInputStream ds, int n) {
    if (n > nrec) return -1;
    /* the assumption here is that the DataInputStream (ds)
    * is already pointing at the right spot!
    */
    try {
      ds.readFully(field, 0, desc.FieldLength);
    } catch (java.io.IOException e) {
      return -1;
    }
    switch (desc.Type) {
      case 'C':
      case 'D':
        character[n] = new String(field, CDM.utf8Charset);
        break;
      case 'N':
        numeric[n] = Double.valueOf(new String(field, CDM.utf8Charset));
        break;
      case 'F':  /* binary floating point */
        if (desc.FieldLength == 4) {
          numeric[n] = (double) Swap.swapFloat(field, 0);
        } else {
          numeric[n] = Swap.swapDouble(field, 0);
        }
        break;
      case 'L':
        switch (field[0]) {
          case 't':
          case 'T':
          case 'Y':
          case 'y':
            logical[n] = true;
            break;
          default:
            logical[n] = false;
            break;
        }
      default:
        return -1;
    }

    return 0;
  }

  /**
   * Method to retrieve the double array for this field
   *
   * @return An array of doubles with the data
   */
  public double[] getDoubles() {
    return numeric;
  }

  /**
   * Method to retrieve a double for this field
   *
   * @param i index of desired double, assumes 0 < i < getNumRec()
   * @return A double with the data
   */
  public double getDouble(int i) {
    return numeric[i];
  }

  /**
   * Method to retrieve a booleans array for this field
   *
   * @return An array of boolean values
   */
  public boolean[] getBooleans() {
    return logical;
  }

  /**
   * Method to retrieve a boolean for this field
   *
   * @param i index of desired boolean, assumes 0 < i < getNumRec()
   * @return A boolean with the data
   */
  public boolean getBoolean(int i) {
    return logical[i];
  }

  /**
   * Method to retrieve an array of Strings for this field
   *
   * @return An array of Strings
   */
  public String[] getStrings() {
    return character;
  }

  /**
   * Method to retrieve a String for this field
   *
   * @param i index of desired String, assumes 0 < i < getNumRec()
   * @return A String with the data
   */
  public String getString(int i) {
    return character[i];
  }

  /**
   * Method to retrieve data for this field
   *
   * @param i index of desired String, assumes 0 < i < getNumRec()
   * @return either a Double, Boolean, or String with the data
   */
  public Object getData(int i) {
    switch (type) {
      case TYPE_CHAR:
        return character[i];
      case TYPE_NUMERIC:
        return numeric[i];
      case TYPE_BOOLEAN:
        return logical[i];
    }
    return null;
  }

  /**
   * @return The number of records in the field.
   */
  public int getNumRec() {
    return nrec;
  }
}


package ucar.nc2.grib.grib2.table;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import ucar.nc2.wmo.Util;

/**
 * Read and manage the WMO GRIB2 Code, Flag, and Parameter tables, in their standard XML format
 */
public class WmoCodeFlagTables {

  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(WmoCodeFlagTables.class);
  public static final Version standard = Version.GRIB2_22_0_0;

  public enum Version {
    GRIB2_22_0_0;

    public String getResourceName() {
      return "/resources/grib2/wmo/" + this.name() + "_CodeFlag_exp_en.xml";
    }

    @Nullable
    String[] getElemNames() {
      if (this == GRIB2_22_0_0) {
        return new String[]{"GRIB2_22_0_0_CodeFlag_exp_en", "Title_en", "SubTitle_en", "MeaningParameterDescription_en",
            "UnitComments_en"};
      }

      return null;
    }
  }

  public enum TableType {param, code, flag, cat}

  private static WmoCodeFlagTables instance = null;

  public static WmoCodeFlagTables getInstance() {
    if (instance == null) {
      instance = new WmoCodeFlagTables();
      try {
        instance.readGribCodes(standard);
      } catch (IOException e) {
        logger.error("Cant read WMO Grib2 tables");
        throw new RuntimeException(e);
      }
    }
    return instance;
  }

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

  private ImmutableList<WmoTable> wmoTables;
  private ImmutableMap<String, WmoTable> wmoTableMap;

  public ImmutableList<WmoTable> getWmoTables() {
    return wmoTables;
  }

  @Nullable
  public TableType getTableType(String tableName) {
    WmoTable wmoTable = wmoTableMap.get(tableName);
    if (wmoTable == null) {
      return null;
    }
    return wmoTable.getType();
  }

  @Nullable
  public WmoCodeTable getCodeTable(String tableName) {
    WmoTable wmoTable = wmoTableMap.get(tableName);
    if (wmoTable == null) {
      return null;
    }
    return new WmoCodeTable(wmoTable);
  }

  @Nullable
  public WmoCodeTable getCodeTable(int m1, int m2) {
    String name = String.format("%d.%d", m1, m2);
    WmoTable wmoTable = wmoTableMap.get(name);
    if (wmoTable == null) {
      return null;
    }
    return new WmoCodeTable(wmoTable);
  }

  @Nullable
  public WmoParamTable getParamTable(int discipline, int category) {
    String name = String.format("4.2.%d.%d", discipline, category);
    WmoTable wmoTable = wmoTableMap.get(name);
    if (wmoTable == null) {
      return null;
    }
    return new WmoParamTable(wmoTable);
  }

  /*
  Param Table:
  <GRIB2_22_0_0_CodeFlag_exp_en>
    <No>524</No>
    <Title_en>Code table 4.2 - Parameter number by product discipline and parameter category</Title_en>
    <SubTitle_en>Product discipline 0 - Meteorological products, parameter category 1: moisture</SubTitle_en>
    <CodeFlag>101</CodeFlag>
    <MeaningParameterDescription_en>Specific number concentration of snow</MeaningParameterDescription_en>
    <UnitComments_en>kg-1</UnitComments_en>
    <ElementDescription_en>Number of particles per unit mass of air</ElementDescription_en>
    <Status>Operational</Status>
  </GRIB2_22_0_0_CodeFlag_exp_en>

  Code Table:
  <GRIB2_22_0_0_CodeFlag_exp_en>
    <No>2</No>
    <Title_en>Code table 0.0 - Discipline of processed data in the GRIB message, number of GRIB Master table</Title_en>
    <CodeFlag>1</CodeFlag>
    <MeaningParameterDescription_en>Hydrological products</MeaningParameterDescription_en>
    <Status>Operational</Status>
  </GRIB2_22_0_0_CodeFlag_exp_en>

  FlagTable:
  <GRIB2_22_0_0_CodeFlag_exp_en>
    <No>168</No>
    <Title_en>Flag table 3.4 - Scanning mode</Title_en>
    <CodeFlag>1</CodeFlag>
    <Value>0</Value>
    <MeaningParameterDescription_en>Points of first row or column scan in the +i (+x) direction</MeaningParameterDescription_en>
    <Status>Operational</Status>
  </GRIB2_22_0_0_CodeFlag_exp_en>
  <GRIB2_22_0_0_CodeFlag_exp_en>
    <No>169</No>
    <Title_en>Flag table 3.4 - Scanning mode</Title_en>
    <CodeFlag>1</CodeFlag>
    <Value>1</Value>
    <MeaningParameterDescription_en>Points of first row or column scan in the -i (-x) direction</MeaningParameterDescription_en>
    <Status>Operational</Status>
  </GRIB2_22_0_0_CodeFlag_exp_en>
  */

  private void readGribCodes(Version version) throws IOException {
    String[] elems = version.getElemNames();
    if (elems == null) {
      throw new IllegalStateException("unknown version = " + version);
    }

    try (InputStream ios = WmoCodeFlagTables.class.getResourceAsStream(version.getResourceName())) {
      if (ios == null) {
        logger.error("cant open WmoCodeTable=" + version.getResourceName());
        throw new IOException("cant open WmoCodeTable=" + version.getResourceName());
      }

      org.jdom2.Document doc;
      try {
        SAXBuilder builder = new SAXBuilder();
        doc = builder.build(ios);
      } catch (JDOMException e) {
        throw new IOException(e.getMessage());
      }
      Element root = doc.getRootElement();

      Map<String, WmoTable> map = new HashMap<>();

      List<Element> featList = root.getChildren(elems[0]); // main element
      for (Element elem : featList) {
        String line = elem.getChildTextNormalize("No");
        String tableName = elem.getChildTextNormalize(elems[1]); // Title_en
        Element subtableElem = elem.getChild(elems[2]); // "SubTitle_en"

        TableType type;
        if (tableName.startsWith("Code table 4.1 ")) {
          type = TableType.cat;
        } else if (tableName.startsWith("Code table 4.2 ")) {
          type = TableType.param;
        } else if (tableName.startsWith("Flag")) {
          type = TableType.flag;
        } else if (tableName.startsWith("Code")) {
          type = TableType.code;
        } else {
          logger.warn("Unknown wmo table entry = '%s'", tableName);
          continue;
        }

        if (subtableElem != null) {
          tableName = subtableElem.getTextNormalize();
        }

        TableType finalType = type;
        WmoTable wmoTable = map.computeIfAbsent(tableName, name -> new WmoTable(name, finalType));

        String code = elem.getChildTextNormalize("CodeFlag");
        String value = elem.getChildTextNormalize("Value");    // Flag table only
        String meaning = elem.getChildTextNormalize(elems[3]); // MeaningParameterDescription_en

        Element unitElem = elem.getChild(elems[4]); // "UnitComments_en"
        String unit = (unitElem == null) ? null : unitElem.getTextNormalize();

        Element statusElem = elem.getChild("Status");
        String status = (statusElem == null) ? null : statusElem.getTextNormalize();

        wmoTable.addEntry(line, code, value, meaning, unit, status);
      }
      ios.close();

      this.wmoTables = map.values().stream().sorted().collect(ImmutableList.toImmutableList());
      ImmutableMap.Builder<String, WmoTable> builder = ImmutableMap.builder();
      map.values().forEach(t -> builder.put(t.getId(), t));
      this.wmoTableMap = builder.build();
    }
  }

  public static class WmoTable implements Comparable<WmoTable> {

    private final String name;
    private final TableType type;
    private final List<WmoEntry> entries = new ArrayList<>();
    private final String id;
    private int m1 = -1, m2 = -1, discipline = -1, category = -1;

    private WmoTable(String name, TableType type) {
      this.name = name;
      this.type = type;

      // extract the table id from the name.
      if (type == TableType.cat) {
        m1 = 4;
        m2 = 1;
        String[] slist = name.split("[ :]+");
        for (int i = 0; i < slist.length; i++) {
          if (slist[i].equalsIgnoreCase("discipline")) {
            discipline = Integer.parseInt(slist[i + 1]);
          }
        }
        if (discipline < 0) {
          throw new IllegalArgumentException("Cant extract param id from table " + name);
        }
        this.id = String.format("%d.%d.%d", m1, m2, discipline);

      } else if (type == TableType.param) {
        m1 = 4;
        m2 = 2;
        String[] slist = name.split("[ :]+");
        for (int i = 0; i < slist.length; i++) {
          if (slist[i].equalsIgnoreCase("discipline")) {
            discipline = Integer.parseInt(slist[i + 1]);
          }
          if (slist[i].equalsIgnoreCase("category")) {
            category = Integer.parseInt(slist[i + 1]);
          }
        }
        if (discipline < 0 || category < 0) {
          throw new IllegalArgumentException("Cant extract param id from table " + name);
        }
        this.id = String.format("%d.%d.%d.%d", m1, m2, discipline, category);

      } else {
        String[] s = name.split(" ");
        String id = s[2];
        String[] slist2 = id.split("\\.");
        if (slist2.length == 2) {
          m1 = Integer.parseInt(slist2[0]);
          m2 = Integer.parseInt(slist2[1]);
        } else {
          logger.warn("WmoCodeTable bad= %s%n" + name);
        }
        if (m1 < 0 || m2 < 0) {
          throw new IllegalArgumentException("Cant extract id from table " + name);
        }
        this.id = String.format("%d.%d", m1, m2);
      }
    }

    private WmoEntry addEntry(String line, String code, String value, String meaning, String unit, String status) {
      WmoEntry entry = new WmoEntry(line, code, value, meaning, unit, status);
      boolean isRange = (entry.start != entry.stop);
      if (!isRange) {
        entries.add(entry);
      }
      return entry;
    }

    @Override
    public int compareTo(@Nonnull WmoTable o) {
      if (m1 != o.m1) {
        return m1 - o.m1;
      }
      if (m2 != o.m2) {
        return m2 - o.m2;
      }
      if (discipline != o.discipline) {
        return discipline - o.discipline;
      }
      return category - o.category;
    }

    public String getName() {
      return name;
    }

    public TableType getType() {
      return type;
    }

    public String getId() {
      return id;
    }

    public ImmutableList<WmoEntry> getEntries() {
      return ImmutableList.copyOf(entries);
    }

    public class WmoEntry {
      private final int start, stop, line;
      private final int number, value;
      private final String code, meaning, name, unit, status;

      WmoEntry(String line, String code, String valueS, String meaning, String unit,
          String status) {
        this.line = Integer.parseInt(line);
        this.code = code;
        this.meaning = meaning;
        this.status = status;
        this.name = meaning;

        String unitW = unit;
        int startW, stopW, numberW = 0;

        try {
          int pos = code.indexOf('-');
          if (pos > 0) {
            startW = Integer.parseInt(code.substring(0, pos));
            String stops = code.substring(pos + 1);
            stopW = Integer.parseInt(stops);
          } else {
            startW = Integer.parseInt(code);
            stopW = startW;
            numberW = startW;
          }
        } catch (Exception e) {
          startW = -1;
          stopW = 0;
        }
        this.start = startW;
        this.stop = stopW;
        this.number = numberW;

        int valueW = -1;
        if (valueS != null) {
          try {
            valueW = Integer.parseInt(valueS);
          } catch (Exception e) {
            valueW = -2;
          }
        }
        this.value = valueW;

        if (type == TableType.param) {
          // massage units
          if (unit != null) {
            unitW = Util.cleanUnit(unit);
          } else {
            unitW = ""; // no null unit allowed
          }
        }
        this.unit = unitW;
      }

      public int getLine() {
        return line;
      }

      public int getStart() {
        return start;
      }

      public int getStop() {
        return stop;
      }

      public int getNumber() {
        return number;
      }

      public int getValue() {
        return value;
      }

      public String getCode() {
        return code;
      }

      public String getMeaning() {
        return meaning;
      }

      public String getName() {
        return name;
      }

      public String getUnit() {
        return unit;
      }

      public String getStatus() {
        return status;
      }

      public String getId() {
        return WmoTable.this.getId() + "." + getNumber();
      }

      public int getCategory() {
        return WmoTable.this.category;
      }

      public int getDiscipline() {
        return WmoTable.this.discipline;
      }
    }
  }

  /*

  @Nullable
  public static String getTableValue(String tableId, int value) {
    if (wmoTables == null)
      try {
        wmoTables = getWmoStandard();
      } catch (IOException e) {
        throw new IllegalStateException("cant open WMO tables");
      }

    WmoCodeTable table = wmoTables.map.get(tableId);
    if (table == null) return null;
    TableEntry entry = table.get(value);
    if (entry == null) return null;
    return entry.meaning;
  }

  @Nullable
  private static TableEntry getTableEntry(String tableId, int value) {
    if (wmoTables == null)
      try {
        wmoTables = getWmoStandard();
      } catch (IOException e) {
        throw new IllegalStateException("cant open wmo tables");
      }

    WmoCodeTable table = wmoTables.map.get(tableId);
    if (table == null) return null;
    return table.get(value);
  }

  void wtf(Formatter f) {

    int total = 0;
    int nsame = 0;
    int nsameIgn = 0;
    int ndiff = 0;
    int unknown = 0;

    f.format("DIFFERENCES between %s and %s%n", gt1.name, gt2.name);
    for (WmoCodeTable gc1 : gt1.list) {

    WmoCodeTable gc2 = gt2.map.get(gc1.tableName);
    if (gc2 == null) {
      f.format("1 table %s not found in %s%n", gc1.getTableId(), gt2.name);
      continue;
    }

    for (TableEntry p1 : gc1.entries) {
      TableEntry p2 = gc2.get(p1.start);
      if (p2 == null) {
        f.format("2 code %s not found in %s%n", p1.getId(), gt2.name);
        continue;
      }

      if (showDiff && !p1.equals(p2)) {
        f.format("3 %s not equal%n  %s%n%n", p1, p2);
      }
    }
  }
    f.format("Total=%d same=%d sameIgn=%d dif=%d unknown=%d%n", total, nsame, nsameIgn, ndiff, unknown);
}

  /* public static void showDiffFromCurrent(List<WmoCodeTable> tlist) throws IOException {
    int total = 0;
    int nsame = 0;
    int nsameIgn = 0;
    int ndiff = 0;
    int unknown = 0;

    f.format("DIFFERENCES with current parameter table (now,org) %n");
    for (WmoCodeTable gt : tlist) {
      if (!gt.isParameter) continue;
      for (TableEntry p : gt.entries) {
        if (p.meaning.equalsIgnoreCase("Missing")) continue;
        if (p.start != p.stop) continue;

        GridParameter gp = ParameterTable.getParameter(gt.discipline, gt.category, p.start);
        String paramOrg = gp.getDescription();
        if (paramOrg.startsWith("Unknown")) {
          unknown++;
          continue;
        }

        String paramOrgM = munge(paramOrg);
        String paramM = munge(p.name);
        boolean same = paramOrgM.equals(paramM);
        if (same) nsame++;
        boolean sameIgnore = paramOrgM.equalsIgnoreCase(paramM);
        if (sameIgnore) nsameIgn++;
        else ndiff++;
        total++;
        String state = same ? "  " : (sameIgnore ? "* " : "**");
        if (!same && !sameIgnore)
          f.format("%s%d %d %d%n %s%n %s%n", state, gt.discipline, gt.category, p.start, p.name, paramOrg);
      }
    }
    f.format("Total=%d same=%d sameIgn=%d dif=%d unknown=%d%n", total, nsame, nsameIgn, ndiff, unknown);
  }

  static String munge(String org) {
    String result = StringUtil2.remove(org, "_");
    result = StringUtil2.remove(result, "-");
    return result;
  }

  public static void showTable(List<WmoCodeTable> tlist,  Formatter f) {
    for (WmoCodeTable gt : tlist) {
      f.format("%d.%d (%d,%d) %s %n", gt.m1, gt.m2, gt.discipline, gt.category, gt.tableName);
      for (TableEntry p : gt.entries) {
        f.format("  %s (%d-%d) = %s %n", p.code, p.start, p.stop, p.meaning);
      }
    }
  } */

}


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

package ucar.nc2.ncml;

import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.XMLOutputter;
import thredds.client.catalog.Catalog;
import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.CDM;

import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.StringTokenizer;

/**
 * Populate a NetcdfFile directly from NcML, can be used by IOSPs.
 * All ncml elements are new, not modified.
 *
 * @author caron
 * @since Feb 26, 2011
 */
public class NcmlConstructor {
  // static private final boolean validate = false;
  static private final boolean debugConstruct = false;
  static private final boolean showParsedXML = false;

  private Formatter errlog = new Formatter();

  public Formatter getErrlog() {
    return errlog;
  }

  /**
   *
   * @param resourceLocation eg "resources/nj22/iosp/ghcnm.ncml"
   * @param target populate this file
   * @return true if success
   * @throws IOException on error
   */
  public boolean populateFromResource(String resourceLocation, NetcdfFile target) throws IOException {
    ClassLoader cl = this.getClass().getClassLoader();
    InputStream is = cl.getResourceAsStream(resourceLocation);
    if (is == null)
      throw new FileNotFoundException(resourceLocation);
    return populate(is, target);
  }

  public boolean populate(String ncml, NetcdfFile target) throws IOException {
    return populate(new ByteArrayInputStream(ncml.getBytes(CDM.utf8Charset)), target);
  }

  public boolean populate(InputStream ncml, NetcdfFile target) throws IOException {
    org.jdom2.Document doc;
    try {
      SAXBuilder builder = new SAXBuilder();
      doc = builder.build(ncml);
    } catch (JDOMException e) {
      throw new IOException(e.getMessage());
    }
    if (showParsedXML) {
      XMLOutputter xmlOut = new XMLOutputter();
      System.out.println("*** NetcdfDataset/showParsedXML = \n" + xmlOut.outputString(doc) + "\n*******");
    }

    Element netcdfElem = doc.getRootElement();
    readGroup(target, target.getRootGroup(), netcdfElem);
    return errlog.toString().length() == 0;
  }

  private void readGroup(NetcdfFile ncfile, Group parent, Element groupElem) throws IOException {

    String name = groupElem.getAttributeValue("name");

    Group g;
    if (parent == ncfile.getRootGroup()) { // special handling
      g = parent;

    } else {
      if (name == null) {
        errlog.format("NcML Group name is required (%s)%n", groupElem);
        return;
      }
      g = new Group(ncfile, parent, name);
      parent.addGroup(g);
    }

    // look for attributes
    java.util.List<Element> attList = groupElem.getChildren("attribute", Catalog.ncmlNS);
    for (Element attElem : attList) {
      readAtt(g, attElem);
    }

    // look for dimensions
    java.util.List<Element> dimList = groupElem.getChildren("dimension", Catalog.ncmlNS);
    for (Element dimElem : dimList) {
      readDim(g, dimElem);
    }

    // look for variables
    java.util.List<Element> varList = groupElem.getChildren("variable", Catalog.ncmlNS);
    for (Element varElem : varList) {
      readVariable(ncfile, g, null, varElem);
    }

    // LOOK for typedef enums

    // look for nested groups
    java.util.List<Element> groupList = groupElem.getChildren("group", Catalog.ncmlNS);
    for (Element gElem : groupList) {
      readGroup(ncfile, g, gElem);
    }
  }

  /**
   * Read a NcML variable element, and nested elements, when it creates a new Variable.
   *
   * @param ncfile      target dataset
   * @param g       parent Group
   * @param parentS parent Structure
   * @param varElem ncml variable element
   * @return return new Variable
   */
  private Variable readVariable(NetcdfFile ncfile, Group g, Structure parentS, Element varElem) {
    String name = varElem.getAttributeValue("name");
    if (name == null) {
      errlog.format("NcML Variable name is required (%s)%n", varElem);
      return null;
    }

    String type = varElem.getAttributeValue("type");
    if (type == null) {
      errlog.format("NcML variable (%s) must have type attribute", name);
      return null;
    }
    DataType dtype = DataType.getType(type);

    String shape = varElem.getAttributeValue("shape");
    if (shape == null)
      shape = ""; // deprecated, prefer explicit ""

    Variable v;

    if (dtype == DataType.STRUCTURE) {
      Structure s = new Structure(ncfile, g, parentS, name);
      s.setDimensions(shape);
      v = s;
      // look for nested variables
      java.util.List<Element> varList = varElem.getChildren("variable", Catalog.ncmlNS);
      for (Element vElem : varList) {
        readVariable(ncfile, g, s, vElem);
      }

    } else if (dtype == DataType.SEQUENCE) {
        Sequence s = new Sequence(ncfile, g, parentS, name);
        v = s;
        // look for nested variables
        java.util.List<Element> varList = varElem.getChildren("variable", Catalog.ncmlNS);
        for (Element vElem : varList) {
          readVariable(ncfile, g, s, vElem);
        }

    } else {
      v = new Variable(ncfile, g, parentS, name, dtype, shape);

      // deal with values
      Element valueElem = varElem.getChild("values", Catalog.ncmlNS);
      if (valueElem != null)
        readValues(v, varElem, valueElem);
      // otherwise has fill values.
    }

    // look for attributes
    java.util.List<Element> attList = varElem.getChildren("attribute", Catalog.ncmlNS);
    for (Element attElem : attList)
      readAtt(v, attElem);

    if (parentS != null)
      parentS.addMemberVariable(v);
    else
      g.addVariable(v);

    return v;
  }

  private void readValues(Variable v, Element varElem, Element valuesElem) {

    // check if values are specified by start / increment
    String startS = valuesElem.getAttributeValue("start");
    String incrS = valuesElem.getAttributeValue("increment");
    String nptsS = valuesElem.getAttributeValue("npts");
    int npts = (nptsS == null) ? (int) v.getSize() : Integer.parseInt(nptsS);

    // either start, increment are specified
    if ((startS != null) && (incrS != null)) {
      double start = Double.parseDouble(startS);
      double incr = Double.parseDouble(incrS);
      v.setValues(npts, start, incr);
      return;
    }

    // otherwise values are listed in text
    String values = varElem.getChildText("values", Catalog.ncmlNS);
    String sep = valuesElem.getAttributeValue("separator");
    if (sep == null) sep = " ";

    if (v.getDataType() == DataType.CHAR) {
      int nhave = values.length();
      int nwant = (int) v.getSize();
      char[] data = new char[nwant];
      int min = Math.min(nhave, nwant);
      for (int i = 0; i < min; i++) {
        data[i] = values.charAt(i);
      }
      Array dataArray = Array.factory(DataType.CHAR, v.getShape(), data);
      v.setCachedData(dataArray, true);

    } else {
      // or a list of values
      List<String> valList = new ArrayList<>();
      StringTokenizer tokn = new StringTokenizer(values, sep);
      while (tokn.hasMoreTokens())
        valList.add(tokn.nextToken());
      v.setValues(valList);
    }
  }

  private void readAtt(Object parent, Element attElem) {
    String name = attElem.getAttributeValue("name");
    if (name == null) {
      errlog.format("NcML Attribute name is required (%s)%n", attElem);
      return;
    }

    try {
      ucar.ma2.Array values = NcMLReader.readAttributeValues(attElem);
      Attribute att = new ucar.nc2.Attribute(name, values);
      if (parent instanceof Group)
        ((Group) parent).addAttribute(att);
      else if (parent instanceof Variable)
        ((Variable) parent).addAttribute(att);
    } catch (RuntimeException e) {
      errlog.format("NcML new Attribute Exception: %s att=%s in=%s%n", e.getMessage(), name, parent);
    }
  }

  /**
   * Read an NcML dimension element.
   *
   * @param g       put dimension into this group
   * @param dimElem ncml dimension element
   */
  private void readDim(Group g, Element dimElem) {
    String name = dimElem.getAttributeValue("name");
    if (name == null) {
      errlog.format("NcML Dimension name is required (%s)%n", dimElem);
      return;
    }

    String lengthS = dimElem.getAttributeValue("length");
    String isUnlimitedS = dimElem.getAttributeValue("isUnlimited");
    String isSharedS = dimElem.getAttributeValue("isShared");
    String isUnknownS = dimElem.getAttributeValue("isVariableLength");

    boolean isUnlimited = (isUnlimitedS != null) && isUnlimitedS.equalsIgnoreCase("true");
    boolean isUnknown = (isUnknownS != null) && isUnknownS.equalsIgnoreCase("true");
    boolean isShared = true;
    if ((isSharedS != null) && isSharedS.equalsIgnoreCase("false"))
      isShared = false;

    int len = Integer.parseInt(lengthS);
    if ((isUnknownS != null) && isUnknownS.equalsIgnoreCase("false"))
      len = Dimension.VLEN.getLength();

    Dimension dim = new Dimension(name, len, isShared, isUnlimited, isUnknown);

    if (debugConstruct) System.out.println(" add new dim = " + dim);
    g.addDimension(dim);
  }

}

/*

 /**
   * Copy contents of "src" to "target". skip ones that already exist (by name).
   * Dimensions and Variables are replaced with equivalent elements, but unlimited dimensions are turned into regular dimensions.
   * Attribute doesnt have to be replaced because its immutable, so its copied by reference.
   *
   * @param ncml ncml as an stream
   * @param target transfer to this NetcdfDataset.
   *
  static public void transferDataset(String ncml, NetcdfFile target) throws IOException {
    transferDataset(new ByteArrayInputStream(ncml.getBytes()), target);
  }

  static public void transferDataset(InputStream ncml, NetcdfFile target) throws IOException {
    NetcdfDataset src = NcMLReader.readNcML(ncml, null);
    transferGroup(src, src.getRootGroup(), target.getRootGroup());
  }

  // transfer the objects in src group to the target group
  static private void transferGroup(NetcdfFile ds, Group src, Group targetGroup) {
    // group attributes
    transferGroupAttributes(src, targetGroup);

    // dimensions
    for (Dimension d : src.getDimensions()) {
      if (null == targetGroup.findDimensionLocal(d.getName())) {
        targetGroup.addDimension(d);
      }
    }

    // variables
    for (Variable v : src.getVariables()) {
      targetGroup.addVariable(v);
    }

    // nested groups - check if target already has it
    for (Group srcNested : src.getGroups()) {
      Group nested = targetGroup.findGroup(srcNested.getShortName());
      if (null == nested) {
        nested = new Group(ds, targetGroup, srcNested.getShortName());
        targetGroup.addGroup(nested);
      }
      transferGroup(ds, srcNested, nested);
    }
  }

  /**
   * Copy attributes from src to target, skip ones that already exist (by name)
   * @param src copy from here
   * @param target copy to here
   *
  static public void transferVariableAttributes(Variable src, Variable target) {
    for (Attribute a : src.getAttributes()) {
      if (null == target.findAttribute(a.getName()))
        target.addAttribute(a);
    }
  }

  /**
   * Copy attributes from src to target, skip ones that already exist (by name)
   * @param src copy from here
   * @param target copy to here
   *
  static public void transferGroupAttributes(Group src, Group target) {
    for (Attribute a : src.getAttributes()) {
      if (null == target.findAttribute(a.getName()))
        target.addAttribute(a);
    }
  }

  /**
   * Find the Group in newFile that corresponds (by name) with oldGroup
   *
   * @param newFile look in this NetcdfFile
   * @param oldGroup corresponding (by name) with oldGroup
   * @return corresponding Group, or null if no match.
   *
  static public Group findGroup(NetcdfFile newFile, Group oldGroup) {
    List<Group> chain = new ArrayList<Group>(5);
    Group g = oldGroup;
    while ( g.getParentGroup() != null) { // skip the root
      chain.add(0, g); // put in front
      g = g.getParentGroup();
    }

    Group newg = newFile.getRootGroup();
    for (Group oldg : chain) {
      newg = newg.findGroup( oldg.getShortName());
      if (newg == null) return null;
    }
    return newg;
  }

 */