private Element addLinkpool(Element topic, boolean prepend, String linkpoolType, Document doc) {
    Element relatedLinks = DITAUtil.findChildByClass(topic, "topic/related-links");
    if (relatedLinks == null) {
      relatedLinks = doc.createElementNS(null, "related-links");
      relatedLinks.setAttributeNS(null, "class", "- topic/related-links ");

      topic.insertBefore(relatedLinks, DITAUtil.findChildByClass(topic, "topic/topic"));
    }

    Element linkpool = doc.createElementNS(null, "linkpool");
    linkpool.setAttributeNS(null, "class", "- topic/linkpool ");

    if (mapHref != null && linkpoolType != null) {
      // mapkeyref is a standard DITA 1.2 attribute meant for this use.
      linkpool.setAttributeNS(null, "mapkeyref", mapHref + " type=" + linkpoolType);
    }

    Node before = null;
    if (prepend) {
      before = relatedLinks.getFirstChild();
    }
    relatedLinks.insertBefore(linkpool, before);

    return linkpool;
  }
  private void processColumn(
      Element reltable,
      int column,
      LoadedDocuments loadedDocs,
      List<Entry> entryList,
      List<Entry[]> cellList) {
    Node child = reltable.getFirstChild();
    while (child != null) {
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        Element childElement = (Element) child;

        if (DITAUtil.hasClass(childElement, "map/relrow")) {
          Element relcell = DOMUtil.getNthChildElement(childElement, column);
          if (relcell != null && DITAUtil.hasClass(relcell, "map/relcell")) {
            entryList.clear();
            processCell(relcell, loadedDocs, entryList);

            Entry[] entries = new Entry[entryList.size()];
            entryList.toArray(entries);
            cellList.add(entries);
          }
        }
      }

      child = child.getNextSibling();
    }
  }
  private void processHierarchy(
      Element topicrefOrMap, LoadedDocuments loadedDocs, List<Entry> childList) {
    CollectionType collectionType = null;
    String type = DITAUtil.getNonEmptyAttribute(topicrefOrMap, null, "collection-type");
    if (type != null) {
      collectionType = CollectionType.fromString(type);
    }
    if (collectionType == null) {
      collectionType = CollectionType.UNORDERED;
    }

    processCollection(topicrefOrMap, collectionType, loadedDocs, childList);

    // Recurse ---

    // LIMITATION: @collection-type is ignored inside reltables.

    Node child = topicrefOrMap.getFirstChild();
    while (child != null) {
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        Element childElement = (Element) child;

        if (DITAUtil.hasClass(childElement, "map/topicref")
            &&
            // Does not make sense inside frontmatter/backmatter.
            // For example, inside a glossary.
            !DITAUtil.hasClass(childElement, "bookmap/frontmatter", "bookmap/backmatter")) {
          processHierarchy(childElement, loadedDocs, childList);
        }
      }

      child = child.getNextSibling();
    }
  }
  private void processCell(Element relcell, LoadedDocuments loadedDocs, List<Entry> entryList) {
    Node child = relcell.getFirstChild();
    while (child != null) {
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        Element childElement = (Element) child;

        String href;
        if (DITAUtil.hasClass(childElement, "map/topicref")
            && (href = DITAUtil.getNonEmptyAttribute(childElement, null, "href")) != null) {
          Entry entry = createEntry(childElement, href, loadedDocs);
          if (entry != null) {
            entryList.add(entry);
          }
        }

        processCell(childElement, loadedDocs, entryList);
      }

      child = child.getNextSibling();
    }
  }
  private void processColumns(
      Element reltable, LoadedDocuments loadedDocs, Map<Element, Entry[]> collected) {
    Element relheader = DITAUtil.findChildByClass(reltable, "map/relheader");
    if (relheader == null) {
      return;
    }

    int column = 0;
    ArrayList<Entry[]> cellList = new ArrayList<Entry[]>();
    ArrayList<Entry> entryList = new ArrayList<Entry>();

    Node child = relheader.getFirstChild();
    while (child != null) {
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        Element childElement = (Element) child;

        if (DITAUtil.hasClass(childElement, "map/relcolspec")) {
          cellList.clear();

          entryList.clear();
          processCell(childElement, loadedDocs, entryList);

          if (entryList.size() > 0) {
            Entry[] entries = new Entry[entryList.size()];
            entryList.toArray(entries);
            cellList.add(entries);

            processColumn(reltable, column, loadedDocs, entryList, cellList);

            addColumn(cellList, collected);
          }
          // Otherwise the relcolspec does not contain topicrefs.

          ++column;
        }
      }

      child = child.getNextSibling();
    }
  }
  private void processRows(
      Element reltable, LoadedDocuments loadedDocs, Map<Element, Entry[]> collected) {
    ArrayList<Entry[]> cellList = new ArrayList<Entry[]>();

    Node child = reltable.getFirstChild();
    while (child != null) {
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        Element childElement = (Element) child;

        if (DITAUtil.hasClass(childElement, "map/relrow")) {
          cellList.clear();
          processRow(childElement, loadedDocs, cellList);

          addRow(cellList, collected);
        }
      }

      child = child.getNextSibling();
    }
  }
  private void processRow(Element relrow, LoadedDocuments loadedDocs, List<Entry[]> cellList) {
    ArrayList<Entry> entryList = new ArrayList<Entry>();

    Node child = relrow.getFirstChild();
    while (child != null) {
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        Element childElement = (Element) child;

        if (DITAUtil.hasClass(childElement, "map/relcell")) {
          entryList.clear();
          processCell(childElement, loadedDocs, entryList);

          Entry[] entries = new Entry[entryList.size()];
          entryList.toArray(entries);
          cellList.add(entries);
        }
      }

      child = child.getNextSibling();
    }
  }
  public void processMap(Element map, URL mapURL, LoadedDocuments loadedDocs) {
    // Process @collection-type (but not inside reltables) ---

    mapHref = mapURL.toExternalForm();
    ArrayList<Entry> childList = new ArrayList<Entry>();

    processHierarchy(map, loadedDocs, childList);

    // Process reltables ---

    IdentityHashMap<Element, Entry[]> collected = new IdentityHashMap<Element, Entry[]>();

    Node child = map.getFirstChild();
    while (child != null) {
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        Element childElement = (Element) child;

        if (DITAUtil.hasClass(childElement, "map/reltable")) {
          processColumns(childElement, loadedDocs, collected);

          processRows(childElement, loadedDocs, collected);
        }
      }

      child = child.getNextSibling();
    }

    Iterator<Map.Entry<Element, Entry[]>> iter = collected.entrySet().iterator();
    while (iter.hasNext()) {
      Map.Entry<Element, Entry[]> e = iter.next();

      Element topic = e.getKey();
      Entry[] entries = e.getValue();

      addRelatedLinks(topic, entries);
    }
  }
  private Entry createEntry(Element topicref, String href, LoadedDocuments loadedDocs) {
    // scope ---

    String scope = DITAUtil.getNonEmptyAttribute(topicref, null, "scope");

    // format ---

    String format = DITAUtil.getFormat(topicref, href);
    if (format == null) {
      console.warning(topicref, Msg.msg("missingAttribute", "format"));
      return null;
    }

    // linking ---

    Linking linking = Linking.NORMAL;

    String value = DITAUtil.getNonEmptyAttribute(topicref, null, "linking");
    if (value != null) {
      if ("none".equals(value)) {
        linking = Linking.NONE;
      } else if ("sourceonly".equals(value)) {
        linking = Linking.SOURCE_ONLY;
      } else if ("targetonly".equals(value)) {
        linking = Linking.TARGET_ONLY;
      } else if ("normal".equals(value)) {
        linking = Linking.NORMAL;
      } else {
        console.warning(topicref, Msg.msg("invalidAttribute", value, "linking"));
        linking = Linking.NONE;
      }
    }

    // linktext, shortdesc ---

    Element linktext = null;
    Element shortdesc = null;

    Element topicmeta = DITAUtil.findChildByClass(topicref, "map/topicmeta");
    if (topicmeta != null) {
      // Notice "map/linktext" and not "topic/linktext".
      linktext = DITAUtil.findChildByClass(topicmeta, "map/linktext");

      // Notice "map/shortdesc" and not "topic/shortdesc".
      shortdesc = DITAUtil.findChildByClass(topicmeta, "map/shortdesc");
    }

    // topicId, elementId, topic ---

    String topicId = null;
    String elementId = null;
    Element topic = null;

    if ((scope == null || "local".equals(scope)) && "dita".equals(format)) {
      // Points to a local topic.

      URL url;
      try {
        url = URLUtil.createURL(href);
      } catch (MalformedURLException ignored) {
        console.warning(topicref, Msg.msg("invalidAttribute", href, "href"));
        return null;
      }

      String fragment = URLUtil.getFragment(url);
      url = URLUtil.setFragment(url, null);

      // LoadedDocuments.get() is sufficient in production.
      // We use load() here just to be able to run the test drive below.
      LoadedDocument loadedDoc = null;
      try {
        loadedDoc = loadedDocs.load(url);
      } catch (Exception shouldNotHappen) {
      }

      if (loadedDoc != null) {
        if (fragment != null) {
          int pos = fragment.indexOf('/');
          if (pos > 0 && pos + 1 < fragment.length()) {
            topicId = fragment.substring(0, pos);
            elementId = fragment.substring(pos + 1);
          } else {
            topicId = fragment;
          }
        }

        LoadedTopic loadedTopic = null;
        if (topicId != null) {
          loadedTopic = loadedDoc.findTopicById(topicId);
        } else {
          loadedTopic = loadedDoc.getFirstTopic();
        }

        if (loadedTopic != null) {
          if (loadedTopic.isExcluded()) {
            return null;
          } else {
            topicId = loadedTopic.topicId;
            topic = loadedTopic.element;

            // Normalize href ---

            StringBuilder buffer = new StringBuilder(url.toExternalForm());
            buffer.append('#');
            buffer.append(URIComponent.quoteFragment(topicId));
            if (elementId != null) {
              buffer.append('/');
              buffer.append(URIComponent.quoteFragment(elementId));
            }
            href = buffer.toString();
          }
        } else {
          console.warning(topicref, Msg.msg("pointsOutsidePreprocessedTopics", href));
          return null;
        }
      } else {
        console.warning(topicref, Msg.msg("pointsOutsidePreprocessedTopics", href));
        return null;
      }
    } else {
      // Points to an external resource.

      if (linking != Linking.NONE) {
        linking = Linking.TARGET_ONLY;
      }
    }

    return new Entry(href, scope, format, linking, linktext, shortdesc, topicId, elementId, topic);
  }
  private void processCollection(
      Element topicrefOrMap,
      CollectionType collectionType,
      LoadedDocuments loadedDocs,
      List<Entry> childList) {
    Entry parent = null;

    String href = DITAUtil.getNonEmptyAttribute(topicrefOrMap, null, "href");
    if (href != null) {
      parent = createEntry(topicrefOrMap, href, loadedDocs);
      if (parent != null && parent.topic == null) {
        parent = null;
      }
    }

    childList.clear();

    Node child = topicrefOrMap.getFirstChild();
    while (child != null) {
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        Element childElement = (Element) child;

        if (DITAUtil.hasClass(childElement, "map/topicref")
            && (href = DITAUtil.getNonEmptyAttribute(childElement, null, "href")) != null) {
          Entry entry = createEntry(childElement, href, loadedDocs);
          if (entry != null && entry.topic == null) {
            entry = null;
          }
          if (entry != null) {
            childList.add(entry);
          }
        }
      }

      child = child.getNextSibling();
    }

    int childCount = childList.size();
    if (childCount > 0) {
      if (parent != null) {
        linkParentToChildren(parent, childList, childCount, collectionType);
      }

      switch (collectionType) {
        case FAMILY:
          {
            for (int i = 0; i < childCount; ++i) {
              Entry entry = childList.get(i);

              linkFamilyMembers(entry, parent, childList, childCount, i);
            }
          }
          break;
        case SEQUENCE:
          {
            Entry previous = null;
            for (int i = 0; i < childCount; ++i) {
              Entry entry = childList.get(i);

              Entry next = (i + 1 < childCount) ? childList.get(i + 1) : null;
              linkSequenceMembers(entry, parent, previous, next);
              previous = entry;
            }
          }
          break;
        default:
          {
            for (int i = 0; i < childCount; ++i) {
              Entry entry = childList.get(i);

              linkMembers(entry, parent, collectionType);
            }
          }
      }
    }
  }