dip.world.variant.VariantManager.java Source code

Java tutorial

Introduction

Here is the source code for dip.world.variant.VariantManager.java

Source

//
//  @(#)VariantManager.java   1.00   7/2002
//
//  Copyright 2002 Zachary DelProposto. All rights reserved.
//  Use is subject to license terms.
//
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program; if not, write to the Free Software
//  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//  Or from http://www.gnu.org/
//
package dip.world.variant;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.jnlp.ServiceManager;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.io.FileUtils;

import dip.gui.dialog.ErrorDialog;
import dip.misc.Log;
import dip.world.variant.data.MapGraphic;
import dip.world.variant.data.SymbolPack;
import dip.world.variant.data.Variant;
import dip.world.variant.parser.XMLSymbolParser;
import dip.world.variant.parser.XMLVariantParser;

/**
 *   Finds Variant-packs, which are of the format:
 *   <ol>
 *      <li>*Variant.zip</li>
 *      <li>*Variants.zip</li>
 *      <li>*Variant.jar</li>
 *      <li>*Variants.jar</li>
 *   </ol>
 *   Faciliates loading of variant resources. Within the above file, the variants.xml
 *   file is parsed to determine the required information.
 *   <p>
 *   Also finds all SymbolPacks, which end in:
 *   <ol>
 *      <li>*Symbols.zip</li>
 *      <li>*Symbols.jar</li>
 *   </ol>
 *   <p>
 *   
 *   TODO: deconflict code may work better if we include the preceding "/" before
 *   the .jar name.<br> 
 *   <br>
 */
public class VariantManager {

    /** Version Constant representing the most recent version of a Variant or SymbolPack */
    public static final float VERSION_NEWEST = -1000.0f;
    /** Version Constant representing the most oldest version of a Variant or SymbolPack */
    public static final float VERSION_OLDEST = -2000.0f;
    // variant constants
    private static final List<String> VARIANT_EXTENSIONS = new ArrayList<String>() {

        /**
        * 
        */
        private static final long serialVersionUID = 1L;

        {
            add("Variant.zip");
            add("Variants.zip");
            add("Variant.jar");
            add("Variants.jar");
        }
    };
    private static final String VARIANT_FILE_NAME = "variants.xml";
    // symbol constants
    private static final List<String> SYMBOL_EXTENSIONS = new ArrayList<String>() {

        /**
        * 
        */
        private static final long serialVersionUID = 1L;

        {
            add("Symbols.zip");
            add("Symbols.jar");
        }
    };
    private static final String SYMBOL_FILE_NAME = "symbols.xml";
    // class variables
    private static VariantManager vm = null;
    // instance variables
    private final boolean isInWebstart;
    private HashMap<String, MapRec> variantMap = null; // map of lowercased Variant names to MapRec objects (which contain VRecs)
    private HashMap<String, MapRec> symbolMap = null; // lowercase symbol names to MapRec objects (which contain SPRecs)
    // cached variables to enhance performance of getResource() methods
    private transient List<Variant> variants = new ArrayList<Variant>(); // The sorted Variant list
    private transient List<SymbolPack> symbolPacks = new ArrayList<SymbolPack>(); // The sorted SymbolPack list
    private transient URLClassLoader currentUCL = null; // The current class loader 
    private transient URL currentPackageURL = null; // The current class loader URL

