/* * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. */ package net.curisit.securis.services.helpers; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import net.curisit.securis.db.Application; import net.curisit.securis.db.ApplicationMetadata; import net.curisit.securis.db.License; import net.curisit.securis.db.LicenseStatus; import net.curisit.securis.db.LicenseType; import net.curisit.securis.db.LicenseTypeMetadata; import net.curisit.securis.db.Pack; import net.curisit.securis.db.PackMetadata; import net.curisit.securis.db.common.Metadata; /** * MetadataHelper *

* Utilities to compare, merge and propagate metadata across the hierarchy: * Application -> LicenseType -> Pack -> (marks License as metadata-obsolete) *

* Provides: * - Equality checks on metadata sets. * - Merge semantics: remove keys not present, update changed values/mandatory flags. * - Propagation from Application down to LicenseType and from LicenseType down to Packs. * - Marking existing licenses as "metadataObsolete" when pack metadata changes and * the license is in a state where consumers could depend on metadata snapshot. * * Thread-safety: ApplicationScoped, stateless. * * @author JRA * Last reviewed by JRA on Oct 5, 2025. */ @ApplicationScoped public class MetadataHelper { private static final Logger log = LogManager.getLogger(MetadataHelper.class); /** * match *

* Compare two metadata entries (key, value, mandatory). * * @param m1 First metadata. * @param m2 Second metadata. * @param Metadata subtype. * @return true if equal in key/value/mandatory, false otherwise or if any is null. */ public boolean match(T m1, T m2) { if (m1 == null || m2 == null) { return false; } return Objects.equals(m1.getKey(), m2.getKey()) && Objects.equals(m1.getValue(), m2.getValue()) && m1.isMandatory() == m2.isMandatory(); } /** * findByKey *

* Find a metadata by key in a collection. * * @param key Metadata key to search. * @param listMd Collection of metadata. * @param Metadata subtype. * @return The first matching metadata or null. */ public Metadata findByKey(String key, Collection listMd) { return listMd.parallelStream().filter(m -> Objects.equals(key, m.getKey())).findAny().orElse(null); } /** * match *

* Compare two sets of metadata for equality (size + all entries match). * * @param listMd1 First set. * @param listMd2 Second set. * @param Metadata subtype. * @return true if both sets match element-wise, false otherwise. */ public boolean match(Set listMd1, Set listMd2) { if (listMd1.size() != listMd2.size()) { return false; } return listMd1.parallelStream().allMatch(m -> this.match(m, findByKey(m.getKey(), listMd2))); } /** * mergeMetadata *

* Merge metadata from a source set (truth) into a target set. * - Removes entries in target whose keys are not in {@code keys}. * - Updates entries in target whose value/mandatory differ from source. * - Does NOT create new entries; caller is expected to persist new ones separately. * * @param em EntityManager to remove/merge. * @param srcListMd Source metadata set (truth). * @param tgtListMd Target metadata set to update. * @param keys Keys present in source. * @param Source metadata type. * @param Target metadata type. */ public void mergeMetadata(EntityManager em, Set srcListMd, Set tgtListMd, Set keys) { // Remove missing keys Set mdToRemove = tgtListMd.parallelStream() .filter(md -> !keys.contains(md.getKey())) .collect(Collectors.toSet()); for (K tgtMd : mdToRemove) { log.info("MD key to remove: {} - {}", tgtMd.getKey(), tgtMd); if (tgtMd instanceof LicenseTypeMetadata) { log.info("LT: {}, tx: {}, contans: {}", LicenseTypeMetadata.class.cast(tgtMd).getLicenseType(), em.isJoinedToTransaction(), em.contains(tgtMd)); } em.remove(tgtMd); } // Update changed keys Set keysToUpdate = tgtListMd.parallelStream() .filter(md -> keys.contains(md.getKey())) .collect(Collectors.toSet()); for (K tgtMd : keysToUpdate) { Metadata md = this.findByKey(tgtMd.getKey(), srcListMd); if (md.isMandatory() != tgtMd.isMandatory() || !Objects.equals(md.getValue(), tgtMd.getValue())) { tgtMd.setMandatory(md.isMandatory()); tgtMd.setValue(md.getValue()); log.info("MD key to update: {}", tgtMd.getKey()); em.merge(tgtMd); } } } // -- Internal helpers to create new metadata rows when propagating /** * createNewMetadata

* Create new metadata * * @param appMd * @param existingMd * @param licenseType * @return newMetadata */ private Set createNewMetadata(Set appMd, Set existingMd, LicenseType licenseType) { Set oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet()); return appMd.parallelStream() .filter(md -> !oldKeys.contains(md.getKey())) .map(appmd -> { LicenseTypeMetadata ltmd = new LicenseTypeMetadata(); ltmd.setLicenseType(licenseType); ltmd.setKey(appmd.getKey()); ltmd.setValue(appmd.getValue()); ltmd.setMandatory(appmd.isMandatory()); return ltmd; }).collect(Collectors.toSet()); } /** * createNewMetadata

* Create the new metadata * * @param ltMd * @param existingMd * @param pack * @return newMetadata */ private Set createNewMetadata(Set ltMd, Set existingMd, Pack pack) { Set oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet()); return ltMd.parallelStream() .filter(md -> !oldKeys.contains(md.getKey())) .map(md -> { PackMetadata pmd = new PackMetadata(); pmd.setPack(pack); pmd.setKey(md.getKey()); pmd.setValue(md.getValue()); pmd.setMandatory(md.isMandatory()); return pmd; }).collect(Collectors.toSet()); } /** * propagateMetadata (Application -> LicenseTypes -> Packs) *

* Propagates application metadata changes down to all its license types and packs: * - mergeMetadata on LicenseType * - create new LicenseTypeMetadata for new keys * - re-fetch LT metadata (detached/merged semantics) * - propagateMetadata(LicenseType) to packs * * @param em EntityManager. * @param app Application with updated metadata loaded. */ public void propagateMetadata(EntityManager em, Application app) { Set appMd = app.getApplicationMetadata(); Set keys = appMd.parallelStream().map(md -> md.getKey()).collect(Collectors.toSet()); for (LicenseType lt : app.getLicenseTypes()) { log.info("Lic type to update: {}", lt.getCode()); this.mergeMetadata(em, appMd, lt.getMetadata(), keys); Set newMdList = createNewMetadata(appMd, lt.getMetadata(), lt); for (LicenseTypeMetadata newMetadata : newMdList) { em.persist(newMetadata); } em.detach(lt); // Re-read updated metadata TypedQuery updatedMdQuery = em.createNamedQuery("list-licensetype-metadata", LicenseTypeMetadata.class); updatedMdQuery.setParameter("licenseTypeId", lt.getId()); Set updatedMd = new HashSet<>(updatedMdQuery.getResultList()); lt.setMetadata(updatedMd); propagateMetadata(em, lt, keys); } } /** * propagateMetadata (LicenseType -> Packs) *

* Propagates license type metadata changes to all its packs: * - mergeMetadata on Pack * - create new PackMetadata for new keys * - markObsoleteMetadata on packs to flag their licenses * * Frozen packs are skipped. * * @param em EntityManager. * @param lt LicenseType with updated metadata set. * @param keys Set of keys present in the source. */ public void propagateMetadata(EntityManager em, LicenseType lt, Set keys) { Set ltMd = lt.getMetadata(); TypedQuery packsQuery = em.createNamedQuery("list-packs-by-lic-type", Pack.class); packsQuery.setParameter("lt_id", lt.getId()); List packs = packsQuery.getResultList(); log.info("Packs to update the metadata: {}", packs.size()); for (Pack pack : packs) { if (pack.isFrozen()) { log.warn("Metadata in LicenseType {} has changed but the Pack {} is frozen and won't be updated.", lt.getCode(), pack.getCode()); continue; } this.mergeMetadata(em, ltMd, pack.getMetadata(), keys); Set newMdList = createNewMetadata(ltMd, pack.getMetadata(), pack); for (PackMetadata newMetadata : newMdList) { em.persist(newMetadata); } markObsoleteMetadata(em, pack); em.detach(pack); } } /** * markObsoleteMetadata *

* For all licenses within the given pack, mark {@code metadataObsolete = true} * if the license is in a relevant state (ACTIVE, PRE_ACTIVE, CANCELLED). * This lets clients know that metadata-dependent artifacts might need refresh. * * @param em EntityManager. * @param pack Pack whose licenses to mark. */ public void markObsoleteMetadata(EntityManager em, Pack pack) { TypedQuery existingPackLicenses = em.createNamedQuery("list-licenses-by-pack", License.class); existingPackLicenses.setParameter("packId", pack.getId()); for (License lic : existingPackLicenses.getResultList()) { log.info("License from pack: {}, status: {}", lic.getCode(), lic.getStatus()); if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE || lic.getStatus() == LicenseStatus.CANCELLED) { lic.setMetadataObsolete(true); em.merge(lic); } } } }