/* * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. */ package net.curisit.securis.db; import java.io.Serializable; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.EntityManager; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.NamedQueries; import jakarta.persistence.NamedQuery; import jakarta.persistence.NoResultException; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.persistence.TypedQuery; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.hibernate.annotations.Type; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import net.curisit.integrity.commons.Utils; import net.curisit.securis.db.common.CreationTimestampEntity; import net.curisit.securis.db.common.ModificationTimestampEntity; import net.curisit.securis.db.listeners.CreationTimestampListener; import net.curisit.securis.db.listeners.ModificationTimestampListener; import net.curisit.securis.services.exception.SeCurisServiceException; /** * License *

* Main license entity. Contains identity, ownership, timestamps and payload fields. * Includes convenience JSON properties for related IDs/names. * * Mapping details: * - Table: license * - Listeners: CreationTimestampListener, ModificationTimestampListener * - Named queries: license-by-code, license-by-activation-code, last-code-suffix-used-in-pack, ... * - Status column uses custom Hibernate type: net.curisit.securis.db.common.LicenseStatusType * * @author JRA * Last reviewed by JRA on Oct 5, 2025. */ @JsonAutoDetect @JsonInclude(Include.NON_NULL) @Entity @EntityListeners({ CreationTimestampListener.class, ModificationTimestampListener.class }) @Table(name = "license") @JsonIgnoreProperties(ignoreUnknown = true) @NamedQueries({ @NamedQuery(name = "license-by-code", query = "SELECT l FROM License l where l.code = :code"), @NamedQuery(name = "license-by-activation-code", query = "SELECT l FROM License l where l.activationCode = :activationCode"), @NamedQuery(name = "last-code-suffix-used-in-pack", query = "SELECT max(l.codeSuffix) FROM License l where l.pack.id = :packId"), @NamedQuery(name = "list-licenses-by-pack", query = "SELECT l FROM License l where l.pack.id = :packId"), @NamedQuery(name = "list-licenses-by-req-data", query = "SELECT l FROM License l where l.reqDataHash = :hash"), @NamedQuery(name = "list-active-licenses-by-req-data", query = "SELECT l FROM License l where l.reqDataHash = :hash and l.status in ('AC', 'PA')"), @NamedQuery(name = "list-valid-licenses-by-req-data", query = "SELECT l FROM License l where l.reqDataHash = :hash and l.status in ('RE', 'AC', 'PA')") }) public class License implements CreationTimestampEntity, ModificationTimestampEntity, Serializable { private static final long serialVersionUID = 2700310404904877227L; private static final Logger LOG = LogManager.getLogger(License.class); // ------------------------------------------------------------------ // Columns & relations // ------------------------------------------------------------------ @Id @GeneratedValue private Integer id; private String code; @Column(name = "metadata_obsolete") @JsonProperty("metadata_obsolete") private Boolean metadataObsolete; @Column(name = "activation_code") @JsonProperty("activation_code") private String activationCode; @Column(name = "code_suffix") @JsonProperty("code_suffix") private Integer codeSuffix; @JsonIgnore @ManyToOne @JoinColumn(name = "pack_id") private Pack pack; @JsonIgnore @ManyToOne @JoinColumn(name = "created_by") private User createdBy; @JsonIgnore @ManyToOne @JoinColumn(name = "cancelled_by") private User cancelledBy; @Type(type = "net.curisit.securis.db.common.LicenseStatusType") private LicenseStatus status; @Column(name = "full_name") @JsonProperty("full_name") private String fullName; private String email; @Column(name = "request_data") @JsonProperty("request_data") private String requestData; /** * Request data hash (not serialized). Automatically updated by setRequestData(). */ @Column(name = "request_data_hash") @JsonIgnore private String reqDataHash; @Column(name = "license_data") @JsonProperty("license_data") @JsonIgnore // License data is delivered separately (e.g., file download). Not sent in list views. private String licenseData; @Column(name = "creation_timestamp") @JsonProperty("creation_timestamp") private Date creationTimestamp; @Column(name = "modification_timestamp") @JsonProperty("modification_timestamp") private Date modificationTimestamp; @Column(name = "last_access_timestamp") @JsonProperty("last_access_timestamp") private Date lastAccessTimestamp; @Column(name = "expiration_date") @JsonProperty("expiration_date") private Date expirationDate; private String comments; @OneToMany(fetch = FetchType.LAZY, mappedBy = "license") @JsonIgnore private List history; // ------------------------------------------------------------------ // Basic accessors // ------------------------------------------------------------------ /** * getId

* Return primary key. * * @return id */ public Integer getId() { return id; } /** * getCode

* Return human-readable license code. * * @return code */ public String getCode() { return code; } /** * setCode

* Set human-readable license code. * * @param code */ public void setCode(String code) { this.code = code; } /** * getCreationTimestamp

* Required by CreationTimestampEntity. * * @return creationTimestamp */ @Override public Date getCreationTimestamp() { return creationTimestamp; } /** * setCreationTimestamp

* Set creation timestamp. * * @param creationTimestamp */ @Override public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; } /** * getCreatedBy

* Return creator user (entity). * * @return user */ public User getCreatedBy() { return createdBy; } /** * setCreatedBy

* Set creator user (entity). * * @param user */ public void setCreatedBy(User createdBy) { this.createdBy = createdBy; } /** * getPack

* Return owning pack. * * @return pack */ public Pack getPack() { return pack; } /** * setPack

* Set owning pack. * * @param pack */ public void setPack(Pack pack) { this.pack = pack; } /** * getCreatedById

* Expose creator username as JSON. * * @return username */ @JsonProperty("created_by_id") public String getCreatedById() { return createdBy == null ? null : createdBy.getUsername(); } /** * setCreatedById

* Setter by username for JSON binding. * * @param username */ @JsonProperty("created_by_id") public void setCreatedById(String username) { if (username == null) { createdBy = null; } else { createdBy = new User(); createdBy.setUsername(username); } } /** * getCancelledById

* Expose cancelling user username as JSON. * * @return username */ @JsonProperty("cancelled_by_id") public String getCancelledById() { return cancelledBy == null ? null : cancelledBy.getUsername(); } /** * setCancelledById

* Setter by username for JSON binding. * * @param username */ @JsonProperty("cancelled_by_id") public void setCancelledById(String username) { if (username == null) { cancelledBy = null; } else { cancelledBy = new User(); cancelledBy.setUsername(username); } } /** * getPackCode

* Expose pack code for convenience. * * @return packCode */ @JsonProperty("pack_code") public String getPackCode() { return pack == null ? null : pack.getCode(); } /** * getPackId

* Expose pack id for convenience. * * @return packId */ @JsonProperty("pack_id") public Integer getPackId() { return pack == null ? null : pack.getId(); } /** * setPackId

* Setter by id for JSON binding (creates a shallow Pack). * * @param packId */ @JsonProperty("pack_id") public void setPackId(Integer idPack) { if (idPack == null) { pack = null; } else { pack = new Pack(); pack.setId(idPack); } } /** * getStatus

* Return license status. * * @return licenseStatus */ public LicenseStatus getStatus() { return status; } /** * setStatus

* Set license status. * * @param status */ public void setStatus(LicenseStatus status) { this.status = status; } /** * getModificationTimestamp

* Required by ModificationTimestampEntity. * * @return modificationTimestamp */ @Override public Date getModificationTimestamp() { return modificationTimestamp; } /** * setModificationTimestamp

* Set modification timestamp. * * @param modificationTimestamp */ @Override public void setModificationTimestamp(Date modificationTimestamp) { this.modificationTimestamp = modificationTimestamp; } /** * getFullName

* Return license holder full name. * * @return name */ public String getFullName() { return fullName; } /** * setFullName

* Set license holder full name. * * @param name */ public void setFullName(String fullName) { this.fullName = fullName; } /** * getEmail

* Return email address. * * @return email */ public String getEmail() { return email; } /** * setEmail

* Set email address. * * @param email */ public void setEmail(String email) { this.email = email; } /** * setId

* Set primary key (rarely used). * * @param id */ public void setId(Integer id) { this.id = id; } /** * getCancelledBy

* Return cancelling user (entity). * * @param user */ public User getCancelledBy() { return cancelledBy; } /** * setCancelledBy

* Set cancelling user (entity). * * @param cancelledBy */ public void setCancelledBy(User cancelledBy) { this.cancelledBy = cancelledBy; } /** * getLastAccessTimestamp

* Return last access timestamp. * * @return lastAccessTimestamp */ public Date getLastAccessTimestamp() { return lastAccessTimestamp; } /** * setLastAccessTimestamp

* Set last access timestamp. * * @param lastAccessTimestamp */ public void setLastAccessTimestamp(Date lastAccessTimestamp) { this.lastAccessTimestamp = lastAccessTimestamp; } /** * getRequestData

* Return raw request data. * * @return requestData */ public String getRequestData() { return requestData; } /** * setRequestData

* Set raw request data and recompute {@link #reqDataHash} immediately using * the same hashing strategy as BlockedRequest (SHA-256). * * @param requestData */ public void setRequestData(String requestData) { this.requestData = requestData; this.reqDataHash = BlockedRequest.generateHash(this.requestData); } /** * getLicenseData

* Return opaque license data (not serialized in lists). * * @return licenseData */ public String getLicenseData() { return licenseData; } /** * setLicenseData

* Set opaque license data (large content kept server-side). * * @param licenseDate */ public void setLicenseData(String licenseData) { this.licenseData = licenseData; } /** * getComments

* Return optional comments. * * @return comments */ public String getComments() { return comments; } /** * setComments

* Set optional comments. * * @param comments */ public void setComments(String comments) { this.comments = comments; } /** * getHistory

* Return change history entries (lazy). * * @return history */ public List getHistory() { return history; } /** * setHistory

* Set change history entries. * * @param history */ public void setHistory(List history) { this.history = history; } /** * getExpirationDate

* Return expiration date (nullable). * * @return expirationDate */ public Date getExpirationDate() { return expirationDate; } /** * setExpirationDate

* Set expiration date (nullable). * * @param expirationDate */ public void setExpirationDate(Date expirationDate) { this.expirationDate = expirationDate; } /** * getReqDataHash

* Return cached hash of request data (not exposed in JSON). * * @return reqDataHash */ public String getReqDataHash() { return reqDataHash; } /** * getCodeSuffix

* Return numeric suffix of the code. * * @return codeSuffix */ public Integer getCodeSuffix() { return codeSuffix; } /** * setCodeSuffix

* Set numeric suffix of the code. * * @param codeSuffix */ public void setCodeSuffix(Integer codeSuffix) { this.codeSuffix = codeSuffix; } /** * getActivationCode

* Return activation code. * * @return activationCode */ public String getActivationCode() { return activationCode; } /** * setActivationCode

* Set activation code. * * @param activationCode */ public void setActivationCode(String activationCode) { this.activationCode = activationCode; } /** * isMetadataObsolete

* Convenience Boolean → primitive with null-safe false. * * @return isMetadataObsolete */ public boolean isMetadataObsolete() { return metadataObsolete != null && metadataObsolete; } /** * setMetadataObsolete

* Set metadata obsolete flag (nullable wrapper). * * @param obsolete */ public void setMetadataObsolete(Boolean obsolete) { this.metadataObsolete = obsolete; } // ------------------------------------------------------------------ // Status transitions helpers // ------------------------------------------------------------------ /** * Action

