/*
 * Copyright (c) 2006, 2017, Oracle and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * -Redistribution of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *
 * -Redistribution in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation
 *  and/or other materials provided with the distribution.
 *
 * Neither the name of Oracle nor the names of contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN")
 * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
 * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 */

package jnlp.sample.servlet;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.BufferedInputStream;
import javax.servlet.ServletContext;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.w3c.dom.*;
import jnlp.sample.util.VersionString;
import jnlp.sample.util.VersionID;

public class ResourceCatalog {
    public static final String VERSION_XML_FILENAME = "version.xml";

    private Logger _log = null;
    private ServletContext _servletContext = null;

    private HashMap _entries;

    /** Class to contain the information we know
     *  about a specific directory
     */
    static private class PathEntries {
        /* Version-based entries at this particular path */
        private List _versionXmlList;
        private List _directoryList;
        private List _platformList;
        /* Last time this entry was updated */
        private long _lastModified; // Last modified time of entry;

        public PathEntries(List versionXmlList, List directoryList, List platformList, long lastModified) {
            _versionXmlList = versionXmlList;
            _directoryList = directoryList;
            _platformList = platformList;
            _lastModified = lastModified;
        }


        public void setDirectoryList(List dirList) {
            _directoryList = dirList;
        }

        public List getVersionXmlList() { return _versionXmlList; }
        public List getDirectoryList()  { return _directoryList; }
        public List getPlatformList()   { return _platformList; }

        public long getLastModified() { return _lastModified; }
    }

    public ResourceCatalog(ServletContext servletContext, Logger log) {
        _entries = new HashMap();
        _servletContext = servletContext;
        _log = log;
    }


    public JnlpResource lookupResource(DownloadRequest dreq) throws ErrorResponseException {
        // Split request up into path and name
        String path = dreq.getPath();
        String name = null;
        String dir  = null;
        int idx = path.lastIndexOf('/');
        if (idx == -1) {
            name = path;
        } else {
            name = path.substring(idx + 1); // Exclude '/'
            dir  = path.substring(0, idx + 1); // Include '/'
        }

        // Lookup up already parsed entries, and san directory for entries if neccesary
        PathEntries pentries =  (PathEntries)_entries.get(dir);
        JnlpResource xmlVersionResPath = new JnlpResource(_servletContext, dir + VERSION_XML_FILENAME);
        if (pentries == null || (xmlVersionResPath.exists() && xmlVersionResPath.getLastModified() > pentries.getLastModified())) {
            _log.addInformational("servlet.log.scandir", dir);
            List dirList = scanDirectory(dir, dreq);
            // Scan XML file
            List versionList = new ArrayList();
            List platformList = new ArrayList();
            parseVersionXML(versionList, platformList, dir, xmlVersionResPath);
            pentries = new PathEntries(versionList, dirList, platformList, xmlVersionResPath.getLastModified());
            _entries.put(dir, pentries);
        }

        // Search for a match
        JnlpResource[] result = new JnlpResource[1];

        if (dreq.isPlatformRequest()) {
            int sts = findMatch(pentries.getPlatformList(), name, dreq, result);
            if (sts != DownloadResponse.STS_00_OK) {
                throw new ErrorResponseException(DownloadResponse.getJnlpErrorResponse(sts));
            }
        } else {
            // First lookup in versions.xml file
            int sts1 = findMatch(pentries.getVersionXmlList(), name, dreq, result);
            if (sts1 != DownloadResponse.STS_00_OK) {
                // Then lookup in directory
                int sts2 = findMatch(pentries.getDirectoryList(), name, dreq, result);
                if (sts2 != DownloadResponse.STS_00_OK) {

                    // fix for 4450104
                    // try rescan and see if it helps
                    pentries.setDirectoryList(scanDirectory(dir, dreq));
                    sts2 = findMatch(pentries.getDirectoryList(), name, dreq, result);
                    // try again after rescanning directory
                    if (sts2 != DownloadResponse.STS_00_OK) {
                        // Throw the most specific error code
                        throw new ErrorResponseException(DownloadResponse.getJnlpErrorResponse(Math.max(sts1, sts2)));
                    }
                }
            }
        }
        return result[0];
    }

