GDS2OphalenProces.java

/*
 * Copyright (C) 2015 B3Partners B.V.
 */
package nl.b3p.brmo.service.scanner;

import static nl.b3p.brmo.persistence.staging.AutomatischProces.LOG_NEWLINE;
import static nl.b3p.gds2.GDS2Util.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.persistence.*;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.Handler;
import nl.b3p.brmo.loader.BrmoFramework;
import nl.b3p.brmo.loader.entity.Brk2Bericht;
import nl.b3p.brmo.loader.entity.BrkBericht;
import nl.b3p.brmo.loader.util.BrmoException;
import nl.b3p.brmo.loader.xml.Brk2SnapshotXMLReader;
import nl.b3p.brmo.loader.xml.BrkSnapshotXMLReader;
import nl.b3p.brmo.persistence.staging.AutomatischProces;
import nl.b3p.brmo.persistence.staging.Bericht;
import nl.b3p.brmo.persistence.staging.ClobElement;
import nl.b3p.brmo.persistence.staging.GDS2OphaalProces;
import nl.b3p.brmo.persistence.staging.LaadProces;
import nl.b3p.brmo.service.util.TrustManagerDelegate;
import nl.b3p.brmo.soap.util.LogMessageHandler;
import nl.kadaster.schemas.gds2.afgifte_bestandenlijstopvragen.v20170401.BestandenlijstOpvragenType;
import nl.kadaster.schemas.gds2.afgifte_bestandenlijstresultaat.afgifte.v20170401.AfgifteType;
import nl.kadaster.schemas.gds2.afgifte_bestandenlijstselectie.v20170401.AfgifteSelectieCriteriaType;
import nl.kadaster.schemas.gds2.afgifte_bestandenlijstselectie.v20170401.BestandKenmerkenType;
import nl.kadaster.schemas.gds2.afgifte_proces.v20170401.FilterDatumTijdType;
import nl.kadaster.schemas.gds2.afgifte_proces.v20170401.KlantAfgiftenummerReeksType;
import nl.kadaster.schemas.gds2.imgds.baseurl.v20170401.BaseURLType;
import nl.kadaster.schemas.gds2.service.afgifte.v20170401.Gds2AfgifteServiceV20170401;
import nl.kadaster.schemas.gds2.service.afgifte.v20170401.Gds2AfgifteServiceV20170401Service;
import nl.kadaster.schemas.gds2.service.afgifte_bestandenlijstopvragen.v20170401.BestandenlijstOpvragenRequest;
import nl.kadaster.schemas.gds2.service.afgifte_bestandenlijstopvragen.v20170401.BestandenlijstOpvragenResponse;
import nl.logius.digikoppeling.gb._2010._10.DataReference;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;
import org.stripesstuff.stripersist.Stripersist;

/**
 * @author Matthijs Laan
 */
public class GDS2OphalenProces extends AbstractExecutableProces {

  private static final Log log = LogFactory.getLog(GDS2OphalenProces.class);

  private final GDS2OphaalProces config;

  @Transient private ProgressUpdateListener l;

  @Transient private SSLContext context;

  private static final int BESTANDENLIJST_ATTEMPTS = 5;
  private static final int BESTANDENLIJST_RETRY_WAIT = 10000;

  private static final int AFGIFTE_DOWNLOAD_ATTEMPTS = 5;
  private static final int AFGIFTE_DOWNLOAD_RETRY_WAIT = 3000;

  public GDS2OphalenProces(GDS2OphaalProces config) {
    this.config = config;
  }

  public static Log getLog() {
    return log;
  }

  @Override
  public void execute() throws BrmoException {
    this.execute(
        new ProgressUpdateListener() {
          @Override
          public void total(long total) {}

          @Override
          public void progress(long progress) {}

          @Override
          public void exception(Throwable t) {
            log.error(t);
          }

          @Override
          public void updateStatus(String status) {}

          @Override
          public void addLog(String log) {
            GDS2OphalenProces.log.info(log);
          }
        });
  }

