BAG2GMLMutatieGroepStream.java

/*
 * Copyright (C) 2021 B3Partners B.V.
 *
 * SPDX-License-Identifier: MIT
 *
 */

package nl.b3p.brmo.bag2.loader;

import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.xml.namespace.QName;
import javax.xml.stream.Location;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import nl.b3p.brmo.bag2.schema.BAG2Object;
import nl.b3p.brmo.bag2.schema.BAG2ObjectType;
import nl.b3p.brmo.bag2.schema.BAG2Schema;
import nl.b3p.brmo.bag2.util.Force2DCoordinateSequenceFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.staxmate.SMInputFactory;
import org.codehaus.staxmate.in.SMEvent;
import org.codehaus.staxmate.in.SMInputCursor;
import org.geotools.api.referencing.FactoryException;
import org.geotools.gml.stream.XmlStreamGeometryReader;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.PrecisionModel;

public class BAG2GMLMutatieGroepStream implements Iterable<BAG2MutatieGroep> {
  private static final Log log = LogFactory.getLog(BAG2GMLMutatieGroepStream.class);

  private static final String NS_BAG_EXTRACT =
      "http://www.kadaster.nl/schemas/lvbag/extract-deelbestand-lvc/v20200601";
  private static final String NS_STANDLEVERING =
      "http://www.kadaster.nl/schemas/standlevering-generiek/1.0";
  private static final String NS_GML_32 = "http://www.opengis.net/gml/3.2";
  private static final String NS_HISTORIE =
      "www.kadaster.nl/schemas/lvbag/imbag/historie/v20200601";
  private static final String NS_OBJECTEN_REF =
      "www.kadaster.nl/schemas/lvbag/imbag/objecten-ref/v20200601";
  private static final String NS_BAG_EXTRACT_MUTATIES =
      "http://www.kadaster.nl/schemas/lvbag/extract-deelbestand-mutaties-lvc/v20200601";
  private static final String NS_MUTATIELEVERING =
      "http://www.kadaster.nl/schemas/mutatielevering-generiek/1.0";

  private static final QName BAG_STAND = new QName(NS_BAG_EXTRACT, "bagStand");
  private static final QName BAG_MUTATIES = new QName(NS_BAG_EXTRACT_MUTATIES, "bagMutaties");
  private static final int SRID = 28992;

  private final XmlStreamGeometryReader geometryReader;

  private final SMInputCursor cursor;

  private boolean isMutaties;
  private boolean hasMutatieGroep;

  private BagInfo bagInfo;

  public static class BagInfo {
    private final Date standTechnischeDatum;
    private final Date mutatieDatumVanaf;
    private final Date mutatieDatumTot;
    private Set<String> gemeenteIdentificaties;

    protected BagInfo(Date standTechnischeDatum, Date mutatieDatumVanaf, Date mutatieDatumTot) {
      this(standTechnischeDatum, mutatieDatumVanaf, mutatieDatumTot, (String) null);
    }

    protected BagInfo(
        Date standTechnischeDatum,
        Date mutatieDatumVanaf,
        Date mutatieDatumTot,
        String gemeenteIdentificatie) {
      this(
          standTechnischeDatum,
          mutatieDatumVanaf,
          mutatieDatumTot,
          Collections.singleton(gemeenteIdentificatie));
    }

    protected BagInfo(
        Date standTechnischeDatum,
        Date mutatieDatumVanaf,
        Date mutatieDatumTot,
        Set<String> gemeenteIdentificaties) {
      this.standTechnischeDatum = standTechnischeDatum;
      this.mutatieDatumVanaf = mutatieDatumVanaf;
      this.mutatieDatumTot = mutatieDatumTot;
      this.gemeenteIdentificaties = gemeenteIdentificaties;
    }

    public Date getStandTechnischeDatum() {
      return standTechnischeDatum;
    }

    public Date getMutatieDatumVanaf() {
      return mutatieDatumVanaf;
    }

    public Date getMutatieDatumTot() {
      return mutatieDatumTot;
    }

    public Set<String> getGemeenteIdentificaties() {
      return gemeenteIdentificaties;
    }

    public void setGemeenteIdentificaties(Set<String> gemeenteIdentificaties) {
      this.gemeenteIdentificaties = gemeenteIdentificaties;
    }

    public boolean equalsExceptGemeenteIdentificaties(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      BagInfo that = (BagInfo) o;
      return Objects.equals(standTechnischeDatum, that.standTechnischeDatum)
          && Objects.equals(mutatieDatumVanaf, that.mutatieDatumVanaf)
          && Objects.equals(mutatieDatumTot, that.mutatieDatumTot);
    }

    @Override
    public int hashCode() {
      return Objects.hash(standTechnischeDatum, mutatieDatumVanaf, mutatieDatumTot);
    }

    @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
    @Override
    public boolean equals(Object o) {
      return this.equalsExceptGemeenteIdentificaties(o);
    }

    @Override
    public String toString() {
      return "BagInfo{"
          + "standTechnischeDatum="
          + standTechnischeDatum
          + ", mutatieDatumVanaf="
          + mutatieDatumVanaf
          + ", mutatieDatumTot="
          + mutatieDatumTot
          + '}';
    }
  }

  public BAG2GMLMutatieGroepStream(InputStream in) throws XMLStreamException {
    this.cursor = initCursor(buildSMInputFactory().rootElementCursor(in));
    this.geometryReader = buildGeometryReader();
  }

  protected SMInputFactory buildSMInputFactory() {
    // Using alternative StAX parsers explicitly:
    // final XMLInputFactory stax = new WstxInputFactory(); // Woodstox
    // final XMLInputFactory stax = new com.fasterxml.aalto.stax.InputFactoryImpl(); // Aalto
    final XMLInputFactory xmlInputFactory =
        XMLInputFactory.newFactory(); // JRE Default, depends on JAR's present or
    // javax.xml.stream.XMLInputFactory property, can be SJSXP
    log.trace("StAX XMLInputFactory: " + xmlInputFactory.getClass().getName());

    xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); // Coalesce characters
    xmlInputFactory.setProperty(
        XMLInputFactory.SUPPORT_DTD,
        Boolean.FALSE); // No XML entity expansions or external entities

    return new SMInputFactory(xmlInputFactory);
  }

  protected XmlStreamGeometryReader buildGeometryReader() {
    GeometryFactory geometryFactory =
        new GeometryFactory(new PrecisionModel(), 28992, new Force2DCoordinateSequenceFactory());
    return new XmlStreamGeometryReader(this.cursor.getStreamReader(), geometryFactory);
  }

  private SMInputCursor initCursor(SMInputCursor cursor) throws XMLStreamException {
    QName root = cursor.advance().getQName();

    if (root.equals(BAG_STAND)) {
      isMutaties = false;
    } else if (root.equals(BAG_MUTATIES)) {
      isMutaties = true;
    } else {
      throw new IllegalArgumentException("XML root element moet bagStand of bagMutaties zijn");
    }

    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
    Date standTechnischeDatum = null;
    if (isMutaties) {
      cursor = cursor.childElementCursor().advance();
      cursor
          .getStreamReader()
          .require(XMLStreamConstants.START_ELEMENT, NS_BAG_EXTRACT_MUTATIES, "bagInfo");

      SMInputCursor bagInfoCursor = cursor.descendantElementCursor().advance();
      Date mutatieDatumVanaf = null, mutatieDatumTot = null;
      do {
        try {
          switch (bagInfoCursor.getLocalName()) {
            case "StandTechnischeDatum":
              standTechnischeDatum = df.parse(bagInfoCursor.collectDescendantText());
              break;
            case "MutatiedatumVanaf":
              mutatieDatumVanaf = df.parse(bagInfoCursor.collectDescendantText());
              break;
            case "MutatiedatumTot":
              mutatieDatumTot = df.parse(bagInfoCursor.collectDescendantText());
              break;
          }
        } catch (ParseException ignored) {
        }
      } while (bagInfoCursor.getNext() != null);
      bagInfo = new BagInfo(standTechnischeDatum, mutatieDatumVanaf, mutatieDatumTot);

      cursor.getNext();
      cursor
          .getStreamReader()
          .require(XMLStreamConstants.START_ELEMENT, NS_MUTATIELEVERING, "mutatieBericht");

      cursor = cursor.childElementCursor(new QName(NS_MUTATIELEVERING, "mutatieGroep"));

      hasMutatieGroep = cursor.getNext() != null;
    } else {
      cursor = cursor.childElementCursor().advance();
      cursor.getStreamReader().require(XMLStreamConstants.START_ELEMENT, NS_BAG_EXTRACT, "bagInfo");

      SMInputCursor bagInfoCursor = cursor.descendantElementCursor().advance();
      String gemeenteIdentificatie = null;
      do {
        switch (bagInfoCursor.getLocalName()) {
          case "StandTechnischeDatum":
            try {
              standTechnischeDatum = df.parse(bagInfoCursor.collectDescendantText());
            } catch (ParseException ignored) {
            }
            break;
          case "GemeenteIdentificatie":
            if (gemeenteIdentificatie != null) {
              throw new IllegalArgumentException(
                  "Alleen een enkele GemeenteIdentificatie in een GemeenteCollectie wordt ondersteund");
            }
            gemeenteIdentificatie = bagInfoCursor.collectDescendantText();
            break;
          case "Gebied-NLD":
            // "9999" is code for entire NL area
            gemeenteIdentificatie = "9999";
            break;
        }
      } while (bagInfoCursor.getNext() != null);

      bagInfo = new BagInfo(standTechnischeDatum, null, null, gemeenteIdentificatie);

      cursor.getNext();
      cursor
          .getStreamReader()
          .require(XMLStreamConstants.START_ELEMENT, NS_STANDLEVERING, "standBestand");

      cursor = cursor.childElementCursor(new QName(NS_STANDLEVERING, "stand"));
    }

    return cursor;
  }

  public BagInfo getBagInfo() {
    return bagInfo;
  }

  @Override
  public Iterator<BAG2MutatieGroep> iterator() {
    return new Iterator<>() {
      SMEvent event = cursor.getCurrEvent();

      @Override
      public boolean hasNext() {
        if (isMutaties && !hasMutatieGroep) {
          return false;
        }

        if (event != null) {
          return true;
        }
        try {
          event = cursor.getNext();
          return event != null;
        } catch (XMLStreamException e) {
          throw new RuntimeException(e);
        }
      }

      @Override
      public BAG2MutatieGroep next() {
        if (event == null) {
          if (!hasNext()) {
            throw new IllegalStateException("No more items");
          }
        }
        // Make sure cursor.getNext() is called in a future next() call
        event = null;

        try {
          if (isMutaties) {
            if (!hasMutatieGroep) {
              throw new IllegalStateException("No items");
            }

            cursor
                .getStreamReader()
                .require(XMLStreamConstants.START_ELEMENT, NS_MUTATIELEVERING, "mutatieGroep");
            SMInputCursor mutatieCursor = cursor.childElementCursor().advance();
            List<BAG2Mutatie> mutaties = new ArrayList<>();
            do {
              String mutatieNaam = mutatieCursor.getLocalName();

              Location location = mutatieCursor.getCursorLocation();
              switch (mutatieNaam) {
                case "wijziging":
                  {
                    SMInputCursor wijziging = mutatieCursor.childElementCursor().advance();
                    wijziging
                        .getStreamReader()
                        .require(XMLStreamConstants.START_ELEMENT, NS_MUTATIELEVERING, "was");
                    BAG2Object was =
                        parseBAG2ObjectFromBagObjectParentElement(
                            wijziging, NS_BAG_EXTRACT_MUTATIES);
                    wijziging.getNext();
                    wijziging
                        .getStreamReader()
                        .require(XMLStreamConstants.START_ELEMENT, NS_MUTATIELEVERING, "wordt");
                    BAG2Object wordt =
                        parseBAG2ObjectFromBagObjectParentElement(
                            wijziging, NS_BAG_EXTRACT_MUTATIES);

                    mutaties.add(new BAG2WijzigingMutatie(location, was, wordt));
                    break;
                  }
                case "toevoeging":
                  {
                    SMInputCursor wijziging = mutatieCursor.childElementCursor().advance();
                    wijziging
                        .getStreamReader()
                        .require(XMLStreamConstants.START_ELEMENT, NS_MUTATIELEVERING, "wordt");
                    BAG2Object toevoeging =
                        parseBAG2ObjectFromBagObjectParentElement(
                            wijziging, NS_BAG_EXTRACT_MUTATIES);
                    mutaties.add(new BAG2ToevoegingMutatie(location, toevoeging));

                    break;
                  }
                case "verwijdering":
                  throw new IllegalArgumentException(
                      "Verwijdering-mutaties mogen niet voorkomen in de BAG2");
                default:
                  throw new IllegalArgumentException("Onbekende mutatie: " + mutatieNaam);
              }
            } while (mutatieCursor.getNext() == SMEvent.START_ELEMENT);

            return new BAG2MutatieGroep(mutaties);
          } else {
            cursor
                .getStreamReader()
                .require(XMLStreamConstants.START_ELEMENT, NS_STANDLEVERING, "stand");
            Location location = cursor.getCursorLocation();
            BAG2Object object = parseBAG2ObjectFromBagObjectParentElement(cursor, NS_BAG_EXTRACT);
            return new BAG2MutatieGroep(List.of(new BAG2ToevoegingMutatie(location, object)));
          }
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
      }
    };
  }

  private BAG2Object parseBAG2ObjectFromBagObjectParentElement(
      SMInputCursor bagObjectParentCursor, String bagObjectNamespace)
      throws XMLStreamException, FactoryException, IOException {
    SMInputCursor bagObjectCursor =
        bagObjectParentCursor
            .childElementCursor(new QName(bagObjectNamespace, "bagObject"))
            .advance()
            .childElementCursor()
            .advance();
    return parseBAG2Object(bagObjectCursor);
  }

  private BAG2Object parseBAG2Object(SMInputCursor bagObjectCursor)
      throws XMLStreamException, FactoryException, IOException {
    String name = bagObjectCursor.getLocalName();

    final BAG2ObjectType objectType = BAG2Schema.getInstance().getObjectTypeByName(name);
    if (objectType == null) {
      throw new IllegalArgumentException("Onbekend object type: " + name);
    }

    Map<String, Object> attributes = new HashMap<>();
    SMInputCursor attributeCursor = bagObjectCursor.childElementCursor();
    while (attributeCursor.getNext() != null) {
      parseAttribute(attributeCursor, attributes);
    }
    return new BAG2Object(objectType, attributes);
  }

  private void parseAttribute(SMInputCursor attribute, Map<String, Object> objectAttributes)
      throws XMLStreamException, FactoryException, IOException {
    String attributeName = attribute.getLocalName();

    switch (attributeName) {
      case "geometrie":
        // Position cursor at child element
        SMInputCursor geomCursor = attribute.childElementCursor().advance();
        // Sometimes there is an element like "punt" which has the actual geometry as child
        // element
        if (!geomCursor.getNsUri().equals(NS_GML_32)) {
          geomCursor.childElementCursor().advance();
        }
        Geometry geom = geometryReader.readGeometry();
        geom.setSRID(SRID);
        objectAttributes.put(attributeName, geom);
        break;
      case "voorkomen":
        // Flatten al Voorkomen child attributes, according to the schema the element names
        // do not conflict
        parseVoorkomen(attribute, objectAttributes);
        break;
      case "BeschikbaarLV":
        // Flatten al Voorkomen/BeschikbaarLV child attributes to the top level, according
        // to the schema the element
        // names do not conflict
        parseBeschikbaarLV(attribute, objectAttributes);
        break;
      case "heeftAlsNevenadres":
        parseNevenadres(attribute, objectAttributes);
        break;
      case "maaktDeelUitVan":
        parseMaaktDeelUitVan(attribute, objectAttributes);
        break;
      case "gebruiksdoel":
        parseGebruiksdoel(attribute, objectAttributes);
        break;
      case "geconstateerd":
        // Parse 'J' or 'N' to 'true' or 'false'
        String jn = attribute.collectDescendantText().trim();
        objectAttributes.put(attributeName, "J".equals(jn) ? "true" : "false");
        break;
      default:
        // String attribute value as default

        // This also works for ligtIn en ligtAan
        objectAttributes.put(attributeName, attribute.collectDescendantText().trim());
        break;
    }
  }

  private void parseVoorkomen(SMInputCursor attributeCursor, Map<String, Object> objectAttributes)
      throws XMLStreamException, FactoryException, IOException {
    attributeCursor =
        attributeCursor
            .childElementCursor(new QName(NS_HISTORIE, "Voorkomen"))
            .advance()
            .childElementCursor();
    while (attributeCursor.getNext() != null) {
      parseAttribute(attributeCursor, objectAttributes);
    }
  }

  private void parseBeschikbaarLV(
      SMInputCursor attributeCursor, Map<String, Object> objectAttributes)
      throws XMLStreamException, FactoryException, IOException {
    attributeCursor = attributeCursor.childElementCursor();
    while (attributeCursor.getNext() != null) {
      parseAttribute(attributeCursor, objectAttributes);
    }
  }

  private void parseNevenadres(SMInputCursor attributeCursor, Map<String, Object> objectAttributes)
      throws XMLStreamException {
    Set<String> values = new HashSet<>();
    attributeCursor =
        attributeCursor.childElementCursor(new QName(NS_OBJECTEN_REF, "NummeraanduidingRef"));
    while (attributeCursor.getNext() != null) {
      values.add(attributeCursor.collectDescendantText().trim());
    }
    objectAttributes.put("heeftAlsNevenadres", values);
  }

  private void parseMaaktDeelUitVan(
      SMInputCursor attributeCursor, Map<String, Object> objectAttributes)
      throws XMLStreamException {
    Set<String> values = new HashSet<>();
    attributeCursor = attributeCursor.childElementCursor(new QName(NS_OBJECTEN_REF, "PandRef"));
    while (attributeCursor.getNext() != null) {
      values.add(attributeCursor.collectDescendantText().trim());
    }
    objectAttributes.put("maaktDeelUitVan", values);
  }

  private void parseGebruiksdoel(
      SMInputCursor attributeCursor, Map<String, Object> objectAttributes)
      throws XMLStreamException {
    Set<String> values = (Set<String>) objectAttributes.get("gebruiksdoel");
    if (values == null) {
      values = new HashSet<>();
      objectAttributes.put("gebruiksdoel", values);
    }
    values.add(attributeCursor.collectDescendantText().trim());
  }
}