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

package ucar.nc2.iosp.gempak;

import ucar.unidata.io.RandomAccessFile;

import java.io.*;

import java.util.*;

/**
 * Read a Gempak grid file
 */
public class GempakFileReader implements GempakConstants {

  /**
   * The file
   */
  protected RandomAccessFile rf;

  /**
   * An error message
   */
  private String errorMessage;

  /**
   * The label common param
   */
  protected DMLabel dmLabel;

  /**
   * header info
   */
  protected List<DMFileHeaderInfo> fileHeaderInfo;

  /**
   * headers
   */
  protected DMHeaders headers;

  /**
   * key
   */
  protected DMKeys keys;

  /**
   * part
   */
  protected List<DMPart> parts;

  /**
   * the machine type byte order
   */
  protected int MTMACH = RandomAccessFile.BIG_ENDIAN;

  /**
   * swap flag
   */
  protected boolean mvmst = false;

  /**
   * swap flag
   */
  protected boolean needToSwap = false;

  /**
   * file size
   */
  protected long fileSize = 0;

  /**
   * masking pattern
   */
  //private static int mskpat = 0xFFFF;
  private static int mskpat = ~0;

  /**
   * Bean ctor
   */
  GempakFileReader() {
  }

  /**
   * Get a RandomAccessFile for the file location
   *
   * @param filename filename to read.
   * @return RandomAccessFile
   * @throws IOException problem reading file
   */
  public static RandomAccessFile getFile(String filename) throws IOException {
    return RandomAccessFile.acquire(filename);
  }

  /**
   * Initialize the file, read in all the metadata (ala DM_OPEN)
   *
   * @param raf       RandomAccessFile to read.
   * @param fullCheck if true, check entire structure
   * @return a GempakFileReader
   * @throws IOException problem reading file
   */
  public static GempakFileReader getInstance(RandomAccessFile raf, boolean fullCheck) throws IOException {
    GempakFileReader gfr = new GempakFileReader();
    gfr.init(raf, fullCheck);
    return gfr;
  }

  /**
   * Initialize the file, read in all the metadata (ala DM_OPEN)
   *
   * @param raf       RandomAccessFile to read.
   * @param fullCheck if true, check entire structure
   * @throws IOException problem reading file
   */
  public boolean init(RandomAccessFile raf, boolean fullCheck) throws IOException {
    setByteOrder();
    rf = raf;
    fileSize = rf.length();
    raf.seek(0);

    return init(fullCheck);
  }

  /**
   * Initialize this reader.  Read all the metadata
   *
   * @return true if successful
   * @throws IOException problem reading the data
   */
  protected boolean init() throws IOException {
    return init(true);
  }

  /**
   * Initialize the file, read in all the metadata (ala DM_OPEN)
   *
   * @param fullCheck if true, check entire structure
   * @return true if successful
   * @throws IOException problem reading file
   */
  protected boolean init(boolean fullCheck) throws IOException {
    if (rf == null) {
      throw new IOException("file has not been set");
    }

    dmLabel = new DMLabel();
    boolean labelOk = dmLabel.init();

    if (!labelOk) {
      logError("not a GEMPAK file");
      return false;
    }

    // Read the keys  (DM_RKEY)
    readKeys();
    if (keys == null) {
      logError("Couldn't read keys");
      return false;
    }

    // Read the headers (DM_RHDA)
    readHeaders();
    if (headers == null) {
      logError("Couldn't read headers");
      return false;
    }

    // Read the parts (DM_RPRT)
    readParts();
    if (parts == null) {
      logError("Couldn't read parts");
      return false;
    }

    // Read the file header info (DM_RFIL)
    readFileHeaderInfo();
    if (fileHeaderInfo == null) {
      logError("Couldn't read file header info");
      return false;
    }

    return true;
  }

  /**
   * Get the file name.
   *
   * @return the name of the file
   */
  public String getFilename() {
    return (rf == null) ? null : rf.getLocation();
  }

  public String getErrorMessage() {
    return errorMessage;
  }

  /**
   * Get initial file size
   *
   * @return the file size when init  was called.
   */
  public long getInitFileSize() {
    return fileSize;
  }

  /**
   * Get the byte order for this system
   *
   * @return byte order
   */
  public int getByteOrder() {
    return MTMACH;
  }

  /**
   * Get the byte order for the machine type.
   *
   * @param kmachn maching type
   * @return byte order
   */
  public int getByteOrder(int kmachn) {
    if ((kmachn == MTVAX) || (kmachn == MTULTX) || (kmachn == MTALPH)
            || (kmachn == MTLNUX) || (kmachn == MTIGPH)) {
      return RandomAccessFile.LITTLE_ENDIAN;
    }
    return RandomAccessFile.BIG_ENDIAN;
  }

  /**
   * LOOK WTF ??
   * Set the machine type for this system.
   *
   * @see http://lopica.sourceforge.net/os.html
   */
  void setByteOrder() {
    String arch = System.getProperty("os.arch");
    if (arch.equals("x86") ||                    // Windows, Linux
            arch.equals("arm") ||                // Window CE
            arch.equals("x86_64") ||         // Windows64, Mac OS-X
            arch.equals("amd64") ||      // Linux64?
            arch.equals("alpha")) {  // Utrix, VAX, DECOS
      MTMACH = RandomAccessFile.LITTLE_ENDIAN;
    } else {
      MTMACH = RandomAccessFile.BIG_ENDIAN;
    }
  }

  /**
   * Read the file header info (DM_RFIL)
   *
   * @throws IOException problem reading file
   */
  protected void readFileHeaderInfo() throws IOException {
    if (dmLabel == null) {
      return;
    }
    int iread = dmLabel.kpfile;
    int numheaders = dmLabel.kfhdrs;
    String[] names = new String[numheaders];
    int[] lens = new int[numheaders];
    int[] types = new int[numheaders];
    for (int i = 0; i < numheaders; i++) {
      names[i] = DM_RSTR(iread++);
    }
    for (int i = 0; i < numheaders; i++) {
      lens[i] = DM_RINT(iread++);
    }
    for (int i = 0; i < numheaders; i++) {
      types[i] = DM_RINT(iread++);
    }
    fileHeaderInfo = new ArrayList<>();

    for (int i = 0; i < numheaders; i++) {
      DMFileHeaderInfo ghi = new DMFileHeaderInfo();
      ghi.kfhnam = names[i];
      ghi.kfhlen = lens[i];
      ghi.kfhtyp = types[i];
      fileHeaderInfo.add(ghi);
    }
  }

  /**
   * Read in the row and column keys (DM_KEY)
   *
   * @throws IOException problem reading file
   */
  protected void readKeys() throws IOException {
    if (dmLabel == null) {
      return;
    }
    keys = new DMKeys();
    // read the row keys
    int num = dmLabel.krkeys;
    List<Key> rkeys = new ArrayList<>(num);
    for (int i = 0; i < num; i++) {
      String key = DM_RSTR(dmLabel.kprkey + i);
      rkeys.add(new Key(key, i, ROW));
    }
    keys.kkrow = rkeys;
    num = dmLabel.kckeys;
    List<Key> ckeys = new ArrayList<>(num);
    for (int i = 0; i < num; i++) {
      String key = DM_RSTR(dmLabel.kpckey + i);
      ckeys.add(new Key(key, i, COL));
    }
    keys.kkcol = ckeys;
  }

  /**
   * keys to swap
   */
  private static String[] swapKeys = {
          "STID", "STD2", "STAT", "COUN", "GPM1", "GVCD"
  };

  /**
   * number  of words to swap
   */
  private static int[] swapNum = {
          1, 1, 1, 1, 3, 1
  };

  /**
   * Read the headers (DM_RHDA)
   *
   * @throws IOException problem reading file
   */
  protected void readHeaders() throws IOException {
    if (dmLabel == null) {
      return;
    }
    headers = new DMHeaders();
    List<int[]> rowHeaders = new ArrayList<>(dmLabel.krow);
    int istart = dmLabel.kprowh;

    // first word is a valid flag so we have to add 1 to size
    int[] header;
    for (int i = 0; i < dmLabel.krow; i++) {
      header = new int[dmLabel.krkeys + 1];
      DM_RINT(istart, header);
      if (header[0] != IMISSD) {
        headers.lstrw = i;
      }
      rowHeaders.add(header);
      istart += header.length;
    }
    headers.rowHeaders = rowHeaders;
    List<int[]> colHeaders = new ArrayList<>(dmLabel.kcol);
    istart = dmLabel.kpcolh;
    for (int i = 0; i < dmLabel.kcol; i++) {
      header = new int[dmLabel.kckeys + 1];
      DM_RINT(istart, header);
      if (header[0] != IMISSD) {
        headers.lstcl = i;
      }
      colHeaders.add(header);
      istart += header.length;
    }
    headers.colHeaders = colHeaders;

    // some of the words are characters
    if (needToSwap) {
      int[] keyLoc = new int[swapKeys.length];
      String[] keyType = new String[swapKeys.length];
      boolean haveRow = false;
      boolean haveCol = false;
      for (int i = 0; i < swapKeys.length; i++) {
        Key key = findKey(swapKeys[i]);
        keyLoc[i] = (key != null)
                ? key.loc + 1
                : 0;
        keyType[i] = (key != null)
                ? key.type
                : "";
        if (keyType[i].equals(ROW)) {
          haveRow = true;
        }
        if (keyType[i].equals(COL)) {
          haveCol = true;
        }
      }
      if (haveRow) {
        for (int[] toCheck : headers.rowHeaders) {
          for (int j = 0; j < swapKeys.length; j++) {
            if (keyType[j].equals(ROW)) {
              if (swapKeys[j].equals("GVCD")
                      && !(toCheck[keyLoc[j]]
                      > GempakUtil.vertCoords.length)) {
                continue;
              }
              GempakUtil.swp4(toCheck, keyLoc[j], swapNum[j]);
            }
          }
        }
      }
      if (haveCol) {
        for (int[] toCheck : headers.colHeaders) {
          for (int j = 0; j < swapKeys.length; j++) {
            if (keyType[j].equals(COL)) {
              if (swapKeys[j].equals("GVCD")
                      && !(toCheck[keyLoc[j]]
                      > GempakUtil.vertCoords.length)) {
                continue;
              }
              GempakUtil.swp4(toCheck, keyLoc[j], swapNum[j]);
            }
          }
        }
      }
    }
  }