* Actions to take with the license */ public static class Action { public static final int CREATE = 1; public static final int REQUEST = 2; public static final int ACTIVATION = 3; public static final int SEND = 4; public static final int DOWNLOAD = 5; public static final int CANCEL = 6; public static final int DELETE = 7; public static final int BLOCK = 8; public static final int UNBLOCK = 9; } /** * Status

* Status of the requested license */ public static class Status { private static final Map> transitions = Utils.createMap( Action.REQUEST, Arrays.asList(LicenseStatus.CREATED, LicenseStatus.REQUESTED), Action.ACTIVATION,Arrays.asList(LicenseStatus.CREATED, LicenseStatus.REQUESTED, LicenseStatus.PRE_ACTIVE, LicenseStatus.EXPIRED), Action.SEND, Arrays.asList(LicenseStatus.ACTIVE, LicenseStatus.PRE_ACTIVE), Action.DOWNLOAD, Arrays.asList(LicenseStatus.ACTIVE, LicenseStatus.PRE_ACTIVE), Action.CANCEL, Arrays.asList(LicenseStatus.ACTIVE, LicenseStatus.PRE_ACTIVE, LicenseStatus.REQUESTED, LicenseStatus.EXPIRED), Action.DELETE, Arrays.asList(LicenseStatus.CANCELLED, LicenseStatus.CREATED, LicenseStatus.BLOCKED), Action.UNBLOCK, Arrays.asList(LicenseStatus.BLOCKED), Action.BLOCK, Arrays.asList(LicenseStatus.CANCELLED) ); /** * isActionValid

* Check whether an action is valid given the current license status. * * @param action action constant from {@link Action} * @param currentStatus current license status * @return true if allowed */ public static boolean isActionValid(Integer action, LicenseStatus currentStatus) { List validStatuses = transitions.get(action); LOG.info("Action {} is valid ? => {} current: {} OK? {}", action, validStatuses, currentStatus, validStatuses != null && validStatuses.contains(currentStatus)); return validStatuses != null && validStatuses.contains(currentStatus); } } // ------------------------------------------------------------------ // Repository helpers (static queries) // ------------------------------------------------------------------ /** * findValidLicenseByRequestData