    /** 
     *   Initiaize the VariantManager. 
     *   <p>
     *   An exception is thrown if no File paths are specified. A "." may be used
     *   to specify th ecurrent directory.
     *   <p>
     *   Loaded XML may be validated if the isValidating flag is set to true.
     *
     */
    public static synchronized void init(final List<File> searchPaths, boolean isValidating)
            throws javax.xml.parsers.ParserConfigurationException, NoVariantsException {
        long ttime = System.currentTimeMillis();
        long vptime = ttime;
        Log.println("VariantManager.init()");

        if (searchPaths == null || searchPaths.isEmpty()) {
            throw new IllegalArgumentException();
        }

        if (vm != null) {
            // perform cleanup
            vm.variantMap.clear();
            vm.variants = new ArrayList<Variant>();
            vm.currentUCL = null;
            vm.currentPackageURL = null;

            vm.symbolPacks = new ArrayList<SymbolPack>();
            vm.symbolMap.clear();
        }

        vm = new VariantManager();

        // find plugins, create plugin loader
        final List<URL> pluginURLs = vm.searchForFiles(searchPaths, VARIANT_EXTENSIONS);

        // setup document builder
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

        try {
            // this may improve performance, and really only apply to Xerces
            dbf.setAttribute("http://apache.org/xml/features/dom/defer-node-expansion", Boolean.FALSE);
            dbf.setAttribute("http://apache.org/xml/properties/input-buffer-size", new Integer(4096));
            dbf.setAttribute("http://apache.org/xml/features/nonvalidating/load-external-dtd", Boolean.FALSE);
        } catch (Exception e) {
            Log.println("VM: Could not set XML feature.", e);
        }

        dbf.setValidating(isValidating);
        dbf.setCoalescing(false);
        dbf.setIgnoringComments(true);

        // setup variant parser
        XMLVariantParser variantParser = new XMLVariantParser(dbf);

        // for each plugin, attempt to find the "variants.xml" file inside. 
        // if it does not exist, we will not load the file. If it does, we will parse it,
        // and associate the variant with the URL in a hashtable.
        for (final URL pluginURL : pluginURLs) {
            URLClassLoader urlCL = new URLClassLoader(new URL[] { pluginURL });
            URL variantXMLURL = urlCL.findResource(VARIANT_FILE_NAME);
            if (variantXMLURL != null) {
                String pluginName = getFile(pluginURL);

                // parse variant description file, and create hash entry of variant object -> URL
                InputStream is = null;
                try {
                    is = new BufferedInputStream(variantXMLURL.openStream());
                    variantParser.parse(is, pluginURL);
                    final List<Variant> variants = variantParser.getVariants();

                    // add variants; variants with same name (but older versions) are
                    // replaced with same-name newer versioned variants
                    for (final Variant variant : variants) {
                        addVariant(variant, pluginName, pluginURL);
                    }
                } catch (IOException e) {
                    // display error dialog
                    ErrorDialog.displayFileIO(null, e, pluginURL.toString());
                } catch (org.xml.sax.SAXException e) {
                    // display error dialog
                    ErrorDialog.displayGeneral(null, e);
                } finally {
                    if (is != null) {
                        try {
                            is.close();
                        } catch (IOException e) {
                        }
                    }
                }
            }
        }

        // if we are in webstart, search for variants within webstart jars
        Enumeration<URL> enum2 = null;
        ClassLoader cl = null;

        if (vm.isInWebstart) {
            cl = vm.getClass().getClassLoader();

            try {
                enum2 = cl.getResources(VARIANT_FILE_NAME);
            } catch (IOException e) {
                enum2 = null;
            }

            if (enum2 != null) {
                while (enum2.hasMoreElements()) {
                    URL variantURL = enum2.nextElement();

                    // parse variant description file, and create hash entry of variant object -> URL
                    InputStream is = null;
                    String pluginName = getWSPluginName(variantURL);

                    try {
                        is = new BufferedInputStream(variantURL.openStream());

                        variantParser.parse(is, variantURL);
                        final List<Variant> variants = variantParser.getVariants();

                        // add variants; variants with same name (but older versions) are
                        // replaced with same-name newer versioned variants
                        for (final Variant variant : variants) {
                            addVariant(variant, pluginName, variantURL);
                        }
                    } catch (IOException e) {
                        // display error dialog
                        ErrorDialog.displayFileIO(null, e, variantURL.toString());
                    } catch (org.xml.sax.SAXException e) {
                        // display error dialog
                        ErrorDialog.displayGeneral(null, e);
                    } finally {
                        if (is != null) {
                            try {
                                is.close();
                            } catch (IOException e) {
                            }
                        }
                    }
                }
            } // if(enum2 != null)
        }

        // check: did we find *any* variants? Throw an exception.
        if (vm.variantMap.isEmpty()) {
            StringBuffer msg = new StringBuffer(256);
            msg.append("No variants found on path: ");
            for (final File searchPath : searchPaths) {
                msg.append(searchPath);
                msg.append("; ");
            }

            throw new NoVariantsException(msg.toString());
        }

        Log.printTimed(vptime, "VariantManager: variant parsing time: ");

        ///////////////// SYMBOLS /////////////////////////

        // now, parse symbol packs
        XMLSymbolParser symbolParser = new XMLSymbolParser(dbf);

        // find plugins, create plugin loader
        final List<URL> pluginURLs2 = vm.searchForFiles(searchPaths, SYMBOL_EXTENSIONS);

        // for each plugin, attempt to find the "variants.xml" file inside. 
        // if it does not exist, we will not load the file. If it does, we will parse it,
        // and associate the variant with the URL in a hashtable.
        for (final URL pluginURL : pluginURLs2) {
            URLClassLoader urlCL = new URLClassLoader(new URL[] { pluginURL });
            URL symbolXMLURL = urlCL.findResource(SYMBOL_FILE_NAME);
            if (symbolXMLURL != null) {
                String pluginName = getFile(pluginURL);

                // parse variant description file, and create hash entry of variant object -> URL
                InputStream is = null;
                try {
                    is = new BufferedInputStream(symbolXMLURL.openStream());
                    symbolParser.parse(is, pluginURL);
                    addSymbolPack(symbolParser.getSymbolPack(), pluginName, pluginURL);
                } catch (IOException e) {
                    // display error dialog
                    ErrorDialog.displayFileIO(null, e, pluginURL.toString());
                } catch (org.xml.sax.SAXException e) {
                    // display error dialog
                    ErrorDialog.displayGeneral(null, e);
                } finally {
                    if (is != null) {
                        try {
                            is.close();
                        } catch (IOException e) {
                        }
                    }
                }
            }
        }

        // if we are in webstart, search for variants within webstart jars      
        enum2 = null;
        cl = null;

        if (vm.isInWebstart) {
            cl = vm.getClass().getClassLoader();

            try {
                enum2 = cl.getResources(SYMBOL_FILE_NAME);
            } catch (IOException e) {
                enum2 = null;
            }

            if (enum2 != null) {
                while (enum2.hasMoreElements()) {
                    URL symbolURL = enum2.nextElement();

                    // parse variant description file, and create hash entry of variant object -> URL
                    InputStream is = null;
                    String pluginName = getWSPluginName(symbolURL);

                    try {
                        is = new BufferedInputStream(symbolURL.openStream());
                        symbolParser.parse(is, symbolURL);
                        addSymbolPack(symbolParser.getSymbolPack(), pluginName, symbolURL);
                    } catch (IOException e) {
                        // display error dialog
                        ErrorDialog.displayFileIO(null, e, symbolURL.toString());
                    } catch (org.xml.sax.SAXException e) {
                        // display error dialog
                        ErrorDialog.displayGeneral(null, e);
                    } finally {
                        if (is != null) {
                            try {
                                is.close();
                            } catch (IOException e) {
                            }
                        }
                    }
                }
            } // if(enum2 != null)
        } // if(isInWebStart)      

        // check: did we find *any* symbol packs? Throw an exception.
        if (vm.symbolMap.isEmpty()) {
            StringBuffer msg = new StringBuffer(256);
            msg.append("No SymbolPacks found on path: ");
            for (final File searchPath : searchPaths) {
                msg.append(searchPath);
                msg.append("; ");
            }

            throw new NoVariantsException(msg.toString());
        }
        Log.printTimed(ttime, "VariantManager: total parsing time: ");
    }// init()

