All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dita.dost.reader.KeyrefReader Maven / Gradle / Ivy

The newest version!
/*
 * This file is part of the DITA Open Toolkit project.
 *
 * Copyright 2010 IBM Corporation
 *
 * See the accompanying LICENSE file for applicable license.

 */
package org.dita.dost.reader;

import static net.sf.saxon.s9api.streams.Predicates.isElement;
import static net.sf.saxon.s9api.streams.Steps.child;
import static net.sf.saxon.s9api.streams.Steps.precedingSibling;
import static net.sf.saxon.type.BuiltInAtomicType.STRING;
import static org.dita.dost.module.filter.MapBranchFilterModule.BRANCH_COPY_TO;
import static org.dita.dost.util.Constants.*;
import static org.dita.dost.util.KeyScope.ROOT_ID;
import static org.dita.dost.util.URLUtils.toURI;
import static org.dita.dost.util.XMLUtils.rootElement;

import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilder;
import net.sf.saxon.event.PipelineConfiguration;
import net.sf.saxon.event.Receiver;
import net.sf.saxon.expr.parser.Loc;
import net.sf.saxon.om.*;
import net.sf.saxon.s9api.XdmDestination;
import net.sf.saxon.s9api.XdmNode;
import net.sf.saxon.s9api.XdmNodeKind;
import net.sf.saxon.serialize.SerializationProperties;
import net.sf.saxon.trans.UncheckedXPathException;
import net.sf.saxon.trans.XPathException;
import net.sf.saxon.type.Untyped;
import org.dita.dost.log.DITAOTLogger;
import org.dita.dost.log.MessageBean;
import org.dita.dost.log.MessageUtils;
import org.dita.dost.util.Job;
import org.dita.dost.util.KeyDef;
import org.dita.dost.util.KeyScope;
import org.dita.dost.util.XMLUtils;

/**
 * KeyrefReader class which reads DITA map file to collect key definitions. Instances are reusable but not thread-safe.
 */
public final class KeyrefReader implements AbstractReader {

  private static final List ATTS = List.of(
    ATTRIBUTE_NAME_HREF,
    ATTRIBUTE_NAME_AUDIENCE,
    ATTRIBUTE_NAME_PLATFORM,
    ATTRIBUTE_NAME_PRODUCT,
    ATTRIBUTE_NAME_OTHERPROPS,
    ATTRIBUTE_NAME_REV,
    ATTRIBUTE_NAME_PROPS,
    "linking",
    ATTRIBUTE_NAME_TOC,
    ATTRIBUTE_NAME_PRINT,
    "search",
    ATTRIBUTE_NAME_FORMAT,
    ATTRIBUTE_NAME_SCOPE,
    ATTRIBUTE_NAME_TYPE,
    ATTRIBUTE_NAME_XML_LANG,
    "dir",
    "translate",
    ATTRIBUTE_NAME_PROCESSING_ROLE,
    ATTRIBUTE_NAME_CASCADE
  );

  private DITAOTLogger logger;
  private Job job;
  private final DocumentBuilder builder;
  private KeyScope rootScope;
  private URI currentFile;
  private XMLUtils xmlUtils;

  /**
   * Constructor.
   */
  public KeyrefReader() {
    builder = XMLUtils.getDocumentBuilder();
  }

  @Override
  public void read(final File filename) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void setLogger(final DITAOTLogger logger) {
    this.logger = logger;
  }

  @Override
  public void setJob(final Job job) {
    this.job = job;
  }

  public void setXmlUtils(XMLUtils xmlUtils) {
    this.xmlUtils = xmlUtils;
  }

  /**
   * Get key definitions for root scope. Each key definition Element has a distinct Document.
   *
   * @return root key scope
   */
  public KeyScope getKeyDefinition() {
    return rootScope;
  }

  /**
   * Read key definitions
   *
   * @param filename absolute URI to DITA map with key definitions
   * @param doc      key definition DITA map
   */
  public void read(final URI filename, final XdmNode doc) {
    currentFile = filename;
    rootScope = null;
    // TODO: use KeyScope implementation that retains order
    final KeyScope keyScope = readScopes(doc);
    final KeyScope keyScopeWithChildren = cascadeChildKeys(keyScope);
    // TODO: determine effective key definitions here
    final KeyScope keyScopeWithParents = inheritParentKeys(keyScopeWithChildren);
    rootScope = resolveIntermediate(keyScopeWithParents);
  }