  /**
   * Read the parts (DM_RPRT)
   *
   * @throws IOException problem reading file
   */
  protected void readParts() throws IOException {
    if (dmLabel == null) {
      return;
    }
    int iread = dmLabel.kppart;
    int numParts = dmLabel.kprt;
    DMPart[] partArray = new DMPart[numParts];
    // read the part names
    for (int i = 0; i < numParts; i++) {
      partArray[i] = new DMPart();
      partArray[i].kprtnm = DM_RSTR(iread++);
    }
    // read the part header lengths
    for (int i = 0; i < numParts; i++) {
      partArray[i].klnhdr = DM_RINT(iread++);
    }
    // read the part types
    for (int i = 0; i < numParts; i++) {
      partArray[i].ktyprt = DM_RINT(iread++);
    }
    // get number of parameters/per part.
    for (int i = 0; i < numParts; i++) {
      partArray[i].kparms = DM_RINT(iread++);
    }
    // read parameter names
    for (int i = 0; i < numParts; i++) {
      int numParms = partArray[i].kparms;
      List<DMParam> parms = new ArrayList<>(numParms);
      for (int j = 0; j < numParms; j++) {
        DMParam dmp = new DMParam();
        parms.add(dmp);
        dmp.kprmnm = DM_RSTR(iread++);
      }
      partArray[i].params = parms;
    }
    // read the scale
    for (int i = 0; i < numParts; i++) {
      int numParms = partArray[i].kparms;
      List parms = partArray[i].params;
      for (int j = 0; j < numParms; j++) {
        DMParam dmp = (DMParam) parms.get(j);
        dmp.kscale = DM_RINT(iread++);
      }
    }
    // read the offset
    for (int i = 0; i < numParts; i++) {
      int numParms = partArray[i].kparms;
      List parms = partArray[i].params;
      for (int j = 0; j < numParms; j++) {
        DMParam dmp = (DMParam) parms.get(j);
        dmp.koffst = DM_RINT(iread++);
      }
    }
    // read the nbits
    for (int i = 0; i < numParts; i++) {
      int numParms = partArray[i].kparms;
      List parms = partArray[i].params;
      for (int j = 0; j < numParms; j++) {
        DMParam dmp = (DMParam) parms.get(j);
        dmp.kbits = DM_RINT(iread++);
      }
    }
    parts = new ArrayList<>(numParts);
    parts.addAll(Arrays.asList(partArray).subList(0, numParts));
    for (DMPart part : parts) {
      if (part.ktyprt == MDRPCK) {
        part.packInfo = new PackingInfo(part);
      }
    }
  }

  /**
   * Get the byte offset in 0 based space from a 1 based 4 byte
   * FORTRAN word.
   *
   * @param fortranWord 1 based word offset
   * @return byte offset to that word
   */
  public static long getOffset(int fortranWord) {
    return (fortranWord - 1) * 4l;
  }

  /**
   * Run the program
   *
   * @param args filename
   * @throws IOException problem reading the file
   */
  public static void main(String[] args) throws IOException {
    if (args.length == 0) {
      System.out.println("need to supply a GEMPAK grid file name");
      System.exit(1);
    }

    GempakFileReader gfr = getInstance(getFile(args[0]), true);
    gfr.printFileLabel();
    gfr.printKeys();
    gfr.printHeaders();
    gfr.printParts();

  }

  /**
   * Class to mimic the GEMPAK DMLABL common block
   */
  class DMLabel {

    /**
     * File identifier
     */
    public static final String DMLABEL = "GEMPAK DATA MANAGEMENT FILE ";

    /**
     * version number
     */
    public int kversn;

    /**
     * # file headers
     */
    public int kfhdrs;

    /**
     * ptr to file keys
     */
    public int kpfile;

    /**
     * # of rows
     */
    public int krow;

    /**
     * # row keys
     */
    public int krkeys;

    /**
     * ptr to row keys
     */
    public int kprkey;

    /**
     * ptr to row headers
     */
    public int kprowh;

    /**
     * # of cols
     */
    public int kcol;

    /**
     * # col keys
     */
    public int kckeys;

    /**
     * ptr to col keys
     */
    public int kpckey;

    /**
     * ptr to col headers
     */
    public int kpcolh;

    /**
     * # of parts
     */
    public int kprt;

    /**
     * # part info
     */
    public int kppart;

    /**
     * ptr to dmg rec
     */
    public int kpdmgt;

    /**
     * length of dmg rec
     */
    public int kldmgt;

    /**
     * ptr to data
     */
    public int kpdata;

    /**
     * file type
     */
    public int kftype;

    /**
     * file source
     */
    public int kfsrce;

    /**
     * machine data type
     */
    public int kmachn;

    /**
     * int missing data values
     */
    public int kmissd;

    /**
     * double missing data values
     */
    public double smissd;

    /**
     * swap byte flags
     */
    public boolean kvmst;

    /**
     * Create a new DMLabel for the GempakFileReader
     */
    public DMLabel() {
    }

    /**
     * Read in all the info based on the block of integer words.
     * Modeled after DM_RLBL.
     *
     * @return true if okay.
     * @throws IOException problem reading the file
     */
    boolean init() throws IOException {
      rf.order(RandomAccessFile.BIG_ENDIAN);
      if (rf.length() < getOffset(31) + 4) return false;

      int mmmm = DM_RINT(26);
      if (mmmm > 100) {
        mmmm = GempakUtil.swp4(mmmm);
        needToSwap = true;
      }
      kmachn = mmmm;
      mvmst = (getByteOrder() == RandomAccessFile.BIG_ENDIAN);
      kvmst = ((kmachn == MTVAX) || (kmachn == MTULTX)
              || (kmachn == MTALPH) || (kmachn == MTLNUX)
              || (kmachn == MTIGPH));

      //      Set the file values of the missing data values to the current
      //      system values so that random values will not be converted.
      kmissd = IMISSD;
      smissd = RMISSD;

      String label = DM_RSTR(1, 28);
      if (!label.equals(DMLABEL)) {
        return false;
      }

      int[] words = new int[23];
      DM_RINT(8, words);
      kversn = words[0];
      kfhdrs = words[1];
      kpfile = words[2];
      krow = words[3];
      krkeys = words[4];
      kprkey = words[5];
      kprowh = words[6];
      kcol = words[7];
      kckeys = words[8];
      kpckey = words[9];
      kpcolh = words[10];
      kprt = words[11];
      kppart = words[12];
      kpdmgt = words[13];
      kldmgt = words[14];
      kpdata = words[15];
      kftype = words[16];
      kfsrce = words[17];
      //kmachn = words[18];  // set above
      kmissd = words[19];
      smissd = DM_RFLT(31);

      return true;
    }

    /**
     * Get a String representation of this.
     *
     * @return a String representation of this.
     */
    public String toString() {
      StringBuilder buf = new StringBuilder();

      buf.append("GEMPAK file label:\n");
      buf.append("\tVersion: " + kversn + "\n");
      buf.append("\t# File keys: " + kfhdrs + "\n");
      buf.append("\tptr to file keys: " + kpfile + "\n");
      buf.append("\t# rows: " + krow + "\n");
      buf.append("\t# row keys: " + krkeys + "\n");
      buf.append("\tptr to row  keys: " + kprkey + "\n");
      buf.append("\tprt to row header: " + kprowh + "\n");
      buf.append("\t# cols: " + kcol + "\n");
      buf.append("\t# cols keys: " + kckeys + "\n");
      buf.append("\tptr to col keys: " + kpckey + "\n");
      buf.append("\tptr to col header: " + kpcolh + "\n");
      buf.append("\t# parts: " + kprt + "\n");
      buf.append("\tptr part info: " + kppart + "\n");
      buf.append("\tptr to data mgmt record: " + kpdmgt + "\n");
      buf.append("\tlen of data mgmt record: " + kldmgt + "\n");
      buf.append("\tdata pointer: " + kpdata + "\n");
      buf.append("\tfile type: " + kftype + "\n");
      buf.append("\tfile source: " + kfsrce + "\n");
      buf.append("\tmachine type: " + kmachn + "\n");
      buf.append("\tinteger missing value: " + kmissd + "\n");
      buf.append("\tfloat missing value: " + smissd + "\n");
      buf.append("\tswap? " + needToSwap);
      buf.append("\tswap flags: " + kvmst);
      return buf.toString();

    }

  }

  /**
   * Class to hold the DM File header info
   */
  protected static class DMFileHeaderInfo {