    /** 
     *   Returns the known Variants. If multiple versions of a Variant
     *   exist, only the latest version is returned. The list is
     *   sorted in alphabetic order.
     */
    public static synchronized List<Variant> getVariants() {
        checkVM();

        if (vm.variants.size() != vm.variantMap.size()) {
            // note that we need to avoid putting duplicates
            // into the array.
            //
            final List<Variant> list = new ArrayList<Variant>(); // list of variants
            final Set<MapRec> set = new HashSet<MapRec>(); // a set of MapRecs
            set.addAll(vm.variantMap.values());

            // fill variant list with variants.
            for (final MapRec mr : set) {
                MapRecObj mro = mr.get(VERSION_NEWEST);
                assert (mro != null);
                if (mro instanceof VRec) {
                    list.add(((VRec) mro).getVariant());
                }
            }

            Collections.sort(list);
            vm.variants = new ArrayList<Variant>();
        }

        return vm.variants;
    }// getVariants()

    /** 
     *   Returns the known SymbolPacks. If multiple versions of a SymbolPack
     *   exist, only the latest version is returned. The list is
     *   sorted in alphabetic order.
     */
    public static synchronized List<SymbolPack> getSymbolPacks() {
        checkVM();

        if (vm.symbolPacks.size() != vm.symbolMap.size()) {
            // avoid putting duplicates into the array.
            final ArrayList<SymbolPack> list = new ArrayList<SymbolPack>(); // list of SymbolPacks
            final Set<MapRec> set = new HashSet<MapRec>(); // a set of MapRecs
            set.addAll(vm.symbolMap.values());

            // fill variant list with variants.
            for (final MapRec mr : set) {
                MapRecObj mro = mr.get(VERSION_NEWEST);
                assert (mro != null);
                if (mro instanceof SPRec) {
                    list.add(((SPRec) mro).getSymbolPack());
                }
            }

            Collections.sort(list);
            vm.symbolPacks = new ArrayList<SymbolPack>(list);
        }

        return vm.symbolPacks;
    }// getSymbolPacks()