  /**
   * Read keys scopes in map.
   */
  private KeyScope readScopes(final XdmNode doc) {
    assert doc.getNodeKind() == XdmNodeKind.DOCUMENT;
    final XdmNode root = doc.select(rootElement()).asNode();
    final List scopes = readScopesRoot(root);
    if (scopes.size() == 1 && scopes.get(0).name() == null) {
      return scopes.get(0);
    } else {
      return new KeyScope(ROOT_ID, null, Collections.emptyMap(), scopes);
    }
  }

  private List readScopesRoot(final XdmNode root) {
    final List childScopes = new ArrayList<>();
    final Map keyDefs = new HashMap<>();
    readScope(root, keyDefs);
    readChildScopes(root, childScopes);
    final String keyscope = root.attribute(ATTRIBUTE_NAME_KEYSCOPE);
    if (keyscope == null || keyscope.trim().isEmpty()) {
      return Collections.singletonList(new KeyScope(ROOT_ID, null, keyDefs, childScopes));
    } else {
      final List res = new ArrayList<>();
      for (final String scope : keyscope.split("\\s+")) {
        res.add(new KeyScope(generateId(root, scope), scope, keyDefs, childScopes));
      }
      return res;
    }
  }

  private String generateId(final XdmNode root, final String scope) {
    final StringBuilder res = new StringBuilder();
    XdmNode elem = root;
    while (elem != null) {
      res.append(elem.getNodeName()).append('[');
      int position = 0;
      for (XdmNode n = elem; n != null; position++) {
        n = n.select(precedingSibling().first()).findFirst().orElse(null);
      }
      res.append(Integer.toString(position)).append(']');
      final XdmNode p = elem.getParent();
      if (p != null && p.getNodeKind() == XdmNodeKind.ELEMENT) {
        elem = p;
      } else {
        elem = null;
      }
    }
    res.append('.').append(scope);
    return res.toString();
  }

  private void readChildScopes(final XdmNode elem, final List childScopes) {
    elem
      .select(child(isElement()))
      .forEach(child -> {
        if (child.attribute(ATTRIBUTE_NAME_KEYSCOPE) != null) {
          final List childScope = readScopesRoot(child);
          childScopes.addAll(childScope);
        } else {
          readChildScopes(child, childScopes);
        }
      });
  }

  /**
   * Read key definitions from a key scope.
   */
  private void readScope(final XdmNode scope, final Map keyDefs) {
    final List maps = new ArrayList<>();
    maps.add(scope);
    for (final XdmNode child : scope.children(isElement())) {
      collectMaps(child, maps);
    }
    for (final XdmNode map : maps) {
      readMap(map, keyDefs);
    }
  }

  private void collectMaps(final XdmNode elem, final List maps) {
    if (elem.attribute(ATTRIBUTE_NAME_KEYSCOPE) != null) {
      return;
    }
    final String classValue = elem.attribute(ATTRIBUTE_NAME_CLASS);
    if (MAP_MAP.matches(classValue) || SUBMAP.matches(classValue)) {
      maps.add(elem);
    }
    for (final XdmNode child : elem.children(isElement())) {
      collectMaps(child, maps);
    }
  }

  /**
   * Recursively read key definitions from a single map fragment.
   */
  private void readMap(final XdmNode map, final Map keyDefs) {
    readKeyDefinition(map, keyDefs);
    for (final XdmNode elem : map.children(isElement())) {
      if (!(SUBMAP.matches(elem) || elem.attribute(ATTRIBUTE_NAME_KEYSCOPE) != null)) {
        readMap(elem, keyDefs);
      }
    }
  }

