/******************************************************************************* * Copyright (c) 2006, 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.context; import java.io.FileNotFoundException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.transform.TransformerException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.Platform; import org.eclipse.help.AbstractContextProvider; import org.eclipse.help.IContext; import org.eclipse.help.IUAElement; import org.eclipse.help.internal.HelpPlugin; import org.eclipse.help.internal.Topic; import org.eclipse.help.internal.UAElement; import org.eclipse.help.internal.dynamic.DocumentProcessor; import org.eclipse.help.internal.dynamic.DocumentReader; import org.eclipse.help.internal.dynamic.DocumentWriter; import org.eclipse.help.internal.dynamic.ExtensionHandler; import org.eclipse.help.internal.dynamic.IncludeHandler; import org.eclipse.help.internal.dynamic.ProcessorHandler; import org.eclipse.help.internal.dynamic.ValidationHandler; import org.eclipse.help.internal.toc.HrefUtil; import org.eclipse.help.internal.util.ResourceLocator; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; /* * Provides context-sensitive help data to the help system, contributed from * context XML files. */ public class ContextFileProvider extends AbstractContextProvider { private static final String EXTENSION_POINT_CONTEXTS = "org.eclipse.help.contexts"; //$NON-NLS-1$ private static final String ELEMENT_CONTEXTS = "contexts"; //$NON-NLS-1$ private static final String ATTRIBUTE_FILE = "file"; //$NON-NLS-1$ private static final String ATTRIBUTE_PLUGIN = "plugin"; //$NON-NLS-1$ // locale -> Map(pluginId -> Map(shortContextId -> Context)[]) private Map<String, Map<String, Map<String, Context>[]>> pluginContextsByLocale; // pluginId -> ContextFile[] private Map<String, ContextFile[]> descriptorsByPluginId; // locale -> Map(ContextFile -> Map(shortContextId -> Context)) private Map<String, Map<ContextFile, Map<String, Context>>> contextFilesByLocale; private DocumentProcessor processor; private DocumentReader reader; private DocumentWriter writer; private Map<String, String[]> requiredAttributes; @Override public IContext getContext(String contextId, String locale) { int index = contextId.lastIndexOf('.'); String pluginId = contextId.substring(0, index); String shortContextId = contextId.substring(index + 1); if (pluginContextsByLocale == null) { pluginContextsByLocale = new HashMap<>(); } Map<String, Map<String, Context>[]> pluginContexts = pluginContextsByLocale.get(locale); if (pluginContexts == null) { pluginContexts = new HashMap<>(); pluginContextsByLocale.put(locale, pluginContexts); } Map<String, Context>[] contexts = pluginContexts.get(pluginId); if (contexts == null) { contexts = getPluginContexts(pluginId, locale); pluginContexts.put(pluginId, contexts); } ArrayList<IContext> matches = new ArrayList<>(); for (int i=0;i<contexts.length;++i) { // Search for contexts Context context = contexts[i].get(shortContextId); if (context != null) { matches.add(context); } } switch (matches.size()) { case 0: return null; case 1: return matches.get(0); default: // Merge the contexts - this is the least common case Context newContext = new Context(matches.get(0), shortContextId); for (int i = 1; i < matches.size(); i++) { newContext.mergeContext(matches.get(i)); } return newContext; } } @Override public String[] getPlugins() { Map<String, ContextFile[]> associations = getPluginAssociations(); return associations.keySet().toArray(new String[associations.size()]); } /* * Returns a mapping of plug-in IDs to arrays of context files that apply * to that plug-in (pluginId -> ContextFile[]). */ private Map<String, ContextFile[]> getPluginAssociations() { if (descriptorsByPluginId == null) { descriptorsByPluginId = new HashMap<>(); IConfigurationElement[] elements = Platform.getExtensionRegistry().getConfigurationElementsFor(EXTENSION_POINT_CONTEXTS); for (int i=0;i<elements.length;++i) { if (ELEMENT_CONTEXTS.equals(elements[i].getName())) { String declaringPluginId = elements[i].getDeclaringExtension().getContributor().getName(); String file = elements[i].getAttribute(ATTRIBUTE_FILE); String plugin = elements[i].getAttribute(ATTRIBUTE_PLUGIN); String targetPluginId = (plugin == null ? declaringPluginId : plugin); ContextFile descriptor = new ContextFile(declaringPluginId, file); ContextFile[] descriptors = descriptorsByPluginId.get(targetPluginId); if (descriptors == null) { descriptors = new ContextFile[] { descriptor }; } else { ContextFile[] temp = new ContextFile[descriptors.length + 1]; System.arraycopy(descriptors, 0, temp, 0, descriptors.length); temp[descriptors.length] = descriptor; descriptors = temp; } descriptorsByPluginId.put(targetPluginId, descriptors); } } } return descriptorsByPluginId; } /* * Returns the context definitions for the given plug-in and locale, * as a mapping of short IDs to Context objects (shortContextId -> Context). */ @SuppressWarnings("unchecked") public Map<String, Context>[] getPluginContexts(String pluginId, String locale) { List<Map<String, Context>> maps = new ArrayList<>(); Map<String, ContextFile[]> associations = getPluginAssociations(); ContextFile[] descriptors = associations.get(pluginId); for (int i=0;i<descriptors.length;++i) { Map<String, Context> contexts = getContexts(descriptors[i], locale); if (contexts != null) { maps.add(contexts); } } return maps.toArray(new Map[maps.size()]); } /* * Returns the context definitions stored in the given file for the given * locale (shortContextId -> Context). */ private Map<String, Context> getContexts(ContextFile descriptor, String locale) { if (contextFilesByLocale == null) { contextFilesByLocale = new HashMap<>(); } Map<ContextFile, Map<String, Context>> contextsByDescriptor = contextFilesByLocale.get(locale); if (contextsByDescriptor == null) { contextsByDescriptor = new HashMap<>(); contextFilesByLocale.put(locale, contextsByDescriptor); } Map<String, Context> contexts = contextsByDescriptor.get(descriptor); if (contexts == null) { contexts = loadContexts(descriptor, locale); if (contexts != null) { contextsByDescriptor.put(descriptor, contexts); } } return contexts; } /* * Loads the given context file for the given locale, and returns its * contents as a mapping from short context ids to Context objects * (shortContextId -> Context). */ private Map<String, Context> loadContexts(ContextFile descriptor, String locale) { // load the file try (InputStream in = ResourceLocator.openFromPlugin(descriptor.getBundleId(), descriptor.getFile(), locale)) { if (in != null) { return loadContextsFromInputStream(descriptor, locale, in); } else { throw new FileNotFoundException(); } } catch (Throwable t) { String msg = "Error reading context-sensitive help file /\"" + getErrorPath(descriptor, locale) + "\" (skipping file)"; //$NON-NLS-1$ //$NON-NLS-2$ HelpPlugin.logError(msg, t); } return null; } private Map<String, Context> loadContextsFromInputStream(ContextFile descriptor, String locale, InputStream in) throws Exception { if (reader == null) { reader = new DocumentReader(); } UAElement root = reader.read(in); if ("contexts".equals(root.getElementName())) { //$NON-NLS-1$ // process dynamic content if (processor == null) { processor = new DocumentProcessor(new ProcessorHandler[] { new ValidationHandler(getRequiredAttributes()), new NormalizeHandler(), new IncludeHandler(reader, locale), new ExtensionHandler(reader, locale) }); } processor.process(root, '/' + descriptor.getBundleId() + '/' + descriptor.getFile()); // build map IUAElement[] children = root.getChildren(); Map<String, Context> contexts = new HashMap<>(); for (int i=0;i<children.length;++i) { if (children[i] instanceof Context) { Context context = (Context)children[i]; String id = context.getId(); if (id != null) { Object existingContext = contexts.get(id); if (existingContext==null) contexts.put(id, context); else { ((Context)existingContext).mergeContext(context); if (HelpPlugin.DEBUG_CONTEXT) { String error = "Context help ID '"+id+"' is found multiple times in file '"+descriptor.getBundleId()+'/'+descriptor.getFile()+"'\n"+ //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ " Description 1: "+((Context)existingContext).getText()+'\n'+ //$NON-NLS-1$ " Description 2: "+context.getText(); //$NON-NLS-1$ System.out.println(error); } } } } } return contexts; } else { String msg = "Required root element \"contexts\" missing from context-sensitive help file \"/" + getErrorPath(descriptor, locale) + "\" (skipping)"; //$NON-NLS-1$ //$NON-NLS-2$ HelpPlugin.logError(msg); } return null; } private String getErrorPath(ContextFile descriptor, String locale) { return ResourceLocator.getErrorPath(descriptor.getBundleId(), descriptor.getFile(), locale); } private Map<String, String[]> getRequiredAttributes() { if (requiredAttributes == null) { requiredAttributes = new HashMap<>(); requiredAttributes.put(Context.NAME, new String[] { Context.ATTRIBUTE_ID }); requiredAttributes.put(Topic.NAME, new String[] { Topic.ATTRIBUTE_LABEL, Topic.ATTRIBUTE_HREF }); requiredAttributes.put("anchor", new String[] { "id" }); //$NON-NLS-1$ //$NON-NLS-2$ requiredAttributes.put("include", new String[] { "path" }); //$NON-NLS-1$ //$NON-NLS-2$ } return requiredAttributes; } /* * Handler that normalizes: * 1. Descriptions - any child elements like bold tags are serialized and inserted into the * text node under the description element. * 2. Related topic hrefs - convert from relative (e.g. "path/file.html") to absolute hrefs * (e.g. "/plugin.id/path/file.html"). */ private class NormalizeHandler extends ProcessorHandler { @Override public short handle(UAElement element, String id) { if (element instanceof Context) { Context context = (Context)element; IUAElement[] children = context.getChildren(); if (children.length > 0 && Context.ELEMENT_DESCRIPTION.equals(((UAElement)children[0]).getElementName())) { StringBuffer buf = new StringBuffer(); Element description = ((UAElement)children[0]).getElement(); Node node = description.getFirstChild(); while (node != null) { if (node.getNodeType() == Node.TEXT_NODE) { buf.append(node.getNodeValue()); } else if (node.getNodeType() == Node.ELEMENT_NODE) { if (writer == null) { writer = new DocumentWriter(); } try { buf.append(writer.writeString((Element)node, false)); } catch (TransformerException e) { String msg = "Internal error while normalizing context-sensitive help descriptions"; //$NON-NLS-1$ HelpPlugin.logError(msg, e); } } Node old = node; node = node.getNextSibling(); description.removeChild(old); } Document document = description.getOwnerDocument(); description.appendChild(document.createTextNode(buf.toString())); } } else if (element instanceof Topic) { Topic topic = (Topic)element; String href = topic.getHref(); if (href != null) { int index = id.indexOf('/', 1); if (index != -1) { String pluginId = id.substring(1, index); topic.setHref(HrefUtil.normalizeHref(pluginId, href)); } } } // give other handlers an opportunity to process return UNHANDLED; } } }