| /******************************************************************************* |
| * 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 |
| * James Blackburn - fix for Bug 230842 |
| *******************************************************************************/ |
| package org.eclipse.help.internal.toc; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.core.runtime.IExtensionRegistry; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.help.AbstractTocProvider; |
| import org.eclipse.help.IToc; |
| import org.eclipse.help.ITocContribution; |
| import org.eclipse.help.ITopic; |
| import org.eclipse.help.internal.HelpData; |
| import org.eclipse.help.internal.HelpPlugin; |
| import org.eclipse.help.internal.Topic; |
| import org.eclipse.help.internal.UAElement; |
| import org.eclipse.help.internal.UAElementFactory; |
| |
| /* |
| * Manages toc contributions (TocContribution) supplied by the various toc |
| * providers (AbstractTocProvider). |
| */ |
| public class TocManager { |
| |
| private static final String EXTENSION_POINT_ID_TOC = HelpPlugin.PLUGIN_ID + ".toc"; //$NON-NLS-1$ |
| private static final String ELEMENT_NAME_TOC_PROVIDER = "tocProvider"; //$NON-NLS-1$ |
| private static final String ATTRIBUTE_NAME_CLASS = "class"; //$NON-NLS-1$ |
| |
| private AbstractTocProvider[] tocProviders; |
| // There are two sets of TOC contributions, one is used for Toc Assembly and is modified from the original |
| // The other is used by the TocServlet and is unprocessed, i.e. anchors are not replaced with the contributions |
| private Map<String, TocContribution[]> tocContributionsByLocale = new HashMap<>(); |
| private Map<String, TocContribution[]> tocContributionsForTocByLocale = new HashMap<>(); |
| private Map<String, Toc[]> tocsByLocale = new HashMap<>(); |
| private Map<String, Toc> tocsById = new HashMap<>(); |
| private Map<String, Toc> tocsByTopic; |
| |
| /* |
| * Returns all toc entries (complete books) for the given locale. |
| */ |
| public synchronized Toc[] getTocs(String locale) { |
| Toc[] tocs = tocsByLocale.get(locale); |
| if (tocs == null) { |
| long start = System.currentTimeMillis(); |
| if (HelpPlugin.DEBUG_TOC) { |
| System.out.println("Start to build toc for locale " + locale); //$NON-NLS-1$ |
| } |
| Set<String> tocsToFilter = getIgnoredTocContributions(); |
| TocContribution[] raw = getRootTocContributions(locale, tocsToFilter); |
| TocContribution[] filtered = filterTocContributions(raw, tocsToFilter); |
| ITocContribution[] ordered = new TocSorter().orderTocContributions(filtered); |
| List<Toc> orderedTocs = new ArrayList<>(ordered.length); |
| for (int i=0;i<ordered.length;++i) { |
| try { |
| Toc toc = (Toc)ordered[i].getToc(); |
| orderedTocs.add(toc); |
| tocsById.put(ordered[i].getId(), toc); |
| } |
| catch (Throwable t) { |
| // log and skip |
| String msg = "Error getting " + Toc.class.getName() + " from " + ITocContribution.class.getName() + ": " + ordered[i]; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| HelpPlugin.logError(msg, t); |
| } |
| } |
| tocs = orderedTocs.toArray(new Toc[orderedTocs.size()]); |
| TopicSorter topicSorter = new TopicSorter(); |
| for (int i = 0; i < tocs.length; i++) { |
| topicSorter.sortChildren(tocs[i]); |
| } |
| tocsByLocale.put(locale, tocs); |
| long stop = System.currentTimeMillis(); |
| if (HelpPlugin.DEBUG_TOC) { |
| System.out.println("Milliseconds to update toc for locale " + locale + " = " + (stop - start)); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } |
| return tocs; |
| } |
| |
| /* |
| * Returns the toc whose toc contribution has the given id, for the |
| * given locale. |
| */ |
| public synchronized Toc getToc(String id, String locale) { |
| getTocs(locale); |
| return tocsById.get(id); |
| } |
| |
| public synchronized Toc getOwningToc(String href) { |
| if (tocsByTopic == null) { |
| tocsByTopic = new HashMap<>(); |
| Toc[] tocs = HelpPlugin.getTocManager().getTocs(Platform.getNL()); |
| for (int i=0;i<tocs.length;++i) { |
| ITocContribution contribution = tocs[i].getTocContribution(); |
| String[] extraDocuments = contribution.getExtraDocuments(); |
| for (int j=0;j<extraDocuments.length;++j) { |
| tocsByTopic.put(extraDocuments[j], tocs[i]); |
| } |
| } |
| } |
| return tocsByTopic.get(href); |
| } |
| |
| public synchronized ITopic getTopic(String href, String locale) { |
| Toc[] tocs = HelpPlugin.getTocManager().getTocs(locale); |
| for (int i=0;i<tocs.length;++i) { |
| ITopic topic = tocs[i].getTopic(href); |
| if (topic != null) { |
| return topic; |
| } |
| } |
| int index = href.indexOf('#'); |
| if (index != -1) { |
| return getTopic(href.substring(0, index), locale); |
| } |
| return null; |
| } |
| |
| public synchronized int[] getTopicPath(String href, String locale) { |
| ITopic topic = getTopic(href, locale); |
| try { |
| if (topic != null && topic instanceof UAElement) { |
| List<Integer> path = new ArrayList<>(); |
| UAElement element = (UAElement) topic; |
| while (!(element instanceof Toc)) { |
| UAElement parent = element.getParentElement(); |
| path.add(Integer.valueOf(indexOf(parent, (Topic)element))); |
| element = parent; |
| } |
| Toc[] tocs = getTocs(locale); |
| for (int i=0;i<tocs.length;++i) { |
| if (tocs[i] == element) { |
| path.add(Integer.valueOf(i)); |
| int[] array = new int[path.size()]; |
| for (int j=0;j<array.length;++j) { |
| array[j] = path.get(array.length - 1 - j).intValue(); |
| } |
| return array; |
| } |
| } |
| } |
| } catch (Exception e) { |
| return null; |
| } |
| // no path; not in toc |
| return null; |
| } |
| |
| /* |
| * Returns the zero-based index at which the child topic is located under |
| * the parent topic/toc. |
| */ |
| private int indexOf(UAElement parent, Topic child) { |
| ITopic[] children; |
| if (parent instanceof Topic) { |
| children = ((Topic)parent).getSubtopics(); |
| } |
| else if (parent instanceof Toc) { |
| children = ((Toc)parent).getTopics(); |
| } |
| else { |
| return -1; |
| } |
| for (int i=0;i<children.length;++i) { |
| if (children[i] == child) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /* |
| * Returns all toc contributions for the given locale, from all toc |
| * providers. |
| */ |
| public TocContribution[] getTocContributions(String locale) { |
| return getAndCacheTocContributions(locale, tocContributionsByLocale); |
| } |
| |
| private TocContribution[] getTocContributionsForToc(String locale) { |
| return getAndCacheTocContributions(locale, tocContributionsForTocByLocale); |
| } |
| |
| private synchronized TocContribution[] getAndCacheTocContributions(String locale, |
| Map<String, TocContribution[]> contributionsByLocale) { |
| TocContribution[] cached = contributionsByLocale.get(locale); |
| if (cached == null) { |
| HashMap<String, TocContribution> contributions = new HashMap<>(); |
| AbstractTocProvider[] providers = getTocProviders(); |
| for (int i=0;i<providers.length;++i) { |
| ITocContribution[] contrib; |
| try { |
| contrib = providers[i].getTocContributions(locale); |
| for (int j=0;j<contrib.length;++j) { |
| TocContribution contribution = new TocContribution(); |
| contribution.setCategoryId(contrib[j].getCategoryId()); |
| contribution.setContributorId(contrib[j].getContributorId()); |
| contribution.setExtraDocuments(contrib[j].getExtraDocuments()); |
| contribution.setId(contrib[j].getId()); |
| contribution.setLocale(contrib[j].getLocale()); |
| contribution.setPrimary(contrib[j].isPrimary()); |
| IToc toc = contrib[j].getToc(); |
| Toc t = toc instanceof Toc ? (Toc)toc : (Toc)UAElementFactory.newElement(toc); |
| t.setLinkTo(contrib[j].getLinkTo()); |
| contribution.setToc(t); |
| if(!contributions.containsKey(contrib[j].getId())) |
| contributions.put(contrib[j].getId(), contribution); |
| } |
| } |
| catch (Throwable t) { |
| // log, and skip the offending provider |
| String msg = "Error getting help table of contents data from provider: " + providers[i].getClass().getName() + " (skipping provider)"; //$NON-NLS-1$ //$NON-NLS-2$ |
| HelpPlugin.logError(msg, t); |
| continue; |
| } |
| |
| } |
| cached = contributions.values().toArray(new TocContribution[contributions.size()]); |
| contributionsByLocale.put(locale, cached); |
| } |
| return cached; |
| } |
| |
| /* |
| * Clears all cached contributions, forcing the manager to query the |
| * providers again next time a request is made. |
| */ |
| public void clearCache() { |
| tocContributionsByLocale.clear(); |
| tocContributionsForTocByLocale.clear(); |
| tocsByLocale.clear(); |
| tocsById.clear(); |
| tocsByTopic = null; |
| tocProviders=null; |
| } |
| |
| /* |
| * Internal hook for unit testing. |
| */ |
| public AbstractTocProvider[] getTocProviders() { |
| if (tocProviders == null) { |
| List<AbstractTocProvider> providers = new ArrayList<>(); |
| IExtensionRegistry registry = Platform.getExtensionRegistry(); |
| IConfigurationElement[] elements = registry.getConfigurationElementsFor(EXTENSION_POINT_ID_TOC); |
| for (int i=0;i<elements.length;++i) { |
| IConfigurationElement elem = elements[i]; |
| if (elem.getName().equals(ELEMENT_NAME_TOC_PROVIDER)) { |
| try { |
| AbstractTocProvider provider = (AbstractTocProvider)elem.createExecutableExtension(ATTRIBUTE_NAME_CLASS); |
| providers.add(provider); |
| } |
| catch (CoreException e) { |
| // log and skip |
| String msg = "Error instantiating help table of contents provider class \"" + elem.getAttribute(ATTRIBUTE_NAME_CLASS) + '"'; //$NON-NLS-1$ |
| HelpPlugin.logError(msg, e); |
| } |
| } |
| } |
| Collections.sort(providers, new TocProviderComparator()); |
| tocProviders = providers.toArray(new AbstractTocProvider[providers.size()]); |
| } |
| return tocProviders; |
| } |
| |
| /* |
| * Internal hook for unit testing. |
| */ |
| public void setTocProviders(AbstractTocProvider[] tocProviders) { |
| this.tocProviders = tocProviders; |
| } |
| |
| /* |
| * Filters the given contributions according to product preferences. If |
| * either the contribution's id or its category's id is listed in the |
| * ignoredTocs, filter the contribution. |
| */ |
| private TocContribution[] filterTocContributions(TocContribution[] unfiltered, Set<String> tocsToFilter) { |
| List<TocContribution> filtered = new ArrayList<>(); |
| for (int i=0;i<unfiltered.length;++i) { |
| if (!tocsToFilter.contains(unfiltered[i].getId()) && |
| !tocsToFilter.contains(unfiltered[i].getCategoryId())) { |
| filtered.add(unfiltered[i]); |
| } |
| } |
| return filtered.toArray(new TocContribution[filtered.size()]); |
| } |
| |
| private TocContribution[] getRootTocContributions(String locale, Set<String> tocsToFilter) { |
| TocContribution[] contributions = getTocContributionsForToc(locale); |
| List<TocContribution> unassembled = new ArrayList<>(Arrays.asList(contributions)); |
| TocAssembler assembler = new TocAssembler(tocsToFilter); |
| List<TocContribution> assembled = assembler.assemble(unassembled); |
| return assembled.toArray(new TocContribution[assembled.size()]); |
| } |
| |
| private Set<String> getIgnoredTocContributions() { |
| HelpData helpData = HelpData.getProductHelpData(); |
| if (helpData != null) { |
| return helpData.getHiddenTocs(); |
| } |
| else { |
| HashSet<String> ignored = new HashSet<>(); |
| String preferredTocs = Platform.getPreferencesService().getString(HelpPlugin.PLUGIN_ID, HelpPlugin.IGNORED_TOCS_KEY, "", null); //$NON-NLS-1$ |
| if (preferredTocs.length() > 0) { |
| StringTokenizer suggestdOrderedInfosets = new StringTokenizer(preferredTocs, " ;,"); //$NON-NLS-1$ |
| while (suggestdOrderedInfosets.hasMoreTokens()) { |
| ignored.add(suggestdOrderedInfosets.nextToken()); |
| } |
| } |
| return ignored; |
| } |
| } |
| |
| /* |
| * Returns whether or not the toc for the given locale has been completely |
| * loaded yet or not. |
| */ |
| public boolean isTocLoaded(String locale) { |
| return tocsByLocale.get(locale) != null; |
| } |
| |
| } |