    /**
     *   Finds Variant with the given name, or null if no Variant is found. 
     *   Attempts to find the version specified. Versions must be > 0.0f or
     *   the version constants VERSION_NEWEST or VERSION_OLDEST.
     *   <p>
     *   Note: Name is <b>not</b> case-sensitive.
     *
     */
    public static synchronized Variant getVariant(String name, float version) {
        checkVM();
        MapRec mr = vm.variantMap.get(name.toLowerCase());
        if (mr != null) {
            return ((VRec) mr.get(version)).getVariant();
        }

        return null;
    }// getVariant()

    /**
     *   Finds SymbolPack with the given name, or null if no SymbolPack is found. 
     *   Attempts to find the version specified. Versions must be > 0.0f or
     *   the version constants VERSION_NEWEST or VERSION_OLDEST.
     *   <p>
     *   Note: Name is <b>not</b> case-sensitive.
     *
     */
    public static synchronized SymbolPack getSymbolPack(String name, float version) {
        checkVM();
        if (name == null) {
            return null;
        }

        MapRec mr = vm.symbolMap.get(name.toLowerCase());
        if (mr != null) {
            return ((SPRec) mr.get(version)).getSymbolPack();
        }

        return null;
    }// getSymbolPack()

    /**
     *   Obtains a SymbolPack via the following criteria:
     *   <ol>
     *      <li>If matching SymbolPack name and Version found, that is returned; otherwise</li>
     *      <li>Returns SymbolPack of same name but of the newest available version; otherwise</li>
     *      <li>Returns the newest available SymbolPack preferred by the MapGraphic (if set); otherwise</li>
     *      <li>Returns the first SymbolPack in the list of SymbolPacks.</li>
     *   </ol>
     *   <p>
     *   Thus it is assured that a SymbolPack will always be obtained.
     */
    public static synchronized SymbolPack getSymbolPack(MapGraphic mg, String symbolPackName,
            final float symbolPackVersion) {
        if (mg == null) {
            throw new IllegalArgumentException();
        }

        // safety:
        // if version is invalid (< 0.0f), convert to VERSION_NEWEST 
        // automatically. Log this method, though
        float spVersion = symbolPackVersion;
        if (spVersion <= 0.0f) {
            Log.println(
                    "WARNING: VariantManager.getSymbolPack() called with symbolPackVersion of <= 0.0f. Check parameters.");
            spVersion = VERSION_NEWEST;
        }

        SymbolPack sp = getSymbolPack(symbolPackName, spVersion);
        if (sp == null) {
            sp = getSymbolPack(symbolPackName, VERSION_NEWEST);
            if (sp == null && mg.getPreferredSymbolPackName() != null) {
                sp = getSymbolPack(mg.getPreferredSymbolPackName(), VERSION_NEWEST);
            }

            if (sp == null) {
                sp = getSymbolPacks().get(0);
            }
        }

        return sp;
    }// getSymbolPack()

    /** 
     *   Returns true if the desired version was found. Version must 
     *   be a positive floating point value, or, a defined constant.
     *   Returns false if the version is not available or the variant
     *   is not found.
     */
    public static boolean hasVariantVersion(final String name, final float version) {
        return (getVariant(name, version) != null);
    }// hasVariantVersion()

    /** 
     *   Returns true if the desired version was found. Version must 
     *   be a positive floating point value, or, a defined constant.
     *   Returns false if the version is not available or the SymbolPack
     *   is not found.
     */
    public static boolean hasSymbolPackVersion(final String name, final float version) {
        return (getSymbolPack(name, version) != null);
    }// hasVariantVersion()

    /** 
     *   Returns the versions of a variant that are available. 
     *   If the variant is not found, a zero-length array is returned.
     */
    public synchronized static float[] getVariantVersions(final String name) {
        checkVM();
        MapRec mr = vm.variantMap.get(name.toLowerCase());
        if (mr != null) {
            return (mr.getVersions());
        }

        return new float[0];
    }// getVariantVersions()

    /** 
     *   Returns the versions of a SymbolPack that are available. 
     *   If the SymbolPack is not found, a zero-length array is returned.
     */
    public synchronized static float[] getSymbolPackVersions(String name) {
        checkVM();
        MapRec mr = vm.symbolMap.get(name.toLowerCase());
        if (mr != null) {
            return (mr.getVersions());
        }

        return new float[0];
    }// getSymbolPackVersions()

    /** Ensures version is positive OR VERSION_NEWEST or VERSION_OLDEST */
    private static void checkVersionConstant(float version) {
        if (version <= 0.0f && (version != VERSION_NEWEST && version != VERSION_OLDEST)) {
            throw new IllegalArgumentException("invalid version or version constant: " + version);
        }
    }// checkVersionConstant()

