| /******************************************************************************* |
| * Copyright (c) 2006, 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.toc; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.help.ICriteria; |
| import org.eclipse.help.ITocContribution; |
| import org.eclipse.help.IUAElement; |
| import org.eclipse.help.internal.Anchor; |
| import org.eclipse.help.internal.HelpPlugin; |
| import org.eclipse.help.internal.Topic; |
| import org.eclipse.help.internal.UAElement; |
| import org.eclipse.help.internal.criteria.CriteriaProviderRegistry; |
| import org.eclipse.help.internal.dynamic.DocumentProcessor; |
| import org.eclipse.help.internal.dynamic.DocumentReader; |
| 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; |
| |
| /* |
| * Assembles toc contributions (toc fragments) into complete, linked, and |
| * assembled books. |
| */ |
| public class TocAssembler { |
| |
| private DocumentProcessor processor; |
| private ProcessorHandler[] handlers; |
| |
| private Map<String, Set<String>> anchorsByContributionId; |
| private List<TocContribution> contributions; |
| private Map<String, TocContribution> contributionsById; |
| private Map<String, ITocContribution[]> contributionsByLinkTo; |
| private Set<ITocContribution> processedContributions; |
| private Map<String, String[]> requiredAttributes; |
| private Set<String> tocsToFilter; |
| |
| |
| public TocAssembler() { |
| this.tocsToFilter = new HashSet<>(); |
| } |
| |
| public TocAssembler(Set<String> tocsToFilter) { |
| this.tocsToFilter = tocsToFilter; |
| } |
| |
| /* |
| * Assembles the given toc contributions into complete, linked |
| * books. The originals are not modified. |
| */ |
| public List<TocContribution> assemble(List<TocContribution> contributions) { |
| this.contributions = contributions; |
| anchorsByContributionId = null; |
| contributionsById = null; |
| contributionsByLinkTo = null; |
| processedContributions = null; |
| |
| List<TocContribution> books = getBooks(); |
| Iterator<TocContribution> iter = books.iterator(); |
| while (iter.hasNext()) { |
| TocContribution book = iter.next(); |
| process(book); |
| } |
| return books; |
| } |
| |
| /* |
| * Returns the list of contributions that should appear as root TOCs |
| * (books). Contributions are books if the following conditions are |
| * true: |
| * |
| * 1. isPrimary() returns true. |
| * 2. The toc has no "link_to" attribute defined (does not link into |
| * another toc), or the link_to target anchor doesn't exist. |
| * 3. No other toc has a link to this contribution (via "link" element). |
| */ |
| private List<TocContribution> getBooks() { |
| Map<String, String> linkedContributionIds = getLinkedContributionIds(contributions); |
| List<TocContribution> books = new ArrayList<>(); |
| Iterator<TocContribution> iter = contributions.iterator(); |
| while (iter.hasNext()) { |
| TocContribution contrib = iter.next(); |
| boolean isValidLinkTo = hasValidLinkTo(contrib); |
| boolean isLinkedId = linkedContributionIds.containsKey(contrib.getId()); |
| if (!isValidLinkTo && !isLinkedId) { |
| if (contrib.isPrimary()) { |
| books.add(contrib); |
| if (HelpPlugin.DEBUG_TOC) { |
| String msg = "Primary Toc Found: " + contrib.getId(); //$NON-NLS-1$ |
| String linkTo = contrib.getLinkTo(); |
| if (linkTo != null) { |
| msg += " - cannot find link to: "; //$NON-NLS-1$ |
| msg += linkTo; |
| } |
| System.out.println(msg); |
| } |
| } else { |
| if (HelpPlugin.DEBUG_TOC) { |
| String msg = "Table of contents is not primary and not linked to another TOC " + contrib.getId() + " (skipping)"; //$NON-NLS-1$ //$NON-NLS-2$ |
| System.out.println(msg); |
| } |
| } |
| } else { |
| contrib.setSubToc(true); |
| if (HelpPlugin.DEBUG_TOC) { |
| String msg = "Toc " + contrib.getId(); //$NON-NLS-1$ |
| if (isValidLinkTo) { |
| msg += " has a valid link to " + contrib.getLinkTo(); //$NON-NLS-1$ |
| } |
| if (isLinkedId) { |
| msg += " is linked from " + linkedContributionIds.get(contrib.getId()); //$NON-NLS-1$ |
| } |
| System.out.println(msg); |
| } |
| } |
| } |
| return books; |
| } |
| |
| /* |
| * Returns the set of ids of contributions that are linked to by other |
| * contributions, i.e. at least one other contribution has a link element |
| * pointing to it. |
| */ |
| private Map<String, String> getLinkedContributionIds(List<TocContribution> contributions) { |
| if (processor == null) { |
| processor = new DocumentProcessor(); |
| } |
| final Map<String, String> linkedContributionIds = new HashMap<>(); |
| ProcessorHandler[] linkFinder = new ProcessorHandler[] { |
| new ValidationHandler(getRequiredAttributes()), |
| new ProcessorHandler() { |
| @Override |
| public short handle(UAElement element, String id) { |
| if (element instanceof Link) { |
| Link link = (Link)element; |
| String toc = link.getToc(); |
| if (toc != null) { |
| TocContribution srcContribution = getContribution(id); |
| linkedContributionIds.put(HrefUtil.normalizeHref(srcContribution.getContributorId(), toc), id); |
| } |
| } |
| return UNHANDLED; |
| } |
| } |
| }; |
| processor.setHandlers(linkFinder); |
| ListIterator<TocContribution> iter = contributions.listIterator(); |
| while (iter.hasNext()) { |
| TocContribution contrib = iter.next(); |
| try { |
| String id = contrib.getId(); |
| if (!tocsToFilter.contains(id)) { |
| processor.process((Toc)contrib.getToc(), id); |
| } |
| } |
| catch (Throwable t) { |
| iter.remove(); |
| String msg = "Error processing help table of contents: " + contrib.getId() + " (skipping)"; //$NON-NLS-1$ //$NON-NLS-2$ |
| HelpPlugin.logError(msg, t); |
| } |
| } |
| return linkedContributionIds; |
| } |
| |
| /* |
| * Checks whether the toc contribution with the given id contains the |
| * given anchor. |
| */ |
| private boolean hasAnchor(String tocContributionId, String anchorId) { |
| TocContribution contrib = getContribution(tocContributionId); |
| if (contrib != null) { |
| process(contrib); |
| if (anchorsByContributionId != null) { |
| Set<String> anchors = anchorsByContributionId.get(tocContributionId); |
| if (anchors != null) { |
| return anchors.contains(anchorId); |
| } |
| } |
| } |
| // invalid contribution, or no anchors |
| return false; |
| } |
| |
| /* |
| * Checks whether the given contribution has a link_to defined, and it |
| * is valid (contribution and anchor exist). |
| */ |
| private boolean hasValidLinkTo(TocContribution contrib) { |
| String linkTo = contrib.getLinkTo(); |
| if (linkTo != null) { |
| String normalized = HrefUtil.normalizeHref(contrib.getContributorId(), linkTo); |
| int index = normalized.indexOf('#'); |
| if (index != -1) { |
| String id = normalized.substring(0, index); |
| String anchorId = normalized.substring(index + 1); |
| return hasAnchor(id, anchorId); |
| } |
| } |
| return false; |
| } |
| |
| /* |
| * Processes the given contribution, if it hasn't been processed yet. This |
| * performs the following operations: |
| * |
| * 1. Topic hrefs are normalized, e.g. "path/doc.html" -> |
| * "/my.plugin/path/doc.html" |
| * 2. Links are resolved, link is replaced with target content, extra docs |
| * are merged. |
| * 3. Anchor contributions are resolved, tocs with link_to's are inserted |
| * at anchors and extra docs merged. |
| */ |
| private void process(ITocContribution contribution) { |
| if (processedContributions == null) { |
| processedContributions = new HashSet<>(); |
| } |
| // don't process the same one twice |
| if (!processedContributions.contains(contribution)) { |
| if (processor == null) { |
| processor = new DocumentProcessor(); |
| } |
| if (handlers == null) { |
| DocumentReader reader = new DocumentReader(); |
| handlers = new ProcessorHandler[] { |
| new NormalizeHandler(), |
| new LinkHandler(), |
| new AnchorHandler(), |
| new IncludeHandler(reader, contribution.getLocale()), |
| new ExtensionHandler(reader, contribution.getLocale()), |
| }; |
| } |
| processor.setHandlers(handlers); |
| processor.process((Toc)contribution.getToc(), contribution.getId()); |
| processedContributions.add(contribution); |
| } |
| } |
| |
| /* |
| * Returns the contribution with the given id. |
| */ |
| private TocContribution getContribution(String id) { |
| if (contributionsById == null) { |
| contributionsById = new HashMap<>(); |
| Iterator<TocContribution> iter = contributions.iterator(); |
| while (iter.hasNext()) { |
| TocContribution contribution = iter.next(); |
| contributionsById.put(contribution.getId(), contribution); |
| } |
| } |
| return contributionsById.get(id); |
| } |
| |
| /* |
| * Returns all contributions that define a link_to attribute pointing to |
| * the given anchor path. The path has the form "<contributionId>#<anchorId>", |
| * e.g. "/my.plugin/toc.xml#myAnchor". |
| */ |
| private ITocContribution[] getAnchorContributions(String anchorPath) { |
| if (contributionsByLinkTo == null) { |
| contributionsByLinkTo = new HashMap<>(); |
| Iterator<TocContribution> iter = contributions.iterator(); |
| while (iter.hasNext()) { |
| TocContribution srcContribution = iter.next(); |
| String linkTo = srcContribution.getLinkTo(); |
| if (linkTo != null) { |
| String destAnchorPath = HrefUtil.normalizeHref(srcContribution.getContributorId(), linkTo); |
| ITocContribution[] array = contributionsByLinkTo.get(destAnchorPath); |
| if (array == null) { |
| array = new TocContribution[] { srcContribution }; |
| } |
| else { |
| // If a contribution of this id is already included don't include a second time |
| boolean isAlreadyIncluded = false; |
| for (int i = 0; i < array.length; i++) { |
| if (srcContribution.getId().equals(array[i].getId())) { |
| isAlreadyIncluded = true; |
| } |
| } |
| if (!isAlreadyIncluded) { |
| TocContribution[] temp = new TocContribution[array.length + 1]; |
| System.arraycopy(array, 0, temp, 0, array.length); |
| temp[array.length] = srcContribution; |
| array = temp; |
| } |
| } |
| contributionsByLinkTo.put(destAnchorPath, array); |
| } |
| } |
| } |
| ITocContribution[] contributions = contributionsByLinkTo.get(anchorPath); |
| if (contributions == null) { |
| contributions = new TocContribution[0]; |
| } |
| return contributions; |
| } |
| |
| private Map<String, String[]> getRequiredAttributes() { |
| if (requiredAttributes == null) { |
| requiredAttributes = new HashMap<>(); |
| requiredAttributes.put(Toc.NAME, new String[] { Toc.ATTRIBUTE_LABEL }); |
| requiredAttributes.put(Topic.NAME, new String[] { Topic.ATTRIBUTE_LABEL }); |
| requiredAttributes.put("anchor", new String[] { "id" }); //$NON-NLS-1$ //$NON-NLS-2$ |
| requiredAttributes.put("include", new String[] { "path" }); //$NON-NLS-1$ //$NON-NLS-2$ |
| requiredAttributes.put("link", new String[] { "toc" }); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| return requiredAttributes; |
| } |
| |
| /* |
| * Adds the given extra documents to the contribution. |
| */ |
| private void addExtraDocuments(TocContribution contribution, String[] extraDocuments) { |
| if (extraDocuments.length > 0) { |
| String[] destExtraDocuments = contribution.getExtraDocuments(); |
| String[] combinedExtraDocuments; |
| if (destExtraDocuments.length == 0) { |
| combinedExtraDocuments = extraDocuments; |
| } |
| else { |
| Set<String> set = new HashSet<>(); |
| set.addAll(Arrays.asList(destExtraDocuments)); |
| set.addAll(Arrays.asList(extraDocuments)); |
| combinedExtraDocuments = set.toArray(new String[set.size()]); |
| } |
| contribution.setExtraDocuments(combinedExtraDocuments); |
| } |
| } |
| |
| /* |
| * Handler that resolves link elements (replaces the link element with |
| * the linked-to toc's children. |
| */ |
| private class LinkHandler extends ProcessorHandler { |
| @Override |
| public short handle(UAElement element, String id) { |
| if (element instanceof Link) { |
| Link link = (Link)element; |
| UAElement parent = link.getParentElement(); |
| if (parent != null) { |
| String toc = link.getToc(); |
| if (toc != null) { |
| TocContribution destContribution = getContribution(id); |
| TocContribution srcContribution = getContribution(HrefUtil.normalizeHref(destContribution.getContributorId(), toc)); |
| if (srcContribution != null) { |
| process(srcContribution); |
| IUAElement[] children = srcContribution.getToc().getChildren(); |
| for (int i=0;i<children.length;++i) { |
| parent.insertBefore((UAElement)children[i], link); |
| } |
| addExtraDocuments(destContribution, srcContribution.getExtraDocuments()); |
| } |
| parent.removeChild(link); |
| } |
| } |
| return HANDLED_SKIP; |
| } |
| return UNHANDLED; |
| } |
| } |
| |
| /* |
| * Handles anchor contributions. If any contribution's toc wants to link |
| * into this one at the current anchor, link it in. |
| */ |
| private class AnchorHandler extends ProcessorHandler { |
| @Override |
| public short handle(UAElement element, String id) { |
| if (element instanceof Anchor) { |
| if (tocsToFilter.contains(id)) { |
| return UNHANDLED; |
| } |
| Anchor anchor = (Anchor)element; |
| UAElement parent = anchor.getParentElement(); |
| if (parent != null) { |
| String anchorId = anchor.getId(); |
| if (anchorId != null) { |
| // add to set of known anchors |
| if (anchorsByContributionId == null) { |
| anchorsByContributionId = new HashMap<>(); |
| } |
| Set<String> set = anchorsByContributionId.get(id); |
| if (set == null) { |
| set = new HashSet<>(); |
| anchorsByContributionId.put(id, set); |
| } |
| set.add(anchorId); |
| |
| // process contributions |
| TocContribution destContribution = getContribution(id); |
| if (destContribution != null) { |
| ITocContribution[] srcContributions = getAnchorContributions( |
| destContribution.getId() + '#' + anchorId); |
| for (int i=0;i<srcContributions.length;++i) { |
| process(srcContributions[i]); |
| IUAElement[] children = srcContributions[i].getToc().getChildren(); |
| for (int j=0;j<children.length;++j) { |
| parent.insertBefore((UAElement)children[j], anchor); |
| } |
| addExtraDocuments(destContribution, srcContributions[i].getExtraDocuments()); |
| } |
| } |
| } |
| } |
| } |
| // allow the extension handler to act on anchors afterwards |
| return UNHANDLED; |
| } |
| } |
| |
| /* |
| * Normalizes topic hrefs, by prepending the plug-in id to form an href. |
| * e.g. "path/myfile.html" -> "/my.plugin/path/myfile.html" |
| */ |
| private class NormalizeHandler extends ProcessorHandler { |
| @Override |
| public short handle(UAElement element, String id) { |
| if (element instanceof Topic) { |
| Topic topic = (Topic)element; |
| String href = topic.getHref(); |
| if (href != null) { |
| topic.setHref(normalize(href, id)); |
| } |
| |
| processCriteria(element, id); |
| |
| return HANDLED_CONTINUE; |
| } |
| else if (element instanceof Toc) { |
| Toc toc = (Toc)element; |
| toc.setHref(id); |
| String topic = toc.getTopic(); |
| if (topic != null) { |
| toc.setTopic(normalize(topic, id)); |
| } |
| |
| processCriteria(element, id); |
| |
| return HANDLED_CONTINUE; |
| } |
| return UNHANDLED; |
| } |
| |
| private String normalize(String href, String id) { |
| ITocContribution contribution = getContribution(id); |
| if (contribution != null) { |
| String pluginId = contribution.getContributorId(); |
| return HrefUtil.normalizeHref(pluginId, href); |
| } |
| else { |
| int index = id.indexOf('/', 1); |
| if (index != -1) { |
| String pluginId = id.substring(1, index); |
| return HrefUtil.normalizeHref(pluginId, href); |
| } |
| } |
| return href; |
| } |
| |
| private void processCriteria(UAElement element, String id) { |
| if(HelpPlugin.getCriteriaManager().isCriteriaEnabled()){ |
| ITocContribution contribution = getContribution(id); |
| String locale = contribution.getLocale(); |
| ICriteria[] criteria = new ICriteria[0]; |
| if (element instanceof Topic) { |
| Topic topic = (Topic) element; |
| criteria = CriteriaProviderRegistry.getInstance().getAllCriteria(topic); |
| } |
| else if (element instanceof Toc) { |
| Toc toc = (Toc) element; |
| criteria = CriteriaProviderRegistry.getInstance().getAllCriteria(toc); |
| } |
| |
| HelpPlugin.getCriteriaManager().addCriteriaValues(criteria, locale); |
| } |
| } |
| } |
| } |