    /**
     * file header name
     */
    public String kfhnam;

    /**
     * file header length
     */
    public int kfhlen;

    /**
     * file header type
     */
    public int kfhtyp;

    /**
     * Default ctor
     */
    public DMFileHeaderInfo() {
    }

    /**
     * Get a String representation of this object.
     *
     * @return a String representation of this object.
     */
    public String toString() {
      return "Name = " + kfhnam + "; length = " + kfhlen + "; type = " + kfhtyp;
    }
  }

  /**
   * Class to mimic the DMKEYS common block.
   */
  protected static class DMPart {

    /**
     * part name
     */
    public String kprtnm;

    /**
     * header length
     */
    public int klnhdr;

    /**
     * data type
     */
    public int ktyprt;

    /**
     * number of params
     */
    public int kparms;

    /**
     * list of params
     */
    public List<DMParam> params;

    /**
     * packing number
     */
    public int kpkno;

    /**
     * length of packed rec
     */
    public int kwordp;

    /**
     * packing info
     */
    public PackingInfo packInfo = null;

    /**
     * Default ctor
     */
    public DMPart() {
    }

    /**
     * Get a String representation of this object.
     *
     * @return a String representation of this object.
     */
    public String toString() {
      StringBuilder buf = new StringBuilder();
      buf.append("Part Name = ");
      buf.append(kprtnm);
      buf.append("; header length = ");
      buf.append(klnhdr);
      buf.append("; type = ");
      buf.append(ktyprt);
      buf.append("; packing num = ");
      buf.append(kpkno);
      buf.append("; packed rec len = ");
      buf.append(kwordp);
      buf.append("\nParameters: ");
      if ((params != null) && !params.isEmpty()) {
        for (DMParam param : params) {
          buf.append("\n  ").append(param);
        }
      }
      return buf.toString();
    }
  }

  /**
   * Class to hold DM Parameter info
   */
  protected static class DMParam {

    /**
     * part name
     */
    public String kprmnm;

    /**
     * scaling terms
     */
    public int kscale;

    /**
     * offset terms
     */
    public int koffst;

    /**
     * # bits
     */
    public int kbits;

    /**
     * Default ctor
     */
    public DMParam() {
    }

    /**
     * Get a String representation of this object.
     *
     * @return a String representation of this object.
     */
    public String toString() {
      return "Param name = " + kprmnm + "; scale = " + kscale + "; offset = " + koffst + "; bits = " + kbits;
    }
  }

  /**
   * Class to hold DM Integer packing info
   */
  protected static class PackingInfo {

    /**
     * offsets
     */
    public int[] koffst;

    /**
     * n bits
     */
    public int[] nbitsc;

    /**
     * scaling
     */
    public double[] scalec;

    /**
     * missing
     */
    public int[] imissc;

    /**
     * missing
     */
    public int[] iswrdc;

    /**
     * missing
     */
    public int[] isbitc;

    /**
     * Set the packing terms for this part
     *
     * @param part The part for the packing
     */
    public PackingInfo(DMPart part) {
      List<DMParam> params = part.params;
      int numParams = params.size();
      koffst = new int[numParams];
      nbitsc = new int[numParams];
      scalec = new double[numParams];
      imissc = new int[numParams];
      iswrdc = new int[numParams];
      isbitc = new int[numParams];
      int i = 0;
      int itotal = 0;
      for (DMParam param : params) {
        koffst[i] = param.koffst;
        nbitsc[i] = param.kbits;
        scalec[i] = Math.pow(10, param.kscale);
        imissc[i] = mskpat >>> (32 - param.kbits);
        iswrdc[i] = (itotal / 32);
        isbitc[i++] = (itotal % 32) + 1;
        itotal += param.kbits;
      }
      part.kwordp = (itotal - 1) / 32 + 1;
    }

    /**
     * Get a String representation of this object.
     * @return a String representation of this object.
     * public String toString() {
     *   StringBuffer buf = new StringBuffer();
     *   buf.append("Param name = ");
     *   buf.append(kprmnm);
     *   buf.append("; scale = ");
     *   buf.append(kscale);
     *   buf.append("; offset = ");
     *   buf.append(koffst);
     *   buf.append("; bits = ");
     *   buf.append(kbits);
     *   return buf.toString();
     * }
     */
  }

  /**
   * Class to hold information about a key.
   */
  protected static class Key {

    /**
     * the key name
     */
    public String name;

    /**
     * the key location
     */
    public int loc;

    /**
     * the key type (ROW or COL)
     */
    public String type;

    /**
     * Create a new key
     *
     * @param name the name
     * @param loc  the location
     * @param type the type
     */
    public Key(String name, int loc, String type) {
      this.name = name;
      this.loc = loc;
      this.type = type;
    }
  }

  /**
   * Class to mimic the DMKEYS common block.
   */
  protected class DMKeys {

    /**
     * row keys
     */
    public List<Key> kkrow;

    /**
     * col keys
     */
    public List<Key> kkcol;

    /**
     * Default ctor
     */
    public DMKeys() {
    }

    /**
     * Get a String representation of this object.
     *
     * @return a String representation of this object.
     */
    public String toString() {
      StringBuilder buf = new StringBuilder();
      buf.append("\nKeys:\n");
      buf.append("  row keys = ");
      for (Key key : keys.kkrow) {
        buf.append(key.name);
        buf.append(", ");
      }
      buf.append("\n");
      buf.append("  column keys = ");
      for (Key key : keys.kkcol) {
        buf.append(key.name);
        buf.append(", ");
      }
      return buf.toString();
    }
  }

  /**
   * Find a key with the given name
   *
   * @param name the name of the key
   * @return the key or null;
   */
  public Key findKey(String name) {
    if (keys == null) {
      return null;
    }
    // search rows
    for (Key key : keys.kkrow) {
      if (key.name.equals(name)) {
        return key;
      }
    }
    // search columns
    for (Key key : keys.kkcol) {
      if (key.name.equals(name)) {
        return key;
      }
    }
    return null;

  }

  /**
   * Class to mimic the DMHDRS common block.
   */
  static protected class DMHeaders {

    /**
     * last valid row
     */
    public int lstrw = 0;

    /**
     * last valid column
     */
    public int lstcl = 0;

    /**
     * row keys
     */
    public List<int[]> rowHeaders;

    /**
     * column keys
     */
    public List<int[]> colHeaders;

    /**
     * Default ctor
     */
    public DMHeaders() {
    }

    /**
     * Get a String representation of this object.
     *
     * @return a String representation of this object.
     */
    public String toString() {
      StringBuilder buf = new StringBuilder();
      buf.append("\nHeaders:\n");
      if (rowHeaders != null) {
        buf.append("  num row headers = ");
        buf.append(rowHeaders.size());
        buf.append("\n");
      }
      buf.append("  last row = ");
      buf.append(lstrw);
      buf.append("\n");
      if (colHeaders != null) {
        buf.append("  num column headers = ");
        buf.append(colHeaders.size());
        buf.append("\n");
      }
      buf.append("  last column = ");
      buf.append(lstcl);
      return buf.toString();
    }
  }

  /**
   * Find the file header with this name
   *
   * @param name name of header
   * @return headerinfo or null if not found
   */
  public DMFileHeaderInfo findFileHeader(String name) {
    if ((fileHeaderInfo == null) || fileHeaderInfo.isEmpty()) {
      return null;
    }
    for (DMFileHeaderInfo fhi : fileHeaderInfo) {
      if (name.equals(fhi.kfhnam)) {
        return fhi;
      }
    }
    return null;
  }

  /**
   * Read in the values for the file header
   *
   * @param name name of header
   * @return values or null if not found
   * @throws IOException problem reading file
   */
  public float[] getFileHeader(String name) throws IOException {
    DMFileHeaderInfo fh = findFileHeader(name);
    if ((fh == null) || (fh.kfhtyp != MDREAL)) {
      return null;
    }
    int knt = fileHeaderInfo.indexOf(fh);  // 0 based
    int iread = dmLabel.kpfile + 3 * dmLabel.kfhdrs;
    for (int i = 0; i < knt; i++) {
      DMFileHeaderInfo fhi = fileHeaderInfo.get(i);
      iread = iread + fhi.kfhlen + 1;
    }
    int nword = DM_RINT(iread);
    if (nword <= 0) {
      logError("Invalid header length for " + name);
      return null;
    }
    iread++;
    float[] rheader = new float[nword];
    if (name.equals("NAVB") && needToSwap) {
      DM_RFLT(iread, 1, rheader, 0);
      needToSwap = false;
      iread++;
      DM_RFLT(iread, 1, rheader, 1);
      needToSwap = true;
      iread++;
      DM_RFLT(iread, nword - 2, rheader, 2);

    } else {
      DM_RFLT(iread, rheader);
    }
    return rheader;
  }

  /**
   * Log an error
   *
   * @param errMsg message to log
   */
  protected void logError(String errMsg) {
    errorMessage = errMsg;
  }

  /**
   * Print the file label
   */
  public void printFileLabel() {
    if (dmLabel == null) {
      return;
    }
    System.out.println(dmLabel);
  }

  /**
   * Print the row and column keys
   */
  public void printKeys() {
    if (keys == null) {
      return;
    }
    System.out.println(keys);
  }

  /**
   * Print the row and column keys
   */
  public void printHeaders() {
    if (headers == null) {
      return;
    }
    System.out.println(headers);
  }