    /** 
     *   Gets a specific resource for a Variant or a SymbolPack, given a URL to
     *   the package and a reference URI. Threadsafe. 
     *   <p>
     *   Typically, getResource(Variant, URI) or getResource(SymbolPack, URI) is
     *   preferred to this method.
     *
     */
    public static synchronized URL getResource(URL packURL, URI uri) {
        // ensure we have been initialized...
        checkVM();

        // if we are in webstart, assume that this is a webstart jar. 
        if (vm.isInWebstart) {
            URL url = getWSResource(packURL, uri);

            // if cannot get it, fall through.
            if (url != null) {
                return url;
            }
        }

        // if URI has a defined scheme, convert to a URL (if possible) and return it.
        if (uri.getScheme() != null) {
            try {
                return uri.toURL();
            } catch (MalformedURLException e) {
                return null;
            }
        }

        // resolve & load.
        URLClassLoader classLoader = getClassLoader(packURL);
        return classLoader.findResource(uri.toString());
    }// getResource()

    /**
     *   Gets a specific resource by properly resolving the URI
     *   to this Variant. Null arguments are illegal. Returns 
     *   null if the resource cannot be resolved. Threadsafe.
     */
    public static URL getResource(Variant variant, URI uri) {
        if (variant == null) {
            throw new IllegalArgumentException();
        }
        return getResource(getVRec(variant), uri);
    }// getResource()

    /**
     *   Gets a specific resource by properly resolving the URI
     *   to this SymbolPack. Null arguments are illegal. Returns 
     *   null if the resource cannot be resolved. Threadsafe.
     */
    public static URL getResource(SymbolPack symbolPack, URI uri) {
        if (symbolPack == null) {
            throw new IllegalArgumentException();
        }
        return getResource(getSPRec(symbolPack), uri);
    }// getResource()

    /** 
     *   Gets the URL to the Variant package (plugin). This is typically
     *   only needed in special circumstances. Returns null if null variant
     *   input OR variant not found.
     *   <p>
     *   Note that this will always return a URL with a JAR prefix.
     *   e.g.: <code>jar:http:/the.location/ajar.zip!/</code> 
     *   or <code>jar:file:/c:/plugins/ajar.zip!/</code>
     */
    public static URL getVariantPackageJarURL(Variant variant) {
        if (variant != null) {
            VRec vr = getVRec(variant);
            if (vr != null) {
                assert (vr.getURL() != null);

                URL url = vr.getURL();
                String txtUrl = url.toString();

                if (txtUrl.startsWith("jar:")) {
                    return url;
                } else {
                    StringBuffer sb = new StringBuffer(txtUrl.length() + 8);
                    sb.append("jar:");
                    sb.append(txtUrl);
                    sb.append("!/");

                    try {
                        return new URL(sb.toString());
                    } catch (MalformedURLException e) {
                        Log.println("Could not convert ", url, " to a JAR url.");
                        Log.println("Exception: ", e);
                    }
                }
            }
        }

        return null;
    }// getVariantPackageURL()

    /** 
     *   Internal getResource() implementation
     */
    private static synchronized URL getResource(MapRecObj mro, URI uri) {
        // ensure we have been initialized...
        checkVM();
        assert (mro != null);

        if (uri == null) {
            throw new IllegalArgumentException("null URI");
        }

        // if we are in webstart, assume that this is a webstart jar. 
        if (vm.isInWebstart) {
            URL url = getWSResource(mro, uri);

            // if cannot get it, fall through.
            if (url != null) {
                return url;
            }
        }

        // if URI has a defined scheme, convert to a URL (if possible) and return it.
        if (uri.getScheme() != null) {
            try {
                return uri.toURL();
            } catch (MalformedURLException e) {
                return null;
            }
        }

        // find the URL
        if (mro.getURL() != null) {
            return getClassLoader(mro.getURL()).findResource(uri.toString());
        }

        return null;
    }// getResource()

    /** Ensures that we have initialized the VariantManager */
    private static void checkVM() {
        if (vm == null) {
            throw new IllegalArgumentException("not initialized");
        }
    }// checkVM()

    /** Singleton */
    private VariantManager() {
        variantMap = new HashMap<String, MapRec>(53);
        symbolMap = new HashMap<String, MapRec>(17);
        isInWebstart = isInWebstart();
    }// VariantManager()

    /**
     *   Searches the given paths for files ending with the given extension(s).
     *   Returns URLs.
     *
     */
    private List<URL> searchForFiles(final List<File> searchPaths, final List<String> extensions) {
        final List<URL> urlList = new ArrayList<URL>();

        for (final File searchPath : searchPaths) {
            final Collection<File> list = FileUtils.listFiles(searchPath, null, false);
            // internal error if list == null; means that 
            // searchPaths[] is not a directory!
            if (list != null) {
                for (final File file : list) {
                    String fileName = file.getPath();

                    if (checkFileName(fileName, extensions)) {
                        try {
                            urlList.add(file.toURL());
                        } catch (java.net.MalformedURLException e) {
                            // do nothing; we just won't add it
                        }
                    }

                }
            }
        }

        return urlList;
    }// searchForFiles()

