Joaquín Reñé
2025-10-07 146a0fb8b0e90f9196e569152f649baf60d6cc8f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/*
 * 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
 * <p>
 * Utilities to compare, merge and propagate metadata across the hierarchy:
 *   Application -> LicenseType -> Pack -> (marks License as metadata-obsolete)
 * <p>
 * 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
     * <p>
     * Compare two metadata entries (key, value, mandatory).
     *
     * @param m1 First metadata.
     * @param m2 Second metadata.
     * @param <T> Metadata subtype.
     * @return true if equal in key/value/mandatory, false otherwise or if any is null.
     */
    public <T extends Metadata> 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
     * <p>
     * Find a metadata by key in a collection.
     *
     * @param key    Metadata key to search.
     * @param listMd Collection of metadata.
     * @param <T>    Metadata subtype.
     * @return The first matching metadata or null.
     */
    public <T extends Metadata> Metadata findByKey(String key, Collection<T> listMd) {
        return listMd.parallelStream().filter(m -> Objects.equals(key, m.getKey())).findAny().orElse(null);
    }
    /**
     * match
     * <p>
     * Compare two sets of metadata for equality (size + all entries match).
     *
     * @param listMd1 First set.
     * @param listMd2 Second set.
     * @param <T>     Metadata subtype.
     * @return true if both sets match element-wise, false otherwise.
     */
    public <T extends Metadata> boolean match(Set<T> listMd1, Set<T> listMd2) {
        if (listMd1.size() != listMd2.size()) {
            return false;
        }
        return listMd1.parallelStream().allMatch(m -> this.match(m, findByKey(m.getKey(), listMd2)));
    }
    /**
     * mergeMetadata
     * <p>
     * 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 <T>        Source metadata type.
     * @param <K>        Target metadata type.
     */
    public <T extends Metadata, K extends Metadata> void mergeMetadata(EntityManager em, Set<T> srcListMd, Set<K> tgtListMd, Set<String> keys) {
        // Remove missing keys
        Set<K> 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<K> 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<p>
     * Create new metadata
     * 
     * @param appMd
     * @param existingMd
     * @param licenseType
     * @return newMetadata
     */
    private Set<LicenseTypeMetadata> createNewMetadata(Set<ApplicationMetadata> appMd, Set<LicenseTypeMetadata> existingMd, LicenseType licenseType) {
        Set<String> 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<p>
     * Create the new metadata
     * 
     * @param ltMd
     * @param existingMd
     * @param pack
     * @return newMetadata
     */
    private Set<PackMetadata> createNewMetadata(Set<LicenseTypeMetadata> ltMd, Set<PackMetadata> existingMd, Pack pack) {
        Set<String> 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)
     * <p>
     * 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<ApplicationMetadata> appMd = app.getApplicationMetadata();
        Set<String> 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<LicenseTypeMetadata> newMdList = createNewMetadata(appMd, lt.getMetadata(), lt);
            for (LicenseTypeMetadata newMetadata : newMdList) {
                em.persist(newMetadata);
            }
            em.detach(lt);
            // Re-read updated metadata
            TypedQuery<LicenseTypeMetadata> updatedMdQuery = em.createNamedQuery("list-licensetype-metadata", LicenseTypeMetadata.class);
            updatedMdQuery.setParameter("licenseTypeId", lt.getId());
            Set<LicenseTypeMetadata> updatedMd = new HashSet<>(updatedMdQuery.getResultList());
            lt.setMetadata(updatedMd);
            propagateMetadata(em, lt, keys);
        }
    }
    /**
     * propagateMetadata (LicenseType -> Packs)
     * <p>
     * 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<String> keys) {
        Set<LicenseTypeMetadata> ltMd = lt.getMetadata();
        TypedQuery<Pack> packsQuery = em.createNamedQuery("list-packs-by-lic-type", Pack.class);
        packsQuery.setParameter("lt_id", lt.getId());
        List<Pack> 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<PackMetadata> newMdList = createNewMetadata(ltMd, pack.getMetadata(), pack);
            for (PackMetadata newMetadata : newMdList) {
                em.persist(newMetadata);
            }
            markObsoleteMetadata(em, pack);
            em.detach(pack);
        }
    }
    /**
     * markObsoleteMetadata
     * <p>
     * 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<License> 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);
            }
        }
    }
}