  private void readKeyDefinition(final XdmNode elem, final Map keyDefs) {
    final String keyName = elem.attribute(ATTRIBUTE_NAME_KEYS);
    if (keyName != null) {
      for (final String key : keyName.trim().split("\\s+")) {
        if (!keyDefs.containsKey(key)) {
          final XdmNode copy = elem;
          final URI href = toURI(
            copy.attribute(BRANCH_COPY_TO) != null
              ? copy.attribute(BRANCH_COPY_TO)
              : copy.attribute(ATTRIBUTE_NAME_COPY_TO) != null
                ? copy.attribute(ATTRIBUTE_NAME_COPY_TO)
                : copy.attribute(ATTRIBUTE_NAME_HREF)
          );
          final String scope = copy.attribute(ATTRIBUTE_NAME_SCOPE);
          final String format = copy.attribute(ATTRIBUTE_NAME_FORMAT);
          final KeyDef keyDef = new KeyDef(key, href, scope, format, currentFile, copy);
          keyDefs.put(key, keyDef);
        }
      }
    }
  }

  /** Cascade child keys with prefixes to parent key scopes. */
  @VisibleForTesting
  KeyScope cascadeChildKeys(final KeyScope rootScope) {
    final Map res = new HashMap<>(rootScope.keyDefinition());
    cascadeChildKeys(rootScope, res, "");
    return new KeyScope(
      rootScope.id(),
      rootScope.name(),
      res,
      rootScope.childScopes().stream().map(this::cascadeChildKeys).collect(Collectors.toList())
    );
  }

  private void cascadeChildKeys(final KeyScope scope, final Map keys, final String prefix) {
    for (final Map.Entry e : scope.keyDefinition().entrySet()) {
      final KeyDef oldKeyDef = e.getValue();
      final KeyDef newKeyDef = new KeyDef(
        prefix + oldKeyDef.keys,
        oldKeyDef.href,
        oldKeyDef.scope,
        oldKeyDef.format,
        oldKeyDef.source,
        oldKeyDef.element
      );
      if (!keys.containsKey(newKeyDef.keys)) {
        keys.put(newKeyDef.keys, newKeyDef);
      }
    }
    for (final KeyScope child : scope.childScopes()) {
      cascadeChildKeys(child, keys, prefix + child.name() + ".");
    }
  }

  /**
   * Inherit parent keys to child key scopes.
   */
  private KeyScope inheritParentKeys(final KeyScope rootScope) {
    return inheritParentKeys(rootScope, Collections.emptyMap());
  }

  private KeyScope inheritParentKeys(final KeyScope current, final Map parent) {
    if (parent.keySet().isEmpty() && current.childScopes().isEmpty()) {
      return current;
    } else {
      final Map resKeys = new HashMap<>();
      resKeys.putAll(current.keyDefinition());
      resKeys.putAll(parent);
      final List resChildren = new ArrayList<>();
      for (final KeyScope child : current.childScopes()) {
        final KeyScope resChild = inheritParentKeys(child, resKeys);
        resChildren.add(resChild);
      }
      return new KeyScope(current.id(), current.name(), resKeys, resChildren);
    }
  }

  /**
   * Resolve intermediate key references.
   */
  private KeyScope resolveIntermediate(final KeyScope scope) {
    final Map keys = new HashMap<>(scope.keyDefinition());
    for (final Map.Entry e : scope.keyDefinition().entrySet()) {
      final KeyDef res = resolveIntermediate(scope, e.getValue(), Collections.singletonList(e.getValue()));
      keys.put(e.getKey(), res);
    }
    final List children = new ArrayList<>();
    for (final KeyScope child : scope.childScopes()) {
      final KeyScope resolvedChild = resolveIntermediate(child);
      children.add(resolvedChild);
    }
    return new KeyScope(scope.id(), scope.name(), keys, children);
  }

  private KeyDef resolveIntermediate(final KeyScope scope, final KeyDef keyDef, final List circularityTracker) {
    final XdmNode elem = keyDef.element;
    final String keyref = elem.attribute(ATTRIBUTE_NAME_KEYREF);
    if (keyref != null && !keyref.trim().isEmpty() && scope.keyDefinition().containsKey(keyref)) {
      KeyDef keyRefDef = scope.keyDefinition().get(keyref);
      if (circularityTracker.contains(keyRefDef)) {
        handleCircularDefinitionException(circularityTracker);
        return keyDef;
      }
      XdmNode defElem = keyRefDef.element;
      final String defElemKeyref = defElem.attribute(ATTRIBUTE_NAME_KEYREF);
      if (defElemKeyref != null && !defElemKeyref.isEmpty()) {
        // TODO use immutable List
        final List ct = new ArrayList<>(circularityTracker.size() + 1);
        ct.addAll(circularityTracker);
        ct.add(keyRefDef);
        keyRefDef = resolveIntermediate(scope, keyRefDef, ct);
      }
      final XdmNode res = mergeMetadata(keyRefDef.element, elem);
      return new KeyDef(keyDef.keys, keyRefDef.href, keyRefDef.scope, keyRefDef.format, keyRefDef.source, res);
    } else {
      return keyDef;
    }
  }