  /**
   * Print the part information
   */
  public void printParts() {
    if (parts == null) {
      return;
    }
    for (int i = 0; i < parts.size(); i++) {
      System.out.println("\nParts[" + i + "]:");
      System.out.println(parts.get(i));
    }
  }

  /**
   * Find the part with the particular name.
   *
   * @param name name of part to find
   * @return part number.
   */
  public int getPartNumber(String name) {
    int part = 0;
    if ((parts != null) && !parts.isEmpty()) {
      for (int i = 0; i < parts.size(); i++) {
        String partName = parts.get(i).kprtnm;
        if (partName.equals(name)) {
          // gotta add 1 because parts are 1 based
          part = i + 1;
          break;
        }
      }
    }
    return part;
  }

  /**
   * Find the part with the particular name.
   *
   * @param name name of part to find
   * @return part or null if not found
   */
  public DMPart getPart(String name) {
    if ((parts != null) && !parts.isEmpty()) {
      for (DMPart part : parts) {
        String partName = part.kprtnm;
        if (partName.equals(name)) {
          return part;
        }
      }
    }
    return null;
  }

  /**
   * Get the pointer to the data.  Taken from DM_RDTR
   *
   * @param irow     row number
   * @param icol     column number
   * @param partName name of the part
   * @return word (1 based) of start of data.
   */
  public int getDataPointer(int irow, int icol, String partName) {
    int ipoint = -1;
    if ((irow < 1) || (irow > dmLabel.krow) || (icol < 1)
            || (icol > dmLabel.kcol)) {
      System.out.println("bad row or column number: " + irow + "/"
              + icol);
      return ipoint;
    }
    int iprt = getPartNumber(partName);
    if (iprt == 0) {
      System.out.println("couldn't find part");
      return ipoint;
    }
    // gotta subtract 1 because parts are 1 but List is 0 based
    DMPart part = parts.get(iprt - 1);
    // check for valid data type
    if ((part.ktyprt != MDREAL) && (part.ktyprt != MDGRID)
            && (part.ktyprt != MDRPCK)) {
      System.out.println("Not a valid type");
      return ipoint;
    }
    int ilenhd = part.klnhdr;
    ipoint = dmLabel.kpdata + (irow - 1) * dmLabel.kcol * dmLabel.kprt
            + (icol - 1) * dmLabel.kprt + (iprt - 1);
    return ipoint;
  }

  /**
   * Read an integer
   *
   * @param word word in file (1 based) to read
   * @return int read
   * @throws IOException problem reading file
   */
  public int DM_RINT(int word) throws IOException {
    if (rf == null) {
      throw new IOException("DM_RINT: no file to read from");
    }
    if (dmLabel == null) {
      throw new IOException("DM_RINT: reader not initialized");
    }
    rf.seek(getOffset(word));
    // set the order
    if (needToSwap) {
      //if ((dmLabel.kmachn != MTMACH) &&
      //   ((dmLabel.kvmst && ! mvmst) ||
      //   (mvmst && !dmLabel.kvmst))) {
      rf.order(RandomAccessFile.LITTLE_ENDIAN);  // swap
    } else {
      rf.order(RandomAccessFile.BIG_ENDIAN);
    }
    int idata = rf.readInt();
    if (IMISSD != dmLabel.kmissd) {
      if (idata == dmLabel.kmissd) {
        idata = IMISSD;
      }
    }
    rf.order(RandomAccessFile.BIG_ENDIAN);
    return idata;
  }

  /**
   * Convenience method to fully read into an array of ints
   *
   * @param word   word in file (1 based) to read
   * @param iarray array to read into
   * @throws IOException problem reading file
   */
  public void DM_RINT(int word, int[] iarray) throws IOException {
    DM_RINT(word, iarray.length, iarray, 0);
  }

  /**
   * Read into an array of ints.
   *
   * @param word   word in file (1 based) to read
   * @param iarray array to read into
   * @param start  starting word in the array (0 based)
   * @param num    number of words to read
   * @throws IOException problem reading file
   */
  public void DM_RINT(int word, int num, int[] iarray, int start)
          throws IOException {

    for (int i = 0; i < num; i++) {
      if (start + i > iarray.length) {
        throw new IOException(
                "DM_RINT: start+num exceeds iarray length");
      }
      iarray[start + i] = DM_RINT(word + i);
    }

  }

  /**
   * Read a float
   *
   * @param word word in file (1 based) to read
   * @return float read
   * @throws IOException problem reading file
   */
  public float DM_RFLT(int word) throws IOException {
    if (rf == null) {
      throw new IOException("DM_RFLT: no file to read from");
    }
    if (dmLabel == null) {
      throw new IOException("DM_RFLT: reader not initialized");
    }
    rf.seek(getOffset(word));
    if (needToSwap) {
      // set the order
      //if ((dmLabel.kmachn != MTMACH) &&
      //   ((dmLabel.kvmst && ! mvmst) ||
      //   (mvmst && !dmLabel.kvmst))) {
      rf.order(RandomAccessFile.LITTLE_ENDIAN);  // swap
    } else {
      rf.order(RandomAccessFile.BIG_ENDIAN);
    }
    float rdata = rf.readFloat();
    if (RMISSD != dmLabel.smissd) {
      if (Math.abs(rdata - dmLabel.smissd) < RDIFFD) {
        rdata = RMISSD;
      }
    }
    // reset to read normally
    rf.order(RandomAccessFile.BIG_ENDIAN);
    return rdata;
  }

  /**
   * Convenience method to fully read into an array of floats
   *
   * @param word   word in file (1 based) to read
   * @param rarray array to read into
   * @throws IOException problem reading file
   */
  public void DM_RFLT(int word, float[] rarray) throws IOException {
    DM_RFLT(word, rarray.length, rarray, 0);
  }

  /**
   * Read into an array of ints.
   *
   * @param word   word in file (1 based) to read
   * @param num    number of words to read
   * @param rarray array to read into
   * @param start  starting word in the array (0 based)
   * @throws IOException problem reading file
   */
  public void DM_RFLT(int word, int num, float[] rarray, int start)
          throws IOException {

    for (int i = 0; i < num; i++) {
      if (start + i > rarray.length) {
        throw new IOException(
                "DM_RFLT: start+num exceeds rarray length");
      }
      rarray[start + i] = DM_RFLT(word + i);
    }

  }

  /**
   * Read a 4-byte String
   *
   * @param isword offset in file (1 based FORTRAN word)
   * @return String read
   * @throws IOException problem reading file
   */
  public String DM_RSTR(int isword) throws IOException {
    return DM_RSTR(isword, 4);
  }

  /**
   * Read a String
   *
   * @param isword offset in file (1 based FORTRAN word)
   * @param nchar  number of characters to read
   * @return String read
   * @throws IOException problem reading file
   */
  public String DM_RSTR(int isword, int nchar) throws IOException {
    if (rf == null) {
      throw new IOException("DM_RSTR: no file to read from");
    }
    rf.seek(getOffset(isword));
    return rf.readString(nchar);
  }

  /**
   * Read the data
   *
   * @param irow     row to read
   * @param icol     column to read
   * @param partName part name
   * @return the header and data array  or null;
   * @throws IOException problem reading file
   */
  public RData DM_RDTR(int irow, int icol, String partName)
          throws IOException {
    return DM_RDTR(irow, icol, partName, 1);
  }

  /**
   * Read the real (float) data
   *
   * @param irow         row to read
   * @param icol         column to read
   * @param partName     part name
   * @param decimalScale scaling factor (power of 10);
   * @return the header and data array  or null;
   * @throws IOException problem reading file
   */
  public RData DM_RDTR(int irow, int icol, String partName,
                       int decimalScale)
          throws IOException {

    int ipoint = -1;
    if ((irow < 1) || (irow > dmLabel.krow) || (icol < 1)
            || (icol > dmLabel.kcol)) {
      System.out.println("bad row/column number " + irow + "/" + icol);
      return null;
    }
    //System.out.println("reading row " + irow + ", column " + icol);
    int iprt = getPartNumber(partName);
    if (iprt == 0) {
      System.out.println("couldn't find part: " + partName);
      return null;
    }
    // gotta subtract 1 because parts are 1 but List is 0 based
    DMPart part = parts.get(iprt - 1);
    // check for valid real data type
    if ((part.ktyprt != MDREAL) && (part.ktyprt != MDGRID)
            && (part.ktyprt != MDRPCK)) {
      System.out.println("Not a valid type");
      return null;
    }
    int ilenhd = part.klnhdr;
    ipoint = dmLabel.kpdata + (irow - 1) * dmLabel.kcol * dmLabel.kprt
            + (icol - 1) * dmLabel.kprt + (iprt - 1);

    float[] rdata;
    int[] header = null;
    int istart = DM_RINT(ipoint);
    if (istart == 0) {
      return null;
    }
    // start catching up here because some files are incorrectly written
    try {
      int length = DM_RINT(istart);
      int isword = istart + 1;
      if (length <= ilenhd) {
        //System.out.println("length (" + length
        //                   + ") is less than header length (" + ilenhd
        //                   + ")");
        return null;
      } else if (Math.abs(length) > 10000000) {
        //System.out.println("length is huge");
        return null;
      }
      header = new int[ilenhd];
      DM_RINT(isword, header);
      int nword = length - ilenhd;
      isword += header.length;
      if (part.ktyprt == MDREAL) {
        rdata = new float[nword];
        DM_RFLT(isword, rdata);
      } else if (part.ktyprt == MDGRID) {
        rdata = DM_RPKG(isword, nword, decimalScale);
      } else {  //  packed ints
        int[] idata = new int[nword];
        DM_RINT(isword, idata);
        rdata = DM_UNPK(part, idata);
      }
    } catch (EOFException eof) {
      //System.err.println("reading off end of file");
      rdata = null;
    }
    RData rd = null;
    if (rdata != null) {
      rd = new RData(header, rdata);
    }

    return rd;

  }

