/////////////////////////////////////////////////////////////////////////////
// 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;

import opendap.dap.parsers.DDSXMLParser;
import ucar.nc2.util.EscapeStrings;

import java.io.BufferedWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Vector;

/**
 * An Attribute holds information about a single attribute in an
 * AttributeTable.  It has a type, and contains either a
 * Vector of Strings containing the attribute's
 * values, or a reference to an AttributeTable, if the
 * Attribute is a container.  An Attribute may also
 * be created as an alias pointing to another Attribute of any
 * type, including container.
 *
 * @author jehamby
 * @version $Revision: 22638 $
 * @see AttributeTable
 */
public class Attribute extends DAPNode
{

    private static final boolean _Debug = false;
    private static final boolean DebugValueChecking = false;

    /**
     * Unknown attribute type.  This is currently unused.
     */
    public static final int UNKNOWN = 0;

    /**
     * Alias attribute type.  This is an attribute that works like a
     * UNIX style soft link to another attribute.
     */
    public static final int ALIAS = 1;

    /**
     * Container attribute type.  This Attribute holds an AttributeTable.
     */
    public static final int CONTAINER = 2;

    /**
     * Byte attribute type. Holds an unsigned Byte.
     */
    public static final int BYTE = 3;

    /**
     * Int16 attribute type.  Holds a signed Short.
     */
    public static final int INT16 = 4;

    /**
     * UInt16 attribute type.  Holds an unsigned Short.
     */
    public static final int UINT16 = 5;

    /**
     * Int32 attribute type.  Holds a signed Integer.
     */
    public static final int INT32 = 6;

    /**
     * UInt32 attribute type.  Holds an unsigned Integer.
     */
    public static final int UINT32 = 7;

    /**
     * Float32 attribute type.  Holds a Float.
     */
    public static final int FLOAT32 = 8;

    /**
     * Float64 attribute type.  Holds a Double.
     */
    public static final int FLOAT64 = 9;

    /**
     * String attribute type.  Holds a String.
     */
    public static final int STRING = 10;

    /**
     * URL attribute type.  Holds a String representing a URL.
     */
    public static final int URL = 11;

    /**
     * The type of the attribute.
     */
    private int type;

    /**
     * Either an AttributeTable or a Vector of String.
     */
    private Object attr;

    /**
     * Construct a container attribute.
     *
     * @param container the AttributeTable container.
     * @deprecated Use the ctor with the name.
     */
    public Attribute(AttributeTable container)
    {
        type = CONTAINER;
        attr = container;
    }

    /**
     * Construct an Attribute with the given type and initial
     * value.
     *
     * @param type      the type of attribute to create.  Use one of the type
     *                  constants defined by this class.
     * @param clearname the name of the attribute.
     * @param value     the initial value of this attribute.  Use the
     *                  appendValue method to create a vector of values.
     * @param check     if true, check the value and throw
     *                  AttributeBadValueException if it's not valid; if false do not check its
     *                  validity.
     * @throws AttributeBadValueException thrown if the value is not a legal
     *                                    member of type
     */
    public Attribute(int type, String clearname, String value, boolean check)
            throws AttributeBadValueException
    {

        super(clearname);
        if(check)
            value = forceValue(type, value);

        this.type = type;
        attr = new Vector();
        ((Vector) attr).addElement(value);
    }

    /**
     * Construct an Attribute with the given type and initial
     * value. Checks the value of the attribute and throws an exception if
     * it's not valid.
     *
     * @param type      the type of attribute to create.  Use one of the type
     *                  constants defined by this class.
     * @param clearname the name of the attribute.
     * @param value     the initial value of this attribute.  Use the
     *                  appendValue method to create a vector of values.
     * @throws AttributeBadValueException thrown if the value is not a legal
     *                                    member of type
     */
    public Attribute(int type, String clearname, String value)
            throws AttributeBadValueException
    {

        super(clearname);
        value = forceValue(type, value);

        this.type = type;
        attr = new Vector();
        ((Vector) attr).addElement(value);
    }

    /**
     * Construct a container attribute.
     *
     * @param container the AttributeTable container.
     */
    public Attribute(String clearname, AttributeTable container)
    {
        super(clearname);
        type = CONTAINER;
        attr = container;
    }

    /**
     * Construct an empty attribute with the given type.
     *
     * @param type the type of attribute to create.  Use one of the type
     *             constants defined by this class, other than CONTAINER.
     * @throws IllegalArgumentException thrown if
     *                                  type is CONTAINER. To construct an empty
     *                                  container attribute, first construct and empty AttributeTable and then
     *                                  use that to construct the Attribute.
     */
    public Attribute(String clearname, int type) throws IllegalArgumentException
    {
        super(clearname);
        this.type = type;
        if(type == CONTAINER)
            throw new IllegalArgumentException("Can't construct an Attribute(CONTAINER)");
        else
            attr = new Vector();
    }

    /**
     * Returns the attribute type as a String.
     *
     * @return the attribute type String.
     */
    public final String getTypeString()
    {
        switch (type) {
        case CONTAINER:
            return "Container";
        case ALIAS:
            return "Alias";
        case BYTE:
            return "Byte";
        case INT16:
            return "Int16";
        case UINT16:
            return "UInt16";
        case INT32:
            return "Int32";
        case UINT32:
            return "UInt32";
        case FLOAT32:
            return "Float32";
        case FLOAT64:
            return "Float64";
        case STRING:
            return "String";
        case URL:
            return "Url";
        //    case BOOLEAN: return "Boolean";
        default:
            return "";
        }
    }

    /**
     * Returns the attribute type as a String.
     *
     * @return the attribute type String.
     */
    public static final int getTypeVal(String s)
    {

        if(s.equalsIgnoreCase("Container"))
            return CONTAINER;
        else if(s.equalsIgnoreCase("Byte"))
            return BYTE;
        else if(s.equalsIgnoreCase("Int16"))
            return INT16;
        else if(s.equalsIgnoreCase("UInt16"))
            return UINT16;
        else if(s.equalsIgnoreCase("Int32"))
            return INT32;
        else if(s.equalsIgnoreCase("UInt32"))
            return UINT32;
        else if(s.equalsIgnoreCase("Float32"))
            return FLOAT32;
        else if(s.equalsIgnoreCase("Float64"))
            return FLOAT64;
        else if(s.equalsIgnoreCase("String"))
            return STRING;
        else if(s.equalsIgnoreCase("URL"))
            return URL;
        else
            return UNKNOWN;
    }

    /**
     * Returns the attribute type constant.
     *
     * @return the attribute type constant.
     */
    public int getType()
    {
        return type;
    }

    /**
     * Returns true if the attribute is a container.
     *
     * @return true if the attribute is a container.
     */
    public boolean isContainer()
    {
        return (type == CONTAINER);
    }

    /**
     * Returns true if the attribute is an alias.
     *
     * @return true if the attribute is an alias.
     */
    public boolean isAlias()
    {
        return (false);
    }

    /**
     * Returns the AttributeTable container.
     *
     * @return the AttributeTable container.
     * @throws NoSuchAttributeException If
     *                                  the instance of Attribute on which it is called is not a container.
     */
    public AttributeTable getContainer() throws NoSuchAttributeException
    {
        checkContainerUsage();
        return (AttributeTable) attr;
    }

    /**
     * Returns the AttributeTable container.
     *
     * @return the AttributeTable container, or null if not a container.
     */
    public AttributeTable getContainerN()
    {
        return (attr instanceof AttributeTable) ? (AttributeTable) attr : null;
    }

    /**
     * Returns the values of this attribute as an Enumeration
     * of String.
     *
     * @return an Enumeration of String.
     */
    public Enumeration getValues() throws NoSuchAttributeException
    {
        checkVectorUsage();
        return ((Vector) attr).elements();
    }

    /**
     * Returns the values of this attribute as an Enumeration of String.
     *
     * @return an Iterator<String> of String , or null if a container..
     */
    public Iterator getValuesIterator()
    {
        return (attr instanceof Vector) ? ((Vector) attr).iterator() : null;
    }

    /**
     * Returns the nummber of values held in this attribute.
     *
     * @return the attribute String at index.
     */
    public int getNumVal() throws NoSuchAttributeException
    {
        checkVectorUsage();
        return ((Vector) attr).size();
    }

    /**
     * Returns the attribute value at index.
     *
     * @param index the index of the attribute value to return.
     * @return the attribute String at index.
     */
    public String getValueAt(int index) throws NoSuchAttributeException
    {
        checkVectorUsage();
        return (String) ((Vector) attr).elementAt(index);
    }

    /**
     * Returns the attribute value at index.
     *
     * @param index the index of the attribute value to return.
     * @return the attribute String at index, or null if a container..
     */
    public String getValueAtN(int index)
    {
        if(!(attr instanceof Vector)) return null;
        return (String) ((Vector) attr).elementAt(index);
    }

    /**
     * Append a value to this attribute. Always checks the validity of the
     * attribute's value.
     *
     * @param value the attribute String to add.
     * @throws AttributeBadValueException thrown if the value is not a legal
     *                                    member of type
     */
    public void appendValue(String value)
            throws NoSuchAttributeException, AttributeBadValueException
    {
        checkVectorUsage();
        appendValue(value, true);
    }

    /**
     * Append a value to this attribute.
     *
     * @param value the attribute String to add.
     * @param check if true, check the validity of he attribute's value, if
     *              false don't.
     * @throws AttributeBadValueException thrown if the value is not a legal
     *                                    member of type
     */
    public void appendValue(String value, boolean check)
            throws NoSuchAttributeException, AttributeBadValueException
    {

        checkVectorUsage();
        if(check)
            value = forceValue(type, value);

        ((Vector) attr).addElement(value);
    }

    /**
     * Remove the i'th String from this attribute.
     *
     * @param index the index of the value to remove.
     */
    public void deleteValueAt(int index)
            throws AttributeBadValueException, NoSuchAttributeException
    {
        checkVectorUsage();
        ((Vector) attr).removeElementAt(index);
    }