    /** This method finds the best match, or return the best error code. The
     *  result parameter must be an array with room for one element.
     *
     *  If a match is found, the method returns DownloadResponse.STS_00_OK
     *  If one or more entries matches on: name, version-id, os, arch, and locale,
     *  then the one with the highest version-id is set in the result[0] field.
     *
     *  If a match is not found, it returns an error code, either: ERR_10_NO_RESOURCE,
     *  ERR_11_NO_VERSION, ERR_20_UNSUP_OS, ERR_21_UNSUP_ARCH, ERR_22_UNSUP_LOCALE,
     *  ERR_23_UNSUP_JRE.
     *
     */
    public int findMatch(List list, String name, DownloadRequest dreq, JnlpResource[] result) {
        if (list == null) return DownloadResponse.ERR_10_NO_RESOURCE;
        // Setup return values
        VersionID bestVersionId = null;
        int error = DownloadResponse.ERR_10_NO_RESOURCE;
        VersionString vs = new VersionString(dreq.getVersion());
        // Iterate through entries
        for(int i = 0; i < list.size(); i++) {
            JnlpResource respath = (JnlpResource)list.get(i);
            VersionID vid = new VersionID(respath.getVersionId());
            int sts = matchEntry(name, vs, dreq, respath, vid);
            if (sts == DownloadResponse.STS_00_OK) {
                if (result[0] == null || vid.isGreaterThan(bestVersionId)) {
                    result[0] = respath;
                    bestVersionId = vid;
                }
            } else {
                error = Math.max(error, sts);
            }
        }
        return (result[0] != null) ? DownloadResponse.STS_00_OK : error;
    }

    public int matchEntry(String name, VersionString vs, DownloadRequest dreq, JnlpResource jnlpres, VersionID vid) {
        if (!name.equals(jnlpres.getName())) {
            return DownloadResponse.ERR_10_NO_RESOURCE;
        }
        if (!vs.contains(vid)) {
            return DownloadResponse.ERR_11_NO_VERSION;
        }
        if (!prefixMatchLists(jnlpres.getOSList(), dreq.getOS())) {
            return DownloadResponse.ERR_20_UNSUP_OS;
        }
        if (!prefixMatchLists(jnlpres.getArchList(), dreq.getArch())) {
            return DownloadResponse.ERR_21_UNSUP_ARCH;
        }
        if (!prefixMatchLists(jnlpres.getLocaleList(), dreq.getLocale())) {
            return DownloadResponse.ERR_22_UNSUP_LOCALE;
        }
        return DownloadResponse.STS_00_OK;
    }


    private static boolean prefixMatchStringList(String[] prefixList, String target) {
        // No prefixes matches everything
        if (prefixList == null) return true;
        // No target, but a prefix list does not match anything
        if (target == null) return false;
        for(int i = 0; i < prefixList.length; i++) {
            if (target.startsWith(prefixList[i])) return true;
        }
        return false;
    }

    /* Return true if at least one of the strings in 'prefixes' are a prefix
     * to at least one of the 'keys'.
     */
    public boolean prefixMatchLists(String[] prefixes, String[] keys) {
        // The prefixes are part of the server resources. If none is given,
        // everything matches
        if (prefixes == null) return true;
        // If no os keyes was given, and the server resource is keyed of this,
        // then return false.
        if (keys  == null) return false;
        // Check for a match on a key
        for(int i = 0; i < keys.length; i++) {
            if (prefixMatchStringList(prefixes, keys[i])) return true;
        }
        return false;
    }

    /** This method scans the directory pointed to by the
     *  given path and creates a list of ResourcePath elements
     *  that contains information about all the entries
     *
     *  The version-based information is encoded in the file name
     *  given the following format:
     *
     *     entry ::= <name> __ ( <options> ). <ext>
     *     options ::= <option> ( __ <options>  )?
     *     option  ::= V<version-id>
     *               | O<os>
     *               | A<arch>
     *               | L<locale>
     *
     */


