/******************************************************************************* * Copyright (c) 2000, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.help.internal.util; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtension; import org.eclipse.core.runtime.IExtensionDelta; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.help.IHelpContentProducer; import org.eclipse.help.internal.HelpPlugin; import org.osgi.framework.Bundle; public class ResourceLocator { public static final String CONTENTPRODUCER_XP_NAME = "contentProducer"; //$NON-NLS-1$ public static final String BINDING = "binding"; //$NON-NLS-1$ public static final String CONTENTPRODUCER_XP_FULLNAME = HelpPlugin.PLUGIN_ID + "." + CONTENTPRODUCER_XP_NAME; //$NON-NLS-1$ private static Hashtable<String, Object> zipCache = new Hashtable<>(); private static final Object ZIP_NOT_FOUND = new Object(); // Indicates there is no dynamic content provider for a particular plugin private static final Object STATIC_DOCS_ONLY = ZIP_NOT_FOUND; // Map of document content providers by plug-in ID; private static Map<String, Object> contentProducers = new HashMap<>(2, 0.5f); static class ProducerDescriptor { private IHelpContentProducer producer; private IConfigurationElement config; public ProducerDescriptor(IConfigurationElement config) { this.config = config; } public boolean matches(String refId) { IExtension ex = config.getDeclaringExtension(); String id = ex.getUniqueIdentifier(); return id != null && id.equals(refId); } public void reset() { producer = null; } public IHelpContentProducer getProducer() { if (producer == null) { try { Object o = config.createExecutableExtension("producer"); //$NON-NLS-1$ if (o instanceof IHelpContentProducer) producer = (IHelpContentProducer) o; } catch (CoreException ce) { HelpPlugin .logError( "Exception occurred creating help content producer for plug-in " + config.getContributor().getName() + ".", ce); //$NON-NLS-1$ //$NON-NLS-2$ } } return producer; } } static { Platform.getExtensionRegistry().addRegistryChangeListener(event -> { IExtensionDelta[] deltas = event.getExtensionDeltas(HelpPlugin.PLUGIN_ID, CONTENTPRODUCER_XP_NAME); for (int i = 0; i < deltas.length; i++) { IExtension extension = deltas[i].getExtension(); String affectedPlugin = extension.getContributor().getName(); // reset producer for the affected plugin, // it will be recreated on demand synchronized (contentProducers) { Object obj = contentProducers.get(affectedPlugin); if (obj instanceof ProducerDescriptor) { ProducerDescriptor desc = (ProducerDescriptor) obj; desc.reset(); } } } }); } /** * Obtains content provider for a documentation plug-in, creates one if necessary. * * @param pluginId * @return ITopicContentProvider or null */ private static IHelpContentProducer getContentProducer(String pluginId) { synchronized (contentProducers) { Object obj = getProducerDescriptor(pluginId); if (obj == null || obj == STATIC_DOCS_ONLY) return null; return ((ProducerDescriptor) obj).getProducer(); } } private static Object getProducerDescriptor(String pluginId) { Object descriptor = contentProducers.get(pluginId); if (descriptor == null) { // first time for the plug-in, so attempt to // find and instantiate provider descriptor = createContentProducer(pluginId); if (descriptor == null) { descriptor = STATIC_DOCS_ONLY; } contentProducers.put(pluginId, descriptor); } return descriptor; } /** * Creates content proivider for a documentation plug-in * * @param pluginId * @return ITopicContentProvider or null */ private static ProducerDescriptor createContentProducer(String pluginId) { IConfigurationElement[] elements = Platform.getExtensionRegistry().getConfigurationElementsFor( CONTENTPRODUCER_XP_FULLNAME); if (elements.length == 0) { return null; } checkForDuplicateExtensionElements(elements); for (int i = 0; i < elements.length; i++) { IConfigurationElement element = elements[i]; if (!elements[i].getContributor().getName().equals(pluginId)) { continue; } if (BINDING.equals(element.getName())) { // producer binding - locate the descriptor // with the matching reference Id String refId = element.getAttribute("producerId"); //$NON-NLS-1$ if (refId != null) { return findContentProducer(elements, refId); } } else if (CONTENTPRODUCER_XP_NAME.equals(element.getName())) { return new ProducerDescriptor(element); } } return null; } private static boolean isCheckedForDuplicates = false; private static void checkForDuplicateExtensionElements(IConfigurationElement[] elements) { if (isCheckedForDuplicates) { return; } isCheckedForDuplicates = true; Set<String> logged = new HashSet<>(); Set<String> keys = new HashSet<>(); for (int i = 0; i < elements.length; i++) { IConfigurationElement element = elements[i]; String pluginName = element.getContributor().getName(); String key = pluginName; if (logged.contains(key)) { continue; } if (keys.contains(key)) { HelpPlugin.logWarning( "Extension " + CONTENTPRODUCER_XP_FULLNAME + //$NON-NLS-1$ "in " + pluginName + " contains more than <" //$NON-NLS-1$ //$NON-NLS-2$ + CONTENTPRODUCER_XP_NAME + "> or <" //$NON-NLS-1$ + BINDING + "> element. All but the first have been ignored."); //$NON-NLS-1$ logged.add(key); } else { keys.add(key); } } } private static ProducerDescriptor findContentProducer(IConfigurationElement [] elements, String refId) { // try existing ones for (Iterator<Object> iter = contentProducers.values().iterator(); iter.hasNext();) { Object obj = iter.next(); if (obj instanceof ProducerDescriptor) { ProducerDescriptor desc = (ProducerDescriptor) obj; if (desc.matches(refId)) return desc; } } // not created yet. Find the matching configuration element, // take its contributing pluginId and get the descriptor // for that plug-in for (int i=0; i<elements.length; i++) { if (CONTENTPRODUCER_XP_NAME.equals(elements[i].getName())) { String id = elements[i].getDeclaringExtension().getUniqueIdentifier(); if (refId.equals(id)) { Object obj = getProducerDescriptor(elements[i].getContributor().getName()); if (obj instanceof ProducerDescriptor) return (ProducerDescriptor)obj; } } } return null; } /** * Opens an input stream to a file contained in a plugin. This includes NL lookup. */ public static InputStream openFromProducer(Bundle pluginDesc, String file, String locale) { IHelpContentProducer producer = getContentProducer(pluginDesc.getSymbolicName()); if (producer == null) { return null; } if (locale == null || locale.length() <= 0) { locale = Platform.getNL(); } Locale l; if (locale.length() >= 5) { l = new Locale(locale.substring(0, 2), locale.substring(3, 5)); } else if (locale.length() >= 2) { l = new Locale(locale.substring(0, 2), ""); //$NON-NLS-1$ } else { l = Locale.getDefault(); } return producer.getInputStream(pluginDesc.getSymbolicName(), file, l); } /** * Opens an input stream to a file contained in a plugin. This includes includes OS, WS and NL * lookup. * * @param pluginId * the plugin id of the plugin that contains the file you are trying to find * @param file * the relative path of the file to find * @param locale * the locale used as an override or <code>null</code> to use the default locale * * @return an InputStream to the file or <code>null</code> if the file wasn't found */ public static InputStream openFromPlugin(String pluginId, String file, String locale) { Bundle bundle = Platform.getBundle(pluginId); if (bundle != null) return openFromPlugin(bundle, file, locale); return null; } /** * Opens an input stream to a file contained in a zip in a plugin. This includes OS, WS and NL * lookup. * * @param pluginDesc * the plugin description of the plugin that contains the file you are trying to find * @param file * the relative path of the file to find * @param locale * the locale used as an override or <code>null</code> to use the default locale * * @return an InputStream to the file or <code>null</code> if the file wasn't found */ public static InputStream openFromZip(Bundle pluginDesc, String zip, String file, String locale) { String pluginID = pluginDesc.getSymbolicName(); Map<String, Object> cache = zipCache; ArrayList<String> pathPrefix = getPathPrefix(locale); for (int i = 0; i < pathPrefix.size(); i++) { // finds the zip file by either using a cached location, or // calling Platform.find - the result is cached for future use. Object cached = cache.get(pluginID + '/' + pathPrefix.get(i) + zip); if (cached == null) { try { URL url = FileLocator.find(pluginDesc, new Path(pathPrefix.get(i) + zip), null); if (url != null) { URL realZipURL = FileLocator.toFileURL(FileLocator.resolve(url)); cached = realZipURL.toExternalForm(); } else { cached = ZIP_NOT_FOUND; } } catch (IOException ioe) { cached = ZIP_NOT_FOUND; } // cache it cache.put(pluginID + '/' + pathPrefix.get(i) + zip, cached); } if (cached == ZIP_NOT_FOUND || cached.toString().startsWith("jar:")) //$NON-NLS-1$ continue; // cached should be a zip file that is actually on the filesystem // now check if the file is in this zip try { URL jurl = new URL("jar", "", (String) cached + "!/" + file); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ URLConnection jconnection = jurl.openConnection(); jconnection.setDefaultUseCaches(false); jconnection.setUseCaches(false); return jconnection.getInputStream(); } catch (IOException ioe) { // a file not found exception is an io exception continue; } } // end for loop // we didn't find the file in any zip return null; } /** * Opens an input stream to a file contained in a plugin. This includes includes OS, WS and NL * lookup. * * @param pluginDesc * the plugin description of the plugin that contains the file you are trying to find * @param file * the relative path of the file to find * @param locale * the locale used as an override or <code>null</code> to use the default locale * * @return an InputStream to the file or <code>null</code> if the file wasn't found */ public static InputStream openFromPlugin(Bundle pluginDesc, String file, String locale) { ArrayList<String> pathPrefix = getPathPrefix(locale); URL flatFileURL = find(pluginDesc, new Path(file), pathPrefix); if (flatFileURL != null) try { return flatFileURL.openStream(); } catch (IOException e) { return null; } return null; } /* * Search the ws, os then nl for a resource. Platform.find can't be used directly with $nl$, * $os$ or $ws$ becuase the root directory will be searched too early. */ public static URL find(Bundle pluginDesc, IPath flatFilePath, ArrayList<String> pathPrefix) { // try to find the actual file. for (int i = 0; i < pathPrefix.size(); i++) { URL url = FileLocator.find(pluginDesc, new Path(pathPrefix.get(i) + flatFilePath), null); if (url != null) return url; } return null; } public static void clearZipCache() { zipCache = new Hashtable<>(); } /* * Gets an ArrayList that has the path prefixes to search. * * @param locale the locale used as an override or <code>null</code> to use the default locale * @return an ArrayList that has path prefixes that need to be search. The returned ArrayList * will have an entry for the root of the plugin. */ public static ArrayList<String> getPathPrefix(String locale) { ArrayList<String> pathPrefix = new ArrayList<>(5); // TODO add override for ws and os similar to how it's done with locale // now String ws = Platform.getWS(); String os = Platform.getOS(); if (locale == null) locale = Platform.getNL(); if (ws != null) pathPrefix.add("ws/" + ws + '/'); //$NON-NLS-1$ if (os != null && !os.equals("OS_UNKNOWN")) //$NON-NLS-1$ pathPrefix.add("os/" + os + '/'); //$NON-NLS-1$ if (locale != null && locale.length() >= 5) pathPrefix.add("nl/" + locale.substring(0, 2) + '/' + locale.substring(3, 5) + '/'); //$NON-NLS-1$ if (locale != null && locale.length() >= 2) pathPrefix.add("nl/" + locale.substring(0, 2) + '/'); //$NON-NLS-1$ // the plugin root pathPrefix.add(""); //$NON-NLS-1$ return pathPrefix; } /** * Finds all topics under specified directory (recursively). This includes includes OS, WS and * NL lookup. * * @param pluginDesc * the plugin description of the plugin that contains the file you are trying to find * @param directory * the relative path of the directory * @param locale * the locale used as an override or <code>null</code> to use the default locale * * @return an InputStream to the file or <code>null</code> if the file wasn't found */ public static Set<String> findTopicPaths(Bundle pluginDesc, String directory, String locale) { Set<String> ret = new HashSet<>(); findTopicPaths(pluginDesc, directory, locale, ret); return ret; } /** * @param pluginDesc * @param directory * @param locale * @param paths */ private static void findTopicPaths(Bundle pluginDesc, String directory, String locale, Set<String> paths) { if (directory.endsWith("/")) //$NON-NLS-1$ directory = directory.substring(0, directory.length() - 1); ArrayList<String> pathPrefix = getPathPrefix(locale); for (int i = 0; i < pathPrefix.size(); i++) { String path = pathPrefix.get(i) + directory; if (path.length() == 0) path = "/"; //$NON-NLS-1$ Enumeration<String> entries = pluginDesc.getEntryPaths(path); if (entries != null) { while (entries.hasMoreElements()) { String topicPath = entries.nextElement(); if (topicPath.endsWith("/")) { //$NON-NLS-1$ findTopicPaths(pluginDesc, topicPath, locale, paths); } else { paths.add(topicPath); } } } } } /** * Create a path for use in error messages that will identify the plugin and * file name as well as a resolved path (if available) which will give * information about which fragment the file was located in * @return pluginId/file followed by a resolved path if the file exists */ public static String getErrorPath(String pluginId, String file, String locale) { String resolvedPath = pluginId + '/' + file; try { ArrayList<String> pathPrefix = ResourceLocator.getPathPrefix(locale); Bundle bundle = Platform.getBundle(pluginId); URL rawURL = ResourceLocator.find(bundle, new Path(file), pathPrefix); URL resolvedURL = FileLocator.resolve(rawURL); resolvedPath += ", URL = " + resolvedURL.toExternalForm(); //$NON-NLS-1$ } catch (Exception e) { } return resolvedPath; } }