Joaquín Reñé
2025-10-07 146a0fb8b0e90f9196e569152f649baf60d6cc8f
securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java
....@@ -1,3 +1,6 @@
1
+/*
2
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
3
+ */
14 package net.curisit.securis.services.helpers;
25
36 import java.util.Collection;
....@@ -24,147 +27,258 @@
2427 import net.curisit.securis.db.PackMetadata;
2528 import net.curisit.securis.db.common.Metadata;
2629
30
+/**
31
+ * MetadataHelper
32
+ * <p>
33
+ * Utilities to compare, merge and propagate metadata across the hierarchy:
34
+ * Application -> LicenseType -> Pack -> (marks License as metadata-obsolete)
35
+ * <p>
36
+ * Provides:
37
+ * - Equality checks on metadata sets.
38
+ * - Merge semantics: remove keys not present, update changed values/mandatory flags.
39
+ * - Propagation from Application down to LicenseType and from LicenseType down to Packs.
40
+ * - Marking existing licenses as "metadataObsolete" when pack metadata changes and
41
+ * the license is in a state where consumers could depend on metadata snapshot.
42
+ *
43
+ * Thread-safety: ApplicationScoped, stateless.
44
+ *
45
+ * @author JRA
46
+ * Last reviewed by JRA on Oct 5, 2025.
47
+ */
2748 @ApplicationScoped
2849 public class MetadataHelper {
2950
30
- private static final Logger log = LogManager.getLogger(MetadataHelper.class);
51
+ private static final Logger log = LogManager.getLogger(MetadataHelper.class);
3152
32
- public <T extends Metadata> boolean match(T m1, T m2) {
33
- if (m1 == null || m2 == null) {
34
- return false;
35
- }
36
- return Objects.equals(m1.getKey(), m2.getKey()) && Objects.equals(m1.getValue(), m2.getValue()) && m1.isMandatory() == m2.isMandatory();
37
- }
53
+ /**
54
+ * match
55
+ * <p>
56
+ * Compare two metadata entries (key, value, mandatory).
57
+ *
58
+ * @param m1 First metadata.
59
+ * @param m2 Second metadata.
60
+ * @param <T> Metadata subtype.
61
+ * @return true if equal in key/value/mandatory, false otherwise or if any is null.
62
+ */
63
+ public <T extends Metadata> boolean match(T m1, T m2) {
64
+ if (m1 == null || m2 == null) {
65
+ return false;
66
+ }
67
+ return Objects.equals(m1.getKey(), m2.getKey()) && Objects.equals(m1.getValue(), m2.getValue()) && m1.isMandatory() == m2.isMandatory();
68
+ }
3869
39
- public <T extends Metadata> Metadata findByKey(String key, Collection<T> listMd) {
40
- return listMd.parallelStream().filter(m -> Objects.equals(key, m.getKey())).findAny().orElse(null);
41
- }
70
+ /**
71
+ * findByKey
72
+ * <p>
73
+ * Find a metadata by key in a collection.
74
+ *
75
+ * @param key Metadata key to search.
76
+ * @param listMd Collection of metadata.
77
+ * @param <T> Metadata subtype.
78
+ * @return The first matching metadata or null.
79
+ */
80
+ public <T extends Metadata> Metadata findByKey(String key, Collection<T> listMd) {
81
+ return listMd.parallelStream().filter(m -> Objects.equals(key, m.getKey())).findAny().orElse(null);
82
+ }
4283
43
- public <T extends Metadata> boolean match(Set<T> listMd1, Set<T> listMd2) {
44
- if (listMd1.size() != listMd2.size()) {
45
- return false;
46
- }
47
- return listMd1.parallelStream().allMatch(m -> this.match(m, findByKey(m.getKey(), listMd2)));
48
- }
84
+ /**
85
+ * match
86
+ * <p>
87
+ * Compare two sets of metadata for equality (size + all entries match).
88
+ *
89
+ * @param listMd1 First set.
90
+ * @param listMd2 Second set.
91
+ * @param <T> Metadata subtype.
92
+ * @return true if both sets match element-wise, false otherwise.
93
+ */
94
+ public <T extends Metadata> boolean match(Set<T> listMd1, Set<T> listMd2) {
95
+ if (listMd1.size() != listMd2.size()) {
96
+ return false;
97
+ }
98
+ return listMd1.parallelStream().allMatch(m -> this.match(m, findByKey(m.getKey(), listMd2)));
99
+ }
49100
50
- public <T extends Metadata, K extends Metadata> void mergeMetadata(EntityManager em, Set<T> srcListMd, Set<K> tgtListMd, Set<String> keys) {
101
+ /**
102
+ * mergeMetadata
103
+ * <p>
104
+ * Merge metadata from a source set (truth) into a target set.
105
+ * - Removes entries in target whose keys are not in {@code keys}.
106
+ * - Updates entries in target whose value/mandatory differ from source.
107
+ * - Does NOT create new entries; caller is expected to persist new ones separately.
108
+ *
109
+ * @param em EntityManager to remove/merge.
110
+ * @param srcListMd Source metadata set (truth).
111
+ * @param tgtListMd Target metadata set to update.
112
+ * @param keys Keys present in source.
113
+ * @param <T> Source metadata type.
114
+ * @param <K> Target metadata type.
115
+ */
116
+ public <T extends Metadata, K extends Metadata> void mergeMetadata(EntityManager em, Set<T> srcListMd, Set<K> tgtListMd, Set<String> keys) {
51117
52
- Set<K> mdToRemove = tgtListMd.parallelStream() //
53
- .filter(md -> !keys.contains(md.getKey())) //
54
- .collect(Collectors.toSet());
55
- for (K tgtMd : mdToRemove) {
56
- log.info("MD key to remove: {} - {}", tgtMd.getKey(), tgtMd);
57
- if (tgtMd instanceof LicenseTypeMetadata) {
58
- log.info("LT: {}, tx: {}, contans: {}", LicenseTypeMetadata.class.cast(tgtMd).getLicenseType(), em.isJoinedToTransaction(), em.contains(tgtMd));
59
- }
60
- em.remove(tgtMd);
61
- }
62
- Set<K> keysToUpdate = tgtListMd.parallelStream() //
63
- .filter(md -> keys.contains(md.getKey())) //
64
- .collect(Collectors.toSet());
65
- for (K tgtMd : keysToUpdate) {
66
- Metadata md = this.findByKey(tgtMd.getKey(), srcListMd);
67
- if (md.isMandatory() != tgtMd.isMandatory() || !Objects.equals(md.getValue(), tgtMd.getValue())) {
68
- tgtMd.setMandatory(md.isMandatory());
69
- tgtMd.setValue(md.getValue());
70
- log.info("MD key to update: {}", tgtMd.getKey());
71
- em.merge(tgtMd);
72
- }
73
- }
74
- }
118
+ // Remove missing keys
119
+ Set<K> mdToRemove = tgtListMd.parallelStream()
120
+ .filter(md -> !keys.contains(md.getKey()))
121
+ .collect(Collectors.toSet());
122
+ for (K tgtMd : mdToRemove) {
123
+ log.info("MD key to remove: {} - {}", tgtMd.getKey(), tgtMd);
124
+ if (tgtMd instanceof LicenseTypeMetadata) {
125
+ log.info("LT: {}, tx: {}, contans: {}", LicenseTypeMetadata.class.cast(tgtMd).getLicenseType(), em.isJoinedToTransaction(), em.contains(tgtMd));
126
+ }
127
+ em.remove(tgtMd);
128
+ }
75129
76
- private Set<LicenseTypeMetadata> createNewMetadata(Set<ApplicationMetadata> appMd, Set<LicenseTypeMetadata> existingMd, LicenseType licenseType) {
77
- Set<String> oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet());
78
- return appMd.parallelStream() //
79
- .filter(md -> !oldKeys.contains(md.getKey())) //
80
- .map(appmd -> {
81
- LicenseTypeMetadata ltmd = new LicenseTypeMetadata();
82
- ltmd.setLicenseType(licenseType);
83
- ltmd.setKey(appmd.getKey());
84
- ltmd.setValue(appmd.getValue());
85
- ltmd.setMandatory(appmd.isMandatory());
86
- return ltmd;
87
- }).collect(Collectors.toSet());
88
- }
130
+ // Update changed keys
131
+ Set<K> keysToUpdate = tgtListMd.parallelStream()
132
+ .filter(md -> keys.contains(md.getKey()))
133
+ .collect(Collectors.toSet());
134
+ for (K tgtMd : keysToUpdate) {
135
+ Metadata md = this.findByKey(tgtMd.getKey(), srcListMd);
136
+ if (md.isMandatory() != tgtMd.isMandatory() || !Objects.equals(md.getValue(), tgtMd.getValue())) {
137
+ tgtMd.setMandatory(md.isMandatory());
138
+ tgtMd.setValue(md.getValue());
139
+ log.info("MD key to update: {}", tgtMd.getKey());
140
+ em.merge(tgtMd);
141
+ }
142
+ }
143
+ }
89144
90
- private Set<PackMetadata> createNewMetadata(Set<LicenseTypeMetadata> ltMd, Set<PackMetadata> existingMd, Pack pack) {
91
- Set<String> oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet());
92
- return ltMd.parallelStream() //
93
- .filter(md -> !oldKeys.contains(md.getKey())) //
94
- .map(md -> {
95
- PackMetadata pmd = new PackMetadata();
96
- pmd.setPack(pack);
97
- pmd.setKey(md.getKey());
98
- pmd.setValue(md.getValue());
99
- pmd.setMandatory(md.isMandatory());
100
- return pmd;
101
- }).collect(Collectors.toSet());
102
- }
145
+ // -- Internal helpers to create new metadata rows when propagating
103146
104
- /**
105
- * Copy the modified app metadata to LicenseTypes and Packs
106
- *
107
- * @param em
108
- * @param app
109
- */
110
- public void propagateMetadata(EntityManager em, Application app) {
111
- Set<ApplicationMetadata> appMd = app.getApplicationMetadata();
112
- Set<String> keys = appMd.parallelStream().map(md -> md.getKey()).collect(Collectors.toSet());
113
- for (LicenseType lt : app.getLicenseTypes()) {
114
- log.info("Lic type to update: {}", lt.getCode());
115
- this.mergeMetadata(em, appMd, lt.getMetadata(), keys);
116
- Set<LicenseTypeMetadata> newMdList = createNewMetadata(appMd, lt.getMetadata(), lt);
117
- for (LicenseTypeMetadata newMetadata : newMdList) {
118
- em.persist(newMetadata);
119
- }
120
- em.detach(lt);
121
- // Probably there is a better way to get the final metadata from JPA...
122
- TypedQuery<LicenseTypeMetadata> updatedMdQuery = em.createNamedQuery("list-licensetype-metadata", LicenseTypeMetadata.class);
123
- updatedMdQuery.setParameter("licenseTypeId", lt.getId());
124
- Set<LicenseTypeMetadata> updatedMd = new HashSet<>(updatedMdQuery.getResultList());
147
+ /**
148
+ * createNewMetadata<p>
149
+ * Create new metadata
150
+ *
151
+ * @param appMd
152
+ * @param existingMd
153
+ * @param licenseType
154
+ * @return newMetadata
155
+ */
156
+ private Set<LicenseTypeMetadata> createNewMetadata(Set<ApplicationMetadata> appMd, Set<LicenseTypeMetadata> existingMd, LicenseType licenseType) {
157
+ Set<String> oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet());
158
+ return appMd.parallelStream()
159
+ .filter(md -> !oldKeys.contains(md.getKey()))
160
+ .map(appmd -> {
161
+ LicenseTypeMetadata ltmd = new LicenseTypeMetadata();
162
+ ltmd.setLicenseType(licenseType);
163
+ ltmd.setKey(appmd.getKey());
164
+ ltmd.setValue(appmd.getValue());
165
+ ltmd.setMandatory(appmd.isMandatory());
166
+ return ltmd;
167
+ }).collect(Collectors.toSet());
168
+ }
125169
126
- lt.setMetadata(updatedMd);
127
- propagateMetadata(em, lt, keys);
128
- }
129
- }
170
+ /**
171
+ * createNewMetadata<p>
172
+ * Create the new metadata
173
+ *
174
+ * @param ltMd
175
+ * @param existingMd
176
+ * @param pack
177
+ * @return newMetadata
178
+ */
179
+ private Set<PackMetadata> createNewMetadata(Set<LicenseTypeMetadata> ltMd, Set<PackMetadata> existingMd, Pack pack) {
180
+ Set<String> oldKeys = existingMd.stream().map(md -> md.getKey()).collect(Collectors.toSet());
181
+ return ltMd.parallelStream()
182
+ .filter(md -> !oldKeys.contains(md.getKey()))
183
+ .map(md -> {
184
+ PackMetadata pmd = new PackMetadata();
185
+ pmd.setPack(pack);
186
+ pmd.setKey(md.getKey());
187
+ pmd.setValue(md.getValue());
188
+ pmd.setMandatory(md.isMandatory());
189
+ return pmd;
190
+ }).collect(Collectors.toSet());
191
+ }
130192
131
- /**
132
- * Copy the modified licenseType metadata to Packs
133
- *
134
- * @param em
135
- * @param lt
136
- * @param keys
137
- */
138
- public void propagateMetadata(EntityManager em, LicenseType lt, Set<String> keys) {
139
- Set<LicenseTypeMetadata> ltMd = lt.getMetadata();
140
- TypedQuery<Pack> packsQuery = em.createNamedQuery("list-packs-by-lic-type", Pack.class);
141
- packsQuery.setParameter("lt_id", lt.getId());
142
- List<Pack> packs = packsQuery.getResultList();
143
- log.info("Packs to update the metadata: {}", packs.size());
144
- for (Pack pack : packs) {
145
- if (pack.isFrozen()) {
146
- log.warn("Metadata in LicenseType {} has changed but the Pack {} is frozen and won't be updated.", lt.getCode(), pack.getCode());
147
- continue;
148
- }
149
- this.mergeMetadata(em, ltMd, pack.getMetadata(), keys);
150
- Set<PackMetadata> newMdList = createNewMetadata(ltMd, pack.getMetadata(), pack);
151
- for (PackMetadata newMetadata : newMdList) {
152
- em.persist(newMetadata);
153
- }
154
- markObsoleteMetadata(em, pack);
155
- em.detach(pack);
156
- }
157
- }
193
+ /**
194
+ * propagateMetadata (Application -> LicenseTypes -> Packs)
195
+ * <p>
196
+ * Propagates application metadata changes down to all its license types and packs:
197
+ * - mergeMetadata on LicenseType
198
+ * - create new LicenseTypeMetadata for new keys
199
+ * - re-fetch LT metadata (detached/merged semantics)
200
+ * - propagateMetadata(LicenseType) to packs
201
+ *
202
+ * @param em EntityManager.
203
+ * @param app Application with updated metadata loaded.
204
+ */
205
+ public void propagateMetadata(EntityManager em, Application app) {
206
+ Set<ApplicationMetadata> appMd = app.getApplicationMetadata();
207
+ Set<String> keys = appMd.parallelStream().map(md -> md.getKey()).collect(Collectors.toSet());
208
+ for (LicenseType lt : app.getLicenseTypes()) {
209
+ log.info("Lic type to update: {}", lt.getCode());
210
+ this.mergeMetadata(em, appMd, lt.getMetadata(), keys);
211
+ Set<LicenseTypeMetadata> newMdList = createNewMetadata(appMd, lt.getMetadata(), lt);
212
+ for (LicenseTypeMetadata newMetadata : newMdList) {
213
+ em.persist(newMetadata);
214
+ }
215
+ em.detach(lt);
158216
159
- public void markObsoleteMetadata(EntityManager em, Pack pack) {
160
- TypedQuery<License> existingPackLicenses = em.createNamedQuery("list-licenses-by-pack", License.class);
161
- existingPackLicenses.setParameter("packId", pack.getId());
162
- for (License lic : existingPackLicenses.getResultList()) {
163
- log.info("License from pack: {}, status: {}", lic.getCode(), lic.getStatus());
164
- if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE || lic.getStatus() == LicenseStatus.CANCELLED) {
165
- lic.setMetadataObsolete(true);
166
- em.merge(lic);
167
- }
168
- }
169
- }
217
+ // Re-read updated metadata
218
+ TypedQuery<LicenseTypeMetadata> updatedMdQuery = em.createNamedQuery("list-licensetype-metadata", LicenseTypeMetadata.class);
219
+ updatedMdQuery.setParameter("licenseTypeId", lt.getId());
220
+ Set<LicenseTypeMetadata> updatedMd = new HashSet<>(updatedMdQuery.getResultList());
221
+
222
+ lt.setMetadata(updatedMd);
223
+ propagateMetadata(em, lt, keys);
224
+ }
225
+ }
226
+
227
+ /**
228
+ * propagateMetadata (LicenseType -> Packs)
229
+ * <p>
230
+ * Propagates license type metadata changes to all its packs:
231
+ * - mergeMetadata on Pack
232
+ * - create new PackMetadata for new keys
233
+ * - markObsoleteMetadata on packs to flag their licenses
234
+ *
235
+ * Frozen packs are skipped.
236
+ *
237
+ * @param em EntityManager.
238
+ * @param lt LicenseType with updated metadata set.
239
+ * @param keys Set of keys present in the source.
240
+ */
241
+ public void propagateMetadata(EntityManager em, LicenseType lt, Set<String> keys) {
242
+ Set<LicenseTypeMetadata> ltMd = lt.getMetadata();
243
+ TypedQuery<Pack> packsQuery = em.createNamedQuery("list-packs-by-lic-type", Pack.class);
244
+ packsQuery.setParameter("lt_id", lt.getId());
245
+ List<Pack> packs = packsQuery.getResultList();
246
+ log.info("Packs to update the metadata: {}", packs.size());
247
+ for (Pack pack : packs) {
248
+ if (pack.isFrozen()) {
249
+ log.warn("Metadata in LicenseType {} has changed but the Pack {} is frozen and won't be updated.", lt.getCode(), pack.getCode());
250
+ continue;
251
+ }
252
+ this.mergeMetadata(em, ltMd, pack.getMetadata(), keys);
253
+ Set<PackMetadata> newMdList = createNewMetadata(ltMd, pack.getMetadata(), pack);
254
+ for (PackMetadata newMetadata : newMdList) {
255
+ em.persist(newMetadata);
256
+ }
257
+ markObsoleteMetadata(em, pack);
258
+ em.detach(pack);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * markObsoleteMetadata
264
+ * <p>
265
+ * For all licenses within the given pack, mark {@code metadataObsolete = true}
266
+ * if the license is in a relevant state (ACTIVE, PRE_ACTIVE, CANCELLED).
267
+ * This lets clients know that metadata-dependent artifacts might need refresh.
268
+ *
269
+ * @param em EntityManager.
270
+ * @param pack Pack whose licenses to mark.
271
+ */
272
+ public void markObsoleteMetadata(EntityManager em, Pack pack) {
273
+ TypedQuery<License> existingPackLicenses = em.createNamedQuery("list-licenses-by-pack", License.class);
274
+ existingPackLicenses.setParameter("packId", pack.getId());
275
+ for (License lic : existingPackLicenses.getResultList()) {
276
+ log.info("License from pack: {}, status: {}", lic.getCode(), lic.getStatus());
277
+ if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE || lic.getStatus() == LicenseStatus.CANCELLED) {
278
+ lic.setMetadataObsolete(true);
279
+ em.merge(lic);
280
+ }
281
+ }
282
+ }
170283 }
284
+