* Return the first license in statuses RE/AC/PA for the given request data hash. * * @param requestData raw request data * @param em entity manager * @return matching license or null * @throws SeCurisServiceException */ public static License findValidLicenseByRequestData(String requestData, EntityManager em) throws SeCurisServiceException { TypedQuery query = em.createNamedQuery("list-valid-licenses-by-req-data", License.class); query.setParameter("hash", BlockedRequest.generateHash(requestData)); try { List list = query.getResultList(); if (list.size() == 0) return null; if (list.size() > 1) { LOG.error("There are more than 1 active or requested license for request data: {}\nHash: {}", requestData, BlockedRequest.generateHash(requestData)); } return list.get(0); } catch (NoResultException e) { return null; } } /** * findLicenseByActivationCode

* Retrieve a license by its activation code. * * @param activationCode * @param entityManager * @return license * @throws SeCurisServiceException */ public static License findLicenseByActivationCode(String activationCode, EntityManager em) throws SeCurisServiceException { TypedQuery query = em.createNamedQuery("license-by-activation-code", License.class); query.setParameter("activationCode", activationCode); try { return query.getSingleResult(); } catch (NoResultException e) { return null; } } /** * findActiveLicenseByRequestData

* Return the first AC/PA license for a given requestData. * * @param requestData * @param entityManager * @return license * @throws SeCurisServiceException */ public static License findActiveLicenseByRequestData(String requestData, EntityManager em) throws SeCurisServiceException { TypedQuery query = em.createNamedQuery("list-active-licenses-by-req-data", License.class); query.setParameter("hash", BlockedRequest.generateHash(requestData)); try { List list = query.getResultList(); if (list.size() == 0) return null; if (list.size() > 1) { LOG.error("There are more than 1 active license for request data: {}\nHash: {}", requestData, BlockedRequest.generateHash(requestData)); } return list.get(0); } catch (NoResultException e) { return null; } } /** * findLicenseByCode

* Retrieve a license by human-readable code. * * @param code * @param entityManager * @return license * @throws SeCurisServiceException */ public static License findLicenseByCode(String code, EntityManager em) throws SeCurisServiceException { TypedQuery query = em.createNamedQuery("license-by-code", License.class); query.setParameter("code", code); try { return query.getSingleResult(); } catch (NoResultException e) { return null; } } }