    /** Returns the URLClassLoader for a given URL, or creates a new one.... */
    private static URLClassLoader getClassLoader(URL packageURL) {
        // WARNING: this method is not (itself) threadsafe
        if (packageURL == null) {
            throw new IllegalArgumentException();
        }

        // see if a classloader for this url already exists (cache of 1)
        if (packageURL.equals(vm.currentPackageURL)) {
            return vm.currentUCL;
        }

        vm.currentUCL = new URLClassLoader(new URL[] { packageURL });
        vm.currentPackageURL = packageURL;
        return vm.currentUCL;
    }// getClassLoader()

    /** Returns the "file" part of the URL; e.g.: x/y/z.jar, returns z.jar */
    private static String getFile(URL url) {
        String s = url.toString();
        return s.substring(s.lastIndexOf("/") + 1, s.length());
    }// getFile()

    /** Get the webstart plugin name */
    private static String getWSPluginName(URL url) {
        final String s = url.toString();
        final int idxExclam = s.indexOf('!');
        if (idxExclam >= 0) {
            return s.substring(s.lastIndexOf("/", idxExclam) + 1, idxExclam);
        } else {
            return s;
        }
    }// getWSPluginName()

    /** Checks if the fileName ends with an allowed extension; if so, returns true. */
    private boolean checkFileName(String fileName, final List<String> extensions) {
        for (final String extension : extensions) {
            if (fileName.endsWith(extension)) {
                return true;
            }
        }

        return false;
    }// checkFileName()

    /** See if we are running under Java Webstart */
    private boolean isInWebstart() {
        // this is not the most optimal code.
        try {
            ServiceManager.lookup("javax.jnlp.BasicService");
            return true;
        } catch (Throwable e) {
            return false;
        }
    }// isInWebstart()

    /** 
     *   Get a resource for a variant. This uses the variantName to 
     *   deconflict, if multiple resources exist with the same name.
     *   <p>
     *   Conflict occur when plugins are loaded under the same ClassLoader,
     *   because variant plugin namespace is not unique.
     *   <p>
     *   This primarily applies to Webstart resources
     */
    private static URL getWSResource(MapRecObj mro, URI uri) {
        assert (vm.isInWebstart);

        if (uri == null) {
            return null;
        }

        ClassLoader cl = vm.getClass().getClassLoader();

        if (mro != null) {
            Enumeration<URL> enum2 = null;

            try {
                enum2 = cl.getResources(uri.toString());
            } catch (IOException e) {
                return null;
            }

            while (enum2.hasMoreElements()) {
                URL url = enum2.nextElement();

                // deconflict. Note that this is not, and cannot be, foolproof;
                // due to name-mangling by webstart. For example, if two plugins
                // called "test" and "Supertest" exist, test may find the data
                // file within Supertest because indexOf(test, SuperTest) >= 0
                // 
                // however, if we can get the mangled name and set it as the
                // 'pluginName', we can be foolproof.
                //
                String lcPath = url.getPath();
                String search = mro.getPluginName() + "!";

                if (lcPath.indexOf(search) >= 0) {
                    return url;
                }
            }
        }

        return null;
    }// getWSResource()

    /** 
     *   Get a resource for a variant. This uses the variantName to 
     *   deconflict, if multiple resources exist with the same name.
     *   <p>
     *   Conflict occur when plugins are loaded under the same ClassLoader,
     *   because variant plugin namespace is not unique.
     *   <p>
     *   This primarily applies to Webstart resources
     */
    private static URL getWSResource(URL packURL, URI uri) {
        /*
        NOTE: this method is used by getResource(URL, URI), which is
        chiefly used by VariantManager and associated parsers; a VariantRecord
        has not yet been created. So we cannot use that; the internal
        logic here is slightly different.
         */
        assert (vm.isInWebstart);

        ClassLoader cl = vm.getClass().getClassLoader();
        String deconflictName = getWSPluginName(packURL);

        Enumeration<URL> enum2 = null;

        try {
            enum2 = cl.getResources(uri.toString());
        } catch (IOException e) {
            return null;
        }

        while (enum2.hasMoreElements()) {
            URL url = enum2.nextElement();

            // deconflict. Note that this is not, and cannot be, foolproof;
            // due to name-mangling by webstart. For example, if two plugins
            // called "test" and "Supertest" exist, test may find the data
            // file within Supertest because indexOf(test, SuperTest) >= 0
            // 
            // however, if we can get the mangled name and set it as the
            // 'pluginName', we can be foolproof.
            //
            String lcPath = url.getPath();
            if (lcPath.indexOf(deconflictName) >= 0) {
                return url;
            }
        }

        return null;
    }// getWSResource()

