/*
* 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);
}
}
}
}