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
/*
 * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
 */
package net.curisit.securis.services.helpers;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.curisit.integrity.exception.CurisRuntimeException;
import net.curisit.securis.beans.LicenseBean;
import net.curisit.securis.db.License;
import net.curisit.securis.db.LicenseHistory;
import net.curisit.securis.db.LicenseStatus;
import net.curisit.securis.db.Pack;
import net.curisit.securis.db.PackMetadata;
import net.curisit.securis.db.User;
import net.curisit.securis.security.BasicSecurityContext;
import net.curisit.securis.services.exception.SeCurisServiceException;
import net.curisit.securis.services.exception.SeCurisServiceException.ErrorCodes;
/**
 * LicenseHelper
 * <p>
 * Stateless utility component for license lifecycle operations and helpers:
 * - cancelation with history auditing
 * - license resolution and validity checks
 * - license file generation in a temp directory
 * - metadata extraction and expiration date computation from packs
 * - sequential code suffix allocation per pack
 *
 * Thread-safety: ApplicationScoped, stateless.
 * 
 * @author JRA
 * Last reviewed by JRA on Oct 5, 2025.
 */
@ApplicationScoped
public class LicenseHelper {
    @SuppressWarnings("unused")
    private static final Logger LOG = LogManager.getLogger(LicenseHelper.class);
    /** Milliseconds per day (used to derive expiration dates). */
    private static final long MS_PER_DAY = 24L * 3600L * 1000L;
    @Inject private UserHelper userHelper;
    /**
     * cancelLicense
     * <p>
     * Transitions a license to CANCELLED, records who canceled it and why,
     * and appends a {@link LicenseHistory} entry.
     *
     * @param lic  Target license (managed).
     * @param reason Human-readable cancellation reason (auditable).
     * @param bsc Current security context (used to identify the user).
     * @param em  Entity manager to persist changes.
     * @throws SeCurisServiceException never thrown here, declared for symmetry with callers.
     */
    public void cancelLicense(License lic, String reason, BasicSecurityContext bsc, EntityManager em) throws SeCurisServiceException {
        lic.setStatus(LicenseStatus.CANCELLED);
        lic.setCancelledById(bsc.getUserPrincipal().getName());
        lic.setModificationTimestamp(new Date());
        em.persist(lic);
        em.persist(createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.CANCEL, "Cancellation reason: " + reason));
    }
    /**
     * getActiveLicenseFromDB
     * <p>
     * Resolve license by code and verify that it's ACTIVE or PRE_ACTIVE.
     *
     * @param licBean License bean containing the code to check.
     * @param em      EntityManager for DB access.
     * @return The managed {@link License} instance.
     * @throws SeCurisServiceException if code not found or license not in an active-ish state.
     */
    public License getActiveLicenseFromDB(LicenseBean licBean, EntityManager em) throws SeCurisServiceException {
        License lic = License.findLicenseByCode(licBean.getLicenseCode(), em);
        if (lic == null) {
            throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license code doesn't exist");
        }
        if (lic.getStatus() != LicenseStatus.ACTIVE && lic.getStatus() != LicenseStatus.PRE_ACTIVE) {
            throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license in not active");
        }
        return lic;
    }
    /**
     * createLicenseHistoryAction
     * <p>
     * Helper to build a {@link LicenseHistory} entry.
     *
     * @param lic      License affected.
     * @param user     User performing the action.
     * @param action   Action code (see {@link LicenseHistory.Actions}).
     * @param comments Optional comments, can be null.
     * @return transient {@link LicenseHistory} ready to persist.
     */
    public LicenseHistory createLicenseHistoryAction(License lic, User user, String action, String comments) {
        LicenseHistory lh = new LicenseHistory();
        lh.setLicense(lic);
        lh.setUser(user);
        lh.setCreationTimestamp(new Date());
        lh.setAction(action);
        lh.setComments(comments);
        return lh;
    }
    /**
     * createLicenseHistoryAction
     * <p>
     * Overload without comments.
     *
     * @param lic    License affected.
     * @param user   User performing the action.
     * @param action Action code.
     * @return transient {@link LicenseHistory}.
     */
    public LicenseHistory createLicenseHistoryAction(License lic, User user, String action) {
        return createLicenseHistoryAction(lic, user, action, null);
    }
    /**
     * createTemporaryLicenseFile
     * <p>
     * Materializes the license payload into a temporary file for emailing/download.
     * The file is created under a unique temporary directory.
     *
     * Caller is responsible for deleting the file and its parent directory.
     *
     * @param lic         License whose JSON/XML/text payload is in {@code getLicenseData()}.
     * @param licFileName Desired file name (e.g. "license.lic").
     * @return A {@link File} pointing to the newly created file.
     * @throws IOException If the temporary directory or file cannot be created/written.
     */
    public File createTemporaryLicenseFile(License lic, String licFileName) throws IOException {
        File f = Files.createTempDirectory("securis-server").toFile();
        f = new File(f, licFileName);
        FileUtils.writeStringToFile(f, lic.getLicenseData(), StandardCharsets.UTF_8);
        return f;
    }
    /**
     * extractPackMetadata
     * <p>
     * Converts pack metadata set to a map for license generation.
     *
     * @param packMetadata Set of {@link PackMetadata}.
     * @return Map with keys/values copied from metadata entries.
     */
    public Map<String, Object> extractPackMetadata(Set<PackMetadata> packMetadata) {
        Map<String, Object> metadata = new HashMap<>();
        for (PackMetadata md : packMetadata) {
            metadata.put(md.getKey(), md.getValue());
        }
        return metadata;
    }
    /**
     * getExpirationDateFromPack
     * <p>
     * Computes license expiration date depending on action type:
     * - Pre-activation: {@code preactivationValidPeriod} days from now.
     * - Renew/Activation: min(renewValidPeriod days, pack end date - now).
     * Fails fast if pack end date is already in the past.
     *
     * @param pack            Pack with policy data.
     * @param isPreActivation Whether the operation is a pre-activation.
     * @return Calculated expiration {@link Date}.
     * @throws CurisRuntimeException if the pack's end date is in the past.
     */
    public Date getExpirationDateFromPack(Pack pack, boolean isPreActivation) {
        Long validPeriod;
        if (pack.getEndValidDate().before(new Date())) {
            throw new CurisRuntimeException("Pack end valid period is reached, no new licenses can be activated.");
        }
        if (isPreActivation) {
            validPeriod = pack.getPreactivationValidPeriod() * MS_PER_DAY;
        } else {
            if (pack.getRenewValidPeriod() <= 0) {
                return pack.getEndValidDate();
            }
            long renewPeriod = pack.getRenewValidPeriod() * MS_PER_DAY;
            long expirationPeriod = pack.getEndValidDate().getTime() - new Date().getTime();
            validPeriod = renewPeriod < expirationPeriod ? renewPeriod : expirationPeriod;
        }
        Date expirationDate = new Date(new Date().getTime() + validPeriod);
        return expirationDate;
    }
    /**
     * getNextCodeSuffix
     * <p>
     * Retrieves the last used code suffix for a given pack and returns the next one.
     * If none found, returns 1.
     *
     * @param packId Pack identifier.
     * @param em     EntityManager to query the DB.
     * @return Next sequential suffix (>= 1).
     */
    public int getNextCodeSuffix(int packId, EntityManager em) {
        TypedQuery<Integer> query = em.createNamedQuery("last-code-suffix-used-in-pack", Integer.class);
        query.setParameter("packId", packId);
        Integer lastCodeSuffix = query.getSingleResult();
        return lastCodeSuffix == null ? 1 : lastCodeSuffix + 1;
    }
}