  /**
   * Unpack an array of packed integers.
   *
   * @param part   the part with packing info
   * @param ibitst packed integer bit string
   * @return unpacked ints as floats
   */
  public float[] DM_UNPK(DMPart part, int[] ibitst) {
    int nparms = part.kparms;
    int nwordp = part.kwordp;
    int npack = (ibitst.length - 1) / nwordp + 1;
    if (npack * nwordp != ibitst.length) {
      //logError("number of packed records not correct");
      // System.out.println("number of packed records not correct: "
      //                   + npack * nwordp + " vs. " + ibitst.length);
      return null;
    }
    float[] data = new float[nparms * npack];
    PackingInfo pkinf = part.packInfo;
    int ir = 0;
    int ii = 0;
    for (int pack = 0; pack < npack; pack++) {
      //
      //  Move bitstring into internal words.  TODO: necessary?
      //
      int[] jdata = new int[nwordp];
      System.arraycopy(ibitst, ii, jdata, 0, nwordp);

      //
      //  Extract each data value.
      //
      for (int idata = 0; idata < nparms; idata++) {

        //
        //  Extract correct bits from words using shift and mask
        //  operations.
        //
        int jbit = pkinf.nbitsc[idata];
        int jsbit = pkinf.isbitc[idata];
        int jshift = 1 - jsbit;
        int jsword = pkinf.iswrdc[idata];
        int jword = jdata[jsword];
        // use >>> to shift avoid carrying sign along
        int mask = mskpat >>> (32 - jbit);
        int ifield = jword >>> Math.abs(jshift);
        ifield = ifield & mask;
        if ((jsbit + jbit - 1) > 32) {
          jword = jdata[jsword + 1];
          jshift = jshift + 32;
          int iword = jword << jshift;
          iword = iword & mask;
          ifield = ifield | iword;
        }
        //
        //  The integer data is now in ifield.  Use the scaling and
        //  offset terms to convert to REAL data.
        //
        if (ifield == pkinf.imissc[idata]) {
          data[ir + idata] = RMISSD;
        } else {
          data[ir + idata] = (ifield + pkinf.koffst[idata])
                  * (float) pkinf.scalec[idata];
        }
      }
      ir += nparms;
      ii += nwordp;
    }
    return data;

  }

  /**
   * Get a bit string for an integer
   *
   * @param b the integer
   * @return a bit string (e.g.: 01100001|11000000|10011010|10110100|)
   */
  protected static String getBits(int b) {
    Formatter s = new Formatter();
    for (int i = 31; i >= 0; i--) {
      if ((b & (1 << i)) != 0) {
        s.format("1");
      } else {
        s.format("0");
      }
      if (i % 8 == 0) {
        s.format("|");
      }
    }
    return s.toString();
  }

  /**
   * subclass should implement
   *
   * @param isword       starting word (1 based)
   * @param nword        number of words to read
   * @param decimalScale decimal scale
   * @return returns null unless subclass overrides
   * @throws IOException problem reading data
   */
  public float[] DM_RPKG(int isword, int nword, int decimalScale)
          throws IOException {
    return null;
  }

  /**
   * A class to hold real (float) data
   *
   * @author Unidata Development Team
   */
  static public class RData {

    /**
     * the header
     */
    public int[] header;

    /**
     * the data
     */
    public float[] data;

    /**
     * Create a new holder for the header and data
     *
     * @param header the header
     * @param data   the data
     */
    public RData(int[] header, float[] data) {
      this.header = header;
      this.data = data;
    }
  }

}


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

// $Id: $

package ucar.nc2.iosp.netcdf3;

import ucar.ma2.*;
import ucar.nc2.*;

import java.io.*;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.Channels;
import java.nio.ByteBuffer;

/**
 *  Experimental
 * @author john
 */
public class N3channelWriter extends N3streamWriter {
  static private int buffer_size = 1000 * 1000;
  static private boolean debugWrite = false;

  ////////////////////////////////////////////////////////////////////////////////////////////////////////
  //private ucar.nc2.NetcdfFile ncfile;
  //private List<Vinfo> vinfoList = new ArrayList<Vinfo>(); // output order of the variables
  //private boolean debugPos = false, debugWriteData = false;
  //private int dataStart, dataOffset = 0;

  public N3channelWriter(ucar.nc2.NetcdfFile ncfile) {
    super(ncfile);
  }

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

  public void writeDataAll(WritableByteChannel channel) throws IOException, InvalidRangeException {
    for (Vinfo vinfo : vinfoList) {
      if (!vinfo.isRecord) {
        Variable v = vinfo.v;
        // assert filePos == vinfo.offset;
        //if (debugPos) System.out.println(" writing at "+filePos+" should be "+vinfo.offset+" "+v.getFullName());
        int nbytes = (int) v.readToByteChannel(v.getShapeAsSection(), channel);
        filePos += nbytes;
        filePos += pad(channel, nbytes);
        if (debugPos) System.out.printf(" read=%d vinfo=%s%n", nbytes, vinfo);
      }
    }

    // must use record dimension if it exists+
    boolean useRecordDimension = ncfile.hasUnlimitedDimension();
    if (useRecordDimension) {
      ncfile.sendIospMessage(NetcdfFile.IOSP_MESSAGE_ADD_RECORD_STRUCTURE);

      Structure recordVar = (Structure) ncfile.findVariable("record");
      Section section = new Section().appendRange(null);

      long bytesDone = 0;
      long done = 0;
      long nrecs = (int) recordVar.getSize();
      int structureSize = recordVar.getElementSize();
      int readAtaTime = Math.max( 10, buffer_size / structureSize);

      for (int count = 0; count < nrecs; count+=readAtaTime) {
        long last = Math.min(nrecs, done+readAtaTime); // dont go over nrecs
        int need = (int) (last - done); // how many to read this time

        section.setRange(0, new Range(count, count+need-1));
        try {
          bytesDone += recordVar.readToByteChannel( section, channel);
          done += need;
        } catch (InvalidRangeException e) {
          e.printStackTrace();
          break;
        }
      }
      assert done == nrecs;
      bytesDone /= 1000 * 1000;
      if (debugWrite) System.out.println("write record var; total = " + bytesDone + " Mbytes # recs=" + done);

      // remove the record structure this is rather fishy, perhaps better to leave it
      ncfile.sendIospMessage(NetcdfFile.IOSP_MESSAGE_REMOVE_RECORD_STRUCTURE);
      ncfile.finish();
    }

  }

  // pad to a 4 byte boundary
  private ByteBuffer padddingBB;
  private int pad(WritableByteChannel channel, int nbytes) throws IOException {
    int pad = N3header.padding(nbytes);
    if ((null != channel) && (pad > 0)) {
      if (padddingBB == null) padddingBB = ByteBuffer.allocate(4); // just 4 zero bytes
      padddingBB.position(0);
      padddingBB.limit(pad);
      channel.write( padddingBB);
    }
    return pad;
  }

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

  public static void writeFromFile(NetcdfFile fileIn, String fileOutName) throws IOException, InvalidRangeException {

    try (FileOutputStream stream = new FileOutputStream(fileOutName)) {
      WritableByteChannel channel = stream.getChannel();
      DataOutputStream dout = new DataOutputStream(Channels.newOutputStream(channel));

      N3channelWriter writer = new N3channelWriter(fileIn);
      int numrec = fileIn.getUnlimitedDimension() == null ? 0 : fileIn.getUnlimitedDimension().getLength();

      writer.writeHeader(dout, numrec);
      dout.flush();

      writer.writeDataAll(channel);
    }
  }

  /**
   * Write ncfile to a WritableByteChannel.
   *
   * @param ncfile the file to write
   * @param wbc write to this WritableByteChannel.
   *   If its a Socket, must have been opened through a call to java.nio.channels.SocketChannel.open()
   * @throws IOException on IO error
   * @throws InvalidRangeException range error
   */
  public static void writeToChannel(NetcdfFile ncfile, WritableByteChannel wbc) throws IOException, InvalidRangeException {
    DataOutputStream stream = new DataOutputStream(new BufferedOutputStream( Channels.newOutputStream(wbc), 8000));
    //DataOutputStream stream = new DataOutputStream(Channels.newOutputStream(wbc));  // buffering seems to improve by 5%
    N3channelWriter writer = new N3channelWriter(ncfile);
    int numrec = ncfile.getUnlimitedDimension() == null ? 0 : ncfile.getUnlimitedDimension().getLength();
    writer.writeHeader(stream, numrec);
    stream.flush();
    writer.writeDataAll(wbc);
  }

}


/////////////////////////////////////////////////////////////////////////////
// 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.dap.parsers;

import java.io.*;
import java.util.List;
import java.util.Iterator;

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

import opendap.dap.*;
import opendap.util.Debug;

/**
 * DDSXMLParser is used to parse a DDX (the XML formatted persistent
 * representation of a DDS) into a DDS object in memory. This parser uses the
 * Xerces XML parser and JDOM packages to perform it's duties.
 *
 * @author ndp
 * @version $Revision: 15901 $
 * @opendap.ddx.experimental WARNING! This class supports a pre-released version of the DDX.
 * The XML schema that this class is designed to work with WILL BE CHANGED prior
 * to software release.
 */
