MessageUtilities.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2026 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.security.message.engine;

import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;
import jakarta.xml.soap.AttachmentPart;

import org.openspcoop2.core.constants.Costanti;
import org.openspcoop2.message.OpenSPCoop2Message;
import org.openspcoop2.message.OpenSPCoop2SoapMessage;
import org.openspcoop2.message.constants.MessageRole;
import org.openspcoop2.message.exception.MessageException;
import org.openspcoop2.message.exception.MessageNotSupportedException;
import org.openspcoop2.message.soap.SoapUtils;
import org.openspcoop2.message.soap.reference.AttachmentReference;
import org.openspcoop2.message.soap.reference.ElementReference;
import org.openspcoop2.message.soap.reference.Reference;
import org.openspcoop2.protocol.sdk.Context;
import org.openspcoop2.security.SecurityException;
import org.openspcoop2.security.message.MessageSecurityContext;
import org.openspcoop2.security.message.SubErrorCodeSecurity;
import org.openspcoop2.security.message.constants.SecurityConstants;
import org.openspcoop2.utils.DynamicStringReplace;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.json.JsonPathExpressionEngine;
import org.slf4j.Logger;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import net.minidev.json.JSONObject;

/**
 * MessageUtilities
 *
 * @author Andrea Poli (apoli@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public class MessageUtilities {

	private MessageUtilities() {}


	public static void checkEncryptionPartElements(Map<QName, QName> notResolved, OpenSPCoop2SoapMessage message, List<SubErrorCodeSecurity> listaErroriInElementi) throws SecurityException {

		try{

			if(notResolved != null && !notResolved.isEmpty()) {
				for (QName expected : notResolved.keySet()) {
					QName actualQName = notResolved.get(expected);
					boolean found = false;
					NodeList it = message.getSOAPPart().getElementsByTagNameNS(actualQName.getNamespaceURI(), actualQName.getLocalPart());
					for (int j = 0; j < it.getLength() && !found; j++) {
						Node elementFather = (Node) it.item(j);
						/**SOAPElement elementFather = (SOAPElement) it.item(j);
						List<SOAPElement> encryptedElements = SoapUtils.getNotEmptyChildSOAPElement(elementFather);*/
						List<Node> encryptedElements = SoapUtils.getNotEmptyChildNodes(message.getFactory(), elementFather, false);
						for (int i = 0; i < encryptedElements.size() && !found; i++) {
							/**SOAPElement actual = encryptedElements.get(i);*/
							Node actual = encryptedElements.get(i);
							String actualNamespaceURI = actual.getNamespaceURI();
							String actualLocalName = actual.getLocalName();
							String expectedNamespaceURI = expected.getNamespaceURI();
							String expectedLocalName = expected.getLocalPart();
							if(((actualNamespaceURI == null && expectedNamespaceURI == null) || (actualNamespaceURI!=null && actualNamespaceURI.equals(expectedNamespaceURI)))
									&& actualLocalName.equals(expectedLocalName)) {
								//trovato l'elemento che ci interessa
								found = true;
							}
						}
					}
					if(!found) {
						SubErrorCodeSecurity subCodice = new SubErrorCodeSecurity();
						subCodice.setEncrypt(true);
						subCodice.setMsgErrore("Expected encryption part("+ expected +") not found");
						subCodice.setTipo(SecurityConstants.ENCRYPTION_PART_CONTENT);
						subCodice.setNamespace(expected.getNamespaceURI());
						subCodice.setName(expected.getLocalPart());
						listaErroriInElementi.add(subCodice);
					}
				}
			}

		}catch(Exception e){
			throw new SecurityException(e.getMessage(),e);
		}
	}

	public static Map<QName, QName> checkEncryptSignatureParts(MessageSecurityContext messageSecurityContext,List<Reference> elementsToClean, OpenSPCoop2SoapMessage message,
			List<SubErrorCodeSecurity> codiciErrore, QName qnameSecurity) throws SecurityException {

		Map<String, Object> properties = messageSecurityContext.getIncomingProperties();
		boolean addAttachmentIdBrackets = properties.containsKey(SecurityConstants.ADD_ATTACHMENT_ID_BRACKETS) ?
				properties.get(SecurityConstants.ADD_ATTACHMENT_ID_BRACKETS).equals(SecurityConstants.ADD_ATTACHMENT_ID_BRACKETS_TRUE) :
					SecurityConstants.ADD_ATTACHMENT_ID_BRACKETS_DEFAULT;
		try{
			int numAttachmentsInMsg = message.countAttachments();
			List<String> cidAttachments = new ArrayList<>();
			if(numAttachmentsInMsg>0){
				Iterator<?> itAttach = message.getAttachments();
				while (itAttach.hasNext()) {
					AttachmentPart ap = (AttachmentPart) itAttach.next();
					String cid = ap.getContentId();
					if (!addAttachmentIdBrackets) {
						cid = cid.replaceAll("(^<)|(>$)", "");
					}
					cidAttachments.add(cid);
				}
			}

			// *** ENCRYPT VERIFY ***
			Map<QName, QName> notResolvedMap = new HashMap<>();
			Object encryptionPartsVerify = messageSecurityContext.getIncomingProperties().get(SecurityConstants.ENCRYPTION_PARTS_VERIFY);
			Object encryptionParts = messageSecurityContext.getIncomingProperties().get(SecurityConstants.ENCRYPTION_PARTS);
			if(encryptionPartsVerify!=null && "true".equalsIgnoreCase(((String)encryptionPartsVerify).trim()) &&
					encryptionParts==null){
				// Se forzo la proprietà verify allora devo indicare le encryption parts
				throw new SecurityException(SecurityConstants.ENCRYPTION_PARTS+" non indicate");
			}
			if( (
					encryptionPartsVerify==null || 
					"true".equalsIgnoreCase(((String)encryptionPartsVerify).trim())
					) &&
					(encryptionParts!=null) ){

				Map<String, Boolean> encryptionPartsMap = new HashMap<>();
				Map<Reference, Boolean> encryptionReferenceMap = new HashMap<>();
				boolean isAllAttachmentEncrypted = false;
				int numAttachmentsEncrypted = 0;
				if(encryptionParts!=null){
					String[]split = ((String)encryptionParts).split(";");
					int numElementsEncrypted = 0;
					for (int i = 0; i < split.length; i++) {
						boolean checked = false;
						String[]split2 = split[i].trim().split("}");
						String tipo = split2[0].trim().substring(1); // Element o Content
						if("".equals(tipo)){
							// caso speciale wss4j {}cid:Attachments
							tipo = SecurityConstants.PART_CONTENT;
						}
						String namespace = null;
						String nome = null;
						boolean attach = false;
						if(split2.length==3){
							namespace = split2[1].trim().substring(1); 
							nome = split2[2].trim();
							attach = SecurityConstants.ENCRYPTION_NAMESPACE_ATTACH.equals(namespace);
						}
						else{
							// caso speciale wss4j {}cid:Attachments ?
							if(SecurityConstants.CID_ATTACH_WSS4J.equalsIgnoreCase(split2[1].trim())){
								namespace = SecurityConstants.ENCRYPTION_NAMESPACE_ATTACH;
								nome = SecurityConstants.ATTACHMENT_INDEX_ALL;
								attach = true;
							}
							else{
								throw new SecurityException("Part ["+split[i]+"] with wrong format");
							}
						}
						if(SecurityConstants.ENCRYPTION_PART_ELEMENT.equals(tipo)) {
							// incremento solamente se non è un attachments
							if(!attach){
								numElementsEncrypted++;
							}
						}
						if(nome.startsWith("{"))
							nome = nome.substring(1);
						/**System.out.println("CIFRO ["+tipo+"] ["+namespace+"] ["+nome+"]");*/	

						List<String> cidAttachmentsEncrypt = new ArrayList<>();
						if(cidAttachments!=null && !cidAttachments.isEmpty()){
							cidAttachmentsEncrypt.addAll(cidAttachments);
						}

						for (Reference reference : elementsToClean) {

							if(reference instanceof AttachmentReference) {
								if(AttachmentReference.TYPE_ENCRYPT_ATTACHMENT==reference.getType()){
									if(SecurityConstants.ENCRYPTION_NAMESPACE_ATTACH.equals(namespace)){
										numAttachmentsEncrypted++;
										if(nome.equals(SecurityConstants.ATTACHMENT_INDEX_ALL)) {
											checked = true;
											isAllAttachmentEncrypted = true;
											encryptionReferenceMap.put(reference, true);
											encryptionPartsMap.put(split[i], checked);
											/** break;*/
											// Vanno iterate tutte, dovendole contare
										}else{
											int position = -1;
											try{
												position = Integer.parseInt(nome);
											}catch(Exception e){
												// position non presente
											}
											if(position>0){
												int refPosition = -1;
												for (int j = 0; j < cidAttachments.size(); j++) {
													if(cidAttachments.get(j).equals(reference.getReference())){
														refPosition = j+1;
														break;
													}
												}
												/**if(refPosition == position){ // Alcune implementazioni durante la spedizione modificano l'ordine degli attachments.*/
												// Non è possibile effettuare tale controllo sulla ricezione. Può essere usato solo per specificare quale attach firmare/cifrare in spedizione
												// verifico solamente che il cid sia presente
												if(refPosition>0){
													cidAttachmentsEncrypt.remove((refPosition-1));
													checked = true;
													encryptionReferenceMap.put(reference, true);
													encryptionPartsMap.put(split[i], checked);
													break;
												}
											}
											else{
												if(nome.equals(reference.getReference())) {
													checked = true;
													encryptionReferenceMap.put(reference, true);
													encryptionPartsMap.put(split[i], checked);
													break;
												}
											}
										}
									}
								}
							} else {
								ElementReference elementReference = ((ElementReference)reference);
								String localName = elementReference.getElement().getLocalName();
								String namespaceURI = elementReference.getElement().getNamespaceURI();
								if(ElementReference.TYPE_ENCRYPT_CONTENT==reference.getType()){
									// Check encrypt content
									if(nome.equals(localName) && namespace.equals(namespaceURI)) {
										checked = SecurityConstants.ENCRYPTION_PART_CONTENT.equals(tipo);
										encryptionReferenceMap.put(reference, checked);
										encryptionPartsMap.put(split[i], checked);
										if(checked){
											break;
										}
									}
								} else if(ElementReference.TYPE_ENCRYPT_ELEMENT==reference.getType() && SecurityConstants.ENCRYPTION_PART_ELEMENT.equals(tipo)) {
									// Segnamo l'elemento come checked, e lo aggiungiamo alla lista di elementi da verificare 
									// dopo la decifratura, visto che allo stato attuale e' imposibile verificarlo
									checked = true;
									encryptionPartsMap.put(split[i], checked);
									QName actual = new QName(namespaceURI, localName);
									QName expected = new QName(namespace, nome);
									boolean localPartSec = localName.equals(qnameSecurity.getLocalPart());
									boolean namespaceURISec = namespaceURI.equals(qnameSecurity.getNamespaceURI()); 
									if(!(localPartSec && namespaceURISec)) {
										if(!notResolvedMap.containsKey(expected)) {
											notResolvedMap.put(expected, actual);
										}
									}
								}
							}
						}
					}

					int numElementsExpectedToBeEncrypted = 0;
					for(Reference reference : elementsToClean) {
						if(reference.getType()==ElementReference.TYPE_ENCRYPT_ELEMENT) {
							numElementsExpectedToBeEncrypted++;
						}
					}

					if(numElementsExpectedToBeEncrypted != numElementsEncrypted) {
						SubErrorCodeSecurity subCodice = new SubErrorCodeSecurity();
						subCodice.setEncrypt(true);
						subCodice.setMsgErrore("Expected encryption {Element} " + numElementsExpectedToBeEncrypted + ", found " + notResolvedMap.size());
						codiciErrore.add(subCodice);
					}
				}
				if(encryptionParts!=null){
					String[]split = ((String)encryptionParts).split(";");
					for (int i = 0; i < split.length; i++) {
						String[]split2 = split[i].trim().split("}");
						String tipo = split2[0].trim().substring(1); // Element o Content
						if("".equals(tipo)){
							// caso speciale wss4j {}cid:Attachments
							tipo = SecurityConstants.PART_CONTENT;
						}
						String namespace = null;
						String nome = null;
						if(split2.length==3){
							namespace = split2[1].trim().substring(1); 
							nome = split2[2].trim();
						}
						else{
							// caso speciale wss4j {}cid:Attachments ?
							if(SecurityConstants.CID_ATTACH_WSS4J.equalsIgnoreCase(split2[1].trim())){
								namespace = SecurityConstants.ENCRYPTION_NAMESPACE_ATTACH;
								nome = SecurityConstants.ATTACHMENT_INDEX_ALL;
							}
							else{
								throw new SecurityException("Part ["+split[i]+"] with wrong format");
							}
						}
						if(nome.startsWith("{"))
							nome = nome.substring(1);
						if(SecurityConstants.ENCRYPTION_PART_CONTENT.equals(tipo)){
							if(!encryptionPartsMap.containsKey(split[i]) || !encryptionPartsMap.get(split[i])) {
								SubErrorCodeSecurity subCodice = new SubErrorCodeSecurity();
								subCodice.setEncrypt(true);
								subCodice.setMsgErrore("Expected encryption part("+ split[i] +") not found");
								subCodice.setTipo(tipo);
								subCodice.setNamespace(namespace);
								subCodice.setName(nome);
								codiciErrore.add(subCodice);
								/**throw new Exception("Expected encryption part("+ split[i] +") not found");*/
							}
						}
					}
				}

				if(isAllAttachmentEncrypted && numAttachmentsEncrypted != numAttachmentsInMsg) {
					SubErrorCodeSecurity subCodice = new SubErrorCodeSecurity();
					subCodice.setEncrypt(true);
					subCodice.setMsgErrore("All attachments in message (found:"+numAttachmentsInMsg+") must be encrypted, but only "+numAttachmentsEncrypted+" appear to be encrypted");
					subCodice.setTipo(SecurityConstants.ENCRYPTION_PART_CONTENT);
					subCodice.setNamespace(SecurityConstants.ENCRYPTION_NAMESPACE_ATTACH);
					subCodice.setName(SecurityConstants.ATTACHMENT_INDEX_ALL);
					codiciErrore.add(subCodice);
					/**throw new Exception("All attachments in message (found:"+numAttachmentsInMsg+") must be encrypted, but only "+numAttachmentsEncrypted+" appear to be encrypted");*/
				}

				for (Reference reference: elementsToClean) {
					if(reference.getType()==ElementReference.TYPE_ENCRYPT_CONTENT || reference.getType()==AttachmentReference.TYPE_ENCRYPT_ATTACHMENT) {
						if(!encryptionReferenceMap.containsKey(reference) || !encryptionReferenceMap.get(reference)) {
							SubErrorCodeSecurity subCodice = new SubErrorCodeSecurity();
							subCodice.setEncrypt(true);
							subCodice.setMsgErrore("Found encryption part("+ reference+") not expected");
							if(reference.getType()==ElementReference.TYPE_ENCRYPT_CONTENT){
								ElementReference elReference = (ElementReference) reference;
								subCodice.setTipo(SecurityConstants.ENCRYPTION_PART_CONTENT);
								subCodice.setNamespace(elReference.getElement().getNamespaceURI());
								subCodice.setName(elReference.getElement().getLocalName());
							}
							else{
								subCodice.setTipo(SecurityConstants.ENCRYPTION_PART_CONTENT);
								subCodice.setNamespace(SecurityConstants.ENCRYPTION_NAMESPACE_ATTACH);
								subCodice.setName(reference.getReference());
							}
							codiciErrore.add(subCodice);
							/**throw new Exception("Found encryption part("+ reference+") not expected");*/
						}
					}
				}

			}

			// *** SIGNATURE VERIFY ***
			Object signaturePartsVerify = messageSecurityContext.getIncomingProperties().get(SecurityConstants.SIGNATURE_PARTS_VERIFY);
			Object signatureParts = messageSecurityContext.getIncomingProperties().get(SecurityConstants.SIGNATURE_PARTS);
			if(signaturePartsVerify!=null && "true".equalsIgnoreCase(((String)signaturePartsVerify).trim()) &&
					signatureParts==null){
				// Se forzo la proprietà verify allora devo indicare le signature parts
				throw new SecurityException(SecurityConstants.SIGNATURE_PARTS+" non indicate");
			}
			if( (
					signaturePartsVerify==null || 
					"true".equalsIgnoreCase(((String)signaturePartsVerify).trim())
					) &&
					(signatureParts!=null) ){

				boolean isAllAttachmentSigned = false;
				int numAttachmentsSigned = 0;
				Map<String, Boolean> signaturePartsMap = new HashMap<>();
				Map<Reference, Boolean> referenceMap = new HashMap<>();
				if(signatureParts!=null){
					String[]split = ((String)signatureParts).split(";");
					for (int i = 0; i < split.length; i++) {
						boolean checked = false;
						String[]split2 = split[i].trim().split("}");
						String tipo = split2[0].trim().substring(1); // Element o Content
						if("".equals(tipo)){
							// caso speciale wss4j {}cid:Attachments
							tipo = SecurityConstants.PART_CONTENT;
						}
						String namespace = null;
						String nome = null;
						if(split2.length==3){
							namespace = split2[1].trim().substring(1); 
							nome = split2[2].trim();
						}
						else{
							// caso speciale wss4j {}cid:Attachments ?
							if(SecurityConstants.CID_ATTACH_WSS4J.equalsIgnoreCase(split2[1].trim())){
								namespace = SecurityConstants.SIGNATURE_NAMESPACE_ATTACH;
								nome = SecurityConstants.ATTACHMENT_INDEX_ALL;
							}
							else{
								throw new SecurityException("Part ["+split[i]+"] with wrong format");
							}
						}
						if(nome.startsWith("{"))
							nome = nome.substring(1);
						/**System.out.println("CIFRO ["+tipo+"] ["+namespace+"] ["+nome+"]");*/	

						List<String> cidAttachmentsSignature = new ArrayList<>();
						if(cidAttachments!=null && !cidAttachments.isEmpty()){
							cidAttachmentsSignature.addAll(cidAttachments);
						}

						for (Reference reference : elementsToClean) {

							if(reference instanceof AttachmentReference) {
								if(AttachmentReference.TYPE_SIGN_ATTACHMENT==reference.getType()){
									if(SecurityConstants.SIGNATURE_NAMESPACE_ATTACH.equals(namespace)){
										numAttachmentsSigned++;
										if(nome.equals(SecurityConstants.ATTACHMENT_INDEX_ALL)) {
											checked = true;
											isAllAttachmentSigned = true;
											referenceMap.put(reference, true);
											/** break;*/
											// Vanno iterate tutte, dovendole contare
										}
										else{
											int position = -1;
											try{
												position = Integer.parseInt(nome);
											}catch(Exception e){
												// position non presente
											}
											if(position>0){
												int refPosition = -1;
												for (int j = 0; j < cidAttachments.size(); j++) {
													if(cidAttachments.get(j).equals(reference.getReference())){
														refPosition = j+1;
														break;
													}
												}
												/**if(refPosition == position){ // Alcune implementazioni durante la spedizione modificano l'ordine degli attachments.*/
												// Non è possibile effettuare tale controllo sulla ricezione. Può essere usato solo per specificare quale attach firmare/cifrare in spedizione
												// verifico solamente che il cid sia presente
												if(refPosition>0){
													cidAttachmentsSignature.remove((refPosition-1));
													checked = true;
													referenceMap.put(reference, true);
													break;
												}
											}
											else{
												if(nome.equals(reference.getReference())) {
													checked = true;
													referenceMap.put(reference, true);
													break;
												}
											}
										}
									}
								}
							} else {
								ElementReference elementReference = ((ElementReference)reference);
								String localName = elementReference.getElement().getLocalName();
								String namespaceURI = elementReference.getElement().getNamespaceURI();
								if(ElementReference.TYPE_SIGNATURE==reference.getType() && nome.equals(localName) && namespace.equals(namespaceURI)) {
									checked = SecurityConstants.SIGNATURE_PART_COMPLETE.equals(tipo) ||
											SecurityConstants.SIGNATURE_PART_CONTENT.equals(tipo) || SecurityConstants.SIGNATURE_PART_ELEMENT.equals(tipo);
									referenceMap.put(reference, checked);
									if(checked){
										break;
									}
								}
							}
						}
						signaturePartsMap.put(split[i], checked);
					}
				}

				for (String part : signaturePartsMap.keySet()) {
					if(!signaturePartsMap.get(part)) {
						String[]split2 = part.trim().split("}");
						String tipo = split2[0].trim().substring(1); // Element o Content
						if("".equals(tipo)){
							// caso speciale wss4j {}cid:Attachments
							tipo = SecurityConstants.PART_CONTENT;
						}
						String namespace = null;
						String nome = null;
						if(split2.length==3){
							namespace = split2[1].trim().substring(1); 
							nome = split2[2].trim();
						}
						else{
							// caso speciale wss4j {}cid:Attachments ?
							if(SecurityConstants.CID_ATTACH_WSS4J.equalsIgnoreCase(split2[1].trim())){
								namespace = SecurityConstants.SIGNATURE_NAMESPACE_ATTACH;
								nome = SecurityConstants.ATTACHMENT_INDEX_ALL;
							}
							else{
								throw new SecurityException("Part ["+part+"] with wrong format");
							}
						}
						if(nome.startsWith("{"))
							nome = nome.substring(1);
						SubErrorCodeSecurity subCodice = new SubErrorCodeSecurity();
						subCodice.setEncrypt(false);
						subCodice.setMsgErrore("Expected signature part("+ part +") not found");
						subCodice.setTipo(tipo);
						subCodice.setNamespace(namespace);
						subCodice.setName(nome);
						codiciErrore.add(subCodice);
						/**throw new Exception("Expected signature part("+ part +") not found");*/
					}
				}


				if(isAllAttachmentSigned && numAttachmentsSigned != numAttachmentsInMsg) {
					SubErrorCodeSecurity subCodice = new SubErrorCodeSecurity();
					subCodice.setEncrypt(false);
					subCodice.setMsgErrore("All attachments in message (found:"+numAttachmentsInMsg+") must be signed, but only "+numAttachmentsSigned+" appear to be signed");
					subCodice.setTipo(SecurityConstants.SIGNATURE_PART_CONTENT);
					subCodice.setNamespace(SecurityConstants.SIGNATURE_NAMESPACE_ATTACH);
					subCodice.setName(SecurityConstants.ATTACHMENT_INDEX_ALL);
					codiciErrore.add(subCodice);
					/**throw new Exception("All attachments in message (found:"+numAttachmentsInMsg+") must be signed, but "+numAttachmentsSigned+" appear to be signed");*/
				}

				for (Reference reference: elementsToClean) {
					if(reference.getType()==ElementReference.TYPE_SIGNATURE || reference.getType()==AttachmentReference.TYPE_SIGN_ATTACHMENT) {
						if(!referenceMap.containsKey(reference) || !referenceMap.get(reference)) {
							SubErrorCodeSecurity subCodice = new SubErrorCodeSecurity();
							subCodice.setEncrypt(false);
							subCodice.setMsgErrore("Found signature part("+ reference+") not expected");
							if(reference.getType()==ElementReference.TYPE_SIGNATURE){
								ElementReference elReference = (ElementReference) reference;
								subCodice.setTipo("Signature"); // Non esiste Content/Element
								subCodice.setNamespace(elReference.getElement().getNamespaceURI());
								subCodice.setName(elReference.getElement().getLocalName());
							}
							else{
								subCodice.setTipo(SecurityConstants.SIGNATURE_PART_CONTENT);
								subCodice.setNamespace(SecurityConstants.ENCRYPTION_NAMESPACE_ATTACH);
								subCodice.setName(reference.getReference());
							}
							codiciErrore.add(subCodice);						
							/**throw new Exception("Found signature part("+ reference+") not expected");*/
						}
					}
				}
			}

			return notResolvedMap;

		}catch(Exception e){
			throw new SecurityException(e.getMessage(),e);
		}
	}


	public static boolean isIntegerInRanges(Integer value, String rawRanges) {
		String[] ranges = rawRanges.replace(" ", "").split(",");
		for (String range : ranges) {
			int minRange = 0;
			int maxRange = 0;
			if (range.contains("-")) {
				String[] limits = range.split("-");
				minRange = limits[0].isBlank() ? 0 : Integer.parseInt(limits[0]);
				maxRange = limits.length == 1 ? Integer.MAX_VALUE : Integer.parseInt(limits[1]);
			} else {
				minRange = Integer.parseInt(range);
				maxRange = minRange;
			}

			if (value >= minRange && value <= maxRange)
				return true;
		}
		return false;
	}

	public static String resolveDynamicValue(String name, String value, Map<String, Object> dynamicMap) throws SecurityException {
		if (value == null || dynamicMap == null) {
			return value;
		}
		try {
			return DynamicStringReplace.replace(value, dynamicMap, true);
		} catch (UtilsException e) {
			throw new SecurityException("Errore nella risoluzione del valore dinamico per '" + name + "': " + e.getMessage(), e);
		}
	}

	
	
	public static void addJSONClaims(MessageSecurityContext messageSecurityContext, OpenSPCoop2Message message, org.openspcoop2.utils.Map<Object> ctx) throws MessageException, MessageNotSupportedException, SecurityException {
		Context context = (ctx instanceof Context) ? (Context) ctx : null;

		// Leggi la policy per i claims esistenti (default: preserve)
		String policy =  MessageUtilities.getOutgoingProperty(messageSecurityContext, SecurityConstants.JWT_CLAIMS_EXISTING_POLICY);
		if (policy == null || policy.isEmpty()) {
			policy = SecurityConstants.JWT_CLAIMS_EXISTING_POLICY_PRESERVE;
		}		

		String contentType =  MessageUtilities.getOutgoingProperty(messageSecurityContext, SecurityConstants.MESSAGE_SECURITY_CONTENT_TYPE);
		if (contentType != null) {
			message.setContentType(contentType);
		}

		JSONObject json = null; // la struttura json viene valorizzata solo se necessario

		json = addIssClaim(messageSecurityContext, context, message, json, policy);

		json = addAudClaim(messageSecurityContext, context, message, json, policy);

		json = addExpClaim(messageSecurityContext, context, message, json, policy);

		addJtiClaim(messageSecurityContext, context, message, json, policy);

	}	 
	
	/**
	 * Risolve il valore del claim in base alla policy configurata per i claims esistenti.
	 *
	 * @param existingJson JSON esistente parsato dal messaggio (può essere null)
	 * @param claimName nome del claim da verificare
	 * @param newValue valore da inserire
	 * @param policy policy da applicare (preserve, override, error)
	 * @return il valore da usare (nuovo valore o null se il claim esiste e policy=preserve)
	 * @throws SecurityException se policy=error e il claim esiste già
	 */
	private static Object resolveClaimValue(JSONObject existingJson, String claimName,
			Object newValue, String policy, MessageRole role) throws SecurityException {

		if (existingJson == null) {
			return newValue;
		}

		boolean claimExists = existingJson.containsKey(claimName);

		if (!claimExists) {
			return newValue;
		}

		// Claim esiste, applica policy
		if (SecurityConstants.JWT_CLAIMS_EXISTING_POLICY_PRESERVE.equals(policy)) {
			// Mantieni originale, non aggiungere nulla
			return null;
		} else if (SecurityConstants.JWT_CLAIMS_EXISTING_POLICY_OVERRIDE.equals(policy)) {
			// Sovrascrivi con il nuovo valore
			return newValue;
		} else if (SecurityConstants.JWT_CLAIMS_EXISTING_POLICY_ERROR.equals(policy)) {
			String msg = MessageRole.REQUEST.equals(role) ? "richiesta" : "risposta";
			throw new SecurityException("Il claim '" + claimName + "' è già presente nella "+msg);
		}

		// Default: preserve
		return null;
	}

	private static JSONObject getJsonMessage(OpenSPCoop2Message message, Logger log) {
		JSONObject existingJson = null;
		try {
			if (message.castAsRestJson().hasContent()) {
				String jsonContent = message.castAsRestJson().getContent();
				if (jsonContent != null && !jsonContent.trim().isEmpty()) {
					existingJson = JsonPathExpressionEngine.getJSONObject(jsonContent);
				}
			}
		} catch (Exception e) {
			log.debug("Parsing JSON esistente fallito: {}", e.getMessage());
		}

		return existingJson;
	}

	private static JSONObject addIssClaim(MessageSecurityContext messageSecurityContext, Context context, OpenSPCoop2Message message, JSONObject existingJson, String policy) throws SecurityException, MessageException, MessageNotSupportedException {
		Map<String, Object> dynamicMap = Costanti.readDynamicMap(context);

		String iss = MessageUtilities.getOutgoingProperty(messageSecurityContext, SecurityConstants.JWT_CLAIMS_ISSUER);
		if (iss != null) {
			if (existingJson == null)
				existingJson = getJsonMessage(message, messageSecurityContext.getLog());

			iss = MessageUtilities.resolveDynamicValue("iss", iss, dynamicMap);
			Object resolvedIss = MessageUtilities.resolveClaimValue(existingJson, "iss", iss, policy, message.getMessageRole());
			if (resolvedIss != null) {
				message.castAsRestJson().addSimpleElement("iss", resolvedIss);
			}
		}

		return existingJson;
	}

	private static JSONObject addAudClaim(MessageSecurityContext messageSecurityContext, Context context, OpenSPCoop2Message message, JSONObject existingJson, String policy) throws SecurityException, MessageException, MessageNotSupportedException {
		Map<String, Object> dynamicMap = Costanti.readDynamicMap(context);

		String audMode =  MessageUtilities.getOutgoingProperty(messageSecurityContext, SecurityConstants.JWT_CLAIMS_AUDIENCE_MODE);
		String aud =  MessageUtilities.getOutgoingProperty(messageSecurityContext, SecurityConstants.JWT_CLAIMS_AUDIENCE_VALUE);
		if (SecurityConstants.JWT_CLAIMS_AUDIENCE_MODE_VOUCHER.equals(audMode)) {
			aud = "${tokenInfo:sub}";
		}
		if (aud != null) {
			if (existingJson == null)
				existingJson = getJsonMessage(message, messageSecurityContext.getLog());

			aud = MessageUtilities.resolveDynamicValue("aud", aud, dynamicMap);
			Object resolvedAud = MessageUtilities.resolveClaimValue(existingJson, "aud", aud, policy, message.getMessageRole());
			if (resolvedAud != null) {
				message.castAsRestJson().addSimpleElement("aud", resolvedAud);
			}
		}

		return existingJson;
	}

	private static JSONObject addExpClaim(MessageSecurityContext messageSecurityContext, Context context, OpenSPCoop2Message message, JSONObject existingJson, String policy) throws SecurityException, MessageException, MessageNotSupportedException {
		Map<String, Object> dynamicMap = Costanti.readDynamicMap(context);

		String ttl =  MessageUtilities.getOutgoingProperty(messageSecurityContext, SecurityConstants.JWT_CLAIMS_EXPIRED_TTL);
		if (ttl != null) {
			if (existingJson == null)
				existingJson = getJsonMessage(message, messageSecurityContext.getLog());

			ttl = MessageUtilities.resolveDynamicValue("ttl", ttl, dynamicMap);
			long instant = Instant.now().getEpochSecond();

			Object resolvedIat = MessageUtilities.resolveClaimValue(existingJson, "iat", instant, policy, message.getMessageRole());
			if (resolvedIat != null) {
				message.castAsRestJson().addSimpleElement("iat", resolvedIat);
			}

			Object resolvedNbf = MessageUtilities.resolveClaimValue(existingJson, "nbf", instant, policy, message.getMessageRole());
			if (resolvedNbf != null) {
				message.castAsRestJson().addSimpleElement("nbf", resolvedNbf);
			}

			long expValue = instant + Integer.parseInt(ttl);
			Object resolvedExp = MessageUtilities.resolveClaimValue(existingJson, "exp", expValue, policy, message.getMessageRole());
			if (resolvedExp != null) {
				message.castAsRestJson().addSimpleElement("exp", resolvedExp);
			}
		}

		return existingJson;
	}

	private static JSONObject addJtiClaim(MessageSecurityContext messageSecurityContext, Context context, OpenSPCoop2Message message, JSONObject existingJson, String policy) throws SecurityException, MessageException, MessageNotSupportedException {
		Map<String, Object> dynamicMap = Costanti.readDynamicMap(context);

		String jtiMode =  MessageUtilities.getOutgoingProperty(messageSecurityContext, SecurityConstants.JWT_CLAIMS_JTI_MODE);
		String jti =  MessageUtilities.getOutgoingProperty(messageSecurityContext, SecurityConstants.JWT_CLAIMS_JTI_VALUE);
		if (SecurityConstants.JWT_CLAIMS_JTI_MODE_TRANSACTION_ID.equals(jtiMode)) {
			jti = message.getTransactionId();
		} else if (jti != null) {
			jti = MessageUtilities.resolveDynamicValue("jti", jti, dynamicMap);
		}
		if (jti != null) {
			if (existingJson == null)
				existingJson = getJsonMessage(message, messageSecurityContext.getLog());

			Object resolvedJti = MessageUtilities.resolveClaimValue(existingJson, "jti", jti, policy, message.getMessageRole());
			if (resolvedJti != null) {
				message.castAsRestJson().addSimpleElement("jti", resolvedJti);
			}
		}

		return existingJson;
	}
	
	public static String getOutgoingProperty(MessageSecurityContext messageSecurityContext, String key) {
		return (String) messageSecurityContext.getOutgoingProperties().get(key);
	}
}