    /**
     * Check if the value is legal for a given type.
     *
     * @param type  the type of the value.
     * @param value the value String.
     * @throws AttributeBadValueException if the value is not a legal
     *                                    member of type
     */
    private static void dispatchCheckValue(int type, String value)
            throws AttributeBadValueException
    {

        switch (type) {

        case BYTE:
            if(!checkByte(value))
                throw new AttributeBadValueException("`" + value + "' is not a Byte value.");
            break;

        case INT16:
            if(!checkShort(value))
                throw new AttributeBadValueException("`" + value + "' is not an Int16 value.");
            break;

        case UINT16:
            if(!checkUShort(value))
                throw new AttributeBadValueException("`" + value + "' is not an UInt16 value.");
            break;

        case INT32:
            if(!checkInt(value))
                throw new AttributeBadValueException("`" + value + "' is not an Int32 value.");
            break;

        case UINT32:
            if(!checkUInt(value))
                throw new AttributeBadValueException("`" + value + "' is not an UInt32 value.");
            break;

        case FLOAT32:
            if(!checkFloat(value))
                throw new AttributeBadValueException("`" + value + "' is not a Float32 value.");
            break;

        case FLOAT64:
            if(!checkDouble(value))
                throw new AttributeBadValueException("`" + value + "' is not a Float64 value.");
            break;

        //    case BOOLEAN:
        //      if(!checkBoolean(value))
        //	throw new AttributeBadValueException("`" + value + "' is not a Boolean value.");
        //      break;

        default:
            // Assume UNKNOWN, CONTAINER, STRING, and URL are okay.
        }
    }

    /**
     * Check if the value is legal for a given type
     * and try to convert to specified type.
     *
     * @param type  the type of the value.
     * @param value the value String.
     * @return original or converted value
     * @throws AttributeBadValueException if the value is not a legal
     *                                    member of type
     */
    private static String forceValue(int type, String value)
            throws AttributeBadValueException
    {
        try {
            dispatchCheckValue(type, value);
        } catch (AttributeBadValueException abe) {
            if(type == BYTE) {// Try again: allow e.g. negative byte values
                short val = Short.parseShort(value);
                if(val > 255 && val < -128)
                    throw new AttributeBadValueException("Cannot convert to byte: " + value);
                value = Integer.toString((val&0xFF));
            }
        }
        return value;
    }

    /**
     * Check if string is a valid Byte.
     *
     * @param s the String to check.
     * @return true if the value is legal.
     */
    private static final boolean checkByte(String s)
            throws AttributeBadValueException
    {
        try {
            // Byte.parseByte() can't be used because values > 127 are allowed
            short val = Short.parseShort(s);
            if(DebugValueChecking) {
                log.debug("Attribute.checkByte() - string: '" + s + "'   value: " + val);
            }
            if(val > 0xFF || val < 0)
                return false;
            else
                return true;
        } catch (NumberFormatException e) {
            throw new AttributeBadValueException("`" + s + "' is not a Byte value.");
        }
    }

