From 146a0fb8b0e90f9196e569152f649baf60d6cc8f Mon Sep 17 00:00:00 2001
From: Joaquín Reñé <jrene@curisit.net>
Date: Tue, 07 Oct 2025 14:52:57 +0000
Subject: [PATCH] #4410 - Comments on classes
---
securis/src/main/java/net/curisit/securis/GzipFilter.java | 21
securis/src/main/java/net/curisit/securis/RestServicesApplication.java | 17
securis/src/main/java/net/curisit/securis/db/common/Metadata.java | 68
securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java | 113
securis/src/main/java/net/curisit/securis/db/BlockedRequest.java | 123
securis/src/main/java/net/curisit/securis/db/User.java | 586 +-
securis/src/main/java/net/curisit/securis/db/Application.java | 167
securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java | 18
securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java | 179
securis/src/main/java/net/curisit/securis/services/ApplicationResource.java | 405 +
securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java | 25
securis/src/main/java/net/curisit/securis/utils/CacheTTL.java | 248 +
securis/src/main/java/net/curisit/securis/db/LicenseStatus.java | 67
securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java | 20
securis/src/main/java/net/curisit/securis/services/LicenseResource.java | 549 +-
securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java | 29
securis/src/main/java/net/curisit/securis/LicenseGenerator.java | 68
securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java | 24
securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java | 298 +
securis/src/main/java/net/curisit/securis/AuthFilter.java | 80
securis/src/main/java/net/curisit/securis/services/BasicServices.java | 189
securis/src/main/java/net/curisit/securis/db/Pack.java | 822 ++-
securis/src/main/java/net/curisit/securis/services/PackResource.java | 407 +
securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java | 33
securis/src/main/java/net/curisit/securis/db/License.java | 940 +++-
securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java | 34
securis/src/main/java/net/curisit/securis/db/Organization.java | 340 +
securis/src/main/java/net/curisit/securis/services/UserResource.java | 643 ++-
securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java | 27
securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java | 112
securis/src/main/java/net/curisit/securis/beans/User.java | 11
securis/src/main/java/net/curisit/securis/utils/EmailManager.java | 217
securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java | 24
securis/src/main/java/net/curisit/securis/services/ApiResource.java | 840 ++--
securis/src/main/java/net/curisit/securis/db/PackStatus.java | 61
securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java | 372 +
securis/src/main/java/net/curisit/securis/db/common/SystemParams.java | 190
securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java | 215
securis/src/main/java/net/curisit/securis/utils/Config.java | 186
securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java | 146
securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java | 47
securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java | 287
securis/src/main/java/net/curisit/securis/utils/TokenHelper.java | 187
securis/src/main/java/net/curisit/securis/db/Settings.java | 105
securis/src/main/java/net/curisit/securis/security/Securable.java | 24
securis/src/main/java/net/curisit/securis/db/LicenseHistory.java | 158
securis/src/main/java/net/curisit/securis/db/LicenseType.java | 302 +
securis/src/main/java/net/curisit/securis/db/PackMetadata.java | 246
securis/src/main/java/net/curisit/securis/services/OrganizationResource.java | 186
securis/src/main/resources/META-INF/persistence.xml | 2
securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java | 31
securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java | 22
securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java | 19
securis/src/main/java/net/curisit/securis/DevFilter.java | 29
securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java | 175
securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java | 257
securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java | 111
57 files changed, 7,451 insertions(+), 3,651 deletions(-)
diff --git a/securis/src/main/java/net/curisit/securis/AuthFilter.java b/securis/src/main/java/net/curisit/securis/AuthFilter.java
index 48acee4..28a2c7f 100644
--- a/securis/src/main/java/net/curisit/securis/AuthFilter.java
+++ b/securis/src/main/java/net/curisit/securis/AuthFilter.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis;
import java.io.IOException;
@@ -17,16 +20,63 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+/**
+* AuthFilter
+* <p>
+* Simple authentication/role wrapper for development and lightweight scenarios.
+* If a request parameter <code>user</code> or a session attribute <code>user</code>
+* is present, this filter wraps the current request with a custom {@link Principal}
+* and an ad-hoc role. The role assignment is temporary and follows the rule:
+* <ul>
+* <li>user == "advance" → role "advance"</li>
+* <li>otherwise → role "normal"</li>
+* </ul>
+* If no user is present, the request continues unmodified.
+*
+* <p><b>Security note:</b> This filter trusts a user name coming from a request parameter,
+* which must not be used in production. Replace with a proper authentication mechanism
+* (e.g., JWT, container security, SSO) and derive roles from authoritative claims.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 6, 2025.
+*/
@ApplicationScoped
@WebFilter(urlPatterns = "/*")
public class AuthFilter implements Filter {
private static final Logger LOG = LogManager.getLogger(AuthFilter.class);
+ // ---------------------------------------------------------------------
+ // Lifecycle
+ // ---------------------------------------------------------------------
+
+ /**
+ * init<p>
+ * Filter initialization hook (unused).
+ */
@Override
public void init(FilterConfig fc) throws ServletException {
}
+ // ---------------------------------------------------------------------
+ // Filtering
+ // ---------------------------------------------------------------------
+
+
+ /**
+ * doFilter
+ * <p>
+ * If a user is detected (request param or session attribute), wrap the request to:
+ * <ul>
+ * <li>Expose a {@link Principal} with the provided username.</li>
+ * <li>Report a single role through {@link HttpServletRequest#isUserInRole(String)}.</li>
+ * </ul>
+ * Otherwise, pass-through.
+ *
+ * @param sr incoming request
+ * @param sr1 outgoing response
+ * @param fc filter chain
+ */
@Override
public void doFilter(ServletRequest sr, ServletResponse sr1, FilterChain fc) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) sr;
@@ -46,21 +96,46 @@
}
+ /**
+ * destroy<p>
+ * Filter destruction hook (unused).
+ */
@Override
public void destroy() {
}
+ // ---------------------------------------------------------------------
+ // Wrapper
+ // ---------------------------------------------------------------------
+
+ /**
+ * UserRoleRequestWrapper
+ * <p>
+ * Wrapper that overrides role checks and the user principal when a synthetic user is present.
+ */
private class UserRoleRequestWrapper extends HttpServletRequestWrapper {
private String role;
private String user;
+ /**
+ * Constructor
+ * <p>
+ * @param role single role to expose via {@link #isUserInRole(String)}
+ * @param user user name to expose via {@link #getUserPrincipal()}
+ * @param request original request to wrap
+ */
public UserRoleRequestWrapper(String role, String user, HttpServletRequest request) {
super(request);
this.role = role;
this.user = user;
}
+ /**
+ * isUserInRole
+ * <p>
+ * Returns {@code true} if the requested role equals the configured synthetic role.
+ */
@Override
public boolean isUserInRole(String role) {
LOG.info("isUserRole METHOD: {}, {}", role, this.role);
@@ -70,6 +145,11 @@
return this.role.equals(role);
}
+ /**
+ * getUserPrincipal
+ * <p>
+ * Returns a minimal {@link Principal} with the configured user name; delegates otherwise.
+ */
@Override
public Principal getUserPrincipal() {
if (this.user == null) {
diff --git a/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java b/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
index 7726c7d..6605752 100644
--- a/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
+++ b/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis;
import jakarta.persistence.EntityManager;
@@ -17,18 +20,49 @@
import net.curisit.securis.services.exception.SeCurisServiceException;
import net.curisit.securis.services.exception.SeCurisServiceException.ErrorCodes;
+/**
+* DefaultExceptionHandler
+* <p>
+* JAX-RS {@link ExceptionMapper} that normalizes error responses across the API.
+* It also makes a best-effort to rollback and close a request-scoped {@link EntityManager}
+* if still open.
+*
+* <p>Response strategy:
+* <ul>
+* <li>{@link ForbiddenException} → 401 UNAUTHORIZED with app-specific error headers.</li>
+* <li>{@link SeCurisServiceException} → 418 (custom) with app error headers.</li>
+* <li>Other exceptions → 500 with generic message and request context logging.</li>
+* </ul>
+*
+* Headers:
+* <ul>
+* <li>{@code X-SECURIS-ERROR-MSG}</li>
+* <li>{@code X-SECURIS-ERROR-CODE}</li>
+* </ul>
+*
+* @author JRA
+* Last reviewed by JRA on Oct 6, 2025.
+*/
@Provider
public class DefaultExceptionHandler implements ExceptionMapper<Exception> {
+
private static final Logger LOG = LogManager.getLogger(DefaultExceptionHandler.class);
-
+
+ /** Default status code used for application-defined errors. */
public static final int DEFAULT_APP_ERROR_STATUS_CODE = 418;
+
+ /** Header name carrying a human-readable error message. */
public static final String ERROR_MESSAGE_HEADER = "X-SECURIS-ERROR-MSG";
+
+ /** Header name carrying a symbolic application error code. */
public static final String ERROR_CODE_MESSAGE_HEADER = "X-SECURIS-ERROR-CODE";
+ /** Default constructor (logs instantiation). */
public DefaultExceptionHandler() {
LOG.info("Creating DefaultExceptionHandler ");
}
+ // Context objects injected by the runtime
@Context
HttpServletRequest request;
@Context
@@ -36,6 +70,12 @@
@Context
EntityManager em;
+ /**
+ * toResponse
+ * <p>
+ * Map a thrown exception to an HTTP {@link Response}, releasing the {@link EntityManager}
+ * if present.
+ */
@Override
public Response toResponse(Exception e) {
releaseEntityManager();
@@ -57,6 +97,11 @@
return Response.serverError().header(ERROR_MESSAGE_HEADER, "Unexpected error: " + e.toString()).type(MediaType.APPLICATION_JSON).build();
}
+ /**
+ * releaseEntityManager
+ * <p>
+ * Best-effort cleanup: rollback active transaction (if joined) and close the {@link EntityManager}.
+ */
private void releaseEntityManager() {
try {
if (em != null && em.isOpen()) {
diff --git a/securis/src/main/java/net/curisit/securis/DevFilter.java b/securis/src/main/java/net/curisit/securis/DevFilter.java
index 2ed1e4d..fe7b3f1 100644
--- a/securis/src/main/java/net/curisit/securis/DevFilter.java
+++ b/securis/src/main/java/net/curisit/securis/DevFilter.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis;
import java.io.IOException;
@@ -16,6 +19,19 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+/**
+* DevFilter
+* <p>
+* Development-time CORS helper. Adds permissive CORS headers to allow front-end
+* resources (e.g. JS served from a different origin) to call the API.
+* Short-circuits <code>OPTIONS</code> preflight requests.
+*
+* <p><b>Security note:</b> This configuration is intentionally permissive and should be
+* restricted for production.
+*
+* @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+*/
@ApplicationScoped
@WebFilter(urlPatterns = "/*")
public class DevFilter implements Filter {
@@ -23,10 +39,19 @@
@SuppressWarnings("unused")
private static final Logger log = LogManager.getLogger(DevFilter.class);
+ /**
+ * init<p>
+ * Filter init hook (unused).
+ */
@Override
public void init(FilterConfig fc) throws ServletException {
}
+ /**
+ * doFilter
+ * <p>
+ * Add CORS headers and pass through non-OPTIONS methods to the next filter.
+ */
@Override
public void doFilter(ServletRequest sreq, ServletResponse sres, FilterChain fc) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) sreq;
@@ -44,6 +69,10 @@
}
}
+ /**
+ * destroy<p>
+ * Filter destroy hook (unused).
+ */
@Override
public void destroy() {
}
diff --git a/securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java b/securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java
index 30556ea..85043a9 100644
--- a/securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java
+++ b/securis/src/main/java/net/curisit/securis/FreeLicenseGenerator.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis;
import java.util.Date;
@@ -9,10 +12,31 @@
import net.curisit.securis.beans.SignedLicenseBean;
import net.curisit.securis.utils.JsonUtils;
+/**
+* FreeLicenseGenerator
+* <p>
+* Helper to generate a signed FREE license (no expiration) for a given app and code.
+*
+* @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+*/
public class FreeLicenseGenerator {
+ /** Constant license type code for FREE licenses. */
public static final String FREE_LICENSE_TYPE = "FREE";
+ /**
+ * generateLicense
+ * <p>
+ * Build and sign a FREE license using the default generator. Uses a <code>Date(-1)</code>
+ * sentinel as "no expiration".
+ *
+ * @param appName application name
+ * @param licCode license code
+ * @param metadata additional metadata to embed
+ * @return signed license bean wrapper
+ * @throws SeCurisException on generation/signature errors
+ */
public static SignedLicenseBean generateLicense(String appName, String licCode, Map<String, Object> metadata) throws SeCurisException {
SignedLicenseBean sl = null;
RequestBean rb = new RequestBean();
@@ -24,6 +48,13 @@
return sl;
}
+
+ /**
+ * Demo main
+ *
+ * @param args
+ * @throws SeCurisException
+ */
public static void main(String[] args) throws SeCurisException {
Map<String, Object> metadata = new HashMap<>();
metadata.put("max_docs", 2000);
diff --git a/securis/src/main/java/net/curisit/securis/GzipFilter.java b/securis/src/main/java/net/curisit/securis/GzipFilter.java
index a8ceaee..9b153f2 100644
--- a/securis/src/main/java/net/curisit/securis/GzipFilter.java
+++ b/securis/src/main/java/net/curisit/securis/GzipFilter.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis;
import java.io.IOException;
@@ -18,6 +21,12 @@
import net.curisit.securis.utils.GZipServletResponseWrapper;
+/**
+* GzipFilter
+* <p>
+* Servlet filter that compresses <code>*.js</code> responses with GZIP when the client
+* advertises <code>Accept-Encoding: gzip</code>.
+*/
@ApplicationScoped
@WebFilter(urlPatterns = "*.js")
public class GzipFilter implements Filter {
@@ -25,10 +34,16 @@
@SuppressWarnings("unused")
private static final Logger LOG = LogManager.getLogger(GzipFilter.class);
+ /** init<p>Filter init hook (unused). */
@Override
public void init(FilterConfig fc) throws ServletException {
}
+ /**
+ * doFilter
+ * <p>
+ * Wrap the response with a GZIP-compressing wrapper if supported by the client.
+ */
@Override
public void doFilter(ServletRequest sreq, ServletResponse sres, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) sreq;
@@ -44,12 +59,18 @@
}
}
+ /**
+ * acceptsGZipEncoding
+ * <p>
+ * @return {@code true} when request header contains "gzip" in <code>Accept-Encoding</code>.
+ */
private boolean acceptsGZipEncoding(HttpServletRequest httpRequest) {
String acceptEncoding = httpRequest.getHeader("Accept-Encoding");
return acceptEncoding != null && acceptEncoding.indexOf("gzip") != -1;
}
+ /** destroy<p>Filter destroy hook (unused). */
@Override
public void destroy() {
}
diff --git a/securis/src/main/java/net/curisit/securis/LicenseGenerator.java b/securis/src/main/java/net/curisit/securis/LicenseGenerator.java
index 558599a..419ba29 100644
--- a/securis/src/main/java/net/curisit/securis/LicenseGenerator.java
+++ b/securis/src/main/java/net/curisit/securis/LicenseGenerator.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis;
import java.io.File;
@@ -24,23 +27,35 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import jakarta.inject.Singleton;
+
/**
- * License generator and signer
- *
- * @author roberto <roberto.sanchez@curisit.net>
- */
-@javax.inject.Singleton
+* LicenseGenerator
+* <p>
+* Factory for building and signing {@link LicenseBean} instances. Uses a process-wide
+* singleton and expects a PKCS#8 private key at:
+* <code>~/.SeCuris/keys/securis_private_key.pkcs8</code>.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
+@Singleton
public class LicenseGenerator {
private static final Logger LOG = LogManager.getLogger(LicenseGenerator.class);
private static LicenseGenerator singleton = new LicenseGenerator();
+ /**
+ * getInstance<p>
+ * Singleton accessor.
+ */
public static LicenseGenerator getInstance() {
return singleton;
}
/**
+ * generateLicense<p>
* Generate a license bean with the specified data
*
* @param req
@@ -66,12 +81,14 @@
}
/**
- * Generate a license file using a {@link LicenseBean}
- *
- * @param license
- * @param file
- * @throws SeCurisException
- */
+ * save
+ * <p>
+ * Persist a pretty-printed JSON representation of the signed license to disk.
+ *
+ * @param license source license
+ * @param file target file path
+ * @throws SeCurisException if serialization or IO fails
+ */
public void save(LicenseBean license, File file) throws SeCurisException {
SignedLicenseBean signedLic = new SignedLicenseBean(license);
byte[] json;
@@ -91,15 +108,14 @@
}
/**
- *
- * @param licBean
- * @return
- * @throws NoSuchAlgorithmException
- * @throws IOException
- * @throws InvalidKeySpecException
- * @throws InvalidKeyException
- * @throws SignatureException
- */
+ * sign
+ * <p>
+ * Compute a Base64 signature for the given license and set it into the bean.
+ *
+ * @param licBean license to sign (in-place)
+ * @return Base64 signature string
+ * @throws SeCurisException if the signature process fails
+ */
public String sign(LicenseBean licBean) throws SeCurisException {
SignatureHelper sh = SignatureHelper.getInstance();
@@ -114,16 +130,8 @@
byte[] signatureData = signature.sign();
licBean.setSignature(Base64.encodeBase64String(signatureData));
return licBean.getSignature();
- } catch (NoSuchAlgorithmException e) {
- LOG.error("Error signing license for " + licBean, e);
- } catch (InvalidKeyException e) {
- LOG.error("Error signing license for " + licBean, e);
- } catch (InvalidKeySpecException e) {
- LOG.error("Error signing license for " + licBean, e);
- } catch (IOException e) {
- LOG.error("Error signing license for " + licBean, e);
- } catch (SignatureException e) {
- LOG.error("Error signing license for " + licBean, e);
+ } catch (NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException | IOException | SignatureException e) {
+ LOG.error("Error signing license for {}", licBean, e);
}
throw new SeCurisException("License could not be generated");
}
diff --git a/securis/src/main/java/net/curisit/securis/RestServicesApplication.java b/securis/src/main/java/net/curisit/securis/RestServicesApplication.java
index fc1c0b5..9c9f1e7 100644
--- a/securis/src/main/java/net/curisit/securis/RestServicesApplication.java
+++ b/securis/src/main/java/net/curisit/securis/RestServicesApplication.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis;
import java.util.HashSet;
@@ -19,11 +22,25 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+/**
+* RestServicesApplication
+* <p>
+* JAX-RS application configuring the REST resource classes and interceptors.
+* Declares base path <code>/</code>.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@ApplicationPath("/")
public class RestServicesApplication extends Application {
private static final Logger LOG = LogManager.getLogger(RestServicesApplication.class);
+ /**
+ * getClasses
+ * <p>
+ * @return set of REST endpoints and filters to be registered by the runtime
+ */
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> classes = new HashSet<>();
diff --git a/securis/src/main/java/net/curisit/securis/beans/User.java b/securis/src/main/java/net/curisit/securis/beans/User.java
index 3b8a4f8..92abfb9 100644
--- a/securis/src/main/java/net/curisit/securis/beans/User.java
+++ b/securis/src/main/java/net/curisit/securis/beans/User.java
@@ -1,5 +1,16 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.beans;
+/**
+* User
+* <p>
+* Placeholder bean for a system user. Intentionally empty in this snapshot.
+* Extend with fields (username, roles, etc.) and proper JSON/JPA annotations as needed.
+*
+* Note: Kept as-is to preserve current behavior.
+*/
public class User {
}
diff --git a/securis/src/main/java/net/curisit/securis/db/Application.java b/securis/src/main/java/net/curisit/securis/db/Application.java
index 138481f..bc57809 100644
--- a/securis/src/main/java/net/curisit/securis/db/Application.java
+++ b/securis/src/main/java/net/curisit/securis/db/Application.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.db;
import java.io.Serializable;
@@ -30,9 +33,25 @@
import com.fasterxml.jackson.annotation.JsonProperty;
/**
- * Entity implementation class for Entity: application
- *
- */
+* Application
+* <p>
+* JPA entity that represents an application registered in the licensing server.
+* Includes descriptive fields and relationships to <code>LicenseType</code>,
+* <code>ApplicationMetadata</code> and <code>User</code>.
+*
+* Mapping details:
+* <ul>
+* <li>Table: <code>application</code></li>
+* <li>Named queries: <code>list-applications</code>, <code>list-applications-by_ids</code></li>
+* <li>Relationships:
+* <ul>
+* <li><code>@OneToMany</code> <b>licenseTypes</b> (mappedBy="application")</li>
+* <li><code>@OneToMany</code> <b>metadata</b> with cascade PERSIST/REMOVE/REFRESH</li>
+* <li><code>@ManyToMany</code> <b>users</b> via join table <code>user_application</code></li>
+* </ul>
+* </li>
+* </ul>
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -46,32 +65,57 @@
private static final long serialVersionUID = 1L;
+ // ------------------------------------------------------------------
+ // Columns
+ // ------------------------------------------------------------------
+
+
+ /** Surrogate primary key. */
@Id
@GeneratedValue
private Integer id;
+ /** Unique short code for the application (business identifier). */
private String code;
+
+ /** Human-friendly application name. */
private String name;
+
+ /** Optional description. */
private String description;
+ /** Default license file name suggested for downloads/exports. */
@Column(name = "license_filename")
@JsonProperty("license_filename")
private String licenseFilename;
+ /** Creation timestamp (server-side). */
@Column(name = "creation_timestamp")
@JsonProperty("creation_timestamp")
private Date creationTimestamp;
- // We don't include the referenced entities to limit the size of each row at
- // // the listing
+ // ----------------------- Relationships ---------------------------
+
+
+ /**
+ * License types attached to this application (ignored in default JSON to keep listings small).
+ *
+ * We don't include the referenced entities to limit the size of each row at the listing
+ */
@JsonIgnore
@OneToMany(fetch = FetchType.LAZY, mappedBy = "application")
private Set<LicenseType> licenseTypes;
+ /**
+ * Metadata key/value entries for this application.
+ */
@OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "application")
@JsonManagedReference
private Set<ApplicationMetadata> metadata;
+ /**
+ * Users that have access/relationship with this application (ignored in JSON listings).
+ */
@JsonIgnore
// We don't include the users to limit the size of each row a the listing
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
@@ -80,73 +124,180 @@
inverseJoinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") })
private Set<User> users;
+ // ------------------------------------------------------------------
+ // Getters & setters
+ // ------------------------------------------------------------------
+
+ /**
+ * getId<p>
+ * Return the primary key.
+ *
+ * @return id
+ */
public Integer getId() {
return id;
}
+ /**
+ * setId<p>
+ * Set the primary key
+ *
+ * @param id
+ */
public void setId(Integer id) {
this.id = id;
}
+ /**
+ * getName<p>
+ * Get the name
+ *
+ * @return name
+ */
public String getName() {
return name;
}
+ /**
+ * setName<p>
+ * Set the name
+ *
+ * @param name
+ */
public void setName(String name) {
this.name = name;
}
+ /**
+ * getDescription<p>
+ * Get the description
+ *
+ * @return description
+ */
public String getDescription() {
return description;
}
+ /**
+ * setDescription<p>
+ * Set the description
+ *
+ * @param description
+ */
public void setDescription(String description) {
this.description = description;
}
+ /**
+ * getCreationTimestamp<p>
+ * Get the creation timestamp
+ *
+ * @return creationTimestamp
+ */
public Date getCreationTimestamp() {
return creationTimestamp;
}
+ /**
+ * setCreationTimestamp<p>
+ * Set the creation timestamp
+ *
+ * @param creationTimestamp
+ */
public void setCreationTimestamp(Date creationTimestamp) {
this.creationTimestamp = creationTimestamp;
}
+ /**
+ * getApplicationMetadata<p>
+ * Set the application metadata
+ *
+ * @return appMetadata
+ */
@JsonProperty("metadata")
public Set<ApplicationMetadata> getApplicationMetadata() {
return metadata;
}
+ /**
+ * setApplicationMetadata<p>
+ * Set the application metadata
+ *
+ * @param metadata
+ */
@JsonProperty("metadata")
public void setApplicationMetadata(Set<ApplicationMetadata> metadata) {
this.metadata = metadata;
}
+ /**
+ * getLicenseFilename<p>
+ * Get the license file name
+ *
+ * @return licenseFilename
+ */
public String getLicenseFilename() {
return licenseFilename;
}
+ /**
+ * setLicenseFilename<p>
+ * Set the license file name
+ *
+ * @param licenseFilename
+ */
public void setLicenseFilename(String licenseFilename) {
this.licenseFilename = licenseFilename;
}
+ /**
+ * getLicenseTypes<p>
+ * Get the license types
+ *
+ * @return licenseTypes
+ */
public Set<LicenseType> getLicenseTypes() {
LOG.info("Getting list license types!!!!");
return licenseTypes;
}
+ /**
+ * setLicenseTypes<p>
+ * Set the license types
+ *
+ * @param licenseTypes
+ */
public void setLicenseTypes(Set<LicenseType> licenseTypes) {
this.licenseTypes = licenseTypes;
}
+ /**
+ * getCode<p>
+ * Get the application code
+ *
+ * @return code
+ */
public String getCode() {
return code;
}
+ /**
+ * setCode<p>
+ * Set the application code
+ *
+ * @param code
+ */
public void setCode(String code) {
this.code = code;
}
+ /**
+ * equals<p>
+ * Compares the current object with the given object
+ *
+ * @param object
+ * @return isEquals
+ */
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Application))
@@ -155,6 +306,12 @@
return id.equals(other.id);
}
+ /**
+ * hashCode<p>
+ * Get the object hashCode
+ *
+ * @param hashCode
+ */
@Override
public int hashCode() {
diff --git a/securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java b/securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java
index eafd418..5b2c982 100644
--- a/securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java
+++ b/securis/src/main/java/net/curisit/securis/db/ApplicationMetadata.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -26,97 +29,179 @@
import net.curisit.securis.db.common.Metadata;
/**
- * Entity implementation class for Entity: application_metadata
- *
- */
+* ApplicationMetadata
+* <p>
+* Single metadata entry (key/value/mandatory) attached to an {@link Application}.
+* Uses a composite PK: (application_id, key).
+* <p>
+* Mapping details:
+* - Table: application_metadata
+* - PK: application_id + key (two @Id fields).
+* - application: @ManyToOne with @JsonBackReference to avoid JSON cycles.
+* - creation_timestamp exposed as "creation_timestamp".
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@Entity
@Table(name = "application_metadata")
@JsonIgnoreProperties(ignoreUnknown = true)
-@NamedQueries({ @NamedQuery(name = "list-application-metadata", query = "SELECT a FROM ApplicationMetadata a where a.application.id = :applicationId") })
+@NamedQueries({
+ @NamedQuery(name = "list-application-metadata",
+ query = "SELECT a FROM ApplicationMetadata a where a.application.id = :applicationId")
+})
public class ApplicationMetadata implements Serializable, Metadata {
- private static final Logger LOG = LogManager.getLogger(ApplicationMetadata.class);
+ private static final Logger LOG = LogManager.getLogger(ApplicationMetadata.class);
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- @Id
- @ManyToOne
- @JoinColumn(name = "application_id")
- @JsonBackReference
- private Application application;
+ /** Part of PK: owning application. */
+ @Id
+ @ManyToOne
+ @JoinColumn(name = "application_id")
+ @JsonBackReference
+ private Application application;
- @Id
- @Column(name = "\"key\"")
- private String key;
+ /** Part of PK: metadata key (quoted column name). */
+ @Id
+ @Column(name = "\"key\"")
+ private String key;
- private String value;
+ /** Arbitrary metadata value. */
+ private String value;
- private boolean mandatory;
+ /** Whether this key is required for the parent application. */
+ private boolean mandatory;
- @Column(name = "creation_timestamp")
- @JsonProperty("creation_timestamp")
- private Date creationTimestamp;
+ /** Server-side creation timestamp. */
+ @Column(name = "creation_timestamp")
+ @JsonProperty("creation_timestamp")
+ private Date creationTimestamp;
- public String getKey() {
- return key;
- }
+ // ---------------------------------------------------------------------
+ // Getters & setters
+ // ---------------------------------------------------------------------
- public void setKey(String key) {
- this.key = key;
- }
+ /**
+ * getKey<p>
+ * Get the metadata key (PK part).
+ *
+ * @return key
+ */
+ public String getKey() { return key; }
- public Application getApplication() {
- LOG.info("Getting application from app metadata: {}", application);
- return application;
- }
+ /**
+ * setKey<p>
+ * Set the metadata key (PK part).
+ *
+ * @param key
+ */
+ public void setKey(String key) { this.key = key; }
- public void setApplication(Application application) {
- this.application = application;
- }
+ /**
+ * getApplication<p>
+ * Get the owning application.
+ *
+ * @return application
+ */
+ public Application getApplication() {
+ LOG.info("Getting application from app metadata: {}", application);
+ return application;
+ }
- public Date getCreationTimestamp() {
- return creationTimestamp;
- }
+ /**
+ * setApplication<p>
+ * Set the owning application (PK part).
+ *
+ * @param application
+ */
+ public void setApplication(Application application) { this.application = application; }
- public void setCreationTimestamp(Date creationTimestamp) {
- this.creationTimestamp = creationTimestamp;
- }
+ /**
+ * getCreationTimestamp<p>
+ * Get the creation timestamp.
+ *
+ * @return creationTimestamp
+ */
+ public Date getCreationTimestamp() { return creationTimestamp; }
- public String getValue() {
- return value;
- }
+ /**
+ * setCreationTimestamp<p>
+ * Set the creation timestamp.
+ *
+ * @param creationTimestamp
+ */
+ public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
- public void setValue(String value) {
- this.value = value;
- }
+ /**
+ * getValue<p>
+ * Get the metadata value.
+ *
+ * @return value
+ */
+ public String getValue() { return value; }
- public boolean isMandatory() {
- return mandatory;
- }
+ /**
+ * setValue<p>
+ * Set the metadata value.
+ *
+ * @param value
+ */
+ public void setValue(String value) { this.value = value; }
- public void setMandatory(boolean mandatory) {
- this.mandatory = mandatory;
- }
+ /**
+ * isMandatory<p>
+ * Whether this entry is required.
+ *
+ * @return mandatory
+ */
+ public boolean isMandatory() { return mandatory; }
- @Override
- public String toString() {
+ /**
+ * setMandatory<p>
+ * Mark this entry as required or optional.
+ *
+ * @param mandatory
+ */
+ public void setMandatory(boolean mandatory) { this.mandatory = mandatory; }
- return String.format("AppMd (%s: %s)", this.key, value);
- }
+ // ---------------------------------------------------------------------
+ // Object methods
+ // ---------------------------------------------------------------------
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof ApplicationMetadata))
- return false;
- ApplicationMetadata other = (ApplicationMetadata) obj;
- return Objects.equals(key, other.key) && Objects.equals(application, other.application);
- }
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
+ @Override
+ public String toString() { return String.format("AppMd (%s: %s)", this.key, value); }
- @Override
- public int hashCode() {
- return Objects.hash(key, application);
- }
+ /**
+ * equals<p>
+ * Compare the current object with the given object
+ *
+ * @param object
+ * @return isEquals
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ApplicationMetadata)) return false;
+ ApplicationMetadata other = (ApplicationMetadata) obj;
+ return Objects.equals(key, other.key) && Objects.equals(application, other.application);
+ }
+ /**
+ * hashCode<p>
+ * Get the object hashCode
+ *
+ * @return hashCode
+ */
+ @Override
+ public int hashCode() { return Objects.hash(key, application); }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/BlockedRequest.java b/securis/src/main/java/net/curisit/securis/db/BlockedRequest.java
index 74f562b..c2fc300 100644
--- a/securis/src/main/java/net/curisit/securis/db/BlockedRequest.java
+++ b/securis/src/main/java/net/curisit/securis/db/BlockedRequest.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -21,9 +24,20 @@
import com.fasterxml.jackson.annotation.JsonProperty;
/**
- * Entity implementation class for Entity: pack
- *
- */
+* BlockedRequest
+* <p>
+* Persistent record marking a request (by hash) as blocked.
+* Primary key is the SHA-256 of the original request_data.
+* Useful to avoid replay/duplicate processing.
+*
+* Mapping details:
+* - Table: blocked_request
+* - PK: hash (SHA-256(request_data))
+* - Optional relation 'blockedBy' for auditing.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@Entity
@@ -33,69 +47,126 @@
private static final long serialVersionUID = 1L;
+ /** Unique SHA-256 hash of {@link #requestData}. */
@Id
private String hash;
+ /** Original request payload. */
@Column(name = "request_data")
@JsonProperty("request_data")
private String requestData;
+ /** Server-side creation timestamp. */
@Column(name = "creation_timestamp")
@JsonProperty("creation_timestamp")
private Date creationTimestamp;
+ /** User who blocked this request (optional, auditing). */
@JsonIgnore
@ManyToOne
@JoinColumn(name = "blocked_by")
private User blockedBy;
- public Date getCreationTimestamp() {
- return creationTimestamp;
- }
+ // ---------------------------------------------------------------------
+ // Getters & setters
+ // ---------------------------------------------------------------------
- public void setCreationTimestamp(Date creationTimestamp) {
- this.creationTimestamp = creationTimestamp;
- }
+ /**
+ * getCreationTimestamp<p>
+ * Get the creation timestamp.
+ *
+ * @return creationTimestamp
+ */
+ public Date getCreationTimestamp() { return creationTimestamp; }
+ /**
+ * setCreationTimestamp<p>
+ * Set the creation timestamp.
+ *
+ * @param creationTimestamp
+ */
+ public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
+
+ /**
+ * equals<p>
+ * Identity based on primary key (hash).
+ */
@Override
public boolean equals(Object obj) {
- if (!(obj instanceof BlockedRequest))
- return false;
+ if (!(obj instanceof BlockedRequest)) return false;
BlockedRequest other = (BlockedRequest) obj;
- return (hash == null && other.hash == null) || hash.equals(other.hash);
+ return (hash == null && other.hash == null) || (hash != null && hash.equals(other.hash));
}
+ /**
+ * hashCode<p>
+ * Hash based on primary key (hash).
+ */
@Override
- public int hashCode() {
+ public int hashCode() { return (hash == null ? 0 : hash.hashCode()); }
- return (hash == null ? 0 : hash.hashCode());
- }
+ /**
+ * getRequestData<p>
+ * Get the original serialized request data.
+ *
+ * @return requestData
+ */
+ public String getRequestData() { return requestData; }
- public String getRequestData() {
- return requestData;
- }
-
+ /**
+ * setRequestData<p>
+ * Set the original request data and recompute the PK hash immediately.
+ * Hash is computed as SHA-256 over the request string.
+ *
+ * @param requestData
+ */
public void setRequestData(String requestData) {
this.requestData = requestData;
this.hash = generateHash(this.requestData);
}
- public User getBlockedBy() {
- return blockedBy;
- }
+ /**
+ * getBlockedBy<p>
+ * Return the user who blocked this request (if any).
+ *
+ * @return blockedBy
+ */
+ public User getBlockedBy() { return blockedBy; }
- public void setBlockedBy(User blockedBy) {
- this.blockedBy = blockedBy;
- }
+ /**
+ * setBlockedBy<p>
+ * Set the user who blocked this request.
+ *
+ * @param blockedBy
+ */
+ public void setBlockedBy(User blockedBy) { this.blockedBy = blockedBy; }
+ // ---------------------------------------------------------------------
+ // Static helpers
+ // ---------------------------------------------------------------------
+
+ /**
+ * generateHash<p>
+ * Compute the SHA-256 hex string for the given request data.
+ *
+ * @param reqData
+ * @return sha256(reqData) or null if reqData is null
+ */
public static String generateHash(String reqData) {
return (reqData != null ? Utils.sha256(reqData) : null);
}
+ /**
+ * isRequestBlocked<p>
+ * Check if a request payload is blocked by looking up its hash as the PK.
+ *
+ * @param requestData original payload
+ * @param em JPA entity manager
+ * @return true if an entry exists with the same hash
+ */
public static boolean isRequestBlocked(String requestData, EntityManager em) {
String hash = generateHash(requestData);
BlockedRequest br = em.find(BlockedRequest.class, hash);
return br != null;
}
-
}
diff --git a/securis/src/main/java/net/curisit/securis/db/License.java b/securis/src/main/java/net/curisit/securis/db/License.java
index 8b36055..d71759d 100644
--- a/securis/src/main/java/net/curisit/securis/db/License.java
+++ b/securis/src/main/java/net/curisit/securis/db/License.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -41,438 +44,677 @@
import net.curisit.securis.services.exception.SeCurisServiceException;
/**
- * Entity implementation class for Entity: license
- *
- */
+* License
+* <p>
+* 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')")
-
+@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 long serialVersionUID = 2700310404904877227L;
- private static final Logger LOG = LogManager.getLogger(License.class);
+ private static final Logger LOG = LogManager.getLogger(License.class);
- @Id
- @GeneratedValue
- private Integer id;
+ // ------------------------------------------------------------------
+ // Columns & relations
+ // ------------------------------------------------------------------
- private String code;
+ @Id
+ @GeneratedValue
+ private Integer id;
- @Column(name = "metadata_obsolete")
- @JsonProperty("metadata_obsolete")
- private Boolean metadataObsolete;
+ private String code;
- @Column(name = "activation_code")
- @JsonProperty("activation_code")
- private String activationCode;
+ @Column(name = "metadata_obsolete")
+ @JsonProperty("metadata_obsolete")
+ private Boolean metadataObsolete;
- @Column(name = "code_suffix")
- @JsonProperty("code_suffix")
- private Integer codeSuffix;
+ @Column(name = "activation_code")
+ @JsonProperty("activation_code")
+ private String activationCode;
- @JsonIgnore
- @ManyToOne
- @JoinColumn(name = "pack_id")
- private Pack pack;
+ @Column(name = "code_suffix")
+ @JsonProperty("code_suffix")
+ private Integer codeSuffix;
- @JsonIgnore
- @ManyToOne
- @JoinColumn(name = "created_by")
- private User createdBy;
+ @JsonIgnore
+ @ManyToOne
+ @JoinColumn(name = "pack_id")
+ private Pack pack;
- @JsonIgnore
- @ManyToOne
- @JoinColumn(name = "cancelled_by")
- private User cancelledBy;
+ @JsonIgnore
+ @ManyToOne
+ @JoinColumn(name = "created_by")
+ private User createdBy;
- @Type(type = "net.curisit.securis.db.common.LicenseStatusType")
- private LicenseStatus status;
+ @JsonIgnore
+ @ManyToOne
+ @JoinColumn(name = "cancelled_by")
+ private User cancelledBy;
- @Column(name = "full_name")
- @JsonProperty("full_name")
- private String fullName;
+ @Type(type = "net.curisit.securis.db.common.LicenseStatusType")
+ private LicenseStatus status;
- private String email;
+ @Column(name = "full_name")
+ @JsonProperty("full_name")
+ private String fullName;
- @Column(name = "request_data")
- @JsonProperty("request_data")
- private String requestData;
+ private String email;
- /**
- * request data hash is automatically set when we use
- * {@link License#setRequestData(String)} method
- */
- @Column(name = "request_data_hash")
- @JsonIgnore
- private String reqDataHash;
+ @Column(name = "request_data")
+ @JsonProperty("request_data")
+ private String requestData;
- @Column(name = "license_data")
- @JsonProperty("license_data")
- @JsonIgnore
- // The license data is sent to user as a separate file, It doesn't need to
- // be included as License attribute on browser
- private String licenseData;
+ /**
+ * Request data hash (not serialized). Automatically updated by setRequestData().
+ */
+ @Column(name = "request_data_hash")
+ @JsonIgnore
+ private String reqDataHash;
- @Column(name = "creation_timestamp")
- @JsonProperty("creation_timestamp")
- private Date creationTimestamp;
+ @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 = "modification_timestamp")
- @JsonProperty("modification_timestamp")
- private Date modificationTimestamp;
+ @Column(name = "creation_timestamp")
+ @JsonProperty("creation_timestamp")
+ private Date creationTimestamp;
- @Column(name = "last_access_timestamp")
- @JsonProperty("last_access_timestamp")
- private Date lastAccessTimestamp;
+ @Column(name = "modification_timestamp")
+ @JsonProperty("modification_timestamp")
+ private Date modificationTimestamp;
- @Column(name = "expiration_date")
- @JsonProperty("expiration_date")
- private Date expirationDate;
+ @Column(name = "last_access_timestamp")
+ @JsonProperty("last_access_timestamp")
+ private Date lastAccessTimestamp;
- private String comments;
+ @Column(name = "expiration_date")
+ @JsonProperty("expiration_date")
+ private Date expirationDate;
- @OneToMany(fetch = FetchType.LAZY, mappedBy = "license")
- @JsonIgnore
- private List<LicenseHistory> history;
+ private String comments;
- public Integer getId() {
- return id;
- }
+ @OneToMany(fetch = FetchType.LAZY, mappedBy = "license")
+ @JsonIgnore
+ private List<LicenseHistory> history;
- public String getCode() {
- return code;
- }
+ // ------------------------------------------------------------------
+ // Basic accessors
+ // ------------------------------------------------------------------
- public void setCode(String code) {
- this.code = code;
- }
+ /**
+ * getId<p>
+ * Return primary key.
+ *
+ * @return id
+ */
+ public Integer getId() { return id; }
- @Override
- public Date getCreationTimestamp() {
- return creationTimestamp;
- }
+ /**
+ * getCode<p>
+ * Return human-readable license code.
+ *
+ * @return code
+ */
+ public String getCode() { return code; }
- @Override
- public void setCreationTimestamp(Date creationTimestamp) {
- this.creationTimestamp = creationTimestamp;
- }
+ /**
+ * setCode<p>
+ * Set human-readable license code.
+ *
+ * @param code
+ */
+ public void setCode(String code) { this.code = code; }
- public User getCreatedBy() {
- return createdBy;
- }
+ /**
+ * getCreationTimestamp<p>
+ * Required by CreationTimestampEntity.
+ *
+ * @return creationTimestamp
+ */
+ @Override
+ public Date getCreationTimestamp() { return creationTimestamp; }
- public void setCreatedBy(User createdBy) {
- this.createdBy = createdBy;
- }
+ /**
+ * setCreationTimestamp<p>
+ * Set creation timestamp.
+ *
+ * @param creationTimestamp
+ */
+ @Override
+ public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
- public Pack getPack() {
- return pack;
- }
+ /**
+ * getCreatedBy<p>
+ * Return creator user (entity).
+ *
+ * @return user
+ */
+ public User getCreatedBy() { return createdBy; }
- public void setPack(Pack pack) {
- this.pack = pack;
- }
+ /**
+ * setCreatedBy<p>
+ * Set creator user (entity).
+ *
+ * @param user
+ */
+ public void setCreatedBy(User createdBy) { this.createdBy = createdBy; }
- @JsonProperty("created_by_id")
- public String getCreatedById() {
- return createdBy == null ? null : createdBy.getUsername();
- }
+ /**
+ * getPack<p>
+ * Return owning pack.
+ *
+ * @return pack
+ */
+ public Pack getPack() { return pack; }
- @JsonProperty("created_by_id")
- public void setCreatedById(String username) {
- if (username == null) {
- createdBy = null;
- } else {
- createdBy = new User();
- createdBy.setUsername(username);
- }
- }
+ /**
+ * setPack<p>
+ * Set owning pack.
+ *
+ * @param pack
+ */
+ public void setPack(Pack pack) { this.pack = pack; }
- @JsonProperty("cancelled_by_id")
- public String getCancelledById() {
- return cancelledBy == null ? null : cancelledBy.getUsername();
- }
+ /**
+ * getCreatedById<p>
+ * Expose creator username as JSON.
+ *
+ * @return username
+ */
+ @JsonProperty("created_by_id")
+ public String getCreatedById() { return createdBy == null ? null : createdBy.getUsername(); }
- @JsonProperty("cancelled_by_id")
- public void setCancelledById(String username) {
- if (username == null) {
- cancelledBy = null;
- } else {
- cancelledBy = new User();
- cancelledBy.setUsername(username);
- }
- }
+ /**
+ * setCreatedById<p>
+ * 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);
+ }
+ }
- @JsonProperty("pack_code")
- public String getPackCode() {
- return pack == null ? null : pack.getCode();
- }
+ /**
+ * getCancelledById<p>
+ * Expose cancelling user username as JSON.
+ *
+ * @return username
+ */
+ @JsonProperty("cancelled_by_id")
+ public String getCancelledById() { return cancelledBy == null ? null : cancelledBy.getUsername(); }
- @JsonProperty("pack_id")
- public Integer getPackId() {
- return pack == null ? null : pack.getId();
- }
+ /**
+ * setCancelledById<p>
+ * 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);
+ }
+ }
- @JsonProperty("pack_id")
- public void setPackId(Integer idPack) {
- if (idPack == null) {
- pack = null;
- } else {
- pack = new Pack();
- pack.setId(idPack);
- }
- }
+ /**
+ * getPackCode<p>
+ * Expose pack code for convenience.
+ *
+ * @return packCode
+ */
+ @JsonProperty("pack_code")
+ public String getPackCode() { return pack == null ? null : pack.getCode(); }
- public LicenseStatus getStatus() {
- return status;
- }
+ /**
+ * getPackId<p>
+ * Expose pack id for convenience.
+ *
+ * @return packId
+ */
+ @JsonProperty("pack_id")
+ public Integer getPackId() { return pack == null ? null : pack.getId(); }
- public void setStatus(LicenseStatus status) {
- this.status = status;
- }
+ /**
+ * setPackId<p>
+ * 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);
+ }
+ }
- @Override
- public Date getModificationTimestamp() {
- return modificationTimestamp;
- }
+ /**
+ * getStatus<p>
+ * Return license status.
+ *
+ * @return licenseStatus
+ */
+ public LicenseStatus getStatus() { return status; }
- @Override
- public void setModificationTimestamp(Date modificationTimestamp) {
- this.modificationTimestamp = modificationTimestamp;
- }
+ /**
+ * setStatus<p>
+ * Set license status.
+ *
+ * @param status
+ */
+ public void setStatus(LicenseStatus status) { this.status = status; }
- public String getFullName() {
- return fullName;
- }
+ /**
+ * getModificationTimestamp<p>
+ * Required by ModificationTimestampEntity.
+ *
+ * @return modificationTimestamp
+ */
+ @Override
+ public Date getModificationTimestamp() { return modificationTimestamp; }
- public void setFullName(String fullName) {
- this.fullName = fullName;
- }
+ /**
+ * setModificationTimestamp<p>
+ * Set modification timestamp.
+ *
+ * @param modificationTimestamp
+ */
+ @Override
+ public void setModificationTimestamp(Date modificationTimestamp) { this.modificationTimestamp = modificationTimestamp; }
- public String getEmail() {
- return email;
- }
+ /**
+ * getFullName<p>
+ * Return license holder full name.
+ *
+ * @return name
+ */
+ public String getFullName() { return fullName; }
- public void setEmail(String email) {
- this.email = email;
- }
+ /**
+ * setFullName<p>
+ * Set license holder full name.
+ *
+ * @param name
+ */
+ public void setFullName(String fullName) { this.fullName = fullName; }
- public void setId(Integer id) {
- this.id = id;
- }
+ /**
+ * getEmail<p>
+ * Return email address.
+ *
+ * @return email
+ */
+ public String getEmail() { return email; }
- public User getCancelledBy() {
- return cancelledBy;
- }
+ /**
+ * setEmail<p>
+ * Set email address.
+ *
+ * @param email
+ */
+ public void setEmail(String email) { this.email = email; }
+
+ /**
+ * setId<p>
+ * Set primary key (rarely used).
+ *
+ * @param id
+ */
+ public void setId(Integer id) { this.id = id; }
+
+ /**
+ * getCancelledBy<p>
+ * Return cancelling user (entity).
+ *
+ * @param user
+ */
+ public User getCancelledBy() { return cancelledBy; }
+
+ /**
+ * setCancelledBy<p>
+ * Set cancelling user (entity).
+ *
+ * @param cancelledBy
+ */
+ public void setCancelledBy(User cancelledBy) { this.cancelledBy = cancelledBy; }
- public void setCancelledBy(User cancelledBy) {
- this.cancelledBy = cancelledBy;
- }
+ /**
+ * getLastAccessTimestamp<p>
+ * Return last access timestamp.
+ *
+ * @return lastAccessTimestamp
+ */
+ public Date getLastAccessTimestamp() { return lastAccessTimestamp; }
- public Date getLastAccessTimestamp() {
- return lastAccessTimestamp;
- }
+ /**
+ * setLastAccessTimestamp<p>
+ * Set last access timestamp.
+ *
+ * @param lastAccessTimestamp
+ */
+ public void setLastAccessTimestamp(Date lastAccessTimestamp) { this.lastAccessTimestamp = lastAccessTimestamp; }
- public void setLastAccessTimestamp(Date lastAccessTimestamp) {
- this.lastAccessTimestamp = lastAccessTimestamp;
- }
+ /**
+ * getRequestData<p>
+ * Return raw request data.
+ *
+ * @return requestData
+ */
+ public String getRequestData() { return requestData; }
- public String getRequestData() {
- return requestData;
- }
+ /**
+ * setRequestData<p>
+ * 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);
+ }
- public void setRequestData(String requestData) {
- this.requestData = requestData;
- this.reqDataHash = BlockedRequest.generateHash(this.requestData);
- }
+ /**
+ * getLicenseData<p>
+ * Return opaque license data (not serialized in lists).
+ *
+ * @return licenseData
+ */
+ public String getLicenseData() { return licenseData; }
- public String getLicenseData() {
- return licenseData;
- }
+ /**
+ * setLicenseData<p>
+ * Set opaque license data (large content kept server-side).
+ *
+ * @param licenseDate
+ */
+ public void setLicenseData(String licenseData) { this.licenseData = licenseData; }
- public void setLicenseData(String licenseData) {
- this.licenseData = licenseData;
- }
+ /**
+ * getComments<p>
+ * Return optional comments.
+ *
+ * @return comments
+ */
+ public String getComments() { return comments; }
- public String getComments() {
- return comments;
- }
+ /**
+ * setComments<p>
+ * Set optional comments.
+ *
+ * @param comments
+ */
+ public void setComments(String comments) { this.comments = comments; }
- public void setComments(String comments) {
- this.comments = comments;
- }
+ /**
+ * getHistory<p>
+ * Return change history entries (lazy).
+ *
+ * @return history
+ */
+ public List<LicenseHistory> getHistory() { return history; }
- public List<LicenseHistory> getHistory() {
- return history;
- }
+ /**
+ * setHistory<p>
+ * Set change history entries.
+ *
+ * @param history
+ */
+ public void setHistory(List<LicenseHistory> history) { this.history = history; }
- public void setHistory(List<LicenseHistory> history) {
- this.history = history;
- }
+ /**
+ * getExpirationDate<p>
+ * Return expiration date (nullable).
+ *
+ * @return expirationDate
+ */
+ public Date getExpirationDate() { return expirationDate; }
- public Date getExpirationDate() {
- return expirationDate;
- }
+ /**
+ * setExpirationDate<p>
+ * Set expiration date (nullable).
+ *
+ * @param expirationDate
+ */
+ public void setExpirationDate(Date expirationDate) { this.expirationDate = expirationDate; }
- public void setExpirationDate(Date expirationDate) {
- this.expirationDate = expirationDate;
- }
+ /**
+ * getReqDataHash<p>
+ * Return cached hash of request data (not exposed in JSON).
+ *
+ * @return reqDataHash
+ */
+ public String getReqDataHash() { return reqDataHash; }
- public String getReqDataHash() {
- return reqDataHash;
- }
+ /**
+ * getCodeSuffix<p>
+ * Return numeric suffix of the code.
+ *
+ * @return codeSuffix
+ */
+ public Integer getCodeSuffix() { return codeSuffix; }
- 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;
- }
+ /**
+ * setCodeSuffix<p>
+ * Set numeric suffix of the code.
+ *
+ * @param codeSuffix
+ */
+ public void setCodeSuffix(Integer codeSuffix) { this.codeSuffix = codeSuffix; }
- public static class Status {
+ /**
+ * getActivationCode<p>
+ * Return activation code.
+ *
+ * @return activationCode
+ */
+ public String getActivationCode() { return activationCode; }
- private static final Map<Integer, List<LicenseStatus>> 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) //
- );
+ /**
+ * setActivationCode<p>
+ * Set activation code.
+ *
+ * @param activationCode
+ */
+ public void setActivationCode(String activationCode) { this.activationCode = activationCode; }
- /**
- * It checks if a given action is valid for the License, passing the
- * action and the current license status
- *
- * @param oldStatus
- * @param newStatus
- * @return
- */
- public static boolean isActionValid(Integer action, LicenseStatus currentStatus) {
- List<LicenseStatus> validStatuses = transitions.get(action);
- LOG.info("Action {} is valid ? => {} current: {} OK? {}", action, validStatuses, currentStatus, validStatuses.contains(currentStatus));
- return validStatuses != null && validStatuses.contains(currentStatus);
- }
- }
+ /**
+ * isMetadataObsolete<p>
+ * Convenience Boolean → primitive with null-safe false.
+ *
+ * @return isMetadataObsolete
+ */
+ public boolean isMetadataObsolete() { return metadataObsolete != null && metadataObsolete; }
- /**
- * Return licenses with status: REquested, ACtive, Pre-Active for a given
- * request data
- *
- * @param requestData
- * @param em
- * @return
- * @throws SeCurisServiceException
- */
- public static License findValidLicenseByRequestData(String requestData, EntityManager em) throws SeCurisServiceException {
- TypedQuery<License> query = em.createNamedQuery("list-valid-licenses-by-req-data", License.class);
- query.setParameter("hash", BlockedRequest.generateHash(requestData));
- try {
- List<License> 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) {
- // There is no license for request data
- return null;
- }
- }
+ /**
+ * setMetadataObsolete<p>
+ * Set metadata obsolete flag (nullable wrapper).
+ *
+ * @param obsolete
+ */
+ public void setMetadataObsolete(Boolean obsolete) { this.metadataObsolete = obsolete; }
- /**
- * Return licenses with status: REquested, ACtive, Pre-Active for a given
- * request data
- *
- * @param requestData
- * @param em
- * @return
- * @throws SeCurisServiceException
- */
- public static License findLicenseByActivationCode(String activationCode, EntityManager em) throws SeCurisServiceException {
- TypedQuery<License> query = em.createNamedQuery("license-by-activation-code", License.class);
- query.setParameter("activationCode", activationCode);
- try {
- return query.getSingleResult();
- } catch (NoResultException e) {
- // There is no license for request data
- return null;
- }
- }
+ // ------------------------------------------------------------------
+ // Status transitions helpers
+ // ------------------------------------------------------------------
- public static License findActiveLicenseByRequestData(String requestData, EntityManager em) throws SeCurisServiceException {
- TypedQuery<License> query = em.createNamedQuery("list-active-licenses-by-req-data", License.class);
- query.setParameter("hash", BlockedRequest.generateHash(requestData));
- try {
- List<License> 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) {
- // There is no license for request data
- return null;
- }
- }
+ /**
+ * Action<p>
+ * 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;
+ }
- public static License findLicenseByCode(String code, EntityManager em) throws SeCurisServiceException {
- TypedQuery<License> query = em.createNamedQuery("license-by-code", License.class);
- query.setParameter("code", code);
- try {
- return query.getSingleResult();
- } catch (NoResultException e) {
- // There is no license for request data
- return null;
- }
- }
+ /**
+ * Status<p>
+ * Status of the requested license
+ */
+ public static class Status {
- public Integer getCodeSuffix() {
- return codeSuffix;
- }
+ private static final Map<Integer, List<LicenseStatus>> 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)
+ );
- public void setCodeSuffix(Integer codeSuffix) {
- this.codeSuffix = codeSuffix;
- }
+ /**
+ * isActionValid<p>
+ * 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<LicenseStatus> 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);
+ }
+ }
- public String getActivationCode() {
- return activationCode;
- }
+ // ------------------------------------------------------------------
+ // Repository helpers (static queries)
+ // ------------------------------------------------------------------
- public void setActivationCode(String activationCode) {
- this.activationCode = activationCode;
- }
+ /**
+ * findValidLicenseByRequestData<p>
+ * 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<License> query = em.createNamedQuery("list-valid-licenses-by-req-data", License.class);
+ query.setParameter("hash", BlockedRequest.generateHash(requestData));
+ try {
+ List<License> 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;
+ }
+ }
- public boolean isMetadataObsolete() {
- return metadataObsolete != null && metadataObsolete;
- }
+ /**
+ * findLicenseByActivationCode<p>
+ * 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<License> query = em.createNamedQuery("license-by-activation-code", License.class);
+ query.setParameter("activationCode", activationCode);
+ try {
+ return query.getSingleResult();
+ } catch (NoResultException e) {
+ return null;
+ }
+ }
- public void setMetadataObsolete(Boolean obsolete) {
- this.metadataObsolete = obsolete;
- }
+ /**
+ * findActiveLicenseByRequestData<p>
+ * 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<License> query = em.createNamedQuery("list-active-licenses-by-req-data", License.class);
+ query.setParameter("hash", BlockedRequest.generateHash(requestData));
+ try {
+ List<License> 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<p>
+ * 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<License> query = em.createNamedQuery("license-by-code", License.class);
+ query.setParameter("code", code);
+ try {
+ return query.getSingleResult();
+ } catch (NoResultException e) {
+ return null;
+ }
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java b/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java
index 7fd0b03..78f8cbd 100644
--- a/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java
+++ b/securis/src/main/java/net/curisit/securis/db/LicenseHistory.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -21,16 +24,26 @@
import com.fasterxml.jackson.annotation.JsonProperty;
/**
- * Entity implementation class for Entity: license
- *
- */
+* LicenseHistory
+* <p>
+* Audit trail entries for a given license (who/what/when).
+*
+* Mapping details:
+* - Table: license_history
+* - Many-to-one to License and User (ignored in JSON).
+* - NamedQuery: list-license-history by license id.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@Entity
@Table(name = "license_history")
@JsonIgnoreProperties(ignoreUnknown = true)
@NamedQueries({
- @NamedQuery(name = "list-license-history", query = "SELECT lh FROM LicenseHistory lh where lh.license.id = :licId")
+ @NamedQuery(name = "list-license-history",
+ query = "SELECT lh FROM LicenseHistory lh where lh.license.id = :licId")
})
public class LicenseHistory implements Serializable {
@@ -57,59 +70,117 @@
@JsonProperty("creation_timestamp")
private Date creationTimestamp;
- public int getId() {
- return id;
- }
+ // ---------------- Getters & setters ----------------
- public License getLicense() {
- return license;
- }
+ /**
+ * getId<p>
+ * Return primary key.
+ *
+ * @return id
+ */
+ public int getId() { return id; }
- public void setLicense(License license) {
- this.license = license;
- }
+ /**
+ * getLicense<p>
+ * Return parent license (entity).
+ *
+ * @return license
+ */
+ public License getLicense() { return license; }
- public User getUser() {
- return user;
- }
+ /**
+ * setLicense<p>
+ * Set parent license (entity).
+ *
+ * @return license
+ */
+ public void setLicense(License license) { this.license = license; }
+ /**
+ * getUser<p>
+ * Return actor user (entity).
+ *
+ * @return user
+ */
+ public User getUser() { return user; }
+
+ /**
+ * getUsername<p>
+ * Expose username for JSON.
+ *
+ * @return username
+ */
@JsonProperty("username")
- public String getUsername() {
- return user == null ? null : user.getUsername();
- }
+ public String getUsername() { return user == null ? null : user.getUsername(); }
- public void setUser(User user) {
- this.user = user;
- }
+ /**
+ * setUser<p>
+ * Set actor user (entity).
+ *
+ * @param user
+ */
+ public void setUser(User user) { this.user = user; }
- public String getAction() {
- return action;
- }
+ /**
+ * getAction<p>
+ * Return action key (e.g., "activate").
+ *
+ * @return action
+ */
+ public String getAction() { return action; }
- public void setAction(String action) {
- this.action = action;
- }
+ /**
+ * setAction<p>
+ * Set action key.
+ *
+ * @param action
+ */
+ public void setAction(String action) { this.action = action; }
- public String getComments() {
- return comments;
- }
+ /**
+ * getComments<p>
+ * Return optional comments.
+ *
+ * @return comments
+ */
+ public String getComments() { return comments; }
- public void setComments(String comments) {
- this.comments = comments;
- }
+ /**
+ * setComments<p>
+ * Set optional comments.
+ *
+ * @param comments
+ */
+ public void setComments(String comments) { this.comments = comments; }
- public void setId(int id) {
- this.id = id;
- }
+ /**
+ * setId<p>
+ * Set primary key (normally framework-managed)
+ *
+ * @param id.
+ */
+ public void setId(int id) { this.id = id; }
- public Date getCreationTimestamp() {
- return creationTimestamp;
- }
+ /**
+ * getCreationTimestamp<p>
+ * Return timestamp.
+ *
+ * @return creationTimestamp
+ */
+ public Date getCreationTimestamp() { return creationTimestamp; }
- public void setCreationTimestamp(Date creationTimestamp) {
- this.creationTimestamp = creationTimestamp;
- }
+ /**
+ * setCreationTimestamp<p>
+ * Set timestamp.
+ *
+ * @param creationTimestamp
+ */
+ public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
+ /**
+ * Actions<p>
+ * Catalog of action names.
+ */
public static class Actions {
public static final String CREATE = "creation";
public static final String ADD_REQUEST = "request";
@@ -125,3 +196,4 @@
public static final String DELETE = "delete";
}
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/LicenseStatus.java b/securis/src/main/java/net/curisit/securis/db/LicenseStatus.java
index df94ccb..9b60df9 100644
--- a/securis/src/main/java/net/curisit/securis/db/LicenseStatus.java
+++ b/securis/src/main/java/net/curisit/securis/db/LicenseStatus.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import net.curisit.securis.db.common.CodedEnum;
@@ -6,42 +9,64 @@
import com.fasterxml.jackson.annotation.JsonValue;
/**
- * Contains the possible license statuses. For further details:
- * https://redmine.curistec.com/projects/securis/wiki/LicensesServerManagement
- *
- * @author rob
- */
+* LicenseStatus
+* <p>
+* Enumerates the possible license states. JSON code/value is the short code (CR, RE, AC, ...).
+* See: https://redmine.curistec.com/projects/securis/wiki/LicensesServerManagement
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
public enum LicenseStatus implements CodedEnum {
CREATED("CR"), REQUESTED("RE"), ACTIVE("AC"), PRE_ACTIVE("PA"), EXPIRED("EX"), CANCELLED("CA"), BLOCKED("BL");
private final String code;
- LicenseStatus(String code) {
- this.code = code;
- }
+ /**
+ * LicenseStatus<p>
+ * Constructor
+ *
+ * @param code
+ */
+ LicenseStatus(String code) { this.code = code; }
- @Override
- public String getCode() {
- return code;
- }
+ /**
+ * getCode<p>
+ * Return the short code used in DB/JSON.
+ *
+ * @return code
+ */
+ @Override public String getCode() { return code; }
+ /**
+ * valueFromCode<p>
+ * Factory from code string (null on unknown).
+ *
+ * @param code
+ */
@JsonCreator
public static LicenseStatus valueFromCode(String code) {
for (LicenseStatus ps : LicenseStatus.values()) {
- if (ps.code.equals(code)) {
- return ps;
- }
+ if (ps.code.equals(code)) return ps;
}
return null;
}
+ /**
+ * getName<p>
+ * Expose the code as JSON value.
+ *
+ * @return name
+ */
@JsonValue
- public String getName() {
- return this.code;
- }
+ public String getName() { return this.code; }
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
@Override
- public String toString() {
- return code;
- }
+ public String toString() { return code; }
}
diff --git a/securis/src/main/java/net/curisit/securis/db/LicenseType.java b/securis/src/main/java/net/curisit/securis/db/LicenseType.java
index 1733787..b2553cd 100644
--- a/securis/src/main/java/net/curisit/securis/db/LicenseType.java
+++ b/securis/src/main/java/net/curisit/securis/db/LicenseType.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -29,135 +32,236 @@
import com.fasterxml.jackson.annotation.JsonProperty;
/**
- * Entity implementation class for Entity: license_type
- *
- */
+* LicenseType
+* <p>
+* Describes a license category within an application. Owns metadata entries.
+*
+* Mapping details:
+* - Table: license_type
+* - Many-to-one to Application (lazy).
+* - One-to-many metadata with cascade PERSIST/REMOVE/REFRESH.
+* - Named queries for listing and filtering by application(s).
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "license_type")
-@NamedQueries({ @NamedQuery(name = "list-license_types", query = "SELECT lt FROM LicenseType lt"),
- @NamedQuery(name = "list-license_types-by_apps-id", query = "SELECT lt FROM LicenseType lt where lt.application.id in :list_ids"),
- @NamedQuery(name = "list-application-license_types", query = "SELECT lt FROM LicenseType lt where lt.application.id = :appId") })
+@NamedQueries({
+ @NamedQuery(name = "list-license_types", query = "SELECT lt FROM LicenseType lt"),
+ @NamedQuery(name = "list-license_types-by_apps-id", query = "SELECT lt FROM LicenseType lt where lt.application.id in :list_ids"),
+ @NamedQuery(name = "list-application-license_types", query = "SELECT lt FROM LicenseType lt where lt.application.id = :appId")
+})
public class LicenseType implements Serializable {
- @SuppressWarnings("unused")
- private static final Logger LOG = LogManager.getLogger(LicenseType.class);
- private static final long serialVersionUID = 1L;
+ @SuppressWarnings("unused")
+ private static final Logger LOG = LogManager.getLogger(LicenseType.class);
+ private static final long serialVersionUID = 1L;
- @Id
- @GeneratedValue
- private Integer id;
+ @Id
+ @GeneratedValue
+ private Integer id;
- private String code;
- private String name;
- private String description;
+ private String code;
+ private String name;
+ private String description;
- @Column(name = "creation_timestamp")
- @JsonProperty("creation_timestamp")
- private Date creationTimestamp;
+ @Column(name = "creation_timestamp")
+ @JsonProperty("creation_timestamp")
+ private Date creationTimestamp;
- @JsonIgnore
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "application_id")
- private Application application;
+ @JsonIgnore
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "application_id")
+ private Application application;
- @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "licenseType")
- @JsonManagedReference
- private Set<LicenseTypeMetadata> metadata;
+ @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "licenseType")
+ @JsonManagedReference
+ private Set<LicenseTypeMetadata> metadata;
- public Set<LicenseTypeMetadata> getMetadata() {
- return metadata;
- }
+ // ---------------- Getters & setters ----------------
- public void setMetadata(Set<LicenseTypeMetadata> metadata) {
- this.metadata = metadata;
- }
+ /**
+ * getMetadata<p>
+ * Return associated metadata entries.
+ *
+ * @return metadata
+ */
+ public Set<LicenseTypeMetadata> getMetadata() { return metadata; }
- public Integer getId() {
- return id;
- }
+ /**
+ * setMetadata<p>
+ * Set associated metadata entries.
+ *
+ * @param metadata
+ */
+ public void setMetadata(Set<LicenseTypeMetadata> metadata) { this.metadata = metadata; }
- public void setId(Integer id) {
- this.id = id;
- }
+ /**
+ * getId<p>
+ * Return primary key.
+ *
+ * @return id
+ */
+ public Integer getId() { return id; }
- public String getName() {
- return name;
- }
+ /**
+ * setId<p>
+ * Set primary key.
+ *
+ * @param id
+ */
+ public void setId(Integer id) { this.id = id; }
- public void setName(String name) {
- this.name = name;
- }
+ /**
+ * getName<p>
+ * Return display name.
+ *
+ * @return name
+ */
+ public String getName() { return name; }
- public String getDescription() {
- return description;
- }
+ /**
+ * setName<p>
+ * Set display name.
+ *
+ * @param name
+ */
+ public void setName(String name) { this.name = name; }
- public void setDescription(String description) {
- this.description = description;
- }
+ /**
+ * getDescription<p>
+ * Return description.
+ *
+ * @return description
+ */
+ public String getDescription() { return description; }
- public String getCode() {
- return code;
- }
+ /**
+ * setDescription<p>
+ * Set description.
+ *
+ * @param description
+ */
+ public void setDescription(String description) { this.description = description; }
- public void setCode(String code) {
- this.code = code;
- }
+ /**
+ * getCode<p>
+ * Return short code.
+ *
+ * @return code
+ */
+ public String getCode() { return code; }
- public Application getApplication() {
- return application;
- }
+ /**
+ * setCode<p>
+ * Set short code.
+ *
+ * @param code
+ */
+ public void setCode(String code) { this.code = code; }
- @JsonProperty("application_name")
- public String getApplicationName() {
- return application == null ? null : application.getName();
- }
+ /**
+ * getApplication<p>
+ * Return owning application (entity).
+ *
+ * @return application
+ */
+ public Application getApplication() { return application; }
- @JsonProperty("application_id")
- public Integer getApplicationId() {
- return application == null ? null : application.getId();
- }
+ /**
+ * getApplicationName<p>
+ * Expose app name for JSON.
+ *
+ * @return appName
+ */
+ @JsonProperty("application_name")
+ public String getApplicationName() { return application == null ? null : application.getName(); }
- @JsonProperty("application_id")
- public void setApplicationId(Integer appId) {
- if (appId == null) {
- application = null;
- } else {
- application = new Application();
- application.setId(appId);
- }
- }
+ /**
+ * getApplicationId<p>
+ * Expose app id for JSON.
+ *
+ * @return appId
+ */
+ @JsonProperty("application_id")
+ public Integer getApplicationId() { return application == null ? null : application.getId(); }
- public void setApplication(Application application) {
- this.application = application;
- }
+ /**
+ * setApplicationId<p>
+ * Setter by id for JSON binding (creates shallow Application).
+ *
+ * @param appId
+ */
+ @JsonProperty("application_id")
+ public void setApplicationId(Integer appId) {
+ if (appId == null) {
+ application = null;
+ } else {
+ application = new Application();
+ application.setId(appId);
+ }
+ }
- public Date getCreationTimestamp() {
- return creationTimestamp;
- }
+ /**
+ * setApplication<p>
+ * Set owning application (entity).
+ *
+ * @param application
+ */
+ public void setApplication(Application application) { this.application = application; }
- public void setCreationTimestamp(Date creationTimestamp) {
- this.creationTimestamp = creationTimestamp;
- }
+ /**
+ * getCreationTimestamp<p>
+ * Return creation timestamp.
+ *
+ * @return creationTimestamp
+ */
+ public Date getCreationTimestamp() { return creationTimestamp; }
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof LicenseType))
- return false;
- LicenseType other = (LicenseType) obj;
- return id.equals(other.id);
- }
+ /**
+ * setCreationTimestamp<p>
+ * Set creation timestamp.
+ *
+ * @param creationTimestamp
+ */
+ public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
- @Override
- public int hashCode() {
- return (id == null ? 0 : id.hashCode());
- }
+ // ---------------- Object methods ----------------
- @Override
- public String toString() {
- return String.format("LT: ID: %d, code: %s", id, code);
- }
+ /**
+ * equals<p>
+ * Compare the current object with the given object
+ *
+ * @param object
+ * @return isEquals
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof LicenseType)) return false;
+ LicenseType other = (LicenseType) obj;
+ return id != null && id.equals(other.id);
+ }
+
+ /**
+ * hashCode<p>
+ * Get the object hashCode
+ *
+ * @return hashCode
+ */
+ @Override
+ public int hashCode() { return (id == null ? 0 : id.hashCode()); }
+
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
+ @Override
+ public String toString() { return String.format("LT: ID: %d, code: %s", id, code); }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java b/securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java
index e17ae8c..a004085 100644
--- a/securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java
+++ b/securis/src/main/java/net/curisit/securis/db/LicenseTypeMetadata.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -21,80 +24,148 @@
import net.curisit.securis.db.common.Metadata;
/**
- * Entity implementation class for Entity: licensetype_metadata
- *
- */
+* LicenseTypeMetadata
+* <p>
+* Single metadata entry attached to a {@link LicenseType}.
+* Composite PK: (license_type_id, key).
+*
+* Mapping details:
+* - Table: licensetype_metadata
+* - @JsonBackReference to avoid recursion with LicenseType.metadata.
+* - NamedQuery: list-licensetype-metadata by license type id.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@Entity
@Table(name = "licensetype_metadata")
@JsonIgnoreProperties(ignoreUnknown = true)
-@NamedQueries({ @NamedQuery(name = "list-licensetype-metadata", query = "SELECT a FROM LicenseTypeMetadata a where a.licenseType.id = :licenseTypeId") })
+@NamedQueries({
+ @NamedQuery(name = "list-licensetype-metadata",
+ query = "SELECT a FROM LicenseTypeMetadata a where a.licenseType.id = :licenseTypeId")
+})
public class LicenseTypeMetadata implements Serializable, Metadata {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- @Id
- @ManyToOne
- @JsonBackReference
- @JoinColumn(name = "license_type_id")
- private LicenseType licenseType;
+ /** PK part: owning license type. */
+ @Id
+ @ManyToOne
+ @JsonBackReference
+ @JoinColumn(name = "license_type_id")
+ private LicenseType licenseType;
- @Id
- @Column(name = "\"key\"")
- private String key;
+ /** PK part: metadata key (quoted). */
+ @Id
+ @Column(name = "\"key\"")
+ private String key;
- private String value;
+ /** Metadata value. */
+ private String value;
- private boolean mandatory;
+ /** Whether this key is mandatory for the license type. */
+ private boolean mandatory;
- public LicenseType getLicenseType() {
- return licenseType;
- }
+ // ---------------- Getters & setters ----------------
- public void setLicenseType(LicenseType licenseType) {
- this.licenseType = licenseType;
- }
+ /**
+ * getLicenseType<p>
+ * Return owning license type.
+ *
+ * @return licenseType
+ */
+ public LicenseType getLicenseType() { return licenseType; }
- public String getValue() {
- return value;
- }
+ /**
+ * setLicenseType<p>
+ * Set owning license type.
+ *
+ * @param licenseType
+ */
+ public void setLicenseType(LicenseType licenseType) { this.licenseType = licenseType; }
- public void setValue(String value) {
- this.value = value;
- }
+ /**
+ * getValue<p>
+ * Return metadata value.
+ *
+ * @return value
+ */
+ public String getValue() { return value; }
- public String getKey() {
- return key;
- }
+ /**
+ * setValue<p>
+ * Set metadata value.
+ *
+ * @param value
+ */
+ public void setValue(String value) { this.value = value; }
- public void setKey(String key) {
- this.key = key;
- }
+ /**
+ * getKey<p>
+ * Return metadata key (PK part).
+ *
+ * @return key
+ */
+ public String getKey() { return key; }
- public boolean isMandatory() {
- return mandatory;
- }
+ /**
+ * setKey<p>
+ * Set metadata key (PK part).
+ *
+ * @param key
+ */
+ public void setKey(String key) { this.key = key; }
- public void setMandatory(boolean mandatory) {
- this.mandatory = mandatory;
- }
+ /**
+ * isMandatory<p>
+ * Return whether this entry is required.
+ *
+ * @return isMandatory
+ */
+ public boolean isMandatory() { return mandatory; }
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof LicenseTypeMetadata))
- return false;
- LicenseTypeMetadata other = (LicenseTypeMetadata) obj;
- return Objects.equals(key, other.key) && Objects.equals(licenseType, other.licenseType);
- }
+ /**
+ * setMandatory<p>
+ * Set whether this entry is required.
+ *
+ * @param mandatory
+ */
+ public void setMandatory(boolean mandatory) { this.mandatory = mandatory; }
- @Override
- public int hashCode() {
- return Objects.hash(key, licenseType);
- }
+ // ---------------- Object methods ----------------
- @Override
- public String toString() {
- return String.format("LTMD (%s: %s)", key, value);
- }
+ /**
+ * equals<p>
+ * Compare the current object with the given object
+ *
+ * @param object
+ * @return isEquals
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof LicenseTypeMetadata)) return false;
+ LicenseTypeMetadata other = (LicenseTypeMetadata) obj;
+ return Objects.equals(key, other.key) && Objects.equals(licenseType, other.licenseType);
+ }
+
+ /**
+ * hashCode<p>
+ * Get the object hashCode
+ *
+ * @return hashCode
+ */
+ @Override
+ public int hashCode() { return Objects.hash(key, licenseType); }
+
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
+ @Override
+ public String toString() { return String.format("LTMD (%s: %s)", key, value); }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/Organization.java b/securis/src/main/java/net/curisit/securis/db/Organization.java
index 6b8b217..b70b8b8 100644
--- a/securis/src/main/java/net/curisit/securis/db/Organization.java
+++ b/securis/src/main/java/net/curisit/securis/db/Organization.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -32,164 +35,253 @@
import com.fasterxml.jackson.annotation.JsonProperty;
/**
- * Entity implementation class for Entity: organization
- *
- */
+* Organization
+* <p>
+* Represents a customer/tenant organization. Manages parent/children hierarchy
+* and user membership.
+*
+* Mapping details:
+* - Table: organization
+* - ManyToMany users via user_organization (ignored in default JSON).
+* - Self-referencing parent/children relation.
+* - Named queries for listing, filtering by ids, and children discovery.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "organization")
-@NamedQueries({ @NamedQuery(name = "list-organizations", query = "SELECT o FROM Organization o"),
- @NamedQuery(name = "list-organizations-by-ids", query = "SELECT o FROM Organization o where id in :list_ids"),
- @NamedQuery(name = "find-children-org", query = "SELECT o FROM Organization o where o.parentOrganization = :parentOrganization") })
+@NamedQueries({
+ @NamedQuery(name = "list-organizations", query = "SELECT o FROM Organization o"),
+ @NamedQuery(name = "list-organizations-by-ids", query = "SELECT o FROM Organization o where id in :list_ids"),
+ @NamedQuery(name = "find-children-org", query = "SELECT o FROM Organization o where o.parentOrganization = :parentOrganization")
+})
public class Organization implements Serializable {
- @SuppressWarnings("unused")
- private static final Logger LOG = LogManager.getLogger(Organization.class);
+ @SuppressWarnings("unused")
+ private static final Logger LOG = LogManager.getLogger(Organization.class);
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- @Id
- @GeneratedValue
- private Integer id;
+ @Id
+ @GeneratedValue
+ private Integer id;
- private String code;
- private String name;
- private String description;
+ private String code;
+ private String name;
+ private String description;
- @Column(name = "creation_timestamp")
- @JsonProperty("creation_timestamp")
- private Date creationTimestamp;
+ @Column(name = "creation_timestamp")
+ @JsonProperty("creation_timestamp")
+ private Date creationTimestamp;
- @JsonIgnore
- // We don't include the users to limit the size of each row a the listing
- @ManyToMany(cascade = CascadeType.REMOVE)
- @JoinTable(name = "user_organization", //
- joinColumns = { @JoinColumn(name = "organization_id", referencedColumnName = "id") }, //
- inverseJoinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") })
- private Set<User> users;
+ @JsonIgnore
+ @ManyToMany(cascade = CascadeType.REMOVE)
+ @JoinTable(name = "user_organization",
+ joinColumns = { @JoinColumn(name = "organization_id", referencedColumnName = "id") },
+ inverseJoinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") })
+ private Set<User> users;
- @JsonIgnore
- // We don't include the users to limit the size of each row a the listing
- @ManyToOne
- @JoinColumn(name = "org_parent_id")
- private Organization parentOrganization;
+ @JsonIgnore
+ @ManyToOne
+ @JoinColumn(name = "org_parent_id")
+ private Organization parentOrganization;
- @JsonIgnore
- // We don't include the users to limit the size of each row a the listing
- @OneToMany(fetch = FetchType.LAZY, mappedBy = "parentOrganization")
- private Set<Organization> childOrganizations;
+ @JsonIgnore
+ @OneToMany(fetch = FetchType.LAZY, mappedBy = "parentOrganization")
+ private Set<Organization> childOrganizations;
- public Integer getId() {
- return id;
- }
+ // ---------------- Getters & setters ----------------
- public void setId(Integer id) {
- this.id = id;
- }
+ /**
+ * getId<p>
+ * Return primary key.
+ *
+ * @return id
+ */
+ public Integer getId() { return id; }
- public String getName() {
- return name;
- }
+ /**
+ * setId<p>
+ * Set primary key.
+ *
+ * @param id
+ */
+ public void setId(Integer id) { this.id = id; }
- public void setName(String name) {
- this.name = name;
- }
+ /**
+ * getName<p>
+ * Return display name.
+ *
+ * @return name
+ */
+ public String getName() { return name; }
- public String getDescription() {
- return description;
- }
+ /**
+ * setName<p>
+ * Set display name.
+ *
+ * @param name
+ */
+ public void setName(String name) { this.name = name; }
- public void setDescription(String description) {
- this.description = description;
- }
+ /**
+ * getDescription<p>
+ * Return optional description.
+ *
+ * @return description
+ */
+ public String getDescription() { return description; }
- public String getCode() {
- return code;
- }
+ /**
+ * setDescription<p>
+ * Set optional description.
+ *
+ * @param description
+ */
+ public void setDescription(String description) { this.description = description; }
- public void setCode(String code) {
- this.code = code;
- }
+ /**
+ * getCode<p>
+ * Return short code.
+ *
+ * @return code
+ */
+ public String getCode() { return code; }
- public Date getCreationTimestamp() {
- return creationTimestamp;
- }
+ /**
+ * setCode<p>
+ * Set short code.
+ *
+ * @param code
+ */
+ public void setCode(String code) { this.code = code; }
- public void setCreationTimestamp(Date creationTimestamp) {
- this.creationTimestamp = creationTimestamp;
- }
+ /**
+ * getCreationTimestamp<p>
+ * Return creation timestamp.
+ *
+ * @return creationTimeStamp
+ */
+ public Date getCreationTimestamp() { return creationTimestamp; }
- public Set<User> getUsers() {
- return users;
- }
+ /**
+ * setCreationTimestamp<p>
+ * Set creation timestamp.
+ *
+ * @param creationTimestamp
+ */
+ public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
- public void setUsers(Set<User> users) {
- this.users = users;
- }
+ /**
+ * getUsers<p>
+ * Return member users (entity set).
+ *
+ * @return users
+ */
+ public Set<User> getUsers() { return users; }
- public Organization getParentOrganization() {
- return parentOrganization;
- }
+ /**
+ * setUsers<p>
+ * Set member users.
+ *
+ * @param users
+ */
+ public void setUsers(Set<User> users) { this.users = users; }
- public void setParentOrganization(Organization parentOrganization) {
- this.parentOrganization = parentOrganization;
- }
+ /**
+ * getParentOrganization<p>
+ * Return parent org (entity).
+ *
+ * @return parentOrganization
+ */
+ public Organization getParentOrganization() { return parentOrganization; }
- // Roberto: Following methods are necessary to include in the REST list
- // response
- // information about the referenced entities.
- @JsonProperty("org_parent_id")
- public void setParentOrgId(Integer orgId) {
- if (orgId != null) {
- parentOrganization = new Organization();
- parentOrganization.setId(orgId);
- } else {
- parentOrganization = null;
- }
- }
+ /**
+ * setParentOrganization<p>
+ * Set parent org (entity).
+ *
+ * @param parentOrganization
+ */
+ public void setParentOrganization(Organization parentOrganization) { this.parentOrganization = parentOrganization; }
- @JsonProperty("org_parent_id")
- public Integer getParentOrgId() {
- return parentOrganization == null ? null : parentOrganization.getId();
- }
+ // JSON helpers for parent organization
- @JsonProperty("org_parent_name")
- public String getParentOrgName() {
- return parentOrganization == null ? null : parentOrganization.getName();
- }
+ /**
+ * setParentOrgId<p>
+ * Setter by id (creates shallow Organization).
+ *
+ * @param orgId
+ */
+ @JsonProperty("org_parent_id")
+ public void setParentOrgId(Integer orgId) {
+ if (orgId != null) {
+ parentOrganization = new Organization();
+ parentOrganization.setId(orgId);
+ } else {
+ parentOrganization = null;
+ }
+ }
- @JsonProperty("users_ids")
- public void setUsersIds(List<String> usersIds) {
- users = new HashSet<>();
- if (usersIds != null) {
- for (String userid : usersIds) {
- User u = new User();
- u.setUsername(userid);
- users.add(u);
- }
- }
- }
+ /**
+ * getParentOrgId<p>
+ * Expose parent org id.
+ *
+ * @return parentOrgId
+ */
+ @JsonProperty("org_parent_id")
+ public Integer getParentOrgId() { return parentOrganization == null ? null : parentOrganization.getId(); }
- @JsonProperty("users_ids")
- public Set<String> getUsersIds() {
- if (users == null) {
- return null;
- }
- Set<String> ids = new HashSet<>();
- for (User user : users) {
- ids.add(user.getUsername());
- }
- return ids;
- }
+ /**
+ * getParentOrgName<p>
+ * Expose parent org name.
+ *
+ * @return parentOrgName
+ */
+ @JsonProperty("org_parent_name")
+ public String getParentOrgName() { return parentOrganization == null ? null : parentOrganization.getName(); }
- public Set<Organization> getChildOrganizations() {
- return childOrganizations;
- }
+ // JSON helpers for users
- public void setChildOrganizations(Set<Organization> childOrganizations) {
- this.childOrganizations = childOrganizations;
- }
+ /**
+ * setUsersIds<p>
+ * Replace users set from a list of usernames.
+ *
+ * @param userId list
+ */
+ @JsonProperty("users_ids")
+ public void setUsersIds(List<String> usersIds) {
+ users = new HashSet<>();
+ if (usersIds != null) {
+ for (String userid : usersIds) {
+ User u = new User();
+ u.setUsername(userid);
+ users.add(u);
+ }
+ }
+ }
+ /**
+ * getUsersIds<p>
+ * Expose member usernames.
+ *
+ * @return userId list
+ */
+ @JsonProperty("users_ids")
+ public Set<String> getUsersIds() {
+ if (users == null) return null;
+ Set<String> ids = new HashSet<>();
+ for (User user : users) { ids.add(user.getUsername()); }
+ return ids;
+ }
+
+ /** getChildOrganizations<p>Return children (entity set). */
+ public Set<Organization> getChildOrganizations() { return childOrganizations; }
+
+ /** setChildOrganizations<p>Set children (entity set). */
+ public void setChildOrganizations(Set<Organization> childOrganizations) { this.childOrganizations = childOrganizations; }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/Pack.java b/securis/src/main/java/net/curisit/securis/db/Pack.java
index aee90fa..94dd5dd 100644
--- a/securis/src/main/java/net/curisit/securis/db/Pack.java
+++ b/securis/src/main/java/net/curisit/securis/db/Pack.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -32,379 +35,606 @@
import net.curisit.integrity.commons.Utils;
/**
- * Entity implementation class for Entity: pack
- *
- */
+* Pack
+* <p>
+* Group/bundle of licenses for an organization and application (via LicenseType).
+* Tracks capacity, availability, status, and validity windows.
+*
+* Mapping details:
+* - Table: pack
+* - ManyToOne to Organization, LicenseType, User (createdBy)
+* - OneToMany licenses (lazy)
+* - Custom type: net.curisit.securis.db.common.PackStatusType
+* - Named queries for listing and filtering by org/app.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@Entity
@Table(name = "pack")
@JsonIgnoreProperties(ignoreUnknown = true)
-@NamedQueries({ @NamedQuery(name = "list-packs", query = "SELECT pa FROM Pack pa"), //
- @NamedQuery(name = "pack-by-code", query = "SELECT pa FROM Pack pa where pa.code = :code"), //
- @NamedQuery(name = "list-packs-by-lic-type", query = "SELECT pa FROM Pack pa where pa.licenseType.id = :lt_id"), //
- @NamedQuery(name = "list-packs-by-orgs-apps", query = "SELECT pa FROM Pack pa where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app "), //
- @NamedQuery(name = "list-packs-by-apps", query = "SELECT pa FROM Pack pa where pa.licenseType.application.id in :list_ids_app ") })
+@NamedQueries({
+ @NamedQuery(name = "list-packs", query = "SELECT pa FROM Pack pa"),
+ @NamedQuery(name = "pack-by-code", query = "SELECT pa FROM Pack pa where pa.code = :code"),
+ @NamedQuery(name = "list-packs-by-lic-type", query = "SELECT pa FROM Pack pa where pa.licenseType.id = :lt_id"),
+ @NamedQuery(name = "list-packs-by-orgs-apps", query = "SELECT pa FROM Pack pa where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app "),
+ @NamedQuery(name = "list-packs-by-apps", query = "SELECT pa FROM Pack pa where pa.licenseType.application.id in :list_ids_app ")
+})
public class Pack implements Serializable {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- @Id
- @GeneratedValue
- private Integer id;
+ @Id
+ @GeneratedValue
+ private Integer id;
- private String code;
+ private String code;
+ private String comments;
+ private Boolean frozen;
- private String comments;
+ @Column(name = "creation_timestamp")
+ @JsonProperty("creation_timestamp")
+ private Date creationTimestamp;
- private Boolean frozen;
+ @JsonIgnore
+ @ManyToOne
+ @JoinColumn(name = "organization_id")
+ private Organization organization;
- @Column(name = "creation_timestamp")
- @JsonProperty("creation_timestamp")
- private Date creationTimestamp;
+ @JsonIgnore
+ @ManyToOne
+ @JoinColumn(name = "license_type_id")
+ private LicenseType licenseType;
- @JsonIgnore
- @ManyToOne
- @JoinColumn(name = "organization_id")
- private Organization organization;
+ @JsonIgnore
+ @ManyToOne
+ @JoinColumn(name = "created_by")
+ private User createdBy;
- @JsonIgnore
- @ManyToOne
- @JoinColumn(name = "license_type_id")
- private LicenseType licenseType;
+ @JsonIgnore
+ @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "pack")
+ private Set<License> licenses;
- @JsonIgnore
- @ManyToOne
- @JoinColumn(name = "created_by")
- private User createdBy;
+ @Column(name = "num_licenses")
+ @JsonProperty("num_licenses")
+ private int numLicenses;
- @JsonIgnore
- @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "pack")
- private Set<License> licenses;
+ @Column(name = "init_valid_date")
+ @JsonProperty("init_valid_date")
+ private Date initValidDate;
- @Column(name = "num_licenses")
- @JsonProperty("num_licenses")
- private int numLicenses;
+ @Column(name = "end_valid_date")
+ @JsonProperty("end_valid_date")
+ private Date endValidDate;
- @Column(name = "init_valid_date")
- @JsonProperty("init_valid_date")
- private Date initValidDate;
+ @Type(type = "net.curisit.securis.db.common.PackStatusType")
+ private PackStatus status;
- @Column(name = "end_valid_date")
- @JsonProperty("end_valid_date")
- private Date endValidDate;
+ @Column(name = "license_preactivation")
+ @JsonProperty("license_preactivation")
+ private boolean licensePreactivation;
- @Type(type = "net.curisit.securis.db.common.PackStatusType")
- private PackStatus status;
+ @Column(name = "preactivation_valid_period")
+ @JsonProperty("preactivation_valid_period")
+ private Integer preactivationValidPeriod;
- @Column(name = "license_preactivation")
- @JsonProperty("license_preactivation")
- private boolean licensePreactivation;
+ @Column(name = "renew_valid_period")
+ @JsonProperty("renew_valid_period")
+ private Integer renewValidPeriod;
- @Column(name = "preactivation_valid_period")
- @JsonProperty("preactivation_valid_period")
- private Integer preactivationValidPeriod;
+ @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "pack")
+ private Set<PackMetadata> metadata;
- @Column(name = "renew_valid_period")
- @JsonProperty("renew_valid_period")
- private Integer renewValidPeriod;
+ // ---------------- Getters & setters ----------------
- @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST, CascadeType.REMOVE, CascadeType.REFRESH }, mappedBy = "pack")
- private Set<PackMetadata> metadata;
+ /**
+ * getId<p>
+ * Return primary key.
+ *
+ * @return id
+ */
+ public Integer getId() { return id; }
- public Integer getId() {
- return id;
- }
+ /**
+ * setId<p>
+ * Set primary key.
+ *
+ * @param id
+ */
+ public void setId(Integer id) { this.id = id; }
- public void setId(Integer id) {
- this.id = id;
- }
+ /**
+ * getCode<p>
+ * Return pack code.
+ *
+ * @return packCode
+ */
+ public String getCode() { return code; }
- public String getCode() {
- return code;
- }
+ /**
+ * setCode<p>
+ * Set pack code.
+ *
+ * @param packCode
+ */
+ public void setCode(String code) { this.code = code; }
- public void setCode(String code) {
- this.code = code;
- }
+ /**
+ * getCreationTimestamp<p>
+ * Return creation timestamp.
+ *
+ * @return creationTimestamp
+ */
+ public Date getCreationTimestamp() { return creationTimestamp; }
- public Date getCreationTimestamp() {
- return creationTimestamp;
- }
+ /**
+ * setCreationTimestamp<p>
+ * Set creation timestamp.
+ *
+ * @param creationTimestamp
+ */
+ public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
- public void setCreationTimestamp(Date creationTimestamp) {
- this.creationTimestamp = creationTimestamp;
- }
+ /**
+ * getOrganization<p>
+ * Return owning organization (entity).
+ *
+ * @return organization
+ */
+ public Organization getOrganization() { return organization; }
- public Organization getOrganization() {
- return organization;
- }
+ /**
+ * setOrganization<p>
+ * Set owning organization (entity).
+ *
+ * @param organization
+ */
+ public void setOrganization(Organization organization) { this.organization = organization; }
- public void setOrganization(Organization organization) {
- this.organization = organization;
- }
+ /**
+ * getLicenseType<p>
+ * Return license type (entity).
+ *
+ * @return licenseType
+ */
+ public LicenseType getLicenseType() { return licenseType; }
- public LicenseType getLicenseType() {
- return licenseType;
- }
+ /**
+ * setLicenseType<p>
+ * Set license type (entity).
+ *
+ * @param licenseType
+ */
+ public void setLicenseType(LicenseType licenseType) { this.licenseType = licenseType; }
- public void setLicenseType(LicenseType licenseType) {
- this.licenseType = licenseType;
- }
+ /**
+ * getCreatedBy<p>
+ * Return creator (entity).
+ *
+ * @return user
+ */
+ public User getCreatedBy() { return createdBy; }
- public User getCreatedBy() {
- return createdBy;
- }
+ /**
+ * setCreatedBy<p>
+ * Set creator (entity).
+ *
+ * @param user
+ */
+ public void setCreatedBy(User createdBy) { this.createdBy = createdBy; }
- public void setCreatedBy(User createdBy) {
- this.createdBy = createdBy;
- }
+ /**
+ * getNumLicenses<p>
+ * Return capacity (licenses).
+ *
+ * @return numLicenses
+ * Number of licenses
+ */
+ public int getNumLicenses() { return numLicenses; }
- public int getNumLicenses() {
- return numLicenses;
- }
+ /**
+ * setNumLicenses<p>
+ * Set capacity (licenses).
+ *
+ * @param numLicenses
+ * Number of licenses
+ */
+ public void setNumLicenses(int numLicenses) { this.numLicenses = numLicenses; }
- public void setNumLicenses(int numLicenses) {
- this.numLicenses = numLicenses;
- }
+ /**
+ * getNumActivations<p>
+ * Count ACTIVE/PRE_ACTIVE licenses in this pack.
+ *
+ * @return numActivations
+ * number of activated licenses
+ */
+ @JsonProperty("num_activations")
+ public int getNumActivations() {
+ if (licenses == null) return 0;
+ int num = 0;
+ for (License lic : licenses) {
+ if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE) num++;
+ }
+ return num;
+ }
- @JsonProperty("num_activations")
- public int getNumActivations() {
- if (licenses == null) {
- return 0;
- }
- int num = 0;
- for (License lic : licenses) {
- if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE) {
- num++;
- }
- }
- return num;
- }
+ /**
+ * getNumCreations<p>
+ * Count all created licenses (including waiting for activation). Ignores CANCELLED.
+ *
+ * @return numCreations
+ * number of created licenses
+ */
+ @JsonProperty("num_creations")
+ public int getNumCreations() {
+ if (licenses == null) return 0;
+ int num = 0;
+ for (License lic : licenses) {
+ if (lic.getStatus() != LicenseStatus.CANCELLED) num++;
+ }
+ return num;
+ }
- /**
- * Counts all created licenses, It counts active licenses and licenses
- * waiting for activation This number will be used to control the max number
- * of licenses created. Ignore canceled licenses.
- *
- * @return
- */
- @JsonProperty("num_creations")
- public int getNumCreations() {
- if (licenses == null) {
- return 0;
- }
- int num = 0;
- for (License lic : licenses) {
- if (lic.getStatus() != LicenseStatus.CANCELLED) {
- num++;
- }
- }
- return num;
- }
+ /**
+ * getNumAvailables<p>
+ * Number of available licenses in this pack: capacity - activations.
+ *
+ * @return numAvailable
+ * Number of available licenses
+ */
+ @JsonProperty("num_available")
+ public int getNumAvailables() { return numLicenses - getNumActivations(); }
- /**
- * Number of available licenses in this pack
- *
- * @return
- */
- @JsonProperty("num_available")
- public int getNumAvailables() {
- return numLicenses - getNumActivations();
- }
+ /**
+ * getOrgName<p>
+ * Expose organization name.
+ *
+ * @return orgName
+ */
+ @JsonProperty("organization_name")
+ public String getOrgName() { return organization == null ? null : organization.getName(); }
- @JsonProperty("organization_name")
- public String getOrgName() {
- return organization == null ? null : organization.getName();
- }
+ /**
+ * getAppName<p>
+ * Expose application name via license type.
+ *
+ * @return appName
+ */
+ @JsonProperty("application_name")
+ public String getAppName() {
+ if (licenseType == null) return null;
+ Application app = licenseType.getApplication();
+ return app == null ? null : app.getName();
+ }
- @JsonProperty("application_name")
- public String getAppName() {
- if (licenseType == null) {
- return null;
- }
- Application app = licenseType.getApplication();
- return app == null ? null : app.getName();
- }
+ /**
+ * getOrgId<p>
+ * Expose organization id.
+ *
+ * @return orgId
+ */
+ @JsonProperty("organization_id")
+ public Integer getOrgId() { return organization == null ? null : organization.getId(); }
- @JsonProperty("organization_id")
- public Integer getOrgId() {
- return organization == null ? null : organization.getId();
- }
+ /**
+ * setOrgId<p>
+ * Setter by id for JSON binding (creates shallow Organization).
+ *
+ * @param orgId
+ */
+ @JsonProperty("organization_id")
+ public void setOrgId(Integer idOrg) {
+ if (idOrg == null) {
+ organization = null;
+ } else {
+ organization = new Organization();
+ organization.setId(idOrg);
+ }
+ }
- @JsonProperty("organization_id")
- public void setOrgId(Integer idOrg) {
- if (idOrg == null) {
- organization = null;
- } else {
- organization = new Organization();
- organization.setId(idOrg);
- }
- }
+ /**
+ * setLicTypeId<p>
+ * Setter by id for JSON binding (creates shallow LicenseType).
+ *
+ * @param licTypeId
+ */
+ @JsonProperty("license_type_id")
+ public void setLicTypeId(Integer idLT) {
+ if (idLT == null) {
+ licenseType = null;
+ } else {
+ licenseType = new LicenseType();
+ licenseType.setId(idLT);
+ }
+ }
- @JsonProperty("license_type_id")
- public void setLicTypeId(Integer idLT) {
- if (idLT == null) {
- licenseType = null;
- } else {
- licenseType = new LicenseType();
- licenseType.setId(idLT);
- }
- }
+ /**
+ * getLicTypeId<p>
+ * Expose license type id.
+ *
+ * @return licTypeId
+ */
+ @JsonProperty("license_type_id")
+ public Integer getLicTypeId() { return licenseType == null ? null : licenseType.getId(); }
- @JsonProperty("license_type_id")
- public Integer getLicTypeId() {
- return licenseType == null ? null : licenseType.getId();
- }
+ /**
+ * getCreatedById<p>
+ * Expose creator username.
+ *
+ * @return username
+ */
+ @JsonProperty("created_by_id")
+ public String getCreatedById() { return createdBy == null ? null : createdBy.getUsername(); }
- @JsonProperty("created_by_id")
- public String getCreatedById() {
- return createdBy == null ? null : createdBy.getUsername();
- }
+ /**
+ * setCreatedById<p>
+ * Setter by username (creates shallow User).
+ *
+ * @param username
+ */
+ @JsonProperty("created_by_id")
+ public void setCreatedById(String username) {
+ createdBy = new User();
+ createdBy.setUsername(username);
+ }
- @JsonProperty("created_by_id")
- public void setCreatedById(String username) {
- createdBy = new User();
- createdBy.setUsername(username);
- }
+ /**
+ * getCreatedByname<p>
+ * Expose creator full display name.
+ *
+ * @return userName
+ */
+ @JsonProperty("created_by_name")
+ public String getCreatedByname() {
+ return createdBy == null ? null
+ : String.format("%s %s (%s)", createdBy.getFirstName(),
+ createdBy.getLastName() != null ? createdBy.getLastName() : "",
+ createdBy.getUsername());
+ }
- @JsonProperty("created_by_name")
- public String getCreatedByname() {
- return createdBy == null ? null
- : String.format("%s %s (%s)", createdBy.getFirstName(), createdBy.getLastName() != null ? createdBy.getLastName() : "", createdBy.getUsername());
- }
+ /**
+ * getLicenseTypeCode<p>
+ * Expose license type code.
+ *
+ * @return licenseTypeCode
+ */
+ @JsonProperty("licensetype_code")
+ public String getLicenseTypeCode() { return licenseType == null ? null : licenseType.getCode(); }
- @JsonProperty("licensetype_code")
- public String getLicenseTypeCode() {
- return licenseType == null ? null : licenseType.getCode();
- }
+ /**
+ * getComments<p>
+ * Return comments.
+ *
+ * @return comments
+ */
+ public String getComments() { return comments; }
- public String getComments() {
- return comments;
- }
+ /**
+ * setComments<p>
+ * Set comments.
+ *
+ * @param comments
+ */
+ public void setComments(String comments) { this.comments = comments; }
- public void setComments(String comments) {
- this.comments = comments;
- }
+ /**
+ * isLicensePreactivation<p>
+ * Whether licenses are pre-activated.
+ *
+ * @return isLicensePreactivation
+ */
+ public boolean isLicensePreactivation() { return licensePreactivation; }
- public boolean isLicensePreactivation() {
- return licensePreactivation;
- }
+ /**
+ * setLicensePreactivation<p>
+ * Set pre-activation flag.
+ *
+ * @param licensePreactivation
+ */
+ public void setLicensePreactivation(boolean licensePreactivation) { this.licensePreactivation = licensePreactivation; }
- public void setLicensePreactivation(boolean licensePreactivation) {
- this.licensePreactivation = licensePreactivation;
- }
+ /**
+ * getMetadata<p>
+ * Return pack metadata entries.
+ *
+ * @return metadata
+ */
+ public Set<PackMetadata> getMetadata() { return metadata; }
- public Set<PackMetadata> getMetadata() {
- return metadata;
- }
+ /**
+ * setMetadata<p>
+ * Set pack metadata entries.
+ *
+ * @param metadata
+ */
+ public void setMetadata(Set<PackMetadata> metadata) { this.metadata = metadata; }
- public void setMetadata(Set<PackMetadata> metadata) {
- this.metadata = metadata;
- }
+ /**
+ * getStatus<p>
+ * Return pack status.
+ *
+ * @return packStatus
+ */
+ public PackStatus getStatus() { return status; }
- public PackStatus getStatus() {
- return status;
- }
+ /**
+ * setStatus<p>
+ * Set pack status.
+ *
+ * @param packStatus
+ */
+ public void setStatus(PackStatus status) { this.status = status; }
- public void setStatus(PackStatus status) {
- this.status = status;
- }
+ /**
+ * getInitValidDate<p>
+ * Return start of validity window.
+ *
+ * @return initValidDate
+ */
+ public Date getInitValidDate() { return initValidDate; }
- public Date getInitValidDate() {
- return initValidDate;
- }
+ /**
+ * setInitValidDate<p>
+ * Set start of validity window.
+ *
+ * @param initValidDate
+ */
+ public void setInitValidDate(Date initValidDate) { this.initValidDate = initValidDate; }
- public void setInitValidDate(Date initValidDate) {
- this.initValidDate = initValidDate;
- }
+ /**
+ * getEndValidDate<p>
+ * Return end of validity window.
+ *
+ * @return endValidDate
+ */
+ public Date getEndValidDate() { return endValidDate; }
- public Date getEndValidDate() {
- return endValidDate;
- }
+ /**
+ * setEndValidDate<p>
+ * Set end of validity window.
+ *
+ * @param endValidDate
+ */
+ public void setEndValidDate(Date endValidDate) { this.endValidDate = endValidDate; }
- public void setEndValidDate(Date endValidDate) {
- this.endValidDate = endValidDate;
- }
+ /**
+ * getLicenses<p>
+ * Return contained licenses (entity set).
+ *
+ * @return licenses
+ */
+ public Set<License> getLicenses() { return licenses; }
- public Set<License> getLicenses() {
- return licenses;
- }
+ /**
+ * setLicenses<p>
+ * Set contained licenses (entity set).
+ *
+ * @param licenses
+ */
+ public void setLicenses(Set<License> licenses) { this.licenses = licenses; }
- public void setLicenses(Set<License> licenses) {
- this.licenses = licenses;
- }
+ /**
+ * getPreactivationValidPeriod<p>
+ * Return preactivation validity (days).
+ *
+ * @return preactivationValidPeriod
+ */
+ public Integer getPreactivationValidPeriod() { return preactivationValidPeriod; }
- public Integer getPreactivationValidPeriod() {
- return preactivationValidPeriod;
- }
+ /**
+ * setPreactivationValidPeriod<p>
+ * Set preactivation validity (days).
+ *
+ * @param preactivationValidPeriod
+ */
+ public void setPreactivationValidPeriod(Integer preactivationValidPeriod) { this.preactivationValidPeriod = preactivationValidPeriod; }
- public void setPreactivationValidPeriod(Integer preactivationValidPeriod) {
- this.preactivationValidPeriod = preactivationValidPeriod;
- }
+ /**
+ * getRenewValidPeriod<p>
+ * Return renewal validity (days).
+ *
+ * @return renewValidPeriod
+ */
+ public Integer getRenewValidPeriod() { return renewValidPeriod; }
- public Integer getRenewValidPeriod() {
- return renewValidPeriod;
- }
+ /**
+ * setRenewValidPeriod<p>
+ * Set renewal validity (days).
+ *
+ * @param renewValidPeriod
+ */
+ public void setRenewValidPeriod(Integer renewValidPeriod) { this.renewValidPeriod = renewValidPeriod; }
- public void setRenewValidPeriod(Integer renewValidPeriod) {
- this.renewValidPeriod = renewValidPeriod;
- }
+ // ---------------- Object methods ----------------
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof Application))
- return false;
- return id.equals(Pack.class.cast(obj).id);
- }
+ /**
+ * equals<p>
+ * Compare the current object with the given object
+ *
+ * @param object
+ * @return isEquals
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Application)) return false;
+ return id != null && id.equals(Pack.class.cast(obj).id);
+ }
- @Override
- public int hashCode() {
- return (id == null ? 0 : id.hashCode());
- }
+ /**
+ * hashCode<p>
+ * Get the object hashCode
+ *
+ * @return hashCode
+ */
+ @Override
+ public int hashCode() { return (id == null ? 0 : id.hashCode()); }
- @Override
- public String toString() {
- return String.format("Pack: ID: %d, code: %s", id, code);
- }
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
+ @Override
+ public String toString() { return String.format("Pack: ID: %d, code: %s", id, code); }
- public boolean isFrozen() {
- return frozen != null && frozen;
- }
+ /**
+ * isFrozen<p>
+ * Null-safe boolean getter.
+ *
+ * @return isFrozen
+ */
+ public boolean isFrozen() { return frozen != null && frozen; }
- public void setFrozen(Boolean frozen) {
- this.frozen = frozen;
- }
+ /**
+ * setFrozen<p>
+ * Set frozen flag (nullable wrapper).
+ *
+ * @param frozen
+ */
+ public void setFrozen(Boolean frozen) { this.frozen = frozen; }
- public static class Action {
- public static final int CREATE = 1;
- public static final int ACTIVATION = 2;
- public static final int PUT_ONHOLD = 3;
- public static final int CANCEL = 4;
- public static final int DELETE = 5;
- }
+ // ---------------- Status transitions ----------------
- public static class Status {
+ /**
+ * Action<p>
+ * Available actions for the Pack
+ */
+ public static class Action {
+ public static final int CREATE = 1;
+ public static final int ACTIVATION = 2;
+ public static final int PUT_ONHOLD = 3;
+ public static final int CANCEL = 4;
+ public static final int DELETE = 5;
+ }
- private static final Map<Integer, List<PackStatus>> transitions = Utils.createMap( //
- Action.ACTIVATION, Arrays.asList(PackStatus.CREATED, PackStatus.ON_HOLD, PackStatus.EXPIRED), //
- Action.PUT_ONHOLD, Arrays.asList(PackStatus.ACTIVE), //
- Action.CANCEL, Arrays.asList(PackStatus.ACTIVE, PackStatus.ON_HOLD, PackStatus.EXPIRED), //
- Action.DELETE, Arrays.asList(PackStatus.CANCELLED, PackStatus.CREATED) //
- );
+ /**
+ * Status<p>
+ * Pack status
+ */
+ public static class Status {
- /**
- * It checks if a given action is valid for the License, passing the
- * action and the current license status
- *
- * @param oldStatus
- * @param newStatus
- * @return
- */
- public static boolean isActionValid(Integer action, PackStatus currentStatus) {
- List<PackStatus> validStatuses = transitions.get(action);
+ private static final Map<Integer, List<PackStatus>> transitions = Utils.createMap(
+ Action.ACTIVATION, Arrays.asList(PackStatus.CREATED, PackStatus.ON_HOLD, PackStatus.EXPIRED),
+ Action.PUT_ONHOLD, Arrays.asList(PackStatus.ACTIVE),
+ Action.CANCEL, Arrays.asList(PackStatus.ACTIVE, PackStatus.ON_HOLD, PackStatus.EXPIRED),
+ Action.DELETE, Arrays.asList(PackStatus.CANCELLED, PackStatus.CREATED)
+ );
- return validStatuses != null && validStatuses.contains(currentStatus);
- }
- }
+ /**
+ * isActionValid<p>
+ * Validate whether an action is allowed given the current pack status.
+ *
+ * @param action action constant
+ * @param currentStatus current pack status
+ * @return true if allowed
+ */
+ public static boolean isActionValid(Integer action, PackStatus currentStatus) {
+ List<PackStatus> validStatuses = transitions.get(action);
+ return validStatuses != null && validStatuses.contains(currentStatus);
+ }
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/PackMetadata.java b/securis/src/main/java/net/curisit/securis/db/PackMetadata.java
index d35b22c..e690db5 100644
--- a/securis/src/main/java/net/curisit/securis/db/PackMetadata.java
+++ b/securis/src/main/java/net/curisit/securis/db/PackMetadata.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -22,106 +25,201 @@
import net.curisit.securis.db.common.Metadata;
/**
- * Entity implementation class for Entity: pack_metadata
- *
- */
+* PackMetadata
+* <p>
+* Single metadata entry (key/value/flags) attached to a {@link Pack}.
+* Uses composite PK: (pack_id, key).
+*
+* Mapping details:
+* - Table: pack_metadata
+* - PK: pack_id + key (two @Id fields)
+* - 'pack' is @JsonIgnore to reduce payload size in list views
+* - NamedQuery: list-pack-metadata by pack id
+*
+* Flags:
+* - readonly: UI hint to prevent edits
+* - mandatory: requires value on pack creation/updates
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@Entity
@Table(name = "pack_metadata")
@JsonIgnoreProperties(ignoreUnknown = true)
-@NamedQueries({ @NamedQuery(name = "list-pack-metadata", query = "SELECT a FROM PackMetadata a where a.pack.id = :packId") })
+@NamedQueries({
+ @NamedQuery(name = "list-pack-metadata",
+ query = "SELECT a FROM PackMetadata a where a.pack.id = :packId")
+})
public class PackMetadata implements Serializable, Metadata {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- @Id
- @JsonIgnore
- @ManyToOne
- @JoinColumn(name = "pack_id")
- private Pack pack;
+ /** PK part: owning pack (ignored in JSON). */
+ @Id
+ @JsonIgnore
+ @ManyToOne
+ @JoinColumn(name = "pack_id")
+ private Pack pack;
- @Id
- @Column(name = "\"key\"")
- private String key;
+ /** PK part: metadata key (quoted column name). */
+ @Id
+ @Column(name = "\"key\"")
+ private String key;
- private String value;
+ /** Metadata value. */
+ private String value;
- private boolean readonly;
+ /** Whether this field can be edited by clients. */
+ private boolean readonly;
- private boolean mandatory;
+ /** Whether this field is required. */
+ private boolean mandatory;
- @JsonProperty("pack_id")
- public Integer getPackId() {
- return pack == null ? null : pack.getId();
- }
+ // -------- JSON helpers to expose pack id --------
- @JsonProperty("pack_id")
- public void setLicenseTypeId(Integer idPack) {
- if (idPack == null) {
- pack = null;
- } else {
- pack = new Pack();
- pack.setId(idPack);
- }
- }
+ /**
+ * getPackId<p>
+ * Expose pack id as JSON scalar.
+ *
+ * @return packId
+ */
+ @JsonProperty("pack_id")
+ public Integer getPackId() {
+ return pack == null ? null : pack.getId();
+ }
- public Pack getPack() {
- return pack;
- }
+ /**
+ * setLicenseTypeId<p>
+ * Setter by id (creates shallow Pack).
+ *
+ * @param packId
+ */
+ @JsonProperty("pack_id")
+ public void setLicenseTypeId(Integer idPack) {
+ if (idPack == null) {
+ pack = null;
+ } else {
+ pack = new Pack();
+ pack.setId(idPack);
+ }
+ }
- public void setPack(Pack pack) {
- this.pack = pack;
- }
+ // -------- Getters & setters --------
- public String getValue() {
- return value;
- }
+ /**
+ * getPack<p>
+ * Return owning pack (entity).
+ *
+ * @return pack
+ */
+ public Pack getPack() { return pack; }
- public void setValue(String value) {
- this.value = value;
- }
+ /**
+ * setPack<p>
+ * Set owning pack (entity).
+ *
+ * @param pack
+ */
+ public void setPack(Pack pack) { this.pack = pack; }
- public String getKey() {
- return key;
- }
+ /**
+ * getValue<p>
+ * Return metadata value.
+ *
+ * @return metadataValue
+ */
+ public String getValue() { return value; }
- public void setKey(String key) {
- this.key = key;
- }
+ /**
+ * setValue<p>
+ * Set the metadata value.
+ *
+ * @param metadataValue
+ */
+ public void setValue(String value) { this.value = value; }
- public boolean isReadonly() {
- return readonly;
- }
+ /**
+ * getKey<p>
+ * Return metadata key (PK part).
+ *
+ * @return key
+ */
+ public String getKey() { return key; }
- public void setReadonly(boolean readonly) {
- this.readonly = readonly;
- }
+ /**
+ * setKey<p>
+ * Set metadata key (PK part).
+ *
+ * @param key
+ */
+ public void setKey(String key) { this.key = key; }
- public boolean isMandatory() {
- return mandatory;
- }
+ /**
+ * isReadonly<p>
+ * Return read-only flag.
+ *
+ * @return isReadonly
+ */
+ public boolean isReadonly() { return readonly; }
- public void setMandatory(boolean mandatory) {
- this.mandatory = mandatory;
- }
+ /**
+ * setReadonly<p>
+ * Set read-only flag.
+ *
+ * @param readonly
+ */
+ public void setReadonly(boolean readonly) { this.readonly = readonly; }
- @Override
- public String toString() {
- return String.format("PackMD (%s: %s)", key, value);
- }
+ /**
+ * isMandatory<p>
+ * Return mandatory flag.
+ *
+ * @return isMandatory
+ */
+ public boolean isMandatory() { return mandatory; }
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof PackMetadata))
- return false;
- PackMetadata other = (PackMetadata) obj;
- return Objects.equals(key, other.key) && Objects.equals(pack, other.pack);
- }
+ /**
+ * setMandatory<p>
+ * Set mandatory flag.
+ *
+ * @param mandatory
+ */
+ public void setMandatory(boolean mandatory) { this.mandatory = mandatory; }
- @Override
- public int hashCode() {
- return Objects.hash(key, pack);
- }
+ // -------- Object methods --------
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
+ @Override
+ public String toString() { return String.format("PackMD (%s: %s)", key, value); }
+
+ /**
+ * equals<p>
+ * Compare the current object with the given object
+ *
+ * @param object
+ * @return isEquals
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PackMetadata)) return false;
+ PackMetadata other = (PackMetadata) obj;
+ return Objects.equals(key, other.key) && Objects.equals(pack, other.pack);
+ }
+
+ /**
+ * hashCode<p>
+ * Get the object hashCode
+ *
+ * @return hashCode
+ */
+ @Override
+ public int hashCode() { return Objects.hash(key, pack); }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/PackStatus.java b/securis/src/main/java/net/curisit/securis/db/PackStatus.java
index 2588fe2..aa1a841 100644
--- a/securis/src/main/java/net/curisit/securis/db/PackStatus.java
+++ b/securis/src/main/java/net/curisit/securis/db/PackStatus.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import net.curisit.securis.db.common.CodedEnum;
@@ -6,38 +9,58 @@
import com.fasterxml.jackson.annotation.JsonValue;
/**
- * Contains the possible pack statuses. For further details:
- * https://redmine.curistec.com/projects/securis/wiki/LicensesServerManagement
- *
- * @author rob
- */
+* PackStatus
+* <p>
+* Enumerates possible pack lifecycle statuses. JSON representation is the short code.
+* See: https://redmine.curistec.com/projects/securis/wiki/LicensesServerManagement
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
public enum PackStatus implements CodedEnum {
+
+ // Available status for the pack
CREATED("CR"), ACTIVE("AC"), ON_HOLD("OH"), EXPIRED("EX"), CANCELLED("CA");
private final String code;
- PackStatus(String code) {
- this.code = code;
- }
+ /**
+ * PackStatus<p>
+ * Constructor
+ *
+ * @param code
+ */
+ PackStatus(String code) { this.code = code; }
- @Override
- public String getCode() {
- return code;
- }
+ /**
+ * getCode<p>
+ * Short code stored in DB / used in JSON.
+ *
+ * @return packCode
+ */
+ @Override public String getCode() { return code; }
+ /**
+ * valueFromCode<p>
+ * Factory method from short code.
+ *
+ * @param packCode
+ * @return packStatus
+ */
@JsonCreator
public static PackStatus valueFromCode(String code) {
for (PackStatus ps : PackStatus.values()) {
- if (ps.code.equals(code)) {
- return ps;
- }
+ if (ps.code.equals(code)) return ps;
}
return null;
}
+ /**
+ * getName<p>
+ * Expose short code as JSON value.
+ *
+ * @return name
+ */
@JsonValue
- public String getName() {
- return this.code;
- }
-
+ public String getName() { return this.code; }
}
diff --git a/securis/src/main/java/net/curisit/securis/db/Settings.java b/securis/src/main/java/net/curisit/securis/db/Settings.java
index 74336e1..ed28e2c 100644
--- a/securis/src/main/java/net/curisit/securis/db/Settings.java
+++ b/securis/src/main/java/net/curisit/securis/db/Settings.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -20,63 +23,103 @@
import com.fasterxml.jackson.annotation.JsonProperty;
/**
- * Entity implementation class for Entity: settings settings is a table that has
- * rows with 3 columns: "key", "value", "timestamp"
- *
- */
-@Entity()
-@EntityListeners({
- ModificationTimestampListener.class
-})
+* Settings
+* <p>
+* Simple key/value store with last modification timestamp.
+* Table rows have columns: "key", "value", "modification_timestamp".
+*
+* Mapping details:
+* - Table: settings
+* - Listeners: {@link ModificationTimestampListener}
+* - NamedQuery: get-param by key
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
+@Entity
+@EntityListeners({ ModificationTimestampListener.class })
@Table(name = "settings")
@NamedQueries({
@NamedQuery(name = "get-param", query = "SELECT p FROM Settings p where p.key = :key")
})
public class Settings implements ModificationTimestampEntity, Serializable {
+
@SuppressWarnings("unused")
private static final Logger LOG = LogManager.getLogger(Settings.class);
private static final long serialVersionUID = 1L;
+ /** Primary key: setting key. */
@Id
String key;
+ /** Setting value as string. */
String value;
+ /** Last modification timestamp. */
@Column(name = "modification_timestamp")
@JsonProperty("modification_timestamp")
private Date modificationTimestamp;
- public String getKey() {
- return key;
- }
+ // -------- Getters/setters --------
- public void setKey(String key) {
- this.key = key;
- }
+ /**
+ * getKey<p>
+ * Return setting key.
+ *
+ * @return key
+ */
+ public String getKey() { return key; }
- public String getValue() {
- return value;
- }
+ /**
+ * setKey<p>
+ * Set setting key.
+ *
+ * @param key
+ */
+ public void setKey(String key) { this.key = key; }
- public void setValue(String value) {
- this.value = value;
- }
+ /**
+ * getValue<p>
+ * Return value.
+ *
+ * @return value
+ */
+ public String getValue() { return value; }
+ /**
+ * setValue<p>
+ * Set value.
+ *
+ * @param value
+ */
+ public void setValue(String value) { this.value = value; }
+
+ /**
+ * getModificationTimestamp<p>
+ * Required by ModificationTimestampEntity.
+ *
+ * @return modificationTimestamp
+ */
@Override
- public Date getModificationTimestamp() {
- return modificationTimestamp;
- }
+ public Date getModificationTimestamp() { return modificationTimestamp; }
+ /**
+ * setModificationTimestamp<p>
+ * Required by ModificationTimestampEntity.
+ *
+ * @param modificationTimestamp
+ */
@Override
- public void setModificationTimestamp(Date modificationTimestamp) {
- this.modificationTimestamp = modificationTimestamp;
- }
+ public void setModificationTimestamp(Date modificationTimestamp) { this.modificationTimestamp = modificationTimestamp; }
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
@Override
- public String toString() {
-
- return String.format("{key: %s, value: %s, ts: %s}", key, value, modificationTimestamp);
- }
-
+ public String toString() { return String.format("{key: %s, value: %s, ts: %s}", key, value, modificationTimestamp); }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/User.java b/securis/src/main/java/net/curisit/securis/db/User.java
index 052082f..13a0f51 100644
--- a/securis/src/main/java/net/curisit/securis/db/User.java
+++ b/securis/src/main/java/net/curisit/securis/db/User.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db;
import java.io.Serializable;
@@ -27,275 +30,426 @@
import com.fasterxml.jackson.annotation.JsonProperty;
/**
- * Entity implementation class for Entity: Users
- *
- */
+* User
+* <p>
+* Application user with bitmask-based roles and membership in organizations
+* and applications. Exposes convenience JSON properties to fetch/set related
+* entity IDs without fetching full entities.
+*
+* Mapping details:
+* - Table: user
+* - ManyToMany organizations via user_organization
+* - ManyToMany applications via user_application
+* - Named queries: list-users, get-user, auth-user, delete-all-users
+*
+* Roles:
+* - Stored as integer bitmask; see {@link Rol}.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@JsonAutoDetect
@JsonInclude(Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Entity
@Table(name = "user")
-@NamedQueries({ @NamedQuery(name = "list-users", query = "SELECT u FROM User u"), @NamedQuery(name = "get-user", query = "SELECT u FROM User u where u.username = :username"),
- @NamedQuery(name = "auth-user", query = "SELECT u FROM User u where u.username = :username and u.password = :password"),
- @NamedQuery(name = "delete-all-users", query = "delete FROM User u") })
+@NamedQueries({
+ @NamedQuery(name = "list-users", query = "SELECT u FROM User u"),
+ @NamedQuery(name = "get-user", query = "SELECT u FROM User u where u.username = :username"),
+ @NamedQuery(name = "auth-user", query = "SELECT u FROM User u where u.username = :username and u.password = :password"),
+ @NamedQuery(name = "delete-all-users", query = "delete FROM User u")
+})
public class User implements Serializable {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- @Id
- private String username;
+ /** Username (PK). */
+ @Id
+ private String username;
- private String password;
+ /** Password hash/string (not exposed in JSON). */
+ private String password;
- @JsonProperty(value = "first_name")
- @Column(name = "first_name")
- private String firstName;
+ @JsonProperty("first_name")
+ @Column(name = "first_name")
+ private String firstName;
- @JsonProperty(value = "last_name")
- @Column(name = "last_name")
- private String lastName;
+ @JsonProperty("last_name")
+ @Column(name = "last_name")
+ private String lastName;
- private int roles;
+ /** Roles bitmask (see Rol constants). */
+ private int roles;
- @Column(name = "last_login")
- private Date lastLogin;
+ @Column(name = "last_login")
+ private Date lastLogin;
- @Column(name = "modification_timestamp")
- private Date modificationTimestamp;
+ @Column(name = "modification_timestamp")
+ private Date modificationTimestamp;
- @Column(name = "creation_timestamp")
- @JsonProperty("creation_timestamp")
- private Date creationTimestamp;
+ @Column(name = "creation_timestamp")
+ @JsonProperty("creation_timestamp")
+ private Date creationTimestamp;
- private String lang;
+ private String lang;
+ private String email;
- private String email;
+ @JsonIgnore
+ @ManyToMany
+ @JoinTable(name = "user_organization",
+ joinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") },
+ inverseJoinColumns = { @JoinColumn(name = "organization_id", referencedColumnName = "id") })
+ private Set<Organization> organizations;
- @JsonIgnore
- @ManyToMany
- @JoinTable(name = "user_organization", //
- joinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") }, //
- inverseJoinColumns = { @JoinColumn(name = "organization_id", referencedColumnName = "id") } //
- )
- private Set<Organization> organizations;
+ @JsonIgnore
+ @ManyToMany
+ @JoinTable(name = "user_application",
+ joinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") },
+ inverseJoinColumns = { @JoinColumn(name = "application_id", referencedColumnName = "id") })
+ private Set<Application> applications;
- @JsonIgnore
- @ManyToMany
- @JoinTable(name = "user_application", //
- joinColumns = { @JoinColumn(name = "username", referencedColumnName = "username") }, //
- inverseJoinColumns = { @JoinColumn(name = "application_id", referencedColumnName = "id") } //
- )
- private Set<Application> applications;
+ // -------- Getters & setters --------
- public String getUsername() {
- return username;
- }
+ /**
+ * getUsername<p>
+ * Return username (PK).
+ *
+ * @return username
+ */
+ public String getUsername() { return username; }
- public void setUsername(String username) {
- this.username = username;
- }
+ /**
+ * setUsername<p>
+ * Set username (PK).
+ *
+ * @param username
+ */
+ public void setUsername(String username) { this.username = username; }
- @JsonProperty("password")
- public String getDummyPassword() {
- return null;
- }
+ /**
+ * getDummyPassword<p>
+ * Forces password to be omitted in JSON responses.
+ *
+ * @return always null
+ */
+ @JsonProperty("password")
+ public String getDummyPassword() { return null; }
- public String getPassword() {
- return password;
- }
+ /**
+ * getPassword<p>
+ * Return raw/hashed password (internal use).
+ *
+ * @return password
+ */
+ public String getPassword() { return password; }
- public void setPassword(String password) {
- this.password = password;
- }
+ /**
+ * setPassword<p>
+ * Set raw/hashed password (internal use).
+ *
+ * @param password
+ */
+ public void setPassword(String password) { this.password = password; }
- public List<Integer> getRoles() {
- if (roles == 0) {
- return null;
- }
- List<Integer> aux = new ArrayList<>();
- for (int rol : Rol.ALL) {
- if ((roles & rol) != 0) { // Each rol is a number with only 1 bit ==
- // 1 in binary representation
- aux.add(rol);
- }
- }
- return aux;
- }
+ /**
+ * getRoles<p>
+ * Return list of individual role flags contained in the bitmask.
+ *
+ * @return list of role integers or null if no roles
+ */
+ public List<Integer> getRoles() {
+ if (roles == 0) return null;
+ List<Integer> aux = new ArrayList<>();
+ for (int rol : Rol.ALL) {
+ if ((roles & rol) != 0) aux.add(rol);
+ }
+ return aux;
+ }
- public void setRoles(List<Integer> roles) {
- this.roles = 0;
- if (roles != null) {
- for (Integer rol : roles) {
- this.roles |= rol;
- }
- }
- }
+ /**
+ * setRoles<p>
+ * Set the roles bitmask from a list of role flags.
+ *
+ * @param roles list of flags
+ */
+ public void setRoles(List<Integer> roles) {
+ this.roles = 0;
+ if (roles != null) {
+ for (Integer rol : roles) this.roles |= rol;
+ }
+ }
- public String getFirstName() {
- return firstName;
- }
+ /**
+ * getFirstName<p>
+ * Return first name.
+ *
+ * @return firstName
+ */
+ public String getFirstName() { return firstName; }
- public void setFirstName(String firstName) {
- this.firstName = firstName;
- }
+ /**
+ * setFirstName<p>
+ * Set first name.
+ *
+ * @param firstName
+ */
+ public void setFirstName(String firstName) { this.firstName = firstName; }
- public String getLastName() {
- return lastName;
- }
+ /**
+ * getLastName<p>
+ * Return last name.
+ *
+ * @return lastName
+ */
+ public String getLastName() { return lastName; }
- public void setLastName(String lastName) {
- this.lastName = lastName;
- }
+ /**
+ * setLastName<p>
+ * Set last name.
+ *
+ * @param lastName
+ */
+ public void setLastName(String lastName) { this.lastName = lastName; }
- public Date getLastLogin() {
- return lastLogin;
- }
+ /**
+ * getLastLogin<p>
+ * Return last login timestamp.
+ *
+ * @return lastLogin
+ */
+ public Date getLastLogin() { return lastLogin; }
- public void setLastLogin(Date lastLogin) {
- this.lastLogin = lastLogin;
- }
+ /**
+ * setLastLogin<p>
+ * Set last login timestamp.
+ *
+ * @param lastLogin
+ */
+ public void setLastLogin(Date lastLogin) { this.lastLogin = lastLogin; }
- public Date getModificationTimestamp() {
- return modificationTimestamp;
- }
+ /**
+ * getModificationTimestamp<p>
+ * Return modification timestamp.
+ *
+ * @return modificationTimestamp
+ */
+ public Date getModificationTimestamp() { return modificationTimestamp; }
- public void setModificationTimestamp(Date modificationTimestamp) {
- this.modificationTimestamp = modificationTimestamp;
- }
+ /**
+ * setModificationTimestamp<p>
+ * Set modification timestamp.
+ *
+ * @param modificationTimestamp
+ */
+ public void setModificationTimestamp(Date modificationTimestamp) { this.modificationTimestamp = modificationTimestamp; }
- public Date getCreationTimestamp() {
- return creationTimestamp;
- }
+ /**
+ * getCreationTimestamp<p>
+ * Return creation timestamp.
+ *
+ * @return creationTimestamp
+ */
+ public Date getCreationTimestamp() { return creationTimestamp; }
- public void setCreationTimestamp(Date creationTimestamp) {
- this.creationTimestamp = creationTimestamp;
- }
+ /**
+ * setCreationTimestamp<p>
+ * Set creation timestamp.
+ *
+ * @param creationTimestamp
+ */
+ public void setCreationTimestamp(Date creationTimestamp) { this.creationTimestamp = creationTimestamp; }
- @Override
- public String toString() {
- return "{User: " + username + " Name: " + firstName + " " + lastName + ", last login: " + lastLogin + "}";
- }
+ /**
+ * getLang<p>
+ * Return preferred language.
+ *
+ * @return lang
+ */
+ public String getLang() { return lang; }
- public String getLang() {
- return lang;
- }
+ /**
+ * setLang<p>
+ * Set preferred language.
+ *
+ * @param lang
+ */
+ public void setLang(String lang) { this.lang = lang; }
- public void setLang(String lang) {
- this.lang = lang;
- }
+ /**
+ * getEmail<p>
+ * Return email address.
+ *
+ * @return email
+ */
+ public String getEmail() { return email; }
- public Set<Organization> getOrganizations() {
- return organizations;
- }
+ /**
+ * setEmail<p>
+ * Set email address.
+ *
+ * @param email
+ */
+ public void setEmail(String email) { this.email = email; }
- public void setOrganizations(Set<Organization> organizations) {
- this.organizations = organizations;
- }
+ /**
+ * getOrganizations<p>
+ * Return organizations (entity set).
+ *
+ * @return organizations
+ */
+ public Set<Organization> getOrganizations() { return organizations; }
- public Set<Application> getApplications() {
- return applications;
- }
+ /**
+ * setOrganizations<p>
+ * Set organizations (entity set).
+ *
+ * @param organizations
+ */
+ public void setOrganizations(Set<Organization> organizations) { this.organizations = organizations; }
- public void setApplications(Set<Application> applications) {
- this.applications = applications;
- }
+ /**
+ * getApplications<p>
+ * Return applications (entity set).
+ *
+ * @return applications
+ */
+ public Set<Application> getApplications() { return applications; }
- @JsonProperty("organizations_ids")
- public void setOrgsIds(List<Integer> orgsIds) {
- organizations = new HashSet<>();
- for (Integer orgid : orgsIds) {
- Organization o = new Organization();
- o.setId(orgid);
- organizations.add(o);
- }
- }
+ /**
+ * setApplications<p>
+ * Set applications (entity set).
+ *
+ * @param applications
+ */
+ public void setApplications(Set<Application> applications) { this.applications = applications; }
- @JsonProperty("organizations_ids")
- public Set<Integer> getOrgsIds() {
- if (organizations == null) {
- return null;
- }
- Set<Integer> ids = new HashSet<>();
- for (Organization org : organizations) {
- ids.add(org.getId());
- }
- return ids;
- }
+ // -------- JSON helpers for related IDs --------
- @JsonProperty("applications_ids")
- public void setAppsIds(Collection<Integer> appIds) {
- applications = new HashSet<>();
- for (Integer appid : appIds) {
- Application a = new Application();
- a.setId(appid);
- applications.add(a);
- }
- }
+ /**
+ * setOrgsIds<p>
+ * Replace organizations from a list of org IDs.
+ *
+ * @param orgsIds
+ */
+ @JsonProperty("organizations_ids")
+ public void setOrgsIds(List<Integer> orgsIds) {
+ organizations = new HashSet<>();
+ for (Integer orgid : orgsIds) {
+ Organization o = new Organization();
+ o.setId(orgid);
+ organizations.add(o);
+ }
+ }
- @JsonProperty("applications_ids")
- public Set<Integer> getAppsIds() {
- if (applications == null) {
- return null;
- }
- Set<Integer> ids = new HashSet<>();
- for (Application app : applications) {
- ids.add(app.getId());
- }
- return ids;
- }
+ /**
+ * getOrgsIds<p>
+ * Expose organization IDs.
+ *
+ * @return orgsIds
+ */
+ @JsonProperty("organizations_ids")
+ public Set<Integer> getOrgsIds() {
+ if (organizations == null) return null;
+ Set<Integer> ids = new HashSet<>();
+ for (Organization org : organizations) ids.add(org.getId());
+ return ids;
+ }
- @JsonIgnore
- public Set<Integer> getAllOrgsIds() {
- if (organizations == null) {
- return null;
- }
- Set<Integer> ids = new HashSet<>();
- includeAllOrgs(this.organizations, ids);
- return ids;
- }
+ /**
+ * setAppsIds<p>
+ * Replace applications from a collection of app IDs.
+ *
+ * @param appIds
+ */
+ @JsonProperty("applications_ids")
+ public void setAppsIds(Collection<Integer> appIds) {
+ applications = new HashSet<>();
+ for (Integer appid : appIds) {
+ Application a = new Application();
+ a.setId(appid);
+ applications.add(a);
+ }
+ }
- @JsonIgnore
- public Set<Integer> getAllAppsIds() {
- if (applications == null) {
- return null;
- }
- Set<Integer> ids = this.applications.parallelStream().map(app -> app.getId()).collect(Collectors.toSet());
+ /**
+ * getAppsIds<p>
+ * Expose application IDs.
+ *
+ * @return appsIds
+ */
+ @JsonProperty("applications_ids")
+ public Set<Integer> getAppsIds() {
+ if (applications == null) return null;
+ Set<Integer> ids = new HashSet<>();
+ for (Application app : applications) ids.add(app.getId());
+ return ids;
+ }
- return ids;
- }
+ // -------- Derived scopes --------
- /**
- * Walk into the organization hierarchy to include all descendants
- *
- * @param list
- * @param orgIds
- */
- private void includeAllOrgs(Set<Organization> list, Set<Integer> orgIds) {
- for (Organization org : list) {
- orgIds.add(org.getId());
- includeAllOrgs(org.getChildOrganizations(), orgIds);
- }
- }
+ /**
+ * getAllOrgsIds<p>
+ * Compute full organization scope including descendants.
+ *
+ * @return set of org IDs (may be null when no organizations)
+ */
+ @JsonIgnore
+ public Set<Integer> getAllOrgsIds() {
+ if (organizations == null) return null;
+ Set<Integer> ids = new HashSet<>();
+ includeAllOrgs(this.organizations, ids);
+ return ids;
+ }
- public String getEmail() {
- return email;
- }
+ /**
+ * getAllAppsIds<p>
+ * Compute application scope (direct associations only).
+ *
+ * @return set of application IDs (may be null when no applications)
+ */
+ @JsonIgnore
+ public Set<Integer> getAllAppsIds() {
+ if (applications == null) return null;
+ return this.applications.parallelStream().map(Application::getId).collect(Collectors.toSet());
+ }
- public void setEmail(String email) {
- this.email = email;
- }
+ /**
+ * includeAllOrgs<p>
+ * Walk organization hierarchy to include all descendants.
+ *
+ * @param list current level orgs
+ * @param orgIds accumulator of ids
+ */
+ private void includeAllOrgs(Set<Organization> list, Set<Integer> orgIds) {
+ for (Organization org : list) {
+ orgIds.add(org.getId());
+ includeAllOrgs(org.getChildOrganizations(), orgIds);
+ }
+ }
- /**
- * Numeric rol mask. Be aware to use different bit position for each role
- *
- * @author rob
- */
- public static class Rol {
- public static final int ADVANCE = 0x01;
- public static final int ADMIN = 0x02;
- public static final int BASIC = 0x04;
- public static final int API_CLIENT = 0x80;
- public static final int[] ALL = new int[] { ADVANCE, ADMIN, BASIC, API_CLIENT };
- }
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
+ @Override
+ public String toString() {
+ return "{User: " + username + " Name: " + firstName + " " + lastName + ", last login: " + lastLogin + "}";
+ }
+
+ /**
+ * Rol
+ * <p>
+ * Bitmask constants for user roles. Each constant must occupy a distinct bit.
+ */
+ public static class Rol {
+ public static final int ADVANCE = 0x01;
+ public static final int ADMIN = 0x02;
+ public static final int BASIC = 0x04;
+ public static final int API_CLIENT= 0x80;
+ public static final int[] ALL = new int[] { ADVANCE, ADMIN, BASIC, API_CLIENT };
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java b/securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java
index 9d9f996..8d38250 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/CodedEnum.java
@@ -1,7 +1,23 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.common;
+/**
+* CodedEnum
+* <p>
+* Small contract for enums persisted as short codes
+*/
public interface CodedEnum {
- public String getCode();
+ /**
+ * getCode<p>
+ * Return the short string code for persistence/JSON.
+ *
+ * @return codeEnum
+ */
+ String getCode();
}
+
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java b/securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java
index e473ea0..f01b366 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/CreationTimestampEntity.java
@@ -1,10 +1,31 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.common;
import java.util.Date;
+/**
+* CreationTimestampEntity
+* <p>
+* Contract for entities that expose a creation timestamp property.
+*/
public interface CreationTimestampEntity {
- public Date getCreationTimestamp();
+ /**
+ * getCreationTimestamp<p>
+ * Return creation timestamp.
+ *
+ * @return creationTimeStamp
+ */
+ Date getCreationTimestamp();
- public void setCreationTimestamp(Date creationTimestamp);
+ /**
+ * setCreationTimestamp<p>
+ * Set creation timestamp.
+ *
+ * @param creationTimestamp
+ */
+ void setCreationTimestamp(Date creationTimestamp);
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java b/securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java
index 8310069..e93ea56 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/LicenseStatusType.java
@@ -1,12 +1,30 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.common;
import net.curisit.securis.db.LicenseStatus;
+/**
+* LicenseStatusType
+* <p>
+* Hibernate user type that persists {@link LicenseStatus} using its code.
+* Delegates specifics to {@link PersistentEnumUserType}.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
public class LicenseStatusType extends PersistentEnumUserType<LicenseStatus> {
+ /**
+ * returnedClass<p>
+ * Return the enum class handled by this type.
+ *
+ * @return licenseStatus
+ */
@Override
public Class<LicenseStatus> returnedClass() {
return LicenseStatus.class;
}
-
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/Metadata.java b/securis/src/main/java/net/curisit/securis/db/common/Metadata.java
index 8706e36..6018d5e 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/Metadata.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/Metadata.java
@@ -1,16 +1,64 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.common;
+/**
+* Metadata
+* <p>
+* Contract for metadata entries (key/value/mandatory).
+* Implemented by ApplicationMetadata, LicenseTypeMetadata, PackMetadata, etc.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
public interface Metadata {
- public String getKey();
+
+ /**
+ * getKey<p>
+ * Return entry key.
+ *
+ * @return key
+ */
+ String getKey();
+
+ /**
+ * setKey<p>
+ * Set entry key.
+ *
+ * @param key
+ */
+ void setKey(String key);
- public void setKey(String key);
+ /**
+ * getValue<p>
+ * Return entry value.
+ *
+ * @return value
+ */
+ String getValue();
+
+ /**
+ * setValue<p>
+ * Set entry value.
+ *
+ * @param value
+ */
+ void setValue(String value);
- public String getValue();
-
- public void setValue(String value);
-
- public boolean isMandatory();
-
- public void setMandatory(boolean mandatory);
-
+ /**
+ * isMandatory<p>
+ * Whether this metadata is required.
+ *
+ * @return isMandatory
+ */
+ boolean isMandatory();
+
+ /**
+ * setMandatory<p>
+ * Set required flag.
+ *
+ * @param mandatory
+ */
+ void setMandatory(boolean mandatory);
}
diff --git a/securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java b/securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java
index a59b56c..17084f8 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/ModificationTimestampEntity.java
@@ -1,10 +1,39 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.common;
import java.util.Date;
+/**
+* ModificationTimestampEntity
+* <p>
+* Contract for entities that track a <b>modification timestamp</b>.
+* Typical usage: attach {@code @EntityListeners(ModificationTimestampListener.class)}
+* so the value is updated automatically on persist/update.
+*
+* Methods:
+* - {@link #getModificationTimestamp()} exposes the timestamp.
+* - {@link #setModificationTimestamp(Date)} sets the timestamp.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
public interface ModificationTimestampEntity {
- public Date getModificationTimestamp();
+ /**
+ * getModificationTimestamp<p>
+ * Return the last modification timestamp.
+ *
+ * @return modificationTimestamp
+ */
+ Date getModificationTimestamp();
- public void setModificationTimestamp(Date modificationTimestamp);
+ /**
+ * setModificationTimestamp<p>
+ * Set/update the last modification timestamp.
+ *
+ * @param modificationTimestamp
+ */
+ void setModificationTimestamp(Date modificationTimestamp);
}
diff --git a/securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java b/securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java
index e562933..f3b81b9 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/PackStatusType.java
@@ -1,12 +1,29 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.common;
import net.curisit.securis.db.PackStatus;
+/**
+* PackStatusType
+* <p>
+* Hibernate {@code UserType} for persisting {@link PackStatus} as its short code.
+* Delegates common enum-code mapping behavior to {@link PersistentEnumUserType}.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
public class PackStatusType extends PersistentEnumUserType<PackStatus> {
+ /**
+ * returnedClass<p>
+ * Return enum class handled by this type.
+ *
+ * @return packStatus
+ */
@Override
public Class<PackStatus> returnedClass() {
return PackStatus.class;
}
-
}
diff --git a/securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java b/securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java
index 19879f2..e11a355 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/PersistentEnumUserType.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.common;
import java.io.Serializable;
@@ -10,57 +13,150 @@
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.UserType;
+/**
+* PersistentEnumUserType
+* <p>
+* Base Hibernate {@link UserType} for enums implementing {@link CodedEnum}.
+* Stores the enum's <b>short code</b> (VARCHAR) and reconstructs the enum
+* from that code on load.
+*
+* Notes:
+* - SQL type is {@code VARCHAR}.
+* - Immutable by default ({@link #isMutable()} returns false).
+* - {@link #equals(Object, Object)} compares by reference (adequate for enums).
+*
+* @param <T> enum type implementing {@link CodedEnum}
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
public abstract class PersistentEnumUserType<T extends CodedEnum> implements UserType {
+ /**
+ * assemble<p>
+ * Return cached value as-is (immutable semantics).
+ *
+ * @param cached
+ * @param owner
+ * @return assembleObject
+ * @throws HibernateException
+ */
@Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return cached;
}
+ /**
+ * deepCopy<p>
+ * Enums are immutable; return value as-is.
+ *
+ * @param value
+ * @return deepCopy
+ */
@Override
public Object deepCopy(Object value) throws HibernateException {
return value;
}
+ /**
+ * disassemble<p>
+ * Return value for 2nd-level cache.
+ *
+ * @param value
+ * @return disassembleObject
+ * @throw HibernateException
+ */
@Override
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) value;
}
+ /**
+ * equals<p>
+ * Reference equality is fine for enums.
+ *
+ * @param x
+ * @param y
+ * @param isEqual
+ * @throws HibernateException
+ */
@Override
public boolean equals(Object x, Object y) throws HibernateException {
return x == y;
}
+ /**
+ * hashCode<p>
+ * Delegate to value hashCode; 0 for null.
+ *
+ * @param object
+ * @return hashCode
+ * @throws HibernateException
+ */
@Override
public int hashCode(Object x) throws HibernateException {
return x == null ? 0 : x.hashCode();
}
+ /**
+ * isMutable<p>
+ * Enums are immutable.
+ *
+ * @return isMutable
+ */
@Override
public boolean isMutable() {
return false;
}
+ /**
+ * replace<p>
+ * Immutable; return original.
+ *
+ * @param original
+ * @param target
+ * @param owner
+ * @return object
+ * @throws HibernateException
+ */
@Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return original;
}
+ /**
+ * returnedClass<p>
+ * Concrete types must provide the enum class.
+ */
@Override
public abstract Class<T> returnedClass();
+ /**
+ * sqlTypes<p>
+ * Persist as single VARCHAR column
+ *
+ * @return sqlTypes
+ */
@Override
public int[] sqlTypes() {
- return new int[] {
- Types.VARCHAR
- };
+ return new int[] { Types.VARCHAR };
}
+ /**
+ * nullSafeGet<p>
+ * Read code from result set and map to the corresponding enum constant.
+ *
+ * @param resultSet
+ * @param names
+ * @param session
+ * @param owner
+ * @return enum instance or null if DB value is null or code not matched
+ */
@Override
public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
throws HibernateException, SQLException {
String code = rs.getString(names[0]);
+ if (code == null) return null;
for (CodedEnum en : returnedClass().getEnumConstants()) {
if (en.getCode().equals(code)) {
return en;
@@ -69,6 +165,15 @@
return null;
}
+ /**
+ * nullSafeSet<p>
+ * Write enum code as VARCHAR or set NULL if value is null.
+ *
+ * @param statement
+ * @param value
+ * @param index
+ * @param session
+ */
@Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
throws HibernateException, SQLException {
@@ -78,5 +183,5 @@
st.setString(index, ((CodedEnum) value).getCode());
}
}
-
}
+
diff --git a/securis/src/main/java/net/curisit/securis/db/common/SystemParams.java b/securis/src/main/java/net/curisit/securis/db/common/SystemParams.java
index 0e745f2..a21a5ca 100644
--- a/securis/src/main/java/net/curisit/securis/db/common/SystemParams.java
+++ b/securis/src/main/java/net/curisit/securis/db/common/SystemParams.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.common;
import java.util.Date;
@@ -13,120 +16,148 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+/**
+* SystemParams
+* <p>
+* Simple façade to read/write application-wide parameters stored in the
+* {@code settings} table (key/value + timestamps).
+*
+* Features:
+* - Typed getters: {@code String}, {@code Integer}, {@code Boolean}, {@code Double}, {@code Date}.
+* - Default value overloads.
+* - Upsert semantics in {@link #setParam(String, String)} and typed variants.
+* - Removal with {@link #removeParam(String)}.
+*
+* Transaction note:
+* - Each write method starts and commits its own transaction. Rollback is invoked
+* only on exceptions.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
@ApplicationScoped
public class SystemParams {
@SuppressWarnings("unused")
private static final Logger LOG = LogManager.getLogger(SystemParams.class);
- @Inject
- private EntityManagerProvider emProvider;
+ @Inject private EntityManagerProvider emProvider;
+
+ // -------------------- Read API --------------------
/**
- * Returns the system parameter value for given key
- *
- * @param key
- * @return the value of the param or null if it doesn't exist
- */
+ * getParam<p>
+ * Get raw string value or {@code null} when absent.
+ *
+ * @param key setting key
+ * @return string value or null
+ */
public String getParam(String key) {
return getParam(key, null);
}
/**
- * Returns the system parameter as int value for given key
- *
- * @param key
- * @return the value of the param or null if it doesn't exist
- */
+ * getParamAsInt<p>
+ * Get value as Integer or null when absent.
+ *
+ * @param key setting key
+ * @return integer value or null
+ */
public Integer getParamAsInt(String key) {
String value = getParam(key, null);
return value == null ? null : Integer.parseInt(value);
}
/**
- *
- * @param key
- * @param defaulValue
- * returned if key doesn't exist in params table
- * @return
- */
+ * getParamAsInt<p>
+ * Get value as Integer with default.
+ *
+ * @param key setting key
+ * @param defaulValue returned if key is missing
+ * @return integer value or default
+ */
public Integer getParamAsInt(String key, Integer defaulValue) {
String value = getParam(key, null);
return value == null ? defaulValue : Integer.parseInt(value);
}
/**
- * Returns the system parameter as Date value for given key
- *
- * @param key
- * @return the value of the param or null if it doesn't exist
- */
+ * getParamAsDate<p>
+ * Get value parsed from ISO-8601.
+ *
+ * @param key setting key
+ * @return date value or null
+ */
public Date getParamAsDate(String key) {
String value = getParam(key, null);
return value == null ? null : Utils.toDateFromIso(value);
}
/**
- * Returns the system parameter as boolean value for given key
- *
- * @param key
- * @return the value of the param or null if it doesn't exist
- */
+ * getParamAsBool<p>
+ * Get value parsed as boolean.
+ *
+ * @param key setting key
+ * @return boolean value or null
+ */
public Boolean getParamAsBool(String key) {
String value = getParam(key, null);
return value == null ? null : Boolean.parseBoolean(value);
}
/**
- *
- * @param key
- * @param defaulValue
- * returned if key doesn't exist in params table
- * @return
- */
+ * getParamAsBool<p>
+ * Get value parsed as boolean with default.
+ *
+ * @param key setting key
+ * @param defaulValue default when missing
+ * @return boolean value or default
+ */
public Boolean getParamAsBool(String key, boolean defaulValue) {
String value = getParam(key, null);
return value == null ? defaulValue : Boolean.parseBoolean(value);
}
/**
- * Returns the system parameter as boolean value for given key
- *
- * @param key
- * @return the value of the param or null if it doesn't exist
- */
+ * getParamAsDouble<p>
+ * Get value parsed as double.
+ *
+ * @param key setting key
+ * @return double value or null
+ */
public Double getParamAsDouble(String key) {
String value = getParam(key, null);
return value == null ? null : Double.parseDouble(value);
}
/**
- * Returns the system parameter value for given key
- *
- * @param key
- * @param defaultValue
- * returned if key doesn't exist in params table
- * @return
- */
+ * getParam<p>
+ * Get raw string value or a default when missing.
+ *
+ * @param key setting key
+ * @param defaultValue default when missing
+ * @return value or default
+ */
public String getParam(String key, String defaultValue) {
EntityManager em = emProvider.getEntityManager();
Settings p = em.find(Settings.class, key);
return p == null ? defaultValue : p.getValue();
}
+ // -------------------- Write API --------------------
+
/**
- * Returns the system parameter value passed as parameter to method
- *
- * @param key
- * @param defaultValue
- * @return
- */
+ * setParam<p>
+ * Upsert a parameter as string.
+ *
+ * @param key setting key
+ * @param value string value
+ */
public void setParam(String key, String value) {
EntityManager em = emProvider.getEntityManager();
em.getTransaction().begin();
try {
Settings p = em.find(Settings.class, key);
-
if (p == null) {
p = new Settings();
p.setKey(key);
@@ -136,15 +167,16 @@
p.setValue(value);
em.merge(p);
}
- em.flush();
em.getTransaction().commit();
- } finally {
+ } catch (Exception ex) {
em.getTransaction().rollback();
+ throw ex;
}
}
- /**
- * Save a parameter as a Date
+ /**
+ * setParam<p>
+ * Save parameter as ISO date string.
*
* @param key
* @param value
@@ -153,8 +185,9 @@
setParam(key, Utils.toIsoFormat(value));
}
- /**
- * Save a parameter as a integer
+ /**
+ * setParam<p>
+ * Save parameter as integer.
*
* @param key
* @param value
@@ -163,8 +196,9 @@
setParam(key, String.valueOf(value));
}
- /**
- * Save a parameter as a boolean
+ /**
+ * setParam<p>
+ * Save parameter as boolean.
*
* @param key
* @param value
@@ -173,8 +207,9 @@
setParam(key, String.valueOf(value));
}
- /**
- * Save a parameter as a double
+ /**
+ * setParam<p>
+ * Save parameter as double.
*
* @param key
* @param value
@@ -184,11 +219,11 @@
}
/**
- * Remove a parameter from params table
- *
- * @param key
- * @return
- */
+ * removeParam<p>
+ * Delete a parameter by key (no-op if missing).
+ *
+ * @param key setting key
+ */
public void removeParam(String key) {
EntityManager em = emProvider.getEntityManager();
em.getTransaction().begin();
@@ -198,13 +233,19 @@
em.remove(p);
}
em.getTransaction().commit();
- } finally {
+ } catch (Exception ex) {
em.getTransaction().rollback();
+ throw ex;
}
}
+ /**
+ * Keys
+ * <p>
+ * Centralized constants for parameter keys (client/common/server).
+ */
public static class Keys {
- // Keys used in basic app
+ // Client app keys
public static final String CONFIG_CLIENT_HOST = "config.client.host";
public static final String CONFIG_CLIENT_PORT = "config.client.port";
public static final String CONFIG_CLIENT_LAST_UPDATE = "config.client.last_update";
@@ -217,9 +258,9 @@
public static final String CONFIG_CLIENT_GS_HOST = "config.client.gs_host";
public static final String CONFIG_CLIENT_GS_PORT = "config.client.gs_port";
- // Keys used in both app
- public static final String CONFIG_COMMON_CUSTOMER_CODE = "config.common.customer_code"; // BP
- public static final String CONFIG_COMMON_CS_CODE = "config.common.cs_code"; // 0000
+ // Shared keys
+ public static final String CONFIG_COMMON_CUSTOMER_CODE = "config.common.customer_code";
+ public static final String CONFIG_COMMON_CS_CODE = "config.common.cs_code";
public static final String CONFIG_COMMON_USERS_VERSION = "config.common.user_version";
public static final String CONFIG_COMMON_SETTINGS_VERSION = "config.common.settings_version";
public static final String CONFIG_COMMON_DATASET_VERSION = "config.common.dataset_version";
@@ -227,7 +268,7 @@
public static final String CONFIG_COMMON_TIMEOUT_SESSION_BA = "config.common.timeout_session_ba";
public static final String CONFIG_COMMON_TIMEOUT_SESSION_CS = "config.common.timeout_session_cs";
- // Keys used in server app
+ // Server app keys
public static final String CONFIG_SERVER_LICENSE_EXPIRATION = "config.server.license.expiation";
public static final String CONFIG_SERVER_MAX_INSTANCES = "config.server.max_instances";
public static final String CONFIG_SERVER_MAX_USERS = "config.server.max_users";
@@ -240,5 +281,4 @@
public static final String CONFIG_SERVER_CREATE_DATASET = "config.server.create_dataset_in_next_startup";
public static final String CONFIG_SERVER_PORT = "config.server.port";
}
-
}
diff --git a/securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java b/securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java
index 0b2c88a..0e858e3 100644
--- a/securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java
+++ b/securis/src/main/java/net/curisit/securis/db/listeners/CreationTimestampListener.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.listeners;
import java.util.Date;
@@ -9,14 +12,31 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+/**
+* CreationTimestampListener
+* <p>
+* JPA entity listener that sets the <b>creation timestamp</b> right before persisting.
+* Entities must implement {@link CreationTimestampEntity} to be compatible.
+*
+* Usage:
+* {@code @EntityListeners(CreationTimestampListener.class)}
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
public class CreationTimestampListener {
private static final Logger log = LogManager.getLogger(CreationTimestampListener.class);
+ /**
+ * updateTimestamp<p>
+ * Set creation timestamp during @PrePersist.
+ *
+ * @param creationTimeStampEntity
+ */
@PrePersist
public void updateTimestamp(CreationTimestampEntity p) {
- log.info("Settings creation timestmap date");
+ log.info("Settings creation timestamp date");
p.setCreationTimestamp(new Date());
}
-
}
diff --git a/securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java b/securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java
index df53613..937269f 100644
--- a/securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java
+++ b/securis/src/main/java/net/curisit/securis/db/listeners/ModificationTimestampListener.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.db.listeners;
import java.util.Date;
@@ -10,15 +13,30 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+/**
+* ModificationTimestampListener
+* <p>
+* JPA entity listener that updates the <b>modification timestamp</b> on both
+* persist and update events. Entities must implement
+* {@link ModificationTimestampEntity}.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
public class ModificationTimestampListener {
private static final Logger log = LogManager.getLogger(ModificationTimestampListener.class);
+ /**
+ * updateTimestamp<p>
+ * Set modification timestamp during @PrePersist/@PreUpdate.
+ *
+ * @param modificationTimestampEntity
+ */
@PreUpdate
@PrePersist
public void updateTimestamp(ModificationTimestampEntity p) {
- log.info("Settings modification timestmap date");
+ log.info("Settings modification timestamp date");
p.setModificationTimestamp(new Date());
}
-
}
diff --git a/securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java b/securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java
index f2e9008..8c8d23d 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/EnsureTransaction.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.ioc;
import java.lang.annotation.ElementType;
@@ -7,11 +10,20 @@
import jakarta.interceptor.InterceptorBinding;
-@Target({
- ElementType.METHOD, ElementType.TYPE
-})
+/**
+* EnsureTransaction
+* <p>
+* CDI interceptor binding to mark resource methods that require a
+* transaction boundary. Interceptors (e.g., in a request filter / writer
+* interceptor) can check this annotation to begin/commit/rollback.
+*
+* Usage:
+* {@code @EnsureTransaction} on JAX-RS methods.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
+@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@InterceptorBinding
-public @interface EnsureTransaction {
-
-}
+public @interface EnsureTransaction { }
diff --git a/securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java b/securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java
index be020f5..f702c97 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/EntityManagerProvider.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.ioc;
import jakarta.enterprise.context.ApplicationScoped;
@@ -8,16 +11,38 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+/**
+* EntityManagerProvider
+* <p>
+* Simple provider for JPA {@link EntityManager} instances using the
+* persistence unit "localdb". Creates an {@link EntityManagerFactory}
+* once per application and returns a fresh {@link EntityManager} per call.
+*
+* Note:
+* - Callers are responsible for closing the obtained EntityManager.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@ApplicationScoped
public class EntityManagerProvider {
@SuppressWarnings("unused")
- private static final Logger log = LogManager.getLogger(EntityManagerProvider.class);
+ private static final Logger log = LogManager.getLogger(EntityManagerProvider.class);
+ /**
+ * entityManagerFactory<p>
+ * Application-wide EMF built from persistence.xml PU "localdb".
+ */
private final EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("localdb");
+ /**
+ * getEntityManager<p>
+ * Create a new {@link EntityManager}.
+ *
+ * @return a new EntityManager; caller must close it
+ */
public EntityManager getEntityManager() {
return entityManagerFactory.createEntityManager();
}
-
}
diff --git a/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java b/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java
index fd61433..2417954 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.ioc;
import java.io.IOException;
@@ -31,143 +34,214 @@
import net.curisit.securis.utils.CacheTTL;
import net.curisit.securis.utils.TokenHelper;
+/**
+* RequestsInterceptor
+* <p>
+* Authentication/authorization interceptor that:
+* <ul>
+* <li>Loads and stores the {@link EntityManager} in the request context.</li>
+* <li>Validates tokens for methods annotated with {@link Securable}.</li>
+* <li>Builds a {@link BasicSecurityContext} with roles and scoped organization/application IDs.</li>
+* <li>Manages transactions when {@code @EnsureTransaction} is present.</li>
+* </ul>
+*
+* <p><b>Cache usage:</b> Uses {@link CacheTTL} to cache roles and scope sets.
+* The new {@link CacheTTL#getSet(String, Class)} helper removes unchecked
+* conversion warnings when retrieving {@code Set<Integer>} from the cache.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@Provider
@Priority(Priorities.AUTHENTICATION)
public class RequestsInterceptor implements ContainerRequestFilter, WriterInterceptor {
- private static final Logger LOG = LogManager.getLogger(RequestsInterceptor.class);
+ private static final Logger LOG = LogManager.getLogger(RequestsInterceptor.class);
- @Context
- private HttpServletResponse servletResponse;
+ @Inject private CacheTTL cache;
+ @Inject private TokenHelper tokenHelper;
+ @Inject private EntityManagerProvider emProvider;
- @Context
- private HttpServletRequest servletRequest;
+ @Context private HttpServletResponse servletResponse;
+ @Context private HttpServletRequest servletRequest;
+ @Context private ResourceInfo resourceInfo;
- @Context
- private ResourceInfo resourceInfo;
+ private static final String EM_CONTEXT_PROPERTY = "curisit.entitymanager";
- @Inject
- private CacheTTL cache;
+ // -------------------------------------------------------------
+ // Request filter (authN/authZ + EM handling)
+ // -------------------------------------------------------------
- @Inject
- private TokenHelper tokenHelper;
+ /**
+ * filter<p>
+ * Entry point before resource method invocation.
+ *
+ * @param requestContext
+ * @throws IOException
+ */
+ @Override
+ public void filter(ContainerRequestContext requestContext) throws IOException {
+ EntityManager em = emProvider.getEntityManager();
+ LOG.debug("GETTING EM: {}", em);
- @Inject
- private EntityManagerProvider emProvider;
+ // Store EntityManager for later retrieval (writer interceptor)
+ requestContext.setProperty(EM_CONTEXT_PROPERTY, em);
- private static final String EM_CONTEXT_PROPERTY = "curisit.entitymanager";
+ Method method = resourceInfo.getResourceMethod();
- @Override
- public void filter(ContainerRequestContext requestContext) throws IOException {
- EntityManager em = emProvider.getEntityManager();
- LOG.debug("GETTING EM: {}", em);
+ if (checkSecurableMethods(requestContext, method)) {
+ if (method.isAnnotationPresent(EnsureTransaction.class)) {
+ LOG.debug("Beginning transaction");
+ em.getTransaction().begin();
+ }
+ }
+ }
- // Guardamos el EntityManager en el contexto para recuperación posterior
- requestContext.setProperty(EM_CONTEXT_PROPERTY, em);
+ /**
+ * checkSecurableMethods<p>
+ * Enforce security checks for methods annotated with {@link Securable}.
+ * Builds {@link BasicSecurityContext} when authorized.
+ *
+ * @param ctx
+ * @param method
+ * @return true if request can proceed; false when aborted
+ */
+ private boolean checkSecurableMethods(ContainerRequestContext ctx, Method method) {
+ if (!method.isAnnotationPresent(Securable.class)) return true;
- Method method = resourceInfo.getResourceMethod();
+ String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM);
+ if (token == null || !tokenHelper.isTokenValid(token)) {
+ LOG.warn("Access denied, invalid token");
+ ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
+ return false;
+ }
- if (checkSecurableMethods(requestContext, method)) {
- if (method.isAnnotationPresent(EnsureTransaction.class)) {
- LOG.debug("Beginning transaction");
- em.getTransaction().begin();
- }
- }
- }
+ String username = tokenHelper.extractUserFromToken(token);
+ int roles = getUserRoles(username);
+ Securable securable = method.getAnnotation(Securable.class);
- private boolean checkSecurableMethods(ContainerRequestContext ctx, Method method) {
- if (!method.isAnnotationPresent(Securable.class)) return true;
+ if (securable.roles() != 0 && (securable.roles() & roles) == 0) {
+ LOG.warn("User {} lacks required roles for method {}", username, method.getName());
+ ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
+ return false;
+ }
- String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM);
- if (token == null || !tokenHelper.isTokenValid(token)) {
- LOG.warn("Access denied, invalid token");
- ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
- return false;
- }
+ BasicSecurityContext sc = new BasicSecurityContext(username, roles, servletRequest.isSecure());
+ sc.setOrganizationsIds(getUserOrganizations(username));
+ sc.setApplicationsIds(getUserApplications(username));
+ ctx.setSecurityContext(sc);
+ return true;
+ }
- String username = tokenHelper.extractUserFromToken(token);
- int roles = getUserRoles(username);
- Securable securable = method.getAnnotation(Securable.class);
+ // -------------------------------------------------------------
+ // Cached lookups (roles/orgs/apps)
+ // -------------------------------------------------------------
- if (securable.roles() != 0 && (securable.roles() & roles) == 0) {
- LOG.warn("User {} lacks required roles for method {}", username, method.getName());
- ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
- return false;
- }
+ /**
+ * getUserRoles<p>
+ * Retrieve roles bitmask for the given user (cached).
+ *
+ * @param username
+ * @return userRoles
+ */
+ private int getUserRoles(String username) {
+ if (username == null) return 0;
+ Integer cached = cache.get("roles_" + username, Integer.class);
+ if (cached != null) return cached;
- BasicSecurityContext sc = new BasicSecurityContext(username, roles, servletRequest.isSecure());
- sc.setOrganizationsIds(getUserOrganizations(username));
- sc.setApplicationsIds(getUserApplications(username));
- ctx.setSecurityContext(sc);
- return true;
- }
+ EntityManager em = emProvider.getEntityManager();
+ User user = em.find(User.class, username);
+ int roles = 0;
+ if (user != null) {
+ List<Integer> r = user.getRoles();
+ if (r != null) for (Integer role : r) roles += role;
+ cache.set("roles_" + username, roles, 3600);
+ // also warm some caches
+ cache.set("orgs_" + username, user.getOrgsIds(), 3600);
+ }
+ return roles;
+ }
- private int getUserRoles(String username) {
- if (username == null) return 0;
- Integer cached = cache.get("roles_" + username, Integer.class);
- if (cached != null) return cached;
+ /**
+ * getUserOrganizations<p>
+ * Retrieve organization scope for the user as a typed {@code Set<Integer>}
+ * using the cache helper that validates element types.
+ *
+ * @param username
+ * @return userOrganizations
+ */
+ private Set<Integer> getUserOrganizations(String username) {
+ Set<Integer> cached = cache.getSet("orgs_" + username, Integer.class);
+ if (cached != null) return cached;
- EntityManager em = emProvider.getEntityManager();
- User user = em.find(User.class, username);
- int roles = 0;
- if (user != null) {
- List<Integer> r = user.getRoles();
- if (r != null) for (Integer role : r) roles += role;
- cache.set("roles_" + username, roles, 3600);
- cache.set("orgs_" + username, user.getOrgsIds(), 3600);
- }
- return roles;
- }
+ User user = emProvider.getEntityManager().find(User.class, username);
+ if (user != null) {
+ Set<Integer> result = user.getAllOrgsIds();
+ cache.set("orgs_" + username, result, 3600);
+ return result;
+ }
+ return Set.of();
+ }
- private Set<Integer> getUserOrganizations(String username) {
- Set<Integer> cached = cache.get("orgs_" + username, Set.class);
- if (cached != null) return cached;
- User user = emProvider.getEntityManager().find(User.class, username);
- if (user != null) {
- Set<Integer> result = user.getAllOrgsIds();
- cache.set("orgs_" + username, result, 3600);
- return result;
- }
- return Set.of();
- }
+ /**
+ * getUserApplications<p>
+ * Retrieve application scope for the user as a typed {@code Set<Integer>}
+ * using the cache helper that validates element types.
+ *
+ * @param username
+ * @return userApplications
+ */
+ private Set<Integer> getUserApplications(String username) {
+ Set<Integer> cached = cache.getSet("apps_" + username, Integer.class);
+ if (cached != null) return cached;
- private Set<Integer> getUserApplications(String username) {
- Set<Integer> cached = cache.get("apps_" + username, Set.class);
- if (cached != null) return cached;
- User user = emProvider.getEntityManager().find(User.class, username);
- if (user != null) {
- Set<Integer> result = user.getAllAppsIds();
- cache.set("apps_" + username, result, 3600);
- return result;
- }
- return Set.of();
- }
+ User user = emProvider.getEntityManager().find(User.class, username);
+ if (user != null) {
+ Set<Integer> result = user.getAllAppsIds();
+ cache.set("apps_" + username, result, 3600);
+ return result;
+ }
+ return Set.of();
+ }
- @Override
- public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
- context.proceed();
+ // -------------------------------------------------------------
+ // Writer interceptor (transaction finalize)
+ // -------------------------------------------------------------
- EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY);
- if (em == null) return;
+ /**
+ * aroundWriteTo<p>
+ * Commit/rollback and close EM after response writing.
+ *
+ * @param context
+ * @throws IOException
+ * @throws WebApplicationException
+ */
+ @Override
+ public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
+ context.proceed();
- try {
- if (em.getTransaction().isActive()) {
- if (servletResponse.getStatus() == Status.OK.getStatusCode()) {
- em.getTransaction().commit();
- LOG.debug("Transaction committed");
- } else {
- em.getTransaction().rollback();
- LOG.debug("Transaction rolled back");
- }
- }
- } finally {
- if (em.isOpen()) {
- try {
- em.close();
- } catch (Exception e) {
- LOG.error("Error closing EntityManager", e);
- }
- }
- }
- }
+ EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY);
+ if (em == null) return;
+
+ try {
+ if (em.getTransaction().isActive()) {
+ if (servletResponse.getStatus() == Status.OK.getStatusCode()) {
+ em.getTransaction().commit();
+ LOG.debug("Transaction committed");
+ } else {
+ em.getTransaction().rollback();
+ LOG.debug("Transaction rolled back");
+ }
+ }
+ } finally {
+ if (em.isOpen()) {
+ try {
+ em.close();
+ } catch (Exception e) {
+ LOG.error("Error closing EntityManager", e);
+ }
+ }
+ }
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java b/securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java
index af144d4..66f4ad3 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/RequestsModule.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.ioc;
import net.curisit.securis.services.ApiResource;
@@ -11,28 +14,42 @@
import com.google.inject.AbstractModule;
+/**
+* RequestsModule
+* <p>
+* Guice module that binds JAX-RS resource classes so they can be
+* injected and discovered by the DI container.
+* <p>
+* Notes:
+* - Currently binds resources explicitly. TODO indicates a future
+* improvement to bind dynamically via reflection / classpath scanning.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
public class RequestsModule extends AbstractModule {
+ /**
+ * configure<p>
+ * Register resource types in the injector.
+ */
@Override
protected void configure() {
// TODO Securis: Make the bind using reflection dynamically
-
bind(BasicServices.class);
bind(UserResource.class);
-
bind(ApplicationResource.class);
bind(LicenseTypeResource.class);
bind(OrganizationResource.class);
bind(ApiResource.class);
bind(LicenseResource.class);
bind(PackResource.class);
-
}
+ // Example provider (kept commented for reference)
// @Provides
// @RequestScoped
// public User provideUser() {
- // return ResteasyProviderFactory.getContextData(User.class);
+ // return ResteasyProviderFactory.getContextData(User.class);
// }
-
}
diff --git a/securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java b/securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java
index 8cdfa22..5bc1ef4 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/SecurisModule.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.ioc;
import java.io.File;
@@ -18,6 +21,25 @@
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
+/**
+* SecurisModule
+* <p>
+* Guice module that provides application-level infrastructural dependencies
+* (base URI, app directories, DB files list, support email/hash, etc.).
+* <p>
+* Configuration:
+* - Reads server port from /securis-server.properties (key: "port").
+* - Defaults to port 9997 when not present or on read errors.
+* - Constructs base URI as http://0.0.0.0:{port}/ with UriBuilder.
+* - Creates working directories under ${user.home}/.SeCuris on demand.
+*
+* Security note:
+* - getPassword/getFilePassword are simple helpers; secrets should be
+* managed via a secure vault/env vars in production.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 7, 2025.
+*/
public class SecurisModule extends AbstractModule {
private static final int DEFAULT_PORT = 9997;
@@ -25,28 +47,51 @@
private static final Logger LOG = LogManager.getLogger(SecurisModule.class);
+ /** configure<p>Currently no explicit bindings; providers below supply instances. */
@Override
- protected void configure() {
+ protected void configure() { }
- }
-
+ /**
+ * getPassword<p>
+ * Composite password (example use with encrypted H2 URL).
+ *
+ * @return concatenated password string
+ */
public String getPassword() {
return getFilePassword() + " " + "53curi5";
}
+ /**
+ * getFilePassword<p>
+ * Standalone file password (for H2 CIPHER).
+ *
+ * @return file password string
+ */
public String getFilePassword() {
return "cur151T";
}
+ /**
+ * getUrl<p>
+ * H2 JDBC URL with AES cipher pointing to {appDir}/db/securis.
+ *
+ * @param appDir application working directory
+ * @return JDBC URL (H2)
+ */
public String getUrl(File appDir) {
return String.format("jdbc:h2:%s/db/securis;CIPHER=AES", appDir.getAbsolutePath());
}
+ /**
+ * getBaseURI<p>
+ * Provide the base URI for the HTTP server using configured or default port.
+ *
+ * @return base URI (http://0.0.0.0:{port}/)
+ */
@Named("base-uri")
@Provides
@ApplicationScoped
public URI getBaseURI() {
- // Read from configuration, where?
try {
String url = MessageFormat.format("http://{0}/", "0.0.0.0");
LOG.debug("Server url{}", url);
@@ -56,33 +101,46 @@
}
}
+ /**
+ * getPort<p>
+ * Read port from properties file or return default.
+ *
+ * @return HTTP port
+ */
private int getPort() {
Integer port;
Properties prop = new Properties();
try {
prop.load(getClass().getResourceAsStream(PROPERTIES_FILE_NAME));
port = Integer.valueOf(prop.getProperty("port"));
- if (port == null) {
- return DEFAULT_PORT;
- } else {
- return port;
- }
+ return (port == null ? DEFAULT_PORT : port);
} catch (Exception ex) {
return DEFAULT_PORT;
}
}
+ /**
+ * getAppDbFiles<p>
+ * List of SQL files to initialize the application DB.
+ *
+ * @return list of classpath resource paths
+ */
protected List<String> getAppDbFiles() {
-
return Arrays.asList("/db/schema.sql");
}
+ /**
+ * getTemporaryDir<p>
+ * Provide a temp directory inside the app working dir (.TEMP).
+ * Creates it if missing and marks for deletion on exit.
+ *
+ * @return temp directory or null if creation failed
+ */
@Named("temporary-dir")
@Provides
@ApplicationScoped
public File getTemporaryDir() {
- String tmp = getAppDir().getAbsolutePath();
- tmp += File.separator + ".TEMP";
+ String tmp = getAppDir().getAbsolutePath() + File.separator + ".TEMP";
File ftmp = new File(tmp);
if (!ftmp.exists()) {
if (!ftmp.mkdirs()) {
@@ -94,14 +152,18 @@
return ftmp;
}
+ /**
+ * getAppDir<p>
+ * Provide the app working directory under ${user.home}/.SeCuris (creates if missing).
+ *
+ * @return working directory or null if creation failed
+ */
@Named("app-dir")
@Provides
@ApplicationScoped
public File getAppDir() {
String appDir = System.getProperty("user.home", System.getProperty("user.dir"));
- if (appDir == null) {
- appDir = ".";
- }
+ if (appDir == null) appDir = ".";
appDir += File.separator + ".SeCuris";
File fAppDir = new File(appDir);
if (!fAppDir.exists()) {
@@ -113,6 +175,12 @@
return fAppDir;
}
+ /**
+ * getSupportEmail<p>
+ * Provide support email address.
+ *
+ * @return email
+ */
@Named("support-email")
@Provides
@ApplicationScoped
@@ -120,6 +188,12 @@
return "support@curisit.net";
}
+ /**
+ * getHashLogo<p>
+ * Provide a static content hash for the logo (cache-busting or integrity).
+ *
+ * @return hex SHA-256
+ */
@Named("hash-logo")
@Provides
@ApplicationScoped
@@ -127,11 +201,17 @@
return "1b42616809d4cd8ccf109e3c30d0ab25067f160b30b7354a08ddd563de0096ba";
}
+ /**
+ * getDbFiles<p>
+ * Provide DB initialization files list (delegates to {@link #getAppDbFiles()}).
+ *
+ * @return list of SQL resource paths
+ */
@Named("db-files")
@Provides
@ApplicationScoped
public List<String> getDbFiles() {
return getAppDbFiles();
}
-
}
+
diff --git a/securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java b/securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java
index 1c831b5..9d5fa0e 100644
--- a/securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java
+++ b/securis/src/main/java/net/curisit/securis/security/BasicSecurityContext.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.security;
import java.security.Principal;
@@ -9,103 +12,193 @@
import net.curisit.integrity.commons.Utils;
import net.curisit.securis.db.User;
+/**
+* BasicSecurityContext
+* <p>
+* Lightweight implementation of JAX-RS {@link SecurityContext} based on:
+* - A {@link Principal} holding the username.
+* - An integer bitmask of roles (see {@link User.Rol}).
+* - Optional scope restrictions (organization/application IDs).
+*
+* Role checks:
+* - {@link #isUserInRole(String)} maps string names to bit constants via {@link #ROLES}.
+*
+* Scope helpers:
+* - {@link #isOrgAccesible(Integer)} and {@link #isAppAccesible(Integer)}.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
public class BasicSecurityContext implements SecurityContext {
- final public static String ROL_ADVANCE = "advance";
- final public static String ROL_ADMIN = "admin";
- final public static String ROL_BASIC = "basic";
+ /** String role names mapped to bit flags. */
+ public static final String ROL_ADVANCE = "advance";
+ public static final String ROL_ADMIN = "admin";
+ public static final String ROL_BASIC = "basic";
- final static Map<String, Integer> ROLES = Utils.<String, Integer> createMap(ROL_BASIC, User.Rol.BASIC, ROL_ADVANCE, User.Rol.ADVANCE, ROL_ADMIN, User.Rol.ADMIN);
+ /** Mapping from role name to bit flag. */
+ static final Map<String, Integer> ROLES =
+ Utils.<String, Integer>createMap(ROL_BASIC, User.Rol.BASIC,
+ ROL_ADVANCE, User.Rol.ADVANCE,
+ ROL_ADMIN, User.Rol.ADMIN);
- Principal user = null;
- int roles = 0;
- boolean secure = false;
- Set<Integer> organizationsIds = null;
- Set<Integer> applicationsIds = null;
- double ran = 0;
+ Principal user = null;
+ int roles = 0;
+ boolean secure = false;
+ Set<Integer> organizationsIds = null;
+ Set<Integer> applicationsIds = null;
+ double ran = 0; // small unique marker for debugging instances
- public BasicSecurityContext(String username, int roles, boolean secure) {
- user = new UserPrincipal(username);
- this.roles = roles;
- this.secure = secure;
- ran = Math.random();
- }
+ /**
+ * BasicSecurityContext<p>
+ * Construct a context for given user, roles and transport security flag.
+ *
+ * @param username principal name
+ * @param roles bitmask of roles
+ * @param secure whether the request is HTTPS
+ */
+ public BasicSecurityContext(String username, int roles, boolean secure) {
+ user = new UserPrincipal(username);
+ this.roles = roles;
+ this.secure = secure;
+ ran = Math.random();
+ }
- @Override
- public Principal getUserPrincipal() {
- return user;
- }
+ /**
+ * getUserPrincipal<p>
+ * Return the user principal.
+ *
+ * @return mainUser
+ */
+ @Override
+ public Principal getUserPrincipal() { return user; }
- @Override
- public boolean isUserInRole(String role) {
- Integer introle = ROLES.get(role);
- return introle != null && (introle & roles) != 0;
- }
+ /**
+ * isUserInRole<p>
+ * Check role membership by name (mapped to bitmask).
+ *
+ * @param role
+ * @return isUserInRole
+ */
+ @Override
+ public boolean isUserInRole(String role) {
+ Integer introle = ROLES.get(role);
+ return introle != null && (introle & roles) != 0;
+ }
- @Override
- public boolean isSecure() {
- return secure;
- }
+ /**
+ * isSecure<p>
+ * Return whether transport is secure (HTTPS).
+ *
+ * @return isSecure
+ */
+ @Override
+ public boolean isSecure() { return secure; }
- @Override
- public String getAuthenticationScheme() {
- return null;
- }
+ /**
+ * getAuthenticationScheme<p>
+ * Not used; returns null.
+ *
+ * @return authenticationsScheme
+ */
+ @Override
+ public String getAuthenticationScheme() { return null; }
- @Override
- public String toString() {
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
+ @Override
+ public String toString() { return String.format("SecurityContextWrapper(%f) %s", ran, user); }
- return String.format("SecurityContextWrapper(%f) %s", ran, user);
- }
+ /**
+ * setOrganizationsIds<p>
+ * Set org scope (IDs allowed).
+ *
+ * @param organizationsIds
+ */
+ public void setOrganizationsIds(Set<Integer> orgs) { this.organizationsIds = orgs; }
- public void setOrganizationsIds(Set<Integer> orgs) {
- this.organizationsIds = orgs;
- }
+ /**
+ * getOrganizationsIds<p>
+ * Return org scope.
+ *
+ * @return organizationsIds
+ */
+ public Set<Integer> getOrganizationsIds() { return this.organizationsIds; }
- public Set<Integer> getOrganizationsIds() {
- return this.organizationsIds;
- }
+ /**
+ * getApplicationsIds<p>
+ * Return app scope.
+ *
+ * @return applicationIds
+ */
+ public Set<Integer> getApplicationsIds() { return applicationsIds; }
- public Set<Integer> getApplicationsIds() {
- return applicationsIds;
- }
+ /**
+ * setApplicationsIds<p>
+ * Set app scope.
+ *
+ * @param applicationIds
+ */
+ public void setApplicationsIds(Set<Integer> applicationsIds) { this.applicationsIds = applicationsIds; }
- public void setApplicationsIds(Set<Integer> applicationsIds) {
- this.applicationsIds = applicationsIds;
- }
+ /**
+ * UserPrincipal<p>
+ * Inner Principal holding only the username.
+ */
+ private class UserPrincipal implements Principal {
+ final String name;
+
+ /**
+ * UserPrincipal<p>
+ * Main user
+ *
+ * @param username
+ */
+ public UserPrincipal(String name) { this.name = name; }
+
+ /**
+ * getName<p>
+ * Get the username
+ *
+ * @return userName
+ */
+ @Override public String getName() { return this.name; }
+
+ /**
+ * toString<p>
+ * Get the string describing the current object
+ *
+ * @return object string
+ */
+ @Override public String toString() { return String.format("[%s]", name); }
+ }
- private class UserPrincipal implements Principal {
+ /**
+ * isOrgAccesible<p>
+ * Check if org id is within scope.
+ *
+ * @param orgId
+ * @return isOrgAccesible
+ */
+ public boolean isOrgAccesible(Integer orgid) {
+ if (organizationsIds == null || orgid == null) return false;
+ return organizationsIds.contains(orgid);
+ }
- final String name;
-
- public UserPrincipal(String name) {
- this.name = name;
- }
-
- @Override
- public String getName() {
- return this.name;
- }
-
- @Override
- public String toString() {
- return String.format("[%s]", name);
- }
-
- }
-
- public boolean isOrgAccesible(Integer orgid) {
- if (organizationsIds == null || orgid == null) {
- return false;
- }
- return organizationsIds.contains(orgid);
- }
-
- public boolean isAppAccesible(Integer appid) {
- if (applicationsIds == null || appid == null) {
- return false;
- }
- return applicationsIds.contains(appid);
- }
-
+ /**
+ * isAppAccesible<p>
+ * Check if app id is within scope.
+ *
+ * @param appId
+ * @return isAppAccesible
+ */
+ public boolean isAppAccesible(Integer appid) {
+ if (applicationsIds == null || appid == null) return false;
+ return applicationsIds.contains(appid);
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/security/Securable.java b/securis/src/main/java/net/curisit/securis/security/Securable.java
index 5580b5f..1ab3cd9 100644
--- a/securis/src/main/java/net/curisit/securis/security/Securable.java
+++ b/securis/src/main/java/net/curisit/securis/security/Securable.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.security;
import java.lang.annotation.ElementType;
@@ -7,16 +10,25 @@
import net.curisit.securis.utils.TokenHelper;
+/**
+* Securable
+* <p>
+* Method-level annotation to declare security requirements:
+* - {@link #header()} name containing the auth token (defaults to {@link TokenHelper#TOKEN_HEADER_PÀRAM}).
+* - {@link #roles()} required role bitmask; {@code 0} means no role restriction.
+*
+* Intended to be enforced by request filters/interceptors (e.g., RequestsInterceptor).
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Securable {
- /**
- * Name of header parameter with the auth token to validate
- */
+
+ /** Header name carrying the token to validate. */
String header() default TokenHelper.TOKEN_HEADER_PÀRAM;
- /**
- * Bit mask with the rol or roles necessary to access the method
- */
+ /** Bitmask of required roles; set 0 for public endpoints (token still may be required). */
int roles() default 0;
}
diff --git a/securis/src/main/java/net/curisit/securis/services/ApiResource.java b/securis/src/main/java/net/curisit/securis/services/ApiResource.java
index 2718e51..07da3d2 100644
--- a/securis/src/main/java/net/curisit/securis/services/ApiResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/ApiResource.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.services;
import java.io.IOException;
@@ -49,446 +52,491 @@
import net.curisit.securis.utils.TokenHelper;
/**
- * External API to be accessed by third parties
- *
- * @author roberto <roberto.sanchez@curisit.net>
- */
+* ApiResource
+* <p>
+* External API for license operations, intended for third-party clients.
+*
+* Endpoints:
+* - GET /api/ -> Plain-text status with date (health check).
+* - GET /api/ping -> JSON status (message + date).
+* - POST /api/request -> Create license from RequestBean (JSON).
+* - POST /api/request -> Create license from request file (multipart).
+* - POST /api/renew -> Renew from previous LicenseBean (JSON).
+* - POST /api/renew -> Renew from previous license file (multipart).
+* - POST /api/validate -> Server-side validation of a license.
+*
+* Security:
+* - Methods that mutate/inspect licenses require {@link Securable} with role {@link Rol#API_CLIENT}.
+* - {@link EnsureTransaction} ensures transaction handling at the filter/interceptor layer.
+*
+* Errors:
+* - Business errors are mapped to {@link SeCurisServiceException} with {@link ErrorCodes}.
+*
+* @author JRA
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@Path("/api")
public class ApiResource {
- private static final Logger LOG = LogManager.getLogger(ApiResource.class);
+ private static final Logger LOG = LogManager.getLogger(ApiResource.class);
- @Inject
- TokenHelper tokenHelper;
+ @Inject TokenHelper tokenHelper;
+ @Inject private LicenseHelper licenseHelper;
+ @Context EntityManager em;
+ @Inject LicenseGenerator licenseGenerator;
- @Inject
- private LicenseHelper licenseHelper;
+ /** Fixed username representing API client actor for audit trails. */
+ public static final String API_CLIENT_USERNAME = "_client";
- @Context
- EntityManager em;
+ /** Default constructor (required by JAX-RS). */
+ public ApiResource() { }
- @Inject
- LicenseGenerator licenseGenerator;
+ // -------------------- Health checks --------------------
- public static final String API_CLIENT_USERNAME = "_client";
+ /**
+ * index<p>
+ * Plain text endpoint to verify API is reachable.
+ *
+ * @return 200 OK with simple message
+ */
+ @GET
+ @Path("/")
+ @Produces({ MediaType.TEXT_PLAIN })
+ public Response index() {
+ return Response.ok("SeCuris API. Date: " + new Date()).build();
+ }
- public ApiResource() {
- }
+ /**
+ * ping<p>
+ * JSON endpoint for health checks.
+ *
+ * @return 200 OK with {@link StatusBean}
+ */
+ @GET
+ @Path("/ping")
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Response ping() {
+ StatusBean status = new StatusBean();
+ status.setDate(new Date());
+ status.setMessage(LicenseManager.PING_MESSAGE);
+ return Response.ok(status).build();
+ }
- /**
- *
- * @return Simple text message to check API status
- */
- @GET
- @Path("/")
- @Produces({ MediaType.TEXT_PLAIN })
- public Response index() {
- return Response.ok("SeCuris API. Date: " + new Date()).build();
- }
+ // -------------------- License creation --------------------
- /**
- *
- * @return Simple text message to check API status
- */
- @GET
- @Path("/ping")
- @Produces({ MediaType.APPLICATION_JSON })
- public Response ping() {
- StatusBean status = new StatusBean();
- status.setDate(new Date());
- status.setMessage(LicenseManager.PING_MESSAGE);
- return Response.ok(status).build();
- }
+ /**
+ * createFromRequest<p>
+ * Create a new license from JSON request data.
+ *
+ * @param request RequestBean payload
+ * @param nameOrReference Holder name or external reference (header)
+ * @param userEmail Email (header)
+ * @return {@link SignedLicenseBean} JSON
+ */
+ @POST
+ @Path("/request")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Securable(roles = Rol.API_CLIENT)
+ @Produces({ MediaType.APPLICATION_JSON })
+ @EnsureTransaction
+ public Response createFromRequest(RequestBean request,
+ @HeaderParam(LicenseManager.HEADER_LICENSE_NAME_OR_REFERENCE) String nameOrReference,
+ @HeaderParam(LicenseManager.HEADER_LICENSE_EMAIL) String userEmail)
+ throws IOException, SeCurisServiceException, SeCurisException {
+ LOG.info("Request to get license: {}", request);
+ SignedLicenseBean lic = createLicense(request, em, nameOrReference, userEmail);
+ return Response.ok(lic).build();
+ }
- /**
- * Request a new license file based in a RequestBean object sent as
- * parameter
- *
- * @param mpfdi
- * @param bsc
- * @return
- * @throws IOException
- * @throws SeCurisServiceException
- */
- @POST
- @Path("/request")
- @Consumes(MediaType.APPLICATION_JSON)
- @Securable(roles = Rol.API_CLIENT)
- @Produces({ MediaType.APPLICATION_JSON })
- @EnsureTransaction
- public Response createFromRequest(RequestBean request, @HeaderParam(LicenseManager.HEADER_LICENSE_NAME_OR_REFERENCE) String nameOrReference,
- @HeaderParam(LicenseManager.HEADER_LICENSE_EMAIL) String userEmail) throws IOException, SeCurisServiceException, SeCurisException {
- LOG.info("Request to get license: {}", request);
- SignedLicenseBean lic = createLicense(request, em, nameOrReference, userEmail);
+ /**
+ * createFromRequestFile<p>
+ * Create a new license from a multipart form (uploaded request fields).
+ *
+ * @param mpfdi multipart input
+ * @param nameOrReference holder name/reference (header)
+ * @param userEmail email (header)
+ * @return {@link SignedLicenseBean} JSON
+ */
+ @POST
+ @Path("/request")
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ @Securable(roles = Rol.API_CLIENT)
+ @Produces({ MediaType.APPLICATION_JSON })
+ @EnsureTransaction
+ @SuppressWarnings("unchecked")
+ public Response createFromRequestFile(MultipartFormDataInput mpfdi,
+ @HeaderParam(LicenseManager.HEADER_LICENSE_NAME_OR_REFERENCE) String nameOrReference,
+ @HeaderParam(LicenseManager.HEADER_LICENSE_EMAIL) String userEmail)
+ throws IOException, SeCurisServiceException, SeCurisException {
+ RequestBean req = new RequestBean();
+ req.setAppCode(mpfdi.getFormDataPart("appCode", String.class, null));
+ req.setActivationCode(mpfdi.getFormDataPart("activationCode", String.class, null));
+ req.setPackCode(mpfdi.getFormDataPart("packCode", String.class, null));
+ req.setLicenseTypeCode(mpfdi.getFormDataPart("licenseTypeCode", String.class, null));
+ req.setCustomerCode(mpfdi.getFormDataPart("customerCode", String.class, null));
+ req.setArch(mpfdi.getFormDataPart("arch", String.class, null));
+ req.setCrcLogo(mpfdi.getFormDataPart("crcLogo", String.class, null));
+ req.setMacAddresses(mpfdi.getFormDataPart("macAddresses", List.class, null));
+ req.setOsName(mpfdi.getFormDataPart("osName", String.class, null));
- return Response.ok(lic).build();
- }
+ return createFromRequest(req, nameOrReference, userEmail);
+ }
- /**
- * Returns a License file in JSON format from an uploaded Request file
- *
- * @param mpfdi
- * @param bsc
- * @return
- * @throws IOException
- * @throws SeCurisServiceException
- * @throws SeCurisException
- */
- @POST
- @Path("/request")
- @Consumes(MediaType.MULTIPART_FORM_DATA)
- @Securable(roles = Rol.API_CLIENT)
- @Produces({ MediaType.APPLICATION_JSON })
- @EnsureTransaction
- @SuppressWarnings("unchecked")
- public Response createFromRequestFile(MultipartFormDataInput mpfdi, @HeaderParam(LicenseManager.HEADER_LICENSE_NAME_OR_REFERENCE) String nameOrReference,
- @HeaderParam(LicenseManager.HEADER_LICENSE_EMAIL) String userEmail) throws IOException, SeCurisServiceException, SeCurisException {
- RequestBean req = new RequestBean();
- req.setAppCode(mpfdi.getFormDataPart("appCode", String.class, null));
- req.setActivationCode(mpfdi.getFormDataPart("activationCode", String.class, null));
- req.setPackCode(mpfdi.getFormDataPart("packCode", String.class, null));
- req.setLicenseTypeCode(mpfdi.getFormDataPart("licenseTypeCode", String.class, null));
- req.setCustomerCode(mpfdi.getFormDataPart("customerCode", String.class, null));
- req.setArch(mpfdi.getFormDataPart("arch", String.class, null));
- req.setCrcLogo(mpfdi.getFormDataPart("crcLogo", String.class, null));
- req.setMacAddresses(mpfdi.getFormDataPart("macAddresses", List.class, null));
- req.setOsName(mpfdi.getFormDataPart("osName", String.class, null));
+ // -------------------- License renew --------------------
- return createFromRequest(req, nameOrReference, userEmail);
- }
+ /**
+ * renewFromPreviousLicense<p>
+ * Renew a license from an existing {@link LicenseBean} JSON payload.
+ * Only <b>Active</b> licenses within one month of expiration are eligible.
+ *
+ * @param previousLic current license bean
+ * @param bsc security context
+ * @return new {@link SignedLicenseBean}
+ */
+ @POST
+ @Path("/renew")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Securable(roles = Rol.API_CLIENT)
+ @Produces({ MediaType.APPLICATION_JSON })
+ @EnsureTransaction
+ public Response renewFromPreviousLicense(LicenseBean previousLic, @Context BasicSecurityContext bsc)
+ throws IOException, SeCurisServiceException, SeCurisException {
+ LOG.info("Renew license: {}", previousLic);
- /**
- * Create a new License file based in a previous one
- *
- * @param request
- * @param bsc
- * @return
- * @throws IOException
- * @throws SeCurisServiceException
- * @throws SeCurisException
- */
- @POST
- @Path("/renew")
- @Consumes(MediaType.APPLICATION_JSON)
- @Securable(roles = Rol.API_CLIENT)
- @Produces({ MediaType.APPLICATION_JSON })
- @EnsureTransaction
- public Response renewFromPreviousLicense(LicenseBean previousLic, @Context BasicSecurityContext bsc) throws IOException, SeCurisServiceException, SeCurisException {
- LOG.info("Renew license: {}", previousLic);
+ if (previousLic.getExpirationDate().after(DateUtils.addMonths(new Date(), 1))) {
+ throw new SeCurisServiceException(ErrorCodes.UNNECESSARY_RENEW, "The license is still valid, not ready for renew");
+ }
- if (previousLic.getExpirationDate().after(DateUtils.addMonths(new Date(), 1))) {
- throw new SeCurisServiceException(ErrorCodes.UNNECESSARY_RENEW, "The license is still valid, not ready for renew");
- }
+ License lic = License.findLicenseByCode(previousLic.getLicenseCode(), em);
+ if (lic == null) {
+ throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license is missing in DB");
+ }
+ if (lic.getStatus() != LicenseStatus.ACTIVE) {
+ throw new SeCurisServiceException(ErrorCodes.LICENSE_NOT_READY_FOR_RENEW, "Only licenses with status 'Active' can be renew");
+ }
- // EntityManager em = emProvider.get();
- License lic = License.findLicenseByCode(previousLic.getLicenseCode(), em);
- if (lic == null) {
- throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "Current license is missing in DB");
- }
+ SignedLicenseBean signedLic = renewLicense(previousLic, em);
+ LOG.info("Renewed license code: {}, until: {}", signedLic.getLicenseCode(), signedLic.getExpirationDate());
- if (lic.getStatus() != LicenseStatus.ACTIVE) {
- throw new SeCurisServiceException(ErrorCodes.LICENSE_NOT_READY_FOR_RENEW, "Only licenses with status 'Active' can be renew");
- }
+ return Response.ok(signedLic).build();
+ }
- SignedLicenseBean signedLic = renewLicense(previousLic, em);
- LOG.info("Renewed license code: {}, until: {}", signedLic.getLicenseCode(), signedLic.getExpirationDate());
+ /**
+ * renewFromLicenseFile<p>
+ * Renew a license from multipart (uploaded prior license fields).
+ *
+ * @param mpfdi multipart input
+ * @param bsc security context
+ * @return new {@link SignedLicenseBean}
+ */
+ @POST
+ @Path("/renew")
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ @Securable(roles = Rol.API_CLIENT)
+ @Produces({ MediaType.APPLICATION_JSON })
+ @EnsureTransaction
+ @SuppressWarnings("unchecked")
+ public Response renewFromLicenseFile(MultipartFormDataInput mpfdi, @Context BasicSecurityContext bsc)
+ throws IOException, SeCurisServiceException, SeCurisException {
+ LicenseBean lic = new LicenseBean();
- return Response.ok(signedLic).build();
- }
+ lic.setAppCode(mpfdi.getFormDataPart("appCode", String.class, null));
+ lic.setActivationCode(mpfdi.getFormDataPart("activationName", String.class, null));
+ lic.setAppName(mpfdi.getFormDataPart("appName", String.class, null));
+ lic.setArch(mpfdi.getFormDataPart("arch", String.class, null));
+ lic.setCrcLogo(mpfdi.getFormDataPart("crcLogo", String.class, null));
+ lic.setPackCode(mpfdi.getFormDataPart("packCode", String.class, null));
+ lic.setLicenseTypeCode(mpfdi.getFormDataPart("licenseCode", String.class, null));
+ lic.setCustomerCode(mpfdi.getFormDataPart("customerCode", String.class, null));
+ lic.setMacAddresses(mpfdi.getFormDataPart("macAddresses", List.class, null));
+ lic.setOsName(mpfdi.getFormDataPart("osName", String.class, null));
+ lic.setExpirationDate(mpfdi.getFormDataPart("expirationDate", Date.class, null));
- /**
- * License validation on server side, in this case we validate that the
- * current licenses has not been cancelled and they are still in valid
- * period. If the pack has reached the end valid period, the license is no
- * longer valid.
- *
- * @param currentLic
- * @param bsc
- * @return
- * @throws IOException
- * @throws SeCurisServiceException
- * @throws SeCurisException
- */
- @POST
- @Path("/validate")
- @Consumes(MediaType.APPLICATION_JSON)
- @Securable(roles = Rol.API_CLIENT)
- @Produces({ MediaType.APPLICATION_JSON })
- @EnsureTransaction
- public Response validate(LicenseBean currentLic, @Context BasicSecurityContext bsc) throws IOException, SeCurisServiceException, SeCurisException {
- LOG.info("Validate license: {}", currentLic);
+ LOG.info("Lic expires at: {}", lic.getExpirationDate());
+ if (lic.getExpirationDate().after(DateUtils.addMonths(new Date(), 1))) {
+ throw new SeCurisServiceException(ErrorCodes.LICENSE_NOT_READY_FOR_RENEW, "The license is still valid, not ready for renew");
+ }
- if (currentLic.getExpirationDate().before(new Date())) {
- throw new SeCurisServiceException(ErrorCodes.LICENSE_IS_EXPIRED, "The license is expired");
- }
+ return renewFromPreviousLicense(lic, bsc);
+ }
- License existingLic = licenseHelper.getActiveLicenseFromDB(currentLic, em);
+ // -------------------- Validation --------------------
- Pack pack = existingLic.getPack();
- if (pack.getEndValidDate().before(new Date())) {
- throw new SeCurisServiceException(ErrorCodes.LICENSE_PACK_IS_NOT_VALID, "The pack end valid date has been reached");
- }
- if (pack.getStatus() != PackStatus.ACTIVE) {
- LOG.error("The Pack {} status is not active, is: {}", pack.getCode(), pack.getStatus());
- throw new SeCurisServiceException(ErrorCodes.LICENSE_PACK_IS_NOT_VALID, "The pack status is not Active");
- }
+ /**
+ * validate<p>
+ * Server-side validation of a license:
+ * - Not expired
+ * - Pack still valid and active
+ * - Signature valid
+ *
+ * @param currentLic license to validate
+ * @param bsc security context
+ * @return same license if valid
+ */
+ @POST
+ @Path("/validate")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Securable(roles = Rol.API_CLIENT)
+ @Produces({ MediaType.APPLICATION_JSON })
+ @EnsureTransaction
+ public Response validate(LicenseBean currentLic, @Context BasicSecurityContext bsc)
+ throws IOException, SeCurisServiceException, SeCurisException {
+ LOG.info("Validate license: {}", currentLic);
- try {
- SignatureHelper.getInstance().validateSignature(currentLic);
- } catch (SeCurisException ex) {
- throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "The license signature is not valid");
- }
+ if (currentLic.getExpirationDate().before(new Date())) {
+ throw new SeCurisServiceException(ErrorCodes.LICENSE_IS_EXPIRED, "The license is expired");
+ }
- return Response.ok(currentLic).build();
- }
+ License existingLic = licenseHelper.getActiveLicenseFromDB(currentLic, em);
- /**
- * Returns a new License file in JSON format based in a previous license
- * There is 2 /renew services with json input and with upload file
- *
- * @param mpfdi
- * @param bsc
- * @return
- * @throws IOException
- * @throws SeCurisServiceException
- * @throws SeCurisException
- */
- @POST
- @Path("/renew")
- @Consumes(MediaType.MULTIPART_FORM_DATA)
- @Securable(roles = Rol.API_CLIENT)
- @Produces({ MediaType.APPLICATION_JSON })
- @EnsureTransaction
- @SuppressWarnings("unchecked")
- public Response renewFromLicenseFile(MultipartFormDataInput mpfdi, @Context BasicSecurityContext bsc) throws IOException, SeCurisServiceException, SeCurisException {
- LicenseBean lic = new LicenseBean();
+ Pack pack = existingLic.getPack();
+ if (pack.getEndValidDate().before(new Date())) {
+ throw new SeCurisServiceException(ErrorCodes.LICENSE_PACK_IS_NOT_VALID, "The pack end valid date has been reached");
+ }
+ if (pack.getStatus() != PackStatus.ACTIVE) {
+ LOG.error("The Pack {} status is not active, is: {}", pack.getCode(), pack.getStatus());
+ throw new SeCurisServiceException(ErrorCodes.LICENSE_PACK_IS_NOT_VALID, "The pack status is not Active");
+ }
- lic.setAppCode(mpfdi.getFormDataPart("appCode", String.class, null));
- lic.setActivationCode(mpfdi.getFormDataPart("activationName", String.class, null));
- lic.setAppName(mpfdi.getFormDataPart("appName", String.class, null));
- lic.setArch(mpfdi.getFormDataPart("arch", String.class, null));
- lic.setCrcLogo(mpfdi.getFormDataPart("crcLogo", String.class, null));
- lic.setPackCode(mpfdi.getFormDataPart("packCode", String.class, null));
- lic.setLicenseTypeCode(mpfdi.getFormDataPart("licenseCode", String.class, null));
- lic.setCustomerCode(mpfdi.getFormDataPart("customerCode", String.class, null));
- lic.setMacAddresses(mpfdi.getFormDataPart("macAddresses", List.class, null));
- lic.setOsName(mpfdi.getFormDataPart("osName", String.class, null));
- lic.setExpirationDate(mpfdi.getFormDataPart("expirationDate", Date.class, null));
- LOG.info("Lic expires at: {}", lic.getExpirationDate());
- if (lic.getExpirationDate().after(DateUtils.addMonths(new Date(), 1))) {
- throw new SeCurisServiceException(ErrorCodes.LICENSE_NOT_READY_FOR_RENEW, "The license is still valid, not ready for renew");
- }
+ try {
+ SignatureHelper.getInstance().validateSignature(currentLic);
+ } catch (SeCurisException ex) {
+ throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "The license signature is not valid");
+ }
- return renewFromPreviousLicense(lic, bsc);
- }
+ return Response.ok(currentLic).build();
+ }
- /**
- * Creates a new signed license from request data or from previous license
- * if It's a renew
- *
- * @param req
- * @param em
- * @param renew
- * @return
- * @throws SeCurisServiceException
- */
- private SignedLicenseBean createLicense(RequestBean req, EntityManager em, String nameOrReference, String email) throws SeCurisServiceException {
- License lic = null;
+ // -------------------- Internal helpers --------------------
- if (req.getActivationCode() != null) {
- lic = License.findLicenseByActivationCode(req.getActivationCode(), em);
- if (lic == null) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The given activation code is invalid: " + req.getActivationCode());
- }
- if (lic.getStatus() == LicenseStatus.ACTIVE) {
- RequestBean initialRequest;
- try {
- initialRequest = JsonUtils.json2object(lic.getRequestData(), RequestBean.class);
- if (!req.match(initialRequest)) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "There is already an active license for given activation code: " + req.getActivationCode());
- } else {
- return JsonUtils.json2object(lic.getLicenseData(), SignedLicenseBean.class);
- }
- } catch (SeCurisException e) {
- LOG.error("Error getting existing license", e);
- throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Original request is wrong");
- }
- } else {
- if (req.getAppCode() != null && !req.getAppCode().equals(lic.getPack().getLicenseType().getApplication().getCode())) {
- LOG.error("Activation code {} belongs to app: {} but was sent by: {}", req.getActivationCode(), lic.getPack().getLicenseType().getApplication().getCode(),
- req.getAppCode());
- throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The given activation code belongs to a different application: " + req.getActivationCode());
- }
- }
- // We validate if the HW is the same, otherwise an error is
- // thrown
- } else {
- try {
- lic = License.findValidLicenseByRequestData(JsonUtils.toJSON(req), em);
- } catch (SeCurisException e1) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Request sent is not valid");
- }
- if (lic != null) {
- try {
- if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE) {
- return JsonUtils.json2object(lic.getLicenseData(), SignedLicenseBean.class);
- }
- } catch (SeCurisException e) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error trying to get the license bean from license code: " + lic.getCode());
- }
- } else {
- lic = new License();
- }
- }
+ /**
+ * createLicense<p>
+ * Creates a new signed license from request data or reuses an existing
+ * pre-active/active one when allowed by business rules.
+ *
+ * @param req request bean
+ * @param em entity manager
+ * @param nameOrReference license holder name/reference (header)
+ * @param email email (header)
+ * @return signed license bean
+ */
+ private SignedLicenseBean createLicense(RequestBean req, EntityManager em, String nameOrReference, String email)
+ throws SeCurisServiceException {
- Pack pack;
- if (lic.getActivationCode() == null) {
- try {
- pack = em.createNamedQuery("pack-by-code", Pack.class).setParameter("code", req.getPackCode()).getSingleResult();
- } catch (NoResultException e) {
- throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "No pack found for code: " + req.getPackCode());
- }
+ License lic = null;
- if (pack.getNumAvailables() <= 0) {
- throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "The current pack has no licenses availables");
- }
- if (lic.getStatus() == LicenseStatus.REQUESTED && !pack.isLicensePreactivation()) {
- throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "Current pack doesn't allow license preactivation");
- }
+ // (1) Activation-code flow
+ if (req.getActivationCode() != null) {
+ lic = License.findLicenseByActivationCode(req.getActivationCode(), em);
+ if (lic == null) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The given activation code is invalid: " + req.getActivationCode());
+ }
+ if (lic.getStatus() == LicenseStatus.ACTIVE) {
+ try {
+ RequestBean initialRequest = JsonUtils.json2object(lic.getRequestData(), RequestBean.class);
+ if (!req.match(initialRequest)) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_DATA,
+ "There is already an active license for given activation code: " + req.getActivationCode());
+ } else {
+ return JsonUtils.json2object(lic.getLicenseData(), SignedLicenseBean.class);
+ }
+ } catch (SeCurisException e) {
+ LOG.error("Error getting existing license", e);
+ throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Original request is wrong");
+ }
+ } else {
+ if (req.getAppCode() != null &&
+ !req.getAppCode().equals(lic.getPack().getLicenseType().getApplication().getCode())) {
+ LOG.error("Activation code {} belongs to app: {} but was sent by: {}",
+ req.getActivationCode(), lic.getPack().getLicenseType().getApplication().getCode(), req.getAppCode());
+ throw new SeCurisServiceException(ErrorCodes.INVALID_DATA,
+ "The given activation code belongs to a different application: " + req.getActivationCode());
+ }
+ }
+ } else {
+ // (2) Request-data flow (idempotent check)
+ try {
+ lic = License.findValidLicenseByRequestData(JsonUtils.toJSON(req), em);
+ } catch (SeCurisException e1) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Request sent is not valid");
+ }
+ if (lic != null) {
+ try {
+ if (lic.getStatus() == LicenseStatus.ACTIVE || lic.getStatus() == LicenseStatus.PRE_ACTIVE) {
+ return JsonUtils.json2object(lic.getLicenseData(), SignedLicenseBean.class);
+ }
+ } catch (SeCurisException e) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT,
+ "Error trying to get the license bean from license code: " + lic.getCode());
+ }
+ } else {
+ lic = new License();
+ }
+ }
- if (!req.getCustomerCode().equals(pack.getOrganization().getCode())) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Customer code is not valid: " + req.getCustomerCode());
- }
+ // (3) Pack validation & constraints
+ Pack pack;
+ if (lic.getActivationCode() == null) {
+ try {
+ pack = em.createNamedQuery("pack-by-code", Pack.class)
+ .setParameter("code", req.getPackCode())
+ .getSingleResult();
+ } catch (NoResultException e) {
+ throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "No pack found for code: " + req.getPackCode());
+ }
- if (!req.getLicenseTypeCode().equals(pack.getLicenseTypeCode())) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "License type code is not valid: " + req.getLicenseTypeCode());
- }
- } else {
- pack = lic.getPack();
- }
- if (pack.getStatus() != PackStatus.ACTIVE) {
- LOG.error("The Pack {} status is not active, is: {}", pack.getCode(), pack.getStatus());
- throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "The pack status is not Active");
- }
- SignedLicenseBean signedLicense;
- try {
- String licCode;
- if (lic.getCode() == null) {
- licCode = LicUtils.getLicenseCode(pack.getCode(), licenseHelper.getNextCodeSuffix(pack.getId(), em));
- } else {
- licCode = lic.getCode();
- }
- Date expirationDate = licenseHelper.getExpirationDateFromPack(pack, lic.getActivationCode() == null);
+ if (pack.getNumAvailables() <= 0) {
+ throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "The current pack has no licenses availables");
+ }
+ if (lic.getStatus() == LicenseStatus.REQUESTED && !pack.isLicensePreactivation()) {
+ throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "Current pack doesn't allow license preactivation");
+ }
+ if (!req.getCustomerCode().equals(pack.getOrganization().getCode())) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Customer code is not valid: " + req.getCustomerCode());
+ }
+ if (!req.getLicenseTypeCode().equals(pack.getLicenseTypeCode())) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "License type code is not valid: " + req.getLicenseTypeCode());
+ }
+ } else {
+ pack = lic.getPack();
+ }
- LicenseBean lb = licenseGenerator.generateLicense(req, licenseHelper.extractPackMetadata(pack.getMetadata()), expirationDate, licCode, pack.getAppName());
- signedLicense = new SignedLicenseBean(lb);
- } catch (SeCurisException e) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString());
- }
- try {
- lic.setRequestData(JsonUtils.toJSON(req));
- if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
- throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be activated");
- }
- lic.setLicenseData(JsonUtils.toJSON(signedLicense));
- } catch (SeCurisException e) {
- LOG.error("Error generating license JSON", e);
- throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generating license JSON");
- }
+ if (pack.getStatus() != PackStatus.ACTIVE) {
+ LOG.error("The Pack {} status is not active, is: {}", pack.getCode(), pack.getStatus());
+ throw new SeCurisServiceException(ErrorCodes.LICENSE_DATA_IS_NOT_VALID, "The pack status is not Active");
+ }
- lic.setModificationTimestamp(new Date());
- lic.setExpirationDate(signedLicense.getExpirationDate());
- User user = em.find(User.class, API_CLIENT_USERNAME);
- if (lic.getStatus() != LicenseStatus.REQUESTED) {
- lic.setPack(pack);
- lic.setCreatedBy(user);
- lic.setCreationTimestamp(new Date());
- if (lic.getActivationCode() != null) {
- lic.setStatus(LicenseStatus.ACTIVE);
- } else {
- lic.setStatus(pack.isLicensePreactivation() ? LicenseStatus.PRE_ACTIVE : LicenseStatus.REQUESTED);
- }
- lic.setCode(signedLicense.getLicenseCode());
- lic.setCodeSuffix(LicUtils.getLicenseCodeSuffix(signedLicense.getLicenseCode()));
- if (lic.getEmail() == null || "".equals(lic.getEmail())) {
- lic.setEmail(email);
- }
- if (lic.getFullName() == null || "".equals(lic.getFullName())) {
- lic.setFullName(nameOrReference);
- }
- if (lic.getId() != null) {
- em.merge(lic);
- } else {
- em.persist(lic);
- }
- em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.CREATE));
- if (lic.getActivationCode() != null) {
- em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.ACTIVATE, "Activated by code on creation"));
- } else {
- if (pack.isLicensePreactivation()) {
- em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.PRE_ACTIVATE, "Pre-activated on creation"));
- } else {
- LOG.warn("License ({}) created, but the pack doesn't allow preactivation", lic.getCode());
- throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "Current pack doesn't allow license preactivation");
- }
- }
- } else {
- lic.setStatus(LicenseStatus.PRE_ACTIVE);
- em.merge(lic);
- em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.PRE_ACTIVATE, "Pre-activated after request"));
- }
+ // (4) License generation
+ SignedLicenseBean signedLicense;
+ try {
+ String licCode = (lic.getCode() == null)
+ ? LicUtils.getLicenseCode(pack.getCode(), licenseHelper.getNextCodeSuffix(pack.getId(), em))
+ : lic.getCode();
- return signedLicense;
- }
+ Date expirationDate = licenseHelper.getExpirationDateFromPack(pack, lic.getActivationCode() == null);
+ LicenseBean lb = licenseGenerator.generateLicense(
+ req,
+ licenseHelper.extractPackMetadata(pack.getMetadata()),
+ expirationDate,
+ licCode,
+ pack.getAppName()
+ );
+ signedLicense = new SignedLicenseBean(lb);
+ } catch (SeCurisException e) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString());
+ }
- /**
- * Creates a new signed license from request data or from previous license
- * if It's a renew
- *
- * @param req
- * @param em
- * @param renew
- * @return
- * @throws SeCurisServiceException
- */
- private SignedLicenseBean renewLicense(LicenseBean previousLicenseBean, EntityManager em) throws SeCurisServiceException {
+ // (5) Persist/merge license + history
+ try {
+ lic.setRequestData(JsonUtils.toJSON(req));
+ if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
+ throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be activated");
+ }
+ lic.setLicenseData(JsonUtils.toJSON(signedLicense));
+ } catch (SeCurisException e) {
+ LOG.error("Error generating license JSON", e);
+ throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generating license JSON");
+ }
- License lic = License.findLicenseByCode(previousLicenseBean.getLicenseCode(), em);
- if (lic.getStatus() != LicenseStatus.ACTIVE && lic.getStatus() != LicenseStatus.PRE_ACTIVE) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The current license has been cancelled");
- }
+ lic.setModificationTimestamp(new Date());
+ lic.setExpirationDate(signedLicense.getExpirationDate());
+ User user = em.find(User.class, API_CLIENT_USERNAME);
- Pack pack = lic.getPack();
- SignedLicenseBean signedLicense;
- try {
- String licCode = lic.getCode();
- Date expirationDate = licenseHelper.getExpirationDateFromPack(pack, false);
+ if (lic.getStatus() != LicenseStatus.REQUESTED) {
+ lic.setPack(pack);
+ lic.setCreatedBy(user);
+ lic.setCreationTimestamp(new Date());
+ if (lic.getActivationCode() != null) {
+ lic.setStatus(LicenseStatus.ACTIVE);
+ } else {
+ lic.setStatus(pack.isLicensePreactivation() ? LicenseStatus.PRE_ACTIVE : LicenseStatus.REQUESTED);
+ }
+ lic.setCode(signedLicense.getLicenseCode());
+ lic.setCodeSuffix(LicUtils.getLicenseCodeSuffix(signedLicense.getLicenseCode()));
+ if (lic.getEmail() == null || "".equals(lic.getEmail())) {
+ lic.setEmail(email);
+ }
+ if (lic.getFullName() == null || "".equals(lic.getFullName())) {
+ lic.setFullName(nameOrReference);
+ }
+ if (lic.getId() != null) {
+ em.merge(lic);
+ } else {
+ em.persist(lic);
+ }
+ em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.CREATE));
+ if (lic.getActivationCode() != null) {
+ em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.ACTIVATE, "Activated by code on creation"));
+ } else {
+ if (pack.isLicensePreactivation()) {
+ em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.PRE_ACTIVATE, "Pre-activated on creation"));
+ } else {
+ LOG.warn("License ({}) created, but the pack doesn't allow preactivation", lic.getCode());
+ throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "Current pack doesn't allow license preactivation");
+ }
+ }
+ } else {
+ lic.setStatus(LicenseStatus.PRE_ACTIVE);
+ em.merge(lic);
+ em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.PRE_ACTIVATE, "Pre-activated after request"));
+ }
- LicenseBean lb = licenseGenerator.generateLicense(previousLicenseBean, licenseHelper.extractPackMetadata(pack.getMetadata()), expirationDate, licCode,
- pack.getAppName());
- signedLicense = new SignedLicenseBean(lb);
- } catch (SeCurisException e) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString());
- }
- try {
- lic.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
- if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
- throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be activated");
- }
- lic.setLicenseData(JsonUtils.toJSON(signedLicense));
- } catch (SeCurisException e) {
- LOG.error("Error generating license JSON", e);
- throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generating license JSON");
- }
+ return signedLicense;
+ }
- lic.setModificationTimestamp(new Date());
- lic.setExpirationDate(signedLicense.getExpirationDate());
- User user = em.find(User.class, API_CLIENT_USERNAME);
+ /**
+ * renewLicense<p>
+ * Internal renew logic used by JSON and multipart variants.
+ *
+ * @param previousLicenseBean previous license data
+ * @param em entity manager
+ * @return new signed license bean
+ */
+ private SignedLicenseBean renewLicense(LicenseBean previousLicenseBean, EntityManager em)
+ throws SeCurisServiceException {
- lic.setStatus(LicenseStatus.ACTIVE);
- em.merge(lic);
- em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.RENEW));
+ License lic = License.findLicenseByCode(previousLicenseBean.getLicenseCode(), em);
+ if (lic.getStatus() != LicenseStatus.ACTIVE && lic.getStatus() != LicenseStatus.PRE_ACTIVE) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The current license has been cancelled");
+ }
- return signedLicense;
- }
+ Pack pack = lic.getPack();
+ SignedLicenseBean signedLicense;
+ try {
+ String licCode = lic.getCode();
+ Date expirationDate = licenseHelper.getExpirationDateFromPack(pack, false);
+
+ LicenseBean lb = licenseGenerator.generateLicense(
+ previousLicenseBean,
+ licenseHelper.extractPackMetadata(pack.getMetadata()),
+ expirationDate,
+ licCode,
+ pack.getAppName()
+ );
+ signedLicense = new SignedLicenseBean(lb);
+ } catch (SeCurisException e) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString());
+ }
+ try {
+ lic.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
+ if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
+ throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be activated");
+ }
+ lic.setLicenseData(JsonUtils.toJSON(signedLicense));
+ } catch (SeCurisException e) {
+ LOG.error("Error generating license JSON", e);
+ throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generating license JSON");
+ }
+
+ lic.setModificationTimestamp(new Date());
+ lic.setExpirationDate(signedLicense.getExpirationDate());
+ User user = em.find(User.class, API_CLIENT_USERNAME);
+
+ lic.setStatus(LicenseStatus.ACTIVE);
+ em.merge(lic);
+ em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.RENEW));
+
+ return signedLicense;
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/services/ApplicationResource.java b/securis/src/main/java/net/curisit/securis/services/ApplicationResource.java
index e9b2776..f004acd 100644
--- a/securis/src/main/java/net/curisit/securis/services/ApplicationResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/ApplicationResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services;
import java.util.Date;
@@ -42,204 +45,246 @@
import net.curisit.securis.utils.TokenHelper;
/**
- * Application resource, this service will provide methods to create, modify and
- * delete applications
- *
- * @author roberto <roberto.sanchez@curisit.net>
+ * ApplicationResource
+ * <p>
+ * REST endpoints to list, fetch, create, update and delete {@link Application}s.
+ * Security:
+ * <ul>
+ * <li>Listing filters by user's accessible application IDs unless ADMIN.</li>
+ * <li>Create/Modify/Delete restricted to ADMIN.</li>
+ * </ul>
+ * Side-effects:
+ * <ul>
+ * <li>Manages {@link ApplicationMetadata} lifecycle on create/update.</li>
+ * <li>Propagates metadata changes via {@link MetadataHelper}.</li>
+ * </ul>
+ *
+ * Author: roberto <roberto.sanchez@curisit.net><br>
+ * Last reviewed by JRA on Oct 5, 2025.
*/
@Path("/application")
public class ApplicationResource {
- @Inject
- TokenHelper tokenHelper;
+ @Inject TokenHelper tokenHelper;
+ @Inject MetadataHelper metadataHelper;
- @Inject
- MetadataHelper metadataHelper;
+ @Context EntityManager em;
- @Context
- EntityManager em;
+ private static final Logger LOG = LogManager.getLogger(ApplicationResource.class);
- private static final Logger LOG = LogManager.getLogger(ApplicationResource.class);
+ /**
+ * ApplicationResource<p>
+ * Constructor
+ */
+ public ApplicationResource() {}
- public ApplicationResource() {
- }
+ /**
+ * index<p>
+ * List applications visible to the current user.
+ *
+ * @param bsc security context
+ * @return 200 with list (possibly empty) or 200 empty if user has no app scope
+ */
+ @GET
+ @Path("/")
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Securable
+ public Response index(@Context BasicSecurityContext bsc) {
+ LOG.info("Getting applications list ");
+ em.clear();
- /**
- *
- * @return the server version in format majorVersion.minorVersion
- */
- @GET
- @Path("/")
- @Produces({ MediaType.APPLICATION_JSON })
- @Securable
- public Response index(@Context BasicSecurityContext bsc) {
- LOG.info("Getting applications list ");
+ TypedQuery<Application> q;
+ if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
+ q = em.createNamedQuery("list-applications", Application.class);
+ } else {
+ if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
+ return Response.ok().build();
+ }
+ q = em.createNamedQuery("list-applications-by_ids", Application.class);
+ q.setParameter("list_ids", bsc.getApplicationsIds());
+ }
+ List<Application> list = q.getResultList();
+ return Response.ok(list).build();
+ }
- // EntityManager em = emProvider.get();
- em.clear();
+ /**
+ * get<p>
+ * Fetch a single application by ID.
+ *
+ * @param appid string ID
+ * @return 200 + entity or 404 if not found
+ * @throws SeCurisServiceException when ID is invalid or not found
+ */
+ @GET
+ @Path("/{appid}")
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Securable
+ public Response get(@PathParam("appid") String appid) throws SeCurisServiceException {
+ LOG.info("Getting application data for id: {}: ", appid);
+ if (appid == null || "".equals(appid)) {
+ LOG.error("Application ID is mandatory");
+ return Response.status(Status.NOT_FOUND).build();
+ }
- TypedQuery<Application> q;
- if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
- q = em.createNamedQuery("list-applications", Application.class);
- } else {
- if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
- return Response.ok().build();
- }
- q = em.createNamedQuery("list-applications-by_ids", Application.class);
+ em.clear();
+ Application app = null;
+ try {
+ LOG.info("READY to GET app: {}", appid);
+ app = em.find(Application.class, Integer.parseInt(appid));
+ } catch (Exception e) {
+ LOG.info("ERROR GETTING app: {}", e);
+ }
+ if (app == null) {
+ LOG.error("Application with id {} not found in DB", appid);
+ throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "Application not found with ID: " + appid);
+ }
+ return Response.ok(app).build();
+ }
- q.setParameter("list_ids", bsc.getApplicationsIds());
- }
- List<Application> list = q.getResultList();
+ /**
+ * create<p>
+ * Create a new application with optional metadata entries.
+ *
+ * @param app application payload
+ * @param token auth token (audited externally)
+ * @return 200 + persisted entity
+ */
+ @POST
+ @Path("/")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces({ MediaType.APPLICATION_JSON })
+ @EnsureTransaction
+ @Securable(roles = Rol.ADMIN)
+ @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+ public Response create(Application app, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+ LOG.info("Creating new application");
+ app.setCreationTimestamp(new Date());
+ em.persist(app);
- return Response.ok(list).build();
- }
+ if (app.getApplicationMetadata() != null) {
+ for (ApplicationMetadata md : app.getApplicationMetadata()) {
+ md.setApplication(app);
+ md.setCreationTimestamp(new Date());
+ em.persist(md);
+ }
+ }
+ LOG.info("Creating application ({}) with date: {}", app.getId(), app.getCreationTimestamp());
+ return Response.ok(app).build();
+ }
- /**
- *
- * @return the server version in format majorVersion.minorVersion
- * @throws SeCurisServiceException
- */
- @GET
- @Path("/{appid}")
- @Produces({ MediaType.APPLICATION_JSON })
- @Securable
- public Response get(@PathParam("appid") String appid) throws SeCurisServiceException {
- LOG.info("Getting application data for id: {}: ", appid);
- if (appid == null || "".equals(appid)) {
- LOG.error("Application ID is mandatory");
- return Response.status(Status.NOT_FOUND).build();
- }
+ /**
+ * modify<p>
+ * Update core fields and reconcile metadata set:
+ * <ul>
+ * <li>Removes missing keys, merges existing, persists new.</li>
+ * <li>Propagates metadata if there were changes.</li>
+ * </ul>
+ *
+ * @param appid path ID
+ * @param app new state
+ */
+ @PUT
+ @POST
+ @Path("/{appid}")
+ @EnsureTransaction
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Securable(roles = Rol.ADMIN)
+ @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+ public Response modify(Application app, @PathParam("appid") String appid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+ LOG.info("Modifying application with id: {}", appid);
+ Application currentapp = em.find(Application.class, Integer.parseInt(appid));
+ if (currentapp == null) {
+ LOG.error("Application with id {} not found in DB", appid);
+ return Response.status(Status.NOT_FOUND)
+ .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid)
+ .build();
+ }
- em.clear();
+ currentapp.setCode(app.getCode());
+ currentapp.setName(app.getName());
+ currentapp.setLicenseFilename(app.getLicenseFilename());
+ currentapp.setDescription(app.getDescription());
- Application app = null;
- try {
- LOG.info("READY to GET app: {}", appid);
- app = em.find(Application.class, Integer.parseInt(appid));
- } catch (Exception e) {
- LOG.info("ERROR GETTING app: {}", e);
- }
- if (app == null) {
- LOG.error("Application with id {} not found in DB", appid);
- throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "Application not found with ID: " + appid);
- }
+ Set<ApplicationMetadata> newMD = app.getApplicationMetadata();
+ Set<ApplicationMetadata> oldMD = currentapp.getApplicationMetadata();
+ boolean metadataChanges = !metadataHelper.match(newMD, oldMD);
+ if (metadataChanges) {
+ Map<String, ApplicationMetadata> directOldMD = getMapMD(oldMD);
+ Map<String, ApplicationMetadata> directNewMD = getMapMD(newMD);
- return Response.ok(app).build();
- }
+ // Remove deleted MD
+ for (ApplicationMetadata currentMd : oldMD) {
+ if (newMD == null || !directNewMD.containsKey(currentMd.getKey())) {
+ em.remove(currentMd);
+ }
+ }
+ // Merge or persist
+ if (newMD != null) {
+ for (ApplicationMetadata md : newMD) {
+ if (directOldMD.containsKey(md.getKey())) {
+ em.merge(md);
+ } else {
+ md.setApplication(currentapp);
+ if (md.getCreationTimestamp() == null) {
+ md.setCreationTimestamp(app.getCreationTimestamp());
+ }
+ em.persist(md);
+ }
+ }
+ }
+ currentapp.setApplicationMetadata(app.getApplicationMetadata());
+ }
- @POST
- @Path("/")
- @Consumes(MediaType.APPLICATION_JSON)
- @Produces({ MediaType.APPLICATION_JSON })
- @EnsureTransaction
- @Securable(roles = Rol.ADMIN)
- @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response create(Application app, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
- LOG.info("Creating new application");
- // EntityManager em = emProvider.get();
- app.setCreationTimestamp(new Date());
- em.persist(app);
+ em.merge(currentapp);
+ if (metadataChanges) {
+ metadataHelper.propagateMetadata(em, currentapp);
+ }
+ return Response.ok(currentapp).build();
+ }
- if (app.getApplicationMetadata() != null) {
- for (ApplicationMetadata md : app.getApplicationMetadata()) {
- md.setApplication(app);
- md.setCreationTimestamp(new Date());
- em.persist(md);
- }
- }
- LOG.info("Creating application ({}) with date: {}", app.getId(), app.getCreationTimestamp());
+ /**
+ * getMapMD<p>
+ * Build a map from metadata key → entity for fast reconciliation.
+ *
+ * @param applicationMetadata
+ * @return mapMD
+ */
+ private Map<String, ApplicationMetadata> getMapMD(Set<ApplicationMetadata> amd) {
+ Map<String, ApplicationMetadata> map = new HashMap<>();
+ if (amd != null) {
+ for (ApplicationMetadata m : amd) {
+ map.put(m.getKey(), m);
+ }
+ }
+ return map;
+ }
- return Response.ok(app).build();
- }
-
- @PUT
- @POST
- @Path("/{appid}")
- @EnsureTransaction
- @Consumes(MediaType.APPLICATION_JSON)
- @Produces({ MediaType.APPLICATION_JSON })
- @Securable(roles = Rol.ADMIN)
- @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response modify(Application app, @PathParam("appid") String appid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
- LOG.info("Modifying application with id: {}", appid);
- // EntityManager em = emProvider.get();
- Application currentapp = em.find(Application.class, Integer.parseInt(appid));
- if (currentapp == null) {
- LOG.error("Application with id {} not found in DB", appid);
- return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid).build();
- }
- currentapp.setCode(app.getCode());
- currentapp.setName(app.getName());
- currentapp.setLicenseFilename(app.getLicenseFilename());
- currentapp.setDescription(app.getDescription());
-
- Set<ApplicationMetadata> newMD = app.getApplicationMetadata();
- Set<ApplicationMetadata> oldMD = currentapp.getApplicationMetadata();
- boolean metadataChanges = !metadataHelper.match(newMD, oldMD);
- if (metadataChanges) {
- Map<String, ApplicationMetadata> directOldMD = getMapMD(oldMD);
- Map<String, ApplicationMetadata> directNewMD = getMapMD(newMD);
- for (ApplicationMetadata currentMd : oldMD) {
- if (newMD == null || !directNewMD.containsKey(currentMd.getKey())) {
- em.remove(currentMd);
- }
- }
-
- if (newMD != null) {
- for (ApplicationMetadata md : newMD) {
- if (directOldMD.containsKey(md.getKey())) {
- em.merge(md);
- } else {
- md.setApplication(currentapp);
- if (md.getCreationTimestamp() == null) {
- md.setCreationTimestamp(app.getCreationTimestamp());
- }
- em.persist(md);
- }
- }
- }
- currentapp.setApplicationMetadata(app.getApplicationMetadata());
- }
- em.merge(currentapp);
- if (metadataChanges) {
- metadataHelper.propagateMetadata(em, currentapp);
- }
- return Response.ok(currentapp).build();
- }
-
- private Map<String, ApplicationMetadata> getMapMD(Set<ApplicationMetadata> amd) {
- Map<String, ApplicationMetadata> map = new HashMap<String, ApplicationMetadata>();
- if (amd != null) {
- for (ApplicationMetadata applicationMetadata : amd) {
- map.put(applicationMetadata.getKey(), applicationMetadata);
- }
- }
- return map;
- }
-
- @DELETE
- @Path("/{appid}")
- @EnsureTransaction
- @Produces({ MediaType.APPLICATION_JSON })
- @Securable(roles = Rol.ADMIN)
- @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response delete(@PathParam("appid") String appid, @Context HttpServletRequest request) {
- LOG.info("Deleting app with id: {}", appid);
- // EntityManager em = emProvider.get();
- Application app = em.find(Application.class, Integer.parseInt(appid));
- if (app == null) {
- LOG.error("Application with id {} can not be deleted, It was not found in DB", appid);
- return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid).build();
- }
- /*
- * if (app.getLicenseTypes() != null &&
- * !app.getLicenseTypes().isEmpty()) { throw new
- * SeCurisServiceException(ErrorCodes.NOT_FOUND,
- * "Application can not be deleted becasue has assigned one or more License types, ID: "
- * + appid); }
- */
- em.remove(app);
- return Response.ok(Utils.createMap("success", true, "id", appid)).build();
- }
-
+ /**
+ * delete<p>
+ * Delete an application by ID.
+ * <p>Note: deletion is not allowed if there are dependent entities (enforced by DB/cascade).</p>
+ *
+ * @param appId
+ * @param request
+ */
+ @DELETE
+ @Path("/{appid}")
+ @EnsureTransaction
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Securable(roles = Rol.ADMIN)
+ @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+ public Response delete(@PathParam("appid") String appid, @Context HttpServletRequest request) {
+ LOG.info("Deleting app with id: {}", appid);
+ Application app = em.find(Application.class, Integer.parseInt(appid));
+ if (app == null) {
+ LOG.error("Application with id {} can not be deleted, It was not found in DB", appid);
+ return Response.status(Status.NOT_FOUND)
+ .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Application not found with ID: " + appid)
+ .build();
+ }
+ em.remove(app);
+ return Response.ok(Utils.createMap("success", true, "id", appid)).build();
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/services/BasicServices.java b/securis/src/main/java/net/curisit/securis/services/BasicServices.java
index f025795..2c989cc 100644
--- a/securis/src/main/java/net/curisit/securis/services/BasicServices.java
+++ b/securis/src/main/java/net/curisit/securis/services/BasicServices.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services;
import java.net.URI;
@@ -32,96 +35,124 @@
import net.curisit.securis.utils.TokenHelper;
/**
- * Basic services for login and basic app wrkflow
- *
- * @author roberto <roberto.sanchez@curisit.net>
+ * BasicServices
+ * <p>
+ * Minimal public endpoints for service liveness, version info and token checks.
+ * Also provides entry routing to SPA (admin/login/licenses) via /index.jsp.
+ *
+ * Security:
+ * <ul>
+ * <li>/check requires a valid bearer token (via {@link Securable}).</li>
+ * <li>/logout just logs intention; token invalidation is outside this class.</li>
+ * </ul>
+ *
+ * Author: roberto <roberto.sanchez@curisit.net>
+ * Last reviewed by JRA on Oct 5, 2025.
*/
@Path("/")
@ApplicationScoped
public class BasicServices {
- private static final Logger LOG = LogManager.getLogger(BasicServices.class);
+ private static final Logger LOG = LogManager.getLogger(BasicServices.class);
- @Inject
- TokenHelper tokenHelper;
+ @Inject TokenHelper tokenHelper;
+ @Context EntityManager em;
- @Context
- EntityManager em;
+ @Inject public BasicServices() {}
- @Inject
- public BasicServices() {
- }
+ /**
+ * info<p>
+ * Simple liveness text endpoint.
+ *
+ * @param request
+ * @return response
+ */
+ @GET
+ @Path("/info")
+ @Produces({ MediaType.TEXT_PLAIN })
+ public Response info(@Context HttpServletRequest request) {
+ return Response.ok().entity("License server running OK. Date: " + new Date()).build();
+ }
- @GET
- @Path("/info")
- @Produces({ MediaType.TEXT_PLAIN })
- public Response info(@Context HttpServletRequest request) {
- return Response.ok().entity("License server running OK. Date: " + new Date()).build();
- }
+ /**
+ * version<p>
+ * Returns semantic app version as JSON.
+ *
+ * @param request
+ * @return version
+ */
+ @GET
+ @Path("/version")
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Map<String, String> version(@Context HttpServletRequest request) {
+ Map<String, String> resp = new HashMap<>();
+ resp.put("version", AppVersion.getInstance().getCompleteVersion());
+ return resp;
+ }
- @GET
- @Path("/version")
- @Produces({ MediaType.APPLICATION_JSON })
- public Map<String, String> version(@Context HttpServletRequest request) {
- Map<String, String> resp = new HashMap<>();
-
- // Get the real version
- String version = AppVersion.getInstance().getCompleteVersion();
- resp.put("version", version);
- return resp;
- }
+ /**
+ * init<p>
+ * Redirects SPA modules to the main index page.
+ *
+ * @param module
+ * @param request
+ * @return response
+ */
+ @GET
+ @Path("/{module:(admin)|(login)|(licenses)}")
+ @Produces({ MediaType.TEXT_HTML })
+ public Response init(@PathParam("module") String module, @Context HttpServletRequest request) {
+ LOG.info("App index main.html");
+ URI uri = UriBuilder.fromUri("/index.jsp").build();
+ return Response.seeOther(uri).build();
+ }
- @GET
- @Path("/{module:(admin)|(login)|(licenses)}")
- @Produces({ MediaType.TEXT_HTML })
- public Response init(@PathParam("module") String module, @Context HttpServletRequest request) {
- LOG.info("App index main.html");
- String page = "/index.jsp";
- URI uri = UriBuilder.fromUri(page).build();
- return Response.seeOther(uri).build();
- }
+ /**
+ * check<p>
+ * Validates a token (from header or query param).
+ *
+ * @param token X-Token header
+ * @param token2 token query param fallback
+ * @return 200 with user/date if valid, 401/403 otherwise
+ */
+ @GET
+ @Securable()
+ @Path("/check")
+ @Produces({ MediaType.APPLICATION_JSON })
+ @EnsureTransaction
+ public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
+ if (token == null) token = token2;
+ if (token == null) {
+ return Response.status(Status.FORBIDDEN).build();
+ }
+ boolean valid = tokenHelper.isTokenValid(token);
+ if (!valid) {
+ return Response.status(Status.UNAUTHORIZED).build();
+ }
- /**
- * Check if current token is valid
- *
- * @param user
- * @param password
- * @param request
- * @return
- */
- @GET
- @Securable()
- @Path("/check")
- @Produces({ MediaType.APPLICATION_JSON })
- @EnsureTransaction
- public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
- if (token == null) {
- token = token2;
- }
- if (token == null) {
- return Response.status(Status.FORBIDDEN).build();
- }
- boolean valid = tokenHelper.isTokenValid(token);
- if (!valid) {
- return Response.status(Status.UNAUTHORIZED).build();
- }
+ String user = tokenHelper.extractUserFromToken(token);
+ Date date = tokenHelper.extractDateCreationFromToken(token);
+ return Response.ok(Utils.createMap("valid", true, "user", user, "date", date)).build();
+ }
- String user = tokenHelper.extractUserFromToken(token);
- Date date = tokenHelper.extractDateCreationFromToken(token);
-
- return Response.ok(Utils.createMap("valid", true, "user", user, "date", date)).build();
- }
-
- @GET
- @POST
- @Path("/logout")
- @Produces({ MediaType.APPLICATION_JSON })
- public Response logout(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
- if (token == null) {
- Response.status(Status.BAD_REQUEST).build();
- }
- String user = tokenHelper.extractUserFromToken(token);
- LOG.info("User {} has logged out", user);
- return Response.ok().build();
- }
+ /**
+ * logout<p>
+ * Logs logout event. (Token invalidation is handled elsewhere.)
+ *
+ * @param token
+ * @return response
+ */
+ @GET
+ @POST
+ @Path("/logout")
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Response logout(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+ if (token == null) {
+ Response.status(Status.BAD_REQUEST).build();
+ }
+ String user = tokenHelper.extractUserFromToken(token);
+ LOG.info("User {} has logged out", user);
+ return Response.ok().build();
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/services/LicenseResource.java b/securis/src/main/java/net/curisit/securis/services/LicenseResource.java
index ed337ce..0885630 100644
--- a/securis/src/main/java/net/curisit/securis/services/LicenseResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/LicenseResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services;
import java.io.File;
@@ -63,34 +66,45 @@
import net.curisit.securis.utils.LicUtils;
/**
- * License resource, this service will provide methods to create, modify and
- * delete licenses
- *
- * @author roberto <roberto.sanchez@curisit.net>
+ * LicenseResource
+ * <p>
+ * REST resource in charge of managing licenses: list, fetch, create, activate,
+ * email delivery, cancel, block/unblock, modify and delete. It relies on
+ * {@link BasicSecurityContext} to scope access (organizations/apps) and
+ * on {@link EnsureTransaction} for mutating endpoints that need a TX.
+ * <p>
+ * Key rules:
+ * <ul>
+ * <li>Non-admin users must belong to the license's organization.</li>
+ * <li>License creation validates code CRC, activation code and email.</li>
+ * <li>Request payload must match Pack constraints (org/type/pack codes).</li>
+ * <li>History is recorded for key actions (CREATE/ACTIVATE/DOWNLOAD/etc.).</li>
+ * </ul>
+ *
+ * @author roberto
+ * Last reviewed by JRA on Oct 5, 2025.
*/
@Path("/license")
public class LicenseResource {
private static final Logger LOG = LogManager.getLogger(LicenseResource.class);
- @Inject
- private EmailManager emailManager;
+ @Inject private EmailManager emailManager;
+ @Inject private UserHelper userHelper;
+ @Inject private LicenseHelper licenseHelper;
+ @Inject private LicenseGenerator licenseGenerator;
- @Inject
- private UserHelper userHelper;
-
- @Inject
- private LicenseHelper licenseHelper;
-
- @Context
- EntityManager em;
-
- @Inject
- private LicenseGenerator licenseGenerator;
+ @Context EntityManager em;
/**
- *
- * @return the server version in format majorVersion.minorVersion
+ * index
+ * <p>
+ * List all licenses for a given pack. If the caller is not admin,
+ * verifies the pack belongs to an accessible organization.
+ *
+ * @param packId Pack identifier to filter licenses (required).
+ * @param bsc Security context to evaluate roles and scoping.
+ * @return 200 OK with a list (possibly empty), or 401 if unauthorized.
*/
@GET
@Path("/")
@@ -98,31 +112,33 @@
@Produces({ MediaType.APPLICATION_JSON })
public Response index(@QueryParam("packId") Integer packId, @Context BasicSecurityContext bsc) {
LOG.info("Getting licenses list ");
-
- // EntityManager em = emProvider.get();
em.clear();
if (!bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
Pack pack = em.find(Pack.class, packId);
- if (pack == null) {
- return Response.ok().build();
- }
+ if (pack == null) return Response.ok().build();
if (!bsc.getOrganizationsIds().contains(pack.getOrganization().getId())) {
LOG.error("Pack with id {} not accesible by user {}", pack, bsc.getUserPrincipal());
- return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack licenses").build();
+ return Response.status(Status.UNAUTHORIZED)
+ .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack licenses")
+ .build();
}
}
TypedQuery<License> q = em.createNamedQuery("list-licenses-by-pack", License.class);
q.setParameter("packId", packId);
List<License> list = q.getResultList();
-
return Response.ok(list).build();
}
/**
- *
- * @return the server version in format majorVersion.minorVersion
- * @throws SeCurisServiceException
+ * get
+ * <p>
+ * Fetch a single license by id, enforcing access scope for non-admin users.
+ *
+ * @param licId License id (required).
+ * @param bsc Security context.
+ * @return 200 OK with the license.
+ * @throws SeCurisServiceException 404 if not found, 401 if out of scope.
*/
@GET
@Path("/{licId}")
@@ -130,17 +146,22 @@
@Produces({ MediaType.APPLICATION_JSON })
public Response get(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
LOG.info("Getting organization data for id: {}: ", licId);
-
- // EntityManager em = emProvider.get();
em.clear();
License lic = getCurrentLicense(licId, bsc, em);
return Response.ok(lic).build();
}
/**
- *
- * @return The license file, only of license is active
- * @throws SeCurisServiceException
+ * download
+ * <p>
+ * Download the license file. Only allowed when the license is ACTIVE
+ * and license data exists. Adds a DOWNLOAD entry in history.
+ *
+ * @param licId License id.
+ * @param bsc Security context.
+ * @return 200 OK with the binary as application/octet-stream and a
+ * Content-Disposition header; otherwise specific error codes.
+ * @throws SeCurisServiceException if state or data is invalid.
*/
@GET
@Path("/{licId}/download")
@@ -149,7 +170,6 @@
@EnsureTransaction
public Response download(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
- // EntityManager em = emProvider.get();
License lic = getCurrentLicense(licId, bsc, em);
if (lic.getLicenseData() == null) {
@@ -166,12 +186,16 @@
}
/**
- * Activate the given license
- *
- * @param licId
- * @param bsc
- * @return
- * @throws SeCurisServiceException
+ * activate
+ * <p>
+ * Set license to ACTIVE provided status transition is valid, pack has
+ * available units and request data passes validation/uniqueness.
+ * Adds an ACTIVATE entry in history.
+ *
+ * @param licId License id.
+ * @param bsc Security context.
+ * @return 200 OK with updated license.
+ * @throws SeCurisServiceException if invalid transition, no availability or invalid request data.
*/
@PUT
@POST
@@ -182,7 +206,6 @@
@Produces({ MediaType.APPLICATION_JSON })
public Response activate(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
- // EntityManager em = emProvider.get();
License lic = getCurrentLicense(licId, bsc, em);
if (!License.Status.isActionValid(License.Action.ACTIVATION, lic.getStatus())) {
@@ -211,12 +234,18 @@
}
/**
- * Send license file by email to the organization
- *
- * @param licId
- * @param bsc
- * @return
- * @throws SeCurisServiceException
+ * send
+ * <p>
+ * Email the license file to the license owner. Builds a temporary file
+ * using the application license filename and cleans it afterwards.
+ * Adds a SEND entry in history.
+ *
+ * @param licId License id.
+ * @param addCC whether to CC the current operator.
+ * @param bsc Security context.
+ * @return 200 OK with the license (no state change).
+ * @throws SeCurisServiceException when no license file exists or user full name is missing.
+ * @throws SeCurisException if JSON/signature process fails.
*/
@SuppressWarnings("deprecation")
@PUT
@@ -229,7 +258,6 @@
public Response send(@PathParam("licId") Integer licId, @DefaultValue("false") @FormParam("add_cc") Boolean addCC, @Context BasicSecurityContext bsc)
throws SeCurisServiceException, SeCurisException {
- // EntityManager em = emProvider.get();
License lic = getCurrentLicense(licId, bsc, em);
Application app = lic.getPack().getLicenseType().getApplication();
File licFile = null;
@@ -259,19 +287,21 @@
}
}
- // lic.setModificationTimestamp(new Date());
- // em.merge(lic);
em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.SEND, "Email sent to: " + lic.getEmail()));
return Response.ok(lic).build();
}
/**
- * Cancel given license
- *
- * @param licId
- * @param bsc
- * @return
- * @throws SeCurisServiceException
+ * cancel
+ * <p>
+ * Cancel a license (requires valid state transition and a non-null reason).
+ * Delegates to {@link LicenseHelper#cancelLicense}.
+ *
+ * @param licId License id.
+ * @param actionData DTO carrying the cancellation reason.
+ * @param bsc Security context.
+ * @return 200 OK with updated license.
+ * @throws SeCurisServiceException when state is invalid or reason is missing.
*/
@PUT
@POST
@@ -282,7 +312,6 @@
@Produces({ MediaType.APPLICATION_JSON })
public Response cancel(@PathParam("licId") Integer licId, CancellationLicenseActionBean actionData, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
- // EntityManager em = emProvider.get();
License lic = getCurrentLicense(licId, bsc, em);
if (!License.Status.isActionValid(License.Action.CANCEL, lic.getStatus())) {
@@ -300,22 +329,24 @@
}
/**
- * Check if there is some pack with the same code
- *
- * @param code
- * Pack code
- * @param em
- * DB session object
- * @return <code>true</code> if code is already used, <code>false</code>
- * otherwise
+ * create
+ * <p>
+ * Create a license. Validates:
+ * <ul>
+ * <li>Unique license code and valid CRC.</li>
+ * <li>Activation code presence and uniqueness.</li>
+ * <li>Valid user email.</li>
+ * <li>Pack existence, ACTIVE status and scope authorization.</li>
+ * <li>Request data consistency and unblock status (if provided).</li>
+ * </ul>
+ * If request data is provided and the Pack has availability, the license is
+ * generated and set to ACTIVE immediately.
+ *
+ * @param lic License payload.
+ * @param bsc Security context.
+ * @return 200 OK with created license.
+ * @throws SeCurisServiceException on validation failures.
*/
- private boolean checkIfCodeExists(String code, EntityManager em) {
- TypedQuery<License> query = em.createNamedQuery("license-by-code", License.class);
- query.setParameter("code", code);
- int lics = query.getResultList().size();
- return lics > 0;
- }
-
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@@ -323,8 +354,6 @@
@Produces({ MediaType.APPLICATION_JSON })
@EnsureTransaction
public Response create(License lic, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
- // EntityManager em = emProvider.get();
-
if (checkIfCodeExists(lic.getCode(), em)) {
throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The license code is already used in an existing license");
}
@@ -370,10 +399,8 @@
}
if (pack.getNumAvailables() > 0) {
-
SignedLicenseBean signedLicense = generateLicense(lic, em);
- // If user provide a request data the license status is passed
- // directly to ACTIVE
+ // Move directly to ACTIVE when request data is provided
lic.setStatus(LicenseStatus.ACTIVE);
try {
lic.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
@@ -404,6 +431,203 @@
return Response.ok(lic).build();
}
+ /**
+ * modify
+ * <p>
+ * Update license basic fields (comments, fullName, email) and, when
+ * status is CREATED and request payload changes, re-normalize/validate and
+ * regenerate the signed license data. Adds a MODIFY history entry.
+ *
+ * @param lic New values.
+ * @param licId License id.
+ * @param bsc Security context.
+ * @return 200 OK with updated license.
+ * @throws SeCurisServiceException if validation fails.
+ */
+ @SuppressWarnings("deprecation")
+ @PUT
+ @POST
+ @Path("/{licId}")
+ @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+ @EnsureTransaction
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Response modify(License lic, @PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+ LOG.info("Modifying license with id: {}", licId);
+
+ License currentLicense = getCurrentLicense(licId, bsc, em);
+ currentLicense.setComments(lic.getComments());
+ currentLicense.setFullName(lic.getFullName());
+ currentLicense.setEmail(lic.getEmail());
+ if (currentLicense.getActivationCode() == null) {
+ currentLicense.setActivationCode(lic.getActivationCode());
+ }
+
+ if (currentLicense.getStatus() == LicenseStatus.CREATED && !ObjectUtils.equals(currentLicense.getReqDataHash(), lic.getReqDataHash())) {
+ if (lic.getRequestData() != null) {
+ SignedLicenseBean signedLicense = generateLicense(lic, em);
+ try {
+ // Normalize the request JSON and update signed license JSON
+ currentLicense.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
+ LOG.info("JSON generated for request: \n{}", currentLicense.getRequestData());
+ if (BlockedRequest.isRequestBlocked(currentLicense.getRequestData(), em)) {
+ throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be used again");
+ }
+ currentLicense.setLicenseData(JsonUtils.toJSON(signedLicense));
+ } catch (SeCurisException e) {
+ LOG.error("Error generaing license JSON", e);
+ throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generaing license JSON");
+ }
+ } else {
+ currentLicense.setRequestData(null);
+ }
+ }
+
+ currentLicense.setModificationTimestamp(new Date());
+ em.persist(currentLicense);
+ em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.MODIFY));
+
+ return Response.ok(currentLicense).build();
+ }
+
+ /**
+ * delete
+ * <p>
+ * Delete the license when the current status allows it. If the license
+ * was BLOCKED, removes the BlockedRequest entry to unblock the request.
+ *
+ * @param licId License id.
+ * @param bsc Security context.
+ * @return 200 OK with a success payload.
+ * @throws SeCurisServiceException if status does not allow deletion.
+ */
+ @DELETE
+ @Path("/{licId}")
+ @EnsureTransaction
+ @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Response delete(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+ LOG.info("Deleting license with id: {}", licId);
+ License lic = getCurrentLicense(licId, bsc, em);
+
+ if (!License.Status.isActionValid(License.Action.DELETE, lic.getStatus())) {
+ LOG.error("License {} can not be deleted with status {}", lic.getCode(), lic.getStatus());
+ throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can not be deleted in current status: " + lic.getStatus().name());
+ }
+ if (lic.getStatus() == LicenseStatus.BLOCKED) {
+ BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash());
+ if (blockedReq != null) {
+ em.remove(blockedReq);
+ }
+ }
+
+ em.remove(lic);
+ return Response.ok(Utils.createMap("success", true, "id", licId)).build();
+ }
+
+ /**
+ * block
+ * <p>
+ * Block the license request data (allowed only from CANCELLED state).
+ * Persists a {@link BlockedRequest} and transitions the license to BLOCKED.
+ *
+ * @param licId License id.
+ * @param bsc Security context.
+ * @return 200 OK with a success payload.
+ * @throws SeCurisServiceException if state is not CANCELLED or already blocked.
+ */
+ @POST
+ @Path("/{licId}/block")
+ @EnsureTransaction
+ @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Response block(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+ LOG.info("Blocking license with id: {}", licId);
+ License lic = getCurrentLicense(licId, bsc, em);
+
+ if (!License.Status.isActionValid(License.Action.BLOCK, lic.getStatus())) {
+ LOG.error("License can only be blocked in CANCELLED status, current: {}", lic.getStatus().name());
+ throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can only be blocked in CANCELLED status");
+ }
+ if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
+ throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is already blocked");
+ }
+ BlockedRequest blockedReq = new BlockedRequest();
+ blockedReq.setCreationTimestamp(new Date());
+ blockedReq.setBlockedBy(userHelper.getUser(bsc, em));
+ blockedReq.setRequestData(lic.getRequestData());
+
+ em.persist(blockedReq);
+ lic.setStatus(LicenseStatus.BLOCKED);
+ lic.setModificationTimestamp(new Date());
+ em.merge(lic);
+
+ em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.BLOCK));
+ return Response.ok(Utils.createMap("success", true, "id", licId)).build();
+ }
+
+ /**
+ * unblock
+ * <p>
+ * Remove the block for the license request data (if present) and move
+ * license back to CANCELLED. Adds an UNBLOCK history entry.
+ *
+ * @param licId License id.
+ * @param bsc Security context.
+ * @return 200 OK with a success payload.
+ * @throws SeCurisServiceException never if not blocked (returns success anyway).
+ */
+ @POST
+ @Path("/{licId}/unblock")
+ @EnsureTransaction
+ @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Response unblock(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+ LOG.info("Unblocking license with id: {}", licId);
+ License lic = getCurrentLicense(licId, bsc, em);
+
+ if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
+ BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash());
+ em.remove(blockedReq);
+
+ lic.setStatus(LicenseStatus.CANCELLED);
+ lic.setModificationTimestamp(new Date());
+ em.merge(lic);
+ em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.UNBLOCK));
+ } else {
+ LOG.info("Request data for license {} is NOT blocked", licId);
+ }
+
+ return Response.ok(Utils.createMap("success", true, "id", licId)).build();
+ }
+
+ // ---------------------------------------------------------------------
+ // Helpers
+ // ---------------------------------------------------------------------
+
+ /**
+ * checkIfCodeExists<p>
+ * Check if there is an existing license with the same code.
+ *
+ * @param code
+ * @param entityManager
+ */
+ private boolean checkIfCodeExists(String code, EntityManager em) {
+ TypedQuery<License> query = em.createNamedQuery("license-by-code", License.class);
+ query.setParameter("code", code);
+ int lics = query.getResultList().size();
+ return lics > 0;
+ }
+
+ /**
+ * generateLicense<p>
+ * Generate a signed license from request data and pack metadata/expiration.
+ *
+ * @param license License with requestData and packId populated.
+ * @param em Entity manager.
+ * @return Signed license bean.
+ * @throws SeCurisServiceException if validation/generation fails.
+ */
private SignedLicenseBean generateLicense(License license, EntityManager em) throws SeCurisServiceException {
SignedLicenseBean sl = null;
Pack pack = em.find(Pack.class, license.getPackId());
@@ -419,12 +643,14 @@
}
/**
- * We check if the given request data is valid for the current Pack and has
- * a valid format
- *
- * @param pack
- * @param requestData
- * @throws SeCurisServiceException
+ * validateRequestData<p>
+ * Validate that requestData matches the Pack and is well-formed.
+ *
+ * @param pack Target pack (org/type constraints).
+ * @param requestData Raw JSON string with the license request.
+ * @param activationCode Activation code from the license payload.
+ * @return Parsed {@link RequestBean}.
+ * @throws SeCurisServiceException on format mismatch or wrong codes.
*/
private RequestBean validateRequestData(Pack pack, String requestData, String activationCode) throws SeCurisServiceException {
if (requestData == null) {
@@ -456,143 +682,16 @@
return rb;
}
- @SuppressWarnings("deprecation")
- @PUT
- @POST
- @Path("/{licId}")
- @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
- @EnsureTransaction
- @Consumes(MediaType.APPLICATION_JSON)
- @Produces({ MediaType.APPLICATION_JSON })
- public Response modify(License lic, @PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
- LOG.info("Modifying license with id: {}", licId);
-
- // EntityManager em = emProvider.get();
-
- License currentLicense = getCurrentLicense(licId, bsc, em);
- currentLicense.setComments(lic.getComments());
- currentLicense.setFullName(lic.getFullName());
- currentLicense.setEmail(lic.getEmail());
- if (currentLicense.getActivationCode() == null) {
- currentLicense.setActivationCode(lic.getActivationCode());
- }
-
- if (currentLicense.getStatus() == LicenseStatus.CREATED && !ObjectUtils.equals(currentLicense.getReqDataHash(), lic.getReqDataHash())) {
- if (lic.getRequestData() != null) {
- SignedLicenseBean signedLicense = generateLicense(lic, em);
- try {
- // Next 2 lines are necessary to normalize the String that
- // contains
- // the request.
- currentLicense.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class));
- LOG.info("JSON generated for request: \n{}", currentLicense.getRequestData());
- if (BlockedRequest.isRequestBlocked(currentLicense.getRequestData(), em)) {
- throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be used again");
- }
- currentLicense.setLicenseData(JsonUtils.toJSON(signedLicense));
- } catch (SeCurisException e) {
- LOG.error("Error generaing license JSON", e);
- throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generaing license JSON");
- }
- } else {
- // This set method could pass a null value
- currentLicense.setRequestData(null);
- }
- }
-
- currentLicense.setModificationTimestamp(new Date());
- em.persist(currentLicense);
- em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.MODIFY));
-
- return Response.ok(currentLicense).build();
- }
-
- @DELETE
- @Path("/{licId}")
- @EnsureTransaction
- @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
- @Produces({ MediaType.APPLICATION_JSON })
- public Response delete(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
- LOG.info("Deleting license with id: {}", licId);
- // EntityManager em = emProvider.get();
- License lic = getCurrentLicense(licId, bsc, em);
-
- if (!License.Status.isActionValid(License.Action.DELETE, lic.getStatus())) {
- LOG.error("License {} can not be deleted with status {}", lic.getCode(), lic.getStatus());
- throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can not be deleted in current status: " + lic.getStatus().name());
- }
- if (lic.getStatus() == LicenseStatus.BLOCKED) {
- // If license is removed and it's blocked then the blocked request
- // should be removed, that is,
- // the license deletion will unblock the request data
- BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash());
- if (blockedReq != null) {
- // This if is to avoid some race condition or if the request has
- // been already removed manually
- em.remove(blockedReq);
- }
- }
-
- em.remove(lic);
- return Response.ok(Utils.createMap("success", true, "id", licId)).build();
- }
-
- @POST
- @Path("/{licId}/block")
- @EnsureTransaction
- @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
- @Produces({ MediaType.APPLICATION_JSON })
- public Response block(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
- LOG.info("Blocking license with id: {}", licId);
- // EntityManager em = emProvider.get();
- License lic = getCurrentLicense(licId, bsc, em);
-
- if (!License.Status.isActionValid(License.Action.BLOCK, lic.getStatus())) {
- LOG.error("License can only be blocked in CANCELLED status, current: {}", lic.getStatus().name());
- throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can only be blocked in CANCELLED status");
- }
- if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
- throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is already blocked");
- }
- BlockedRequest blockedReq = new BlockedRequest();
- blockedReq.setCreationTimestamp(new Date());
- blockedReq.setBlockedBy(userHelper.getUser(bsc, em));
- blockedReq.setRequestData(lic.getRequestData());
-
- em.persist(blockedReq);
- lic.setStatus(LicenseStatus.BLOCKED);
- lic.setModificationTimestamp(new Date());
- em.merge(lic);
-
- em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.BLOCK));
- return Response.ok(Utils.createMap("success", true, "id", licId)).build();
- }
-
- @POST
- @Path("/{licId}/unblock")
- @EnsureTransaction
- @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
- @Produces({ MediaType.APPLICATION_JSON })
- public Response unblock(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
- LOG.info("Unblocking license with id: {}", licId);
- // EntityManager em = emProvider.get();
- License lic = getCurrentLicense(licId, bsc, em);
-
- if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) {
- BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash());
- em.remove(blockedReq);
-
- lic.setStatus(LicenseStatus.CANCELLED);
- lic.setModificationTimestamp(new Date());
- em.merge(lic);
- em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.UNBLOCK));
- } else {
- LOG.info("Request data for license {} is NOT blocked", licId);
- }
-
- return Response.ok(Utils.createMap("success", true, "id", licId)).build();
- }
-
+ /**
+ * getCurrentLicense<p>
+ * Load a license and verify scope for non-admin users.
+ *
+ * @param licId License id.
+ * @param bsc Security context.
+ * @param em Entity manager.
+ * @return License entity.
+ * @throws SeCurisServiceException if id is missing, not found or unauthorized.
+ */
private License getCurrentLicense(Integer licId, BasicSecurityContext bsc, EntityManager em) throws SeCurisServiceException {
if (licId == null || "".equals(Integer.toString(licId))) {
LOG.error("License ID is mandatory");
@@ -611,6 +710,13 @@
return lic;
}
+ // ---------------------------------------------------------------------
+ // DTOs
+ // ---------------------------------------------------------------------
+
+ /**
+ * DTO used to carry a cancellation reason for the cancel endpoint.
+ */
@JsonAutoDetect
@JsonIgnoreProperties(ignoreUnknown = true)
static class CancellationLicenseActionBean {
@@ -618,3 +724,4 @@
private String reason;
}
}
+
diff --git a/securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java b/securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java
index 28b5c7d..04c15a2 100644
--- a/securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/LicenseTypeResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services;
import java.util.Date;
@@ -44,31 +47,34 @@
import net.curisit.securis.utils.TokenHelper;
/**
- * LicenseType resource, this service will provide methods to create, modify and
- * delete license types
- *
- * @author roberto <roberto.sanchez@curisit.net>
+ * LicenseTypeResource
+ * <p>
+ * CRUD for license types. Non-admin queries are scoped to the applications
+ * accessible by the caller. Metadata changes are reconciled and, when keys
+ * change, can be propagated to dependent entities via {@link MetadataHelper}.
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
*/
@Path("/licensetype")
public class LicenseTypeResource {
private static final Logger LOG = LogManager.getLogger(LicenseTypeResource.class);
- @Inject
- TokenHelper tokenHelper;
+ @Inject TokenHelper tokenHelper;
+ @Inject MetadataHelper metadataHelper;
- @Inject
- MetadataHelper metadataHelper;
+ @Context EntityManager em;
- @Context
- EntityManager em;
-
- public LicenseTypeResource() {
- }
+ public LicenseTypeResource() { }
/**
- *
- * @return the server version in format majorVersion.minorVersion
+ * index
+ * <p>
+ * List license types. Non-admin users get only types for their allowed apps.
+ *
+ * @param bsc security context.
+ * @return 200 OK with list (possibly empty).
*/
@GET
@Path("/")
@@ -76,8 +82,6 @@
@Securable
public Response index(@Context BasicSecurityContext bsc) {
LOG.info("Getting license types list ");
-
- // EntityManager em = emProvider.get();
em.clear();
TypedQuery<LicenseType> q;
if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
@@ -87,18 +91,21 @@
return Response.ok().build();
}
q = em.createNamedQuery("list-license_types-by_apps-id", LicenseType.class);
-
q.setParameter("list_ids", bsc.getApplicationsIds());
}
List<LicenseType> list = q.getResultList();
-
return Response.ok(list).build();
}
/**
- *
- * @return the server version in format majorVersion.minorVersion
- * @throws SeCurisServiceException
+ * get
+ * <p>
+ * Fetch a license type by id.
+ *
+ * @param ltid LicenseType id (string form).
+ * @param token (unused) header token.
+ * @return 200 OK with the entity.
+ * @throws SeCurisServiceException 404 if not found.
*/
@GET
@Path("/{ltid}")
@@ -111,7 +118,6 @@
return Response.status(Status.NOT_FOUND).build();
}
- // EntityManager em = emProvider.get();
em.clear();
LicenseType lt = em.find(LicenseType.class, Integer.parseInt(ltid));
if (lt == null) {
@@ -121,6 +127,16 @@
return Response.ok(lt).build();
}
+ /**
+ * create
+ * <p>
+ * Create a new license type. Requires ADMIN. Sets application reference,
+ * persists metadata entries and stamps creation time.
+ *
+ * @param lt Payload.
+ * @param token (unused) token header.
+ * @return 200 OK with created entity, or 404 if app missing.
+ */
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@@ -130,7 +146,6 @@
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
public Response create(LicenseType lt, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
LOG.info("Creating new license type");
- // EntityManager em = emProvider.get();
try {
setApplication(lt, lt.getApplicationId(), em);
@@ -158,16 +173,18 @@
return Response.ok(lt).build();
}
- private Set<String> getMdKeys(Set<LicenseTypeMetadata> mds) {
- Set<String> ids = new HashSet<String>();
- if (mds != null) {
- for (LicenseTypeMetadata md : mds) {
- ids.add(md.getKey());
- }
- }
- return ids;
- }
-
+ /**
+ * modify
+ * <p>
+ * Update an existing license type. Reconciles metadata:
+ * removes keys not present in the new set; merges existing; persists new ones.
+ * If keys changed, {@link MetadataHelper#propagateMetadata} is invoked.
+ *
+ * @param lt New values.
+ * @param ltid LicenseType id.
+ * @param token (unused) token.
+ * @return 200 OK with updated entity; 404 if not found or app missing.
+ */
@PUT
@POST
@Path("/{ltid}")
@@ -178,7 +195,6 @@
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
public Response modify(LicenseType lt, @PathParam("ltid") String ltid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
LOG.info("Modifying license type with id: {}", ltid);
- // EntityManager em = emProvider.get();
LicenseType currentlt = em.find(LicenseType.class, Integer.parseInt(ltid));
if (currentlt == null) {
LOG.error("LicenseType with id {} not found in DB", ltid);
@@ -230,28 +246,23 @@
return Response.ok(currentlt).build();
}
- private void setApplication(LicenseType licType, Integer applicationId, EntityManager em) throws SeCurisException {
- Application app = null;
- if (applicationId != null) {
- app = em.find(Application.class, applicationId);
- if (app == null) {
- LOG.error("LicenseType application with id {} not found in DB", applicationId);
-
- throw new SecurityException("License type's app not found with ID: " + applicationId);
- }
- }
- licType.setApplication(app);
- }
-
+ /**
+ * delete
+ * <p>
+ * Delete a license type by id. Requires ADMIN.
+ *
+ * @param ltid LicenseType id.
+ * @param req request (unused).
+ * @return 200 OK on success; 404 if not found.
+ */
@DELETE
@Path("/{ltid}")
@EnsureTransaction
@Produces({ MediaType.APPLICATION_JSON })
@Securable(roles = Rol.ADMIN)
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response delete(@PathParam("ltid") String ltid, @Context HttpServletRequest request) {
+ public Response delete(@PathParam("ltid") String ltid, @Context HttpServletRequest req) {
LOG.info("Deleting app with id: {}", ltid);
- // EntityManager em = emProvider.get();
LicenseType app = em.find(LicenseType.class, Integer.parseInt(ltid));
if (app == null) {
LOG.error("LicenseType with id {} can not be deleted, It was not found in DB", ltid);
@@ -262,4 +273,39 @@
return Response.ok(Utils.createMap("success", true, "id", ltid)).build();
}
+ // ---------------------------------------------------------------------
+ // Helpers
+ // ---------------------------------------------------------------------
+
+ private Set<String> getMdKeys(Set<LicenseTypeMetadata> mds) {
+ Set<String> ids = new HashSet<String>();
+ if (mds != null) {
+ for (LicenseTypeMetadata md : mds) {
+ ids.add(md.getKey());
+ }
+ }
+ return ids;
+ }
+
+ /**
+ * setApplication<p>
+ * Resolve and set the application of a license type.
+ *
+ * @param licType target LicenseType.
+ * @param applicationId id of the application (nullable).
+ * @param em entity manager.
+ * @throws SeCurisException if id provided but not found.
+ */
+ private void setApplication(LicenseType licType, Integer applicationId, EntityManager em) throws SeCurisException {
+ Application app = null;
+ if (applicationId != null) {
+ app = em.find(Application.class, applicationId);
+ if (app == null) {
+ LOG.error("LicenseType application with id {} not found in DB", applicationId);
+ throw new SecurityException("License type's app not found with ID: " + applicationId);
+ }
+ }
+ licType.setApplication(app);
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/services/OrganizationResource.java b/securis/src/main/java/net/curisit/securis/services/OrganizationResource.java
index ee23619..30d5940 100644
--- a/securis/src/main/java/net/curisit/securis/services/OrganizationResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/OrganizationResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services;
import java.util.Date;
@@ -39,10 +42,12 @@
import net.curisit.securis.utils.TokenHelper;
/**
- * Organization resource, this service will provide methods to create, modify
- * and delete organizations
- *
- * @author roberto <roberto.sanchez@curisit.net>
+ * OrganizationResource
+ * <p>
+ * CRUD and listing of organizations. Non-admin users are scoped by their
+ * accessible organization ids when listing.
+ *
+ * Last reviewed by JRA on Oct 5, 2025.
*/
@Path("/organization")
@RequestScoped
@@ -50,18 +55,18 @@
private static final Logger LOG = LogManager.getLogger(OrganizationResource.class);
- @Context
- EntityManager em;
+ @Context EntityManager em;
+ @Context BasicSecurityContext bsc;
- @Context
- BasicSecurityContext bsc;
-
- public OrganizationResource() {
- }
+ public OrganizationResource() { }
/**
- *
- * @return the server version in format majorVersion.minorVersion
+ * index
+ * <p>
+ * List organizations. For admins returns all; for non-admins filters
+ * by the ids in {@link BasicSecurityContext#getOrganizationsIds()}.
+ *
+ * @return 200 OK with the list.
*/
@GET
@Path("/")
@@ -69,8 +74,6 @@
@Securable
public Response index() {
LOG.info("Getting organizations list ");
-
- // EntityManager em = emProvider.get();
em.clear();
TypedQuery<Organization> q;
if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
@@ -84,15 +87,18 @@
q.setParameter("list_ids", bsc.getOrganizationsIds());
}
}
-
List<Organization> list = q.getResultList();
-
return Response.ok(list).build();
}
/**
- *
- * @return the server version in format majorVersion.minorVersion
+ * get
+ * <p>
+ * Fetch an organization by id.
+ *
+ * @param orgid organization id (string form).
+ * @param token header token (unused).
+ * @return 200 OK with entity or 404 if not found.
*/
@GET
@Path("/{orgid}")
@@ -104,8 +110,6 @@
LOG.error("Organization ID is mandatory");
return Response.status(Status.NOT_FOUND).build();
}
-
- // EntityManager em = emProvider.get();
em.clear();
Organization org = em.find(Organization.class, Integer.parseInt(orgid));
if (org == null) {
@@ -115,16 +119,15 @@
return Response.ok(org).build();
}
- private boolean isCyclicalRelationship(int currentId, Organization parent) {
- while (parent != null) {
- if (parent.getId() == currentId) {
- return true;
- }
- parent = parent.getParentOrganization();
- }
- return false;
- }
-
+ /**
+ * create
+ * <p>
+ * Create a new organization, setting optional parent and user members.
+ * Requires ADMIN.
+ *
+ * @param org payload with code/name/etc., optional parentOrgId and usersIds.
+ * @return 200 OK with created organization or 404 when parent/user not found.
+ */
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@@ -134,7 +137,6 @@
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
public Response create(Organization org) {
LOG.info("Creating new organization");
- // EntityManager em = emProvider.get();
try {
this.setParentOrg(org, org.getParentOrgId(), em);
@@ -162,36 +164,17 @@
return Response.ok(org).build();
}
- private void setParentOrg(Organization org, Integer parentOrgId, EntityManager em) throws SeCurisException {
- Organization parentOrg = null;
- if (parentOrgId != null) {
- parentOrg = em.find(Organization.class, parentOrgId);
- if (parentOrg == null) {
- LOG.error("Organization parent with id {} not found in DB", org.getParentOrgId());
- throw new SecurityException("Organization's parent not found with ID: " + org.getParentOrgId());
- }
- }
-
- org.setParentOrganization(parentOrg);
- }
-
- private void setOrgUsers(Organization org, Set<String> usersIds, EntityManager em) throws SeCurisException {
- Set<User> users = null;
- if (usersIds != null && !usersIds.isEmpty()) {
- users = new HashSet<>();
- for (String username : usersIds) {
- User user = em.find(User.class, username);
- if (user == null) {
- LOG.error("Organization user with id '{}' not found in DB", username);
- throw new SecurityException("Organization's user not found with ID: " + username);
- }
- users.add(user);
- }
- }
-
- org.setUsers(users);
- }
-
+ /**
+ * modify
+ * <p>
+ * Update an organization. Validates no cyclic parent relationship,
+ * updates parent and user set. Requires ADMIN.
+ *
+ * @param org new values (including optional parent/usersIds).
+ * @param orgid target id.
+ * @param token (unused) header token.
+ * @return 200 OK with updated organization, or specific error status.
+ */
@PUT
@POST
@Path("/{orgid}")
@@ -202,7 +185,6 @@
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
public Response modify(Organization org, @PathParam("orgid") String orgid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
LOG.info("Modifying organization with id: {}", orgid);
- // EntityManager em = emProvider.get();
Organization currentOrg = em.find(Organization.class, Integer.parseInt(orgid));
if (currentOrg == null) {
LOG.error("Organization with id {} not found in DB", orgid);
@@ -233,15 +215,23 @@
return Response.ok(currentOrg).build();
}
+ /**
+ * delete
+ * <p>
+ * Delete an organization if it has no children. Requires ADMIN.
+ *
+ * @param orgid target id.
+ * @param req request (unused).
+ * @return 200 OK with success map, or 404/403 on constraints.
+ */
@DELETE
@Path("/{orgid}")
@EnsureTransaction
@Produces({ MediaType.APPLICATION_JSON })
@Securable(roles = Rol.ADMIN)
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response delete(@PathParam("orgid") String orgid, @Context HttpServletRequest request) {
+ public Response delete(@PathParam("orgid") String orgid, @Context HttpServletRequest req) {
LOG.info("Deleting organization with id: {}", orgid);
- // EntityManager em = emProvider.get();
Organization org = em.find(Organization.class, Integer.parseInt(orgid));
if (org == null) {
LOG.error("Organization with id {} can not be deleted, It was not found in DB", orgid);
@@ -256,4 +246,70 @@
return Response.ok(Utils.createMap("success", true, "id", orgid)).build();
}
+ // ---------------------------------------------------------------------
+ // Helpers
+ // ---------------------------------------------------------------------
+
+ /**
+ * isCyclicalRelationship<p>
+ * Detects cycles by walking up the parent chain.
+ *
+ * @param currentId
+ * @param parent
+ * @return isCyclicalRelationship
+ */
+ private boolean isCyclicalRelationship(int currentId, Organization parent) {
+ while (parent != null) {
+ if (parent.getId() == currentId) return true;
+ parent = parent.getParentOrganization();
+ }
+ return false;
+ }
+
+ /**
+ * setParentOrg<p>
+ * Resolve and set parent organization (nullable).
+ *
+ * @param org
+ * @param parentOrgId
+ * @param entitymanager
+ * @throws SeCurisException
+ */
+ private void setParentOrg(Organization org, Integer parentOrgId, EntityManager em) throws SeCurisException {
+ Organization parentOrg = null;
+ if (parentOrgId != null) {
+ parentOrg = em.find(Organization.class, parentOrgId);
+ if (parentOrg == null) {
+ LOG.error("Organization parent with id {} not found in DB", org.getParentOrgId());
+ throw new SecurityException("Organization's parent not found with ID: " + org.getParentOrgId());
+ }
+ }
+ org.setParentOrganization(parentOrg);
+ }
+
+ /**
+ * setOrgUsers<p>
+ * Replace organization users from the provided usernames set.
+ *
+ * @param org
+ * @param usersIds
+ * @param entityManager
+ * @throws SeCurisException
+ */
+ private void setOrgUsers(Organization org, Set<String> usersIds, EntityManager em) throws SeCurisException {
+ Set<User> users = null;
+ if (usersIds != null && !usersIds.isEmpty()) {
+ users = new HashSet<>();
+ for (String username : usersIds) {
+ User user = em.find(User.class, username);
+ if (user == null) {
+ LOG.error("Organization user with id '{}' not found in DB", username);
+ throw new SecurityException("Organization's user not found with ID: " + username);
+ }
+ users.add(user);
+ }
+ }
+ org.setUsers(users);
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/services/PackResource.java b/securis/src/main/java/net/curisit/securis/services/PackResource.java
index 4623314..85b784f 100644
--- a/securis/src/main/java/net/curisit/securis/services/PackResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/PackResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services;
import java.security.Principal;
@@ -53,31 +56,35 @@
import net.curisit.securis.utils.TokenHelper;
/**
- * Pack resource, this service will provide methods to create, modify and delete
- * packs
- *
- * @author roberto <roberto.sanchez@curisit.net>
+ * PackResource
+ * <p>
+ * Manages Packs (group of licenses bound to an organization, application/type,
+ * and configuration/metadata). Provides list/filter, get, create, modify,
+ * state transitions (activate/hold/cancel) and deletion.
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
*/
@Path("/pack")
public class PackResource {
private static final Logger LOG = LogManager.getLogger(PackResource.class);
- @Inject
- TokenHelper tokenHelper;
+ @Inject TokenHelper tokenHelper;
+ @Inject MetadataHelper metadataHelper;
+ @Inject private LicenseHelper licenseHelper;
- @Inject
- MetadataHelper metadataHelper;
-
- @Context
- EntityManager em;
-
- @Inject
- private LicenseHelper licenseHelper;
+ @Context EntityManager em;
/**
- *
- * @return the server version in format majorVersion.minorVersion
+ * index
+ * <p>
+ * List packs with optional filters (organizationId, applicationId, licenseTypeId).
+ * For non-admins, results are scoped by both apps and orgs from {@link BasicSecurityContext}.
+ *
+ * @param uriInfo supplies query parameters.
+ * @param bsc security scope/roles.
+ * @return 200 OK with the list (possibly empty).
*/
@GET
@Path("/")
@@ -86,70 +93,25 @@
public Response index(@Context UriInfo uriInfo, @Context BasicSecurityContext bsc) {
LOG.info("Getting packs list ");
MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
-
- // EntityManager em = emProvider.get();
em.clear();
TypedQuery<Pack> q = createQuery(queryParams, bsc);
if (q == null) {
return Response.ok().build();
}
-
List<Pack> list = q.getResultList();
-
return Response.ok(list).build();
}
- private String generateWhereFromParams(boolean addWhere, MultivaluedMap<String, String> queryParams) {
- List<String> conditions = new ArrayList<>();
- if (queryParams.containsKey("organizationId")) {
- conditions.add(String.format("pa.organization.id = %s", queryParams.getFirst("organizationId")));
- }
- if (queryParams.containsKey("applicationId")) {
- conditions.add(String.format("pa.licenseType.application.id = %s", queryParams.getFirst("applicationId")));
- }
- if (queryParams.containsKey("licenseTypeId")) {
- conditions.add(String.format("pa.licenseType.id = %s", queryParams.getFirst("licenseTypeId")));
- }
- String connector = addWhere ? " where " : " and ";
- return (conditions.isEmpty() ? "" : connector) + String.join(" and ", conditions);
- }
-
- private TypedQuery<Pack> createQuery(MultivaluedMap<String, String> queryParams, BasicSecurityContext bsc) {
- TypedQuery<Pack> q;
- String hql = "SELECT pa FROM Pack pa";
- if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
- hql += generateWhereFromParams(true, queryParams);
- q = em.createQuery(hql, Pack.class);
- } else {
- if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
- return null;
- }
- if (bsc.getOrganizationsIds() == null || bsc.getOrganizationsIds().isEmpty()) {
- hql += " where pa.licenseType.application.id in :list_ids_app ";
- } else {
- hql += " where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app ";
- }
- hql += generateWhereFromParams(false, queryParams);
- q = em.createQuery(hql, Pack.class);
- if (hql.contains("list_ids_org")) {
- q.setParameter("list_ids_org", bsc.getOrganizationsIds());
- }
- q.setParameter("list_ids_app", bsc.getApplicationsIds());
- LOG.info("Getting packs from orgs: {} and apps: {}", bsc.getOrganizationsIds(), bsc.getApplicationsIds());
- }
-
- return q;
- }
-
- private Response generateErrorUnathorizedAccess(Pack pack, Principal user) {
- LOG.error("Pack with id {} not accesible by user {}", pack, user);
- return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack").build();
- }
-
/**
- *
- * @return the server version in format majorVersion.minorVersion
+ * get
+ * <p>
+ * Fetch a pack by id. If the caller is an ADVANCE user, validates
+ * the organization scope.
+ *
+ * @param packId pack id.
+ * @param bsc security context.
+ * @return 200 OK with entity, or 404/401 accordingly.
*/
@GET
@Path("/{packId}")
@@ -162,7 +124,6 @@
return Response.status(Status.NOT_FOUND).build();
}
- // EntityManager em = emProvider.get();
em.clear();
Pack pack = em.find(Pack.class, packId);
if (pack == null) {
@@ -175,6 +136,18 @@
return Response.ok(pack).build();
}
+ /**
+ * create
+ * <p>
+ * Create a new pack. Validates code uniqueness, sets organization and
+ * license type references, stamps creator and timestamps, and persists
+ * metadata entries.
+ *
+ * @param pack payload.
+ * @param bsc security context (for createdBy).
+ * @return 200 OK with created pack or 404 when references not found.
+ * @throws SeCurisServiceException on duplicated code.
+ */
@POST
@Path("/")
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@@ -184,7 +157,6 @@
@EnsureTransaction
public Response create(Pack pack, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
LOG.info("Creating new pack");
- // EntityManager em = emProvider.get();
if (checkIfCodeExists(pack.getCode(), em)) {
throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is already used in an existing pack");
@@ -221,67 +193,16 @@
}
/**
- * Check if there is some pack with the same code
- *
- * @param code
- * Pack code
- * @param em
- * DB session object
- * @return <code>true</code> if code is already used, <code>false</code>
- * otherwise
+ * modify
+ * <p>
+ * Update a pack basic fields and reconcile metadata (remove/merge/persist).
+ * If metadata keys changed, marks dependent licenses metadata as obsolete via
+ * {@link MetadataHelper#markObsoleteMetadata}.
+ *
+ * @param pack payload values.
+ * @param packId target id.
+ * @return 200 OK with updated pack or 404 on ref errors.
*/
- private boolean checkIfCodeExists(String code, EntityManager em) {
- TypedQuery<Pack> query = em.createNamedQuery("pack-by-code", Pack.class);
- query.setParameter("code", code);
- int packs = query.getResultList().size();
- return packs > 0;
- }
-
- /**
- *
- * @return The next available code suffix in pack for license code
- * @throws SeCurisServiceException
- */
- @GET
- @Path("/{packId}/next_license_code")
- @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
- @Produces({ MediaType.TEXT_PLAIN })
- public Response getCodeSuffix(@PathParam("packId") Integer packId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
- // EntityManager em = emProvider.get();
-
- if (packId == null) {
- throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is mandatory");
- }
- Integer codeSuffix = licenseHelper.getNextCodeSuffix(packId, em);
- Pack pack = em.find(Pack.class, packId);
- ;
-
- String licCode = LicUtils.getLicenseCode(pack.getCode(), codeSuffix);
- return Response.ok(licCode).build();
- }
-
- private void setPackLicenseType(Pack pack, Integer licTypeId, EntityManager em) throws SeCurisException {
- LicenseType lt = null;
- if (licTypeId != null) {
- lt = em.find(LicenseType.class, pack.getLicTypeId());
- if (lt == null) {
- LOG.error("Pack license type with id {} not found in DB", licTypeId);
- throw new SeCurisException("Pack license type not found with ID: " + licTypeId);
- }
- }
- pack.setLicenseType(lt);
- }
-
- private Set<String> getMdKeys(Set<PackMetadata> mds) {
- Set<String> ids = new HashSet<String>();
- if (mds != null) {
- for (PackMetadata md : mds) {
- ids.add(md.getKey());
- }
- }
- return ids;
- }
-
@PUT
@POST
@Path("/{packId}")
@@ -292,7 +213,6 @@
@Produces({ MediaType.APPLICATION_JSON })
public Response modify(Pack pack, @PathParam("packId") Integer packId) {
LOG.info("Modifying pack with id: {}", packId);
- // EntityManager em = emProvider.get();
Pack currentPack = em.find(Pack.class, packId);
try {
@@ -348,6 +268,15 @@
return Response.ok(currentPack).build();
}
+ /**
+ * activate
+ * <p>
+ * Move a pack to ACTIVE (only from allowed states).
+ *
+ * @param packId target pack id.
+ * @return 200 OK with updated pack or error when invalid transition.
+ * @throws SeCurisServiceException when invalid state transition.
+ */
@POST
@Path("/{packId}/activate")
@EnsureTransaction
@@ -357,7 +286,6 @@
@Produces({ MediaType.APPLICATION_JSON })
public Response activate(@PathParam("packId") Integer packId) throws SeCurisServiceException {
LOG.info("Activating pack with id: {}", packId);
- // EntityManager em = emProvider.get();
Pack currentPack = em.find(Pack.class, packId);
@@ -372,8 +300,17 @@
return Response.ok(currentPack).build();
}
+ /**
+ * onhold
+ * <p>
+ * Put a pack ON_HOLD from allowed states.
+ *
+ * @param packId id.
+ * @return 200 OK with updated pack or error on invalid state.
+ * @throws SeCurisServiceException on invalid state.
+ */
@POST
- @Path("/{packId}/putonhold")
+ @Path("/{packId}/putonhold}")
@EnsureTransaction
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
@@ -381,7 +318,6 @@
@Produces({ MediaType.APPLICATION_JSON })
public Response onhold(@PathParam("packId") Integer packId) throws SeCurisServiceException {
LOG.info("Putting On hold pack with id: {}", packId);
- // EntityManager em = emProvider.get();
Pack currentPack = em.find(Pack.class, packId);
@@ -396,6 +332,18 @@
return Response.ok(currentPack).build();
}
+ /**
+ * cancel
+ * <p>
+ * Cancel a pack. Cascades cancel to ACTIVE/PRE_ACTIVE licenses in the pack
+ * via {@link LicenseHelper#cancelLicense}.
+ *
+ * @param packId id.
+ * @param reason cancellation reason.
+ * @param bsc actor for history entries.
+ * @return 200 OK with updated pack.
+ * @throws SeCurisServiceException on invalid state.
+ */
@POST
@Path("/{packId}/cancel")
@EnsureTransaction
@@ -405,7 +353,6 @@
@Produces({ MediaType.APPLICATION_JSON })
public Response cancel(@PathParam("packId") Integer packId, @FormParam("reason") String reason, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
LOG.info("Cancelling pack with id: {}", packId);
- // EntityManager em = emProvider.get();
Pack currentPack = em.find(Pack.class, packId);
@@ -426,18 +373,41 @@
return Response.ok(currentPack).build();
}
- private void setPackOrganization(Pack currentPack, Integer orgId, EntityManager em) throws SeCurisException {
- Organization org = null;
- if (orgId != null) {
- org = em.find(Organization.class, orgId);
- if (org == null) {
- LOG.error("Organization pack with id {} not found in DB", orgId);
- throw new SeCurisException("Pack organization not found with ID: " + orgId);
- }
+ /**
+ * getCodeSuffix
+ * <p>
+ * Compute the next available license code for a pack, by asking the helper
+ * for the next numeric suffix and composing with {@link LicUtils}.
+ *
+ * @param packId id.
+ * @param bsc (unused) security context.
+ * @return 200 OK with the full code text.
+ * @throws SeCurisServiceException if packId missing.
+ */
+ @GET
+ @Path("/{packId}/next_license_code")
+ @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
+ @Produces({ MediaType.TEXT_PLAIN })
+ public Response getCodeSuffix(@PathParam("packId") Integer packId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
+ if (packId == null) {
+ throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is mandatory");
}
- currentPack.setOrganization(org);
+ Integer codeSuffix = licenseHelper.getNextCodeSuffix(packId, em);
+ Pack pack = em.find(Pack.class, packId);
+ String licCode = LicUtils.getLicenseCode(pack.getCode(), codeSuffix);
+ return Response.ok(licCode).build();
}
+ /**
+ * delete
+ * <p>
+ * Delete a pack after ensuring there are no ACTIVE/PRE_ACTIVE licenses.
+ * Removes remaining licenses then the pack itself.
+ *
+ * @param packId String id.
+ * @return 200 OK with success map, 404 if missing, or 409 if active license exists.
+ * @throws SeCurisServiceException on constraint errors.
+ */
@DELETE
@Path("/{packId}")
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@@ -446,13 +416,11 @@
@Produces({ MediaType.APPLICATION_JSON })
public Response delete(@PathParam("packId") String packId) throws SeCurisServiceException {
LOG.info("Deleting pack with id: {}", packId);
- // EntityManager em = emProvider.get();
Pack pack = em.find(Pack.class, Integer.parseInt(packId));
if (pack == null) {
LOG.error("Pack with id {} can not be deleted, It was not found in DB", packId);
return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Pack was not found, ID: " + packId).build();
}
- // Pack metadata is removed in cascade automatically.
Set<License> licenses = pack.getLicenses();
for (License license : licenses) {
@@ -466,4 +434,149 @@
return Response.ok(Utils.createMap("success", true, "id", packId)).build();
}
+ // ---------------------------------------------------------------------
+ // Helpers
+ // ---------------------------------------------------------------------
+
+ /**
+ * generateWhereFromParams<p>
+ * Generate where clause to include to a query
+ *
+ * @param addWhere
+ * @param queryParams
+ * @return whereClause
+ */
+ private String generateWhereFromParams(boolean addWhere, MultivaluedMap<String, String> queryParams) {
+ List<String> conditions = new ArrayList<>();
+ if (queryParams.containsKey("organizationId")) {
+ conditions.add(String.format("pa.organization.id = %s", queryParams.getFirst("organizationId")));
+ }
+ if (queryParams.containsKey("applicationId")) {
+ conditions.add(String.format("pa.licenseType.application.id = %s", queryParams.getFirst("applicationId")));
+ }
+ if (queryParams.containsKey("licenseTypeId")) {
+ conditions.add(String.format("pa.licenseType.id = %s", queryParams.getFirst("licenseTypeId")));
+ }
+ String connector = addWhere ? " where " : " and ";
+ return (conditions.isEmpty() ? "" : connector) + String.join(" and ", conditions);
+ }
+
+ /**
+ * createQuery<p>
+ * Build a typed query considering role-based scopes and filters.
+ *
+ * @param queryParams
+ * @param basicSecurityContext
+ */
+ private TypedQuery<Pack> createQuery(MultivaluedMap<String, String> queryParams, BasicSecurityContext bsc) {
+ TypedQuery<Pack> q;
+ String hql = "SELECT pa FROM Pack pa";
+ if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
+ hql += generateWhereFromParams(true, queryParams);
+ q = em.createQuery(hql, Pack.class);
+ } else {
+ if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
+ return null;
+ }
+ if (bsc.getOrganizationsIds() == null || bsc.getOrganizationsIds().isEmpty()) {
+ hql += " where pa.licenseType.application.id in :list_ids_app ";
+ } else {
+ hql += " where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app ";
+ }
+ hql += generateWhereFromParams(false, queryParams);
+ q = em.createQuery(hql, Pack.class);
+ if (hql.contains("list_ids_org")) {
+ q.setParameter("list_ids_org", bsc.getOrganizationsIds());
+ }
+ q.setParameter("list_ids_app", bsc.getApplicationsIds());
+ LOG.info("Getting packs from orgs: {} and apps: {}", bsc.getOrganizationsIds(), bsc.getApplicationsIds());
+ }
+ return q;
+ }
+
+ /**
+ * generateErrorUnathorizedAccess<p>
+ * Convenience 401 generator with log.
+ *
+ * @param pack
+ * @param user
+ */
+ private Response generateErrorUnathorizedAccess(Pack pack, Principal user) {
+ LOG.error("Pack with id {} not accesible by user {}", pack, user);
+ return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack").build();
+ }
+
+ /**
+ * setPackLicenseType<p>
+ * Set the pack type
+ *
+ * @param pack
+ * @param licTypeId
+ * @param em
+ * @throws SeCurisException
+ */
+ private void setPackLicenseType(Pack pack, Integer licTypeId, EntityManager em) throws SeCurisException {
+ LicenseType lt = null;
+ if (licTypeId != null) {
+ lt = em.find(LicenseType.class, pack.getLicTypeId());
+ if (lt == null) {
+ LOG.error("Pack license type with id {} not found in DB", licTypeId);
+ throw new SeCurisException("Pack license type not found with ID: " + licTypeId);
+ }
+ }
+ pack.setLicenseType(lt);
+ }
+
+ /**
+ * getMdKeys<p>
+ * Get the MD keys
+ *
+ * @param mds
+ * @return mdKeys
+ */
+ private Set<String> getMdKeys(Set<PackMetadata> mds) {
+ Set<String> ids = new HashSet<String>();
+ if (mds != null) {
+ for (PackMetadata md : mds) {
+ ids.add(md.getKey());
+ }
+ }
+ return ids;
+ }
+
+ /**
+ * checkIfCodeExists<p>
+ * Check if the code already exist
+ *
+ * @param code
+ * @param em
+ * @return codeExist
+ */
+ private boolean checkIfCodeExists(String code, EntityManager em) {
+ TypedQuery<Pack> query = em.createNamedQuery("pack-by-code", Pack.class);
+ query.setParameter("code", code);
+ int packs = query.getResultList().size();
+ return packs > 0;
+ }
+
+ /**
+ * setPackOrganization<p>
+ * Set the organization of the pack
+ *
+ * @param currentPack
+ * @param orgId
+ * @param em
+ * @throws SeCurisException
+ */
+ private void setPackOrganization(Pack currentPack, Integer orgId, EntityManager em) throws SeCurisException {
+ Organization org = null;
+ if (orgId != null) {
+ org = em.find(Organization.class, orgId);
+ if (org == null) {
+ LOG.error("Organization pack with id {} not found in DB", orgId);
+ throw new SeCurisException("Pack organization not found with ID: " + orgId);
+ }
+ }
+ currentPack.setOrganization(org);
+ }
}
diff --git a/securis/src/main/java/net/curisit/securis/services/UserResource.java b/securis/src/main/java/net/curisit/securis/services/UserResource.java
index d5a9690..7c4681a 100644
--- a/securis/src/main/java/net/curisit/securis/services/UserResource.java
+++ b/securis/src/main/java/net/curisit/securis/services/UserResource.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services;
import java.util.Date;
@@ -47,291 +50,437 @@
import net.curisit.securis.utils.TokenHelper;
/**
- * User resource
- *
+ * UserResource
+ * <p>
+ * REST resource that manages users (CRUD + authentication helpers).
+ * All endpoints are guarded and ADMIN-only unless otherwise stated.
+ * <p>
+ * Notes:
+ * - Uses {@link BasicSecurityContext} authorization via @Securable and @RolesAllowed.
+ * - Uses JPA {@link EntityManager} injected through @Context.
+ * - Mutating endpoints are wrapped in @EnsureTransaction to guarantee commit/rollback.
+ * - Passwords are stored as SHA-256 hashes (see {@link Utils#sha256(String)}).
+ *
+ * Endpoints:
+ * GET /user/ -> list users
+ * GET /user/{uid} -> get user by username
+ * POST /user/ -> create user (idempotent: upsert semantics)
+ * PUT /user/{uid} -> update user (creates if not exists)
+ * POST /user/login -> password authentication; returns token and basic identity
+ * POST /user/check -> validates a token and returns token metadata
+ * GET /user/logout -> invalidates HTTP session (non-token based)
+ *
+ * Thread-safety: RequestScoped. No shared mutable state.
+ *
* @author roberto <roberto.sanchez@curisit.net>
+ * Last reviewed by JRA on Oct 5, 2025.
*/
@Path("/user")
@RequestScoped
public class UserResource {
- @Inject
- TokenHelper tokenHelper;
+ /** Token encoder/decoder & validator. */
+ @Inject TokenHelper tokenHelper;
- @Inject
- private CacheTTL cache;
+ /** Small cache to invalidate role/org derived data after user mutations. */
+ @Inject private CacheTTL cache;
- @Context
- EntityManager em;
+ /** JPA entity manager bound to the current request context. */
+ @Context EntityManager em;
- private static final Logger LOG = LogManager.getLogger(UserResource.class);
+ private static final Logger LOG = LogManager.getLogger(UserResource.class);
- public UserResource() {
- }
+ /**
+ * UserResource
+ * Default constructor for CDI.
+ */
+ public UserResource() {
+ }
- /**
- *
- * @return the server version in format majorVersion.minorVersion
- */
- @GET
- @Path("/")
- @Produces({ MediaType.APPLICATION_JSON })
- @Securable(roles = Rol.ADMIN)
- @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response index() {
- LOG.info("Getting users list ");
+ // ---------------------------------------------------------------------
+ // Read operations
+ // ---------------------------------------------------------------------
- // EntityManager em = emProvider.get();
- em.clear();
- TypedQuery<User> q = em.createNamedQuery("list-users", User.class);
+ /**
+ * index
+ * <p>
+ * List all users.
+ *
+ * Security: ADMIN only.
+ *
+ * @return 200 OK with JSON array of {@link User}, or 200 OK with empty list.
+ */
+ @GET
+ @Path("/")
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Securable(roles = Rol.ADMIN)
+ @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+ public Response index() {
+ LOG.info("Getting users list ");
- List<User> list = q.getResultList();
+ em.clear();
+ TypedQuery<User> q = em.createNamedQuery("list-users", User.class);
+ List<User> list = q.getResultList();
- return Response.ok(list).build();
- }
+ return Response.ok(list).build();
+ }
- /**
- *
- * @return The user
- */
- @GET
- @Path("/{uid}")
- @Produces({ MediaType.APPLICATION_JSON })
- @Securable(roles = Rol.ADMIN)
- @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response get(@PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
- LOG.info("Getting user data for id: {}: ", uid);
- if (uid == null || "".equals(uid)) {
- LOG.error("User ID is mandatory");
- return Response.status(Status.NOT_FOUND).build();
- }
+ /**
+ * get
+ * <p>
+ * Retrieve a single user by username.
+ *
+ * Security: ADMIN only.
+ *
+ * @param uid Username (primary key).
+ * @param token Optional token header (unused here, enforced by filters).
+ * @return 200 OK with user payload or 404 if not found/invalid uid.
+ */
+ @GET
+ @Path("/{uid}")
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Securable(roles = Rol.ADMIN)
+ @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+ public Response get(@PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+ LOG.info("Getting user data for id: {}: ", uid);
+ if (uid == null || "".equals(uid)) {
+ LOG.error("User ID is mandatory");
+ return Response.status(Status.NOT_FOUND).build();
+ }
- // EntityManager em = emProvider.get();
- em.clear();
- User lt = em.find(User.class, uid);
- if (lt == null) {
- LOG.error("User with id {} not found in DB", uid);
- return Response.status(Status.NOT_FOUND).build();
- }
- return Response.ok(lt).build();
- }
+ em.clear();
+ User lt = em.find(User.class, uid);
+ if (lt == null) {
+ LOG.error("User with id {} not found in DB", uid);
+ return Response.status(Status.NOT_FOUND).build();
+ }
+ return Response.ok(lt).build();
+ }
- @POST
- @Path("/")
- @Consumes(MediaType.APPLICATION_JSON)
- @Produces({ MediaType.APPLICATION_JSON })
- @EnsureTransaction
- @Securable(roles = Rol.ADMIN)
- @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response create(User user, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
- LOG.info("Creating new user");
- // EntityManager em = emProvider.get();
- User currentUser = em.find(User.class, user.getUsername());
- if (currentUser != null) {
- LOG.info("User with id {} was found in DB, we'll try to modify it", user.getUsername());
- return modify(user, user.getUsername(), token);
- }
+ // ---------------------------------------------------------------------
+ // Create / Update / Delete
+ // ---------------------------------------------------------------------
- try {
- this.setUserOrgs(user, user.getOrgsIds(), em);
- } catch (SeCurisException e) {
- return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
- }
- try {
- this.setUserApps(user, user.getAppsIds(), em);
- } catch (SeCurisException e) {
- return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
- }
- if (user.getPassword() != null && !"".equals(user.getPassword())) {
- user.setPassword(Utils.sha256(user.getPassword()));
- } else {
- return Response.status(DefaultExceptionHandler.DEFAULT_APP_ERROR_STATUS_CODE).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "User password is mandatory")
- .build();
- }
- user.setModificationTimestamp(new Date());
- user.setLastLogin(null);
- user.setCreationTimestamp(new Date());
- em.persist(user);
+ /**
+ * create
+ * <p>
+ * Create a new user. If the username already exists, delegates to {@link #modify(User, String, String)}
+ * to behave like an upsert.
+ *
+ * Security: ADMIN only.
+ * Transaction: yes (via @EnsureTransaction).
+ *
+ * @param user Incoming user payload. Password must be non-empty (plain text).
+ * Password is SHA-256 hashed before persist.
+ * @param token Security token header (unused here; enforced by filters).
+ * @return 200 OK with created/updated user; 4xx on validation errors.
+ */
+ @POST
+ @Path("/")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces({ MediaType.APPLICATION_JSON })
+ @EnsureTransaction
+ @Securable(roles = Rol.ADMIN)
+ @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+ public Response create(User user, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+ LOG.info("Creating new user");
- return Response.ok(user).build();
- }
+ User currentUser = em.find(User.class, user.getUsername());
+ if (currentUser != null) {
+ LOG.info("User with id {} was found in DB, we'll try to modify it", user.getUsername());
+ return modify(user, user.getUsername(), token);
+ }
- private void setUserOrgs(User user, Set<Integer> orgsIds, EntityManager em) throws SeCurisException {
- Set<Organization> orgs = null;
- if (orgsIds != null && !orgsIds.isEmpty()) {
- orgs = new HashSet<>();
- for (Integer orgId : orgsIds) {
- Organization o = em.find(Organization.class, orgId);
- if (o == null) {
- LOG.error("User organization with id {} not found in DB", orgId);
- throw new SeCurisException("User's organization not found with ID: " + orgId);
- }
- orgs.add(o);
- }
- }
+ try {
+ this.setUserOrgs(user, user.getOrgsIds(), em);
+ } catch (SeCurisException e) {
+ return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+ }
+ try {
+ this.setUserApps(user, user.getAppsIds(), em);
+ } catch (SeCurisException e) {
+ return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+ }
- user.setOrganizations(orgs);
+ // Password must be provided on create
+ if (user.getPassword() != null && !"".equals(user.getPassword())) {
+ user.setPassword(Utils.sha256(user.getPassword()));
+ } else {
+ return Response.status(DefaultExceptionHandler.DEFAULT_APP_ERROR_STATUS_CODE)
+ .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "User password is mandatory")
+ .build();
+ }
- }
+ user.setModificationTimestamp(new Date());
+ user.setLastLogin(null);
+ user.setCreationTimestamp(new Date());
+ em.persist(user);
- private void setUserApps(User user, Set<Integer> appsIds, EntityManager em) throws SeCurisException {
- Set<Application> apps = null;
- if (appsIds != null && !appsIds.isEmpty()) {
- apps = new HashSet<>();
- for (Integer appId : appsIds) {
- Application o = em.find(Application.class, appId);
- if (o == null) {
- LOG.error("User application with id {} not found in DB", appId);
- throw new SeCurisException("User's application not found with ID: " + appId);
- }
- apps.add(o);
- }
- }
+ return Response.ok(user).build();
+ }
- user.setApplications(apps);
- }
+ /**
+ * setUserOrgs
+ * <p>
+ * Resolve and set the organizations for a user from a set of IDs.
+ * Validates each id exists in DB.
+ *
+ * @param user Target user entity.
+ * @param orgsIds Organization ids to assign (nullable/empty allowed).
+ * @param em EntityManager.
+ * @throws SeCurisException if any of the referenced organizations does not exist.
+ */
+ private void setUserOrgs(User user, Set<Integer> orgsIds, EntityManager em) throws SeCurisException {
+ Set<Organization> orgs = null;
+ if (orgsIds != null && !orgsIds.isEmpty()) {
+ orgs = new HashSet<>();
+ for (Integer orgId : orgsIds) {
+ Organization o = em.find(Organization.class, orgId);
+ if (o == null) {
+ LOG.error("User organization with id {} not found in DB", orgId);
+ throw new SeCurisException("User's organization not found with ID: " + orgId);
+ }
+ orgs.add(o);
+ }
+ }
+ user.setOrganizations(orgs);
+ }
- @PUT
- @POST
- @Path("/{uid}")
- @EnsureTransaction
- @Consumes(MediaType.APPLICATION_JSON)
- @Produces({ MediaType.APPLICATION_JSON })
- @Securable(roles = Rol.ADMIN)
- @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response modify(User user, @PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
- LOG.info("Modifying user with id: {}", uid);
- // EntityManager em = emProvider.get();
- User currentUser = em.find(User.class, uid);
- if (currentUser == null) {
- LOG.info("User with id {} not found in DB, we'll try to create it", uid);
- return create(user, token);
- }
+ /**
+ * setUserApps
+ * <p>
+ * Resolve and set the applications for a user from a set of IDs.
+ * Validates each id exists in DB.
+ *
+ * @param user Target user entity.
+ * @param appsIds Application ids to assign (nullable/empty allowed).
+ * @param em EntityManager.
+ * @throws SeCurisException if any of the referenced applications does not exist.
+ */
+ private void setUserApps(User user, Set<Integer> appsIds, EntityManager em) throws SeCurisException {
+ Set<Application> apps = null;
+ if (appsIds != null && !appsIds.isEmpty()) {
+ apps = new HashSet<>();
+ for (Integer appId : appsIds) {
+ Application o = em.find(Application.class, appId);
+ if (o == null) {
+ LOG.error("User application with id {} not found in DB", appId);
+ throw new SeCurisException("User's application not found with ID: " + appId);
+ }
+ apps.add(o);
+ }
+ }
+ user.setApplications(apps);
+ }
- try {
- this.setUserOrgs(currentUser, user.getOrgsIds(), em);
- } catch (SeCurisException e) {
- return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
- }
- try {
- this.setUserApps(currentUser, user.getAppsIds(), em);
- } catch (SeCurisException e) {
- return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
- }
- currentUser.setFirstName(user.getFirstName());
- currentUser.setLastName(user.getLastName());
- currentUser.setRoles(user.getRoles());
- currentUser.setLang(user.getLang());
- currentUser.setModificationTimestamp(new Date());
- if (user.getPassword() != null && !"".equals(user.getPassword())) {
- currentUser.setPassword(Utils.sha256(user.getPassword()));
- } else {
- // Password has not been modified
- // return
- }
+ /**
+ * modify
+ * <p>
+ * Update an existing user. If the user does not exist, delegates to {@link #create(User, String)}.
+ * Password is updated only if a non-empty password is provided.
+ * Organizations & applications are fully replaced with the given ids.
+ *
+ * Security: ADMIN only.
+ * Transaction: yes (via @EnsureTransaction).
+ *
+ * @param user Incoming user payload.
+ * @param uid Username (path param) to update.
+ * @param token Security token header (unused here).
+ * @return 200 OK with updated user; 404 if reference entities are missing.
+ */
+ @PUT
+ @POST
+ @Path("/{uid}")
+ @EnsureTransaction
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Securable(roles = Rol.ADMIN)
+ @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+ public Response modify(User user, @PathParam("uid") String uid, @HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token) {
+ LOG.info("Modifying user with id: {}", uid);
- currentUser.setLastLogin(user.getLastLogin());
+ User currentUser = em.find(User.class, uid);
+ if (currentUser == null) {
+ LOG.info("User with id {} not found in DB, we'll try to create it", uid);
+ return create(user, token);
+ }
- em.persist(currentUser);
- clearUserCache(currentUser.getUsername());
+ try {
+ this.setUserOrgs(currentUser, user.getOrgsIds(), em);
+ } catch (SeCurisException e) {
+ return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+ }
+ try {
+ this.setUserApps(currentUser, user.getAppsIds(), em);
+ } catch (SeCurisException e) {
+ return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
+ }
- return Response.ok(currentUser).build();
- }
+ currentUser.setFirstName(user.getFirstName());
+ currentUser.setLastName(user.getLastName());
+ currentUser.setRoles(user.getRoles());
+ currentUser.setLang(user.getLang());
+ currentUser.setModificationTimestamp(new Date());
- @DELETE
- @Path("/{uid}")
- @EnsureTransaction
- @Produces({ MediaType.APPLICATION_JSON })
- @Securable(roles = Rol.ADMIN)
- @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
- public Response delete(@PathParam("uid") String uid, @Context HttpServletRequest request) {
- LOG.info("Deleting app with id: {}", uid);
- // EntityManager em = emProvider.get();
- User user = em.find(User.class, uid);
- if (user == null) {
- LOG.error("User with id {} can not be deleted, It was not found in DB", uid);
- return Response.status(Status.NOT_FOUND).build();
- }
+ // Optional password update
+ if (user.getPassword() != null && !"".equals(user.getPassword())) {
+ currentUser.setPassword(Utils.sha256(user.getPassword()));
+ }
- em.remove(user);
- clearUserCache(user.getUsername());
- return Response.ok(Utils.createMap("success", true, "id", uid)).build();
- }
+ // lastLogin can be set through API (rare), otherwise managed at login
+ currentUser.setLastLogin(user.getLastLogin());
- private void clearUserCache(String username) {
- cache.remove("roles_" + username);
- cache.remove("orgs_" + username);
- }
+ em.persist(currentUser);
+ clearUserCache(currentUser.getUsername());
- @POST
- @Path("/login")
- @Produces({ MediaType.APPLICATION_JSON })
- public Response login(@FormParam("username") String username, @FormParam("password") String password, @Context HttpServletRequest request) throws SeCurisServiceException {
- LOG.info("index session: " + request.getSession());
+ return Response.ok(currentUser).build();
+ }
- // EntityManager em = emProvider.get();
- User user = em.find(User.class, username);
- if (user == null) {
- LOG.error("Unknown username {} used in login service", username);
- throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
- }
- String securedPassword = Utils.sha256(password);
+ /**
+ * delete
+ * <p>
+ * Delete a user by username.
+ *
+ * Security: ADMIN only.
+ * Transaction: yes (via @EnsureTransaction).
+ *
+ * @param uid Username to delete.
+ * @param request Http servlet request (unused).
+ * @return 200 OK on success; 404 if user does not exist.
+ */
+ @DELETE
+ @Path("/{uid}")
+ @EnsureTransaction
+ @Produces({ MediaType.APPLICATION_JSON })
+ @Securable(roles = Rol.ADMIN)
+ @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
+ public Response delete(@PathParam("uid") String uid, @Context HttpServletRequest request) {
+ LOG.info("Deleting app with id: {}", uid);
- if (securedPassword == null || !securedPassword.equals(user.getPassword())) {
- throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
- }
- user.setLastLogin(new Date());
- em.getTransaction().begin();
- try {
- em.persist(user);
- em.getTransaction().commit();
- } catch (PersistenceException ex) {
- LOG.error("Error updating last login date for user: {}", username);
- LOG.error(ex);
- em.getTransaction().rollback();
- }
- clearUserCache(username);
- String userFullName = String.format("%s %s", user.getFirstName(), user.getLastName() == null ? "" : user.getLastName()).trim();
- String tokenAuth = tokenHelper.generateToken(username);
- return Response.ok(Utils.createMap("success", true, "token", tokenAuth, "username", username, "full_name", userFullName)).build();
- }
+ User user = em.find(User.class, uid);
+ if (user == null) {
+ LOG.error("User with id {} can not be deleted, It was not found in DB", uid);
+ return Response.status(Status.NOT_FOUND).build();
+ }
- /**
- * Check if current token is valid
- *
- * @param user
- * @param password
- * @param request
- * @return
- */
- @POST
- @Path("/check")
- @Produces({ MediaType.APPLICATION_JSON })
- public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
- if (token == null) {
- token = token2;
- }
- if (token == null) {
- return Response.status(Status.FORBIDDEN).build();
- }
+ em.remove(user);
+ clearUserCache(user.getUsername());
+ return Response.ok(Utils.createMap("success", true, "id", uid)).build();
+ }
- LOG.info("Token : " + token);
- String user = tokenHelper.extractUserFromToken(token);
- LOG.info("Token user: " + user);
- Date date = tokenHelper.extractDateCreationFromToken(token);
- LOG.info("Token date: " + date);
- boolean valid = tokenHelper.isTokenValid(token);
+ /**
+ * clearUserCache
+ * <p>
+ * Helper to invalidate cached role/org projections after changes.
+ *
+ * @param username The user whose cache entries must be cleared.
+ */
+ private void clearUserCache(String username) {
+ cache.remove("roles_" + username);
+ cache.remove("orgs_" + username);
+ }
- LOG.info("Is Token valid: " + valid);
+ // ---------------------------------------------------------------------
+ // Auth helpers
+ // ---------------------------------------------------------------------
- return Response.ok(Utils.createMap("valid", true, "user", user, "date", date, "token", token)).build();
- }
+ /**
+ * login
+ * <p>
+ * Validates username & password against stored SHA-256 hash. On success,
+ * updates lastLogin timestamp, clears cache and returns an auth token.
+ *
+ * Token format: Base64("<secret> <user> <ISO8601-date>")
+ * where secret = SHA-256(seed + user + date).
+ *
+ * @param username Plain username.
+ * @param password Plain password (SHA-256 will be computed server-side).
+ * @param request Http request, used to log underlying session (not required for token flow).
+ * @return 200 OK with {token, username, full_name}; 401 on invalid credentials.
+ * @throws SeCurisServiceException if user is missing or password mismatch.
+ */
+ @POST
+ @Path("/login")
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Response login(@FormParam("username") String username, @FormParam("password") String password, @Context HttpServletRequest request) throws SeCurisServiceException {
+ LOG.info("index session: " + request.getSession());
- @GET
- @Path("/logout")
- @Produces({ MediaType.APPLICATION_JSON })
- public Response logout(@Context HttpServletRequest request) {
- request.getSession().invalidate();
- return Response.ok().build();
- }
+ User user = em.find(User.class, username);
+ if (user == null) {
+ LOG.error("Unknown username {} used in login service", username);
+ throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
+ }
+ String securedPassword = Utils.sha256(password);
+
+ if (securedPassword == null || !securedPassword.equals(user.getPassword())) {
+ throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Wrong credentials");
+ }
+
+ user.setLastLogin(new Date());
+ em.getTransaction().begin();
+ try {
+ em.persist(user);
+ em.getTransaction().commit();
+ } catch (PersistenceException ex) {
+ LOG.error("Error updating last login date for user: {}", username);
+ LOG.error(ex);
+ em.getTransaction().rollback();
+ }
+
+ clearUserCache(username);
+ String userFullName = String.format("%s %s", user.getFirstName(), user.getLastName() == null ? "" : user.getLastName()).trim();
+ String tokenAuth = tokenHelper.generateToken(username);
+ return Response.ok(Utils.createMap("success", true, "token", tokenAuth, "username", username, "full_name", userFullName)).build();
+ }
+
+ /**
+ * check
+ * <p>
+ * Validates a token and echoes token claims (user, creation date, token string).
+ * Accepts header or query param for convenience.
+ *
+ * @param token Token in header {@link TokenHelper#TOKEN_HEADER_PÀRAM}, may be null.
+ * @param token2 Token in query param 'token', used if header is null.
+ * @return 200 OK with {valid, user, date, token} or 403 if token missing.
+ */
+ @POST
+ @Path("/check")
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Response check(@HeaderParam(TokenHelper.TOKEN_HEADER_PÀRAM) String token, @QueryParam("token") String token2) {
+ if (token == null) {
+ token = token2;
+ }
+ if (token == null) {
+ return Response.status(Status.FORBIDDEN).build();
+ }
+
+ LOG.info("Token : " + token);
+ String user = tokenHelper.extractUserFromToken(token);
+ LOG.info("Token user: " + user);
+ Date date = tokenHelper.extractDateCreationFromToken(token);
+ LOG.info("Token date: " + date);
+ boolean valid = tokenHelper.isTokenValid(token);
+
+ LOG.info("Is Token valid: " + valid);
+
+ return Response.ok(Utils.createMap("valid", true, "user", user, "date", date, "token", token)).build();
+ }
+
+ /**
+ * logout
+ * <p>
+ * Invalidates the HTTP session (useful if the UI also tracks session).
+ * Note: token-based auth is stateless; tokens are not revoked here.
+ *
+ * @param request HttpServletRequest to invalidate session.
+ * @return 200 OK always.
+ */
+ @GET
+ @Path("/logout")
+ @Produces({ MediaType.APPLICATION_JSON })
+ public Response logout(@Context HttpServletRequest request) {
+ request.getSession().invalidate();
+ return Response.ok().build();
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java b/securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java
index bfe790c..e44e944 100644
--- a/securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java
+++ b/securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java
@@ -1,51 +1,90 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services.exception;
import net.curisit.integrity.exception.CurisException;
+/**
+ * SeCurisServiceException
+ * <p>
+ * Checked exception for service-layer errors with an attached numeric error code.
+ * Extends {@link CurisException} and is intended to be translated to HTTP responses
+ * by upstream exception mappers/handlers.
+ *
+ * Usage:
+ * - Prefer specific {@code ErrorCodes.*} when throwing.
+ * - Use the single-arg constructor to default to UNEXPECTED_ERROR.
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+ */
public class SeCurisServiceException extends CurisException {
- private int errorCode = 0;
+ /** Numeric error code associated with this exception. */
+ private int errorCode = 0;
- public SeCurisServiceException(int errorCode, String msg) {
- super(msg);
- this.errorCode = errorCode;
- }
+ /**
+ * Constructor with explicit error code.
+ *
+ * @param errorCode See {@link ErrorCodes}.
+ * @param msg Human-readable message (safe to expose).
+ */
+ public SeCurisServiceException(int errorCode, String msg) {
+ super(msg);
+ this.errorCode = errorCode;
+ }
- public SeCurisServiceException(String msg) {
- super(msg);
- this.errorCode = ErrorCodes.UNEXPECTED_ERROR;
- }
+ /**
+ * Constructor defaulting to {@link ErrorCodes#UNEXPECTED_ERROR}.
+ *
+ * @param msg Human-readable message (safe to expose).
+ */
+ public SeCurisServiceException(String msg) {
+ super(msg);
+ this.errorCode = ErrorCodes.UNEXPECTED_ERROR;
+ }
- public int getStatus() {
- return errorCode;
- }
+ /**
+ * getStatus
+ * <p>
+ * Returns the stored numeric error code.
+ *
+ * @return integer error code.
+ */
+ public int getStatus() {
+ return errorCode;
+ }
- /**
- *
- */
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 1L;
- public static class ErrorCodes {
- public static int UNEXPECTED_ERROR = 1000;
- public static int INVALID_CREDENTIALS = 1001;
- public static int UNAUTHORIZED_ACCESS = 1002;
- public static int NOT_FOUND = 1003;
- public static int INVALID_FORMAT = 1004;
- public static int WRONG_STATUS = 1005;
- public static int UNNECESSARY_RENEW = 1006;
+ /**
+ * ErrorCodes
+ * <p>
+ * Canonical set of service-layer error codes.
+ * Grouped by feature areas (1000 generic, 11xx license, 12xx request data, 13xx validation).
+ */
+ public static class ErrorCodes {
+ public static int UNEXPECTED_ERROR = 1000;
+ public static int INVALID_CREDENTIALS = 1001;
+ public static int UNAUTHORIZED_ACCESS = 1002;
+ public static int NOT_FOUND = 1003;
+ public static int INVALID_FORMAT = 1004;
+ public static int WRONG_STATUS = 1005;
+ public static int UNNECESSARY_RENEW = 1006;
- public static int INVALID_LICENSE_REQUEST_DATA = 1100;
- public static int LICENSE_NOT_READY_FOR_RENEW = 1101;
- public static int LICENSE_DATA_IS_NOT_VALID = 1102;
- public static int LICENSE_IS_EXPIRED = 1103;
- public static int LICENSE_PACK_IS_NOT_VALID = 1104;
+ public static int INVALID_LICENSE_REQUEST_DATA = 1100;
+ public static int LICENSE_NOT_READY_FOR_RENEW = 1101;
+ public static int LICENSE_DATA_IS_NOT_VALID = 1102;
+ public static int LICENSE_IS_EXPIRED = 1103;
+ public static int LICENSE_PACK_IS_NOT_VALID = 1104;
- public static int INVALID_REQUEST_DATA = 1201;
- public static int INVALID_REQUEST_DATA_FORMAT = 1202;
- public static int BLOCKED_REQUEST_DATA = 1203;
- public static int DUPLICATED_REQUEST_DATA = 1204;
- public static int NO_AVAILABLE_LICENSES = 1205;
+ public static int INVALID_REQUEST_DATA = 1201;
+ public static int INVALID_REQUEST_DATA_FORMAT = 1202;
+ public static int BLOCKED_REQUEST_DATA = 1203;
+ public static int DUPLICATED_REQUEST_DATA = 1204;
+ public static int NO_AVAILABLE_LICENSES = 1205;
- public static int INVALID_DATA = 1301;
- }
+ public static int INVALID_DATA = 1301;
+ }
}
diff --git a/securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java b/securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java
index 7159ddc..a2115d6 100644
--- a/securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java
+++ b/securis/src/main/java/net/curisit/securis/services/helpers/LicenseHelper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services.helpers;
import java.io.File;
@@ -30,128 +33,192 @@
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);
- private static final long MS_PER_DAY = 24L * 3600L * 1000L;
+ @SuppressWarnings("unused")
+ private static final Logger LOG = LogManager.getLogger(LicenseHelper.class);
- @Inject
- private UserHelper userHelper;
+ /** Milliseconds per day (used to derive expiration dates). */
+ private static final long MS_PER_DAY = 24L * 3600L * 1000L;
- /**
- * Cancel the license
- *
- * @param lic
- * @param em
- */
- 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);
+ @Inject private UserHelper userHelper;
- em.persist(createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.CANCEL, "Cancellation reason: " + reason));
- }
+ /**
+ * 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);
- /**
- * Validates that the passed license exists and is still valid
- *
- * @param licBean
- * @param em
- * @return The License instance in DB
- * @throws SeCurisServiceException
- */
- 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;
- }
+ em.persist(createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.CANCEL, "Cancellation reason: " + reason));
+ }
- 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;
- }
+ /**
+ * 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;
+ }
- public LicenseHistory createLicenseHistoryAction(License lic, User user, String action) {
- return createLicenseHistoryAction(lic, user, action, null);
- }
+ /**
+ * 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;
+ }
- /**
- * Create a license file in a temporary directory
- *
- * @param lic
- * @param licFileName
- * @return
- * @throws IOException
- */
- 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;
- }
+ /**
+ * 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);
+ }
- public Map<String, Object> extractPackMetadata(Set<PackMetadata> packMetadata) {
- Map<String, Object> metadata = new HashMap<>();
- for (PackMetadata md : packMetadata) {
- metadata.put(md.getKey(), md.getValue());
- }
+ /**
+ * 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;
+ }
- return metadata;
- }
+ /**
+ * 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;
+ }
- /**
- * If the action is a renew the expiration date is got form pack end valid
- * date, if the action is a pre-activation the expiration date is calculated
- * using the pack default valid period
- *
- * @param pack
- * @param isPreActivation
- * @return
- */
- 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;
- }
+ /**
+ * 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;
+ }
- /**
- * Get the next free code suffis for a given Pack
- *
- * @param packId
- * @param em
- * @return
- */
- 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;
- }
-
+ /**
+ * 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;
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java b/securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java
index ac70711..4fb1e3e 100644
--- a/securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java
+++ b/securis/src/main/java/net/curisit/securis/services/helpers/MetadataHelper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services.helpers;
import java.util.Collection;
@@ -24,147 +27,258 @@
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);
+ private static final Logger log = LogManager.getLogger(MetadataHelper.class);
- 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();
- }
+ /**
+ * 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();
+ }
- public <T extends Metadata> Metadata findByKey(String key, Collection<T> listMd) {
- return listMd.parallelStream().filter(m -> Objects.equals(key, m.getKey())).findAny().orElse(null);
- }
+ /**
+ * 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);
+ }
- 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)));
- }
+ /**
+ * 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)));
+ }
- public <T extends Metadata, K extends Metadata> void mergeMetadata(EntityManager em, Set<T> srcListMd, Set<K> tgtListMd, Set<String> keys) {
+ /**
+ * 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) {
- 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);
- }
- 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);
- }
- }
- }
+ // 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);
+ }
- 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());
- }
+ // 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);
+ }
+ }
+ }
- 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());
- }
+ // -- Internal helpers to create new metadata rows when propagating
- /**
- * Copy the modified app metadata to LicenseTypes and Packs
- *
- * @param em
- * @param app
- */
- 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);
- // Probably there is a better way to get the final metadata from JPA...
- TypedQuery<LicenseTypeMetadata> updatedMdQuery = em.createNamedQuery("list-licensetype-metadata", LicenseTypeMetadata.class);
- updatedMdQuery.setParameter("licenseTypeId", lt.getId());
- Set<LicenseTypeMetadata> updatedMd = new HashSet<>(updatedMdQuery.getResultList());
+ /**
+ * 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());
+ }
- lt.setMetadata(updatedMd);
- propagateMetadata(em, lt, keys);
- }
- }
+ /**
+ * 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());
+ }
- /**
- * Copy the modified licenseType metadata to Packs
- *
- * @param em
- * @param lt
- * @param keys
- */
- 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);
- }
- }
+ /**
+ * 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);
- 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);
- }
- }
- }
+ // 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);
+ }
+ }
+ }
}
+
diff --git a/securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java b/securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java
index 4c561dc..b9bff32 100644
--- a/securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java
+++ b/securis/src/main/java/net/curisit/securis/services/helpers/UserHelper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.services.helpers;
import jakarta.enterprise.context.ApplicationScoped;
@@ -8,14 +11,45 @@
import net.curisit.securis.security.BasicSecurityContext;
import net.curisit.securis.services.exception.SeCurisServiceException;
+/**
+ * UserHelper
+ * <p>
+ * Small helper to resolve the current user (from security context) or by username.
+ * Throws a typed {@link SeCurisServiceException} if the user cannot be found.
+ *
+ * Thread-safety: ApplicationScoped, stateless.
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+ */
@ApplicationScoped
public class UserHelper {
+ /**
+ * getUser
+ * <p>
+ * Resolve the current authenticated user from {@link BasicSecurityContext}.
+ *
+ * @param bsc Security context containing a principal.
+ * @param em EntityManager to fetch the user.
+ * @return Managed {@link User}.
+ * @throws SeCurisServiceException if the principal is null or not found in DB.
+ */
public User getUser(BasicSecurityContext bsc, EntityManager em) throws SeCurisServiceException {
String username = bsc.getUserPrincipal().getName();
return getUser(username, em);
}
+ /**
+ * getUser
+ * <p>
+ * Resolve a user by username.
+ *
+ * @param username Username to look up (nullable allowed; returns null).
+ * @param em EntityManager to fetch the user.
+ * @return Managed {@link User} or null if username is null.
+ * @throws SeCurisServiceException if a non-null username does not exist.
+ */
public User getUser(String username, EntityManager em) throws SeCurisServiceException {
User user = null;
if (username != null) {
diff --git a/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java b/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
index 2b0146a..8376449 100644
--- a/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
+++ b/securis/src/main/java/net/curisit/securis/utils/CacheTTL.java
@@ -1,3 +1,6 @@
+/*
+* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+*/
package net.curisit.securis.utils;
import java.util.ArrayList;
@@ -5,6 +8,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -13,122 +17,258 @@
import org.apache.logging.log4j.Logger;
/**
- * Cache implementation with TTL (time To Live) The objects are removed from
- * cache when TTL is reached.
- *
- * @author roberto <roberto.sanchez@curisit.net>
- */
+* CacheTTL
+* <p>
+* Simple in-memory cache with per-entry TTL (time-to-live). A background
+* cleaning thread periodically removes expired entries.
+*
+* <p><b>Type-safety note:</b> Besides generic getters, this cache provides
+* {@link #getSet(String, Class)} to safely retrieve {@code Set<E>} values
+* without unchecked warnings at call sites. The method validates that all
+* elements match the requested {@code elementType}.
+*
+* <p><b>Threading:</b> This implementation is lightweight and uses a single
+* cleaner thread. The internal map is not synchronized beyond the remove loop,
+* which is acceptable for low-concurrency scenarios. For heavier usage,
+* consider switching to a {@code ConcurrentHashMap} and/or a scheduled executor.
+*
+* @author roberto
+* Last reviewed by JRA on Oct 5, 2025.
+*/
@ApplicationScoped
public class CacheTTL {
private static final Logger LOG = LogManager.getLogger(CacheTTL.class);
+ /** Default TTL (seconds) for entries when not specified. */
+ private static final int DEFAULT_CACHE_DURATION = 24 * 60 * 60;
+
+ /** Backing store: key → cached object + expiration. */
+ private final Map<String, CachedObject> data = new HashMap<>();
+
+ /** Background cleaner thread. */
+ private final Thread cleaningThread;
+
/**
- * Period before token expires, set in seconds.
- */
- private static int DEFAULT_CACHE_DURATION = 24 * 60 * 60;
-
- private Map<String, CachedObject> data = new HashMap<>();
-
- private Thread cleaningThread = null;
-
+ * CacheTTL<p>
+ * Construct a cache and start the background cleaner that removes expired
+ * entries every 60 seconds.
+ */
@Inject
public CacheTTL() {
- cleaningThread = new Thread(new Runnable() {
-
- @Override
- public void run() {
- while (CacheTTL.this.data != null) {
- try {
- // We check for expired object every 60 seconds
- Thread.sleep(60 * 1000);
- } catch (InterruptedException e) {
- LOG.error("Exiting from Cache Thread");
- data.clear();
- return;
- }
- Date now = new Date();
- List<String> keysToRemove = new ArrayList<>();
- for (String key : CacheTTL.this.data.keySet()) {
- CachedObject co = CacheTTL.this.data.get(key);
- if (now.after(co.getExpireAt())) {
- keysToRemove.add(key);
- }
- }
- for (String key : keysToRemove) {
- // If we try to remove directly in the previous loop an
- // exception is thrown
- // java.util.ConcurrentModificationException
- CacheTTL.this.data.remove(key);
+ cleaningThread = new Thread(() -> {
+ while (true) {
+ try {
+ // Check for expired objects every 60 seconds
+ Thread.sleep(60_000);
+ } catch (InterruptedException e) {
+ LOG.warn("Cache cleaner interrupted. Clearing cache and stopping.");
+ data.clear();
+ return;
+ }
+ Date now = new Date();
+ List<String> keysToRemove = new ArrayList<>();
+ for (String key : data.keySet()) {
+ CachedObject co = data.get(key);
+ if (co != null && now.after(co.getExpireAt())) {
+ keysToRemove.add(key);
}
}
+ for (String key : keysToRemove) {
+ // Avoid ConcurrentModificationException by removing after iteration
+ data.remove(key);
+ }
}
- });
+ }, "CacheTTL-Cleaner");
+ cleaningThread.setDaemon(true);
cleaningThread.start();
}
+ // ---------------------------------------------------------------------
+ // Putters
+ // ---------------------------------------------------------------------
+
/**
- *
- * @param key
- * @param obj
- * @param ttl
- * Time To Live in seconds
- */
+ * set<p>
+ * Store a value with an explicit TTL.
+ *
+ * @param key cache key
+ * @param obj value to store (may be any object, including collections)
+ * @param ttl TTL in seconds
+ */
public void set(String key, Object obj, int ttl) {
- Date expirationDate = new Date(new Date().getTime() + ttl * 1000);
+ Date expirationDate = new Date(System.currentTimeMillis() + (long) ttl * 1000L);
data.put(key, new CachedObject(expirationDate, obj));
}
+ /**
+ * set<p>
+ * Store a value with the default TTL.
+ *
+ * @param key cache key
+ * @param obj value to store
+ */
public void set(String key, Object obj) {
set(key, obj, DEFAULT_CACHE_DURATION);
}
+ // ---------------------------------------------------------------------
+ // Getters
+ // ---------------------------------------------------------------------
+
+ /**
+ * get<p>
+ * Retrieve a value as {@code Object}. Returns {@code null} if not present
+ * or expired (expired entries are eagerly removed by the cleaner).
+ *
+ * @param key cache key
+ * @return cached value or null
+ */
public Object get(String key) {
CachedObject co = data.get(key);
return co == null ? null : co.getObject();
}
+ /**
+ * get<p>
+ * Retrieve a value and cast it to the requested type. The cast is unchecked
+ * due to type erasure, but localized within the cache implementation.
+ *
+ * @param key cache key
+ * @param type expected value type
+ * @param <T> generic type
+ * @return cached value typed or null
+ */
public <T> T get(String key, Class<T> type) {
CachedObject co = data.get(key);
return co == null ? null : co.getObject(type);
}
+ /**
+ * getSet<p>
+ * Retrieve a {@code Set<E>} in a type-safe way without unchecked warnings
+ * at the call site. The method validates that the cached value is a
+ * {@code Set} and that <b>all</b> elements are instances of {@code elementType}.
+ * If any element does not match, the method returns {@code null} and logs a warning.
+ *
+ * @param key cache key
+ * @param elementType class of the set elements (e.g., {@code Integer.class})
+ * @return typed set or null if missing/type-mismatch
+ */
+ @SuppressWarnings("unchecked")
+ public <E> Set<E> getSet(String key, Class<E> elementType) {
+ Object obj = get(key);
+ if (obj == null) return null;
+ if (!(obj instanceof Set<?> raw)) {
+ LOG.warn("Cache key '{}' expected Set<{}> but found {}", key, elementType.getSimpleName(), obj.getClass().getName());
+ return null;
+ }
+ // Validate element types to avoid ClassCastException later
+ for (Object el : raw) {
+ if (el != null && !elementType.isInstance(el)) {
+ LOG.warn("Cache key '{}' contains element of type {}, expected {}", key,
+ el.getClass().getName(), elementType.getName());
+ return null;
+ }
+ }
+ // Safe due to element-wise validation
+ return (Set<E>) raw;
+ }
+
+ // ---------------------------------------------------------------------
+ // Removers & maintenance
+ // ---------------------------------------------------------------------
+
+ /**
+ * remove<p>
+ * Remove and return a value typed.
+ *
+ * @param key cache key
+ * @param type expected type
+ * @return removed value or null
+ */
public <T> T remove(String key, Class<T> type) {
CachedObject co = data.remove(key);
return co == null ? null : co.getObject(type);
}
+ /**
+ * remove<p>
+ * Remove and return a value as {@code Object}.
+ *
+ * @param key cache key
+ * @return removed value or null
+ */
public Object remove(String key) {
CachedObject co = data.remove(key);
return co == null ? null : co.getObject();
}
+ /**
+ * clear<p>
+ * Remove all entries from the cache.
+ */
public void clear() {
data.clear();
}
- private class CachedObject {
- Date expireAt;
- Object object;
+ // ---------------------------------------------------------------------
+ // Internal structure
+ // ---------------------------------------------------------------------
+ /**
+ * CachedObject
+ * <p>
+ * Internal wrapper that pairs an arbitrary object with its expiration date.
+ */
+ private static class CachedObject {
+ private final Date expireAt;
+ private final Object object;
+
+ /**
+ * Constructor<p>
+ * Set expiration and payload.
+ *
+ * @param date
+ * @param object
+ */
public CachedObject(Date date, Object obj) {
- expireAt = date;
- object = obj;
+ this.expireAt = date;
+ this.object = obj;
}
+ /**
+ * getExpireAt<p>
+ * Return expiration date.
+ *
+ * @return date
+ */
public Date getExpireAt() {
return expireAt;
}
+ /**
+ * getObject<p>
+ * Return payload as {@code Object}.
+ *
+ * @return object
+ */
public Object getObject() {
return object;
}
+ /**
+ * getObject<p>
+ * Return payload cast to the requested type. Cast is localized here.
+ *
+ * @param type requested type
+ * @param <T> generic type
+ * @return typed payload
+ */
@SuppressWarnings("unchecked")
public <T> T getObject(Class<T> type) {
return (T) object;
}
-
}
-
}
+
diff --git a/securis/src/main/java/net/curisit/securis/utils/Config.java b/securis/src/main/java/net/curisit/securis/utils/Config.java
index 1d43249..a2e4c35 100644
--- a/securis/src/main/java/net/curisit/securis/utils/Config.java
+++ b/securis/src/main/java/net/curisit/securis/utils/Config.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.utils;
import java.io.IOException;
@@ -11,17 +14,31 @@
import org.apache.logging.log4j.Logger;
/**
- * Class that loads and serves global config parameters.
+ * Config
+ * <p>
+ * Class that loads and serves global config parameters from a classpath properties file
+ * and, as a fallback, from environment variables.
+ *
+ * Initialization:
+ * - Static initializer loads {@link #KEY_CONFIG_FILE} from classpath (fails hard if missing).
+ *
+ * Accessors:
+ * - {@link #get(String)} / {@link #get(String, String)}
+ * - Integer variants: {@link #getInt(String)} / {@link #getInt(String, int)}
+ * - Namespaced helpers: by prefix/domain, and sequential lists via {@link #getListByPrefix(String)}.
+ *
+ * Thread-safety: static-only utility; internal state is read-only after init.
*
- * @author rsanchez
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
*/
public class Config {
private static final Logger LOG = LogManager.getLogger(Config.class);
/**
- * Key used to store config file resource location. In a web application,
- * can be set as initial parameter in a servlet loaded on startup
+ * Resource path of the application properties file (in classpath).
+ * E.g. "/securis-server.properties".
*/
public static final String KEY_CONFIG_FILE = "/securis-server.properties";
@@ -37,12 +54,12 @@
}
/**
- * Loads application global parameters from a classpath resource
- *
- * @param resource
- * : Resource location in classpath, i.e:
- * "/resource/cp-securis.conf"
- * @throws IOException
+ * loadParameters
+ * <p>
+ * Loads application global parameters from a classpath resource.
+ *
+ * @param resource Classpath location (e.g. "/resource/cp-securis.conf").
+ * @throws IOException If the resource cannot be found or read.
*/
public static void loadParameters(String resource) throws IOException {
@@ -51,7 +68,6 @@
params = new Properties();
try {
-
params.load(fileis);
LOG.debug("Params loaded OK from {}", resource);
} catch (IOException e) {
@@ -59,84 +75,112 @@
params = null;
throw e;
}
-
}
+ /**
+ * getByDomain
+ * <p>
+ * Convenience accessor for domain-suffixed parameters (param.domain).
+ *
+ * @param domain Domain suffix.
+ * @param paramname Base parameter name.
+ * @return String value or null if not present.
+ */
public static String getByDomain(String domain, String paramname) {
return getByDomain(domain, paramname, null);
}
+ /**
+ * getByPrefix
+ * <p>
+ * Returns parameter value from "{prefix}.{param}" or falls back to the plain "{param}".
+ *
+ * @param prefix Namespace prefix.
+ * @param paramname Parameter name.
+ * @return Resolved value or null.
+ */
public static String getByPrefix(String prefix, String paramname) {
return get(prefix + "." + paramname, get(paramname));
}
+ /**
+ * getByPrefix
+ * <p>
+ * Returns parameter value from "{prefix}.{param}" or provided default (which itself
+ * falls back to "{param}" or its default).
+ *
+ * @param prefix Namespace prefix.
+ * @param paramname Parameter name.
+ * @param defaultVal Default value if none resolved.
+ * @return Resolved value.
+ */
public static String getByPrefix(String prefix, String paramname, String defaultVal) {
return get(prefix + "." + paramname, get(paramname, defaultVal));
}
+ /**
+ * getByDomain
+ * <p>
+ * Returns value from "{param}.{domain}" or provided default.
+ *
+ * @param domain domain suffix.
+ * @param paramname base name.
+ * @param defaultval fallback if not found.
+ * @return resolved string.
+ */
public static String getByDomain(String domain, String paramname, String defaultval) {
return get(paramname + "." + domain, defaultval);
}
+ /**
+ * getIntByDomain
+ * <p>
+ * Integer variant of {@link #getByDomain(String, String)} with fallback to plain param.
+ */
public static int getIntByDomain(String domain, String paramname) {
return getInt(paramname + "." + domain, getInt(paramname));
}
+ /**
+ * getIntByDomain
+ * <p>
+ * Integer variant returning provided default when missing.
+ */
public static int getIntByDomain(String domain, String paramname, int defaultval) {
return getInt(paramname + "." + domain, defaultval);
}
/**
- * Gets a List with all values of properties that begins with
- * <code>prefix</code> It reads sequentially. For example:
- *
- * <pre>
- * securis.sort.comparator.0: net.cp.securis.comparators.ComparePttidVsPtn
- * securis.sort.comparator.1: net.cp.securis.comparators.CompareFrequency
- * securis.sort.comparator.2: net.cp.securis.comparators.CompareOutgoingVsIncomming
- * securis.sort.comparator.3: net.cp.securis.comparators.CompareDuration
- * securis.sort.comparator.4: net.cp.securis.comparators.CompareCallVsSms
- * </pre>
- *
- * That config (for prefix: "securis.sort.comparator" ) will return a
- * List<String> with values:
- *
- * <pre>
- * "net.cp.securis.comparators.ComparePttidVsPtn",
- * "net.cp.securis.comparators.CompareFrequency",
- * "net.cp.securis.comparators.CompareOutgoingVsIncomming",
- * "net.cp.securis.comparators.CompareDuration",
- * "net.cp.securis.comparators.CompareCallVsSms"
- * </pre>
- *
- * Note: If there is a gap between suffixes process will stop, that is, only
- * will be returned properties found before gap.
- *
- * @param prefix
- * @return
+ * getListByPrefix
+ * <p>
+ * Reads sequential properties using numeric suffixes starting from 0 and stops on first gap.
+ * Example:
+ * securis.sort.comparator.0=...
+ * securis.sort.comparator.1=...
+ * ...
+ *
+ * @param prefix Base prefix (e.g. "securis.sort.comparator").
+ * @return Ordered list of values until first missing index.
*/
public static List<String> getListByPrefix(String prefix) {
List<String> list = new ArrayList<String>();
-
String tpl = prefix + ".{0}";
-
int i = 0;
String value = get(MessageFormat.format(tpl, i++));
while (value != null) {
list.add(value);
value = get(MessageFormat.format(tpl, i++));
}
-
return list;
}
/**
- * Gets param value in config file or environment variables
- *
- * @param paramname
- * Global parameter's name
- * @return Value of paramname or null if paramname is not found neither in
- * config file nor in environment variables
+ * get
+ * <p>
+ * Get a parameter value from the loaded properties or environment variables.
+ *
+ * @param paramname Parameter key.
+ * @return Value or null if not found anywhere.
*/
public static String get(String paramname) {
@@ -149,12 +193,13 @@
}
/**
- * Gets param value from config file or environment variables
- *
- * @param paramname
- * Global parameter's name
- * @param defaultval
- * @return Value of paramname or defaultval if paramname is not found
+ * get
+ * <p>
+ * Returns parameter value or default if missing.
+ *
+ * @param paramname Key.
+ * @param defaultval Default fallback.
+ * @return value or default.
*/
public static String get(String paramname, String defaultval) {
String value = get(paramname);
@@ -162,12 +207,12 @@
}
/**
- * Gets param value in config file or environment variables
- *
- * @param paramname
- * Global parameter's name
- * @return Integer value of paramname or -1 if paramname is not found
- * neither in config file nor in environment variables
+ * getInt
+ * <p>
+ * Integer accessor, returns -1 if missing.
+ *
+ * @param paramname Key.
+ * @return Parsed integer or -1.
*/
public static int getInt(String paramname) {
String value = get(paramname);
@@ -175,19 +220,24 @@
}
/**
- * Gets param value from config file or environment variables
- *
- * @param paramname
- * Global parameter's name
- * @param defaultval
- * @return Integer value of paramname or defaultval if paramname is not
- * found
+ * getInt
+ * <p>
+ * Integer accessor, returns provided default when missing.
+ *
+ * @param paramname Key.
+ * @param defaultval Default fallback.
+ * @return Parsed integer or default.
*/
public static int getInt(String paramname, int defaultval) {
String value = get(paramname);
return (value == null ? defaultval : Integer.parseInt(value));
}
+ /**
+ * KEYS
+ * <p>
+ * Strongly-typed keys used across the application.
+ */
public static class KEYS {
public static final String SERVER_HOSTNAME = "license.server.hostname";
@@ -205,5 +255,5 @@
public static final String EMAIL_FROM_ADDRESS = "email.from.address";
public static final String EMAIL_LIC_DEFAULT_SUBJECT = "email.lic.default.subject";
}
-
}
+
diff --git a/securis/src/main/java/net/curisit/securis/utils/EmailManager.java b/securis/src/main/java/net/curisit/securis/utils/EmailManager.java
index 5b3ad1f..4d427f5 100644
--- a/securis/src/main/java/net/curisit/securis/utils/EmailManager.java
+++ b/securis/src/main/java/net/curisit/securis/utils/EmailManager.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.utils;
import java.io.File;
@@ -40,22 +43,61 @@
import org.apache.logging.log4j.Logger;
/**
- * Component that send emails using Mailgun API:
- * http://documentation.mailgun.com/user_manual.html#sending-messages
- *
- * @author roberto <roberto.sanchez@curisit.net>
+ * EmailManager
+ * <p>
+ * Small utility to send plain-text emails (optionally with one attachment)
+ * using the <b>Mailgun</b> API over HTTPS.
+ * <p>
+ * Design notes:
+ * <ul>
+ * <li>Reads Mailgun credentials and the "from" address from {@link Config}.</li>
+ * <li>Builds a preconfigured {@link HttpClientBuilder} with basic auth and a permissive SSL socket factory.</li>
+ * <li>Exposes synchronous (blocking) and asynchronous (non-blocking) send methods.</li>
+ * <li>Scope is {@code @ApplicationScoped}; the underlying builder is created once per container.</li>
+ * </ul>
+ *
+ * Thread-safety:
+ * <p>
+ * The class is effectively stateless after construction; using a shared {@link HttpClientBuilder}
+ * is safe as a new {@link HttpClient} is built per request.
+ *
+ * Configuration keys (see {@link Config.KEYS}):
+ * <ul>
+ * <li>{@code mailgun.domain}</li>
+ * <li>{@code mailgun.api.key}</li>
+ * <li>{@code email.from.address}</li>
+ * </ul>
+ *
+ * Failure handling:
+ * <p>
+ * Network and HTTP errors are surfaced as {@link SeCurisServiceException} with appropriate error codes.
+ *
+ * @author roberto <roberto.sanchez@curisit.net>
+ * Last reviewed by JRA on Oct 6, 2025.
*/
@ApplicationScoped
public class EmailManager {
+ /** Class logger. */
private static final Logger LOG = LogManager.getLogger(EmailManager.class);
+ /** Mailgun endpoint composed from configured domain. */
private final String serverUrl;
+
+ /** Preconfigured builder that carries SSL and basic-auth configuration. */
private final HttpClientBuilder httpClientBuilder;
+ // ---------------------------------------------------------------------
+ // Constructors
+ // ---------------------------------------------------------------------
+
/**
- *
- * @throws SeCurisException
+ * EmailManager
+ * <p>
+ * Default constructor that validates required configuration and prepares an
+ * HTTP client builder with Mailgun credentials and SSL settings.
+ *
+ * @throws SeCurisException if mandatory configuration is missing or the SSL builder cannot be created
*/
public EmailManager() throws SeCurisException {
String domain = Config.get(Config.KEYS.MAILGUN_DOMAIN);
@@ -64,13 +106,29 @@
}
serverUrl = String.format("https://api.mailgun.net/v2/%s/messages", domain);
httpClientBuilder = createHttpClient();
-
}
+ // ---------------------------------------------------------------------
+ // Internal helpers
+ // ---------------------------------------------------------------------
+
+ /**
+ * createHttpClient
+ * <p>
+ * Builds a {@link HttpClientBuilder} that:
+ * <ul>
+ * <li>Accepts any server certificate (permissive trust strategy).</li>
+ * <li>Applies HTTP Basic Auth using Mailgun's API key as the password and user "api".</li>
+ * </ul>
+ *
+ * @return a preconfigured {@link HttpClientBuilder} ready to build clients
+ * @throws SeCurisException if SSL initialization fails
+ */
private HttpClientBuilder createHttpClient() throws SeCurisException {
SSLContextBuilder builder = new SSLContextBuilder();
SSLConnectionSocketFactory sslsf = null;
try {
+ // Trust all X509 certificates (relies on HTTPS + Basic Auth; consider hardening in production).
builder.loadTrustMaterial((KeyStore) null, new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
@@ -82,30 +140,45 @@
LOG.error(e1);
throw new SeCurisException("Error creating SSL socket factory");
}
+
+ // Configure Basic Auth with Mailgun API key
CredentialsProvider provider = new BasicCredentialsProvider();
- UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("api", Config.get(Config.KEYS.MAILGUN_API_KEY));
+ UsernamePasswordCredentials credentials =
+ new UsernamePasswordCredentials("api", Config.get(Config.KEYS.MAILGUN_API_KEY));
provider.setCredentials(AuthScope.ANY, credentials);
- return HttpClientBuilder.create().setDefaultCredentialsProvider(provider).setSSLSocketFactory(sslsf);
+ return HttpClientBuilder.create()
+ .setDefaultCredentialsProvider(provider)
+ .setSSLSocketFactory(sslsf);
}
+ // ---------------------------------------------------------------------
+ // Email sending API
+ // ---------------------------------------------------------------------
+
/**
- * Basic method to send emails in text mode with attachment. The method is
- * synchronous, It waits until server responses.
- *
- * @param subject
- * @param body
- * @param to
- * @param file
- * @throws SeCurisException
- * @throws UnsupportedEncodingException
+ * sendEmail
+ * <p>
+ * Sends a plain-text email (UTF-8) via Mailgun. Optionally attaches a single file.
+ * This call is <b>synchronous</b> (blocking) and only returns once the HTTP response is received.
+ *
+ * @param subject Email subject (will be sent as UTF-8).
+ * @param body Email body in plain text (UTF-8).
+ * @param to Recipient address (required).
+ * @param cc Optional CC address, may be {@code null}.
+ * @param file Optional file to attach, may be {@code null}.
+ *
+ * @throws SeCurisServiceException if the HTTP call fails or Mailgun responds with a non-200 status
+ * @throws UnsupportedEncodingException kept for API compatibility (body/subject are forced to UTF-8)
*/
@SuppressWarnings("deprecation")
- public void sendEmail(String subject, String body, String to, String cc, File file) throws SeCurisServiceException, UnsupportedEncodingException {
+ public void sendEmail(String subject, String body, String to, String cc, File file)
+ throws SeCurisServiceException, UnsupportedEncodingException {
+
HttpPost postRequest = new HttpPost(serverUrl);
+ // Build multipart form body compatible with Mailgun
MultipartEntityBuilder builder = MultipartEntityBuilder.create();
-
builder.setCharset(Charset.forName("utf-8"));
builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
builder.addTextBody("from", Config.get(Config.KEYS.EMAIL_FROM_ADDRESS));
@@ -113,28 +186,34 @@
if (cc != null) {
builder.addTextBody("cc", cc);
}
- builder.addTextBody("subject", subject, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
- builder.addTextBody("text", body, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
- if (file != null) {
+ builder.addTextBody("subject",
+ subject, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
+ builder.addTextBody("text",
+ body, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), Charset.forName("utf-8")));
+ if (file != null) {
LOG.info("File to attach: {}", file.getAbsoluteFile());
builder.addPart("attachment", new FileBody(file));
}
postRequest.setEntity(builder.build());
+
+ // Execute HTTP request
HttpResponse response;
HttpClient httpClient = httpClientBuilder.build();
try {
response = httpClient.execute(postRequest);
+ // Mailgun returns JSON. We parse it to a Map for logging/validation.
String jsonLic = IOUtils.toString(response.getEntity().getContent());
if (response.getStatusLine().getStatusCode() == 200) {
LOG.debug("Response content read OK: {}", jsonLic);
Map<String, Object> responseBean = JsonUtils.json2map(jsonLic);
-
LOG.debug("Response mail read OK: {}", responseBean);
} else {
- throw new SeCurisServiceException(ErrorCodes.UNEXPECTED_ERROR, "Error sending email, response estatus: " + response.getStatusLine());
+ throw new SeCurisServiceException(
+ ErrorCodes.UNEXPECTED_ERROR,
+ "Error sending email, response estatus: " + response.getStatusLine());
}
} catch (IOException e) {
LOG.error("Error sending email", e);
@@ -143,21 +222,27 @@
}
/**
- * Basic method to send emails in text mode with attachment. The method is
- * asynchronous, It returns immediately
- *
- * @param subject
- * @param body
- * @param to
- * @param file
- * @throws SeCurisException
- * @throws UnsupportedEncodingException
+ * sendEmailAsync
+ * <p>
+ * Asynchronous variant of {@link #sendEmail(String, String, String, String, File)}.
+ * The call returns immediately and performs the HTTP request in a single-thread executor.
+ *
+ * @param subject Email subject (UTF-8).
+ * @param body Email body in plain text (UTF-8).
+ * @param to Recipient address.
+ * @param cc Optional CC address, may be {@code null}.
+ * @param file Optional attachment, may be {@code null}.
+ * @param callback Non-null callback to be notified on success or failure.
+ *
+ * @throws SeCurisException if there is a configuration or environment problem before dispatch
+ * @throws UnsupportedEncodingException for API compatibility (subject/body are encoded as UTF-8)
*/
- public void sendEmailAsync(String subject, String body, String to, String cc, File file, EmailCallback callback) throws SeCurisException,
- UnsupportedEncodingException {
+ public void sendEmailAsync(
+ String subject, String body, String to, String cc, File file, EmailCallback callback)
+ throws SeCurisException, UnsupportedEncodingException {
+
Executor ex = Executors.newSingleThreadExecutor();
ex.execute(new Runnable() {
-
@Override
public void run() {
try {
@@ -168,37 +253,61 @@
} catch (SeCurisServiceException e) {
callback.error(e);
}
-
}
});
-
}
+ // ---------------------------------------------------------------------
+ // Callback contract
+ // ---------------------------------------------------------------------
+
+ /**
+ * EmailCallback
+ * <p>
+ * Functional contract to be notified when an async send finishes.
+ */
public static interface EmailCallback {
+ /**
+ * success<p>
+ * Called when the email was sent and Mailgun returned HTTP 200.
+ */
public void success();
+ /**
+ * error<p>
+ * Called when there was a problem sending the email.
+ *
+ * @param e encapsulates the reason of failure
+ */
public void error(SeCurisServiceException e);
}
+ // ---------------------------------------------------------------------
+ // Manual test harness
+ // ---------------------------------------------------------------------
+
+ /**
+ * main<p>
+ * Simple manual test for the async email flow. Adjust addresses and file path before use.
+ *
+ * @param args program arguments (unused)
+ * @throws SeCurisException if configuration is invalid
+ * @throws UnsupportedEncodingException if UTF-8 encoding fails (unlikely)
+ */
public static void main(String[] args) throws SeCurisException, UnsupportedEncodingException {
- // new EmailManager().sendEmail("España así de bien",
- // "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response",
- // "info@r75.es", new File(
- // "/Users/rob/Downloads/test.req"));
- new EmailManager().sendEmailAsync("España así de bien", "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response", "info@r75.es",
- "dev@r75.es", new File("/Users/rob/Downloads/test.req"), new EmailCallback() {
+ // Example async call (subject/body contain non-ASCII content to validate UTF-8 handling).
+ new EmailManager().sendEmailAsync("España así de bien",
+ "Me gusta esta prueba\nCon varias líneas\n\n\n--\nNo response",
+ "info@r75.es",
+ "dev@r75.es",
+ new File("/Users/rob/Downloads/test.req"),
+ new EmailCallback() {
+ @Override
+ public void success() { LOG.info("Success!!!"); }
@Override
- public void success() {
- LOG.info("Success!!!");
- }
-
- @Override
- public void error(SeCurisServiceException e) {
- LOG.error("Error: {} !!!", e);
- }
+ public void error(SeCurisServiceException e) { LOG.error("Error: {} !!!", e); }
});
LOG.info("Waiting for email to be sent...");
}
-
}
diff --git a/securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java b/securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java
index 82ba7f6..e252ebb 100644
--- a/securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java
+++ b/securis/src/main/java/net/curisit/securis/utils/GZipServletResponseWrapper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.utils;
import java.io.IOException;
@@ -10,38 +13,92 @@
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
+/**
+ * GZipServletResponseWrapper
+ * <p>
+ * {@link HttpServletResponseWrapper} that transparently compresses the response
+ * body using GZIP. Intended for use in filters/servlets where the caller wants
+ * to wrap the original response and write compressed bytes to the client.
+ * <p>
+ * How it works:
+ * <ul>
+ * <li>Overrides {@link #getOutputStream()} and {@link #getWriter()} to route output through a {@link GZIPOutputStream}.</li>
+ * <li>Ensures mutual exclusivity between OutputStream and Writer access per Servlet API requirements.</li>
+ * <li>Ignores {@link #setContentLength(int)} since compressed size differs from uncompressed.</li>
+ * </ul>
+ *
+ * Usage:
+ * <pre>
+ * GZipServletResponseWrapper gz = new GZipServletResponseWrapper(resp);
+ * chain.doFilter(request, gz);
+ * gz.close(); // important: finish compression and flush buffers
+ * </pre>
+ *
+ * Thread-safety:
+ * <p>
+ * Instances are per-request and not shared.
+ *
+ * Limitations:
+ * <p>
+ * Caller is responsible for setting "Content-Encoding: gzip" and for avoiding
+ * double-compression scenarios.
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 6, 2025.
+ */
public class GZipServletResponseWrapper extends HttpServletResponseWrapper {
private GZIPServletOutputStream gzipOutputStream = null;
private PrintWriter printWriter = null;
+ // ---------------------------------------------------------------------
+ // Constructors
+ // ---------------------------------------------------------------------
+
+ /**
+ * GZipServletResponseWrapper
+ * <p>
+ * Wraps the given response. Actual GZIP streams are lazily created on first write.
+ *
+ * @param response the original {@link HttpServletResponse} to wrap
+ * @throws IOException if the underlying response streams cannot be accessed
+ */
public GZipServletResponseWrapper(HttpServletResponse response) throws IOException {
super(response);
}
- public void close() throws IOException {
+ // ---------------------------------------------------------------------
+ // Lifecycle
+ // ---------------------------------------------------------------------
- //PrintWriter.close does not throw exceptions.
- //Hence no try-catch block.
+ /**
+ * close<p>
+ * Closes any open writer or output stream and finalizes the GZIP stream.
+ * Must be called once all response content has been written.
+ *
+ * @throws IOException if closing the underlying streams fails
+ */
+ public void close() throws IOException {
+ // PrintWriter.close does not throw exceptions. Hence no try-catch block.
if (this.printWriter != null) {
this.printWriter.close();
}
-
if (this.gzipOutputStream != null) {
this.gzipOutputStream.close();
}
}
/**
- * Flush OutputStream or PrintWriter
+ * flushBuffer<p>
+ * Flushes the writer and the GZIP output stream, then delegates to the wrapped response.
+ * If multiple exceptions occur, the first encountered is thrown (typical servlet practice).
*
- * @throws IOException
+ * @throws IOException if flushing any of the streams fails
*/
-
@Override
public void flushBuffer() throws IOException {
- //PrintWriter.flush() does not throw exception
+ // PrintWriter.flush() does not throw exception
if (this.printWriter != null) {
this.printWriter.flush();
}
@@ -62,12 +119,23 @@
exception2 = e;
}
- if (exception1 != null)
- throw exception1;
- if (exception2 != null)
- throw exception2;
+ if (exception1 != null) throw exception1;
+ if (exception2 != null) throw exception2;
}
+ // ---------------------------------------------------------------------
+ // Output acquisition
+ // ---------------------------------------------------------------------
+
+ /**
+ * getOutputStream<p>
+ * Returns a {@link ServletOutputStream} that writes compressed data.
+ * Mutually exclusive with {@link #getWriter()} as per Servlet API.
+ *
+ * @return compressed {@link ServletOutputStream}
+ * @throws IOException if the underlying output stream cannot be obtained
+ * @throws IllegalStateException if the writer has been already acquired
+ */
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (this.printWriter != null) {
@@ -79,6 +147,15 @@
return this.gzipOutputStream;
}
+ /**
+ * getWriter<p>
+ * Returns a {@link PrintWriter} that writes compressed data (UTF-8 by default, inherited from response).
+ * Mutually exclusive with {@link #getOutputStream()} as per Servlet API.
+ *
+ * @return compressed {@link PrintWriter}
+ * @throws IOException if streams cannot be allocated
+ * @throws IllegalStateException if the output stream has been already acquired
+ */
@Override
public PrintWriter getWriter() throws IOException {
if (this.printWriter == null && this.gzipOutputStream != null) {
@@ -91,50 +168,118 @@
return this.printWriter;
}
+ /**
+ * setContentLength<p>
+ * No-op. The content length of the zipped content is not known a priori and
+ * will not match the uncompressed length; therefore we do not set it here.
+ *
+ * @param len ignored
+ */
@Override
public void setContentLength(int len) {
- //ignore, since content length of zipped content
- //does not match content length of unzipped content.
+ // ignore, since content length of zipped content does not match content length of unzipped content.
}
+ // ---------------------------------------------------------------------
+ // Inner compressed stream
+ // ---------------------------------------------------------------------
+
+ /**
+ * GZIPServletOutputStream
+ * <p>
+ * Decorates the original {@link ServletOutputStream} with a {@link GZIPOutputStream}.
+ * Delegates readiness and listener to the underlying (container) stream.
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 5, 2025.
+ */
private static class GZIPServletOutputStream extends ServletOutputStream {
private final ServletOutputStream servletOutputStream;
private final GZIPOutputStream gzipStream;
+ /**
+ * GZIPServletOutputStream<p>
+ * Creates a new compressed stream wrapper.
+ *
+ * @param servletOutputStream underlying (container-provided) output stream
+ * @throws IOException if the GZIP stream cannot be created
+ */
public GZIPServletOutputStream(ServletOutputStream servletOutputStream) throws IOException {
this.servletOutputStream = servletOutputStream;
this.gzipStream = new GZIPOutputStream(servletOutputStream);
}
+ /**
+ * isReady<p>
+ * Check if the output stream is ready
+ * {@inheritDoc}
+ *
+ * @return isReady
+ */
@Override
public boolean isReady() {
return this.servletOutputStream.isReady();
}
+ /**
+ * setWriteListener<p>
+ * Set the write listener for the output stream
+ * {@inheritDoc}
+ *
+ * @param writeListener
+ */
@Override
public void setWriteListener(WriteListener writeListener) {
this.servletOutputStream.setWriteListener(writeListener);
}
+ /**
+ * write<p>
+ * Write on the gzip stream
+ * {@inheritDoc}
+ *
+ * @param b
+ * @throws IOException
+ */
@Override
public void write(int b) throws IOException {
this.gzipStream.write(b);
}
+ /**
+ * close<p>
+ * Close the gzip stream
+ * {@inheritDoc}
+ *
+ * @throws IOException
+ */
@Override
public void close() throws IOException {
this.gzipStream.close();
}
+ /**
+ * flush<p>
+ * Flush the gzip stream
+ * {@inheritDoc}
+ *
+ * @throws IOException
+ */
@Override
public void flush() throws IOException {
this.gzipStream.flush();
}
+ /**
+ * finish<p>
+ * Explicitly finishes writing of the GZIP stream, without closing the underlying stream.
+ * Not used by the wrapper but available for completeness.
+ *
+ * @throws IOException if finishing fails
+ */
@SuppressWarnings("unused")
public void finish() throws IOException {
this.gzipStream.finish();
}
}
-
}
diff --git a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
index ff567f7..74455a8 100644
--- a/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
+++ b/securis/src/main/java/net/curisit/securis/utils/TokenHelper.java
@@ -1,3 +1,6 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
package net.curisit.securis.utils;
import java.io.IOException;
@@ -20,37 +23,108 @@
import java.util.Base64;
import java.nio.charset.StandardCharsets;
+/**
+ * TokenHelper
+ * <p>
+ * Utility component to generate and validate short-lived authentication tokens
+ * for SeCuris services. Tokens are:
+ * </p>
+ * <ul>
+ * <li>Base64-encoded UTF-8 strings.</li>
+ * <li>Composed as: {@code <secret> <username> <iso8601-timestamp>} (space-separated).</li>
+ * <li>Where {@code secret} is a deterministic SHA-256 HMAC-like hash built from a static seed,
+ * the username, and the issuance timestamp.</li>
+ * </ul>
+ *
+ * <p><b>Lifecycle & scope:</b> {@code @ApplicationScoped}. Stateless and thread-safe.</p>
+ *
+ * <p><b>Security notes:</b>
+ * The {@code seed} acts like a shared secret. Keep it private and rotate if compromised.
+ * Tokens expire after {@link #VALID_TOKEN_PERIOD} hours except a special client token
+ * defined by {@link ApiResource#API_CLIENT_USERNAME} issued at epoch-1 (see {@link #isTokenValid(String)}).
+ * </p>
+ *
+ * <p><b>Format details:</b>
+ * <pre>
+ * token = Base64( secret + " " + user + " " + Utils.toIsoFormat(date) )
+ * secret = hex(SHA-256(seed || user || isoDate))
+ * </pre>
+ * </p>
+ *
+ * @author JRA
+ * Last reviewed by JRA on Oct 6, 2025.
+ */
@ApplicationScoped
public class TokenHelper {
private static final Logger LOG = LogManager.getLogger(TokenHelper.class);
/**
- * Period before token expires, set in hours.
+ * Validity window for standard tokens, in hours.
+ * <p>
+ * Any token with a creation date older than this window will be rejected
+ * (unless it matches the special API client rule documented in
+ * {@link #isTokenValid(String)}).
+ * </p>
*/
private static int VALID_TOKEN_PERIOD = 24;
+
+ /** Standard HTTP header used by SeCuris clients to carry the token. */
public static final String TOKEN_HEADER_PÀRAM = "X-SECURIS-TOKEN";
+ /**
+ * TokenHelper<p>
+ * CDI no-arg constructor.
+ * <p>
+ * Kept for dependency injection. No initialization logic is required.
+ * </p>
+ */
@Inject
public TokenHelper() {
}
+ /**
+ * Static secret seed used to derive the token {@code secret} portion.
+ * <p>
+ * Treat this as confidential. Changing it invalidates all outstanding tokens.
+ * </p>
+ */
private static byte[] seed = "S3Cur15S33dForT0k3nG3n3r@tion".getBytes();
+ // ---------------------------------------------------------------------
+ // Token generation
+ // ---------------------------------------------------------------------
+
/**
- * Generate a token encoded in Base64 for user passed as parameter and
- * taking the current moment as token timestamp
- *
- * @param user
- * @return
+ * generateToken
+ * <p>
+ * Convenience overload that generates a token for {@code user} using the current
+ * system time as the issuance timestamp.
+ * </p>
+ *
+ * @param user Username to embed in the token (must be non-null/non-empty).
+ * @return Base64-encoded token string, or {@code null} if a cryptographic error occurs.
*/
public String generateToken(String user) {
-
return generateToken(user, new Date());
}
- ;
-
+ /**
+ * generateToken
+ * <p>
+ * Builds a token for a given user and issuance date. The token body is:
+ * </p>
+ * <pre>
+ * secret + " " + user + " " + Utils.toIsoFormat(date)
+ * </pre>
+ * <p>
+ * and then Base64-encoded in UTF-8.
+ * </p>
+ *
+ * @param user Username to embed.
+ * @param date Issuance date to include in the token (affects expiry and secret derivation).
+ * @return Base64 token, or {@code null} upon failure.
+ */
public String generateToken(String user, Date date) {
try {
String secret = generateSecret(user, date);
@@ -61,7 +135,7 @@
sb.append(' ');
sb.append(Utils.toIsoFormat(date));
- // Codificación estándar con UTF-8
+ // Standard UTF-8 encoding before Base64
return Base64.getEncoder().encodeToString(sb.toString().getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
@@ -72,42 +146,86 @@
return null;
}
+ /**
+ * generateSecret
+ * <p>
+ * Derives the deterministic secret (a 64-hex-character SHA-256 digest) used to
+ * authenticate a token. Inputs are concatenated in the following order:
+ * </p>
+ * <ol>
+ * <li>{@link #seed}</li>
+ * <li>{@code user} (UTF-8 bytes)</li>
+ * <li>{@code Utils.toIsoFormat(date)}</li>
+ * </ol>
+ *
+ * @param user Username to mix in the digest.
+ * @param date Token issuance date to mix in the digest.
+ * @return 64-char hex string.
+ * @throws UnsupportedEncodingException If UTF-8 is unavailable (unexpected).
+ * @throws NoSuchAlgorithmException If SHA-256 is unavailable (unexpected).
+ */
private String generateSecret(String user, Date date) throws UnsupportedEncodingException, NoSuchAlgorithmException {
MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
mDigest.update(seed, 0, seed.length);
+
byte[] userbytes = user.getBytes("utf-8");
mDigest.update(userbytes, 0, userbytes.length);
+
byte[] isodate = Utils.toIsoFormat(date).getBytes();
mDigest.update(isodate, 0, isodate.length);
+
BigInteger i = new BigInteger(1, mDigest.digest());
String secret = String.format("%1$064x", i);
return secret;
}
+ // ---------------------------------------------------------------------
+ // Token validation & parsing
+ // ---------------------------------------------------------------------
+
/**
- * Check if passed token is still valid, It use to check if token is expired
- * the attribute VALID_TOKEN_PERIOD (in hours)
- *
- * @param token
- * @return
+ * isTokenValid
+ * <p>
+ * Validates the structure, signature and expiry of the given token.
+ * Steps performed:
+ * </p>
+ * <ol>
+ * <li>Base64-decode the token into {@code "secret user isoDate"}.</li>
+ * <li>Parse {@code user} and {@code isoDate}; recompute the expected secret via
+ * {@link #generateSecret(String, Date)} and compare with the provided one.</li>
+ * <li>Check expiry: if the token's timestamp is older than
+ * {@link #VALID_TOKEN_PERIOD} hours, it's invalid.</li>
+ * <li>Special-case: if {@code user} equals {@link ApiResource#API_CLIENT_USERNAME}
+ * and the date returns a non-positive epoch time (e.g., created with {@code new Date(-1)}),
+ * the expiry check is skipped (client integration token).</li>
+ * </ol>
+ *
+ * @param token Base64 token string.
+ * @return {@code true} if valid and not expired; {@code false} otherwise.
*/
public boolean isTokenValid(String token) {
try {
- String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
+ String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
String[] parts = StringUtils.split(tokenDecoded, ' ');
if (parts == null || parts.length < 3) {
return false;
}
+
String secret = parts[0];
String user = parts[1];
Date date = Utils.toDateFromIso(parts[2]);
+
+ // Expiry check (unless special client token rule applies)
if (date.getTime() > 0 || !user.equals(ApiResource.API_CLIENT_USERNAME)) {
if (new Date().after(new Date(date.getTime() + VALID_TOKEN_PERIOD * 60 * 60 * 1000))) {
return false;
}
- } // else: It's a securis-client API call
+ }
+
+ // Signature check
String newSecret = generateSecret(user, date);
return newSecret.equals(secret);
+
} catch (IOException e) {
LOG.error("Error decoding Base64 token", e);
} catch (NoSuchAlgorithmException e) {
@@ -116,6 +234,16 @@
return false;
}
+ /**
+ * extractUserFromToken
+ * <p>
+ * Extracts the username portion from a validly structured token.
+ * </p>
+ *
+ * @param token Base64 token string (may be {@code null}).
+ * @return Username if the token has at least three space-separated fields after decoding;
+ * {@code null} on error or malformed input.
+ */
public String extractUserFromToken(String token) {
try {
if (token == null) {
@@ -134,6 +262,16 @@
return null;
}
+ /**
+ * extractDateCreationFromToken
+ * <p>
+ * Parses and returns the issuance {@link Date} embedded in the token, without
+ * performing validation or expiry checks.
+ * </p>
+ *
+ * @param token Base64 token string.
+ * @return Issuance {@link Date}, or {@code null} if the token is malformed or cannot be decoded.
+ */
public Date extractDateCreationFromToken(String token) {
try {
String tokenDecoded = new String(Base64.getDecoder().decode(token), StandardCharsets.UTF_8);
@@ -149,6 +287,20 @@
return null;
}
+ // ---------------------------------------------------------------------
+ // Demo / manual test
+ // ---------------------------------------------------------------------
+
+ /**
+ * main
+ * <p>
+ * Simple manual test demonstrating generation and validation of a special
+ * "_client" token that bypasses expiry via a negative epoch date.
+ * </p>
+ *
+ * @param args CLI args (unused).
+ * @throws IOException If something goes wrong while encoding/decoding Base64 (unlikely).
+ */
public static void main(String[] args) throws IOException {
// client token:
// OTk3ODRiMzY5NzQ5MWI5NmYyZGQyODRiYjY2ZTU2YzdmMTZjYzM3YTY3N2ExM2M3ODI2MjU5ZTMzOTIyYjUzNSBfY2xpZW50IDE5NzAtMDEtMDFUMDA6NTk6NTkuOTk5KzAxMDA=
@@ -160,3 +312,4 @@
System.out.println("is valid client token: " + new TokenHelper().isTokenValid(t));
}
}
+
diff --git a/securis/src/main/resources/META-INF/persistence.xml b/securis/src/main/resources/META-INF/persistence.xml
index 638909f..62abb5f 100644
--- a/securis/src/main/resources/META-INF/persistence.xml
+++ b/securis/src/main/resources/META-INF/persistence.xml
@@ -19,4 +19,4 @@
</properties>
</persistence-unit>
-</persistence>
+</persistence>
\ No newline at end of file
--
Gitblit v1.3.2