  @Override
  public void execute(ProgressUpdateListener listener) {
    this.l = listener;

    try {
      l.updateStatus("Initialiseren...");
      final Date startDate = new Date();
      l.addLog(
          String.format("Initialiseren GDS2 ophalen proces %d... %tc", config.getId(), startDate));
      this.config.updateSamenvattingEnLogfile(
          String.format("Het GDS2 ophalen proces is gestart op %tc.", startDate));
      this.config.setStatus(AutomatischProces.ProcessingStatus.PROCESSING);
      this.config.setLastrun(startDate);
      Stripersist.getEntityManager().merge(this.config);
      Stripersist.getEntityManager().flush();

      BestandenlijstOpvragenRequest request = new BestandenlijstOpvragenRequest();
      BestandenlijstOpvragenType verzoek = new BestandenlijstOpvragenType();
      AfgifteSelectieCriteriaType criteria = new AfgifteSelectieCriteriaType();
      request.setVerzoek(verzoek);
      verzoek.setAfgifteSelectieCriteria(criteria);
      criteria.setBestandKenmerken(new BestandKenmerkenType());

      String contractnummer =
          ClobElement.nullSafeGet(this.config.getConfig().get("gds2_contractnummer"));
      if (contractnummer != null) {
        log.debug("GDS2 contractnummer is: " + contractnummer);
        criteria.getBestandKenmerken().setContractnummer(contractnummer);
      }
      String artikelnummer =
          ClobElement.nullSafeGet(this.config.getConfig().get("gds2_artikelnummer"));
      if (artikelnummer != null) {
        log.debug("GDS2 artikelnummer is: " + artikelnummer);
        l.addLog("GDS2 artikelnummer is: " + artikelnummer);
        criteria.getBestandKenmerken().setArtikelnummer(artikelnummer);
      }

      String alGerapporteerd =
          ClobElement.nullSafeGet(this.config.getConfig().get("gds2_al_gerapporteerde_afgiftes"));
      if (!"true".equals(alGerapporteerd)) {
        criteria.setNogNietGerapporteerd(true);
      }

      // datum tijd parsen/instellen
      GregorianCalendar vanaf;
      GregorianCalendar tot =
          getDatumTijd(ClobElement.nullSafeGet(this.config.getConfig().get("totdatum")));
      String sVanaf = ClobElement.nullSafeGet(this.config.getConfig().get("vanafdatum"));
      if (tot != null && ("-1".equals(sVanaf) | "-2".equals(sVanaf) | "-3".equals(sVanaf))) {
        vanaf =
            getDatumTijd(
                ClobElement.nullSafeGet(this.config.getConfig().get("totdatum")),
                Integer.parseInt(sVanaf));
      } else {
        vanaf = getDatumTijd(sVanaf);
      }

      List<AfgifteType> afgiftes = new ArrayList<>();
      BestandenlijstOpvragenResponse response;
      int hoogsteKlantAfgifteNummer = -1;

      Gds2AfgifteServiceV20170401 gds2 = initGDS2();
      l.updateStatus("Uitvoeren SOAP request naar Kadaster...");

      boolean afgifteNummerGebaseerdOphalen = false;
      if (vanaf == null && tot == null) {
        //  instellen laagste en hoogste afgiftenummer voor dit verzoek, mag alleen als er
        // geen periode is gedefinieerd
        KlantAfgiftenummerReeksType afgiftenummers = new KlantAfgiftenummerReeksType();
        String nr =
            ClobElement.nullSafeGet(this.config.getConfig().get("klantafgiftenummer_vanaf"));
        if (StringUtils.isNotBlank(nr)) {
          BigInteger afgifteNrVanaf = new BigInteger(nr);
          afgiftenummers.setKlantAfgiftenummerVanaf(afgifteNrVanaf);
          log.info("Ophalen vanaf klant afgifte nummer: " + afgifteNrVanaf);
          l.addLog("Ophalen vanaf klant afgifte nummer: " + afgifteNrVanaf);

          nr = ClobElement.nullSafeGet(this.config.getConfig().get("klantafgiftenummer_totenmet"));
          if (StringUtils.isNotBlank(nr)) {
            BigInteger afgifteNrTM = new BigInteger(nr);
            afgiftenummers.setKlantAfgiftenummerTotmet(afgifteNrTM);
            log.info("Ophalen tot en met klant afgifte nummer: " + afgifteNrTM);
            l.addLog("Ophalen tot en met klant afgifte nummer: " + afgifteNrTM);
          }
          criteria.setKlantAfgiftenummerReeks(afgiftenummers);
          afgifteNummerGebaseerdOphalen = true;
        }
      }

      if (afgifteNummerGebaseerdOphalen) {
        // <editor-fold> afgiftenummer-based ophalen
        l.addLog(
            "GDS2 verzoek voor afgiftenummers vanaf: "
                + criteria.getKlantAfgiftenummerReeks().getKlantAfgiftenummerVanaf());
        l.addLog(
            "GDS2 verzoek voor afgiftenummers tot-en-met: "
                + criteria.getKlantAfgiftenummerReeks().getKlantAfgiftenummerTotmet());
        response =
            retryBestandenLijstOpvragen(
                gds2, request, BESTANDENLIJST_ATTEMPTS, BESTANDENLIJST_RETRY_WAIT);
        hoogsteKlantAfgifteNummer = response.getAntwoord().getKlantAfgiftenummerMax();
        boolean hasMore = response.getAntwoord().isMeerAfgiftesbeschikbaar();
        int aantalInAntwoord = response.getAntwoord().getAfgifteAantalInLijst();
        if (aantalInAntwoord > 0) {
          afgiftes.addAll(response.getAntwoord().getBestandenLijst().getAfgifte());
        }
        l.addLog("Aantal in antwoord: " + aantalInAntwoord);
        l.addLog("Meer afgiftes beschikbaar: " + hasMore);

        while (hasMore) {
          criteria
              .getKlantAfgiftenummerReeks()
              .setKlantAfgiftenummerVanaf(
                  // vanaf ophogen met aantal opgehaald
                  criteria
                      .getKlantAfgiftenummerReeks()
                      .getKlantAfgiftenummerVanaf()
                      .add(BigInteger.valueOf(aantalInAntwoord)));
          l.addLog(
              "GDS2 verzoek voor afgiftenummers vanaf: "
                  + criteria.getKlantAfgiftenummerReeks().getKlantAfgiftenummerVanaf());
          l.addLog(
              "GDS2 verzoek voor afgiftenummers tot-en-met: "
                  + criteria.getKlantAfgiftenummerReeks().getKlantAfgiftenummerTotmet());
          response =
              retryBestandenLijstOpvragen(
                  gds2, request, BESTANDENLIJST_ATTEMPTS, BESTANDENLIJST_RETRY_WAIT);
          hoogsteKlantAfgifteNummer = response.getAntwoord().getKlantAfgiftenummerMax();
          aantalInAntwoord = response.getAntwoord().getAfgifteAantalInLijst();
          hasMore = response.getAntwoord().isMeerAfgiftesbeschikbaar();
          l.addLog("Aantal in antwoord: " + aantalInAntwoord);
          l.addLog("Meer afgiftes beschikbaar: " + hasMore);

          if (aantalInAntwoord > 0) {
            afgiftes.addAll(response.getAntwoord().getBestandenLijst().getAfgifte());
          }
        }
        // </editor-fold> afgiftenummer-based ophalen
      } else {
        // <editor-fold> time-based ophalen
        GregorianCalendar currentMoment = null;
        boolean parseblePeriod = false;
        int loopType = Calendar.DAY_OF_MONTH;
        int loopMax = 180;
        int loopNum = 0;
        boolean reducePeriod = false;
        boolean increasePeriod = false;

        if (vanaf != null && tot != null && vanaf.before(tot)) {
          parseblePeriod = true;
          currentMoment = vanaf;
        }

        boolean morePeriods2Process = false;
        do /* while morePeriods2Process is true */ {
          l.addLog("\n*** start periode ***");
          // zet periode in criteria indien gewenst
          if (parseblePeriod) {
            // check of de periodeduur verkleind moet worden
            if (reducePeriod) {
              switch (loopType) {
                case Calendar.DAY_OF_MONTH:
                  currentMoment.add(loopType, -1);
                  loopType = Calendar.HOUR_OF_DAY;
                  l.addLog("* Verklein loop periode naar uur");
                  break;
                case Calendar.HOUR_OF_DAY:
                  currentMoment.add(loopType, -1);
                  loopType = Calendar.MINUTE;
                  l.addLog("* Verklein loop periode naar minuut");
                  break;
                case Calendar.MINUTE:
                default:
                  /*
                   * Hier kom je alleen als binnen een minuut meer
                   * dan 2000 berichten zijn aangamaakt en het
                   * vinkje ook "al rapporteerde berichten
                   * ophalen" staat aan.
                   */
                  l.addLog("Niet alle gevraagde berichten zijn opgehaald");
              }
              reducePeriod = false;
            }

            // check of de periodeduur vergroot moet worden
            if (increasePeriod) {
              switch (loopType) {
                case Calendar.HOUR_OF_DAY:
                  loopType = Calendar.DAY_OF_MONTH;
                  l.addLog("* Vergroot loop periode naar dag");
                  break;
                case Calendar.MINUTE:
                  loopType = Calendar.HOUR_OF_DAY;
                  l.addLog("* Vergroot loop periode naar uur");
                  break;
                case Calendar.DAY_OF_MONTH:
                default:
                  // not possible
              }
              increasePeriod = false;
            }

            FilterDatumTijdType d = new FilterDatumTijdType();
            d.setDatumTijdVanaf(getXMLDatumTijd(currentMoment));
            l.addLog(String.format("Datum vanaf: %tc", currentMoment.getTime()));
            currentMoment.add(loopType, 1);
            d.setDatumTijdTotmet(getXMLDatumTijd(currentMoment));
            l.addLog(String.format("Datum tot: %tc", currentMoment.getTime()));
            criteria.setPeriode(d);

            switch (loopType) {
              case Calendar.HOUR_OF_DAY:
                // 0-23
                if (currentMoment.get(loopType) == 0) {
                  increasePeriod = true;
                }
                break;
              case Calendar.MINUTE:
                // 0-59
                if (currentMoment.get(loopType) == 0) {
                  increasePeriod = true;
                }
                break;
              case Calendar.DAY_OF_MONTH:
              default:
                // alleen dagen tellen, uur en minuut altijd helemaal
                loopNum++;
            }

            // bereken of einde van periode bereikt is
            morePeriods2Process = currentMoment.before(tot) && loopNum < loopMax;
          }

          verzoek.setAfgifteSelectieCriteria(criteria);
          response =
              retryBestandenLijstOpvragen(
                  gds2, request, BESTANDENLIJST_ATTEMPTS, BESTANDENLIJST_RETRY_WAIT);
          hoogsteKlantAfgifteNummer = response.getAntwoord().getKlantAfgiftenummerMax();

          int aantalInAntwoord = response.getAntwoord().getAfgifteAantalInLijst();
          l.addLog("Aantal in antwoord: " + aantalInAntwoord);
          // opletten; in de xsd staat een default value van 'J' voor
          // meerAfgiftesbeschikbaar
          boolean hasMore = response.getAntwoord().isMeerAfgiftesbeschikbaar();
          l.addLog("Meer afgiftes beschikbaar: " + hasMore);

          /*
           * Als "al gerapporteerde berichten" moeten worden opgehaald en
           * er zitten dan 2000 berichten in het antwoord dan heeft het
           * geen zin om meer keer de berichten op te halen, je krijgt
           * telkens dezelfde.
           */
          if (hasMore && "true".equals(alGerapporteerd)) {
            reducePeriod = true;
            morePeriods2Process = true;
            increasePeriod = false;
            // als er geen parsable periode is
            // (geen periode ingevuld en alGerapporteerd is true
            // dan moet morePeriods2Process false worden om een
            // eindloze while loop te voorkomen
            if (!parseblePeriod) {
              morePeriods2Process = false;
            } else {
              continue;
            }
          }

          if (aantalInAntwoord > 0) {
            afgiftes.addAll(response.getAntwoord().getBestandenLijst().getAfgifte());
          }

          /*
           * Indicatie nog niet gerapporteerd: Met deze indicatie wordt
           * aangegeven of uitsluitend de nog niet gerapporteerde
           * bestanden moeten worden opgenomen in de lijst, of dat alle
           * beschikbare bestanden worden genoemd. Niet gerapporteerd
           * betekent in dit geval ‘niet eerder opgevraagd in deze
           * bestandenlijst’. Als deze indicator wordt gebruikt, dan
           * worden na terugmelding van de bestandenlijst de bijbehorende
           * bestanden gemarkeerd als zijnde ‘gerapporteerd’ in het
           * systeem van GDS.
           */
          int moreCount = 0;
          String dontGetMoreConfig =
              ClobElement.nullSafeGet(
                  this.config.getConfig().get("gds2_niet_gerapporteerde_afgiftes_niet_ophalen"));

          while (hasMore && !"true".equals(dontGetMoreConfig)) {
            l.updateStatus(
                "Uitvoeren SOAP request naar Kadaster voor meer afgiftes..." + moreCount++);
            criteria.setNogNietGerapporteerd(true);
            response =
                retryBestandenLijstOpvragen(
                    gds2, request, BESTANDENLIJST_ATTEMPTS, BESTANDENLIJST_RETRY_WAIT);
            hoogsteKlantAfgifteNummer = response.getAntwoord().getKlantAfgiftenummerMax();

            List<AfgifteType> afgifteLijst =
                response.getAntwoord().getBestandenLijst().getAfgifte();
            for (AfgifteType t : afgiftes) {
              // lijst urls naar aparte logfile loggen
              if (t.getDigikoppelingExternalDatareferences() != null
                  && t.getDigikoppelingExternalDatareferences().getDataReference() != null) {
                for (DataReference dr :
                    t.getDigikoppelingExternalDatareferences().getDataReference()) {
                  log.info(
                      "GDS2url te downloaden: "
                          + dr.getTransport().getLocation().getSenderUrl().getValue());
                  break;
                }
              }
            }
            afgiftes.addAll(afgifteLijst);
            aantalInAntwoord = response.getAntwoord().getAfgifteAantalInLijst();
            l.addLog("Aantal in antwoord: " + aantalInAntwoord);
            hasMore = response.getAntwoord().isMeerAfgiftesbeschikbaar();
            l.addLog("Nog meer afgiftes beschikbaar: " + hasMore);
          }

        } while (morePeriods2Process);
        // </editor-fold> time-based ophalen
      }
      l.total(afgiftes.size());
      l.addLog("Totaal aantal op te halen berichten: " + afgiftes.size());
      l.addLog("Hoogste klant afgifte nummer: " + hoogsteKlantAfgifteNummer);

      this.config
          .getConfig()
          .put("hoogste_afgiftenummer", new ClobElement("" + hoogsteKlantAfgifteNummer));

      final String soort =
          this.config.getConfig().getOrDefault("gds2_br_soort", new ClobElement("brk")).getValue();
      switch (soort) {
        case "brk2":
        case "brk":
          verwerkAfgiftes(afgiftes, getCertificaatBaseURL(response.getAntwoord()), soort);
          break;
        default:
          throw new BrmoException("Onbekende basisregistratie soort: " + soort);
      }

    } catch (Exception e) {
      log.error("Fout bij ophalen van GDS2 berichten", e);
      this.config.setLastrun(new Date());
      this.config.setSamenvatting("Er is een fout opgetreden, details staan in de logs");
      this.config.setStatus(AutomatischProces.ProcessingStatus.ERROR);
      String m = "Fout bij ophalen van GDS2 berichten: " + ExceptionUtils.getMessage(e);
      if (e.getCause() != null) {
        m += ", oorzaak: " + ExceptionUtils.getRootCauseMessage(e);
      }
      l.addLog(m);
      l.exception(e);
    } finally {
      if (Stripersist.getEntityManager().getTransaction().getRollbackOnly()) {
        // XXX bij rollback only wordt status niet naar ERROR gezet vanwege
        // rollback, zou in aparte transactie moeten
        Stripersist.getEntityManager().getTransaction().rollback();
      } else {
        Stripersist.getEntityManager().merge(this.config);
        Stripersist.getEntityManager().getTransaction().commit();
      }
      l.updateStatus("Uitvoeren SOAP request naar Kadaster afgerond");
    }
  }