    private String jnlpGetPath(DownloadRequest dreq) {
        // fix for 4474021
        // try to manuually generate the filename
        // extract file name
        String path = dreq.getPath();
        String filename = path.substring(path.lastIndexOf("/") + 1);
        path = path.substring(0, path.lastIndexOf("/") + 1);
        String name = filename;
        String ext = null;

        if (filename.lastIndexOf(".") != -1) {
            ext = filename.substring(filename.lastIndexOf(".") + 1);

            filename = filename.substring(0, filename.lastIndexOf("."));

        }
        if (dreq.getVersion() != null) {
            filename += "__V" + dreq.getVersion();
        }

        String[] temp = dreq.getOS();

        if (temp != null) {
            for (int i=0; i<temp.length; i++) {
                filename += "__O" + temp[i];
            }
        }

        temp = dreq.getArch();

        if (temp != null) {
            for (int i=0; i<temp.length; i++) {
                filename += "__A" + temp[i];
            }
        }
        temp = dreq.getLocale();

        if (temp != null) {
            for (int i=0; i<temp.length; i++) {
                filename += "__L" + temp[i];
            }
        }

        if (ext != null) {
            filename += "." + ext;
        }

        path += filename;

        return path;
    }

    public List scanDirectory(String dirPath, DownloadRequest dreq) {
        ArrayList list = new ArrayList();

        // fix for 4474021
        if (_servletContext.getRealPath(dirPath) == null) {
            String path = jnlpGetPath(dreq);

            String name = dreq.getPath().substring(path.lastIndexOf("/") + 1);

            JnlpResource jnlpres = new JnlpResource(_servletContext, name, dreq.getVersion(), dreq.getOS(), dreq.getArch(), dreq.getLocale(), path, dreq.getVersion());

            // the file does not exist
            if (jnlpres.getResource() == null) return null;

            list.add(jnlpres);
            return list;
        }
        File dir = new File(_servletContext.getRealPath(dirPath));
        _log.addDebug("File directory: " + dir);
        if (dir.exists() && dir.isDirectory()) {
            File[] entries = dir.listFiles();
            for(int i = 0; i < entries.length; i++) {
                JnlpResource jnlpres = parseFileEntry(dirPath, entries[i].getName());
                if (jnlpres != null) {
                    if (_log.isDebugLevel()) {
                        _log.addDebug("Read file resource: " + jnlpres);
                    }
                    list.add(jnlpres);
                }
            }
        }
        return list;
    }

    private JnlpResource parseFileEntry(String dir, String filename) {
        int idx = filename .indexOf("__");
        if (idx == -1) return null;

        // Cut out name
        String name = filename.substring(0, idx);
        String rest = filename.substring(idx);

        // Cut out extension
        idx = rest.lastIndexOf('.');
        String extension = "";
        if (idx != -1 ) {
            extension = rest.substring(idx);
            rest = rest .substring(0, idx);
        }

        // Parse options
        String versionId = null;
        ArrayList osList = new ArrayList();
        ArrayList archList = new ArrayList();
        ArrayList localeList = new ArrayList();
        while(rest.length() > 0) {
            /* Must start with __ at this point */
            if (!rest.startsWith("__")) return null;
            rest = rest.substring(2);
            // Get option and argument
            char option = rest.charAt(0);
            idx = rest.indexOf("__");
            String arg = null;
            if (idx == -1) {
                arg = rest.substring(1);
                rest = "";
            } else {
                arg = rest.substring(1, idx);
                rest = rest.substring(idx);
            }
            switch(option) {
                case 'V': versionId = arg; break;
                case 'O': osList.add(arg); break;
                case 'A': archList.add(arg); break;
                case 'L': localeList.add(arg); break;
                default: return null; // error
            }
        }

        return new JnlpResource(_servletContext,
                                name + extension, /* Resource name in URL request */
                                versionId,
                                listToStrings(osList),
                                listToStrings(archList),
                                listToStrings(localeList),
                                dir + filename, /* Resource name in WAR file */
                                versionId);
    }

    private String[] listToStrings(List list) {
        if (list.size() == 0) return null;
        return (String[])list.toArray(new String[list.size()]);
    }