    /** 
     *   Adds a Variant. If the variant already exists with the same
     *   name, checks the version. If the same version already exists,
     *   an exception is thrown. If not, the new version is also added.
     *   If we are in Web Start, however, no exception is thrown.
     *   <p>
     *   All names and aliases are mapped to the MapRec, not the VRec.
     *   When mapping an alias, if it corresponds to a DIFFERENT
     *   MapRec, an exception is thrown (this represents a non-unique
     *   alias).
     *   <p>
     *   NOTE: names and aliases are always mapped in all lower case.
     */
    private static void addVariant(Variant v, String pluginName, URL pluginURL) throws IOException {
        if (v == null || pluginName == null || pluginURL == null) {
            throw new IllegalArgumentException();
        }

        VRec vr = new VRec();
        vr.setPluginName(pluginName);
        vr.setURL(pluginURL);
        vr.setVariant(v);

        final String vName = v.getName().toLowerCase();

        // see if we are mapped to a MapRec already.
        //
        MapRec mapRec = vm.variantMap.get(vName);
        if (mapRec == null) {
            // not yet mapped! let's map it.
            mapRec = new MapRec(vr);
            vm.variantMap.put(vName, mapRec);
        } else {
            // we are mapped. See if this version has been added. 
            // If not, we'll add it.
            if (!mapRec.add(vr) && !vm.isInWebstart) {
                final VRec vrec2 = (VRec) mapRec.get(v.getVersion());
                final Variant v2 = vrec2.getVariant();

                // 2 variants with identical versions! we are confused!
                // try to provide as much helpful info as possible.
                throw new IOException("Two variants with identical version numbers have been found.\n"
                        + "Conflicting version: " + v.getVersion() + "\n" + "Variant 1: name=" + v.getName()
                        + "; pluginName = " + vr.getPluginName() + "; pluginURL = " + vr.getURL() + "\n"
                        + "Variant 2: name=" + v2.getName() + "; pluginName = " + vrec2.getPluginName()
                        + "; pluginURL = " + vrec2.getURL() + "\n");
            }
        }

        // map the aliases and/or check that aliases refer to the
        // same MapRec (this prevents two different Variants with the same
        // alias from causing a subtle error)
        //
        final String[] aliases = v.getAliases();
        for (int idx = 0; idx < aliases.length; idx++) {
            // not if it's "" though...
            if (!"".equals(aliases[idx])) {
                final String alias = aliases[idx].toLowerCase();
                MapRec testMapRec = vm.variantMap.get(alias);
                if (testMapRec == null) {
                    // add alias
                    vm.variantMap.put(alias, mapRec);
                } else if (testMapRec != mapRec) {
                    // ERROR! incorrect alias map
                    final Variant v2 = ((VRec) testMapRec.get(VERSION_OLDEST)).getVariant();
                    throw new IOException("Two variants have a conflicting (non-unique) alias.\n"
                            + "Variant 1: name=" + v.getName() + "; version=" + v.getVersion() + "; pluginName = "
                            + vr.getPluginName() + "; pluginURL = " + vr.getURL() + "\n" + "Variant 2: name="
                            + v2.getName() + "; (must check all variants with this name)\n");
                }
                // else {} : we are already mapped correctly. Nothing to change.
            }
        }
    }// addVariant()

    /** 
     *   Adds a SymbolPack. If the SymbolPack already exists with the same
     *   name, checks the version. If the same version already exists,
     *   an exception is thrown. If not, the new version is also added.
     *   <p>
     *   SymbolPacks do not support aliases.
     *   <p>
     *   Names are always mapped in all lower case.
     */
    private static void addSymbolPack(SymbolPack sp, String pluginName, URL pluginURL) throws IOException {
        if (sp == null || pluginName == null || pluginURL == null) {
            throw new IllegalArgumentException();
        }

        SPRec spRec = new SPRec();
        spRec.setPluginName(pluginName);
        spRec.setURL(pluginURL);
        spRec.setSymbolPack(sp);

        final String spName = sp.getName().toLowerCase();

        // see if we are mapped to a MapRec already.
        //
        MapRec mapRec = vm.symbolMap.get(spName);
        if (mapRec == null) {
            // not yet mapped! let's map it.
            mapRec = new MapRec(spRec);
            vm.symbolMap.put(spName, mapRec);
        } else {
            // we are mapped. See if this version has been added. 
            if (!mapRec.add(spRec) && !vm.isInWebstart) {
                SPRec spRec2 = (SPRec) mapRec.get(sp.getVersion());
                final SymbolPack sp2 = spRec2.getSymbolPack();
                if (sp2.getVersion() == sp.getVersion()) {
                    // 2 SymbolPacks with identical versions! we are confused!
                    // try to provide as much helpful info as possible.
                    throw new IOException("Two SymbolPcaks with identical version numbers have been found.\n"
                            + "Conflicting version: " + sp.getVersion() + "\n" + "SymbolPack 1: name="
                            + sp.getName() + "; pluginName = " + spRec.getPluginName() + "; pluginURL = "
                            + spRec.getURL() + "\n" + "SymbolPack 2: name=" + sp2.getName() + "; pluginName = "
                            + spRec2.getPluginName() + "; pluginURL = " + spRec2.getURL() + "\n");
                }
            }

            // we haven't been added (not a dupe); add
            mapRec.add(spRec);
        }
    }// addSymbolPack()

