| /******************************************************************************* |
| * Copyright (c) 2000, 2016 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * 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; |
| } |
| } |