package nxm.sys.lib;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import nxm.sys.inc.MidasReference;
import nxm.sys.inc.Tablizable;

/** Supports the reading/writing of Java Network Launching Protocol (JNLP) files.
    <br>
    See JNLP File Syntax at<br>
      http://docs.oracle.com/javase/8/docs/technotes/guides/javaws/developersguide/syntax.html
    <br><br>
    See JSR-56: Java Network Launching Protocol and API at<br>
      https://jcp.org/aboutJava/communityprocess/maintenance/jsr056/index7.html
    <br>
    See JSR-56 Version 6.0.10 change log, is a proper superset of version 1.0 and of version 6.0<br>
      https://jcp.org/aboutJava/communityprocess/maintenance/jsr056/56-MR4.html
    <br>
    See JSR-56 Version 6.0.18 change log, is a proper superset of version 1.0 and of version 6.0.10<br>
      https://jcp.org/aboutJava/communityprocess/maintenance/jsr056/jnlp-6.0.18.html
    <br>
    See JSR-56 Version 7.0 change log, is a proper superset of version 1.0 and of version 6.0.18<br>
      https://jcp.org/aboutJava/communityprocess/maintenance/jsr056/jnlp-7_0-changes.html
    <br><br>
    NOTE: as of 2014-07-31, the proposed changed to JNLP Version 8.2 (8.20?) has not been accepted.
    @author  J. Czechowski IV
    @since NeXtMidas 3.3.0
 */
public class JnlpFile extends XmlFile implements Tablizable {
  /** JNLP specification version {@value}. */
  public static final String SPEC_1_0 = "1.0+";
  /** JNLP specification version {@value}. */
  public static final String SPEC_1_5 = "1.5+";
  /** JNLP specification version {@value}. */
  public static final String SPEC_6_0 = "6.0+";
  /** JNLP specification version {@value}. */
  public static final String SPEC_7_0 = "7.0+";

  /** JNLP specification version "1.0". */
  public static final int VERSION_1_0    = encodeVersion(1, 0, 0);
  /** JNLP specification version "1.5". */
  public static final int VERSION_1_5    = encodeVersion(1, 5, 0);
  /** JNLP specification version "6". */
  public static final int VERSION_6      = encodeVersion(6, 0, 0);
  /** JNLP specification version "6.0.10". */
  public static final int VERSION_6_0_10 = encodeVersion(6, 0, 10);
  /** JNLP specification version "6.0.18". */
  public static final int VERSION_6_0_18 = encodeVersion(6, 0, 18);
  /** JNLP specification version "7". */
  public static final int VERSION_7      = encodeVersion(7, 0, 0);

  /** A list of fully supported versions.
      Tags and attributes of the following versions are fully supported.
      Tags and attributes from future versions will be ignored.
   */
  public static final int[] SUPPORTED_VERSIONS = {
    VERSION_1_0,
    VERSION_1_5,
    VERSION_6,
    VERSION_6_0_10,
    VERSION_6_0_18,
    VERSION_7,
  };

  /** The latest JNLP specification version supported by this class. This is
   *  currently equal to {@link #VERSION_7} but may change in future versions.
   */
  public static final int VERSION_LATEST = VERSION_7;

  /** The JNLP specification version supported by this class that is suggested
   *  for use in situations where maximum-compatibility is desired. This is
   *  currently equal to {@link #VERSION_1_0} but may change in future
   *  versions.
   */
  public static final int VERSION_COMPATIBLE = VERSION_1_0;

  /** Download mode, JNLP 6.0.18 added progress */
  public static enum JnlpDownload { /** eager */ eager,
                                    /** lazy */ lazy,
                                    /** progress */ progress;
    /** parse JNLP download
     *
     *  @param s value to parse
     *  @return JnlpDownload
     */
    public static JnlpDownload parse (String s) {
      return (s == null)? null : valueOf(s);
    }
  };

  /** JNLP update check (or 'when to check?'). (JNLP 6.0) */
  public static enum JnlpUpdateCheck { /** always */ always,
                                       /** timeout */ timeout,
                                       /** background */ background;
    /** parse JNLP update policy
    *
    *  @param s policy to parse
    *  @return JnlpUpdatePolicy
    */
    public static JnlpUpdateCheck parse (String s) {
      return (s == null)? null : valueOf(s);
    }
  };

  /** JNLP update policy. */
  public static enum JnlpUpdatePolicy { /** always */ always,
                                        /** prompt update */ prompt_update,
                                        /** prompt run */ prompt_run;
    @Override public String toString () { return super.name().replace('_', '-'); }
    /** parse JNLP update policy
     *
     *  @param s policy to parse
     *  @return JnlpUpdatePolicy
     */
    public static JnlpUpdatePolicy parse (String s) {
      return (s == null)? null : valueOf(s);
    }
  }

  /** JNLP icon kinds. */
  public static enum JnlpIconKind { /** splash */ splash;
    /** parse JNLP Icon Kind
     *
     *  @param s value to parse
     *  @return JnlpIconKind
     */
    public static JnlpIconKind parse (String s) {
      return (s == null)? null : valueOf(s);
    }
  };

  /** JNLP description kinds. Note that "brief" acts as a stand-in for "short"
   *  since "short" is a reserved word in Java.
   */
  public static enum JnlpDescriptionKind { /** one line */ one_line,
                                           /** brief */ brief,
                                           /** tool tip */ tooltip;
    @Override
    public String toString () {
      if (this == brief) return "short";
      return super.name().replace('_', '-');
    }
    /** parse JNLP Description Kind
     *
     *  @param s value to parse
     *  @return JnlpDescriptionKind
     */
    public static JnlpDescriptionKind parse (String s) {
      if ("".equals("short")) return brief;
      return (s == null)? null : valueOf(s);
    }
  };

  protected       double                        seek;              // seek position
  protected       Integer                       ver;               // spec version
  protected       String                        spec;              // spec=     attribute
  protected       String                        version;           // version=  attribute
  protected       String                        codebase;          // codebase= attribute
  protected       String                        href;              // href=     attribute
  protected final NonEmptyList<JnlpInformation> jnlpInformation;   // <information> tag (1+)
  protected       boolean                       allPermissions;    // <security><all-permissions/>
  protected       boolean                       clientPermissions; // <security><all-permissions/>
  protected       JnlpUpdateCheck               updateCheck;       // <update check=?>
  protected       JnlpUpdatePolicy              updatePolicy;      // <update policy=?>
  protected final List<JnlpResources>           jnlpResources;     // <resource>    tag (0+)
  protected       JnlpDescElement               jnlpDesc;          // <*-desc>      tag (1)

  /** Initialize new instance. */
  public JnlpFile () {
    this.jnlpInformation = new NonEmptyList<>(new JnlpInformation("","",""));
    this.jnlpResources   = new LinkedList<>();
    this.jnlpDesc        = new JnlpComponentDesc();
  }

  /** Initialize new instance with filename.
      @param fileName The name of the file to use.
   */
  public JnlpFile (String fileName) {
    this();
    init(null,fileName);
  }

  /** Initialize new instance with reference and filename.
      @param ref      The reference to use.
      @param fileName The name of the file to use.
      @deprecated since NeXtMidas 3.5.0 use {@link #JnlpFile(MidasReference, Object)}
   */
  @Deprecated
  public JnlpFile (Object ref, Object fileName) {
    this();
    init(Convert.ref2Midas(ref), fileName);
  }