    // Returns false if parsing failed
    private void parseVersionXML(final List versionList, final List platformList,
                                 final String dir, final JnlpResource versionRes) {
        if (!versionRes.exists()) return;

        // Parse XML into a more understandable format
        XMLNode root = null;
        try {
            DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
            Document doc = docBuilder.parse(new BufferedInputStream(versionRes.getResource().openStream()));
            doc.getDocumentElement().normalize();

            // Convert document into an XMLNode structure, since we already got utility methods
            //  to handle these. We should really use the data-binding stuff here - but that will come
            //  later
            //
            root = XMLParsing.convert(doc.getDocumentElement());
        } catch (SAXParseException err) {
            _log.addWarning("servlet.log.warning.xml.parsing",
                            versionRes.getPath(),
                            Integer.toString(err.getLineNumber()),
                            err.getMessage());
            return;
        } catch (Throwable t) {
            _log.addWarning("servlet.log.warning.xml.reading", versionRes.getPath(), t);
            return;
        }

        // Check that root element is a <jnlp> tag
        if (!root.getName().equals("jnlp-versions")) {
            _log.addWarning("servlet.log.warning.xml.missing-jnlp", versionRes.getPath());
            return;
        }

        // Visit all <resource> elements
        XMLParsing.visitElements(root, "<resource>", new XMLParsing.ElementVisitor() {
                    public void visitElement(XMLNode node) {
                        XMLNode pattern = XMLParsing.findElementPath(node, "<pattern>");
                        if (pattern == null) {
                            _log.addWarning("servlet.log.warning.xml.missing-pattern", versionRes.getPath());
                        } else {
                            // Parse pattern
                            String name =      XMLParsing.getElementContent(pattern , "<name>", "");
                            String versionId = XMLParsing.getElementContent(pattern , "<version-id>");
                            String[] os      = XMLParsing.getMultiElementContent(pattern, "<os>");
                            String[] arch    = XMLParsing.getMultiElementContent(pattern, "<arch>");
                            String[] locale = XMLParsing.getMultiElementContent(pattern, "<locale>");
                            // Get return request
                            String file = XMLParsing.getElementContent(node, "<file>");
                            if (versionId == null || file == null) {
                                _log.addWarning("servlet.log.warning.xml.missing-elems", versionRes.getPath());
                            } else {
                                JnlpResource res = new JnlpResource(_servletContext,
                                                                    name,
                                                                    versionId,
                                                                    os,
                                                                    arch,
                                                                    locale,
                                                                    dir + file,
                                                                    versionId);
                                if (res.exists()) {
                                    versionList.add(res);
                                    if (_log.isDebugLevel()) {
                                        _log.addDebug("Read resource: " + res);
                                    }
                                } else {
                                    _log.addWarning("servlet.log.warning.missing-file", file, versionRes.getPath());
                                }
                            }
                        }
                    }
                });

        // Visit all <resource> elements
        XMLParsing.visitElements(root, "<platform>", new XMLParsing.ElementVisitor() {
                    public void visitElement(XMLNode node) {
                        XMLNode pattern = XMLParsing.findElementPath(node, "<pattern>");
                        if (pattern == null) {
                            _log.addWarning("servlet.log.warning.xml.missing-pattern", versionRes.getPath());
                        } else {
                            // Parse pattern
                            String name =      XMLParsing.getElementContent(pattern , "<name>", "");
                            String versionId = XMLParsing.getElementContent(pattern , "<version-id>");
                            String[] os      = XMLParsing.getMultiElementContent(pattern, "<os>");
                            String[] arch    = XMLParsing.getMultiElementContent(pattern, "<arch>");
                            String[] locale = XMLParsing.getMultiElementContent(pattern, "<locale>");
                            // Get return request
                            String file = XMLParsing.getElementContent(node, "<file>");
                            String productId = XMLParsing.getElementContent(node, "<product-version-id>");

                            if (versionId == null || file == null || productId == null) {
                                _log.addWarning("servlet.log.warning.xml.missing-elems2", versionRes.getPath());
                            } else {
                                JnlpResource res = new JnlpResource(_servletContext,
                                                                    name,
                                                                    versionId,
                                                                    os,
                                                                    arch,
                                                                    locale,
                                                                    dir + file,
                                                                    productId);
                                if (res.exists()) {
                                    platformList.add(res);
                                    if (_log.isDebugLevel()) {
                                        _log.addDebug("Read platform resource: " + res);
                                    }
                                } else {
                                    _log.addWarning("servlet.log.warning.missing-file", file, versionRes.getPath());
                                }
                            }
                        }
                    }
                });
    }
}
