PdndPublicazioneTracciamento.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2025 Link.it srl (https://link.it).
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3, as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package org.openspcoop2.monitor.engine.statistic;

import java.io.ByteArrayOutputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Stream;

import javax.mail.BodyPart;
import javax.mail.internet.InternetHeaders;
import javax.servlet.http.HttpServletResponse;

import org.openspcoop2.core.statistiche.StatistichePdndTracing;
import org.openspcoop2.core.statistiche.constants.PdndMethods;
import org.openspcoop2.core.statistiche.constants.PossibiliStatiPdnd;
import org.openspcoop2.core.statistiche.constants.PossibiliStatiRichieste;
import org.openspcoop2.core.statistiche.constants.TipoIntervalloStatistico;
import org.openspcoop2.core.statistiche.dao.IStatistichePdndTracingService;
import org.openspcoop2.generic_project.exception.ExpressionException;
import org.openspcoop2.generic_project.exception.ExpressionNotImplementedException;
import org.openspcoop2.generic_project.exception.MultipleResultException;
import org.openspcoop2.generic_project.exception.NotFoundException;
import org.openspcoop2.generic_project.exception.NotImplementedException;
import org.openspcoop2.generic_project.exception.ServiceException;
import org.openspcoop2.generic_project.expression.IExpression;
import org.openspcoop2.generic_project.expression.IPaginatedExpression;
import org.openspcoop2.generic_project.expression.SortOrder;
import org.openspcoop2.utils.Utilities;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.date.DateManager;
import org.openspcoop2.utils.json.JSONUtils;
import org.openspcoop2.utils.mime.MimeMultipart;
import org.openspcoop2.utils.transport.http.ContentTypeUtilities;
import org.openspcoop2.utils.transport.http.HttpConstants;
import org.openspcoop2.utils.transport.http.HttpRequest;
import org.openspcoop2.utils.transport.http.HttpRequestMethod;
import org.openspcoop2.utils.transport.http.HttpResponse;
import org.openspcoop2.utils.transport.http.HttpUtilities;
import org.slf4j.Logger;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * PdndPublicazioneTracciamento
 *
 * @author Tommaso Burlon (tommaso.burlon@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public class PdndPublicazioneTracciamento implements IStatisticsEngine {
	
	private static final String PDND_DATE_FORMAT = "yyyy-MM-dd";
	private static final String TRACING_ID_FIELD = "tracingId";
	
	
	
	// Errore nella connessione con la pdnd, in tal caso necessario retry
	private static final String CONNECTION_ERROR = "CONNECTION_ERROR";
	
	// Errore fornito dalla pdnd dopo il parsing del documento
	private static final String PDND_PARSING_ERROR = "PDND_PARSING_ERROR";
	
	// Errore fornito dalla pdnd all'inivio del documento
	private static final String PDND_PUBLISHING_ERROR = "PDND_PUBLISHING_ERROR";

	
	private IStatistichePdndTracingService pdndStatisticheSM;
	private org.openspcoop2.core.statistiche.dao.IServiceManager statisticheSM;
	private Logger logger;
	private StatisticsConfig config;
	private PdndTracciamentoInfo internalPddCodes;
	private Map<Date, StatistichePdndTracing> updateTracingIdStats;
	
	PdndPublicazioneTracciamento(){
			super();
	}
	
	@Override
	public void init(StatisticsConfig config,
			org.openspcoop2.core.statistiche.dao.IServiceManager statisticheSM,
			org.openspcoop2.core.transazioni.dao.IServiceManager transazioniSM,
			org.openspcoop2.monitor.engine.config.statistiche.dao.IServiceManager pluginsStatisticheSM,
			org.openspcoop2.core.plugins.dao.IServiceManager pluginsBaseSM,
			org.openspcoop2.core.commons.search.dao.IServiceManager utilsSM,
			org.openspcoop2.monitor.engine.config.transazioni.dao.IServiceManager pluginsTransazioniSM) {
		this.logger = config.getLogCore();
		this.config = config;
		
		this.updateTracingIdStats = new HashMap<>();
		
		try {
			this.statisticheSM = statisticheSM;
			this.pdndStatisticheSM = statisticheSM.getStatistichePdndTracingService();
			this.internalPddCodes = PdndTracciamentoUtils.getEnabledPddCodes(utilsSM, this.config);
			PdndTracciamentoUtils.logDebugSoggettiAbilitati(this.internalPddCodes, this.logger);
		} catch (Throwable e) { // lasciare Throwable
			this.logger.error("Impossibile inizializzare la classe PdndGenerazioneTracciamento", e);
		}
	}
	
	private HttpRequest getBaseRequest(String pddCode) {
		PdndTracciamentoSoggetto s = this.internalPddCodes.getInfoByIdentificativoPorta(pddCode, true, false);
		return this.config.getPdndTracciamentoRequestConfig().getBaseRequest(s.getIdSoggetto().getNome());
	}
	
	
	private class Result<R, T extends Throwable> {
		private final R res;
		private final T error;
		
		public Result(R result) {
			this.res = result;
			this.error = null;
		}
		
		public Result(T error) {
			this.res = null;
			this.error = error;
		}
		
		public R get() throws T {
			if (this.res == null)
				throw this.error;
			return this.res;
		}
	}
	
	
	private HttpResponse httpInvokeWithException(HttpRequest req) throws UtilsException {
		HttpResponse res = HttpUtilities.httpInvoke(req);
		
		if(res==null) {
			throw new UtilsException("HttpResponse is null");
		}
		if(res.getResultHTTPOperation()<200 || res.getResultHTTPOperation()>204 ) {
			throw new UtilsException("HttpResponse return code '"+res.getResultHTTPOperation()+"'");
		}
		if(res.getContent()==null || res.getContent().length<=0) {
			throw new UtilsException("HttpResponse is empty");
		}
		return res;
	}
	
	private Iterator<Result<JsonNode, UtilsException>> iteratorHttpList(HttpRequest req) {
		final int limit = 50;
		
		AtomicInteger offset = new AtomicInteger(-limit);
		Queue<JsonNode> list = new LinkedList<>();
		JSONUtils jsonUtils = JSONUtils.getInstance();
		
		return Stream.generate((Supplier<Result<JsonNode, UtilsException>>) () -> {
			if (list.isEmpty()) {
				offset.addAndGet(limit);
				req.addParam("limit", Integer.toString(limit));
				req.addParam("offset", Integer.toString(offset.get()));
				
				try {
					HttpResponse res = httpInvokeWithException(req);
					
					JsonNode node = jsonUtils.getAsNode(res.getContent());
					JsonNode result = node.get("results");
					
					for ( int i = 0; i < result.size(); i++)
						list.add(result.get(i));
					
				} catch (Exception e) {
					this.logger.error("Impossibile ottenere risultati dalla PDND: "+e.getMessage(), e);
					list.add(null);
					return new Result<>(new UtilsException(e));
				}
			}
			return list.isEmpty() || list.peek() == null ? null : new Result<>(list.remove());
		}).takeWhile(Objects::nonNull).sequential().iterator();
		
	}
	
	private String getUploadPath(StatistichePdndTracing stat) {
		switch (stat.getMethod()) {
		case REPLACE:
			return String.format("/tracings/%s/replace", stat.getTracingId());
		case RECOVER:
			return String.format("/tracings/%s/recover", stat.getTracingId());
		case SUBMIT:
			return "/tracings/submit";
		}
		return null;
	}
	
	private MimeMultipart getUploadBody(StatistichePdndTracing stat) throws UtilsException {
		MimeMultipart multipart = new MimeMultipart(HttpConstants.CONTENT_TYPE_MULTIPART_FORM_DATA_SUBTYPE);
		
		InternetHeaders headers = new InternetHeaders();
		headers.addHeader(HttpConstants.CONTENT_TYPE, HttpConstants.CONTENT_TYPE_CSV);
		headers.addHeader(HttpConstants.CONTENT_DISPOSITION, HttpConstants.CONTENT_DISPOSITION_FORM_DATA_NAME_PREFIX+"\"file\"; "+HttpConstants.CONTENT_DISPOSITION_FILENAME_PREFIX+"\"record.csv\"");		
		BodyPart bp = multipart.createBodyPart(headers, stat.getCsv());
	
		multipart.addBodyPart(bp);
		
		if (stat.getMethod().equals(PdndMethods.SUBMIT)) {
			final SimpleDateFormat pdndDateFormat = new SimpleDateFormat(PDND_DATE_FORMAT);
			headers = new InternetHeaders();
			headers.addHeader(HttpConstants.CONTENT_TYPE, HttpConstants.CONTENT_TYPE_PLAIN);
			headers.addHeader(HttpConstants.CONTENT_DISPOSITION, HttpConstants.CONTENT_DISPOSITION_FORM_DATA_NAME_PREFIX+"\"date\"");		
			bp = multipart.createBodyPart(headers, pdndDateFormat.format(stat.getDataTracciamento()).getBytes());
			multipart.addBodyPart(bp);
		}
		
		return multipart;
	}
	
	private void writePdndError(StatistichePdndTracing stat, JsonNode response) throws StatisticsEngineException {
		ArrayNode errors = (ArrayNode) response.get(PDND_RESPONSE_ERRORS_CLAIM);
		
		try {
			stat.setErrorDetails(getErrorDetails(PDND_PUBLISHING_ERROR, errors));
		} catch (StatisticsEngineException e) {
			stat.setStatoPdnd(PossibiliStatiPdnd.ERROR);
			stat.setErrorDetails(getErrorDetails(PDND_PUBLISHING_ERROR, "Il messaggio di errore ricevuto dalla PDND non è conforme all'openAPI fornito"));
			return;
		}
		
		stat.setStatoPdnd(PossibiliStatiPdnd.ERROR);
		
		if(errors!=null) {
			for (JsonNode node : errors) {
				String code = node.get("code").asText();
				
				if (code.equals("TRACING_ALREADY_EXISTS")) {
					stat.setStatoPdnd(PossibiliStatiPdnd.PENDING);
					this.updateTracingIdStats.put(stat.getDataTracciamento(), stat);
				}
			}
		}
	}
	
	
	private String getErrorDetails(String type, JsonNode node) throws StatisticsEngineException {
		JSONUtils json = JSONUtils.getInstance();
		
		try {
			ObjectNode errorNode = json.newObjectNode();
			
			errorNode.set("pdnd_details", node);
			errorNode.put("type", type);
			return json.toString(errorNode);
		} catch (UtilsException e) {
			throw new StatisticsEngineException("Errore nella generazione del nodeo di errore", e);
		}
	}
	
	
	private String getErrorDetails(String type, String errMsg) throws StatisticsEngineException {
		JSONUtils json = JSONUtils.getInstance();
		
		try {
			ObjectNode errorNode = json.newObjectNode();
			
			errorNode.put("details", errMsg);
			errorNode.put("type", type);
			return json.toString(errorNode);
		} catch (UtilsException e) {
			throw new StatisticsEngineException("Errore nella generazione del nodeo di errore", e);
		}
	}
	
	private static final String PDND_RESPONSE_ERRORS_CLAIM = "errors";
	
	private void checkSendTraceResult(StatistichePdndTracing stat, HttpResponse res, String errMsg) throws StatisticsEngineException {
		// errore nella comunicazione con la pdnd
		if (res == null) {
			stat.setErrorDetails(getErrorDetails(CONNECTION_ERROR, errMsg));
			stat.setStato(PossibiliStatiRichieste.FAILED);
			return;
		}
		
		Integer code = res.getResultHTTPOperation();
		byte[] content = res.getContent();
		JsonNode node = null;
		
		try {
			JSONUtils jsonUtils = JSONUtils.getInstance();
			node = jsonUtils.getAsNode(content);
		} catch (UtilsException e) {
			// ignore 
		}

		String baseType = null;
		try {
			baseType = ContentTypeUtilities.readBaseTypeFromContentType(res.getContentType());
		}catch(Exception e) {
			// la PDND non ha ritornato un json
			stat.setErrorDetails(getErrorDetails(PDND_PUBLISHING_ERROR,
					"Content-Type della risposta invalido, content-type: " + res.getContentType()
							+ ", content: " + new String(content)));
			stat.setStato(PossibiliStatiRichieste.FAILED);
			return;
		}
		if (!HttpConstants.CONTENT_TYPE_JSON.equals(baseType)
				&& !HttpConstants.CONTENT_TYPE_JSON_PROBLEM_DETAILS_RFC_7807.equals(baseType)) {
			// la PDND non ha ritornato un json
			stat.setErrorDetails(getErrorDetails(PDND_PUBLISHING_ERROR,
					"Content-Type della risposta non di tipo json, content-type: " + res.getContentType()
							+ ", content: " + new String(content)));
			stat.setStato(PossibiliStatiRichieste.FAILED);
		} else if (node == null || node.get(PDND_RESPONSE_ERRORS_CLAIM)==null) {
			// il json fornito dalla pdnd non è stato parsato correttamente
			stat.setErrorDetails(getErrorDetails(PDND_PUBLISHING_ERROR,
					"Non è stato possibile interpretare il contenuto: " + new String(content)));
			stat.setStato(PossibiliStatiRichieste.FAILED);
		} else if (code != HttpServletResponse.SC_OK) {
			// la pdnd ha ritornato un codice di errore
			writePdndError(stat, node);
			stat.setStato(PossibiliStatiRichieste.PUBLISHED);
			stat.setStatoPdnd(PossibiliStatiPdnd.ERROR);
		} else if (node.get(TRACING_ID_FIELD).isNull()) {
			// la pdnd ha ritornato un json senza il campo tracing_id
			stat.setErrorDetails(getErrorDetails(PDND_PUBLISHING_ERROR,
					"Dal contenuto non è stato possibile ottenere il campo tracingId: " + new String(content)));
			stat.setStato(PossibiliStatiRichieste.FAILED);
		} else {
			// tutto è andato correttamente salvo il tracing id
			String tracingId = node.get(TRACING_ID_FIELD).asText();

			stat.setTracingId(tracingId);
			stat.setStato(PossibiliStatiRichieste.PUBLISHED);
			stat.setStatoPdnd(PossibiliStatiPdnd.PENDING);
		}
	}
	
	/**
	 * Invio un tracciamento alla PDND
	 * @param pddCode: codice pdd del soggetto multitenante a cui si riferiscono i tracciati
	 * @param stat: tracciamento da inviare
	 * @throws StatisticsEngineException 
	 */
	private void sendTrace(String pddCode, StatistichePdndTracing stat) throws StatisticsEngineException {
		
		// controllo che non sia stato superato il massimo numero di tentativi a meno che la pubblicazione non sia forzata
		if (this.config.getPdndTracciamentoMaxAttempt() != null 
				&& this.config.getPdndTracciamentoMaxAttempt() <= stat.getTentativiPubblicazione()
				&& !stat.isForcePublish())
			return;
		
		// ripulisco eventuali errori precedenti
		stat.setErrorDetails(null);
		stat.setStato(null);
		
		// nel caso faccia una submit in ritardo dovro aggiornare il tracingId e fare una recover (in quanto sara diventato un MISSING)
		if ((stat.getMethod().equals(PdndMethods.SUBMIT) && Duration.between(stat.getDataTracciamento().toInstant(), DateManager.getDate().toInstant()).compareTo(Duration.ofDays(2)) >= 0) 
				|| (!stat.getMethod().equals(PdndMethods.SUBMIT) && stat.getTracingId() == null)) {
			if (stat.getMethod().equals(PdndMethods.SUBMIT))
				stat.setMethod(PdndMethods.RECOVER);
			this.updateTracingIdStats.put(stat.getDataTracciamento(), stat);
			return;
		}
		
		stat.setForcePublish(false);
		
		HttpResponse res =  null;
		String errMsg = null;
		
		try (ByteArrayOutputStream os = new ByteArrayOutputStream()){
		
			// aggiorno i tentativi
			stat.setTentativiPubblicazione(stat.getTentativiPubblicazione() + 1);
			stat.setDataPubblicazione(DateManager.getDate());
			
			// provo ad inviare il tracciato alla PDND
			HttpRequest req = getBaseRequest(pddCode);
			req.setMethod(HttpRequestMethod.POST);
			req.setUrl(req.getBaseUrl() + getUploadPath(stat));
		
			MimeMultipart multipart = getUploadBody(stat);
			multipart.writeTo(os);
			req.setContentType(multipart.getContentType());
			req.setContent(os.toByteArray());
			res = HttpUtilities.httpInvoke(req);	
			
		} catch (Exception e) {
			errMsg = e.getMessage();
		}
		
		checkSendTraceResult(stat, res, errMsg);
		
	}
	
	
	private void logPublishResults(StatistichePdndTracing stat, String nomeSoggetto) {
		String dataTracciamentoFormat = dataTracciamentoFormat(stat.getDataTracciamento());

		if (PossibiliStatiRichieste.FAILED.equals(stat.getStato())
				|| PossibiliStatiPdnd.ERROR.equals(stat.getStatoPdnd())) {
			this.logger.error("Fallita la pubblicazione del tracciato [{}], soggetto: {}, dettagli: {}",
					dataTracciamentoFormat,
					nomeSoggetto,
					stat.getErrorDetails());
		} else if (PossibiliStatiRichieste.PUBLISHED.equals(stat.getStato())
				&& PossibiliStatiPdnd.PENDING.equals(stat.getStatoPdnd())) {
			this.logger.info("Pubblicazione tracciato [{}], soggetto: {}, inviata correttamente alla PDND",
					dataTracciamentoFormat,
					nomeSoggetto);
		} else {
			this.logger.info("tracciato [{}], soggetto: {}, non inviato in quanto il tracingId non risulta presente nel db riproverò nella prossima fase",
					dataTracciamentoFormat,
					nomeSoggetto);
		}
	}
	
	/**
	 * Pubblico i record dal db che risultano nello stato WAITING e con csv valorizzato
	 * @param pddCode: codice pdd del soggetto multitenante a cui si riferiscono i tracciati
	 * @throws ServiceException
	 * @throws NotFoundException
	 * @throws NotImplementedException
	 * @throws ExpressionNotImplementedException
	 * @throws ExpressionException
	 * @throws StatisticsEngineException 
	 */
	private void publishRecords(String nomeSoggetto, String pddCode) throws ServiceException, NotFoundException, NotImplementedException, ExpressionNotImplementedException, ExpressionException, StatisticsEngineException {		
		// ottengo la lista dei record in stato WAITING con csv valorizzzato
		IPaginatedExpression expr = this.pdndStatisticheSM.newPaginatedExpression();
		expr.isNotNull(StatistichePdndTracing.model().CSV);
		expr.equals(StatistichePdndTracing.model().STATO_PDND, PossibiliStatiPdnd.WAITING);
		expr.equals(StatistichePdndTracing.model().PDD_CODICE, pddCode);
		expr.equals(StatistichePdndTracing.model().HISTORY, 0);
		
		// se i tentativi di pubblicazione sono inferiori al massimo o la flag di force pubblicazione risulta abilitata
		if (this.config.getPdndTracciamentoMaxAttempt() != null) {
			IExpression attemptsExpr = this.pdndStatisticheSM.newExpression();
			attemptsExpr.or().lessThan(StatistichePdndTracing.model().TENTATIVI_PUBBLICAZIONE, this.config.getPdndTracciamentoMaxAttempt());
			attemptsExpr.or().equals(StatistichePdndTracing.model().FORCE_PUBLISH, true);
			expr.and(attemptsExpr);
		}
		
		expr.addOrder(StatistichePdndTracing.model().DATA_TRACCIAMENTO, SortOrder.ASC);
		
		List<StatistichePdndTracing> stats = null; 
		
		try {
			stats = this.pdndStatisticheSM.findAll(expr);
		} catch (Exception e) {
			if (e.getCause() instanceof NotFoundException || e instanceof NotFoundException) {
				this.logger.debug("Nessun record da pubblicare per il soggetto: {} trovato", nomeSoggetto);
				return;
			}
			throw new StatisticsEngineException("Errore inaspettato nella ricerca delle transazioni", e);
		}
		
		// per ogni record invio la traccia alla pdnd e poi aggiorno il db
		this.logger.debug("Trovati {} record da pubblicare per il soggetto: {}", stats.size(), nomeSoggetto);
		for (StatistichePdndTracing stat : stats) {
			sendTrace(pddCode, stat);
			this.pdndStatisticheSM.update(stat);
					
			this.logPublishResults(stat, nomeSoggetto);
		}
		
	}
	
	/**
	 * Aggiorna il tracing id a tutti i tracciati non submittati che non hanno un tracingId aggiornato
	 * @param pddCode: codice pdd del soggetto multitenante a cui si riferiscono i tracciati
	 * @throws ServiceException
	 * @throws NotFoundException
	 * @throws NotImplementedException
	 * @throws StatisticsEngineException
	 */
	private void fixPublishRecords(String nomeSoggetto, String pddCode) throws ServiceException, NotFoundException, NotImplementedException, StatisticsEngineException {
		// itero su la lista di tutti gli stati fin quando ho record che vanno aggiornati (in teoria non ne dovrei quasi mai avere)
		HttpRequest req = getBaseRequest(pddCode);
		req.setMethod(HttpRequestMethod.GET);
		req.setUrl(req.getBaseUrl() + "/tracings");
		
		Iterator<Result<JsonNode, UtilsException>> itr = iteratorHttpList(req);
		
		if (this.updateTracingIdStats.isEmpty()) {
			this.logger.info("Non ci sono tracciati senza un tracingId valido per il soggetto {}", nomeSoggetto);
		} else {
			this.logger.info("Individuati {} tracciati senza un tracingId valido per il soggetto: {}, procedo ad interrogare la PDND per ricavarlo",
					this.updateTracingIdStats.size(), 
					nomeSoggetto);
		}
		
		while (!this.updateTracingIdStats.isEmpty() && itr.hasNext()) {
			try {
				JsonNode node = itr.next().get();
				String tracingId = node.get(TRACING_ID_FIELD).asText();
				String tracingDate = node.get("date").asText();
				
				final SimpleDateFormat pdndDateFormat = new SimpleDateFormat(PDND_DATE_FORMAT);
				Date date = pdndDateFormat.parse(tracingDate);
				
				StatistichePdndTracing stat = this.updateTracingIdStats.remove(date);
				if (stat != null) {
					String formatTracingDate = dataTracciamentoFormat(stat.getDataTracciamento());
					stat.setTracingId(tracingId);
					if (!PossibiliStatiRichieste.FAILED.equals(stat.getStato())
							&& !PossibiliStatiPdnd.ERROR.equals(stat.getStatoPdnd())) {
						this.logger.info("Individuato tracciato [{}] per il soggetto: {}, senza tracingId valido, tracingId: {} aggiornato, applico l'operazione {}",
								formatTracingDate,
								nomeSoggetto,
								tracingId,
								stat.getMethod());
						
						sendTrace(pddCode, stat);
					}
					
					this.pdndStatisticheSM.update(stat);
					
					this.logPublishResults(stat, nomeSoggetto);
				}
			} catch (ParseException | UtilsException e) {
				throw new StatisticsEngineException(e, "Errore nella lettura dei tracciati ricevuti dalla PDND");
			}
		}
		
		
	}
	
	
	/**
	 * Aggiorna lo stato di un singolo tracciato che non si trova nello stato PENDING
	 * @param pddCode: codice pdd del soggetto multitenante a cui si riferiscono i tracciati
	 * @param stat: Tracciato da aggiornare
	 * @throws StatisticsEngineException: errore fatale il batch non e' stato configurato correttamente 
	 */
	private void updateTraceStatus(String pddCode, StatistichePdndTracing stat) throws StatisticsEngineException {
		
		// itero su tutti gli errori ritornati
		HttpRequest req = getBaseRequest(pddCode);
		req.setMethod(HttpRequestMethod.GET);
		req.setUrl(req.getBaseUrl() + "/tracings/" + stat.getTracingId() + "/errors");
		
		Iterator<Result<JsonNode, UtilsException>> itr = iteratorHttpList(req);
		
		// se non ci sono errori tutto funziona correttamente
		if (!itr.hasNext()) {
			stat.setStatoPdnd(PossibiliStatiPdnd.OK);
			return;
		}
		
		try {
			// se ci sono errori li 
			ArrayNode arr = JSONUtils.getInstance().newArrayNode();
			while(itr.hasNext()) {
				JsonNode node = itr.next().get();
				arr.add(node);
			}
			
			stat.setStatoPdnd(PossibiliStatiPdnd.ERROR);
			stat.setErrorDetails(getErrorDetails(PDND_PARSING_ERROR, arr));
		} catch (UtilsException e) {
			stat.setStato(PossibiliStatiRichieste.FAILED);
			stat.setStatoPdnd(PossibiliStatiPdnd.PENDING);
			stat.setErrorDetails(getErrorDetails(PDND_PARSING_ERROR, "Errore nel parsing della risposta dalla pdnd: " + e.getMessage()));
		}
	}
	
	/**
	 * Controlla lo stato delle richieste pending
	 * @param pddCode: codice pdd del soggetto multitenante a cui si riferiscono i tracciati
	 * @throws ServiceException
	 * @throws NotImplementedException
	 * @throws ExpressionNotImplementedException
	 * @throws ExpressionException
	 * @throws NotFoundException
	 * @throws StatisticsEngineException: eccezione fatale il batch non e' stato configurato correttamente
	 * @throws UtilsException
	 * @return true se non ci sono piu richieste pending nel db
	 */
	private boolean checkPending(String nomeSoggetto, String pddCode) throws ServiceException, NotImplementedException, ExpressionNotImplementedException, ExpressionException, NotFoundException, StatisticsEngineException {
		
		// ottengo la lista di tutti i tracciati nello stato PENDING secondo la PDND
		Map<String, Date> pendingIds = getTracingIdStatus(pddCode, "PENDING");
		
		IPaginatedExpression expr = this.pdndStatisticheSM.newPaginatedExpression();
		expr.equals(StatistichePdndTracing.model().STATO_PDND, PossibiliStatiPdnd.PENDING);
		expr.equals(StatistichePdndTracing.model().PDD_CODICE, pddCode);
		expr.equals(StatistichePdndTracing.model().HISTORY, 0);
		expr.addOrder(StatistichePdndTracing.model().DATA_TRACCIAMENTO, SortOrder.ASC);

		// ottengo tutte le tabelle che sono in stato pending
		List<StatistichePdndTracing> stats = null;
		try {
			stats = this.pdndStatisticheSM.findAll(expr);
		} catch (ServiceException e) {
			this.logger.info("Nessun tracciato nello stato PENDING");
			return true;
		}

		this.logger.info("Tracciati con stato PENDING trovati: {}, soggetto: {}", stats.size(), nomeSoggetto);
		
		boolean noMorePending = true;
		for (StatistichePdndTracing stat : stats) {
			String dataTracciamentoFormat = dataTracciamentoFormat(stat.getDataTracciamento());

			// aggiorno lo stato solo dei tracciati che non risultano PENDING alla PDND
			if (!pendingIds.containsKey(stat.getTracingId())) {
				updateTraceStatus(pddCode, stat);
				this.pdndStatisticheSM.update(stat);

				// log risultato pubblicazione
				if (PossibiliStatiPdnd.ERROR.equals(stat.getStatoPdnd())) {
					this.logger.error("La PDND ha rilevato un errore nel tracciato [{}], soggetto: {}, dettagli: {}",
							dataTracciamentoFormat,
							nomeSoggetto,
							stat.getErrorDetails());
				} else if (PossibiliStatiPdnd.OK.equals(stat.getStatoPdnd())) {
					this.logger.info("La PDND ha caricato con successo tracciato [{}], soggetto: {}, inviata correttamente all PDND",
							dataTracciamentoFormat,
							nomeSoggetto);
				}
			} else {
				this.logger.info("Tracciato [{}], soggetto: {}, ancora in stato PENDING, non aggiorno", 
						dataTracciamentoFormat, 
						nomeSoggetto);
				noMorePending = false;
			}
		}
		
		return noMorePending;
	}
	
	/**
	 * Aggiorna il db con tutti i tracciati che non risultano MISSING alla PDND
	 * @param pddCode: codice pdd del soggetto multitenante a cui si riferiscono i tracciati
	 * @throws StatisticsEngineException, errore fatale il batch non e' stato configurato correttamente
	 * @throws ServiceException
	 * @throws NotImplementedException
	 * @throws ExpressionException 
	 * @throws ExpressionNotImplementedException 
	 * @throws MultipleResultException 
	 */
	private void updateMissing(String nomeSoggetto, String pddCode) throws StatisticsEngineException, ServiceException, NotImplementedException, ExpressionNotImplementedException, ExpressionException, MultipleResultException {
		Map<String, Date> missingIds = getTracingIdStatus(pddCode, "MISSING");
		
		if (!missingIds.isEmpty()) {
			this.logger.info("Le seguenti date [{}], risultano mancanti lato PDND per il soggetto {}, procedo alla creazione di entry vuote",
					missingIds.values(),
					nomeSoggetto);
		} else {
			this.logger.info("Tutti i tracciati per il soggetto {} sono stati caricati, nessun MISSING", nomeSoggetto);
		}
		
		for (Map.Entry<String, Date> id : missingIds.entrySet()) {
			StatistichePdndTracing stat = new StatistichePdndTracing();
			stat.setTracingId(id.getKey());
			stat.setDataTracciamento(id.getValue());
			stat.setDataRegistrazione(DateManager.getDate());
			stat.setCsv(null);
			stat.setMethod(PdndMethods.RECOVER);
			stat.setHistory(0);
			stat.setPddCodice(pddCode);
			stat.setStatoPdnd(PossibiliStatiPdnd.WAITING);
			
			
			
			try {
				IExpression expr = this.pdndStatisticheSM.newExpression();
				expr.equals(StatistichePdndTracing.model().PDD_CODICE, pddCode);
				expr.equals(StatistichePdndTracing.model().DATA_TRACCIAMENTO, id.getValue());
				expr.equals(StatistichePdndTracing.model().HISTORY, 0);
				
				this.pdndStatisticheSM.find(expr);
			} catch (Exception e) {
				if (e.getCause() instanceof NotFoundException || e instanceof NotFoundException) {
					this.pdndStatisticheSM.create(stat);
				}
				else {
					// ignore
				}
			} 
		}
	}
	
	
	private Map<String, Date> getTracingIdStatus(String pddCode, String status) throws StatisticsEngineException {
		
		HttpRequest req = getBaseRequest(pddCode);
		req.setMethod(HttpRequestMethod.GET);
		req.addParam("states", status);
		req.setUrl(req.getBaseUrl() + "/tracings");
		
		Iterator<Result<JsonNode, UtilsException>> itr = iteratorHttpList(req);
		Map<String, Date> ids = new HashMap<>();
		
		while(itr.hasNext()) {
			try {
				JsonNode node = itr.next().get();
				String tracingId = node.get(TRACING_ID_FIELD).asText();
				String tracingDate = node.get("date").asText();
				
				final SimpleDateFormat pdndDateFormat = new SimpleDateFormat(PDND_DATE_FORMAT);
				Date date = pdndDateFormat.parse(tracingDate);
				ids.put(tracingId, date);
			} catch (ParseException | UtilsException e) {
				throw new StatisticsEngineException(e, "Errore nella lettura dei tracciati ricevuti dalla PDND");
			}
		}
		return ids;
	}
	
	
	/**
	 * Fa una semplice richiesta alla risorsa status delle PDND (dovrebbe semplicemente ritornare 200)
	 * @param pddCode: codice pdd del soggetto multi tenante che effettuera la richiesta
	 * @return true se la PDND ritorna un codice 200
	 */
	private boolean checkPdnd(String pddCode) {
		
		HttpRequest req = getBaseRequest(pddCode);
		req.setUrl(req.getBaseUrl() + "/status");
		req.setMethod(HttpRequestMethod.GET);
		
		HttpResponse res = null;
		try {
			res = HttpUtilities.httpInvoke(req);
		} catch (UtilsException e) {
			this.logger.error("Errore nella comunicazione con la risorsa /status della PDND tracciamento (url invocata: "+req.getUrl()+")", e);
			return false;
		}
		
		int code = res.getResultHTTPOperation();
		
		if (code != HttpServletResponse.SC_OK) {
			this.logger.error("Errore nella comunicazione con la risorsa /status della PDND tracciamento (url invocata: {}), return code: {}", req.getUrl(), code);
			return false;
		}
		
		return true;
	}
	
	
	public void generate(PdndTracciamentoSoggetto soggetto) throws StatisticsEngineException {
		
		String nomeSoggetto = soggetto.getIdSoggetto().getNome();
		String pddCode = soggetto.getIdSoggetto().getCodicePorta();
		
		List<String> soggettiAggregati =  PdndTracciamentoUtils.getNomiSoggettiAggregati(soggetto); 
		
		// controllo che la pdnd sia funzionante
		this.logger.info("------- FASE 0 [soggetto: {} aggregati: {}] verifica disponibilità pdnd -------", nomeSoggetto, soggettiAggregati);
		if (!checkPdnd(pddCode)) {
			this.logger.info("PDND non disponibile; termino gestione");
			return;
		}
		
		this.updateTracingIdStats.clear();
		
		try {
			
			// pubblico i record che sono nello stato WAITING e con un csv valorizzato
			this.logger.info("------- FASE 1 [soggetto: {} aggregati: {}] pubblicazione record -------", nomeSoggetto, soggettiAggregati);
			publishRecords(nomeSoggetto, pddCode);
			
			// alcuni record potrebbero essere stati rifiutati e devono avere il tracingId valorizzato
			this.logger.info("------- FASE 2 [soggetto: {} aggregati: {}] patch dei record senza tracingId -------", nomeSoggetto, soggettiAggregati);
			fixPublishRecords(nomeSoggetto, pddCode);
			
			// aggiorno i record che la pdnd mi sta informando mancanti
			this.logger.info("------- FASE 3 [soggetto: {} aggregati: {}] creazione dei record MISSING -------", nomeSoggetto, soggettiAggregati);
			updateMissing(nomeSoggetto, pddCode);
			
			// controllo lo stato delle varie richieste pending
			this.logger.info("------- FASE 4 [soggetto: {} aggregati: {}] controllo dei record PENDING -------", nomeSoggetto, soggettiAggregati);
			
			Integer delayIndex = 0;
			List<Integer> delays = this.config.getPdndTracciamentoPendingCheck();
			
			
			boolean checkPendingResult = false;
			do {
				Integer delayI = delays.get(delayIndex++);
				int delay = Objects.requireNonNullElse(delayI, 0); // non dovrebbe mai succedere il caso di null
				
				if (delayIndex == 1) {
					this.logger.info("Aspetto {}s prima di aggiornare i record in PENDING, numero massimo di tentativi da effettuare: {}", delay, delays.size());
				} else if (delayIndex <= delays.size()) {
					this.logger.info("La PDND non ha ancora valutato tutti i tracciati con lo stato PENDING, tentativo: {}, aspetto {}s e riprovo", delayIndex, delay);
				}
				
				if(delay>0) {
					Utilities.sleep(delay*1000l);
				}
				
				checkPendingResult = checkPending(nomeSoggetto, pddCode);
			} while(!checkPendingResult && delayIndex < delays.size());
			
			if (!checkPendingResult)
				this.logger.info("La PDND non ha ancora valutato tutti i tracciati con lo stato PENDING ma ho superato i tentativi massimi, riproverò alla prossima esecuzione");
			else
				this.logger.info("La PDND ha valutato tutti i tracciati con lo stato PENDING, nessun record in PENDING rimasto");
		
		} catch (ServiceException 
				| NotFoundException 
				| NotImplementedException
				| ExpressionNotImplementedException 
				| ExpressionException 
				| MultipleResultException e) {
			this.logger.error("Errore nella pubblicazione delle tracce, pdd code: {}", pddCode, e);
		}
	}
	
	@Override
	public void generate() throws StatisticsEngineException {
		this.logger.info("********************* INIZIO PUBBLICAZIONE TRACCIATO PDND *********************");
		
		Date currDate = DateManager.getDate();
		try {
			StatisticsInfoUtils.updateDataUltimaGenerazioneStatistiche(
					this.statisticheSM.getStatisticaInfoServiceSearch(), 
					this.statisticheSM.getStatisticaInfoService(), 
					TipoIntervalloStatistico.PDND_PUBBLICAZIONE_TRACCIAMENTO,
					this.config.getLogSql(), currDate);
		} catch (Exception e) {
			this.logger.error("Errore nell'aggiornamento della data ultima statistica {}", TipoIntervalloStatistico.PDND_PUBBLICAZIONE_TRACCIAMENTO, e);
		}
		
		for (PdndTracciamentoSoggetto soggetto : this.internalPddCodes.getSoggetti()) {
			this.logger.info("*--------- Gestione soggetto '{}' --------", soggetto.getIdSoggetto());
			this.generate(soggetto);
			this.logger.info("*--------- Fine gestione soggetto {}  --------", soggetto.getIdSoggetto());
		}
		
		this.logger.info("********************* FINE PUBBLICAZIONE TRACCIATO PDND *********************");
	}
	
	@Override
	public boolean isEnabled(StatisticsConfig config) {
		return config.isPdndTracciamentoPubblicazione();
	}
	
	private String dataTracciamentoFormat(Date date) {
		return new SimpleDateFormat(PDND_DATE_FORMAT).format(date);
	}
}