  private void handleCircularDefinitionException(final List circularityTracker) {
    final StringBuilder sb = new StringBuilder();
    Collections.reverse(circularityTracker);
    for (final KeyDef keyDef : circularityTracker) {
      sb.append(keyDef.keys).append(" -> ");
    }
    sb.append(circularityTracker.get(0).keys);
    final MessageBean ex = MessageUtils
      .getMessage("DOTJ069E", sb.toString())
      .setLocation(circularityTracker.get(0).element);
    logger.error(ex.toString(), ex.toException());
  }

  private XdmNode mergeMetadata(final XdmNode defElem, final XdmNode refElem) {
    try {
      final XdmDestination dst = new XdmDestination();
      dst.setBaseURI(refElem.getBaseURI());
      dst.setDestinationBaseURI(refElem.getBaseURI());
      final PipelineConfiguration pipe = refElem.getUnderlyingNode().getConfiguration().makePipelineConfiguration();
      final Receiver receiver = dst.getReceiver(pipe, new SerializationProperties());
      receiver.open();
      receiver.startDocument(0);

      final NodeInfo rni = refElem.getUnderlyingNode();

      final AttributeMap atts = new SmallAttributeMap(
        defElem
          .getUnderlyingNode()
          .attributes()
          .asList()
          .stream()
          // only attributes from ATTS
          .filter(attr -> ATTS.contains(attr.getNodeName().getLocalPart()))
          // only if refElem doesn't have it.
          .filter(attr -> refElem.attribute(attr.getNodeName().getLocalPart()) == null)
          .collect(Collectors.toList())
      );
      receiver.startElement(
        NameOfNode.makeName(rni),
        rni.getSchemaType(),
        atts,
        rni.getAllNamespaces(),
        rni.saveLocation(),
        0
      );

      final XdmNode defMeta = getTopicmeta(defElem);
      if (defMeta != null) {
        final XdmNode resMeta = getTopicmeta(refElem);
        if (resMeta == null) {
          final SingletonAttributeMap attrs = SingletonAttributeMap.of(
            new AttributeInfo(new NoNamespaceName(ATTRIBUTE_NAME_CLASS), STRING, MAP_TOPICMETA.toString(), Loc.NONE, 0)
          );
          receiver.startElement(
            new NoNamespaceName(MAP_TOPICMETA.localName),
            Untyped.getInstance(),
            attrs,
            rni.getAllNamespaces(),
            Loc.NONE,
            0
          );
        } else {
          final NodeInfo ni = resMeta.getUnderlyingNode();

          receiver.startElement(
            NameOfNode.makeName(ni),
            ni.getSchemaType(),
            resMeta.getUnderlyingNode().attributes().remove(new NoNamespaceName(ATTRIBUTE_NAME_KEYREF)),
            resMeta.getUnderlyingNode().getAllNamespaces(),
            ni.saveLocation(),
            0
          );
        }
        defMeta
          .select(child())
          .forEach(child -> {
            try {
              receiver.append(child.getUnderlyingNode());
            } catch (XPathException e) {
              throw new UncheckedXPathException(e);
            }
          });
        receiver.endElement();
      }

      receiver.endElement();

      receiver.endDocument();
      receiver.close();
      return dst.getXdmNode().select(rootElement()).asNode();
    } catch (XPathException | UncheckedXPathException e) {
      logger.error("Failed to merge topicmeta: " + e.getMessage(), e);
      return refElem;
    }
  }

  private XdmNode getTopicmeta(final XdmNode topicref) {
    return topicref.select(child(MAP_TOPICMETA::matches).first()).findAny().orElse(null);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy