BAG2LoaderUtils.java

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

package nl.b3p.brmo.bag2.loader;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.datatype.XMLGregorianCalendar;
import nl.b3p.brmo.bag2.xml.leveringsdocument.BAGExtractLevering;
import nl.b3p.brmo.bag2.xml.leveringsdocument.Gemeente;
import nl.b3p.brmo.util.http.HttpSeekableByteChannel;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;

public class BAG2LoaderUtils {

  public static final String BUNDLE_NAME = "BAG2Loader";

  public static final String LEVERINGSDOCUMENT_FILENAME = "Leveringsdocument-BAG-Extract.xml";

  static final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE_NAME);

  public static String getUserAgent() {
    return String.format(
        "%s, %s (%s)/%s, %s/%s",
        getBundleString("app.user-agent"),
        System.getProperty("os.name"),
        System.getProperty("os.arch"),
        System.getProperty("os.version"),
        System.getProperty("java.vm.name"),
        System.getProperty("java.vm.version"));
  }

  public static ResourceBundle getBundle() {
    return bundle;
  }

  public static String getBundleString(String key) {
    return bundle.getString(key);
  }

  public static String getMessageFormattedString(String key, Object... args) {
    ResourceBundle bundle = getBundle();
    return new MessageFormat(bundle.getString(key), bundle.getLocale()).format(args);
  }

  public static String getLoaderVersion() {
    return getBundleString("app.version");
  }

  public static String getBrmoVersion() {
    return getBundleString("brmo.version");
  }

  public static BAGExtractSelectie getBAGExtractSelectieFromZip(String zipFileName)
      throws IOException {
    if (zipFileName.startsWith("http://") || zipFileName.startsWith("https://")) {
      // Try to parse filename in URL to avoid having to download the ZIP and extract the
      // leveringsdocument --
      // because this may happen many times to check whether a mutatie ZIP is applicable
      try {
        return BAGExtractSelectieFromFilename.parse(zipFileName);
      } catch (IllegalArgumentException iae) {
        // Could not parse BAG2 extract selection from filename, use HTTP random access to
        // parse only
        // leveringsdocument without downloading/streaming the entire ZIP file
        return getBAGExtractSelectieFromHttpZip(URI.create(zipFileName));
      }
    } else {
      return getBAGExtractSelectieFromZipFile(new File(zipFileName));
    }
  }

  private static BAGExtractSelectie getBAGExtractSelectieFromHttpZip(URI uri) throws IOException {
    String message = getMessageFormattedString("load.leveringsdocument.readzip", uri);
    try (HttpSeekableByteChannel channel = new HttpSeekableByteChannel(uri);
        org.apache.commons.compress.archivers.zip.ZipFile zipFile =
            new org.apache.commons.compress.archivers.zip.ZipFile(
                channel, uri.toString(), "UTF8", false, true)) {
      message = getMessageFormattedString("load.leveringsdocument.zipentries", uri);
      Iterator<ZipArchiveEntry> iterator =
          zipFile.getEntries(LEVERINGSDOCUMENT_FILENAME).iterator();
      if (!iterator.hasNext()) {
        throw new IOException(getMessageFormattedString("load.leveringsdocument.notfound", uri));
      }
      message = getMessageFormattedString("load.leveringsdocument.unmarshal", uri);
      JAXBContext jaxbContext = JAXBContext.newInstance(BAGExtractLevering.class);
      Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
      try (InputStream in = zipFile.getInputStream(iterator.next())) {
        BAGExtractLevering levering = (BAGExtractLevering) unmarshaller.unmarshal(in);
        return new BAGExtractSelectieFromLeveringsdocument(levering);
      }
    } catch (Exception e) {
      throw new IOException(message, e);
    }
  }

  public static BAGExtractSelectie getBAGExtractSelectieFromZipFile(File zipFile)
      throws IOException {
    String zipFileName = zipFile.getName();
    String message = getMessageFormattedString("load.leveringsdocument.readzip", zipFileName);
    try (ZipFile zf = new ZipFile(zipFile)) {
      message = getMessageFormattedString("load.leveringsdocument.zipentries", zipFileName);
      Enumeration<? extends ZipEntry> entries = zf.entries();
      while (entries.hasMoreElements()) {
        ZipEntry entry = entries.nextElement();
        if (LEVERINGSDOCUMENT_FILENAME.equals(entry.getName())) {
          message = getMessageFormattedString("load.leveringsdocument.unmarshal", zipFileName);
          JAXBContext jaxbContext = JAXBContext.newInstance(BAGExtractLevering.class);
          Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
          try (InputStream in = zf.getInputStream(entry)) {
            BAGExtractLevering levering = (BAGExtractLevering) unmarshaller.unmarshal(in);
            return new BAGExtractSelectieFromLeveringsdocument(levering);
          }
        }
      }
    } catch (Exception e) {
      throw new IOException(message, e);
    }
    throw new IOException(
        getMessageFormattedString("load.leveringsdocument.notfound", zipFileName));
  }

  public interface BAGExtractSelectie {
    boolean isStand();

    boolean isGebiedNLD();

    LocalDate getMutatiesFrom();

    LocalDate getMutatiesTot();

    Set<String> getGemeenteCodes();
  }

  public static class BAGExtractSelectieFromFilename implements BAGExtractSelectie {
    private String name;
    private boolean isStand;
    private boolean isGebiedNLD;
    private String gemeenteCode;
    private LocalDate mutatiesFrom;
    private LocalDate mutatiesTot;

    /**
     * @param name BAG2 filename to parse, can be URL or full path.
     * @throws IllegalArgumentException If the filename is not a BAG2 filename (outer zip only)
     */
    public static BAGExtractSelectie parse(String name) throws IllegalArgumentException {

      if (name.startsWith("http://") || name.startsWith("https://")) {
        URI uri = URI.create(name);
        String[] parts = uri.getPath().split("/");
        name = parts[parts.length - 1];
      }
      if (name.contains(File.separator)) {
        Path p = Path.of(name);
        name = p.getFileName().toString();
      }

      BAGExtractSelectieFromFilename selectie = new BAGExtractSelectieFromFilename();
      selectie.name = name;

      if (name.equals("lvbag-extract-nl.zip") || name.matches("BAGNLDL-\\d{8}\\.zip")) {
        selectie.isStand = true;
        selectie.isGebiedNLD = true;
        return selectie;
      }

      if (name.matches("BAGGEM\\d{4}L-\\d{8}\\.zip")) {
        selectie.isStand = true;
        selectie.isGebiedNLD = false;
        selectie.gemeenteCode = name.substring(6, 10);
        return selectie;
      }

      DateTimeFormatter dtf = DateTimeFormatter.ofPattern("ddMMyyyy");

      if (name.matches("BAGGEM\\d{4}M-\\d{8}-\\d{8}\\.zip")) {
        selectie.isStand = false;
        selectie.isGebiedNLD = false;
        selectie.gemeenteCode = name.substring(6, 10);
        // Gemeente can only have monthly updates
        selectie.mutatiesFrom = LocalDate.parse(name.substring(12, 20), dtf);
        selectie.mutatiesTot = LocalDate.parse(name.substring(21, 29), dtf);
        return selectie;
      }

      if (name.matches("BAGNLDM-\\d{8}-\\d{8}\\.zip")) {
        selectie.isStand = false;
        selectie.isGebiedNLD = true;
        selectie.mutatiesFrom = LocalDate.parse(name.substring(8, 16), dtf);
        selectie.mutatiesTot = LocalDate.parse(name.substring(17, 25), dtf);
        return selectie;
      }

      throw new IllegalArgumentException("Ongeldige BAG2 bestandsnaam: " + name);
    }

    public String getName() {
      return name;
    }

    @Override
    public boolean isStand() {
      return isStand;
    }

    @Override
    public boolean isGebiedNLD() {
      return isGebiedNLD;
    }

    @Override
    public Set<String> getGemeenteCodes() {
      return Collections.singleton(gemeenteCode);
    }

    @Override
    public LocalDate getMutatiesFrom() {
      return mutatiesFrom;
    }

    @Override
    public LocalDate getMutatiesTot() {
      return mutatiesTot;
    }
  }

  public static class BAGExtractSelectieFromLeveringsdocument implements BAGExtractSelectie {
    BAGExtractLevering levering;

    private BAGExtractSelectieFromLeveringsdocument(BAGExtractLevering levering) {
      this.levering = levering;
    }

    @Override
    public boolean isStand() {
      return levering.getSelectieGegevens().getLVCExtract() != null;
    }

    @Override
    public boolean isGebiedNLD() {
      return levering.getSelectieGegevens().getGebiedRegistratief().getGebiedNLD() != null;
    }

    @Override
    public LocalDate getMutatiesFrom() {
      XMLGregorianCalendar xmlGregorianCalendar =
          levering.getSelectieGegevens().getMUTExtract().getMutatieperiode().getMutatiedatumVanaf();
      return LocalDate.of(
          xmlGregorianCalendar.getYear(),
          xmlGregorianCalendar.getMonth(),
          xmlGregorianCalendar.getDay());
    }

    @Override
    public LocalDate getMutatiesTot() {
      XMLGregorianCalendar xmlGregorianCalendar =
          levering.getSelectieGegevens().getMUTExtract().getMutatieperiode().getMutatiedatumTot();
      return LocalDate.of(
          xmlGregorianCalendar.getYear(),
          xmlGregorianCalendar.getMonth(),
          xmlGregorianCalendar.getDay());
    }

    @Override
    public Set<String> getGemeenteCodes() {
      return levering
          .getSelectieGegevens()
          .getGebiedRegistratief()
          .getGebiedGEM()
          .getGemeenteCollectie()
          .getGemeente()
          .stream()
          .map(Gemeente::getGemeenteIdentificatie)
          .collect(Collectors.toSet());
    }
  }
}