    /** Gets the VRec associated with a Variant (via name and version) */
    private static VRec getVRec(Variant v) {
        MapRec mapRec = vm.variantMap.get(v.getName().toLowerCase());
        return (VRec) mapRec.get(v.getVersion());
    }// getVRec()

    /** Gets the SPRec associated with a SymbolPack (via name and version) */
    private static SPRec getSPRec(SymbolPack sp) {
        MapRec mapRec = vm.symbolMap.get(sp.getName().toLowerCase());
        return (SPRec) mapRec.get(sp.getVersion());
    }// getSPRec()

    /** The value which is stored within the name mapping */
    private static class MapRec {

        private final List<MapRecObj> list = new ArrayList<MapRecObj>(2);

        // this constructor prevents us from having an empty list.
        public MapRec(MapRecObj obj) {
            if (obj == null) {
                throw new IllegalArgumentException();
            }
            list.add(obj);
        }// MapRec()

        /**
         *   Adds the MapRecObj to this MapRec, but only if it is of
         *   a unique version. If it is not, returns false. Otherwise,
         *   the MapRecObj is added and returns true.
         */
        public boolean add(MapRecObj obj) {
            for (final MapRecObj temp : list) {
                if (temp.getVersion() == obj.getVersion()) {
                    return false;
                }
            }

            list.add(obj);
            return true;
        }// add()

        /** Get all available versions */
        public float[] getVersions() {
            final float[] versions = new float[list.size()];
            for (int i = 0; i < list.size(); i++) {
                MapRecObj mro = list.get(i);
                versions[i] = mro.getVersion();
            }

            return versions;
        }// getVersions()

        /** 
         *   Get the desired version. Supports version constants. 
         *   Returns null if version not found (shouldn't occur if
         *   version constants used, and at least one element exists)
         */
        public MapRecObj get(final float version) {
            checkVersionConstant(version);

            final int size = list.size();

            // typical-case
            if (size == 1 && (version == VERSION_OLDEST || version == VERSION_NEWEST)) {
                return list.get(0);
            }

            MapRecObj selected = null;
            for (int i = 0; i < size; i++) {
                MapRecObj mro = list.get(i);
                selected = (selected == null) ? mro : selected;

                if ((version == VERSION_OLDEST && mro.getVersion() < selected.getVersion())
                        || (version == VERSION_NEWEST && mro.getVersion() > selected.getVersion())) {
                    selected = mro;
                } else if (mro.getVersion() == version) {
                    return mro;
                }
            }

            return selected;
        }// get()
    }// inner class VMRec

    /** MapRec stores a list of ObjRecs */
    private static abstract class MapRecObj {

        private URL fileURL;
        private String pluginName;

        public String getPluginName() {
            return pluginName;
        }

        public void setPluginName(String value) {
            pluginName = value;
        }

        public URL getURL() {
            return fileURL;
        }

        public void setURL(URL value) {
            fileURL = value;
        }

        public abstract float getVersion();
    }// inner class ObjRec

    /** An ObjRec for Variant objects */
    private static class VRec extends MapRecObj {

        private Variant variant;

        public Variant getVariant() {
            return variant;
        }

        public void setVariant(Variant value) {
            variant = value;
        }

        @Override
        public float getVersion() {
            return variant.getVersion();
        }
    }// inner class VRec

    /** An ObjRec for SymbolPack objects */
    private static class SPRec extends MapRecObj {

        private SymbolPack symbolPack;

        public SymbolPack getSymbolPack() {
            return symbolPack;
        }

        public void setSymbolPack(SymbolPack value) {
            symbolPack = value;
        }

        @Override
        public float getVersion() {
            return symbolPack.getVersion();
        }
    }// inner class SPRec
}// class VariantManager