  /** Initialize new instance with reference and filename.
      @param ref      The Midas reference to use.
      @param fileName The name of the file to use.
      @since NeXtMidas 3.5.0
   */
  public JnlpFile (MidasReference ref, Object fileName) {
    this();
    init(ref,fileName);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Version number encoding/decoding
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** Converts version identifier to specification version. */
  private String ver2spec (int ver) {
    int major  = (ver / 1000000);
    int minor  = (ver /    1000) % 1000;
    return ""+major+"."+minor+"+";
  }

  /** Converts version identifier to specification version. */
  private String ver2string (int ver) {
    int major  = (ver / 1000000);
    int minor  = (ver /    1000) % 1000;
    int update = (ver          ) % 1000;
    return ""+major+"."+minor+"."+update;
  }

  /** Converts specification version to version identifier. */
  private int spec2ver (String spec, boolean latestUpdate) {
    if (spec.endsWith("+")) spec = spec.substring(0, spec.length()-1);

    // Parse major, minor and update. Logic for "update" Since 3.5.1 for BUG 2846
    int major  = 0;
    int minor  = 0;
    int update = 0;
    try {
      String[] token = spec.split("[.]");
      major  = Integer.parseInt(token[0]);
      if ( token.length > 1) {
        minor  = Integer.parseInt(token[1]);
        if ( token.length > 2) {
          update  = Integer.parseInt(token[2]);
        }
      }
    } catch (NumberFormatException e ) {
      // If malformed specification version then default to version 1.0
      warning("JNLP file has malformed specification version = " + spec + ". Using Version 1.0");
      major=1;
    }
    int ver    = encodeVersion(major, minor, update);

    if (latestUpdate && (ver == VERSION_6)) ver = VERSION_6_0_18;

    return ver;
  }

  /** Encodes a version number. */
  private static int encodeVersion (int major, int minor, int update) {
    if (major <= 0) {
      throw new IllegalArgumentException("Unexpected major version: "+major);
    }
    if ((minor < 0) || (minor > 1000)) {
      throw new IllegalArgumentException("Unexpected minor version: "+minor);
    }
    if ((update < 0) || (update > 1000)) {
      throw new IllegalArgumentException("Unexpected update version: "+update);
    }
    return (major * 1000000)
         + (minor *    1000)
         + (update         );
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods overridden from BaseFile
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Since there is only one JNLP definition per file, the seek position will
   *  always be forced to be 0.0 or 1.0;
   */
  @Override
  public int seek (double val) {
    seek = (seek >= 1.0)? 1.0 : 0.0;
    return 0;
  }

  /** Since there is only one JNLP definition per file, this will always be
   *  0.0 (start of file) or 1.0 (end of file).
   */
  @Override public double seek () { return seek; }

  /** Number of available JNLP definitions to read. This will always be 1 or 0. */
  @Override public double avail () { return getSize() - seek(); }

  /** There is always one JNLP definition per file. */
  @Override public double getSize () { return 1; }

  /** The preferred JNLP mime-type ("application/x-java-jnlp-file"). */
  @Override public String getMimeType () { return "application/x-java-jnlp-file"; }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + (allPermissions ? 1231 : 1237);
    result = prime * result + (clientPermissions ? 1231 : 1237);
    result = prime * result + ((codebase == null) ? 0 : codebase.hashCode());
    result = prime * result + ((href == null) ? 0 : href.hashCode());
    result = prime * result + ((jnlpDesc == null) ? 0 : jnlpDesc.hashCode());
    result = prime * result + ((jnlpInformation == null) ? 0 : jnlpInformation.hashCode());
    result = prime * result + ((jnlpResources == null) ? 0 : jnlpResources.hashCode());
    long temp;
    temp = Double.doubleToLongBits(seek);
    result = prime * result + (int) (temp ^ (temp >>> 32));
    result = prime * result + ((spec == null) ? 0 : spec.hashCode());
    result = prime * result + ((updateCheck == null) ? 0 : updateCheck.hashCode());
    result = prime * result + ((updatePolicy == null) ? 0 : updatePolicy.hashCode());
    result = prime * result + ((ver == null) ? 0 : ver.hashCode());
    result = prime * result + ((version == null) ? 0 : version.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object inobj, int flags) {
    if (flags == EQ_BIN)
      return equalsBinary(inobj);
    return equals(inobj);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (getClass() != obj.getClass())
      return false;
    JnlpFile other = (JnlpFile) obj;
    this.open();
    other.open();
    if (allPermissions != other.allPermissions)
      return false;
    if (clientPermissions != other.clientPermissions)
      return false;
    if (codebase == null) {
      if (other.codebase != null)
        return false;
      }
    else if (!codebase.equals(other.codebase))
      return false;
    if (href == null) {
      if (other.href != null)
        return false;
      }
    else if (!href.equals(other.href))
      return false;
    if (jnlpDesc == null) {
      if (other.jnlpDesc != null)
        return false;
      }
    else if (!jnlpDesc.equals(other.jnlpDesc))
      return false;
    if (jnlpInformation == null) {
      if (other.jnlpInformation != null)
        return false;
      }
    else if (!jnlpInformation.equals(other.jnlpInformation))
      return false;
    if (jnlpResources == null) {
      if (other.jnlpResources != null)
        return false;
      }
    else if (!jnlpResources.equals(other.jnlpResources))
      return false;
    if (Double.doubleToLongBits(seek) != Double.doubleToLongBits(other.seek))
      return false;
    if (spec == null) {
      if (other.spec != null)
        return false;
      }
    else if (!spec.equals(other.spec))
      return false;
    if (updateCheck != other.updateCheck)
      return false;
    if (updatePolicy != other.updatePolicy)
      return false;
    if (ver == null) {
      if (other.ver != null)
        return false;
      }
    else if (!ver.equals(other.ver))
      return false;
    if (version == null) {
      if (other.version != null)
        return false;
      }
    else if (!version.equals(other.version))
      return false;
    return true;
  }

  /** Test for binary equivalence of two JnlpFiles.
      @param inobj The file to compare to this one.
      @return <code>true</code> if the given file is equal to this file based
              on the specified equality tests, <code>false</code> otherwise.
      @since NeXtMidas 3.5.1
  */
  public boolean equalsBinary (Object inobj) {
    if (this == inobj) {
      return true;
    }
    if (inobj == null) {
      return false;
    }
    boolean theSame = false;
    if (inobj.getClass() == getClass()) {
      // cannot use JnlpFile since getSize() and avail(), etc. returns 1
      BaseFile bf1 = new BaseFile(M, getFileName());
      BaseFile bf2 = new BaseFile(M, ((JnlpFile) inobj).getFileName());

      flags = EQ_BIN|EQ_SIZE;
      return bf1.equalsBF(bf2, flags);
    }
    return theSame;
  }

  @Override
  public String toString () { return "JnlpFile : " + getURL(); }

  @Override
  public boolean open() {
    if (super.open()) {
      if (isInput) readFile();
      setInternals();
    }
    return isOpen;
  }

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

  @Override
  public void update () {
    flush();
  }

  @Override
  public void flush() {
    if (isOutput && isOpen) {
      CharSequence xml = toXML();
      byte[]       buf = xml.toString().getBytes();

      super.seek(0.0);
      super.write(buf, 0, buf.length);
    }
  }

  @Override
  public String listHeader () {
    StringBuilder str = new StringBuilder(256);
    str.append("JnlpFile    : ").append(getURL()).append("\n");
    return str.toString();
  }

  @Override
  public String listElements (double start, int elem, String format, int flags) {
    return "\n"+toXML();
  }

  @Override
  public int listElementsPerLine (int lineWidth, String format, int flags) {
    return 1;
  }

  /** Provides an XML representation of the file.
   *  @return XML representation of the file */
  public CharSequence toXML () {
    int _ver = getVer();
    StringBuilder xml = new StringBuilder(4096);
    xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
    appendOpenTag(xml, "", "jnlp", "spec",     getSpec(),
                                   "version",  getVersion(),
                                   "codebase", getCodebase(),
                                   "href",     getHref());
    for (JnlpInformation info : getInformation()) {
      info.appendXML(xml, _ver);
    }
    if (isAllPermissions() || isClientPermissions()) {
      xml.append("  <security>\n");
      if (isAllPermissions()   ) xml.append("    <all-permissions/>\n");
      if (isClientPermissions()) xml.append("    <j2ee-application-client-permissions/>\n");
      xml.append("  </security>\n");
    }
    if (_ver >= VERSION_6) {
      if ((getUpdateCheck() != null) || (getUpdatePolicy() != null)) {
        appendOpenCloseTag(xml, "  ", "update", "check",  getUpdateCheck(),
                                                "policy", getUpdatePolicy());
      }
    }
    for (JnlpResources resource : getResources()) {
      resource.appendXML(xml, _ver);
    }
    getDesc().appendXML(xml, _ver);
    xml.append("</jnlp>\n");

    return xml;
  }

  protected byte[] readAll () {
    seek(0.0);
    byte[] buf     = new byte[32768];
    int    numRead = super.read(buf, 0, buf.length);
    int    boff    = 0;

    while (numRead >= 0) {
      boff += numRead;

      int toRead = buf.length - boff;
      if (toRead < 4096) {
        Arrays.copyOf(buf, buf.length + 32768);
        toRead = buf.length - boff;
      }
      numRead = super.read(buf, boff, toRead);
    }
    return Arrays.copyOf(buf, boff);
  }

  /** Reads and parses the file. */
  protected void readFile () {
    // READ THE ENTIRE FILE IN
    seek(0.0);
    byte[] buf     = new byte[32768];
    int    numRead = super.read(buf, 0, buf.length);
    int    length  = 0;

    while (numRead >= 0) {
      length += numRead;

      int toRead = buf.length - length;
      if (toRead < 4096) {
        Arrays.copyOf(buf, buf.length + 32768);
        toRead = buf.length - length;
      }
      numRead = super.read(buf, length, toRead);
    }

    // PARSE THE FILE
    String            xml     = new String(buf, 0, length);
    XmlToTableHandler handler = new XmlToTableHandler("#", "#CDATA#", 0, true, Table.LHM);

    parseXmlFast(xml, handler);

    fromTable(handler.getTable());
  }

  /** Creates a table based on the file. */
  @Override
  public Table toTable () {
    XmlToTableHandler handler = new XmlToTableHandler("#", "#CDATA#", 0, true, Table.LHM);

    parseXmlFast(toXML().toString(), handler);

    return handler.getTable();
  }

  /** Builds the file based on a table.
   *  @param tbl table from which to build the file
   */
  public void fromTable (Table tbl) {
    if (tbl.containsKey("SPEC"    )) setSpec(    tbl.getS("SPEC"    ));
    if (tbl.containsKey("VERSION" )) setVersion( tbl.getS("VERSION" ));
    if (tbl.containsKey("CODEBASE")) setCodebase(tbl.getS("CODEBASE"));
    if (tbl.containsKey("HREF"    )) setHref(    tbl.getS("HREF"    ));

    getInformation().clear(null);
    for (Table t : getTables(tbl, "INFORMATION")) {
      getInformation().add(new JnlpInformation(t));
    }
    getInformation().remove(null);

    if (tbl.containsKey("SECURITY")) {
      Table t = tbl.getTable("SECURITY");
      setAllPermissions(   t.containsKey("ALL-PERMISSIONS"));
      setClientPermissions(t.containsKey("J2EE-APPLICATION-CLIENT-PERMISSIONS"));
    }

    if (tbl.containsKey("UPDATE")) {
      Table t = tbl.getTable("UPDATE");
      setUpdateCheck( t.getS("CHECK" ));
      setUpdatePolicy(t.getS("POLICY"));
    }

    getResources().clear();
    for (Table t : getTables(tbl, "RESOURCES")) {
      getResources().add(new JnlpResources(t));
    }

    if (tbl.containsKey("APPLICATION-DESC")) setDesc(new JnlpApplicationDesc(tbl.getTable("APPLICATION-DESC")));
    if (tbl.containsKey("COMPONENT-DESC"  )) setDesc(new JnlpComponentDesc(tbl.getTable("COMPONENT-DESC")));
    if (tbl.containsKey("INSTALLER-DESC"  )) setDesc(new JnlpInstallerDesc(tbl.getTable("INSTALLER-DESC")));
  }

  /** The versions of the JNLP specification this JNLP file works with. This
   *  is effectively an indication of the version of the JNLP XML schema used.
   *  @return he versions of the JNLP specification this JNLP file works with
   */
  public final String getSpec () {
    return ver2spec(getVer());
  }

  /** The versions of the JNLP specification this JNLP file works with. This
   *  is effectively an indication of the version of the JNLP XML schema used. <br>
   *  <br>
   *  If multiple updates of a given version are available, this will assume
   *  the latest update, which follows the examples provided by Oracle for use
   *  of version "6.0+" where some features are technically listed as "6.0.18".
   *  @param spec the versions of the JNLP specification this JNLP file works with
   */
  public final void setSpec (String spec) {
    setVer(spec2ver(spec, true));
  }

  /** The spec attribute (see {@link #getSpec()} in encoded form.
   *  @return spec attribute */
  public int getVer () {
    return (ver == null)? VERSION_COMPATIBLE : ver;
  }

  /** The spec attribute (see {@link #setSpec(String)} in encoded form.
   *  Setting this to null will cause {@link #VERSION_COMPATIBLE} to be used.
   *  @param ver spec attribute
   */
  public void setVer (Integer ver) {
    if (ver == null) {
      this.ver = null;
    }
    else {
      if (Arrays.binarySearch(SUPPORTED_VERSIONS, ver) < 0) {
        warning("Specification version " + ver2string(ver) + " (or " + ver2spec(ver) + ") is not fully supported");
      }
      this.ver  = ver;
    }
  }

  /** The version of the application being launched, as well as the version of
   *  the JNLP file itself. This differs from {@link #getVer} in that this
   *  specifies the version of the content, and not the version of the XML
   *  schema used for the JNLP file.
   *  @return version of the application being launched
   */
  public String getVersion () { return version; }
  /** The version of the application being launched, as well as the version of
   *  the JNLP file itself. This differs from {@link #getVer} in that this
   *  specifies the version of the content, and not the version of the XML
   *  schema used for the JNLP file.
   *  @param version version of the application being launched
   */
  public void setVersion (String version) {
    this.version = version;
  }

  /** The codebase specifies base URL for all relative URLs in href attributes.
   *  @return base URL for all relative URLs in href attributes. */
  public String getCodebase () { return codebase; }

  /** The codebase specifies base URL for all relative URLs in href attributes.
   *  @param codebase base URL for all relative URLs in href attributes. */
  public void setCodebase (String codebase) {
    this.codebase = FileUtil.terminatePath(codebase);
  }

  /** The href contains the location of the JNLP file as a URL. By default this
   *  will be the basic name of the file (e.g. "myfile.jnlp") on the assumption
   *  that the file will be placed immediately below the codebase.
   *  @return location of the JNLP file as URL
   */
  public String getHref () { return (href != null)? href : getName().getBasename(); }

  /** The href contains the location of the JNLP file as a URL.
   *  @param href location of the JNLP file as URL */
  public void setHref (String href) { this.href = href; }

  /** Gets the information block(s) of which there is typically only one.
   *  @return information block(s) */
  public NonEmptyList<JnlpInformation> getInformation () { return jnlpInformation; }

  /** Gets the first information block.
   *  @return first information block */
  public JnlpInformation getInfo () { return getInformation().get(0); }

  /** Gets the information resources.
   *  @return information resources */
  public List<JnlpResources> getResources () { return jnlpResources; }

  /** Does the application need full access the the local system and network?
   *  @return true if full access needed */
  public boolean isAllPermissions () { return allPermissions; }
  /** Does the application need full access the the local system and network?
   *  @param all true if full access needed */
  public void setAllPermissions (boolean all) { this.allPermissions = all; }

  /** Does the application need the set of permissions defined for a J2EE
   *  application client?
   *  @return true if the application needs permissions defined
   */
  public boolean isClientPermissions () { return clientPermissions; }
  /** Does the application need the set of permissions defined for a J2EE
   *  application client?
   *  @param client true if the application needs permissions defined
   */
  public void setClientPermissions (boolean client) { this.clientPermissions = client; }

  /** The application's preference for when to check for updates.
   *  @return preference for when to check for updates */
  public JnlpUpdateCheck getUpdateCheck () { return updateCheck; }
  /** The application's preference for when to check for updates.
   *  @param check preference for when to check for updates */
  public void setUpdateCheck (JnlpUpdateCheck check) { this.updateCheck = check; }
  /** The application's preference for when to check for updates.
   *  @param check preference on when to check for updates */
  public final void setUpdateCheck (String check) {
    setUpdateCheck(JnlpUpdateCheck.parse(check));
  }
  /** The application's preference for what to do when updates are available.
   *  @return update policy */
  public JnlpUpdatePolicy getUpdatePolicy () { return updatePolicy; }
  /** The application's preference for what to do when updates are available.
   *  @param policy update policy */
  public void setUpdatePolicy (JnlpUpdatePolicy policy) { this.updatePolicy = policy; }
  /** The application's preference for what to do when updates are available.
   *  @param policy update policy */
  public final void setUpdatePolicy (String policy) {
    setUpdatePolicy(JnlpUpdatePolicy.parse(policy));
  }

  /** Gets the description element.
   *  @return element description */
  public JnlpDescElement getDesc () { return jnlpDesc; }
  /** Sets the description element.
   *  @param desc element description*/
  public void setDesc (JnlpDescElement desc) { jnlpDesc = desc; }

  //////////////////////////////////////////////////////////////////////////////
  // XML Utility Functions
  //////////////////////////////////////////////////////////////////////////////

  /** Special table.getTable(..) that converts a non-table into a table with a
   *  #CDATA# entry.
   */
  private static Table _getTable (Table tbl, String key) {
    Table t = tbl.getTable(key);
    if (t == null) {
      t = new Table();
      t.put("#CDATA#", tbl.getS(key));
    }
    return t;
  }

  /** Gets all table entries associated with the given key as tables. */
  private static List<Table> getTables (Table tbl, String key) {
    List<Table> all = new LinkedList<>();
    int         i   = 0;
    String      k   = key + "#" + (i++);

    if (tbl.containsKey(key)) {
      all.add(_getTable(tbl, key));
    }

    while (tbl.containsKey(k)) {
      all.add(_getTable(tbl, k));
      k = key + "#" + (i++);
    }
    return all;
  }

  /** Gets all table entries associated with the given key as Strings. */
  private static List<String> getStrings (Table tbl, String key) {
    List<String> all = new LinkedList<>();
    int          i   = 0;
    String       k   = key + "#" + (i++);
    if (tbl.containsKey(key)) {
      all.add(tbl.getS(key));
    }

    while (tbl.containsKey(k)) {
      all.add(tbl.getS(k));
      k = key + "#" + (i++);
    }
    return all;
  }

  /** Internal use only: Used by appendOpenCloseTag(..) and appendOpenTag(..). */
  private static StringBuilder _appendOpenTag (StringBuilder xml, String prefix,
                                               String name, String close,
                                               Object ... tagValue) {
    StringBuilder subPrefix = new StringBuilder();
    for (int i = 0; i < (prefix.length() + name.length() + 1); i++) {
      subPrefix.append(' ');
    }
    boolean first = true;

    xml.append(prefix).append('<').append(name);
    for (int i = 0; i < tagValue.length; i+=2) {
      if (tagValue[i+1] != null) {
        if (!first) xml.append('\n').append(subPrefix);
        xml.append(' ').append(tagValue[ i ]).append("=\"")
                       .append(tagValue[i+1]).append('\"');
        first = false;
      }
    }
    xml.append(close);
    return xml;
  }

  /** Appends an XML open/close tag.
   *  @param xml       The xml to append to (also the return value).
   *  @param prefix    The prefix (this will be some number of spaces indicating
   *                   the indentation level).
   *  @param name      The name of the tag.
   *  @param tagValue  The tag=value pairs to include whenever the value is not
   *                   equal to null.
   */
  private static StringBuilder appendOpenCloseTag (StringBuilder xml, String prefix,
                                                   String name, Object ... tagValue) {
    return _appendOpenTag(xml, prefix, name, "/>\n", tagValue);
  }

  /** Appends an XML open tag.
   *  @param xml       The xml to append to (also the return value).
   *  @param prefix    The prefix (this will be some number of spaces indicating
   *                   the indentation level).
   *  @param name      The name of the tag.
   *  @param tagValue  The tag=value pairs to include whenever the value is not
   *                   equal to null.
   */
  private static StringBuilder appendOpenTag (StringBuilder xml, String prefix,
                                              String name, Object ... tagValue) {
    return _appendOpenTag(xml, prefix, name, ">\n", tagValue);
  }

  /** Appends an XML element if the value is not null. */
  private static StringBuilder appendElement (StringBuilder xml, String prefix,
                                              Object val, String suffix) {
    if (val != null) xml.append(prefix).append(val).append(suffix);
    return xml;
  }

  //////////////////////////////////////////////////////////////////////////////
  // Nested Element Classes
  //////////////////////////////////////////////////////////////////////////////
  /** An element in the JNLP file with special handling. */
  public static abstract class JnlpElement {
    JnlpElement () { } // package-private

    /** Appends the XML content for the element to the given buffer and then
     *  returns that same buffer.
     *  @param xml XML content
     *  @param ver version
     *  @return buffer with XML content appended
     */
    public abstract StringBuilder appendXML (StringBuilder xml, int ver);

    @Override
    public String toString () {
      return appendXML(new StringBuilder(), VERSION_LATEST).toString();
    }
  }

  /** An element in the JNLP file with special handling. */
  public static abstract class JnlpElement2 extends JnlpElement {
    private String       href;
    private String       version;
    private Integer      size;

    JnlpElement2 (String href) { // package-private
      setHref(href);
    }

    @SuppressWarnings("all")
    JnlpElement2 (Table tbl) {
      setHref(      tbl.getS("HREF"   ));
      setVersion(   tbl.getS("VERSION"));

      if (tbl.containsKey("SIZE")) setSize(tbl.getL("SIZE"));
    }

    /** The URL to a location on the web containing the resource.
     *  @return URL to a location on the web containing the resource. */
    public final String getHref () { return href; }
    /** The URL to a location on the web containing the resource.
     *  @param href URL to a location on the web containing the resource. */
    public final void setHref (String href) {
      if (href == null) throw new NullPointerException("HREF can not be null.");
      this.href = href;
    }

    /** A  string describing the version of the image that is requested.
     *  @return version of the image that is requested */
    public String getVersion () { return version; }
    /** A  string describing the version of the image that is requested.
     *  @param version version of the image that is requested */
    public void setVersion (String version) { this.version = version; }

    /** The size of the resource file in octets (for reference).
     *  @return size of the resource file */
    protected Integer getSize () { return size; }
    /** The size of the resource file in octets (for reference).
     *  @param size size of the resource file */
    protected void setSize (Integer size) { this.size = size; }
    /** The size of the resource file in octets (for reference).
     *  @param size size of the resource file */
    protected void setSize (Number size) {
      this.size = (size == null)? null : size.intValue();
    }
  }

  /** List of extensions to associate. */
  public static abstract class DescribedJnlpElement extends JnlpElement {
    private String               description;
    private JnlpDescriptionKind  descriptionKind;
    private JnlpIcon             icon;

    /** Creates a new instance. */
    DescribedJnlpElement () {
      // done
    }
    /** Creates a new instance. */
    @SuppressWarnings("all")
    DescribedJnlpElement (Table tbl) {
      if (tbl.containsKey("DESCRIPTION")) {
        if (tbl.containsKey("DESCRIPTION.#CDATA#")) {
          setDescription(tbl.getS("DESCRIPTION.#CDATA#"),
                         tbl.getS("DESCRIPTION.KIND"));
        }
        else {
          setDescription(tbl.getS("DESCRIPTION"));
        }
      }
      if (tbl.containsKey("ICON")) {
        setIcon(new JnlpIcon(tbl.getTable("ICON")));
      }
    }

    /** A description of the association.
     *  @return description of association */
    public String getDescription () { return description; }
    /** A description of the association.
     *  @param descr description of association */
    public void setDescription (String descr) { this.description = descr; }
    /** A description of the association and its kind.
     *  @param descr description of association
     *  @param kind  kind of the description */
    public void setDescription (String descr, JnlpDescriptionKind kind) {
      this.description     = descr;
      this.descriptionKind = kind;
    }
    /** The kind of the description of the association.
     *  @return kind of the description */
    public JnlpDescriptionKind getDescriptionKind () { return descriptionKind; }
    /** The kind of the description of the association.
     *  @param kind kind of the description */
    public void setDescriptionKind (JnlpDescriptionKind kind) { this.descriptionKind = kind; }
    /** A description of the association and its kind.
     *  @param descr description of association
     *  @param kind  kind of the description */
    public final void setDescription (String descr, String kind) {
      setDescription(descr, JnlpDescriptionKind.parse(kind));
    }
    /** The kind of the description of the association.
     *  @param kind the kind of description */
    public final void setDescriptionKind (String kind) {
      setDescriptionKind(JnlpDescriptionKind.parse(kind));
    }

    /** The associated icon.
     *  @return the associated icon */
    public JnlpIcon getIcon () { return icon; }
    /** The associated icon.
     *  @param icon the associated icon */
    public void setIcon (JnlpIcon icon) { this.icon = icon; }
  }

  /** An information block. */
  public static final class JnlpInformation extends JnlpElement {
    private       String                   os;                 // os=           (JNLP 1.5)
    private       String                   arch;               // arch=         (JNLP 1.5)
    private       String                   platform;           // platform=     (JNLP 1.5)
    private       String                   locale;             // locale=       (JNLP 1.5)
    private       String                   title;              // <title>
    private       String                   vendor;             // <vendor>
    private       String                   homepage;           // <homepage href=?>
    private       String                   description;        // <description>
    private       String                   descriptionOneLine; // <description kind='one-line'>
    private       String                   descriptionShort;   // <description kind='short'>
    private       String                   descriptionTooltip; // <description kind='tooltip'>
    private final List<JnlpIcon>           icons;              // <icon>
    private       boolean                  offlineAllowed;     // <offline-allowed>
    private       Boolean                  shortcutOnline;     // <shortcut online=?>  (JNLP 1.5)
    private       Boolean                  shortcutInstall;    // <shortcut install=?> (JNLP 7.0)
    private       boolean                  shortcutDesktop;    // <shortcut><desktop/> (JNLP 1.5)
    private       boolean                  shortcutMenu;       // <shortcut><menu/>    (JNLP 1.5)
    private       String                   shortcutSubMenu;    // <shortcut><menu submenu=?/> (JNLP 1.5)
    private final List<JnlpAssociation>    associations;       // <association>        (JNLP 1.5)
    private final List<JnlpRelatedContent> relatedContent;     // <related-content>    (JNLP 1.5)

    /** Creates a new instance with the three required fields.
     *  @param title    The title    (see {@link #setTitle}).
     *  @param vendor   The vendor   (see {@link #setVendor}).
     *  @param homepage The homepage (see {@link #setHomepage}).
     */
    public JnlpInformation (String title, String vendor, String homepage) {
      this.icons          = new LinkedList<>();
      this.associations   = new LinkedList<>();
      this.relatedContent = new LinkedList<>();

      setTitle(title);
      setVendor(vendor);
      setHomepage(homepage);
    }

    /** Creates a new instance with the three required fields.
     *  @param title    The title    (see {@link #setTitle}).
     *  @param vendor   The vendor   (see {@link #setVendor}).
     *  @param homepage The homepage (see {@link #setHomepage}).
     */
    JnlpInformation (Table tbl) {
      this(tbl.getS("TITLE"),
           tbl.getS("VENDOR"),
           tbl.getS("HOMEPAGE.HREF"));
      setOS(      tbl.getS("OS"      ));
      setArch(    tbl.getS("ARCH"    ));
      setPlatform(tbl.getS("PLATFORM"));
      setLocale(  tbl.getS("LOCALE"  ));
      setDescription(null);
      setDescriptionShort(null);
      setDescriptionOneLine(null);
      setDescriptionTooltip(null);
      setOfflineAllowed(tbl.containsKey("OFFLINE-ALLOWED"));
      setShortcutOnline(null);
      setShortcutDesktop(false);
      setShortcutMenu(false);
      setShortcutSubMenu(null);

      for (Table t : getTables(tbl, "DESCRIPTION")) {
        String kind  = t.getS("KIND",    null);
        String cdata = t.getS("#CDATA#", "");
             if (kind == null)            setDescription(cdata);
        else if (kind.equals("short"   )) setDescriptionShort(cdata);
        else if (kind.equals("one-line")) setDescriptionOneLine(cdata);
        else if (kind.equals("tooltip" )) setDescriptionTooltip(cdata);
      }

      Table s = tbl.getTable("SHORTCUT");          // JNLP 1.5
      if (s != null) {
        setShortcutOnline(s.getState("ONLINE"));
        setShortcutInstall(s.getState("INSTALL")); // JNLP 7.0
        setShortcutDesktop(s.containsKey("DESKTOP"));
        setShortcutMenu(s.containsKey("MENU"));
        setShortcutSubMenu(s.getS("MENU.SUBMENU"));
      }

      getIcons().clear();
      for (Table t : getTables(tbl, "ICON")) {
        getIcons().add(new JnlpIcon(t));
      }

      getAssociations().clear();
      for (Table t : getTables(tbl, "ASSOCIATION")) {
        getAssociations().add(new JnlpAssociation(t));
      }
      getRelatedContent().clear();
      for (Table t : getTables(tbl, "RELATED-CONTENT")) {
        getRelatedContent().add(new JnlpRelatedContent(t));
      }
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      if (ver >= VERSION_1_5) {
        appendOpenTag(xml, "  ", "information", "os",       getOS(),
                                                "arch",     getArch(),
                                                "platform", getPlatform(),
                                                "locale",   getLocale());
      }
      else {
        appendOpenTag(xml, "  ", "information");
      }
      appendElement(xml, "    <title>",                       getTitle(),              "</title>\n");
      appendElement(xml, "    <vendor>",                      getVendor(),             "</vendor>\n");
      appendElement(xml, "    <homepage href='",              getHomepage(),           "'/>\n");
      appendElement(xml, "    <description>",                 getDescription(),        "</description>\n");
      appendElement(xml, "    <description kind='one-line'>", getDescriptionOneLine(), "</description>\n");
      appendElement(xml, "    <description kind='short'>",    getDescriptionShort(),   "</description>\n");
      appendElement(xml, "    <description kind='tooltip'>",  getDescriptionTooltip(), "</description>\n");
      for (JnlpIcon i : getIcons()) {
        i.appendXML(xml, ver);
      }
      if (isOfflineAllowed()) {
        xml.append("    <offline-allowed/>\n");
      }
      if (ver >= VERSION_1_5) {
        if (isShortcutDesktop() || isShortcutMenu()) {
          Boolean shortcutInstall = (ver >= VERSION_7) ? getShortcutInstall() : null; // JNLP 7.0
          appendOpenTag(xml, "    ", "shortcut", "online", getShortcutOnline(), "install", shortcutInstall);
          if (isShortcutDesktop()) appendOpenCloseTag(xml, "      ", "desktop");
          if (isShortcutMenu()   ) appendOpenCloseTag(xml, "      ", "menu", "submenu", getShortcutSubMenu());
          xml.append("    </shortcut>\n");
        }
        for (JnlpAssociation a : getAssociations()) {
          a.appendXML(xml, ver);
        }
        for (JnlpRelatedContent r : getRelatedContent()) {
          r.appendXML(xml, ver);
        }
      }
      xml.append("  </information>\n");
      return xml;
    }

    /** The operating system this element should be considered. Omitting this
     *  (i.e. setting it to null) indicates it applies to all operating systems.
     *  @return the operation system this element applies to
     */
    public String getOS () { return os; }
    /** The operating system this element should be considered. Omitting this
     *  (i.e. setting it to null) indicates it applies to all operating systems.
     *  @param os the operating system this element applies to
     */
    public void setOS (String os) { this.os = os; }

    /** The architecture this element should be considered ("x86", "x86_64",
     *  etc). Omitting this (i.e. setting it to null) indicates it applies
     *  to all architectures.
     *  @return the architecture applies to
     */
    public String getArch () { return arch; }
    /** The architecture this element should be considered ("x86", "x86_64",
     *  etc). Omitting this (i.e. setting it to null) indicates it applies
     *  to all architecture.
     *  @param arch the architecture this element applies to
     */
    public void setArch (String arch) { this.arch = arch; }

    /** The platform this element should be considered. Omitting this (i.e.
     *  setting it to null) indicates it applies to all platforms.
     *  @return the platform this element applies to
     */
    public String getPlatform () { return platform; }
    /** The platform this element should be considered. Omitting this (i.e.
     *  setting it to null) indicates it applies to all platforms.
     *  @param platform the platform this element applies to
     */
    public void setPlatform (String platform) { this.platform = platform; }

    /** The locale this element should be considered. Omitting this (i.e.
     *  setting it to null) indicates it applies to all locales.
     *  @return the locale this element applies to
     */
    public String getLocale () { return locale; }
    /** The locale this element should be considered. Omitting this (i.e.
     *  setting it to null) indicates it applies to all locales.
     *  @param locale the locale this element applies to
     */
    public void setLocale (String locale) { this.locale = locale; }

    /** The title of the application.
     *  @return the title of the application */
    public String getTitle () {
      return title;
    }
    /** The title of the application.
     *  @param title the title of the application */
    public void setTitle (String title) {
      if (title == null) throw new NullPointerException("Title can not be null.");
      this.title = title;
    }

    /** The vendor of the application.
     *  @return the vendor of the application */
    public String getVendor () {
      return vendor;
    }
    /** The vendor of the application.
     *  @param vendor the vendor of the application */
    public void setVendor (String vendor) {
      if (vendor == null) throw new NullPointerException("Vendor can not be null.");
      this.vendor = vendor;
    }

    /** The homepage of the application.
     *  @return the homepage of the application */
    public String getHomepage () {
      return homepage;
    }
    /** The homepage of the application.
     *  @param homepage the homepage of the application */
    public void setHomepage (String homepage) {
      if (homepage == null) throw new NullPointerException("Homepage can not be null.");
      this.homepage = homepage;
    }

    /** The (default) description of the application.
     *  @return the default description of the application */
    public String getDescription () { return description; }
    /** The (default) description of the application.
     *  @param descr default description of the application */
    public void setDescription (String descr) { this.description = descr; }
    /** The one-line description of the application.
     *  @param descr one-line description of the application */
    public void setDescriptionOneLine (String descr) { this.descriptionOneLine = descr; }
    /** The one-line description of the application.
     *  @return one-line description of the application */
    public String getDescriptionOneLine () { return descriptionOneLine; }
    /** The "short" (one-paragraph) description of the application.
     *  @param descr short description of the application */
    public void setDescriptionShort (String descr) { this.descriptionShort = descr; }
    /** The "short" (one-paragraph) description of the application.
     *  @return short description of application */
    public String getDescriptionShort () { return descriptionShort; }
    /** The tooltip description of the application.
     *  @param descr tooltip description */
    public void setDescriptionTooltip (String descr) { this.descriptionTooltip = descr; }
    /** The tooltip description of the application.
     *  @return the tooltip description */
    public String getDescriptionTooltip () { return descriptionTooltip; }

    /** The icons for the application.
     *  @return the icons for the application */
    public List<JnlpIcon> getIcons () { return icons; }
    /** The associations.
     *  @return the associations */
    public List<JnlpAssociation> getAssociations () { return associations; }
    /** The associations.
     *  @return the associations */
    public List<JnlpRelatedContent> getRelatedContent () { return relatedContent; }

    /** Can the application be launched offline?
     *  @return true if application can be launched online */
    public boolean isOfflineAllowed () { return offlineAllowed; }
    /** Can the application be launched offline
     *  @param allowed true if application can be launched online */
    public void setOfflineAllowed (boolean allowed) { this.offlineAllowed = allowed; }

    /** Launch shortcut online?
     *  @return shortcut online */
    public boolean isShortcutOnline () { return shortcutOnline; }
    /** Launch shortcut online?
     *  @param online shortcut online */
    public void setShortcutOnline (boolean online) { this.shortcutOnline = online; }

    /** Is a shortcut on the desktop desired?
     *  @return true if shortcut on desktop is desired */
    public boolean isShortcutDesktop () { return shortcutDesktop; }
    /** Is a shortcut on the desktop desired?
     *  @param desktop true if shortcut on the desktop is desired */
    public void setShortcutDesktop (boolean desktop) { this.shortcutDesktop = desktop; }
    /** Is a shortcut in the menu desired?
     *  @return true if shortcut in the menu is desired */
    public boolean isShortcutMenu () { return shortcutMenu; }
    /** Is a shortcut in the menu desired?
     *  @param menu true if shortcut in menu is desired */
    public void setShortcutMenu (boolean menu) { this.shortcutMenu = menu; }
    /** Is a shortcut in the submenu desired?
     *  @return true if shortcut submenu desired */
    public String getShortcutSubMenu () { return shortcutSubMenu; }
    /** Is a shortcut in the submenu desired?
     *  @param submenu true if shortcut submenu desired */
    public void setShortcutSubMenu (String submenu) { this.shortcutSubMenu = submenu; }
    /** Does the application prefer to launch online (true) or offline (false)?
     *  @return true if prefer to launch online */
    public Boolean getShortcutOnline () { return shortcutOnline; }
    /** Does the application prefer to launch online (true) or offline (false)?
     * @param online true if prefer to launch online */
    public void setShortcutOnline (Boolean online) { this.shortcutOnline = online; }
    /** Does the application prefer for being consider installed?
     *  @return true if prefer install */
    public Boolean getShortcutInstall () { return shortcutInstall; }
    /** Does the application prefer for being consider installed?
     *  @param val prefer install */
    public void setShortcutInstall (Boolean val) { this.shortcutInstall = val; }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((arch == null) ? 0 : arch.hashCode());
      result = prime * result + ((associations == null) ? 0 : associations.hashCode());
      result = prime * result + ((description == null) ? 0 : description.hashCode());
      result = prime * result + ((descriptionOneLine == null) ? 0 : descriptionOneLine.hashCode());
      result = prime * result + ((descriptionShort == null) ? 0 : descriptionShort.hashCode());
      result = prime * result + ((descriptionTooltip == null) ? 0 : descriptionTooltip.hashCode());
      result = prime * result + ((homepage == null) ? 0 : homepage.hashCode());
      result = prime * result + ((icons == null) ? 0 : icons.hashCode());
      result = prime * result + ((locale == null) ? 0 : locale.hashCode());
      result = prime * result + (offlineAllowed ? 1231 : 1237);
      result = prime * result + ((os == null) ? 0 : os.hashCode());
      result = prime * result + ((platform == null) ? 0 : platform.hashCode());
      result = prime * result + ((relatedContent == null) ? 0 : relatedContent.hashCode());
      result = prime * result + (shortcutDesktop ? 1231 : 1237);
      result = prime * result + (shortcutMenu ? 1231 : 1237);
      result = prime * result + ((shortcutOnline == null) ? 0 : shortcutOnline.hashCode());
      result = prime * result + ((shortcutInstall == null) ? 0 : shortcutInstall.hashCode());
      result = prime * result + ((shortcutSubMenu == null) ? 0 : shortcutSubMenu.hashCode());
      result = prime * result + ((title == null) ? 0 : title.hashCode());
      result = prime * result + ((vendor == null) ? 0 : vendor.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      JnlpInformation other = (JnlpInformation) obj;
      if (arch == null) {
        if (other.arch != null)
          return false;
      }
      else if (!arch.equals(other.arch))
        return false;
      if (associations == null) {
        if (other.associations != null)
          return false;
      }
      else if (!associations.equals(other.associations))
        return false;
      if (description == null) {
        if (other.description != null)
          return false;
      }
      else if (!description.equals(other.description))
        return false;
      if (descriptionOneLine == null) {
        if (other.descriptionOneLine != null)
          return false;
      }
      else if (!descriptionOneLine.equals(other.descriptionOneLine))
        return false;
      if (descriptionShort == null) {
        if (other.descriptionShort != null)
          return false;
      }
      else if (!descriptionShort.equals(other.descriptionShort))
        return false;
      if (descriptionTooltip == null) {
        if (other.descriptionTooltip != null)
          return false;
      }
      else if (!descriptionTooltip.equals(other.descriptionTooltip))
        return false;
      if (homepage == null) {
        if (other.homepage != null)
          return false;
      }
      else if (!homepage.equals(other.homepage))
        return false;
      if (icons == null) {
        if (other.icons != null)
          return false;
      }
      else if (!icons.equals(other.icons))
        return false;
      if (locale == null) {
        if (other.locale != null)
          return false;
      }
      else if (!locale.equals(other.locale))
        return false;
      if (offlineAllowed != other.offlineAllowed)
        return false;
      if (os == null) {
        if (other.os != null)
          return false;
      }
      else if (!os.equals(other.os))
        return false;
      if (platform == null) {
        if (other.platform != null)
          return false;
      }
      else if (!platform.equals(other.platform))
        return false;
      if (relatedContent == null) {
        if (other.relatedContent != null)
          return false;
      }
      else if (!relatedContent.equals(other.relatedContent))
        return false;
      if (shortcutDesktop != other.shortcutDesktop)
        return false;
      if (shortcutMenu != other.shortcutMenu)
        return false;
      if (shortcutOnline == null) {
        if (other.shortcutOnline != null)
          return false;
      }
      else if (!shortcutOnline.equals(other.shortcutOnline))
        return false;
      if (shortcutInstall == null) {
        if (other.shortcutInstall != null)
          return false;
      }
      else if (!shortcutInstall.equals(other.shortcutInstall))
        return false;
      if (shortcutSubMenu == null) {
        if (other.shortcutSubMenu != null)
          return false;
      }
      else if (!shortcutSubMenu.equals(other.shortcutSubMenu))
        return false;
      if (title == null) {
        if (other.title != null)
          return false;
      }
      else if (!title.equals(other.title))
        return false;
      if (vendor == null) {
        if (other.vendor != null)
          return false;
      }
      else if (!vendor.equals(other.vendor))
        return false;
      return true;
    }
  }

  /** A JNLP icon description. */
  public final static class JnlpIcon extends JnlpElement2 {
    private Integer      width;
    private Integer      height;
    private JnlpIconKind kind;
    private Integer      depth;

    /** Creates a new instance with the required values.
     *  @param href     The href.
     *  @param version  The version.
     */
    public JnlpIcon (String href, String version) {
      super(href);
      setVersion(version);
    }

    @SuppressWarnings("all")
    JnlpIcon (Table tbl) {
      super(tbl);
      setKind(tbl.getS("KIND"));
      if (tbl.containsKey("WIDTH" )) setWidth( tbl.getL("WIDTH" ));
      if (tbl.containsKey("HEIGHT")) setHeight(tbl.getL("HEIGHT"));
      if (tbl.containsKey("DEPTH" )) setDepth( tbl.getL("DEPTH" ));
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      return appendOpenCloseTag(xml, "        ", "icon", "href",    getHref(),
                                                         "version", getVersion(),
                                                         "width",   getWidth(),
                                                         "height",  getHeight(),
                                                         "kind",    getKind(),
                                                         "depth",   getDepth(),
                                                         "size",    getSize());
    }

    @Override
    public void setVersion (String version) {
      if (version == null) throw new NullPointerException("Version can not be null.");
      super.setVersion(version);
    }
    /** The size of the resource file in octets (for reference). */
    @Override public Integer getSize () { return super.getSize(); }
    /** The size of the resource file in octets (for reference). */
    @Override public void setSize (Integer size) { super.setSize(size); }

    /** Get the width of the image in pixels (for reference).
     *  @return the width of the image */
    public Integer getWidth () { return width; }
    /** Set the width of the image in pixels (for reference).
     *  @param width width of the image */
    public void setWidth (Integer width) { this.width = width; }

    /** Get the height of the image in pixels (for reference).
     *  @return the height of the image */
    public Integer getHeight () { return height; }
    /** Set the height of the image in pixels (for reference).
     *  @param height height of the icon */
    public void setHeight (Integer height) { this.height = height; }

    /** Get the kind of icon (null = normal).
     *  @return the kind of icon */
    public JnlpIconKind getKind () { return kind; }
    /** Set the kind of icon (null = normal).
     *  @param kind kind of icon */
    public void setKind (JnlpIconKind kind) { this.kind = kind; }
    /** Set the kind of icon (null = normal).
     *  @param kind String representation kind of icon */
    public void setKind (String kind) {
      setKind(JnlpIconKind.parse(kind));
    }

    /** The color depth of the image in bits-per-pixels (null = use default).
     *  Typical values are 8, 16, or 24.
     *  @return color depth
     */
    public Integer getDepth () { return depth; }
    /** The color depth of the image in bits-per-pixels (null = use default).
     *  Typical values are 8, 16, or 24.
     *  @param depth color depth
     */
    public void setDepth (Integer depth) { this.depth = depth; }
  }

  /** A description of required resources. */
  public final static class JnlpResources extends JnlpElement {
    private       String                 os;
    private       String                 arch;
    private       String                 locale;
    private final List<AbstractJnlpJ2SE> java        = new LinkedList<>();
    private final List<AbstractJnlpJar>  jars        = new LinkedList<>();
    private final List<JnlpExtension>    extensions  = new LinkedList<>();
    private final Map<String,String>     properties  = new LinkedHashMap<>();
    private final List<JnlpPackage>      packages    = new LinkedList<>();

    /** Creates a new instance. */
    public JnlpResources () {
      // done
    }

    /** Creates a new instance. */
    JnlpResources (Table tbl) { // package-private
      setOS(    tbl.getS("OS"    ));
      setArch(  tbl.getS("ARCH"  ));
      setLocale(tbl.getS("LOCALE"));

      for (Table t : getTables(tbl, "JAVA"     )) { getJava().add(new JnlpJava(t)); }
      for (Table t : getTables(tbl, "J2SE"     )) { getJava().add(new JnlpJ2SE(t)); }
      for (Table t : getTables(tbl, "JAR"      )) { getJars().add(new JnlpJar(t)); }
      for (Table t : getTables(tbl, "EXTENSION")) { getExtensions().add(new JnlpExtension(t)); }
      for (Table t : getTables(tbl, "PACKAGE"  )) { getPackages().add(new JnlpPackage(t)); }

      for (Table t : getTables(tbl, "PROPERTY")) {
        String name  = t.getS("NAME" );
        String value = t.getS("VALUE");
        getProperties().put(name, value);
      }
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      appendOpenTag(xml, "  ", "resources", "os",     getOS(),
                                            "arch",   getArch(),
                                            "locale", getLocale());
      for (AbstractJnlpJ2SE v : getJava()      ) { v.appendXML(xml, ver); }
      for (AbstractJnlpJar  v : getJars()      ) { v.appendXML(xml, ver); }
      for (JnlpPackage      v : getPackages()  ) { v.appendXML(xml, ver); }
      for (Map.Entry<String,String> e : properties.entrySet()) {
        appendOpenCloseTag(xml, "    ", "property", "name",  e.getKey(),
                                                    "value", e.getValue());
      }
      xml.append("  </resources>\n");
      return xml;
    }

    /** Get the operating system this resource definition applies to (null = ALL).
     *  @return the operating system */
    public String getOS () { return os; }
    /** Set the operating system this resource definition applies to (null = ALL).
     *  @param os operating system */
    public void setOS (String os) { this.os = os; }

    /** Get the system architecture this resource definition applies to (null = ALL).
     *  @return the system architecture */
    public String getArch () { return arch; }
    /** Set the system architecture this resource definition applies to (null = ALL).
     *  @param arch system architecture */
    public void setArch (String arch) { this.arch = arch; }

    /** Get the locale this resource definition applies to (null = ALL).
     * @return the locale this resource definition applies to */
    public String getLocale () { return locale; }
    /** Set the locale this resource definition applies to (null = ALL).
     *  @param locale the locale this resource definition applies to */
    public void setLocale (String locale) { this.locale = locale; }

    /** The JARs this resource depends on. This includes *both* JJAR files
     *  containing .class files and those containing native libraries.
     *  @return list of JARs this resource depends on
     */
    public List<AbstractJnlpJar> getJars () { return jars; }
    /** The JRE version (and optional resources) this resource depends on.
     *  @return the JRE version */
    public List<AbstractJnlpJ2SE> getJava () { return java; }
    /** Get the extensions this resource depends on.
     *  @return the extensions this resource depends on. */
    public List<JnlpExtension> getExtensions () { return extensions; }
    /** Get the properties this resource depends on.
     *  @return the properties this resource depends on */
    public Map<String,String> getProperties () { return properties; }
    /** Get the packages this resource depends on.
     *  @return the packages this resource depends on */
    public List<JnlpPackage> getPackages () { return packages; }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((arch == null) ? 0 : arch.hashCode());
      result = prime * result + ((extensions == null) ? 0 : extensions.hashCode());
      result = prime * result + ((jars == null) ? 0 : jars.hashCode());
      result = prime * result + ((java == null) ? 0 : java.hashCode());
      result = prime * result + ((locale == null) ? 0 : locale.hashCode());
      result = prime * result + ((os == null) ? 0 : os.hashCode());
      result = prime * result + ((packages == null) ? 0 : packages.hashCode());
      result = prime * result + ((properties == null) ? 0 : properties.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      JnlpResources other = (JnlpResources) obj;
      if (arch == null) {
        if (other.arch != null)
          return false;
      }
      else if (!arch.equals(other.arch))
        return false;
      if (extensions == null) {
        if (other.extensions != null)
          return false;
      }
      else if (!extensions.equals(other.extensions))
        return false;
      if (jars == null) {
        if (other.jars != null)
          return false;
      }
      else if (!jars.equals(other.jars))
        return false;
      if (java == null) {
        if (other.java != null)
          return false;
      }
      else if (!java.equals(other.java))
        return false;
      if (locale == null) {
        if (other.locale != null)
          return false;
      }
      else if (!locale.equals(other.locale))
        return false;
      if (os == null) {
        if (other.os != null)
          return false;
      }
      else if (!os.equals(other.os))
        return false;
      if (packages == null) {
        if (other.packages != null)
          return false;
      }
      else if (!packages.equals(other.packages))
        return false;
      if (properties == null) {
        if (other.properties != null)
          return false;
      }
      else if (!properties.equals(other.properties))
        return false;
      return true;
    }
  }

  /** A description of required J2SE resources. */
  public static class AbstractJnlpJ2SE extends JnlpElement {
    private final String        tag;
    private       String        version;
    private       String        href;
    private       String        initialHeapSize;
    private       String        maxHeapSize;
    private       String        vmArgs;
    private       JnlpResources resources;

    AbstractJnlpJ2SE (String tag) { // package-private
      this.tag = tag;
    }

    @SuppressWarnings("all")
    AbstractJnlpJ2SE (String tag, String ver, String initialHeap, String maxHeap) { // package-private
      this.tag = tag;
      setVersion(ver);
      setHref("http://java.sun.com/products/autodl/j2se");
      setInitialHeapSize(initialHeap);
      setMaxHeapSize(maxHeap);
    }

    @SuppressWarnings("all")
    AbstractJnlpJ2SE (String tag, Table tbl) { // package-private
      this(tag);
      setVersion(        tbl.getS("VERSION"          ));
      setHref(           tbl.getS("HREF"             ));
      setInitialHeapSize(tbl.getS("INITIAL-HEAP-SIZE"));
      setMaxHeapSize(    tbl.getS("MAX-HEAP-SIZE"    ));
      setVMArgs(         tbl.getS("VM-ARGS"          ));
      if (tbl.containsKey("RESOURCES")) {
        setResources(new JnlpResources(tbl.getTable("RESOURCES")));
      }
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      String _tag = tag;
      if ((ver < VERSION_6) && !tag.equals("j2se")) {
        _tag = "j2se";
      }
      appendOpenTag(xml, "    ", _tag, "version",           getVersion(),
                                       "href",              getHref(),
                                       "initial-heap-size", getInitialHeapSize(),
                                       "max-heap-size",     getMaxHeapSize(),
                                       "vm-args",           getVMArgs());
      if (getResources() != null) getResources().appendXML(xml, ver);
      xml.append("    </").append(tag).append(">\n");
      return xml;
    }

    /** Get the version(s) of the JRE this application is supported on (may end with
     *  a '+' to include later versions).
     *  @return the version of the JRE
     */
    public String getVersion () { return version; }
    /** Set the version(s) of the JRE this application is supported on (may end with
     *  a '+' to include later versions).
     *  @param version the version of the JRE
     */
    public void setVersion (String version) { this.version = version; }

    /** Get the URL for downloading the JRE from.
     *  @return the URL */
    public String getHref () { return href; }
    /** Set the URL for downloading the JRE from.
     *  @param href the URL for downloading */
    public void setHref (String href) { this.href = href; }

    /** Get the preferred initial heap size for the Java VM.
     *  @return preferred initial heap size */
    public String getInitialHeapSize () { return initialHeapSize; }
    /** Set the preferred initial heap size for the Java VM.
     *  @param initialHeapSize preferred initial heap size */
    public void setInitialHeapSize (String initialHeapSize) {this.initialHeapSize = initialHeapSize;}

    /** The preferred max heap size for the Java VM.
     *  @return maximum heap size */
    public String getMaxHeapSize () { return maxHeapSize; }
    /** The preferred max heap size for the Java VM
     *  @param maxHeapSize maximum heap size */
    public void setMaxHeapSize (String maxHeapSize) { this.maxHeapSize = maxHeapSize; }

    /** Get the preferred set of arguments to the Java VM.
     *  @return the preferred */
    public String getVMArgs () { return vmArgs; }
    /** Set the preferred set of arguments to the Java VM.
     *  @param vmArgs the preferred set of arguments */
    public void setVMArgs (String vmArgs) { this.vmArgs = vmArgs; }

    /** Get the resources used.
     *  @return the resources used */
    public JnlpResources getResources () { return resources; }
    /** Set the resources used.
     *  @param resources the resources */
    public void setResources (JnlpResources resources) { this.resources = resources; }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((href == null) ? 0 : href.hashCode());
      result = prime * result + ((initialHeapSize == null) ? 0 : initialHeapSize.hashCode());
      result = prime * result + ((maxHeapSize == null) ? 0 : maxHeapSize.hashCode());
      result = prime * result + ((resources == null) ? 0 : resources.hashCode());
      result = prime * result + ((tag == null) ? 0 : tag.hashCode());
      result = prime * result + ((version == null) ? 0 : version.hashCode());
      result = prime * result + ((vmArgs == null) ? 0 : vmArgs.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      AbstractJnlpJ2SE other = (AbstractJnlpJ2SE) obj;
      if (href == null) {
        if (other.href != null)
          return false;
      }
      else if (!href.equals(other.href))
        return false;
      if (initialHeapSize == null) {
        if (other.initialHeapSize != null)
          return false;
      }
      else if (!initialHeapSize.equals(other.initialHeapSize))
        return false;
      if (maxHeapSize == null) {
        if (other.maxHeapSize != null)
          return false;
      }
      else if (!maxHeapSize.equals(other.maxHeapSize))
        return false;
      if (resources == null) {
        if (other.resources != null)
          return false;
      }
      else if (!resources.equals(other.resources))
        return false;
      if (tag == null) {
        if (other.tag != null)
          return false;
      }
      else if (!tag.equals(other.tag))
        return false;
      if (version == null) {
        if (other.version != null)
          return false;
      }
      else if (!version.equals(other.version))
        return false;
      if (vmArgs == null) {
        if (other.vmArgs != null)
          return false;
      }
      else if (!vmArgs.equals(other.vmArgs))
        return false;
      return true;
    }
  }

  /** A description of required J2SE resources. */
  public final static class JnlpJ2SE extends AbstractJnlpJ2SE {
    /** JnlpJ2SE Default Constructor */
    public JnlpJ2SE () { super("j2se"); }
    JnlpJ2SE (Table tbl) { super("j2se", tbl); }

    /** Creates a new instance for the specified version.
     *  @param ver         version
     *  @param initialHeap initial heap
     *  @param maxHeap     maximum heap
     */
    public JnlpJ2SE (String ver, String initialHeap, String maxHeap) {
      super("j2se", ver, initialHeap, maxHeap);
    }
  }

  /** A description of required Java resources. Although this is technically
   *  legal in the JNLP specification (at least in version 6.0) nearly everyone
   *  - including the Oracle examples and documentation - use the old J2SE
   *  (see {@link JnlpJ2SE}) version.
   */
  public final static class JnlpJava extends AbstractJnlpJ2SE {
    /** JnlpJava Default Constructor */
    public JnlpJava () { super("java"); }
    JnlpJava (Table tbl) { super("java", tbl); }

    /** Creates a new instance for the specified version.
     *  @param ver         version
     *  @param initialHeap initial heap
     *  @param maxHeap     maximum heap
     */
    public JnlpJava (String ver, String initialHeap, String maxHeap) {
      super("java", ver, initialHeap, maxHeap);
    }
  }

  /** A JAR-based resource. */
  public static abstract class AbstractJnlpJar extends JnlpElement2 {
    private final String        tag;
    private       Boolean       main;
    private       JnlpDownload  download;
    private       String        part;

    protected AbstractJnlpJar (String tag, String href) {
      super(href);
      this.tag = tag;
    }

    @SuppressWarnings("all")
    AbstractJnlpJar (String tag, Table tbl) {
      super(tbl);
      this.tag = tag;

      if (tbl.containsKey("MAIN"    )) setMain(tbl.getState("MAIN"));
      if (tbl.containsKey("PART"    )) setPart(tbl.getS("PART"));
      if (tbl.containsKey("DOWNLOAD")) setDownload(tbl.getS("DOWNLOAD"));
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      JnlpDownload tmpDownload = getDownload();
      if ((tmpDownload == JnlpDownload.progress) && (ver < VERSION_6_0_18)) {
        tmpDownload = null; // ignore download=progress when desired JNLP version is < 6.0.18
      }
      return appendOpenCloseTag(xml, "    ", tag, "href",     getHref(),
                                                  "version",  getVersion(),
                                                  "main",     isMain(),
                                                  "download", tmpDownload,
                                                  "size",     getSize(),
                                                  "part",     getPart());
    }

    /** The size of the resource file in octets (for reference). */
    @Override public Integer getSize () { return super.getSize(); }
    /** The size of the resource file in octets (for reference). */
    @Override public void setSize (Integer size) { super.setSize(size); }

    /** <b>Internal use only:</b> Only applies to JAR.
     *  @return true if main */
    protected Boolean isMain () { return main; }
    /** <b>Internal use only:</b> Only applies to JAR.
     *  @param main true if main */
    protected void setMain (Boolean main) { this.main = main; }

    /** Should this element must be downloaded before an application is
     *  launched (eager), or not (lazy)?
     *  @return should this element be downloaded before launch
     */
    public JnlpDownload getDownload () { return download; }
    /** Should this element must be downloaded before an application is
     *  launched (eager), or not (lazy)?
     *  @param download if element must be downloaded before launch
     */
    public void setDownload (JnlpDownload download) { this.download = download; }
    /** Should this element must be downloaded before an application is
     *  launched (eager), or not (lazy)?
     *  @param download if element must be downloaded before launch
     */
    public final void setDownload (String download) {
      setDownload(JnlpDownload.parse(download));
    }

    /** The name of the group the JAR belongs to.
     *  @return the name of the group the JAR belongs to */
    public String getPart () { return part; }
    /** The name of the group the JAR belongs to.
     *  @param part the name of the group the JAR belongs tp
     */
    public void setPart (String part) { this.part = part; }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((download == null) ? 0 : download.hashCode());
      result = prime * result + ((main == null) ? 0 : main.hashCode());
      result = prime * result + ((part == null) ? 0 : part.hashCode());
      result = prime * result + ((tag == null) ? 0 : tag.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      AbstractJnlpJar other = (AbstractJnlpJar) obj;
      if (download != other.download)
        return false;
      if (main == null) {
        if (other.main != null)
          return false;
      }
      else if (!main.equals(other.main))
        return false;
      if (part == null) {
        if (other.part != null)
          return false;
      }
      else if (!part.equals(other.part))
        return false;
      if (tag == null) {
        if (other.tag != null)
          return false;
      }
      else if (!tag.equals(other.tag))
        return false;
      return true;
    }
  }

  /** A JAR resource containing Java .class files. */
  public static final class JnlpJar extends AbstractJnlpJar {
    /** Creates a new instance using the given resource URL.
     *  @param href URL from which to create a new instance */
    public JnlpJar (String href) {
      super("jar", href);
    }
    /** Creates a new instance using the given table.
     *  @param tbl table from which to creAte the new instance */
    JnlpJar (Table tbl) {
      super("jar", tbl);
    }

    /** Does this JAR contain the main class? */
    @Override public Boolean isMain () { return super.isMain(); }
    /** Does this JAR contain the main class? */
    @Override public void setMain (Boolean main) { super.setMain(main); }
  }

  /** A JAR resource containing native libraries files. */
  public static final class JnlpNativeLib extends AbstractJnlpJar {
    /** Creates a new instance using the given resource URL.
     *  @param href URL from which to creat a new instance */
    public JnlpNativeLib (String href) {
      super("nativelib", href);
    }
    /** Creates a new instance using the given table
     *  @param tbl table from which to create a new instance */
    JnlpNativeLib (Table tbl) {
      super("nativelib", tbl);
    }
  }

  /** JNLP Extension Class */
  public static final class JnlpExtension extends JnlpElement2 {
    private final List<JnlpExtDownload> extDownload = new LinkedList<>();
    private       String                name;

    /** Creates a new extension with the given URL.
     *  @param href URL from which to create a new extension */
    public JnlpExtension (String href) {
      super(href);
    }

    /** Creates a new extension with the given table.
     *  @param tbl table from which to create the new extension */
    JnlpExtension (Table tbl) {
      super(tbl);
      setName(tbl.getS("NAME"));

      for (Table t : getTables(tbl, "EXT-DOWNLOAD")) {
        getExtDownload().add(new JnlpExtDownload(t));
      }
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      appendOpenTag(xml, "        ", "extension", "version", getVersion(),
                                                  "name",    getName(),
                                                  "href",    getHref());
      for (JnlpExtDownload d : getExtDownload()) { d.appendXML(xml, ver); }
      xml.append("        </extension>\n");
      return xml;
    }

    /** The name of the extension.
     *  @return the name of the extension */
    public String getName () { return name; }
    /** The name of the extension.
     *  @param name the name of the extension */
    public void setName (String name) { this.name = name; }

    /** The extension downloads.
     *  @return the extension downloads */
    public List<JnlpExtDownload> getExtDownload () { return extDownload; }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((extDownload == null) ? 0 : extDownload.hashCode());
      result = prime * result + ((name == null) ? 0 : name.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      JnlpExtension other = (JnlpExtension) obj;
      if (extDownload == null) {
        if (other.extDownload != null)
          return false;
      }
      else if (!extDownload.equals(other.extDownload))
        return false;
      if (name == null) {
        if (other.name != null)
          return false;
      }
      else if (!name.equals(other.name))
        return false;
      return true;
    }
  } // end inner static final class JnlpExtension


  /** JNLP Ext Download Class */
  public static final class JnlpExtDownload extends JnlpElement {
    private String       extPart;
    private JnlpDownload download;
    private String       part;

    /** Creates a new instance for the given name.
     *  @param extPart name for which to create the instance
     */
    public JnlpExtDownload (String extPart) {
      setExtPart(extPart);
    }

    /** Creates a new instance for the given table.
     *  @param tbl table from which to create the instance
     */
    JnlpExtDownload (Table tbl) {
      setExtPart(tbl.getS("EXT-PART"));
      setDownload(tbl.getS("DOWNLOAD"));
      setPart(tbl.getS("PART"));
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      return appendOpenCloseTag(xml, "          ", "ext-download", "ext-part", getExtPart(),
                                                                   "download", getDownload(),
                                                                   "part",     getPart());
    }

    /** Name of the part in the extension.
     *  @return the name of the part in the extension
     */
    public String getExtPart () { return extPart; }
    /** Name of the part in the extension.
     *  @param extPart the name of the part of the extension
     */
    public void setExtPart (String extPart) {
      if (extPart == null) throw new NullPointerException("ext-part can not be null");
      this.extPart = extPart;
    }

    /** Should this element must be downloaded before an application is
     *  launched (eager), or not (lazy)?
     *  @return true if this element should be downloaded before launch
     */
    public JnlpDownload getDownload () { return download; }
    /** Should this element must be downloaded before an application is
     *  launched (eager), or not (lazy)?
     *  @param download true if this element should be downloaded before launch
     */
    public void setDownload (JnlpDownload download) { this.download = download; }
    /** Should this element must be downloaded before an application is
     *  launched (eager), or not (lazy)?
     *  @param download if this element should be downloaded before launch
     */
    public final void setDownload (String download) {
      setDownload(JnlpDownload.parse(download));
    }

    /** Get the name of the part in to which this extension belongs.
     *  @return the name of the part in to which this extension belongs */
    public String getPart () { return part; }
    /** Set the name of the part in to which this extension belongs.
     * @param part the name of the part in to which this extension belongs */
    public void setPart (String part) { this.part = part; }
  } // end inner static final class JnlpExtDownload

  /** Defines the relationship between a Java package or class name and a part. */
  public static final class JnlpPackage extends JnlpElement {
    private String  name;
    private String  part;
    private Boolean recursive;

    /** Creates a new instance.
     *  @param name name of the package or class
     *  @param part the name of the part to which this package or class belongs */
    public JnlpPackage (String name, String part) {
      setName(name);
      setPart(part);
    }

    /** Creates a new instance.
     *  @param tbl table containing name, part, and recursive keys
     */
    JnlpPackage (Table tbl) {
      setName(tbl.getS("NAME"));
      setPart(tbl.getS("PART"));
      if (tbl.containsKey("RECURSIVE")) setRecursive(tbl.getState("RECURSIVE"));
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      return appendOpenCloseTag(xml, "    ", "package", "name",      getName(),
                                                        "part",      getPart(),
                                                        "recursive", isRecursive());
    }

    /** Get the name of the package or class.
     *  @return name of the package or class */
    public String getName () { return name; }
    /**Set the name of the package or class.
     * @param name of the package or class */
    public void setName (String name) {
      if (name == null) throw new NullPointerException("Name can not be null");
      this.name = name;
    }

    /** Get the part in to which this package or class belongs.
     *  @return part the name of the part to which this package or class belongs */
    public String getPart () { return part; }
    /** Set the name of the part in to which this package or class belongs.
     * @param part the name of the part to which this package or class belongs */
    public void setPart (String part) {
      if (part == null) throw new NullPointerException("Part can not be null");
      this.part = part;
    }

    /** Are all sub-packages of this particular package is also included?
     *  @return true if all sub=packages included */
    public Boolean isRecursive () { return recursive; }
    /** Are all sub-packages of this particular package is also included?
     *  @param recursive if all sub-packages are included */
    public void setRecursive (Boolean recursive) { this.recursive = recursive; }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((name == null) ? 0 : name.hashCode());
      result = prime * result + ((part == null) ? 0 : part.hashCode());
      result = prime * result + ((recursive == null) ? 0 : recursive.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      JnlpPackage other = (JnlpPackage) obj;
      if (name == null) {
        if (other.name != null)
          return false;
      }
      else if (!name.equals(other.name))
        return false;
      if (part == null) {
        if (other.part != null)
          return false;
      }
      else if (!part.equals(other.part))
        return false;
      if (recursive == null) {
        if (other.recursive != null)
          return false;
      }
      else if (!recursive.equals(other.recursive))
        return false;
      return true;
    }
  } // end inner static final class JnlpPackage

  /** A description element in the JNLP file with special handling. */
  public static abstract class JnlpDescElement extends JnlpElement {
    private final String tag;
    private       String mainClass;
    private       String progressClass; // JNLP 6.0.18

    /** Creates a new instance.
     *
     *  @param tag tag
     */
    JnlpDescElement (String tag) { // package-private
      this.tag = tag;
    }

    /** Creates a new instance.
     *
     *  @param tag tag
     *  @param tbl table with MAIN-CLASS and PROGRESS-CLASS values
     */
    @SuppressWarnings("all")
    JnlpDescElement (String tag, Table tbl) {
      this(tag);
      setMainClass(tbl.getS("MAIN-CLASS"));
      setProgressClass(tbl.getS("PROGRESS-CLASS"));
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      if (ver >= VERSION_6_0_18) {
        return appendOpenCloseTag(xml, "  ", tag, "main-class",     getMainClass(),
                                                  "progress-class", getProgressClass());
      }
      else {
        return appendOpenCloseTag(xml, "  ", tag, "main-class", getMainClass());
      }
    }

    /** Get the main class.
     *  @return the main class */
    public String getMainClass () { return mainClass; }
    /** Set the main class.
     *  @param mainClass the main class */
    protected void setMainClass (String mainClass) { this.mainClass = mainClass; }

    /** The name of the class implementing DownloadServiceListener that is used
     *  to indicate the download progress.
     *  @return progress class
     */
    public String getProgressClass () { return progressClass; }
    /** The name of the class implementing DownloadServiceListener that is used
     *  to indicate the download progress.
     *  @param progressClass name of the class implementing DownloadServiceListener
     */
    protected void setProgressClass (String progressClass) { this.progressClass = progressClass; }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((mainClass == null) ? 0 : mainClass.hashCode());
      result = prime * result + ((progressClass == null) ? 0 : progressClass.hashCode());
      result = prime * result + ((tag == null) ? 0 : tag.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;
      if (obj == null)
        return false;
      if (getClass() != obj.getClass())
        return false;
      JnlpDescElement other = (JnlpDescElement) obj;
      if (mainClass == null) {
        if (other.mainClass != null)
          return false;
      }
      else if (!mainClass.equals(other.mainClass))
        return false;
      if (progressClass == null) {
        if (other.progressClass != null)
          return false;
      }
      else if (!progressClass.equals(other.progressClass))
        return false;
      if (tag == null) {
        if (other.tag != null)
          return false;
      }
      else if (!tag.equals(other.tag))
        return false;
      return true;
    }
  } // end inner static abstract class JnlpDescElement

  /** A component extension. */
  public final static class JnlpComponentDesc extends JnlpDescElement {
    /** default constructor */
    public JnlpComponentDesc () { super("component-desc"); }
    JnlpComponentDesc (Table tbl) { super("component-desc", tbl); }

    @Override public void setProgressClass (String mainClass) { super.setProgressClass(mainClass); }
  } // end inner final static class JnlpComponentDesc

  /** An installer/uninstaller. */
  public static final class JnlpInstallerDesc extends JnlpDescElement {
    /** default constructor */
    public JnlpInstallerDesc () { super("installer-desc"); }
    /** Constructor
     *  @param tbl table with MAIN-CLASS and PROGRESS-CLASS values
     */
    JnlpInstallerDesc (Table tbl) { super("installer-desc", tbl); }
  } // end inner static final class JnlpInstallerDesc

  /** Describes an application. */
  public static final class JnlpApplicationDesc extends JnlpDescElement {
    private final List<String> arguments = new LinkedList<>();

    /** Creates a new instance. */
    public JnlpApplicationDesc () {
      super("application-desc");
    }
    /** Creates a new instance.
     *  @param tbl table containing application description
     */
    JnlpApplicationDesc (Table tbl) {
      super("application-desc", tbl);
      for (String s : getStrings(tbl, "ARGUMENT")) {
        getArguments().add(s);
      }
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      if (ver >= VERSION_6_0_18) {
        appendOpenTag(xml, "    ", "application-desc", "main-class",     getMainClass(),
                                                       "progress-class", getProgressClass());
      }
      else {
        appendOpenTag(xml, "    ", "application-desc", "main-class", getMainClass());
      }
      for (String a : getArguments()) {
        xml.append("      <argument>").append(a).append("</argument>\n");
      }
      return xml.append("    </application-desc>\n");
    }

    @Override public void setMainClass (String mainClass) { super.setMainClass(mainClass); }
    @Override public void setProgressClass (String mainClass) { super.setProgressClass(mainClass); }

    /** Get the application arguments.
     *  @return the application arguments */
    public List<String> getArguments () { return arguments; }
  } // end inner static final class JnlpApplicationDesc

  /** Describes an applet.
   *  @deprecated Since 4.1.0, will be removed soon, because Applet support has been removed */
  @Deprecated  // TODO: remove in 4.3.0
  public final static class JnlpAppletDesc extends JnlpDescElement {
    private       String             documentBase;
    private       String             name;
    private       int                width;
    private       int                height;
    private final Map<String,String> params = new LinkedHashMap<>();

    /** Creates a new instance.
     *  @param tbl description table
     */
    JnlpAppletDesc (Table tbl) {
      super("applet-desc", tbl);
      setDocumentBase(tbl.getS("DOCUMENT-BASE"));
      setName(        tbl.getS("NAME"         ));

      if (tbl.containsKey("WIDTH" )) setWidth( tbl.getL("WIDTH" ));
      if (tbl.containsKey("HEIGHT")) setHeight(tbl.getL("HEIGHT"));

      for (Table t : getTables(tbl, "ARGUMENT")) {
        getParams().put(t.getS("NAME"), t.getS("VALUE"));
      }
    }

    /** Creates a new instance with the required parameters.
     *  @param mainClass The main class.
     *  @param name      Name of the applet.
     *  @param width     Width of the applet.
     *  @param height    Height of the applet.
     */
    public JnlpAppletDesc (String mainClass, String name, int width, int height) {
      super("applet-desc");
      setMainClass(mainClass);
      setName(name);
      setWidth(width);
      setHeight(height);
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      if (ver >= VERSION_6_0_18) {
        appendOpenTag(xml, "    ", "applet-desc", "documentbase",   getDocumentBase(),
                                                  "main-class",     getMainClass(),
                                                  "progress-class", getProgressClass(),
                                                  "name",           getName(),
                                                  "width",          getWidth(),
                                                  "height",         getHeight());
      }
      else {
        appendOpenTag(xml, "    ", "applet-desc", "documentbase", getDocumentBase(),
                                                  "main-class",   getMainClass(),
                                                  "name",         getName(),
                                                  "width",        getWidth(),
                                                  "height",       getHeight());
      }
      for (Map.Entry<String,String> p : getParams().entrySet()) {
        appendOpenCloseTag(xml, "      ", "param", "name",  p.getKey(),
                                                   "value", p.getValue());
      }
      return xml.append("    </applet-desc>\n");
    }

    @Override public void setProgressClass (String mainClass) { super.setProgressClass(mainClass); }

    /** Get the main class of the applet.
     *  @return the main class of the Applet */
    public String getDocumentBase () { return documentBase; }
    /** Set the main class of the applet.
     *  @param documentBase main class of the Applet */
    public void setDocumentBase (String documentBase) { this.documentBase = documentBase; }

    @Override
    public void setMainClass (String mainClass) {
      if (mainClass == null) throw new NullPointerException("Main class can not be null.");
      super.setMainClass(mainClass);
    }

    /** Get the name of the applet.
     *  @return the name of the Applet */
    public String getName () { return name; }
    /** Set the name of the applet.
     *  @param name the name of the Applet */
    public void setName (String name) {
      if (name == null) throw new NullPointerException("Name can not be null.");
      this.name = name;
    }

    /** Get the width of the applet.
     *  @return the width of the Applet */
    public int getWidth () { return width; }
    /** Set the width of the applet.
     *  @param width the width of the Applet */
    public void setWidth (int width) {
      if (width <= 0) throw new IllegalArgumentException("Width can not be "+width);
      this.width = width;
    }

    /** Get the height of the applet.
     *  @return the height of the applet */
    public int getHeight () { return height; }
    /** Set the height of the applet.
     *  @param height the height of the applet */
    public void setHeight (int height) {
      if (height <= 0) throw new IllegalArgumentException("Height can not be "+width);
      this.height = height;
    }

    /** Get the applet parameters.
     *  @return the Applet parameters */
    public Map<String,String> getParams () { return params; }
  } // end inner final static class JnlpAppletDesc

  /** List of extensions to associate. (JNLP 1.5) */
  public static final class JnlpAssociation extends DescribedJnlpElement {
    private final NonEmptyList<String> extensions;
    private       String               mimeType;

    /** Creates a new association with the given mime-type and extensions.
     *  @param mimeType mime types
     *  @param ext      extensions */
    public JnlpAssociation (String mimeType, String ... ext) {
      this.extensions = new NonEmptyList<>(Arrays.asList(ext));
      setMimeType(mimeType);
    }

    /** Creates a new association with the given mime-type and extensions.
     *  @param mimeType  mime types
     *  @param extensions extensions
     */
    public JnlpAssociation (String mimeType, NonEmptyList<String> extensions) {
      this.extensions = extensions;
      setMimeType(mimeType);
    }

    /** Creates a new association with the given mime-type and extensions.
     *  @param tbl table containing mime-type and extensions */
    JnlpAssociation (Table tbl) {
      super(tbl);
      this.extensions = new NonEmptyList<>(Arrays.asList((String)null));
      setMimeType(tbl.getS("MIME-TYPE"));
      setExt(tbl.getS("EXTENSIONS"));

      if (tbl.containsKey("ICON")) {
        setIcon(new JnlpIcon(tbl.getTable("ICON")));
      }
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      String kind = (getDescriptionKind() == null)? ""
                                                  : " kind='"+getDescriptionKind()+"'";

      appendOpenTag(xml, "    ", "association", "extensions", getExt(),
                                                "mime-type",  getMimeType());
      appendElement(xml, "      <description"+kind+">", getDescription(), "</description>");
      if (getIcon() != null) getIcon().appendXML(xml, ver);
      return xml.append("    </association>\n");
    }

    /** <b>Internal use only:</b> The list of extensions supported.
     *  @return extensions supported */
    public CharSequence getExt () {
      StringBuilder str = new StringBuilder();
      str.append(extensions.get(0));
      for (int i = 1; i < extensions.size(); i++) {
        str.append(' ').append(extensions.get(i));
      }
      return str;
    }
    /** <b>Internal use only:</b> The list of extensions supported.
     *  @param ext extensions supported */
    public void setExt (CharSequence ext) {
      String[] exts = ext.toString().split(" ");
      if (extensions.size() > 0) {
        extensions.removeAll(extensions.subList(1, extensions.size()));
      }
      extensions.set(0, exts[0]);
      for (int i = 1; i < exts.length; i++) {
        extensions.add(exts[i]);
      }
    }
    /** The list of extensions supported.
     *  @return list of extensions supported */
    public NonEmptyList<String> getExtensions () { return extensions; }

    /** Get the mime-type supported.
     *  @return mime-type supported */
    public String getMimeType () { return mimeType; }
    /** Set the mime-type supported.
     *  @param mimeType mime type supported */
    public void setMimeType (String mimeType) {
      if (mimeType== null) throw new NullPointerException("mime-type can not be null");
      this.mimeType = mimeType;
    }
  } // end inner static final class JnlpAssociation

  /** A description of additional content that may be integrated with the application. */
  public static final class JnlpRelatedContent extends DescribedJnlpElement {
    private String href;
    private String title;

    /** Creates a new instance pointing to the given URL.
     *  @param href URL to point to */
    public JnlpRelatedContent (String href) {
      super();
      setHref(href);
    }

    /** Creates a new instance pointing to the given table.
     *  @param tbl table to point to */
    JnlpRelatedContent (Table tbl) {
      super(tbl);
      setHref(tbl.getS("HREF"));
      setTitle(tbl.getS("TITLE"));
    }

    @Override
    public StringBuilder appendXML (StringBuilder xml, int ver) {
      String kind = (getDescriptionKind() == null)? ""
                                                  : " kind='"+getDescriptionKind()+"'";

      appendOpenTag(xml, "    ", "related-content", "href", getHref());
      appendElement(xml, "      <title>",               getTitle(),       "</title>");
      appendElement(xml, "      <description"+kind+">", getDescription(), "</description>");
      if (getIcon() != null) getIcon().appendXML(xml, ver);
      return xml.append("    </related-content>\n");
    }

    /** The URL to a location on the web containing the resource.
     *  @return URL of rescource */
    public final String getHref () { return href; }
    /** The URL to a location on the web containing the resource.
     *  @param href URL of resource */
    public final void setHref (String href) {
      if (href == null) throw new NullPointerException("HREF can not be null.");
      this.href = href;
    }

    /** A title of the related content.
     *  @return title oif the related content */
    public String getTitle () { return title; }
    /** A title of the related content.
     *  @param title A title of related content */
    public void setTitle (String title) { this.title = title; }
  } // end inner static final class JnlpRelatedContent

  //////////////////////////////////////////////////////////////////////////////
  // Nested Collection Classes
  //////////////////////////////////////////////////////////////////////////////
  /** An extension to {@link List} that requires the list to be non-empty.
   *  @param <E> the list type
   */
  public static final class NonEmptyList<E> implements List<E> {
    private final LinkedList<E> list = new LinkedList<>();

    /** Creates a new instance with the given initial value.
        @param value initial value */
    public NonEmptyList (E value) {
      add(value);
    }

    /** Creates a new instance with the given initial values.
     *  @param values initial values */
    public NonEmptyList (Collection<E> values) {
      if (values.isEmpty()) {
        throw new IllegalArgumentException("Can not create an empty NonEmptyList.");
      }
      addAll(values);
    }

    @Override public boolean  add         (E v)             { return list.add(v);   }
    @Override public void     add         (int i, E v)      {        list.add(i,v); }
    @Override public boolean  contains    (Object v)        { return list.contains(v); }
    @Override public boolean  containsAll (Collection<?> c) { return list.containsAll(c); }
    @Override public boolean  equals      (Object o)        { return list.equals(o); }
    @Override public E        get         (int i)           { return list.get(i);   }
    @Override public int      hashCode    ()                { return list.hashCode(); }
    @Override public int      indexOf     (Object v)        { return list.indexOf(v); }
    @Override public boolean  isEmpty     ()                { return false;         }
    @Override public int      lastIndexOf (Object v)        { return list.lastIndexOf(v); }
    @Override public E        set         (int i, E v)      { return list.set(i, v); }
    @Override public int      size        ()                { return list.size();   }
    @Override public List<E>  subList     (int i, int j)    { return list.subList(i,j);   }
    @Override public Object[] toArray     ()                { return list.toArray(); }
    @Override public String   toString    ()                { return list.toString(); }

    @Override
    public Iterator<E> iterator () {
      return new ListIter<>(this, list.listIterator(0));
    }

    @Override
    public ListIterator<E> listIterator () {
      return new ListIter<>(this, list.listIterator(0));
    }

    @Override
    public ListIterator<E> listIterator (int i) {
      return new ListIter<>(this, list.listIterator(i));
    }

    @Override
    public boolean addAll (Collection<? extends E> c) {
      return list.addAll(c);
    }

    @Override
    public boolean addAll (int i, Collection<? extends E> c) {
      return list.addAll(i,c);
    }

    /** Throws an exception as it would make the list empty. */
    @Override
    public void clear () {
      throw new UnsupportedOperationException("Can not remove all elements "
                                             +"from a NonEmptyList");
    }

    /** Clears the list and adds the given entry in.
     *  @param entry entry to add
     */
    public void clear (E entry) {
      list.clear();
      add(entry);
    }

    /** Removes an element from the list, throws an exception if doing so will
     *  make the list empty.
     */
    @Override
    public E remove (int i) {
      if ((size() == 1) && (i == 0)) {
        clear();
      }
      return list.remove(i);
    }

    /** Removes an element from the list, throws an exception if doing so will
     *  make the list empty.
     */
    @Override
    @SuppressWarnings("all")
    public boolean remove (Object v) {
      if ((size() == 1) && contains(v)) {
        clear();
      }
      return list.remove(v);
    }

    /** Removes elements from the list, throws an exception if doing so will
     *  make the list empty.
     */
    @Override
    @SuppressWarnings("all")
    public boolean removeAll (Collection<?> c) {
      if (c.size() < size()) return list.removeAll(c);

      int count = 0;
      for (Object o : c) {
        if (contains(o)) count++;
      }

      if (count == size()) clear();
      return list.removeAll(c);
    }

    /** Removes elements from the list, throws an exception if doing so will
     *  make the list empty.
     */
    @Override
    public boolean retainAll (Collection<?> c) {
      if (c.isEmpty()) clear();

      int count = 0;
      for (E v : this) {
        if (c.contains(v)) count++;
      }

      if (count == size()) clear();
      return list.retainAll(c);
    }

    @Override
    public <T> T[] toArray (T[] a) {
      return list.toArray(a);
    }
  } // end inner static final class NonEmptyList<E>

  /** A basic version of ListIterator that keeps at least one element in the list. */
  private static class ListIter<E> implements ListIterator<E> {
    private final NonEmptyList<E> list;
    private final ListIterator<E> iter;

    ListIter (NonEmptyList<E> list, ListIterator<E> iter) {
      this.list = list;
      this.iter = iter;
    }

    @Override public void    add (E v)        {        iter.add(v);          }
    @Override public boolean hasNext ()       { return iter.hasNext();       }
    @Override public boolean hasPrevious ()   { return iter.hasPrevious();   }
    @Override public E       next ()          { return iter.next();          }
    @Override public int     nextIndex ()     { return iter.nextIndex();     }
    @Override public E       previous ()      { return iter.previous();      }
    @Override public int     previousIndex () { return iter.previousIndex(); }
    @Override public void    set (E v)        {        iter.set(v);          }

    @Override
    public void remove () {
      if (list.size() == 1) {
        throw new UnsupportedOperationException("Can not remove all elements from a NonEmptyList");
      }
      iter.remove();
    }
  } // end inner static class ListIter<E>

}