  private String getLaadprocesBestandsnaam(AfgifteType a) {
    return a.getBestand().getBestandsnaam() + " (" + a.getAfgifteID() + ")";
  }

  private boolean isAfgifteAlGeladen(AfgifteType a) {
    try {
      Stripersist.getEntityManager()
          .createQuery("select 1 from LaadProces lp where lp.bestand_naam = :n")
          .setParameter("n", getLaadprocesBestandsnaam(a))
          .getSingleResult();
      return true;
    } catch (NoResultException nre) {
      return false;
    }
  }

  private Bericht laadAfgifte(AfgifteType a, String url, String brkSoort) throws Exception {
    EntityManager em = Stripersist.getEntityManager();
    String msg = "Downloaden " + url;
    l.updateStatus(msg);
    l.addLog(msg);

    ByteArrayOutputStream bos = new ByteArrayOutputStream();

    int attempt = 0;
    while (true) {
      try {
        URLConnection connection = new URL(url).openConnection();
        if (connection instanceof HttpsURLConnection) {
          ((HttpsURLConnection) connection).setSSLSocketFactory(context.getSocketFactory());
        }
        InputStream input = (InputStream) connection.getContent();
        IOUtils.copy(input, bos);
        break;
      } catch (Exception e) {
        attempt++;
        if (attempt == AFGIFTE_DOWNLOAD_ATTEMPTS) {
          l.addLog(
              "Fout bij laatste poging downloaden afgifte: "
                  + e.getClass().getName()
                  + ": "
                  + e.getMessage());
          throw e;
        } else {
          l.addLog(
              "Fout bij poging "
                  + attempt
                  + " om afgifte te downloaden: "
                  + e.getClass().getName()
                  + ": "
                  + e.getMessage());
          Thread.sleep(AFGIFTE_DOWNLOAD_RETRY_WAIT);
          l.addLog(
              "Uitvoeren poging " + (attempt + 1) + " om afgifte " + url + " te downloaden...");
        }
      }
    }

    LaadProces lp = new LaadProces();
    lp.setBestand_naam(getLaadprocesBestandsnaam(a));
    lp.setKlantafgiftenummer(a.getKlantAfgiftenummer());
    lp.setContractafgiftenummer(a.getContractAfgiftenummer());
    lp.setArtikelnummer(a.getBestandKenmerken().getArtikelnummer());
    lp.setContractnummer(a.getBestandKenmerken().getContractnummer());
    lp.setAfgifteid(a.getAfgifteID());
    lp.setAfgiftereferentie(a.getAfgiftereferentie());
    lp.setBestandsreferentie(a.getBestand().getBestandsreferentie());
    lp.setBeschikbaar_tot(a.getBeschikbaarTot().toGregorianCalendar().getTime());
    lp.setBestand_naam_hersteld(a.getBestand().getBestandsnaam());

    if (a.getDigikoppelingExternalDatareferences() != null
        && a.getDigikoppelingExternalDatareferences().getDataReference() != null) {
      for (DataReference dr : a.getDigikoppelingExternalDatareferences().getDataReference()) {
        lp.setBestand_datum(
            dr.getLifetime().getCreationTime().getValue().toGregorianCalendar().getTime());
        break;
      }
    }
    lp.setSoort(brkSoort);
    lp.setStatus(LaadProces.STATUS.STAGING_OK);
    lp.setStatus_datum(new Date());
    SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
    lp.setOpmerking("GDS2 download van " + url + " op " + sdf.format(new Date()));
    lp.setAutomatischProces(em.find(AutomatischProces.class, config.getId()));

    Bericht b = new Bericht();
    b.setLaadprocesid(lp);
    b.setDatum(lp.getBestand_datum());
    b.setSoort(brkSoort);
    b.setStatus_datum(new Date());

    ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(bos.toByteArray()));
    ZipEntry entry = zip.getNextEntry();
    while (entry != null && !entry.getName().toLowerCase().endsWith(".xml")) {
      msg = "Overslaan zip entry geen XML: " + entry.getName();
      l.addLog(msg);
      entry = zip.getNextEntry();
    }
    if (entry == null) {
      msg = "Geen geschikt XML bestand gevonden in zip bestand!";
      l.addLog(msg);
      return null;
    }
    b.setBr_orgineel_xml(IOUtils.toString(zip, StandardCharsets.UTF_8));

