blob: 0cfcff221372fbb4a26618b8c2400d36ffcf98f6 [file] [log] [blame]
/*******************************************************************************
* 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);
}
}
}
}