/* * @(#)ResourceCatalog.java 1.6 05/11/17 * * Copyright (c) 2006 Sun Microsystems, Inc. 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 Sun Microsystems, Inc. or 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 MIDROSYSTEMS, 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. * * You acknowledge that this software is not designed, licensed or intended * for use in the design, construction, operation or maintenance of any * nuclear facility. */ 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()); } } } } }); } }