    if (log.isDebugEnabled()) {
      // dump xml bestand naar /tmp/ voordat de xml wordt geparsed
      String fName = File.createTempFile(lp.getBestand_naam(), ".xml").getAbsolutePath();
      log.debug("Dump xml bericht naar: " + fName);
      IOUtils.write(b.getBr_orgineel_xml(), new FileWriter(fName, StandardCharsets.UTF_8));
    }
    try {
      String brXML;
      Date brDatum;
      Integer brVolgordeNummer;
      String brObjectRef;
      if (BrmoFramework.BR_BRK.equals(brkSoort)) {
        BrkSnapshotXMLReader reader =
            new BrkSnapshotXMLReader(
                new ByteArrayInputStream(b.getBr_orgineel_xml().getBytes(StandardCharsets.UTF_8)));
        BrkBericht parsedBericht = reader.next();
        brXML = parsedBericht.getBrXml();
        brDatum = parsedBericht.getDatum();
        brVolgordeNummer = parsedBericht.getVolgordeNummer();
        brObjectRef = parsedBericht.getObjectRef();
      } else {
        Brk2SnapshotXMLReader reader2 =
            new Brk2SnapshotXMLReader(
                new ByteArrayInputStream(b.getBr_orgineel_xml().getBytes(StandardCharsets.UTF_8)));
        Brk2Bericht parsedBericht = reader2.next();
        brXML = parsedBericht.getBrXml();
        brDatum = parsedBericht.getDatum();
        brVolgordeNummer = parsedBericht.getVolgordeNummer();
        brObjectRef = parsedBericht.getObjectRef();
      }

      if (null != brDatum) {
        b.setDatum(brDatum);
      }
      b.setBr_xml(brXML);
      b.setVolgordenummer(brVolgordeNummer);

      // Als objectRef niet opgehaald kan worden,dan kan het
      // bericht niet verwerkt worden.
      if (null != brObjectRef && !brObjectRef.isBlank()) {
        b.setObject_ref(brObjectRef);
        b.setStatus(Bericht.STATUS.STAGING_OK);
        b.setOpmerking("Klaar voor verwerking.");
      } else {
        b.setStatus(Bericht.STATUS.STAGING_NOK);
        b.setOpmerking("Object Ref niet gevonden in bericht-xml, neem contact op met leverancier.");
      }
    } catch (Exception e) {
      b.setStatus(Bericht.STATUS.STAGING_NOK);
      StringWriter sw = new StringWriter();
      e.printStackTrace(new PrintWriter(sw));
      b.setOpmerking("Fout bij parsen BRK bericht: " + sw);
    }
    try {
      em.persist(lp);
      em.persist(b);
      em.merge(this.config);
      em.flush();
      em.getTransaction().commit();
      em.clear();
    } catch (PersistenceException pe) {
      if (!em.getTransaction().isActive()) {
        em.getTransaction().begin();
      }
      em.getTransaction().rollback();
      em.getTransaction().begin();
      lp.setId(null);
      log.warn("Opslaan van bericht uit laadproces " + lp.getBestand_naam() + " is mislukt.", pe);
      log.warn(
          "Duplicaat bericht: "
              + b.getObject_ref()
              + ":"
              + b.getBr_orgineel_xml()
              + "("
              + b.getBr_xml()
              + ")");
      lp.setOpmerking(
          lp.getOpmerking() + ": Fout, duplicaat bericht. Berichtinhoud: \n\n" + b.getBr_xml());
      b = null;
      lp.setStatus(LaadProces.STATUS.STAGING_DUPLICAAT);
      lp.setAutomatischProces(em.find(AutomatischProces.class, this.config.getId()));
      em.merge(this.config);
      em.persist(lp);
      em.flush();
      em.getTransaction().commit();
      em.clear();
    }
    return b;
  }

  /**
   * Post de xml naar de geconfigureerde url.
   *
   * @param proces proces waarvoor doorgetuurd wordt
   * @param l update listener
   * @param b door te sturen bericht
   * @param endpoint url waarnaartoe wordt gepost
   * @return {@code true} als succesvol doorgestuurd
   */
  public static boolean doorsturenBericht(
      AutomatischProces proces, ProgressUpdateListener l, Bericht b, String endpoint) {
    String msg;
    SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
    try {
      if (endpoint == null || endpoint.length() < 1) {
        return false;
      }
      URL url = new URL(endpoint);
      HttpURLConnection conn = (HttpURLConnection) url.openConnection();
      conn.setDoOutput(true);
      conn.setRequestMethod("POST");
      conn.setRequestProperty("Content-Length", "" + b.getBr_orgineel_xml().length());
      conn.setRequestProperty("Content-Type", "application/octet-stream");
      IOUtils.copy(
          IOUtils.toInputStream(b.getBr_orgineel_xml(), StandardCharsets.UTF_8),
          conn.getOutputStream());
      conn.disconnect();
      if ((conn.getResponseCode() == HttpURLConnection.HTTP_OK)
          || (conn.getResponseCode() == HttpURLConnection.HTTP_CREATED)
          || (conn.getResponseCode() == HttpURLConnection.HTTP_ACCEPTED)) {
        msg =
            "Bericht "
                + b.getObject_ref()
                + " op "
                + sdf.format(new Date())
                + " doorgestuurd naar "
                + endpoint;
        b.setStatus(Bericht.STATUS.STAGING_FORWARDED);
      } else {
        msg =
            String.format(
                "HTTP foutcode bij doorsturen bericht %s op %s naar endpoint %s: %s",
                b.getObject_ref(),
                sdf.format(new Date()),
                endpoint,
                conn.getResponseCode() + ": " + conn.getResponseMessage());
        b.setStatus(Bericht.STATUS.STAGING_NOK);
      }
      b.setOpmerking(msg);
      l.addLog(msg);

      return b.getStatus() == Bericht.STATUS.STAGING_FORWARDED;
    } catch (Exception e) {
      msg =
          String.format(
              "Fout bij doorsturen bericht op %s naar endpoint %s: %s",
              sdf.format(new Date()), endpoint, e.getClass() + ": " + e.getMessage());
      b.setOpmerking(msg);
      b.setStatus(Bericht.STATUS.STAGING_NOK);
      l.addLog(msg);
      log.error(msg, e);
      return false;
    }
  }

  /**
   * verwerk de afgiftes in de response en stuur eventueel door.
   *
   * @param afgiftes lijst afgiftes
   * @throws Exception if any
   */
  private void verwerkAfgiftes(List<AfgifteType> afgiftes, BaseURLType baseUrl, final String soort)
      throws Exception {
    int filterAlVerwerkt = 0;
    int aantalGeladen = 0;
    int aantalDoorgestuurd = 0;
    int progress = 0;
    List<Long> geladenBerichtIds = new ArrayList<>();
    String doorsturenUrl =
        ClobElement.nullSafeGet(this.config.getConfig().get("delivery_endpoint"));

    for (AfgifteType a : afgiftes) {
      String url = getAfgifteURL(a, baseUrl);

      if (null != url && !url.isBlank()) {
        if (isAfgifteAlGeladen(a)) {
          filterAlVerwerkt++;
        } else {
          Bericht b = laadAfgifte(a, url, soort);
          if (b != null && doorsturenUrl != null) {
            geladenBerichtIds.add(b.getId());
          }
          aantalGeladen++;
        }
      }
      l.progress(++progress);
    }

    Bericht b;
    if (doorsturenUrl != null && !geladenBerichtIds.isEmpty()) {
      l.addLog("Doorsturen van de opgehaalde berichten, in totaal " + geladenBerichtIds.size());
      l.total(geladenBerichtIds.size());
      progress = 0;

      for (Long bId : geladenBerichtIds) {
        b = Stripersist.getEntityManager().find(Bericht.class, bId);
        l.updateStatus("Doorsturen bericht: " + b.getObject_ref());
        doorsturenBericht(this.config, l, b, doorsturenUrl);
        Stripersist.getEntityManager().merge(b);
        Stripersist.getEntityManager().merge(this.config);
        Stripersist.getEntityManager().flush();
        Stripersist.getEntityManager().getTransaction().commit();
        Stripersist.getEntityManager().clear();
        aantalDoorgestuurd++;
        l.progress(++progress);
      }
    }

    l.addLog("\n\n**** resultaat ****\n");
    l.addLog("Aantal afgiftes die al waren verwerkt: " + filterAlVerwerkt);
    l.addLog("\nAantal afgiftes geladen: " + aantalGeladen);
    l.addLog("\nAantal afgiftes doorgestuurd: " + aantalDoorgestuurd + "\n");

    l.addLog(
        String.format(
            "Het GDS2 ophalen proces met ID %d is afgerond op %tc",
            config.getId(), Calendar.getInstance()));
    this.config.updateSamenvattingEnLogfile(
        String.format(
            "Het GDS2 ophalen proces, gestart op %tc, is afgerond op %tc. "
                + LOG_NEWLINE
                + "Aantal afgiftes die al waren verwerkt: %d"
                + LOG_NEWLINE
                + "Aantal afgiftes geladen: %d"
                + LOG_NEWLINE
                + "Aantal afgiftes doorgestuurd: %d"
                + LOG_NEWLINE
                + "Hoogste klantafgiftenummer: %s",
            this.config.getLastrun(),
            Calendar.getInstance(),
            filterAlVerwerkt,
            aantalGeladen,
            aantalDoorgestuurd,
            ClobElement.nullSafeGet(this.config.getConfig().get("hoogste_afgiftenummer"))));

    this.config.setStatus(AutomatischProces.ProcessingStatus.WAITING);
    this.config.setLastrun(new Date());
    Stripersist.getEntityManager().merge(this.config);
    // om geheugen problemen te vookomen bij runs met veel downloads en berichten
    // een flush/commit/clear forceren
    Stripersist.getEntityManager().flush();
    Stripersist.getEntityManager().getTransaction().commit();
    Stripersist.getEntityManager().clear();
  }

  private Gds2AfgifteServiceV20170401 initGDS2() throws Exception {
    Gds2AfgifteServiceV20170401 gds2 =
        new Gds2AfgifteServiceV20170401Service().getAGds2AfgifteServiceV20170401();
    BindingProvider bp = (BindingProvider) gds2;
    Map<String, Object> ctxt = bp.getRequestContext();

    // soap berichten logger inhaken (actief met TRACE level)
    List<Handler> handlerChain = bp.getBinding().getHandlerChain();
    handlerChain.add(new LogMessageHandler());
    bp.getBinding().setHandlerChain(handlerChain);

    String endpoint = (String) ctxt.get(BindingProvider.ENDPOINT_ADDRESS_PROPERTY);
    l.addLog("Kadaster endpoint: " + endpoint);
    l.updateStatus("Laden keys...");

    l.addLog("Initializing KeyManagerFactory");
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    KeyStore ks = KeyStore.getInstance("jks");
    final char[] thePassword = "changeit".toCharArray();
    PrivateKey privateKey =
        getPrivateKeyFromPEM(this.config.getConfig().get("gds2_privkey").getValue());
    Certificate certificate =
        getCertificateFromPEM(this.config.getConfig().get("gds2_pubkey").getValue());
    ks.load(null);
    ks.setKeyEntry("thekey", privateKey, thePassword, new Certificate[] {certificate});
    kmf.init(ks, thePassword);

    l.updateStatus("Opzetten SSL context...");
    context = createSslContext(kmf);

    Client client = ClientProxy.getClient(gds2);
    HTTPConduit http = (HTTPConduit) client.getConduit();
    TLSClientParameters tlsClientParameters = new TLSClientParameters();
    tlsClientParameters.setSSLSocketFactory(context.getSocketFactory());
    http.setTlsClientParameters(tlsClientParameters);

    return gds2;
  }

  private SSLContext createSslContext(KeyManagerFactory kmf)
      throws KeyStoreException,
          IOException,
          CertificateException,
          NoSuchProviderException,
          NoSuchAlgorithmException,
          KeyManagementException {
    final SSLContext sslContext = SSLContext.getInstance("TLS", "SunJSSE");

    l.addLog("Loading PKIX Overheid keystore");
    KeyStore ks = KeyStore.getInstance("jks");
    ks.load(
        nl.b3p.gds2.GDS2Util.class.getResourceAsStream("/pkioverheid.jks"),
        "changeit".toCharArray());

    l.addLog("Initializing default TrustManagerFactory");
    final TrustManagerFactory javaDefaultTrustManager =
        TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    javaDefaultTrustManager.init((KeyStore) null);
    X509TrustManager defaultX509TrustManager = null;
    for (TrustManager t : javaDefaultTrustManager.getTrustManagers()) {
      if (t instanceof X509TrustManager) {
        defaultX509TrustManager = (X509TrustManager) t;
        break;
      }
    }
    l.addLog("Initializing PKIX TrustManagerFactory");
    final TrustManagerFactory customCaTrustManager = TrustManagerFactory.getInstance("PKIX");
    customCaTrustManager.init(ks);

    l.addLog("Initializing SSLContext");
    sslContext.init(
        kmf.getKeyManagers(),
        new TrustManager[] {
          new TrustManagerDelegate(
              // customCaTrustManager is PKIX dus altijd X.509 (RFC3280)
              (X509TrustManager) customCaTrustManager.getTrustManagers()[0],
              defaultX509TrustManager)
        },
        null);
    return sslContext;
  }
}