public class DDSXMLParser {

    // Debugging output control
    private boolean _Debug = false;

    // depth of recursion in the parser.
    // This used only for Debugging purposes.
    private int parseLevel;

    // The DDS that the parser is building.
    private DDS dds = null;

    // The BaseTypeFactory the parser uses
    // to create each variable in the DDS.
    private BaseTypeFactory factory = null;

    // The namespace through which the XML document is validated.
    // This gets set up in the constructor for this class.
    private Namespace opendapNameSpace;

    // The parent DConstructor of the current variable.
    // This is state information used by the parser.
    // Variables in the DDS are always added to a
    // a container of this type (DDS is an instance
    // of a DConstructor)
    private DConstructor parentDC;

    // The BaseType thatis currently being parsed.
    // This is state information used by the parser.
    private BaseType currentBT = null;

    // The Attribute Table (container) to which
    // Attributes currently being parsed should be added.
    // This is state information used by the parser.
    private AttributeTable currentAT = null;

    // The last DOM Document that we used to construct a DDS;
    private Document lastDoc;

    /**
     * Constructs a new DDSXMLParser.
     * The OPeNDAP namespace is defined during the construction
     * of an instance of this class.
     */
    public DDSXMLParser(String nameSpace) {
        super();
        opendapNameSpace = Namespace.getNamespace(nameSpace);
        lastDoc = null;
        _Debug = Debug.isSet("DDSXMLParser");
    }

    /**
     * Returns the Document object created by the most
     * recent invocation of the parser. This might be usefull to some XML
     * hacker somewhere. :)
     *
     * @return The Document object created by the most
     *         recent invocation of the parser.
     */
    public Document getLastDomDoc() {
        return (lastDoc);
    }

    /**
     * Parse the DDX waiting in the InputStream and instantiate all of
     * the member BaseType variables and their associated Attributes
     *  into a DDS using the passed BaseTypeFactory
     *
     * @param ddx        A JDOM Document containing the DDX to parse.
     * @param targetDDS  The DDS in which to place all of the BaseType
     *                    variables and their associated Attributes.
     * @param fac        The BaseTypeFactory to use when creating new
     *                   BaseType variables.
     * @param validation Is a boolean indicating whether or not the parser should
     *                   validate the XML document using the Schema (typically referenced in the
     *                   document itself). In general server side applications should always vaidate,
     *                   while clients shouldn't bother (since they are ostensibly receiving the
     *                   document from a server that has already done so.)
     * @see DDS
     * @see BaseTypeFactory
     */
    public void parse(Document ddx, DDS targetDDS, BaseTypeFactory fac, boolean validation) throws DAP2Exception {

        dds = targetDDS;
        factory = fac;

        // Build up the OPeNDAP data objects rpresented by the XML document.
        // Additional validation will take place  during this process.

        Element root = ddx.getRootElement();
        lastDoc = ddx;

        // This is just a little tracker to help with debugging.
        parseLevel = 0;

        // Make sure the root element is in fact a Dataset.
        // Trying to enforce this in the schema would create a
        // contrived and difficult to interpret schema design.
        String type = root.getName();
        if (!(type.equals("Dataset"))) {
            throw new NoSuchTypeException("Root Element MUST be <Dataset>. Found: " + type);
        }

        String name = root.getAttribute("name").getValue();
        //System.out.println("DDS should be named: "+name);
        dds.setClearName(name);
        parentDC = dds;
        currentBT = dds;

        // Parse any Attributes (or AttributeTables/containers) at the
        // top level in the Dataset.
        parseAttributes(root, "-- ");

        // Parse any Aliases at the
        // top level in the Dataset.
        parseAliases(root, "++ ");

        // Parse all of the child elements (which would be OPeNDAP
        // BaseType variables) in the Dataset.
        Iterator ci = root.getChildren().iterator();
        while (ci.hasNext()) {
            Element child = (Element) ci.next();
            parseBase(child, "    ");

        }

//	catch(Exception e) {
//	    throw new DAP2Exception("PARSER ERROR! \n"+
//	                            e.getClass().getName()+ ": "+e.getMessage());
//	}

    }

    /**
     * Parse the DDX waiting in the InputStream and instantiate all of
     * the member BaseType variables and their associated Attributes
     *  into a DDS using the passed BaseTypeFactory
     *
     * @param is         The InputStream containing the DDX to parse.
     * @param targetDDS  The DDS in which to place all of the BaseType
     *                    variables and their associated Attributes.
     * @param fac        The BaseTypeFactory to use when creating new
     *                   BaseType variables.
     * @param validation Is a boolean indicating whether or not the parser should
     *                   validate the XML document using the Schema (typically referenced in the
     *                   document itself). In general server side applications should always vaidate,
     *                   while clients shouldn't bother (since they are ostensibly receiving the
     *                   document from a server that has already done so.)
     * @see DDS
     * @see BaseTypeFactory
     */
    public void parse(InputStream is, DDS targetDDS, BaseTypeFactory fac, boolean validation) throws DAP2Exception {

        try {

            // get a jdom parser to parse and validate the XML document.
            SAXBuilder parser = new SAXBuilder();
            // optionally turn on validation
            parser.setFeature("http://apache.org/xml/features/validation/schema", validation);

            // parse the document into a hierarchical document
            Document doc = parser.build(is);

            if (_Debug) System.out.println("Document is " +
                    (validation ? "valid and " : "") +
                    "well-formed.\nContent: " + doc);

            parse(doc, targetDDS, fac, validation);

        } catch (JDOMException jde) {
            throw new DAP2Exception(jde);
        }
        catch (IOException ioe) {
            throw new DAP2Exception(ioe);
        }

    }

    /**
     * This method recursively travels through the DOM tree, locating
     * BaseType derived nodes and placing them in the DDS. The structure
     * of the BaseType derived elements in the XML instance document
     * is captured in the DOM object that is being parsed. This structure
     * again reflected in the resulting DDS.
     */
    private void parseBase(Element e, String indent)
            throws DASException, NoSuchTypeException, BadSemanticsException {

        parseLevel++;

        String type = e.getName();

        if (type.equals("Attribute")) {
            // Do nothing here, the Attributes get parsed when the BaseType's
            // get built. This conditional basically serves as a "trap" to
            // ignore the <Attribute> tag.
        } else if (type.equals("Alias")) {
            // Do nothing here, the Aliases get parsed when the BaseType's
            // get built. This conditional basically serves as a "trap" to
            // ignore the <Alias> tag.

        } else if (type.equals("dataBLOB")) {

            // dataBLOB?
            // The schema says that the href attribute is
            // required for the dataBLOB element.
            org.jdom2.Attribute hrefAttr = e.getAttribute("href");

            // Since it's required we know that the getAttribute()
            // method is not going to return null.
            String contentID = hrefAttr.getValue();

            if (_Debug) System.out.println("Found dataBLOB element. contentID=\"" + contentID + "\"");

            dds.setBlobContentID(contentID);

        } else {

            // What's left must be a OPeNDAP BaseType

            if (_Debug) System.out.println("Parsing new BaseType element. Parse level: " + parseLevel);
            if (_Debug) showXMLElement(e, indent);

            // Go get a new BaseType formed from this element
            BaseType bt = newBaseType(e);

            // Set it's parent.
            // bt.setParent(parentDC);

            // Add it to it's parent (container)
            parentDC.addVariable(bt);

            // Now we need to make sure this particular BaseType
            // derived element isn't some special type that needs
            // additional parsing:

            // Is it a container?
            if (bt instanceof DConstructor) {
                // Up date the parsers state, (cache my parent)
                DConstructor myParentDC = parentDC;
                parentDC = (DConstructor) bt;
                try {
                // Grids are special containers, handle them
                if (bt instanceof DGrid) {
                    parseGrid(e, indent);
                } else {
                    // Otherwise, recurse on the children
                  for (Element child : e.getChildren()) {
                    parseBase(child, indent + "    ");
                  }
                }
                } finally {
                // restore my parent
                parentDC = myParentDC;
                }
            } else if (bt instanceof DArray) {
                // Array's are special, better build it if it is one

                if (_Debug) System.out.println("Parsing Array instance.  Array name: '" + bt.getClearName() + "'");

                parseArray(e, (DArray) bt, indent);

            }

        }

        parseLevel--;

    }

    /**
     * Arrays have special parsing need as their syntax is different from
     * that of a typical BaseType derived type or a container type. The
     * array is based on a template variable that can have ANY structure
     * that can be represented by the OPeNDAP data model. With the exception
     * of (you knew this was comming, right?) of other arrays. IE You can't
     * have Arrays of Arrays. This caveat is enforced by the XML schema.
     */
    private void parseArray(Element ArrayElement, DArray da, String indent)
            throws DASException, NoSuchTypeException, BadSemanticsException {

        int countTemplateVars = 0;
        int numDims = 0;

      for (Element e : ArrayElement.getChildren()) {

        if (_Debug) System.out.println(indent + "Working on Array element: " + e.getName());

        // Is this element an Attribute of the Array?
        if (e.getName().equals("Attribute")) {
          //Then ignore it!
        }
        // Is this element an Attribute of the Alias?
        else if (e.getName().equals("Alias")) {
          //Then ignore it!
        }
        // Is this element an array dimension?
        else if (e.getName().equals("dimension")) {

          // Then count it,
          numDims++;

          // And now let's add it to the array...

          // Array dimension are not required to have names, so
          // the schema does not enforce the use of the name attribute.

          // try to get the dimension's name, and use it id it's there.
          String name = null;
          Attribute nameAttr = e.getAttribute("name");

          if (nameAttr != null)
            name = nameAttr.getValue();

          // The presence of the 'size' attribute is enforeced by the schema.
          // get it, parse it, use it.
          int size = Integer.parseInt(e.getAttribute("size").getValue());

          // add the dimension to the array.
          da.appendDim(size, (name));
        } else { // otherwise, it must be THE template element.

          // Just to make sure the schema validation didn't fail (because
          // I am basically paranoid about software) count the number of
          // template candidates we find and throw an Exception later
          // if there was more than one.
          countTemplateVars++;

          // The template element is just another BaseType
          // derived element. So, let's go build it!
          BaseType template = buildArrayTemplate(e, indent);

          // Oddly, in the OPeNDAP implementation of Array, the Array variable
          // takes it's name from it's (internal) template variable. This
          // is probably an artifact of the original DDSParser.

          // So, set the name of the template variable to the name of the Array.
          template.setClearName(da.getClearName());

          // Add the template variable to the Array
          da.addVariable(template);

        }
      }

        if (_Debug) {
            System.out.println(indent + "Built Array: ");
            da.printDecl(System.out, indent);
            System.out.println(indent + "dimensions: " + numDims + "  templates: " + countTemplateVars);
        }

        if (countTemplateVars != 1) {
            throw new NoSuchTypeException("ONLY ONE (1) TEMPLATE VARIABLE ALLOWED PER ARRAY!");
        }

    }

    /**
     * Builds the template variable for an array. The logic here has a lot
     * in common with the logic in parseBase. I considered trying to
     * refactor the code so that the two methods could utilize the same logic,
     * but I bagged it. Cie' la vie.
     *

     * Arrays of arrays are not allowed this rule should is enforced
     * through the schema validation process.
     */
    private BaseType buildArrayTemplate(Element template, String indent)
            throws DASException, NoSuchTypeException, BadSemanticsException {

        BaseType bt = null;

        if (_Debug) showXMLElement(template, indent + "...:");

        // Get all of the Attribute elements (tagged <Attribute>)
        Iterator attrElements = template.getChildren("Attribute", opendapNameSpace).iterator();

        if (attrElements.hasNext())
            throw new BadSemanticsException("Array Template Variables MAY NOT have Attributes");

        // Build the appropriate BaseType from the tag.
        bt = newBaseType(template);

        if (_Debug) System.out.println("Got template: " + bt.getTypeName() + "   " + bt.getClearName());

        // Now we need to make sure this particular BaseType
        // derived element isn't some special type that needs
        // additional parsing:

        // Is it a container?
        if (bt instanceof DConstructor) {

            // Up date the parsers state, (cache my parent)
            DConstructor myParentDC = parentDC;
            parentDC = (DConstructor) bt;
            try {
            // Grids are special containers, handle them
            if (bt instanceof DGrid) {
                parseGrid(template, indent);
            } else {
                // Otherwise, recurse on the children
              for (Element child : template.getChildren()) {
                parseBase(child, indent + "    ");
              }
            }
            } finally {
            // restore my parent
            parentDC = myParentDC;
            }

        }

        return (bt);
    }

    /**
     * Grids are unusual examples of DConstructor and require special
     * handling when parsing.
     */
    private void parseGrid(Element gridElement, String indent)
            throws DASException, NoSuchTypeException, BadSemanticsException {

        parseLevel++;

        // Grab the parent object (which better be a Grid!)
        // just to elminate the hassle of casting everytime...
        DGrid myGrid = (DGrid) parentDC;

        if (_Debug) {
            System.out.println("Parsing Grid Element: " + gridElement);
            System.out.println("Grid Elements: ");
            //showXMLElement(gridElement, indent);
          for (Element element : gridElement.getChildren()) System.out.println(element);
        }

        // Get and parse the grid's Array element.
        String eName = "Array";
        if (_Debug) {
            System.out.println("Parsing Array element.");
            System.out.println("Asking for element: '" + eName + "' in namespace: '" + opendapNameSpace + "'");
        }
        Element arrayElement = gridElement.getChild(eName, opendapNameSpace);
        if (_Debug) System.out.println("Got Array element: " + arrayElement);
        DArray gridArray = (DArray) newBaseType(arrayElement);
        parseArray(arrayElement, gridArray, indent + "    ");

        // Add it to the Grid
        myGrid.addVariable(gridArray, DGrid.ARRAY);

        // Get the Map elements
        eName = "Map";
        if (_Debug) {
            System.out.println("Parsing Map elements.");
            System.out.println("Asking for element: '" + eName + "' in namespace: '" + opendapNameSpace + "'");
        }
        List<Element> mapElements = gridElement.getChildren("Map", opendapNameSpace);

        // Make sure the number of Map elements matches the dimension of the Grid Array.
        if (mapElements.size() != gridArray.numDimensions())
            throw new BadSemanticsException("Error in Grid syntax: " +
                    "The number of Map arrays must " +
                    "equal the number of dimensions " +
                    "of the data array.");

        // Parse each Map element and poke it into the Grid.
      for (Element mapElement : mapElements) {
        DArray thisMap = (DArray) newBaseType(mapElement);
        parseArray(mapElement, thisMap, indent + "    ");

        if (thisMap.numDimensions() != 1)
          throw new BadSemanticsException("Error in Grid syntax: " +
                  "Maps may have only one dimension.");

        myGrid.addVariable(thisMap, DGrid.MAPS);

      }

        parseLevel--;

    }

    /**
     * A convienience function used for displaying information in _Debug mode.
     * Prints an XML element's name, content (if any) and Attributes to
     * System.out
     */
    private void showXMLElement(Element e, String indent) {

        System.out.print(parseLevel + indent + "Element: " + e.getName() + "  ");
        String text = e.getTextNormalize();
        if (!text.equals(""))
            System.out.print(" = " + text + "   ");

        //System.out.println("");

      for (Attribute att : e.getAttributes()) {
        //System.out.print(parseLevel + indent + "    ");
        System.out.print(att.getName() + ": " + att.getValue() + "  ");
      }
        System.out.println("");

      for (Element kid : e.getChildren()) {
        showXMLElement(kid, indent + "    ");
      }

    }

    /*
    * Builds a new BaseType derived type from the passed XML element.
    *
This happens in 4 steps:
    *

    *
 1) Determine the OPeNDAP type and vairiable name

    *
 2) Get an new one of the thing in (1)

    *
 3) Parse any Attribute tags associated with this OPeNDAP type.
    *         (They appear as children of the XML element)

    *
 4) Parse any Alias tags associated with this OPeNDAP type.
    *         (They appear as children of the XML element)

    *

    *
    */
    private BaseType newBaseType(Element e) throws DASException, NoSuchTypeException {

        if (_Debug) System.out.println("Getting new BaseType() from: " + e);

        // What's the Element Name? This IS the OPeNDAP typename.
        String type = e.getName();

        // What is the name of this variable? Since BaseType derived types
        // are not required to have names we have to do this carefully.
        String name = null;
        org.jdom2.Attribute nameAttr = e.getAttribute("name");

        if (nameAttr != null)
            name = nameAttr.getValue();

        if (_Debug) System.out.println("    type: " + type + "   name: '" + name + "'");

        // GO get a fresh new OPeNDAP variable (BaseType derived type)
        currentBT = newBaseTypeFactory(type, name);

        //Parse any Attribute tagged child elements for this variable.
        parseAttributes(e, "--- ");

        //Parse any Alias tagged child elements for this variable.
        parseAliases(e, "+++ ");

        return (currentBT);
    }

    /**
     * The name of this method might be a bit misleading. This method is basically
     * a wrapper for the BaseTypeFactory associated with the DDS that we are building.
     *

     * I think it should really be a method of BaseTypeFactory.
     *

     * Something like:
     *

     * BaseTypeFactory.getNewVariable(String typeString, String name)
     *

     * But, well BaseTypeFactory is an interface, so that's a crappy idea. *sigh*
     */
    private BaseType newBaseTypeFactory(String typeString, String name) throws NoSuchTypeException {

        BaseType bt;

        if (typeString.equals("Array") || typeString.equals("Map")) {

            bt = factory.newDArray();
            bt.setClearName(name);

        } else if (typeString.equals("Grid")) {

            bt = factory.newDGrid();
            bt.setClearName(name);

        } else if (typeString.equals("Structure")) {

            bt = factory.newDStructure();
            bt.setClearName(name);

        } else if (typeString.equals("Sequence")) {

            bt = factory.newDSequence();
            bt.setClearName(name);

        } else if (typeString.equals("Int16")) {

            bt = factory.newDInt16();
            bt.setClearName(name);

        } else if (typeString.equals("UInt16")) {

            bt = factory.newDUInt16();
            bt.setClearName(name);

        } else if (typeString.equals("Int32")) {

            bt = factory.newDInt32();
            bt.setClearName(name);

        } else if (typeString.equals("UInt32")) {

            bt = factory.newDUInt32();
            bt.setClearName(name);

        } else if (typeString.equals("Float32")) {

            bt = factory.newDFloat32();
            bt.setClearName(name);

        } else if (typeString.equals("Float64")) {

            bt = factory.newDFloat64();
            bt.setClearName(name);

        } else if (typeString.equals("Byte")) {

            bt = factory.newDByte();
            bt.setClearName(name);

        } else if (typeString.equals("String")) {

            bt = factory.newDString();
            bt.setClearName(name);

        } else if (typeString.equals("Url")) {

            bt = factory.newDURL();
            bt.setClearName(name);

        } else
            throw new NoSuchTypeException("Unknown Type: " + typeString);

        return bt;

    }

    /**
     * Parse the Attribute tags for a given variable element in the XML document.
     * Build the appropriate Attributes and AttributeTables and add them to the
     * the current variable's (currentBT's) AttributeTable.
     */

    private void parseAttributes(Element e, String indent)
            throws DASException, NoSuchTypeException {

        parseLevel++;

        String subIndent = indent + "    ";
        if (_Debug) System.out.println(indent + "Parsing Attributes: ");

        if (_Debug)
            System.out.println(subIndent + "currentBT: " + currentBT.getTypeName() + " " + currentBT.getClearName());

        // Get all of the Attribute elements (tagged <Attribute>)
      for (Element attrElement : e.getChildren("Attribute", opendapNameSpace)) {

        String name = null;
        Attribute nameAttr = attrElement.getAttribute("name");
        // no need to check that the getAttribute call worked because the Schema enforces
        // the presence of the "name" attribute for the <Attribute> tag in the OPeNDAP namespace
        name = nameAttr.getValue();

        String typeName = null;
        Attribute typeAttr = attrElement.getAttribute("type");
        // no need to check that the getAttribute call worked because the Schema enforces
        // the presence of the "type" attribute for the <Attribute> tag in the OPeNDAP namespace
        typeName = typeAttr.getValue();

        // Is this Attribute a container??
        if (typeName.equals("Container")) {

          // Make sure that the document is valid for Attribute Containers and Values
          Iterator valueChildren = attrElement.getChildren("value", opendapNameSpace).iterator();
          if (valueChildren.hasNext())
            throw new AttributeBadValueException(
                    "Container Attributes may " +
                            "contain only other Attributes.\n" +
                            "Container Attributes may NOT " +
                            "contain values.");

          // Cache the currentAT (AttributeTable), this might be a null
          // in which case the the container should be added to the currentBT.
          AttributeTable cacheAttributeTable = currentAT;

          if (_Debug)
            System.out.println(indent + "currentBT: " + currentBT.getTypeName() + " " + currentBT.getClearName());
          if (_Debug) System.out.println(indent + "Attribute '" + name + "' is type " + typeName);

          // Add the Attribute container to the appropriate object.
          // If the currentAT is null, this indicates that we are working
          // on the top level attributes for the currentBT, if it's not
          // then we are working on the Attributes for some AttributeTable
          // contained within the top level Attributes in the currentBT.
          // Set the currentAT to the newly built (and returned) AttributeTable
          if (currentAT == null)
            currentAT = currentBT.appendAttributeContainer(name);
          else
            currentAT = currentAT.appendContainer(name);

          // Go parse the child Attributes of this Attribute table.
          // Note that this is a recursive call.
          parseAttributes(attrElement, indent + "    ");

          // Now parse all of the Aliases that exist in this Attribute table.
          parseAliases(attrElement, "+++ ");

          // restore the currentAT from the cached one, thus regaining the
          // the state that we entered this method with.
          currentAT = cacheAttributeTable;

        } else {

          // Make sure that the document is valid for Attribute Containers and Values
          Iterator attrChildren = attrElement.getChildren("Attribute", opendapNameSpace).iterator();
          if (attrChildren.hasNext())
            throw new AttributeBadValueException(
                    "Attributes must be of type Container " +
                            "in order to contain other Attributes.\n" +
                            "Attributes of types other than Container " +
                            "must contain values.");

          // Walk through the <value> elements
          for (Element valueChild : attrElement.getChildren("value", opendapNameSpace)) {
            // Get the content of the value.
            // There are several methods for getting this content in the
            // org.jdom2.Element object. The method getText() makes no effort
            // to "normalize" the white space content. IE tabs, spaces,
            // carriage return, newlines are all preserved. This might not
            // be the right thing to do, but only time will tell.
            String value = valueChild.getText();

            if (_Debug) {
              System.out.println(subIndent + "Attribute '" + name + "' of " + currentBT.getClearName() +
                      " is type " + typeName + " and has value: " + value);
            }

            // get the Attribute value type code.
            int typeVal = opendap.dap.Attribute.getTypeVal(typeName);

            // Add the attribute and it's value to the appropriat
            // container. Note that the interface for appending
            // values to opendap.dap.Attributes is built such that
            // the Attribute must be named each time. If the Attribte
            // name already exists, then the value is added to the list
            // of values for the Attribute. If the Attribute name does not
            // already exist, a new Attribute is made to hold the value.
            if (currentAT == null)
              currentBT.appendAttribute(name, typeVal, value, true);
            else
              currentAT.appendAttribute(name, typeVal, value, true);

          }

        }

      }

        parseLevel--;

    }

    /**
     * Parse all of the Alias tags in this element of the XML document.
     * Add each one to the correct Attribute Table.
     */
    private void parseAliases(Element e, String indent) throws DASException {

        parseLevel++;

        String subIndent = indent + "    ";
        if (_Debug) System.out.println(indent + "Parsing Aliases: ");

        if (_Debug)
            System.out.println(subIndent + "currentBT: " + currentBT.getTypeName() + " " + currentBT.getClearName());

        // Get the Alias elements
      for (Element aliasElement : e.getChildren("Alias", opendapNameSpace)) {
        String name = null;
        Attribute nameAttr = aliasElement.getAttribute("name");
        // no need to check that the getAttribute call worked because the Schema enforces
        // the presence of the "name" attribute for the <Alias> tag in the OPeNDAP namespace
        name = nameAttr.getValue();

        String attributeName = null;
        Attribute attributeAttr = aliasElement.getAttribute("Attribute");
        // no need to check that the getAttribute call worked because the Schema enforces
        // the presence of the "Attribute" attribute for the <Alias> tag in the OPeNDAP namespace
        attributeName = attributeAttr.getValue();

        if (_Debug) {
          System.out.println(subIndent + "The name '" + name +
                  "' is aliased to dds attribute: '" + attributeName + "'");
        }

        // Add the Alias to the appropriate container.
        if (currentAT == null)
          currentBT.addAttributeAlias(name, attributeName);
        else
          currentAT.addAlias(name, attributeName);

      }

        parseLevel--;
    }

    /**
     * This method is used to normalize strings prior
     * to their inclusion in XML documents. XML has certain parsing requirements
     * around reserved characters. These reserved characters must be replaced with
     * symbols recognized by the XML parser as place holder for the actual symbol.
     *

     * The rule for this normalization is as follows:
     *

     *

     *
 The < (less than) character is replaced with &lt;
     *
 The > (greater than) character is replaced with &gt;
     *
 The & (ampersand) character is replaced with &amp;
     *
 The ' (apostrophe) character is replaced with &apos;
     *
 The " (double quote) character is replaced with &quot;
     *

     *
     * @param s The String to be normalized.
     * @return The normalized String.
     */
    public static String normalizeToXML(String s) {

        // Some handy definitons.
        String xmlGT = ">";
        String xmlLT = "<";
        String xmlAmp = "&";
        String xmlApos = "'";
        String xmlQuote = """;

        boolean Debug = false;
        StringBuilder sb = new StringBuilder(s);

        for (int offset = 0; offset < sb.length(); offset++) {

            char c = sb.charAt(offset);

            switch (c) {

                case '>': // GreaterThan
                    sb.replace(offset, offset + 1, xmlGT);
                    break;

                case '<': // Less Than
                    sb.replace(offset, offset + 1, xmlLT);
                    break;

                case '&': // Ampersand
                    sb.replace(offset, offset + 1, xmlAmp);
                    break;

                case '\'': // Single Quote
                    sb.replace(offset, offset + 1, xmlApos);
                    break;

                case '\"': // Double Quote
                    sb.replace(offset, offset + 1, xmlQuote);
                    break;

                default:
                    break;
            }

        }
        //Coverity[DEADCODE]
        if (Debug) System.out.println("String: `" + s + "` normalized to: `" + sb + "`");

        return (sb.toString());

    }

}


package ucar.nc2.ogc.gml;

import net.opengis.gml.x32.FeaturePropertyType;
import net.opengis.waterml.x20.MonitoringPointDocument;
import ucar.nc2.ft.StationTimeSeriesFeature;
import ucar.nc2.ogc.waterml.NcMonitoringPointType;

/**
 * Created by cwardgar on 2014/02/26.
 */
public abstract class NcFeaturePropertyType {
    // wml2:Collection/wml2:observationMember/om:OM_Observation/om:featureOfInterest
    public static FeaturePropertyType initFeatureOfInterest(
            FeaturePropertyType featureOfInterest, StationTimeSeriesFeature stationFeat) {
        // wml2:MonitoringPoint
        MonitoringPointDocument monitoringPointDoc = MonitoringPointDocument.Factory.newInstance();
        NcMonitoringPointType.initMonitoringPointType(monitoringPointDoc.addNewMonitoringPoint(), stationFeat);
        featureOfInterest.set(monitoringPointDoc);

        return featureOfInterest;
    }

    private NcFeaturePropertyType() { }
}