WozXMLReader.java

/*
 * Copyright (C) 2021 B3Partners B.V.
 */
package nl.b3p.brmo.loader.xml;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import nl.b3p.brmo.loader.BrmoFramework;
import nl.b3p.brmo.loader.StagingProxy;
import nl.b3p.brmo.loader.entity.Bericht;
import nl.b3p.brmo.loader.entity.WozBericht;
import nl.b3p.brmo.loader.util.RsgbTransformer;
import org.apache.commons.io.input.TeeInputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class WozXMLReader extends BrmoXMLReader {
  public static final String PREFIX_PRS = "WOZ.NPS.";
  public static final String PREFIX_NNP = "WOZ.NNP.";
  public static final String PREFIX_WOZ = "WOZ.WOZ.";
  public static final String PREFIX_VES = "WOZ.VES.";
  private static final Log LOG = LogFactory.getLog(WozXMLReader.class);
  private final String pathToXsl = "/xsl/woz-brxml-preprocessor.xsl";
  private final StagingProxy staging;
  private final XPathFactory xPathfactory = XPathFactory.newInstance();
  private InputStream in;
  private Templates template;
  private NodeList objectNodes = null;
  private int index;
  private String brOrigXML = null;
  private String betrokkenWaterschap = null;
  private String gemeenteCode = null;

  public WozXMLReader(InputStream in, Date bestandsDatum, StagingProxy staging) throws Exception {
    this.in = in;
    this.staging = staging;
    setBestandsDatum(bestandsDatum);
    init();
  }

  @Override
  public void init() throws Exception {
    soort = BrmoFramework.BR_WOZ;
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    in = new TeeInputStream(in, bos, true);

    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    DocumentBuilder builder = factory.newDocumentBuilder();
    Document doc = builder.parse(in);

    brOrigXML = bos.toString(StandardCharsets.UTF_8);
    LOG.trace("Originele WOZ xml is: \n" + brOrigXML);

    TransformerFactory tf = TransformerFactory.newInstance();
    tf.setURIResolver(
        (href, base) -> {
          LOG.debug("looking for: " + href + " base: " + base);
          return new StreamSource(RsgbTransformer.class.getResourceAsStream("/xsl/" + href));
        });

    Source xsl = new StreamSource(WozXMLReader.class.getResourceAsStream(pathToXsl));
    this.template = tf.newTemplates(xsl);

    XPath xpath = xPathfactory.newXPath();

    if (this.getBestandsDatum() == null) {
      // probeer datum nog uit doc te halen..
      LOG.debug("Tijdstip bericht was niet gegeven; alsnog proberen op te zoeken in bericht.");
      XPathExpression tijdstipBericht = xpath.compile("//*[local-name()='tijdstipBericht']");
      Node datum = (Node) tijdstipBericht.evaluate(doc, XPathConstants.NODE);
      setDatumAsString(datum.getTextContent(), "yyyyMMddHHmmssSSS");
      LOG.debug("Tijdstip bericht ingesteld op " + getBestandsDatum());
    }

    XPathExpression eersteGemeente =
        xpath.compile(
            "//*[local-name()='verantwoordelijkeGemeente']/*[local-name()='gemeenteCode'][1]");
    Node gem = (Node) eersteGemeente.evaluate(doc, XPathConstants.NODE);
    gemeenteCode = null != gem ? gem.getTextContent() : null;
    if (null == gemeenteCode) {
      eersteGemeente = xpath.compile("//*[local-name()='verantwoordelijkeGemeente'][1]");
      gem = (Node) eersteGemeente.evaluate(doc, XPathConstants.NODE);
      gemeenteCode = null != gem ? gem.getTextContent() : null;
    }
    XPathExpression eersteWaterschap =
        xpath.compile(
            "//*[local-name()='ligtIn']/*[local-name()='gerelateerde']/*[local-name()='betrokkenWaterschap'][1]");
    Node ws = (Node) eersteWaterschap.evaluate(doc, XPathConstants.NODE);
    betrokkenWaterschap = null != ws ? ws.getTextContent() : null;

    // actuele woz:object nodes
    XPathExpression objectNode =
        xpath.compile("//*[local-name()='object'][not(ancestor::*[local-name()='historie'])]");
    objectNodes = (NodeList) objectNode.evaluate(doc, XPathConstants.NODESET);

    // mogelijk zijn er omhang berichten (WGEM_hangSubjectOm_Di01)
    if (objectNodes.getLength() < 1) {
      objectNode = xpath.compile("//*[local-name()='nieuweGemeenteNPS']");
      objectNodes = (NodeList) objectNode.evaluate(doc, XPathConstants.NODESET);
      if (LOG.isDebugEnabled() && objectNodes.getLength() > 0) {
        LOG.debug("nieuweGemeente NPS omhangbericht");
      }
    }
    if (objectNodes.getLength() < 1) {
      objectNode = xpath.compile("//*[local-name()='nieuweGemeenteNNP']");
      objectNodes = (NodeList) objectNode.evaluate(doc, XPathConstants.NODESET);
      if (LOG.isDebugEnabled() && objectNodes.getLength() > 0) {
        LOG.debug("nieuweGemeente NNP omhangbericht");
      }
    }
    if (objectNodes.getLength() < 1) {
      objectNode = xpath.compile("//*[local-name()='nieuweGemeenteVES']");
      objectNodes = (NodeList) objectNode.evaluate(doc, XPathConstants.NODESET);
      if (LOG.isDebugEnabled() && objectNodes.getLength() > 0) {
        LOG.debug("nieuweGemeente VES omhangbericht");
      }
    }
    index = 0;
  }

  @Override
  public boolean hasNext() throws Exception {
    return index < objectNodes.getLength();
  }

  @Override
  public WozBericht next() throws Exception {
    Node n = objectNodes.item(index);
    index++;
    String object_ref = getObjectRef(n);
    StringWriter sw = new StringWriter();

    // kijk hier of dit bericht een voorganger heeft: zo niet, dan moet niet de preprocessor
    // template gebruikt worden, maar de gewone.
    Bericht old =
        staging.getPreviousBericht(object_ref, getBestandsDatum(), -1L, new StringBuilder());
    Transformer t;
    if (old != null) {
      LOG.debug("gebruik preprocessor xsl");
      t = this.template.newTransformer();
    } else {
      LOG.debug("gebruik extractie xsl");
      t = TransformerFactory.newInstance().newTransformer();
    }

    t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
    t.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
    t.setOutputProperty(OutputKeys.INDENT, "no");
    t.setOutputProperty(OutputKeys.METHOD, "xml");
    t.transform(new DOMSource(n), new StreamResult(sw));

    Map<String, String> bsns = extractBSN(n);
    String origXML = sw.toString();
    StringBuilder brXML =
        new StringBuilder("<root>").append(origXML).append(getXML(bsns)).append("<fallback>");
    if (null != gemeenteCode) {
      brXML.append("<gemeenteCode>").append(gemeenteCode).append("</gemeenteCode>");
    }
    if (null != betrokkenWaterschap) {
      brXML
          .append("<betrokkenWaterschap>")
          .append(betrokkenWaterschap)
          .append("</betrokkenWaterschap>");
    }
    brXML.append("</fallback>").append("</root>");

    WozBericht b = new WozBericht(brXML.toString());
    b.setDatum(getBestandsDatum());
    if (index == 1) {
      // alleen op 1e brmo bericht van mogelijk meer uit originele bericht
      b.setBrOrgineelXml(brOrigXML);
    }
    // TODO volgorde nummer:
    //  bepaal aan de hand van de object_ref of volgordenummer opgehoogd moet worden. Een soap
    // bericht kan meerdere
    //  object entiteiten bevatten die een eigen type objectref krijgen. bijv. een
    // entiteittype="WOZ" en een entiteittype="NPS"
    //  bovendien kan een entiteittype="WOZ" een genests gerelateerde hebben die een apart
    // bericht moet/zou kunnen opleveren met objectref
    //  van een NPS, maar met een hoger volgordenummer...
    //  vooralsnog halen we niet de geneste entiteiten uit het bericht
    b.setVolgordeNummer(index);
    if (index > 1) {
      // om om het probleem van 2 subjecten uit 1 bericht op zelfde tijdstip dus heen te
      // werken hoger volgordenummer ook iets later maken
      b.setDatum(new Date(getBestandsDatum().getTime() + 10));
    }
    b.setObjectRef(object_ref);
    if (StringUtils.isEmpty(b.getObjectRef())) {
      // geen object_ref kunnen vaststellen; dan ook niet transformeren
      b.setStatus(Bericht.STATUS.STAGING_NOK);
      b.setOpmerking(Bericht.GEEN_OBJECT_REF_MSG);
    }

    LOG.trace("bericht: " + b);
    return b;
  }

  private String getObjectRef(Node wozObjectNode) throws XPathExpressionException {
    // WOZ:object StUF:entiteittype="WOZ"/WOZ:wozObjectNummer
    XPathExpression wozObjectNummer =
        xPathfactory.newXPath().compile("./*[local-name()='wozObjectNummer']");
    NodeList obRefs = (NodeList) wozObjectNummer.evaluate(wozObjectNode, XPathConstants.NODESET);
    if (obRefs.getLength() > 0) {
      return PREFIX_WOZ + obRefs.item(0).getTextContent();
    }

    // WOZ:object StUF:entiteittype="NPS"/WOZ:isEen/WOZ:gerelateerde/BG:inp.bsn
    XPathExpression bsn =
        xPathfactory
            .newXPath()
            .compile("./*/*[local-name()='gerelateerde']/*[local-name()='inp.bsn']");
    obRefs = (NodeList) bsn.evaluate(wozObjectNode, XPathConstants.NODESET);
    if (obRefs.getLength() > 0) {
      return PREFIX_PRS + getHash(obRefs.item(0).getTextContent());
    }

    // ./WOZ:object
    // StUF:entiteittype="WOZ"/WOZ:heeftBelanghebbende/WOZ:gerelateerde/WOZ:natuurlijkPersoon/WOZ:soFiNummer
    bsn =
        xPathfactory
            .newXPath()
            .compile(
                "//*[local-name()='heeftBelanghebbende']/*[local-name()='gerelateerde']/*[local-name()='natuurlijkPersoon']/*[local-name()='soFiNummer']");
    obRefs = (NodeList) bsn.evaluate(wozObjectNode, XPathConstants.NODESET);
    if (obRefs.getLength() > 0 && !StringUtils.isEmpty(obRefs.item(0).getTextContent())) {
      return PREFIX_PRS + obRefs.item(0).getTextContent();
    }

    // WOZ:object StUF:entiteittype="NNP"/WOZ:isEen/WOZ:gerelateerde/BG:inn.nnpId
    XPathExpression nnpIdXpath =
        xPathfactory
            .newXPath()
            .compile("./*/*[local-name()='gerelateerde']/*[local-name()='inn.nnpId']");
    obRefs = (NodeList) nnpIdXpath.evaluate(wozObjectNode, XPathConstants.NODESET);
    if (obRefs.getLength() > 0 && !StringUtils.isEmpty(obRefs.item(0).getTextContent())) {
      return PREFIX_NNP + obRefs.item(0).getTextContent();
    }
    // er komen berichten voor in test set waarin geen nnpId zit, maar wel
    // "aanvullingSoFiNummer" is gevuld...
    // WOZ:object StUF:entiteittype="NNP"/WOZ:aanvullingSoFiNummer
    nnpIdXpath =
        xPathfactory
            .newXPath()
            .compile("*[@StUF:entiteittype='NNP']/*[local-name()='aanvullingSoFiNummer']");
    obRefs = (NodeList) nnpIdXpath.evaluate(wozObjectNode, XPathConstants.NODESET);
    if (obRefs.getLength() > 0 && !StringUtils.isEmpty(obRefs.item(0).getTextContent())) {
      LOG.warn("WOZ NNP zonder `inn.nnpId`, gebruik `aanvullingSoFiNummer` voor id.");
      return PREFIX_NNP + obRefs.item(0).getTextContent();
    }

    // WOZ:object StUF:entiteittype="WRD"/WOZ:isVoor/WOZ:gerelateerde/WOZ:wozObjectNummer
    XPathExpression wrd =
        xPathfactory
            .newXPath()
            .compile("./*/*[local-name()='gerelateerde']/*[local-name()='wozObjectNummer']");
    obRefs = (NodeList) wrd.evaluate(wozObjectNode, XPathConstants.NODESET);
    if (obRefs.getLength() > 0 && !StringUtils.isEmpty(obRefs.item(0).getTextContent())) {
      return PREFIX_WOZ + obRefs.item(0).getTextContent();
    }

    // WOZ:object StUF:entiteittype="VES"/WOZ:isEen/WOZ:gerelateerde/BG:vestigingsNummer
    XPathExpression ves =
        xPathfactory
            .newXPath()
            .compile("./*/*[local-name()='gerelateerde']/*[local-name()='vestigingsNummer']");
    obRefs = (NodeList) ves.evaluate(wozObjectNode, XPathConstants.NODESET);
    if (obRefs.getLength() > 0 && !StringUtils.isEmpty(obRefs.item(0).getTextContent())) {
      return PREFIX_VES + obRefs.item(0).getTextContent();
    }

    return null;
  }

  /**
   * maakt een map met bsn,bsnhash.
   *
   * @param n document node met bsn-nummer
   * @return hashmap met bsn,bsnhash
   * @throws XPathExpressionException if any
   */
  public Map<String, String> extractBSN(Node n) throws XPathExpressionException {
    Map<String, String> hashes = new HashMap<>();
    XPath xpath = xPathfactory.newXPath();
    XPathExpression expr = xpath.compile("//*[local-name() = 'inp.bsn']");
    NodeList nodelist = (NodeList) expr.evaluate(n, XPathConstants.NODESET);
    for (int i = 0; i < nodelist.getLength(); i++) {
      Node bsn = nodelist.item(i);
      String bsnString = bsn.getTextContent();
      String hash = getHash(bsnString);
      hashes.put(bsnString, hash);
    }
    return hashes;
  }

  public String getXML(Map<String, String> map) throws ParserConfigurationException {
    if (map.isEmpty()) {
      // als in bericht geen personen zitten
      return "";
    }
    String root = "<bsnhashes>";
    for (Map.Entry<String, String> entry : map.entrySet()) {
      if (!entry.getKey().isEmpty() && !entry.getValue().isEmpty()) {
        String hash = entry.getValue();
        String el =
            "<"
                + PREFIX_PRS
                + entry.getKey()
                + ">"
                + hash
                + "</"
                + PREFIX_PRS
                + entry.getKey()
                + ">";
        root += el;
      }
    }
    root += "</bsnhashes>";
    return root;
  }
}