    /**
     * Check if string is a valid Int16.
     *
     * @param s the String to check.
     * @return true if the value is legal.
     */
    private static final boolean checkShort(String s)
    {
        try {
            short val = Short.parseShort(s);
            if(DebugValueChecking) {
                DAPNode.log.debug("Attribute.checkShort() - string: '" + s + "'   value: " + val);
            }
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /**
     * Check if string is a valid UInt16.
     *
     * @param s the String to check.
     * @return true if the value is legal.
     */
    private static final boolean checkUShort(String s)
    {
        // Note: Because there is no Unsigned class in Java, use Long instead.
        try {
            long val = Long.parseLong(s);
            if(DebugValueChecking) {
                DAPNode.log.debug("Attribute.checkUShort() - string: '" + s + "'   value: " + val);
            }
            if(val > 0xFFFFL)
                return false;
            else
                return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /**
     * Check if string is a valid Int32.
     *
     * @param s the String to check.
     * @return true if the value is legal.
     */
    private static final boolean checkInt(String s)
    {
        try {
            //Coverity[FB.DLS_DEAD_LOCAL_STORE]
            int val = Integer.parseInt(s);
            if(DebugValueChecking) {
                DAPNode.log.debug("Attribute.checkInt() - string: '" + s + "'   value: " + val);
            }
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /**
     * Check if string is a valid UInt32.
     *
     * @param s the String to check.
     * @return true if the value is legal.
     */
    private static final boolean checkUInt(String s)
    {
        // Note: Because there is no Unsigned class in Java, use Long instead.
        try {
            long val = Long.parseLong(s);
            if(DebugValueChecking) {
                DAPNode.log.debug("Attribute.checkUInt() - string: '" + s + "'   value: " + val);
            }
            if(val > 0xFFFFFFFFL)
                return false;
            else
                return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /**
     * Check if string is a valid Float32.
     *
     * @param s the String to check.
     * @return true if the value is legal.
     */
    private static final boolean checkFloat(String s)
    {
        try {
            //Coverity[FB.DLS_DEAD_LOCAL_STORE]=
            float val = Float.parseFloat(s);
            if(DebugValueChecking) {
                DAPNode.log.debug("Attribute.checkFloat() - string: '" + s + "'   value: " + val);
            }
            return true;
        } catch (NumberFormatException e) {
            if(s.equalsIgnoreCase("nan") || s.equalsIgnoreCase("inf"))
                return true;

            return false;
        }
    }

    /**
     * Check if string is a valid Float64.
     *
     * @param s the String to check.
     * @return true if the value is legal.
     */
    private static final boolean checkDouble(String s)
    {
        try {
            //Coverity[FB.DLS_DEAD_LOCAL_STORE]
            double val = Double.parseDouble(s);
            if(DebugValueChecking) {
                DAPNode.log.debug("Attribute.checkDouble() - string: '" + s + "'   value: " + val);
            }
            return true;
        } catch (NumberFormatException e) {
            if(s.equalsIgnoreCase("nan") || s.equalsIgnoreCase("inf"))
                return true;

            return false;
        }
    }

/* NOTE: THis method is flawed think of a better way to do this
    BEFORE you uncomment it!! The valueOf() method for
	Boolean returns true if and only if the string passed
	is equal to (ignoring case) to "true"
	Otherwise it returns false... A lousy test., UNLESS
	the JAVA implementation of a Boolean string works for
	you.

   * Check if string is a valid Boolean.
   * @param s the String to check.
   * @return true if the value is legal.

  private static final boolean checkBoolean(String s) {
    try {
	Boolean.valueOf(s);
	return true;
    }
    catch (NumberFormatException e) {
	return false;
    }
  }

*/

    private void checkVectorUsage() throws NoSuchAttributeException
    {

        if(!(attr instanceof Vector)) {
            throw new NoSuchAttributeException(
                    "The Attribute '" + getEncodedName() + "' is a container. " +
                            "It's contents are Attribues, not values.");
        }
    }

    private void checkContainerUsage() throws NoSuchAttributeException
    {

        if(_Debug) {
            DAPNode.log.debug("Attribute.checkContainerUsage(): ");
        }

        if(!(attr instanceof AttributeTable)) {
            throw new NoSuchAttributeException(
                    "The Attribute '" + getEncodedName() + "' is not a container (AttributeTable)." +
                            "It's content is made up of values, not other Attributes.");
        }
        if(_Debug) {
            DAPNode.log.debug("The Attribute is a container");
        }
    }

    public void print(PrintWriter os, String pad)
    {

        if(_Debug) os.println("Entered Attribute.print()");

        if(this.attr instanceof AttributeTable) {

            if(_Debug) os.println("  Attribute \"" + _nameClear + "\" is a Container.");

            ((AttributeTable) this.attr).print(os, pad);

        } else {
            if(_Debug) os.println("    Printing Attribute \"" + _nameClear + "\".");

            os.print(pad + getTypeString() + " " + getEncodedName() + " ");

            Enumeration es = ((Vector) this.attr).elements();

            while(es.hasMoreElements()) {
                String val = (String) es.nextElement();

/* Base quoting on type
        boolean useQuotes = false;
        if (val.indexOf(' ') >= 0 ||
                val.indexOf('\t') >= 0 ||
                val.indexOf('\n') >= 0 ||
                val.indexOf('\r') >= 0
                ) {

          if (val.indexOf('\"') != 0)
            useQuotes = true;
        }

        if (useQuotes)
          os.print("\"" + val + "\"");
        else
          os.print(val);
*/
                if(this.type == Attribute.STRING) {
                    String quoted = "\"" + EscapeStrings.backslashEscapeDapString(val) + "\"";
                    for(int i = 0; i < quoted.length(); i++) {
                        os.print((char) ((int) quoted.charAt(i)));
                    }
                    //os.print(quoted);
                } else
                    os.print(val);

                if(es.hasMoreElements())
                    os.print(", ");
            }
            os.println(";");
        }
        if(_Debug) os.println("Leaving Attribute.print()");
        os.flush();
    }

    /**
     * Print the attribute on the given OutputStream.
     *
     * @param os  the OutputStream to use for output.
     * @param pad the number of spaces to indent each line.
     */
    public final void print(OutputStream os, String pad)
    {
        print(new PrintWriter(new BufferedWriter(new OutputStreamWriter(os, Util.UTF8))), pad);
    }

    /**
     * Print the attribute on the given PrintWriter with
     * four spaces of indentation.
     *
     * @param os the PrintWriter to use for output.
     */
    public final void print(PrintWriter os)
    {
        print(os, "");
    }

    /**
     * Print the attribute on the given OutputStream with
     * four spaces of indentation.
     *
     * @param os the OutputStream to use for output.
     */
    public final void print(OutputStream os)
    {
        print(os, "");
    }

    /**
     * @param os
     * @opendap.ddx.experimental
     */
    public void printXML(OutputStream os)
    {
        printXML(os, "");
    }

    /**
     * @param os
     * @param pad
     * @opendap.ddx.experimental
     */
    public void printXML(OutputStream os, String pad)
    {
        PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os, Util.UTF8)));
        printXML(pw, pad);
        pw.flush();
    }

    /**
     * @param pw
     * @opendap.ddx.experimental
     */
    public void printXML(PrintWriter pw)
    {
        printXML(pw, "");
    }

    /**
     * @param pw
     * @param pad
     * @opendap.ddx.experimental
     */
    public void printXML(PrintWriter pw, String pad)
    {
        printXML(pw, pad, false);
    }

    /**
     * @param pw
     * @param pad
     * @param constrained
     * @opendap.ddx.experimental
     */
    public void printXML(PrintWriter pw, String pad, boolean constrained)
    {

        if(_Debug) pw.println("Entered Attribute.printXML()");

        if(this.attr instanceof AttributeTable) {

            if(_Debug) pw.println("  Attribute \"" + _nameClear + "\" is a Container.");

            ((AttributeTable) this.attr).printXML(pw, pad, constrained);

        } else {
            if(_Debug) pw.println("    Printing Attribute \"" + _nameClear + "\".");

            pw.println(pad + "<Attribute name=\"" +
                    DDSXMLParser.normalizeToXML(getEncodedName()) +
                    "\" type=\"" + getTypeString() + "\">");

            Enumeration es = ((Vector) this.attr).elements();
            while(es.hasMoreElements()) {
                String val = (String) es.nextElement();
                pw.println(pad + "\t" +
                        "<value>" +
                        DDSXMLParser.normalizeToXML(val) +
                        "</value>");
            }
            pw.println(pad + "</Attribute>");
        }
        if(_Debug) pw.println("Leaving Attribute.print()");
        pw.flush();

    }

    static String
    fixnan(String value)
    {
    /* Check for hyrax error */
        if(value.equalsIgnoreCase("nan."))
            value = "nan";
        else if(value.equalsIgnoreCase("inf."))
            value = "inf";
        return value;
    }

    /**
     * Returns a clone of this Attribute.
     * See DAPNode.cloneDag()
     *
     * @param map track previously cloned nodes
     * @return a clone of this Attribute.
     */
    public DAPNode cloneDAG(CloneMap map)
            throws CloneNotSupportedException
    {
        Attribute a = (Attribute) super.cloneDAG(map);
        // assume type, is_alias, and aliased_to have been cloned already
        if(type == CONTAINER)
            a.attr = (AttributeTable) cloneDAG(map, ((AttributeTable) attr));
        else
            a.attr = ((Vector) attr).clone(); // ok, attr is a vector of strings
        return a;
    }

}

// $Log: Attribute.java,v $
// Revision 1.2  2003/09/02 17:49:34  ndp
// *** empty log message ***
//
// Revision 1.1  2003/08/12 23:51:25  ndp
// Mass check in to begin Java-OPeNDAP development work
//
// Revision 1.11 2011/01/09 Dennis Heimbigner
//  - Make subclass of BaseType for uniformity
//
// Revision 1.10  2003/04/16 21:50:53  caron
// turn off debug flag
//
// Revision 1.9  2003/04/07 22:12:32  jchamber
// added serialization
//
// Revision 1.8  2003/02/12 16:41:15  ndp
// *** empty log message ***
//
// Revision 1.7  2002/10/08 21:59:18  ndp
// Added XML functionality to the core. This includes the new DDS code (aka DDX)
// for parsing XML representations of the dataset description ( that's a DDX)
// Also BaseType has been modified to hold Attributes and methods added to DDS
// to ingest DAS's (inorder to add Attributes to variables) and to get the DAS
// object from the DDS. Geturl and DConnect hav been modified to provide client
// access to this new set of functionalites. ndp 10/8/2002
//
// Revision 1.6  2002/08/27 04:30:11  ndp
// AttributeTable added to BaseType
// Interfaces for AttributeTable implmented in BaseType, DConstructor, and DDS
// Methods added to DDS to print DAS and to return a DAS object.
// XMLParser updated to populate BaseType AttributeTables.
//
// Revision 1.5  2002/05/30 23:24:58  jimg
// I added methods that provide a way to add attribute values without checking
// their type/value validity. This provides a way to add bad values in a
// *_dods_error attribute container so that attributes that are screwed up won't
// be lost (because they might make sense to a person, for example) or cause the
// whole DAS to break. The DAS parser (DASParser.jj) uses this new code.
//


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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ucar.ma2.*;
import ucar.nc2.*;
import ucar.nc2.constants.CDM;
import ucar.nc2.constants.DataFormatType;
import ucar.nc2.iosp.netcdf3.N3iosp;
import ucar.nc2.util.Misc;

import javax.annotation.Nonnull;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;

import static ucar.ma2.DataType.*;

/**
 * Implementation of EnhanceScaleMissingUnsigned for unsigned data, scale/offset packed data, and missing data.
 *
 * @author caron
 * @author cwardgar
 * @see EnhanceScaleMissingUnsigned
 */
class EnhanceScaleMissingUnsignedImpl implements EnhanceScaleMissingUnsigned {
  private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  private DataType origDataType = null, unsignedConversionType = null, scaledOffsetType = null;

  // defaults from NetcdfDataset modes
  private boolean invalidDataIsMissing = NetcdfDataset.invalidDataIsMissing;
  private boolean fillValueIsMissing = NetcdfDataset.fillValueIsMissing;
  private boolean missingDataIsMissing = NetcdfDataset.missingDataIsMissing;

  private boolean hasScaleOffset = false;
  private double scale = 1.0, offset = 0.0;

  private boolean hasValidRange = false, hasValidMin = false, hasValidMax = false;
  private double validMin = -Double.MAX_VALUE, validMax = Double.MAX_VALUE;

  private boolean hasFillValue = false;
  private double fillValue; // LOOK: making it double not really correct. What about CHAR?

  private boolean hasMissingValue = false;
  private double[] missingValue;  // LOOK: also wrong to make double, for the same reason.

  private DataType.Signedness signedness;

  /**
   * Constructor, when you dont want anything done.
   */
  EnhanceScaleMissingUnsignedImpl() {
  }

  /**
   * Constructor, default values.
   *
   * @param forVar the Variable to decorate.
   */
  EnhanceScaleMissingUnsignedImpl(VariableDS forVar) {
    this(forVar, NetcdfDataset.fillValueIsMissing, NetcdfDataset.invalidDataIsMissing,
        NetcdfDataset.missingDataIsMissing);
  }

  /**
   * Constructor.
   * If scale/offset attributes are found, remove them from the decorated variable.
   *
   * @param forVar               the Variable to decorate.
   * @param fillValueIsMissing   use _FillValue for isMissing()
   * @param invalidDataIsMissing use valid_range for isMissing()
   * @param missingDataIsMissing use missing_value for isMissing()
   */
  private EnhanceScaleMissingUnsignedImpl(VariableDS forVar, boolean fillValueIsMissing, boolean invalidDataIsMissing,
          boolean missingDataIsMissing) {
    this.fillValueIsMissing = fillValueIsMissing;
    this.invalidDataIsMissing = invalidDataIsMissing;
    this.missingDataIsMissing = missingDataIsMissing;

    this.origDataType = forVar.getDataType();
    this.unsignedConversionType = origDataType;

    // unsignedConversionType is initialized to origDataType, and origDataType may be a non-integral type that doesn't
    // have an "unsigned flavor" (such as FLOAT and DOUBLE). Furthermore, unsignedConversionType may start out as
    // integral, but then be widened to non-integral (i.e. LONG -> DOUBLE). For these reasons, we cannot rely upon
    // unsignedConversionType to store the signedness of the variable. We need a separate field.
    this.signedness = origDataType.getSignedness();

    // In the event of conflict, "unsigned" wins. Potential conflicts include:
    // 1. origDataType is unsigned, but variable has "_Unsigned == false" attribute.
    // 2. origDataType is signed,   but variable has "_Unsigned == true"  attribute.
    if (signedness == Signedness.SIGNED) {
      Attribute unsignedAtt = forVar.findAttributeIgnoreCase(CDM.UNSIGNED);
      if (unsignedAtt != null && unsignedAtt.getStringValue().equalsIgnoreCase("true")) {
          this.signedness = Signedness.UNSIGNED;
      }
    }

    if (signedness == Signedness.UNSIGNED) {
      // We may need a larger data type to hold the results of the unsigned conversion.
      this.unsignedConversionType = nextLarger(origDataType).withSignedness(Signedness.UNSIGNED);
      logger.debug("assign unsignedConversionType = {}", unsignedConversionType);
    }

    DataType scaleType = null, offsetType = null, validType = null;
    logger.debug("{} for Variable = {}", getClass().getSimpleName(), forVar.getFullName());

    Attribute scaleAtt = forVar.findAttribute(CDM.SCALE_FACTOR);
    if (scaleAtt != null && !scaleAtt.isString()) {
      scaleType = getAttributeDataType(scaleAtt);
      scale = convertUnsigned(scaleAtt.getNumericValue(), scaleType).doubleValue();
      hasScaleOffset = true;
      logger.debug("scale = {}    type = {}", scale, scaleType);
    }

    Attribute offsetAtt = forVar.findAttribute(CDM.ADD_OFFSET);
    if (offsetAtt != null && !offsetAtt.isString()) {
      offsetType = getAttributeDataType(offsetAtt);
      offset = convertUnsigned(offsetAtt.getNumericValue(), offsetType).doubleValue();
      hasScaleOffset = true;
      logger.debug("offset = {}", offset);
    }

    ////// missing data : valid_range. assume here its in units of unpacked data. correct this below
    Attribute validRangeAtt = forVar.findAttribute(CDM.VALID_RANGE);
    if (validRangeAtt != null && !validRangeAtt.isString() && validRangeAtt.getLength() > 1) {
      validType = getAttributeDataType(validRangeAtt);
      validMin = convertUnsigned(validRangeAtt.getNumericValue(0), validType).doubleValue();
      validMax = convertUnsigned(validRangeAtt.getNumericValue(1), validType).doubleValue();
      hasValidRange = true;
      logger.debug("valid_range = {}  {}", validMin, validMax);
    }

    Attribute validMinAtt = forVar.findAttribute(CDM.VALID_MIN);
    Attribute validMaxAtt = forVar.findAttribute(CDM.VALID_MAX);

    // Only process the valid_min and valid_max attributes if valid_range isn't present.
    if (!hasValidRange) {
      if (validMinAtt != null && !validMinAtt.isString()) {
        validType = getAttributeDataType(validMinAtt);
        validMin = convertUnsigned(validMinAtt.getNumericValue(), validType).doubleValue();
        hasValidMin = true;
        logger.debug("valid_min = {}", validMin);
      }

      if (validMaxAtt != null && !validMaxAtt.isString()) {
        validType = largestOf(validType, getAttributeDataType(validMaxAtt));
        validMax = convertUnsigned(validMaxAtt.getNumericValue(), validType).doubleValue();
        hasValidMax = true;
        logger.debug("valid_min = {}", validMax);
      }

      if (hasValidMin && hasValidMax) {
        hasValidRange = true;
      }
    }

    /// _FillValue
    Attribute fillValueAtt = forVar.findAttribute(CDM.FILL_VALUE);
    if (fillValueAtt != null && !fillValueAtt.isString()) {
      DataType fillType = getAttributeDataType(fillValueAtt);
      fillValue = convertUnsigned(fillValueAtt.getNumericValue(), fillType).doubleValue();
      fillValue = applyScaleOffset(fillValue);  // This will fail when _FillValue is CHAR.
      hasFillValue = true;
    } else {
      // No _FillValue attribute found. Instead, if file is NetCDF and variable is numeric, use the default fill value.
      String fileTypeId = forVar.getNetcdfFile() == null ? null : forVar.getNetcdfFile().getFileTypeId();

      boolean isNetcdfIosp = DataFormatType.NETCDF.getDescription().equals(fileTypeId) ||
              DataFormatType.NETCDF4.getDescription().equals(fileTypeId);

      if (isNetcdfIosp && unsignedConversionType.isNumeric()) {
        fillValue = applyScaleOffset(N3iosp.getFillValueDefault(unsignedConversionType));
        hasFillValue = true;
      }
    }

    /// missing_value
    Attribute missingValueAtt = forVar.findAttribute(CDM.MISSING_VALUE);
    if (missingValueAtt != null) {
      if (missingValueAtt.isString()) {
        String svalue = missingValueAtt.getStringValue();
        if (origDataType == DataType.CHAR) {
          missingValue = new double[1];
          if (svalue.length() == 0) {
            missingValue[0] = 0;
          } else {
            missingValue[0] = svalue.charAt(0);
          }

          hasMissingValue = true;
        } else {  // not a CHAR - try to fix problem where they use a numeric value as a String attribute
          try {
            missingValue = new double[1];
            missingValue[0] = Double.parseDouble(svalue);
            hasMissingValue = true;
          } catch (NumberFormatException ex) {
            logger.debug("String missing_value not parseable as double = {}", missingValueAtt.getStringValue());
          }
        }
      } else { // not a string
        DataType missType = getAttributeDataType(missingValueAtt);

        missingValue = new double[missingValueAtt.getLength()];
        for (int i = 0; i < missingValue.length; i++) {
          missingValue[i] = convertUnsigned(missingValueAtt.getNumericValue(i), missType).doubleValue();
          missingValue[i] = applyScaleOffset(missingValue[i]);
        }
        logger.debug("missing_data: {}", Arrays.toString(missingValue));

        for (double mv : missingValue) {
          if (!Double.isNaN(mv)) {
            hasMissingValue = true;   // dont need to do anything if its already a NaN
            break;
          }
        }
      }
    }

    /// assign convertedDataType if needed
    if (hasScaleOffset) {
      scaledOffsetType = largestOf(unsignedConversionType, scaleType, offsetType).withSignedness(signedness);
      logger.debug("assign scaledOffsetType = {}", scaledOffsetType);

      // validData may be packed or unpacked
      if (hasValidData()) {
        if (rank(validType) == rank(largestOf(scaleType, offsetType)) &&
                rank(validType) > rank(unsignedConversionType)) {
          // If valid_range is the same type as the wider of scale_factor and add_offset, PLUS
          // it is wider than the (packed) data, we know that the valid_range values were stored as unpacked.
          // We already assumed that this was the case when we first read the attribute values, so there's
          // nothing for us to do here.
        } else {
          // Otherwise, the valid_range values were stored as packed. So now we must unpack them.
          if (hasValidRange || hasValidMin) {
            validMin = applyScaleOffset(validMin);
          }
          if (hasValidRange || hasValidMax) {
            validMax = applyScaleOffset(validMax);
          }
        }
      }
    }
  }

  // Get the data type of an attribute. Make it unsigned if the variable is unsigned.
  private DataType getAttributeDataType(Attribute attribute) {
    DataType dataType = attribute.getDataType();
    if (signedness == Signedness.UNSIGNED) {
      // If variable is unsigned, make its integral attributes unsigned too.
      dataType = dataType.withSignedness(signedness);
    }
    return dataType;
  }

  /**
   * Returns a distinct integer for each of the {@link DataType#isNumeric() numeric} data types that can be used to
   * (roughly) order them by the range of the DataType. {@code BYTE < UBYTE < SHORT < USHORT < INT < UINT <
   * LONG < ULONG < FLOAT < DOUBLE}. {@code -1} will be returned for all non-numeric data types.
   *
   * @param dataType  a numeric data type.
   * @return  a distinct integer for each of the numeric data types that can be used to (roughly) order them by size.
   */
  public static int rank(DataType dataType) {
    if (dataType == null) {
      return -1;
    }

    switch (dataType) {
      case BYTE:   return 0;
      case UBYTE:  return 1;
      case SHORT:  return 2;
      case USHORT: return 3;
      case INT:    return 4;
      case UINT:   return 5;
      case LONG:   return 6;
      case ULONG:  return 7;
      case FLOAT:  return 8;
      case DOUBLE: return 9;
      default:     return -1;
    }
  }

  /**
   * Returns the data type that is the largest among the arguments. Relative sizes of data types are determined via
   * {@link #rank(DataType)}.
   *
   * @param dataTypes  an array of numeric data types.
   * @return  the data type that is the largest among the arguments.
   */
  public static DataType largestOf(DataType... dataTypes) {
    DataType widest = null;
    for (DataType dataType : dataTypes) {
      if (widest == null) {
        widest = dataType;
      } else if (rank(dataType) > rank(widest)) {
        widest = dataType;
      }
    }
    return widest;
  }

  /**
   * Returns the smallest numeric data type that:
   *

   *
can hold a larger integer than {@code dataType} can

   *
if integral, has the same signedness as {@code dataType}

   *

   * The relative sizes of data types are determined in a manner consistent with {@link #rank(DataType)}.
   *

   *

   *
  Argument	  Result

   *
  BYTE	      SHORT

   *
  UBYTE	     USHORT

   *
  SHORT	     INT

   *
  USHORT	    UINT

   *
  INT	       LONG

   *
  UINT	      ULONG

   *
  LONG	      DOUBLE

   *
  ULONG	     DOUBLE

   *

   *         Any other data type
   *         Just return argument
   *

   *

   *

   * The returned type is intended to be just big enough to hold the result of performing an unsigned conversion of a
   * value of the smaller type. For example, the {@code byte} value {@code -106} equals {@code 150} when interpreted
   * as unsigned. That won't fit in a (signed) {@code byte}, but it will fit in a {@code short}.
   *
   * @param dataType  an integral data type.
   * @return  the next larger type.
   */
  public static DataType nextLarger(DataType dataType) {
    switch (dataType) {
      case BYTE:   return SHORT;
      case UBYTE:  return USHORT;
      case SHORT:  return INT;
      case USHORT: return UINT;
      case INT:    return LONG;
      case UINT:   return ULONG;
      case LONG:   return DOUBLE;
      case ULONG:  return DOUBLE;
      default:     return dataType;
    }
  }

  @Override public double getScaleFactor() {
    return scale;
  }

  @Override public double getOffset() {
    return offset;
  }

  @Override public Signedness getSignedness() {
    return signedness;
  }

  @Override public DataType getScaledOffsetType() {
    return scaledOffsetType;
  }

  @Nonnull @Override public DataType getUnsignedConversionType() {
    return unsignedConversionType;
  }

  @Override
  public boolean hasValidData() {
    return hasValidRange || hasValidMin || hasValidMax;
  }

  @Override
  public double getValidMin() {
    return validMin;
  }

  @Override
  public double getValidMax() {
    return validMax;
  }

  @Override
  public boolean isInvalidData(double val) {
    // valid_min and valid_max may have been multiplied by scale_factor, which could be a float, not a double.
    // That potential loss of precision means that we cannot do the nearlyEquals() comparison with
    // Misc.defaultMaxRelativeDiffDouble.
    boolean greaterThanOrEqualToValidMin =
        Misc.nearlyEquals(val, validMin, Misc.defaultMaxRelativeDiffFloat) || val > validMin;
    boolean lessThanOrEqualToValidMax =
        Misc.nearlyEquals(val, validMax, Misc.defaultMaxRelativeDiffFloat) || val < validMax;

    return (hasValidRange && !(greaterThanOrEqualToValidMin && lessThanOrEqualToValidMax)) ||
           (hasValidMin   && !greaterThanOrEqualToValidMin) ||
           (hasValidMax   && !lessThanOrEqualToValidMax);
  }

  @Override
  public boolean hasFillValue() {
    return hasFillValue;
  }

  @Override
  public boolean isFillValue(double val) {
    return hasFillValue && Misc.nearlyEquals(val, fillValue, Misc.defaultMaxRelativeDiffFloat);
  }

  @Override
  public double getFillValue() {
    return fillValue;
  }

  @Override
  public boolean hasScaleOffset() {
    return hasScaleOffset;
  }

  @Override
  public boolean hasMissingValue() {
    return hasMissingValue;
  }

  @Override
  public boolean isMissingValue(double val) {
    if (!hasMissingValue) {
      return false;
    }
    for (double aMissingValue : missingValue) {
      if (Misc.nearlyEquals(val, aMissingValue, Misc.defaultMaxRelativeDiffFloat)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public double[] getMissingValues() {
    return missingValue;
  }

  @Override
  public void setFillValueIsMissing(boolean b) {
    this.fillValueIsMissing = b;
  }

  @Override
  public void setInvalidDataIsMissing(boolean b) {
    this.invalidDataIsMissing = b;
  }

  @Override
  public void setMissingDataIsMissing(boolean b) {
    this.missingDataIsMissing = b;
  }

  @Override
  public boolean hasMissing() {
    return (invalidDataIsMissing && hasValidData()) || (fillValueIsMissing && hasFillValue()) ||
        (missingDataIsMissing && hasMissingValue());
  }

  @Override
  public boolean isMissing(double val) {
    if (Double.isNaN(val)) {
      return true;
    } else {
      return (missingDataIsMissing && isMissingValue(val)) || (fillValueIsMissing && isFillValue(val)) ||
             (invalidDataIsMissing && isInvalidData(val));
    }
  }

  @Override
  public Number convertUnsigned(Number value) {
    return convertUnsigned(value, signedness);
  }

  private static Number convertUnsigned(Number value, DataType dataType) {
    return convertUnsigned(value, dataType.getSignedness());
  }

  private static Number convertUnsigned(Number value, Signedness signedness) {
    if (signedness == Signedness.UNSIGNED) {
      // Handle integral types that should be treated as unsigned by widening them if necessary.
      return DataType.widenNumberIfNegative(value);
    } else {
      return value;
    }
  }

  @Override
  public Array convertUnsigned(Array in) {
    return convert(in, true, false, false);
  }

  @Override
  public double applyScaleOffset(Number value) {
    double convertedValue = value.doubleValue();
    return hasScaleOffset ? scale * convertedValue + offset : convertedValue;
  }

  @Override
  public Array applyScaleOffset(Array in) {
    return convert(in, false, true, false);
  }

  @Override
  public Number convertMissing(Number value) {
    return isMissing(value.doubleValue()) ? Double.NaN : value;
  }

  @Override
  public Array convertMissing(Array in) {
    return convert(in, false, false, true);
  }

  @Override
  public Array convert(Array in, boolean convertUnsigned, boolean applyScaleOffset, boolean convertMissing) {
    if (!in.getDataType().isNumeric() || (!convertUnsigned && !applyScaleOffset && !convertMissing)) {
      return in;  // Nothing to do!
    }

    if (getSignedness() == Signedness.SIGNED) {
      convertUnsigned = false;
    }
    if (!hasScaleOffset()) {
      applyScaleOffset = false;
    }

    DataType outType = origDataType;
    if (convertUnsigned) {
      outType = getUnsignedConversionType();
    }
    if (applyScaleOffset) {
      outType = getScaledOffsetType();
    }

    if (outType != DataType.FLOAT && outType != DataType.DOUBLE) {
      convertMissing = false;
    }

    Array out = Array.factory(outType, in.getShape());
    IndexIterator iterIn = in.getIndexIterator();
    IndexIterator iterOut = out.getIndexIterator();

    while (iterIn.hasNext()) {
      Number value = (Number) iterIn.getObjectNext();

      if (convertUnsigned) {
        value = convertUnsigned(value);
      }
      if (applyScaleOffset) {
        value = applyScaleOffset(value);
      }
      if (convertMissing) {
        value = convertMissing(value);
      }

      iterOut.setObjectNext(value);
    }

    return out;
  }
}


/*
 * Copyright (c) 2010 The University of Reading
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 * 3. Neither the name of the University of Reading, nor the names of the
 *    authors or contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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 thredds.server.wms;

import java.net.URI;
import java.net.URISyntaxException;
import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import thredds.client.catalog.Access;
import thredds.client.catalog.Dataset;
import thredds.client.catalog.ServiceType;
import thredds.server.config.ThreddsConfig;
import thredds.server.viewer.Viewer;
import thredds.server.viewer.ViewerLinkProvider;

/**
 * A Viewer for viewing datasets using the built-in Godiva3 client.  The viewer
 * must be configured in {@code ${tomcat_home}/content/thredds/threddsConfig.xml}, as per
 * instructions here(needs updated). *
 * @author Jon
 */
public class Godiva3Viewer implements Viewer {
  static private final Logger logger = LoggerFactory.getLogger(Godiva3Viewer.class);

  static private final String title = "Godiva3 (browser-based)";

  /**
   * Returns true if this is a gridded dataset that is accessible via WMS.
   */
  @Override
  public boolean isViewable(Dataset ds) {
    Access access = ds.getAccess(ServiceType.WMS);
    return access != null && (ThreddsConfig.getBoolean("WMS.allow", false));
  }

  @Override
  public String getViewerLinkHtml(Dataset ds, HttpServletRequest req) {
    ViewerLinkProvider.ViewerLink viewerLink = getViewerLink(ds, req);
    return "" + viewerLink.getTitle() + "";
  }

  @Override
  public ViewerLinkProvider.ViewerLink getViewerLink(Dataset ds, HttpServletRequest req) {
    Access access = ds.getAccess(ServiceType.WMS);
    if (access == null) return null;

    URI dataURI = access.getStandardUri();
    if (dataURI == null) {
      logger.warn("Godiva3Viewer access URL failed on {}", ds.getName());
      return null;
    }

    try {
      URI base = new URI(req.getRequestURL().toString());
      dataURI = base.resolve(dataURI);

    } catch (URISyntaxException e) {
      logger.warn("Godiva3Viewer URL=" + req.getRequestURL().toString(), e);
      return null;
    }
    String url = req.getContextPath() + "/Godiva.html?server=" + dataURI.toString();
    return new ViewerLinkProvider.ViewerLink(Godiva3Viewer.title, url);
  }
}


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

import org.springframework.web.util.HtmlUtils;

import java.io.UnsupportedEncodingException;
import java.io.File;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.nio.charset.Charset;
import java.nio.charset.CharacterCodingException;
import java.nio.CharBuffer;
import java.nio.ByteBuffer;

/**
 * Utility methods for validating strings.
 *
 * @author edavis
 * @since 3.16.47
 */
public class StringValidateEncodeUtils {
  private static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StringValidateEncodeUtils.class);

  private StringValidateEncodeUtils() {
  }

  public final static String CHARACTER_ENCODING_UTF_8 = "UTF-8";

  /**
   * Return true if the given String is a valid single-line String.
   *

   *
A string will be considered a valid single-line string if it does not
   * contain any characters from these Unicode general categories:
   *

   *

   *
Cc - Other, Control

   *
Cf - Other, Format

   *
Cs - Other, Surrogate

   *
Co - Other, Private Use

   *
Cn - Other, Not Assigned

   *
Zl - Separator, Line

   *
Zp - Separator, Paragraph

   *

   *

   *
Or, in other words, allow: Letters, Numbers, Marks, Punctuation,
   * Symbols, and Space separators.
   *
   * @param singleLineString the String to validate
   * @return true if the given String is a valid single-line String.
   */
  public static boolean validSingleLineString(String singleLineString) {
    if (singleLineString == null) return false;
    Matcher m = INVALID_CHARACTERS_FOR_SINGLE_LINE_STRING_PATTERN.matcher(singleLineString);
    return !m.find();
  }

  private final static Pattern INVALID_CHARACTERS_FOR_SINGLE_LINE_STRING_PATTERN
          = Pattern.compile("[\\p{Zl}\\p{Zp}\\p{C}]");

  /**
   * Return true if the given String is a valid path.
   *

   *

   * A String is considered a valid path if:
   *

   *
 when passed to validSingleLineString(String) true is returned, and
   *
 it does not contain any parent path segments ("../").

   *

   *

   *
   * @param path the String to validate
   * @return true if the given String is a valid path.
   * @see #validSingleLineString(String)
   */
  @SuppressWarnings({"SimplifiableIfStatement"})
  public static boolean validPath(String path) {
    if (path == null)
      return false;
    // Don't allow ".." directories in path.
    if (path.indexOf("/../") != -1 || path.equals("..")
            || path.startsWith("../") || path.endsWith("/.."))
      return false;

    return validSingleLineString(path);
  }

  /**
   * Return true if the given String is a valid File path.
   *

   *

   * A String is considered a valid File path if:
   *

   *
 when passed to validPath(String) true is returned; and

   *
 it does not contain the Java File path separator
   * (java.io.File.pathSeparatorChar) which is system dependant.

   *

   *
   * @param path the String to validate
   * @return true if the given String is a valid File path.
   * @see #validPath(String)
   */
  @SuppressWarnings({"SimplifiableIfStatement"})
  public static boolean validFilePath(String path) {
    if (path == null)
      return false;
    if (path.indexOf(File.pathSeparatorChar) != -1)
      return false;
    return validPath(path);
  }

  /**
   * Return true if the given String is a valid URI string.
   *

   *

   * A String is considered a valid URI path if:
   *

   *
 when passed to validPath(String) true is returned; and

   *
 ??? see Note below

   *

   *

   *
NOTE: Check compliance with URI RFC (RFC 3986) - TODO.
   *
   * @param uri the String to validate.
   * @return true if the given String is a valid URI string.
   */
  @SuppressWarnings({"SimplifiableIfStatement"})
  public static boolean validUriString(String uri) {
    if (uri == null)
      return false;
    return validPath(uri);
  }

  /**
   * Return true if the given String is a valid ID string.
   *

   *

   * A String is considered a valid ID string if:
   *

   *
it contains no space separator characters (Unicode general
   * category Zs - Separator, Space); and

   *
true is returned when the string is passed to
   * validSingleLineString(String).

   *

   *
   * @param id the String to validate
   * @return true if the given String is a valid ID string.
   * @see #validSingleLineString(String)
   */
  public static boolean validIdString(String id) {
    if (id == null) return false;
    Matcher m = INVALID_CHARACTERS_FOR_ID_STRING_PATTERN.matcher(id);
    return !(m.find() || !validSingleLineString(id));
  }

  private final static Pattern INVALID_CHARACTERS_FOR_ID_STRING_PATTERN
          = Pattern.compile("[\\p{Zs}]");

  /**
   * Return true if the given String contains any less than ("<") or
   * greater than (">") characters; otherwise return false.
   *
   * @param string the String to check.
   * @return true if the given String contains any less than ("<") or greater than (">") characters
   */
  public static boolean containsAngleBracketCharacters(String string) {
    if (string == null)
      return false;
    if (string.indexOf("<") == -1
            && string.indexOf(">") == -1)
      return false;

    return true;
  }

  /**
   * Return true if the given String contains any ampersand ("&")
   * characters; otherwise return false.
   *
   * @param string the String to check.
   * @return true if the given String contains any ampersand ("&") characters
   */
  public static boolean containsAmpersandCharacters(String string) {
    if (string == null)
      return false;
    if (string.indexOf("&") == -1)
      return false;

    return true;
  }

  /**
   * Return true if the given String contains any backslash ("\")
   * characters; otherwise return false.
   *
   * @param string the String to check.
   * @return true if the given String contains any backslash ("\") characters
   */
  public static boolean containsBackslashCharacters(String string) {
    if (string == null)
      return false;
    if (string.indexOf("\\") == -1)
      return false;

    return true;
  }

  public static boolean validDecimalNumber(String number) {
    if (number == null)
      return false;
    Matcher m = VALID_DECIMAL_DIGITS_PATTERN.matcher(number);
    return m.matches();
  }

  private final static Pattern VALID_DECIMAL_DIGITS_PATTERN
          = Pattern.compile("[\\+\\-]?[0-9]+");

  /**
   * Return true if the given String is "true" or "false", ignoring case.
   *
   * @param boolString the String to validate.
   * @return true if the given String is "true" or "false", ignoring case.
   */
  @SuppressWarnings({"SimplifiableIfStatement"})
  public static boolean validBooleanString(String boolString) {
    if (boolString == null)
      return false;
    Matcher m = VALID_CHARACTERS_FOR_BOOLEAN_STRING_PATTERN.matcher(boolString);
    if (!m.matches())
      return false;
    return boolString.equalsIgnoreCase("true")
            || boolString.equalsIgnoreCase("false");
  }

  private final static Pattern VALID_CHARACTERS_FOR_BOOLEAN_STRING_PATTERN
          = Pattern.compile("[trueTRUEfalsFALS]*");

  /**
   * Return true if the given String is an alphanumeric string.
   *
   * @param alphNumString the String to validate.
   * @return true if the given String is an alphanumeric string.
   */
  public static boolean validAlphanumericString(String alphNumString) {
    if (alphNumString == null)
      return false;
    Matcher m = VALID_CHARACTERS_FOR_ALPHANUMERIC_STRING_PATTERN.matcher(alphNumString);
    return m.matches();
  }

  private final static Pattern VALID_CHARACTERS_FOR_ALPHANUMERIC_STRING_PATTERN
          = Pattern.compile("[a-zA-Z0-9]*");

  /**
   * Return true if the given String is an alphanumeric string and one of
   * the valid strings in the constrained set.
   *
   * @param alphNumString  the String to validate.
   * @param constrainedSet the set of valid strings
   * @param ignoreCase     if true ignore the case of the letters
   * @return true if the given String is an alphanumeric string.
   */
  public static boolean validAlphanumericStringConstrainedSet(String alphNumString,
                                                              String[] constrainedSet,
                                                              boolean ignoreCase) {
    if (alphNumString == null || constrainedSet == null || constrainedSet.length == 0)
      return false;
    Matcher m = VALID_CHARACTERS_FOR_ALPHANUMERIC_STRING_PATTERN.matcher(alphNumString);
    if (!m.matches())
      return false;
    for (String s : constrainedSet) {
      if (ignoreCase ? alphNumString.equalsIgnoreCase(s) : alphNumString.equals(s))
        return true;
    }
    return false;
  }

  /**
   * Return true if the given path does not ascend into parent directory.
   *
   * @param path the path to check
   * @return true if the given path does not ascend into parent directory.
   */
  @SuppressWarnings({"UnnecessaryContinue"})
  public static boolean descendOnlyFilePath(String path) {
    String[] pathSegments = path.split("/");
    //String[] newPathSegments = new String[pathSegments.length];
    int i = 0;
    for (int indxOrigSegs = 0; indxOrigSegs < pathSegments.length; indxOrigSegs++) {
      String s = pathSegments[indxOrigSegs];
      if (s.equals("."))
        continue;
      else if (s.equals("..")) {
        if (i == 0)
          return false;
        i--;
      } else {
        //newPathSegments[i] = s;
        i++;
      }
    }

    return true;
  }

  /**
   * Check that the given string is a valid percentHexOctets string. The
   * string is considered valid if it only contains a sequence of "%" prefixed,
   * two character strings where each two character string is composed only of
   * US-ASCII digits and upper- or lower-case A-F.
   *

   * For example: "%31%32" or "%7b%7d%7E"
   *
   * @param percentHexOctetsString the string to check for validity
   * @return true if the string is valid, false otherwise.
   */
  public static boolean validPercentHexOctetsString(String percentHexOctetsString) {
    if (percentHexOctetsString == null)
      return false;
    Matcher m = VALID_PERCENT_HEX_OCTETS_PATTERN.matcher(percentHexOctetsString);
    return m.matches();
  }

  private final static Pattern VALID_PERCENT_HEX_OCTETS_PATTERN
          = Pattern.compile("(?:%[0-9a-fA-F]{2})*");

  /**
   * Return the percentHexOctets string that represents the given Unicode
   * code point in the given character set or null if the given character
   * set cannot encode the given code point.
   *
   * @param codePoint   the given Unicode code point
   * @param charsetName the name of the character set.
   * @return the percentHexOctets string that represents the given Unicode code point in the given character set.
   * @throws IllegalArgumentException if the code point is not defined or the the character set is not supported.
   */
  public static String unicodeCodePoint2PercentHexString(int codePoint, String charsetName) {
    if (!Character.isDefined(codePoint))
      throw new IllegalArgumentException(String.format("Given code point [U+%1$04X - %1$d] not assigned to an abstract character.", codePoint));
    if (Character.getType(codePoint) == Character.SURROGATE)
      throw new IllegalArgumentException(String.format("Given code point [U+%1$04X - %1$d] is an unencodable (by itself) surrogate character.", codePoint));

    Charset charset = Charset.availableCharsets().get(charsetName);
    if (charset == null)
      throw new IllegalArgumentException(String.format("Unsupported charset [%s].", charsetName));

    char[] chars = Character.toChars(codePoint);
    ByteBuffer byteBuffer = null;
    try {
      byteBuffer = charset.newEncoder().encode(CharBuffer.wrap(chars));
    } catch (CharacterCodingException e) {
      String message = String.format("Given code point [U+%1$04X - %1$d] cannot be encode in given charset [%2$s].", codePoint, charsetName);
      throw new IllegalArgumentException(message, e);
    }
    byteBuffer.rewind();
    StringBuilder encodedString = new StringBuilder();
    for (int i = 0; i < byteBuffer.limit(); i++) {
      String asHex = Integer.toHexString(byteBuffer.get() & 0xFF);
      encodedString.append("%").append(asHex.length() == 1 ? "0" : "").append(asHex);
    }

    return encodedString.toString();
  }
}


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

package ucar.nc2.iosp.grid;

import ucar.nc2.util.Misc;

import java.util.HashMap;
import java.util.Map;

/**
 * Class to represent the grid definition (projection) information
 * purpose is to convert from String representation to native value.
 */
public abstract class GridDefRecord {

  public static final boolean debug = false;

  /**
   * UNDEFINED int value
   */
  public static final int UNDEFINED = -9999;

  // enumerations for common variables. These enumerations values are consistent
  // with the binary and the older text indexes. If they are changed then
  // many iosp grid classes will fail.

  /**
   * GDS key
   */
  public static final String GDS_KEY = "GDSkey";

  /**
   * Wind flag
   * @deprecated  use Grib2Tables.VectorComponentFlag
   */
  public static final String WIND_FLAG = "Winds";

  /**
   * number of points in X direction (columns)
   */
  public static final String NX = "Nx";

  /**
   * number of points in Y direction (rows)
   */
  public static final String NY = "Ny";

  /**
   * distance in X direction
   */
  public static final String DX = "Dx";

  /**
   * distance in Y direction
   */
  public static final String DY = "Dy";

  /**
   * resolution
   */
  public static final String RESOLUTION = "ResCompFlag";

  /**
   * resolution
   */
  public static final String VECTOR_COMPONENT_FLAG = "VectorComponentFlag";

  /**
   * first lat
   */
  public static final String LATIN = "Latin";

  /**
   * first lat
   */
  public static final String LATIN1 = "Latin1";

  /**
   * second lat
   */
  public static final String LATIN2 = "Latin2";

  /**
   * La1
   */
  public static final String LA1 = "La1";

  /**
   * Lo1
   */
  public static final String LO1 = "Lo1";

  /**
   * La2
   */
  public static final String LA2 = "La2";

  /**
   * Lo2
   */
  public static final String LO2 = "Lo2";

  /**
   * LoD
   */
  public static final String LAD = "LaD";

  /**
   * LoV
   */
  public static final String LOV = "LoV";

  /**
   * Lap
   */
  public static final String LAP = "Lap";

  /**
   * Lop
   */
  public static final String LOP = "Lop";

  /**
   * pLat
   */
  public static final String PLAT = "pLat";

  /**
   * pLon
   */
  public static final String PLON = "pLon";

  /**
   * SpLat
   */
  public static final String SPLAT = "SpLat";

  /**
   * SpLon
   */
  public static final String SPLON = "SpLon";

  /**
   * RotationAngle
   */
  public static final String ROTATIONANGLE = "RotationAngle";

  /**
   * StretchingFactor
   */
  public static final String STRETCHINGFACTOR = "StretchingFactor";

  /**
   * Angle
   */
  public static final String ANGLE = "Angle";

  /**
   * BasicAngle
   */
  public static final String BASICANGLE = "BasicAngle";

  /**
   * Xp
   */
  public static final String XP = "Xp";

  /**
   * Yp
   */
  public static final String YP = "Yp";

  /**
   * Xo
   */
  public static final String XO = "Xo";

  /**
   * Yo
   */
  public static final String YO = "Yo";

  /**
   * Np
   */
  public static final String NP = "Np";

  /**
   * Nr
   */
  public static final String NR = "Nr";

  /**
   * J
   */
  public static final String J = "J";

  /**
   * K
   */
  public static final String K = "K";

  /**
   * M
   */
  public static final String M = "M";

  /**
   * NumberParallels
   */
  public static final String NUMBERPARALLELS = "NumberParallels";

  /**
   * MethodNorm
   */
  public static final String METHODNORM = "MethodNorm";

  /**
   * ModeOrder
   */
  public static final String MODEORDER = "ModeOrder";
  /**
   * PROJ
   */
  public static final String PROJ = "ProjFlag";

  /**
   * North pole PROJ
   */
  public static final String NPPROJ = "NpProj";

  /**
   * grid type
   */
  public static final String GRID_TYPE = "grid_type";

  /**
   * grid name
   */
  public static final String GRID_NAME = "grid_name";

  /**
   * GRID_SHAPE_CODE
   */
  public static final String GRID_SHAPE_CODE = "grid_shape_code";

  /**
   * GRID_SHAPE
   */
  public static final String GRID_SHAPE = "grid_shape";

  /**
   * Radius of spherical earth
   */
  public static final String RADIUS_SPHERICAL_EARTH =
      "grid_radius_spherical_earth";

  /**
   * major axis of earth
   */
  public static final String MAJOR_AXIS_EARTH = "grid_major_axis_earth";

  /**
   * minor axis of earth
   */
  public static final String MINOR_AXIS_EARTH = "grid_minor_axis_earth";

  /**
   * Quasi
   */
  public static final String QUASI = "Quasi";

  /**
   * grid spacing units (DX, DY)
   */
  public static final String GRID_UNITS = "grid_units";

  /**
   * Scanning mode for the data
   */
  public static final String SCANNING_MODE = "scanning_mode";

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

  /**
   * A Map to hold grid parameter definitions of String
   */
  private final Map<String, String> paramStr = new HashMap<String, String>();

  /**
   * A Map to hold grid parameter definitions of Integer
   */
  private final Map<String, Integer> paramInt = new HashMap<String, Integer>();

  /**
   * A Map to hold grid parameter definitions of Double
   */
  private final Map<String, Double> paramDbl = new HashMap<String, Double>();

  /**
   * String of all parameters with their values
   */
  private String paramsValues;

  /*
  * These and other projection type int/double vars are store in the
  * approprite MAPs  paramInt and paramDble for quick access without
  * conversion except for the first request. Use routines:
  * getInt( String ) and getDouble( String )
  *
  * public String gdsKey, winds;
  * public int grid_type, nx, ny, resolution;
  * public double dx, dy;
  * public double latin1, latin2, La1, Lo1, LaD, LoV;
  * public int grid_shape_code;
  * public double radius_spherical_earth, major_axis_earth, minor_axis_earth;
  */

  /**
   * constructors.
   */
  public GridDefRecord() {
    paramsValues = "";
  }

  public GridDefRecord(String paramsValues) {
    this.paramsValues = paramsValues;
    String[] split = paramsValues.split("\\t");
    for (int i = 0; i < split.length; i += 2) {
      paramStr.put(split[i], split[i + 1]);
    }
  }

  /**
   * adds a param and value.
   *
   * @param key   name of the param
   * @param value of the param
   */
  public final void addParam(String key, String value) {
    //System.out.println(" adding " + key + " = " + value);
    paramStr.put(key.trim(), value);
    paramsValues = paramsValues +"\t"+ key +"\t"+ value;
  }

  /**
   * adds a param and value.
   *
   * @param key   name of the param
   * @param value of the param
   */
  public final void addParam(String key, int value) {
    //System.out.println(" adding " + key + " = " + value);
    paramInt.put(key, new Integer(value));
    paramStr.put(key, Integer.toString(value));
  }

  /**
   * adds a param and value.
   *
   * @param key   name of the param
   * @param value of the param
   */
  public final void addParam(String key, Integer value) {
    //System.out.println(" adding " + key + " = " + value);
    paramInt.put(key, value);
  }

  /**
   * adds a param and value.
   *
   * @param key   name of the param
   * @param value of the param
   */
  public final void addParam(String key, float value) {
    //System.out.println(" adding " + key + " = " + value);
    paramDbl.put(key, new Double (value));
    paramStr.put(key, Float.toString(value));
  }

  /**
   * adds a param and value.
   *
   * @param key   name of the param
   * @param value of the param
   */
  public final void addParam(String key, double value) {
    //System.out.println(" adding " + key + " = " + value);
    paramDbl.put(key, new Double (value));
    paramStr.put(key, Double.toString(value));
  }

  /**
   * adds a param and value.
   *
   * @param key   name of the param
   * @param value of the param
   */
  public final void addParam(String key, Double value) {
    //System.out.println(" adding " + key + " = " + value);
    paramDbl.put(key, value);
  }

  /**
   * gets a param and value.
   *
   * @param key name of the param
   * @return the value or null
   */
  public final String getParam(String key) {
    String value = paramStr.get(key);
    if(value == null) { // check the dbl and int tables
        Double result = paramDbl.get(key);
        if (result != null) {
            value = result.toString();
        } else {
            Integer intResult = paramInt.get(key);
            if (intResult != null) {
                value = intResult.toString();
            }
        }
        // save it back off to the string table for next time
        if (value != null) {
            paramStr.put(key, value);
        }
    }
    if (debug && value == null) {
        System.out.println( key +" value not found");
    }
    return value;
  }

  /*
  * Only converts the String param to a int once, then stores it as a
  * Integer in paramInt Map for future accesses
  * @deprecated use getInt()
  */
  public final int getParamInt(String key) {
    return getInt(key);
  }

  public final int getInt(String key) {
    Integer result = paramInt.get(key);
    if (result != null) {
      return result.intValue();
    } else {
      String value = paramStr.get(key);
      if (value != null) {
        try {
          result = new Integer(value.trim());
        } catch (NumberFormatException e) {
          // number might be written as int like 65.0
          double dvalue = getDouble( key );
          if ( ! Double.isNaN(dvalue )) {
            result = new Integer( (int) dvalue );
            paramInt.put(key, result);
            return result.intValue();
          }
          e.printStackTrace();
          if( debug )
            System.out.println( key +" cannot be convert to Int");
          return UNDEFINED;
        }
        paramInt.put(key, result);
        return result.intValue();
      } else {
        if( debug )
          System.out.println( key +" value not found");
        return UNDEFINED;
      }
    }
  }

  /*
  * Only converts the String param to a double once, then stores it as a
  * Double in paramDbl Map for future accesses
  * @deprecated use getDouble()
  */
  public final double getParamDouble(String key) {
    return getDouble(key);
  }

  public final double getDouble(String key) {
    Double result = paramDbl.get(key);
    if (result != null) {
      return result.doubleValue();
    } else {
      String value = paramStr.get(key);
      if (value != null) {
        try {
          result = new Double(value.trim());
        } catch (NumberFormatException e) {
          e.printStackTrace();
          if( debug )
            System.out.println( key +" cannot be convert to Double");
          return Double.NaN;
        }
        paramDbl.put(key, result);
        return result.doubleValue();
      } else {
        if( debug )
          System.out.println( key +" value not found");
        return Double.NaN;
      }
    }
  }

  /**
   * get the hcs as a String of params values
   * @return the hcs as a String of params values
   */
  public String getParamsValues() {
    return paramsValues;
  }

  /**
   * Get a short name for this GDSKey for the netCDF group.
   * Subclasses should implement as a short description
   *
   * @return short name
   */
  public abstract String getGroupName();

  /**
   * get the keySet
   *
   * @return the set of keys
   */
  public final java.util.Set<String> getKeys() {
    return paramStr.keySet();
  }

  /**
   * returns the value of the param.
   *
   * @param name param name
   * @return value, or NaN if value doest exist
   * @deprecated use getParamDouble(String key)
   */
  public final double readDouble(String name) {
    return getParamDouble(name);
  }

  /**
   * Compare GridDefRecords, the numerics will use nearlyEquals so values that
   * differ in 3 or 4th decimal places will return equal. This is being coded
   * because the NDFD model dx differ in the 3 decimal place otherwise equal.
   */
  public static boolean compare( GridDefRecord local, GridDefRecord other ) {
    java.util.Set<String> keys = local.getKeys();
    java.util.Set<String> okeys = other.getKeys();
    if( keys.size() != okeys.size() )
      return false;

    for( String key : keys ) {
      if( key.equals(WIND_FLAG) || key.equals(RESOLUTION) || key.equals(VECTOR_COMPONENT_FLAG)
        || key.equals(GDS_KEY))
        continue;

      String val = local.getParam( key );
      String oval = other.getParam( key );   //

      if( val.matches( "^[0-9]+\\.[0-9]*")) {
        //double
        double d = local.getDouble( key );
        double od = other.getDouble( key );
        if( ! Misc.nearlyEquals(d, od) )
          return false;
      } else if( val.matches( "^[0-9]+")) {
        // int
        if( !val.equals( oval ))
          return false;
      } else {
        // String
        if( !val.equals( oval ))
          return false;
      }
    }
    return true;
  }

}


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

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.GregorianCalendar;

import ucar.nc2.iosp.bufr.*;
import ucar.unidata.io.RandomAccessFile;
import ucar.unidata.io.InMemoryRandomAccessFile;

/**
 * Recieves data from an InputStream, breaks into BUFR messages by scanning for "BUFR" header.
 * Multithreaded:
 * main thread for reading input stream, and breaking into messages
 * seperate thread for processing messages (MessageDispatch)
 * seperate thread pool (using executor) for writing output.
 * seperate thread for indexing messsage (Indexer)
 *

 * Processing steps:

 *
 the main thread reads from InputStream, finds message boundaries, places messaages onto the message Queue.
 *
 messProcessor thread waits on message Queue, sends messages to the MessageDispatch, which decides
 * what should be done with a message (ignore, check bad bits, write to file). Messages to be written are
 * placed on a queue in a MessageWriter, which controls all access to a single file.
 * The MessageWriter task is submitted to the completionService.
 * So no IO is done in this thread.
 *
 The completionService uses threads from the ExecutorService to call the MessageWriter to append messages
 * to the file.
 *
 indexProcessor waits for Futures from the MessageWriter. These contain the info to write indices for
 * some of the bufr message types.
 *

 *

 * TDB: close MessageWriter periodically. sync indexer. delete old messages, and their indices.
 *
 * @author caron
 * @since Aug 8, 2008
 */
public class MessageBroker {
  private static final ucar.unidata.io.KMPMatch matcher = new ucar.unidata.io.KMPMatch("BUFR".getBytes(CDM.utf8Charset));
  private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MessageBroker.class);

  private ExecutorService executor;
  private BlockingQueue<Future<IndexerTask>> completionQ; // = new ArrayBlockingQueue<Future<IndexerTask>>(1000); // unbounded, threadsafe
  private CompletionService<IndexerTask> completionService;

  private Thread messProcessor;
  private Thread indexProcessor;
  private ArrayBlockingQueue<MessageTask> messQ = new ArrayBlockingQueue<MessageTask>(1000); // unbounded, threadsafe

  private MessageDispatchDDS dispatch;

  int bad_msgs = 0;
  int total_msgs = 0;

  public MessageBroker(ExecutorService executor, BlockingQueue<Future<IndexerTask>> blockingQueue,
          CompletionService<IndexerTask> completionService, MessageDispatchDDS dispatcher) throws IOException {

    this.executor = executor;
    this.completionQ = blockingQueue;
    this.completionService = completionService; // completionService manages Callable objects that write bufr message to files
    this.dispatch = dispatcher;

    //a thread for processing messages as they come off the wire
    messProcessor = new Thread(new MessageProcessor());

    // a thread for indexing messages after they have been written
    indexProcessor = new Thread(new IndexProcessor());

    // start up the threads
    messProcessor.start();
    indexProcessor.start();
  }

  public void exit() throws IOException {
    try {
      Thread.currentThread().sleep(1000); // wait a sec
      } catch (InterruptedException e) {
      }

    System.out.println("On exit 1: messageQ size = "+messQ.size());
    while (messQ.peek() != null) {
      try {
        Thread.currentThread().sleep(1000); // wait a sec
      } catch (InterruptedException e) {
        System.out.println("Thread interrupted messQ size = "+messQ.size());
        break; // ok get out of here
      }
    }
    System.out.println("On exit 2: messageQ size = "+messQ.size());
    // shut down the message processor
    messProcessor.interrupt();

    // wait for all io to be complete
    System.out.println("On exit 3: completionQ size = "+completionQ.size());
    while (completionQ.peek() != null) {
      try {
        Thread.currentThread().sleep(1000); // wait a sec
      } catch (InterruptedException e) {
        System.out.println("Thread interrupted messQ size = "+completionQ.size());
        break; // ok get out of here
      }
    }
    System.out.println("On exit 4: completionQ size = "+completionQ.size());

    shutdownAndAwaitTermination( executor);

    // shut down the index processor
    indexProcessor.interrupt();

    // shut down the indexers
    dispatch.exit();
  }

  // from javadoc
  private void shutdownAndAwaitTermination(ExecutorService pool) {
    pool.shutdown(); // Disable new tasks from being submitted
    try {
      // Wait a while for existing tasks to terminate
      if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
        pool.shutdownNow(); // Cancel currently executing tasks
        // Wait a while for tasks to respond to being cancelled
        if (!pool.awaitTermination(60, TimeUnit.SECONDS))
            System.err.println("Pool did not terminate");
      }
    } catch (InterruptedException ie) {
      // (Re-)Cancel if current thread also interrupted
      pool.shutdownNow();
      // Preserve interrupt status
      Thread.currentThread().interrupt();
    }
    System.out.println("Pool terminated");
  }

  //////////////////////////////////////////////////////
  // Step 4 - optionally write an index

  private class IndexProcessor implements Runnable {
    private boolean cancel = false;

    public void run() {
      while (true) {
        try {
          Future<IndexerTask> f = completionService.poll(); // see if ones ready
          if (f == null) {
            if (cancel) break; // check if interrupted
            f = completionService.take(); // block until ready
          }

          IndexerTask itask = f.get();
          itask.process(); // write index

        } catch (InterruptedException e) {
          cancel = true;

        } catch (Exception e) {
          logger.error("MessageBroker.IndexProcessor ", e);
        }
      }
      System.out.println("exit IndexProcessor");
    }
  }

  //////////////////////////////////////////////////////////////////
  // Step 2 - dispatch message

  private class MessageProcessor implements Runnable {
    private boolean cancel = false;

    public void run() {
      while (true) {
        try {
          MessageTask mtask = messQ.poll(); // see if ones ready
          if (mtask == null) {
            if (cancel) break; // check if interrupted
            mtask = messQ.take(); // block until ready
          }
          process(mtask);

        } catch (InterruptedException e) {
          cancel = true;
        }
      }

      System.out.println("exit MessageProcessor");
    }

    void process(MessageTask mtask) {
      //out.format("    %d start process %n", mtask.id);
      try {
        Message m = getMessage(new InMemoryRandomAccessFile("BUFR", mtask.mess));
        if (null == m) return;
        m.setHeader(mtask.header);
        m.setRawBytes(mtask.mess);

        // decide what to do with the message
        // Step 3 (write to file) is done in the dispatcher
        dispatch.dispatch(m);

      } catch (IOException e) {
        e.printStackTrace();
      }
    }

    public Message getMessage(RandomAccessFile raf) throws IOException {
      raf.seek(4);
      BufrIndicatorSection is = new BufrIndicatorSection(raf);
      BufrIdentificationSection ids = new BufrIdentificationSection(raf, is);
      BufrDataDescriptionSection dds = new BufrDataDescriptionSection(raf);

      long dataPos = raf.getFilePointer();
      int dataLength = BufrNumbers.uint3(raf);
      BufrDataSection dataSection = new BufrDataSection(dataPos, dataLength);

      if ((is.getBufrEdition() > 4) || (is.getBufrEdition() < 2)) {
        System.out.println("Edition " + is.getBufrEdition() + " is not supported");
        bad_msgs++;
        return null;
      }

      GregorianCalendar cal = new GregorianCalendar(); // ??
      return new Message(raf, is, ids, dds, dataSection, cal);
    }
  }

  private static AtomicInteger seqId = new AtomicInteger();

  private class MessageTask {
    byte[] mess;
    int len, have;
    int id;
    String header;

    MessageTask(int messlen) {
      this.len = messlen;
      this.mess = new byte[messlen];
      this.have = 0;
      this.id = seqId.getAndIncrement();
    }

  }

  //////////////////////////////////////////////////////
  // Step 1 - read and extract a Bufr Message

  public void process(InputStream is) throws IOException {
    int pos = -1;
    Buffer b = null;

    while (true) {
      b = (pos < 0) ? readBuffer(is) : readBuffer(is, b, pos);
      pos = process(b, is);
      if (b.done) break;
    }
  }

  // return where in the buffer we got to.
  private int process(Buffer b, InputStream is) throws IOException {
    int start = 0;
    while (start < b.have) {
      int matchPos = matcher.indexOf(b.buff, start, b.have - start);

      // didnt find "BUFR" match
      if (matchPos < 0) {
        if (start == 0) // discard all but last 3 bytes
          return b.have - 3;
        else
          return start; // indicates part of the buffer thats not processed
      }

      // do we have the length already read ??
      if (matchPos + 6 >= b.have) {
        return start; // this will save the end of the buffer and read more in.
      }

      // read BUFR message length
      int b1 = (b.buff[matchPos + 4] & 0xff);
      int b2 = (b.buff[matchPos + 5] & 0xff);
      int b3 = (b.buff[matchPos + 6] & 0xff);
      int messLen = b1 << 16 | b2 << 8 | b3;
      // System.out.println("match at=" + matchPos + " len= " + messLen);

      // create a task for this message
      //int headerLen = matchPos - start;
      MessageTask task = new MessageTask(messLen);
      task.header = extractHeader(start, matchPos, b);

      // copy message bytes into it
      int last = matchPos + messLen;
      if (last > b.have) {
        task.have = b.have - matchPos;
        System.arraycopy(b.buff, matchPos, task.mess, 0, task.have);

        // read the rest of the message
        if (!readBuffer(is, task.mess, task.have, task.len - task.have)) {
          System.out.println("Failed to read remaining BUFR message");
          break;
        }

      } else {
        task.have = task.len;
        System.arraycopy(b.buff, matchPos, task.mess, 0, task.have);
      }

      boolean ok = true;

      // check on ending
      for (int i = task.len - 4; i < task.len; i++) {
        int bb = task.mess[i];
        if (bb != 55) {
          //System.out.println("Missing End of BUFR message at pos=" + i + " " + bb);
          ok = false;
          bad_msgs++;
        }
      }

      try {
        if (ok) messQ.put(task);
        total_msgs++;
        //System.out.println(" added message " + task.id + " start=" + matchPos + " end= " + (matchPos + messLen));
      } catch (InterruptedException e) {
        System.out.println(" interrupted queue put - assume process exit");
        break;
      }

      start = matchPos + messLen + 1;
    }

    return -1;
  }

  private String extractHeader(int startScan, int messagePos, Buffer buff) {
    int sizeHeader = messagePos - startScan;
    if (sizeHeader > 30) sizeHeader = 30;
    byte[] header = new byte[sizeHeader];
    int startHeader = messagePos - sizeHeader;
    System.arraycopy(buff.buff, startHeader, header, 0, sizeHeader);

    // cleanup
    int start;
    for (start = 0; start < header.length; start++) {
      byte b = header[start];
      if ((b == 73) || (b == 74)) // I or J
        break;
    }

    byte[] bb = new byte[sizeHeader];
    int count = 0;
    for (int i = start; i = 32 && b < 127) // ascii only
        bb[count++] = b;
    }
    return new String(bb, 0, count);
  }

  private boolean showRead = false;

  // read into dest byte array, until buffer is full or end of stream
  private boolean readBuffer(InputStream is, byte[] dest, int start, int want) throws IOException {
    int done = 0;
    while (done < want) {
      int got = is.read(dest, start + done, want - done);
      if (got < 0)
        return false;
      done += got;
    }

    if (showRead) System.out.println("Read buffer at " + bytesRead + " len=" + done);
    bytesRead += done;
    return true;
  }

  // read into new Buffer, until buffer is full or end of stream
  private Buffer readBuffer(InputStream is) throws IOException {
    Buffer b = new Buffer();
    int want = BUFFSIZE;
    while (b.have < want) {
      int got = is.read(b.buff, b.have, want - b.have);
      if (got < 0) {
        b.done = true;
        break;
      }
      b.have += got;
    }
    if (showRead) System.out.println("Read buffer at " + bytesRead + " len=" + b.have);
    bytesRead += b.have;
    return b;
  }

  // read into new Buffer, until buffer is full or end of stream
  private Buffer readBuffer(InputStream is, Buffer prev, int pos) throws IOException {
    Buffer b = new Buffer();

    // copy remains of last buffer here
    int remain = prev.have - pos;
    //if (remain > BUFFSIZE /2)
    //  out.format(" remain = "+remain+" bytesRead="+bytesRead);

    System.arraycopy(prev.buff, pos, b.buff, 0, remain);
    b.have = remain;

    int want = BUFFSIZE;
    while (b.have < want) {
      int got = is.read(b.buff, b.have, want - b.have);
      if (got < 0) {
        b.done = true;
        break;
      }
      b.have += got;
    }

    if (showRead) System.out.println("Read buffer at " + bytesRead + " len=" + b.have);
    bytesRead += b.have;
    return b;
  }

  private long bytesRead = 0;
  private int BUFFSIZE = 15000;

  private class Buffer {
    byte[] buff;
    int have;
    boolean done;

    Buffer() {
      buff = new byte[BUFFSIZE];
      have = 0;
      done = false;
    }
  }

}