From 4ee50e257b32f6ec0f72907305d1f2b1212808a4 Mon Sep 17 00:00:00 2001
From: Joaquín Reñé <jrene@curisit.net>
Date: Fri, 27 Mar 2026 15:07:12 +0000
Subject: [PATCH] #4479 - upgrade SecurisServer to Java 21

---
 securis/src/main/java/net/curisit/securis/services/exception/CurisException.java          |   86 +++
 securis/pom.xml                                                                           |   29 
 securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java                    |   37 +
 securis/src/main/java/net/curisit/securis/RestServicesApplication.java                    |    1 
 securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java                    |  202 +++++++-
 securis/src/main/webapp/WEB-INF/web.xml                                                   |   83 +-
 securis/src/main/java/net/curisit/securis/utils/Utils.java                                |  719 +++++++++++++++++++++++++++++++
 securis/src/main/java/net/curisit/securis/services/BasicServices.java                     |    4 
 securis/src/main/java/net/curisit/securis/AppVersion.java                                 |  158 ++++++
 securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java |    2 
 10 files changed, 1,222 insertions(+), 99 deletions(-)

diff --git a/securis/pom.xml b/securis/pom.xml
index eb22eda..4d4555d 100644
--- a/securis/pom.xml
+++ b/securis/pom.xml
@@ -5,7 +5,8 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>net.curisit</groupId>
     <artifactId>securis-server</artifactId>
-    <version>2.0.2</version>
+    <version>3.0.0</version>
+    <packaging>war</packaging>
     <name>SeCuris-Server</name>
 
     <properties>
@@ -14,11 +15,13 @@
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <maven.compiler.source>21</maven.compiler.source>
         <maven.compiler.target>21</maven.compiler.target>
+        
         <resteasy.version>6.2.4.Final</resteasy.version>
         <hibernate.version>5.6.15.Final</hibernate.version>
         <jakarta.persistence.version>3.1.0</jakarta.persistence.version>
-        <jakarta.servlet.version>6.0.0</jakarta.servlet.version>
+        <jakarta.servlet.version>6.1.0</jakarta.servlet.version>
         <jakarta.cdi.version>4.0.1</jakarta.cdi.version>
+        <beanutils.version>1.9.4</beanutils.version>
         <log4j.version>2.18.0</log4j.version>
     </properties>
 
@@ -36,12 +39,7 @@
             <artifactId>resteasy-core</artifactId>
             <version>${resteasy.version}</version>
         </dependency>
-        <dependency>
-            <groupId>org.jboss.resteasy</groupId>
-            <artifactId>resteasy-servlet-initializer</artifactId>
-            <version>${resteasy.version}</version>
-        </dependency>
-		<dependency>
+ 		<dependency>
 		  <groupId>org.jboss.resteasy</groupId>
 		  <artifactId>resteasy-multipart-provider</artifactId>
 		  <version>${resteasy.version}</version>
@@ -83,6 +81,13 @@
         </dependency>
 
         <!-- Hibernate 5 compatible con Jakarta Persistence -->
+        <!--
+        <dependency>
+            <groupId>org.hibernate</groupId>
+            <artifactId>hibernate-core</artifactId>
+            <version>${hibernate.version}</version>
+        </dependency>
+        -->
         <dependency>
             <groupId>org.hibernate</groupId>
             <artifactId>hibernate-core</artifactId>
@@ -111,6 +116,14 @@
 		    <artifactId>guice</artifactId>
 		    <version>5.1.0</version>
 		</dependency>
+		
+		<!--  Bean utils -->
+		<dependency>
+			<groupId>commons-beanutils</groupId>
+			<artifactId>commons-beanutils</artifactId>
+			<version>${beanutils.version}</version>
+		</dependency>
+
         
     </dependencies>
 
diff --git a/securis/src/main/java/net/curisit/securis/AppVersion.java b/securis/src/main/java/net/curisit/securis/AppVersion.java
new file mode 100644
index 0000000..0e27181
--- /dev/null
+++ b/securis/src/main/java/net/curisit/securis/AppVersion.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
+package net.curisit.securis;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Properties;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * AppVersion
+ * <p>
+ * This class allows to read a version.properties file with information about
+ * application version This properties file is created automatically during
+ * compilation process. The source of this information is the version string
+ * inside pom.xml file. Format version has this format:
+ * {majorVersion}.{minorVersion}.{incrementalVersion}[-{qualifier}]
+ * 
+ * @author cesar 
+ * Last reviewed by JRA on Oct 5, 2025.
+ */
+public class AppVersion {
+
+	private static AppVersion instance;
+	private Properties prop;
+
+	private static final String PROPERTIES_FILE_NAME = "/version.properties";
+	private static final Logger LOG = LogManager.getLogger(AppVersion.class);
+
+	/**
+	 * AppVersion<p>
+	 * Constructor
+	 */
+	private AppVersion() {
+		prop = new Properties();
+		try {
+			prop.load(getClass().getResourceAsStream(PROPERTIES_FILE_NAME));
+		} catch (IOException e) {
+			LOG.error("Version file is missing", e);
+		}
+	}
+
+	/**
+	 * getInstance<p>
+	 * Get unique instance
+	 * 
+	 * @return instance
+	 */
+	public synchronized static AppVersion getInstance() {
+		if (instance == null) {
+			instance = new AppVersion();
+		}
+		return instance;
+	}
+
+	/**
+	 * getMajorVersion<p>
+	 * Returns the major version
+	 * 
+	 * @return majorVersion
+	 */
+	public Integer getMajorVersion() {
+		return getParamAsInt(Keys.MAJOR_VERSION, -1);
+	}
+
+	/**
+	 * getMinorVersion<p>
+	 * Return the minor version
+	 * 
+	 * @return minorVersion
+	 */
+	public Integer getMinorVersion() {
+		return getParamAsInt(Keys.MINOR_VERSION, -1);
+	}
+
+	/**
+	 * getIncrementalVersion<p>
+	 * Returns the incremental version
+	 * 
+	 * @return incrementalVersion
+	 */
+	public Integer getIncrementalVersion() {
+		return getParamAsInt(Keys.INCREMENTAL_VERSION, -1);
+	}
+
+	/**
+	 * getQualifier<p>
+	 * Returns qualifier if it exists
+	 * 
+	 * @return qualifier
+	 */
+	public String getQualifier() {
+		return getParam(Keys.QUALIFIER);
+	}
+
+	/**
+	 * getCompleteVersion<p>
+	 * Return complete version
+	 * 
+	 * @return completeVersion
+	 */
+	public String getCompleteVersion() {
+		String strVersion = MessageFormat.format("{0}.{1}.{2}", getMajorVersion(), getMinorVersion(), getIncrementalVersion());
+		if (getQualifier() != null && !getQualifier().isEmpty())
+			strVersion = strVersion + "-" + getQualifier();
+		return strVersion;
+	}
+
+	/*********************** Private methods *********************/
+
+	/**
+	 * getParam<p>
+	 * Get the parameter associated with the key
+	 * 
+	 * @param key
+	 * @return param
+	 */
+	private String getParam(String key) {
+		return prop.getProperty(key, null);
+	}
+
+	/**
+	 * getParamAsInt<p>
+	 * Get the parameter as integer
+	 * 
+	 * @param key
+	 * @param defaulValue
+	 * @return paramAsInt
+	 */
+	private Integer getParamAsInt(String key, Integer defaulValue) {
+		String value = getParam(key);
+		try {
+			if (value == null) {
+				LOG.error("Wrong version field");
+				return defaulValue;
+			} else
+				return Integer.parseInt(value);
+		} catch (NumberFormatException e) {
+			LOG.error("Wrong version field");
+			return defaulValue;
+		}
+	}
+
+	/**
+	 * Keys<p>
+	 * Application version keys
+	 */
+	private static class Keys {
+		public static final String MAJOR_VERSION = "majorVersion";
+		public static final String MINOR_VERSION = "minorVersion";
+		public static final String INCREMENTAL_VERSION = "incrementalVersion";
+		public static final String QUALIFIER = "qualifier";
+	}
+
+}
diff --git a/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java b/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
index 6605752..014103d 100644
--- a/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
+++ b/securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
@@ -67,8 +67,6 @@
 	HttpServletRequest request;
 	@Context
 	SecurityContext bsc;
-	@Context
-	EntityManager em;
 
 	/**
 	* toResponse
@@ -81,20 +79,42 @@
 		releaseEntityManager();
 		if (e instanceof ForbiddenException) {
 			LOG.warn("ForbiddenException: {}", e.toString());
-			return Response.status(Status.UNAUTHORIZED).header(ERROR_CODE_MESSAGE_HEADER, ErrorCodes.INVALID_CREDENTIALS)
-					.header(ERROR_MESSAGE_HEADER, "Unathorized access to the application").type(MediaType.APPLICATION_JSON).build();
+			return Response.status(Status.UNAUTHORIZED)
+					.header(ERROR_CODE_MESSAGE_HEADER, ErrorCodes.INVALID_CREDENTIALS)
+					.header(ERROR_MESSAGE_HEADER, "Unathorized access to the application")
+					.type(MediaType.APPLICATION_JSON)
+					.build();
 		}
 
 		if (e instanceof SeCurisServiceException) {
 			LOG.warn("SeCurisServiceException: {}", e.toString());
-			return Response.status(DEFAULT_APP_ERROR_STATUS_CODE).header(ERROR_CODE_MESSAGE_HEADER, ((SeCurisServiceException) e).getStatus())
-					.header(ERROR_MESSAGE_HEADER, e.getMessage()).type(MediaType.APPLICATION_JSON).build();
+			return Response.status(DEFAULT_APP_ERROR_STATUS_CODE)
+					.header(ERROR_CODE_MESSAGE_HEADER, ((SeCurisServiceException) e).getStatus())
+					.header(ERROR_MESSAGE_HEADER, e.getMessage())
+					.type(MediaType.APPLICATION_JSON)
+					.build();
 		}
 
+	    String path = request != null ? request.getPathInfo() : null;
+	    Object user = (bsc != null && bsc.getUserPrincipal() != null) ? bsc.getUserPrincipal() : null;
+	    String host = request != null ? request.getRemoteHost() : null;
+	    String ua = request != null ? request.getHeader("User-Agent") : null;
+	    String url = request != null ? String.valueOf(request.getRequestURL()) : null;
+
+	    LOG.error("Unexpected error accessing to '{}' by user: {}", path, user);
+	    LOG.error("Request sent from {}, with User-Agent: {}", host, ua);
+	    LOG.error("Request url: {}", url, e);
+
+	    /**
 		LOG.error("Unexpected error accesing to '{}' by user: {}", request.getPathInfo(), bsc.getUserPrincipal());
 		LOG.error("Request sent from {}, with User-Agent: {}", request.getRemoteHost(), request.getHeader("User-Agent"));
 		LOG.error("Request url: " + request.getRequestURL(), e);
-		return Response.serverError().header(ERROR_MESSAGE_HEADER, "Unexpected error: " + e.toString()).type(MediaType.APPLICATION_JSON).build();
+		*/
+		
+		return Response.serverError()
+				.header(ERROR_MESSAGE_HEADER, "Unexpected error: " + e.toString())
+				.type(MediaType.APPLICATION_JSON)
+				.build();
 	}
 
 	/**
@@ -103,6 +123,8 @@
 	* Best-effort cleanup: rollback active transaction (if joined) and close the {@link EntityManager}.
 	*/
 	private void releaseEntityManager() {
+		
+		/**
 		try {
 			if (em != null && em.isOpen()) {
 				LOG.debug("CLOSING EM: {}, trans: {}", em, em.isJoinedToTransaction());
@@ -116,5 +138,6 @@
 			ex.printStackTrace();
 			LOG.error("Error closing EM: {}, {}", em, ex);
 		}
+		*/
 	}
 }
diff --git a/securis/src/main/java/net/curisit/securis/RestServicesApplication.java b/securis/src/main/java/net/curisit/securis/RestServicesApplication.java
index 9c9f1e7..086e4e9 100644
--- a/securis/src/main/java/net/curisit/securis/RestServicesApplication.java
+++ b/securis/src/main/java/net/curisit/securis/RestServicesApplication.java
@@ -31,7 +31,6 @@
 * @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);
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 2417954..dcc89b9 100644
--- a/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java
+++ b/securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java
@@ -81,18 +81,67 @@
      */
     @Override
     public void filter(ContainerRequestContext requestContext) throws IOException {
-        EntityManager em = emProvider.getEntityManager();
-        LOG.debug("GETTING EM: {}", em);
+    	
+    	Method method = resourceInfo != null ? resourceInfo.getResourceMethod() : null;
+        if (method == null) {
+            LOG.warn("RequestsInterceptor: resource method is null");
+            return;
+        }
 
-        // Store EntityManager for later retrieval (writer interceptor)
-        requestContext.setProperty(EM_CONTEXT_PROPERTY, em);
+        boolean securable = method.isAnnotationPresent(Securable.class);
+        boolean ensureTransaction = method.isAnnotationPresent(EnsureTransaction.class);
 
-        Method method = resourceInfo.getResourceMethod();
+        // Only require injected helpers when the endpoint actually needs them
+        if (securable) {
+            if (tokenHelper == null || cache == null || emProvider == null) {
+                LOG.error(
+                    "RequestsInterceptor is not fully initialized for secured endpoint '{}'. " +
+                    "tokenHelper={}, cache={}, emProvider={}",
+                    method.getName(), tokenHelper, cache, emProvider
+                );
+                requestContext.abortWith(
+                    Response.status(Status.INTERNAL_SERVER_ERROR)
+                            .entity("Security infrastructure not initialized")
+                            .build()
+                );
+                return;
+            }
 
-        if (checkSecurableMethods(requestContext, method)) {
-            if (method.isAnnotationPresent(EnsureTransaction.class)) {
-                LOG.debug("Beginning transaction");
-                em.getTransaction().begin();
+            if (!checkSecurableMethods(requestContext, method)) {
+                return;
+            }
+        }
+
+        // Only open/use EM when needed
+        if (ensureTransaction || securable) {
+            EntityManager em = getEntityManagerSafely();
+            if (em == null) {
+                LOG.error("No EntityManager available for method '{}'", method.getName());
+                requestContext.abortWith(
+                    Response.status(Status.INTERNAL_SERVER_ERROR)
+                            .entity("Persistence infrastructure not initialized")
+                            .build()
+                );
+                return;
+            }
+
+            LOG.debug("GETTING EM: {}", em);
+            requestContext.setProperty(EM_CONTEXT_PROPERTY, em);
+
+            if (ensureTransaction) {
+                try {
+                    if (!em.getTransaction().isActive()) {
+                        LOG.debug("Beginning transaction");
+                        em.getTransaction().begin();
+                    }
+                } catch (Exception e) {
+                    LOG.error("Error beginning transaction for method '{}'", method.getName(), e);
+                    requestContext.abortWith(
+                        Response.status(Status.INTERNAL_SERVER_ERROR)
+                                .entity("Could not begin transaction")
+                                .build()
+                    );
+                }
             }
         }
     }
@@ -107,9 +156,11 @@
     * @return true if request can proceed; false when aborted
     */
     private boolean checkSecurableMethods(ContainerRequestContext ctx, Method method) {
-        if (!method.isAnnotationPresent(Securable.class)) return true;
+        if (!method.isAnnotationPresent(Securable.class)) {
+            return true;
+        }
 
-        String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM);
+        String token = servletRequest != null ? servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM) : null;
         if (token == null || !tokenHelper.isTokenValid(token)) {
             LOG.warn("Access denied, invalid token");
             ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
@@ -126,11 +177,30 @@
             return false;
         }
 
-        BasicSecurityContext sc = new BasicSecurityContext(username, roles, servletRequest.isSecure());
+        boolean secure = servletRequest != null && servletRequest.isSecure();
+        BasicSecurityContext sc = new BasicSecurityContext(username, roles, secure);
         sc.setOrganizationsIds(getUserOrganizations(username));
         sc.setApplicationsIds(getUserApplications(username));
         ctx.setSecurityContext(sc);
         return true;
+    }
+
+    /**
+     * getEntityManagerSafely<p>
+     * Get the entity manager in a safely way
+     * 
+     * @return entityManager
+     */
+    private EntityManager getEntityManagerSafely() {
+        try {
+            if (emProvider == null) {
+                return null;
+            }
+            return emProvider.getEntityManager();
+        } catch (Exception e) {
+            LOG.error("Error obtaining EntityManager from provider", e);
+            return null;
+        }
     }
 
     // -------------------------------------------------------------
@@ -145,18 +215,31 @@
     * @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;
+        if (username == null || cache == null) {
+            return 0;
+        }
 
-        EntityManager em = emProvider.getEntityManager();
+        Integer cached = cache.get("roles_" + username, Integer.class);
+        if (cached != null) {
+            return cached;
+        }
+
+        EntityManager em = getEntityManagerSafely();
+        if (em == null) {
+            LOG.error("Cannot resolve user roles: EntityManager is not available");
+            return 0;
+        }
+
         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;
+            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;
@@ -171,10 +254,22 @@
     * @return userOrganizations
     */
     private Set<Integer> getUserOrganizations(String username) {
-        Set<Integer> cached = cache.getSet("orgs_" + username, Integer.class);
-        if (cached != null) return cached;
+        if (username == null || cache == null) {
+            return Set.of();
+        }
 
-        User user = emProvider.getEntityManager().find(User.class, username);
+        Set<Integer> cached = cache.getSet("orgs_" + username, Integer.class);
+        if (cached != null) {
+            return cached;
+        }
+
+        EntityManager em = getEntityManagerSafely();
+        if (em == null) {
+            LOG.error("Cannot resolve user organizations: EntityManager is not available");
+            return Set.of();
+        }
+
+        User user = em.find(User.class, username);
         if (user != null) {
             Set<Integer> result = user.getAllOrgsIds();
             cache.set("orgs_" + username, result, 3600);
@@ -192,10 +287,22 @@
     * @return userApplications
     */
     private Set<Integer> getUserApplications(String username) {
-        Set<Integer> cached = cache.getSet("apps_" + username, Integer.class);
-        if (cached != null) return cached;
+        if (username == null || cache == null) {
+            return Set.of();
+        }
 
-        User user = emProvider.getEntityManager().find(User.class, username);
+        Set<Integer> cached = cache.getSet("apps_" + username, Integer.class);
+        if (cached != null) {
+            return cached;
+        }
+
+        EntityManager em = getEntityManagerSafely();
+        if (em == null) {
+            LOG.error("Cannot resolve user applications: EntityManager is not available");
+            return Set.of();
+        }
+
+        User user = em.find(User.class, username);
         if (user != null) {
             Set<Integer> result = user.getAllAppsIds();
             cache.set("apps_" + username, result, 3600);
@@ -218,30 +325,45 @@
      */
     @Override
     public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
-        context.proceed();
-
-        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");
-                }
-            }
+            context.proceed();
         } finally {
-            if (em.isOpen()) {
+            EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY);
+            if (em == null) {
+                return;
+            }
+
+            try {
+                if (em.getTransaction() != null && em.getTransaction().isActive()) {
+                    int status = servletResponse != null ? servletResponse.getStatus() : Status.INTERNAL_SERVER_ERROR.getStatusCode();
+                    if (status >= 200 && status < 300) {
+                        em.getTransaction().commit();
+                        LOG.debug("Transaction committed");
+                    } else {
+                        em.getTransaction().rollback();
+                        LOG.debug("Transaction rolled back");
+                    }
+                }
+            } catch (Exception e) {
+                LOG.error("Error finalizing transaction", e);
                 try {
-                    em.close();
+                    if (em.getTransaction() != null && em.getTransaction().isActive()) {
+                        em.getTransaction().rollback();
+                    }
+                } catch (Exception rollbackEx) {
+                    LOG.error("Error rolling back transaction", rollbackEx);
+                }
+            } finally {
+                try {
+                    if (em.isOpen()) {
+                        em.close();
+                    }
                 } catch (Exception e) {
                     LOG.error("Error closing EntityManager", e);
                 }
             }
         }
     }
+
 }
 
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 2c989cc..3b8ce51 100644
--- a/securis/src/main/java/net/curisit/securis/services/BasicServices.java
+++ b/securis/src/main/java/net/curisit/securis/services/BasicServices.java
@@ -28,8 +28,8 @@
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
-import net.curisit.integrity.AppVersion;
-import net.curisit.integrity.commons.Utils;
+import net.curisit.securis.AppVersion;
+import net.curisit.securis.utils.Utils;
 import net.curisit.securis.ioc.EnsureTransaction;
 import net.curisit.securis.security.Securable;
 import net.curisit.securis.utils.TokenHelper;
diff --git a/securis/src/main/java/net/curisit/securis/services/exception/CurisException.java b/securis/src/main/java/net/curisit/securis/services/exception/CurisException.java
new file mode 100644
index 0000000..caaf311
--- /dev/null
+++ b/securis/src/main/java/net/curisit/securis/services/exception/CurisException.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright © 2015 CurisIT, S.L. All Rights Reserved.
+ */
+package net.curisit.securis.services.exception;
+
+/**
+ * CurisException<p>
+ * This class manages the standard (before or after computation) error on computation that implies that the problem is reported
+ * and the computation can continue.
+ *
+ * @author JRA
+ * Last reviewed by APB on April 05, 2022.
+ */
+public class CurisException extends Exception {
+
+	private static final long serialVersionUID = 3830386897219028662L;
+
+	// i18 code for message localization
+	String i18key = null;
+
+	/**
+	 * CurisException<p>
+	 * This method is used to manage the standard exception with the message.
+	 *
+	 * @param msg
+	 * 		The exception message
+	 */
+	public CurisException(String msg) {
+		super(msg);
+	}
+
+	/**
+	 * CurisException<p>
+	 * This method is used to manage the standard exception with the message and the cause.
+	 *
+	 * @param msg
+	 * 		The exception message
+	 * @param cause
+	 * 		The error cause
+	 */
+	public CurisException(String msg, Throwable cause) {
+		super(msg, cause);
+	}
+
+	/**
+	 * CurisException<p>
+	 * This method is used to manage the standard exception with the message and the cause.
+	 *
+	 * @param msg
+	 * 		The exception message
+	 * @param cause
+	 * 		The error cause
+	 */
+	public CurisException(String msg, String i18k) {
+		this(msg);
+		this.i18key = i18k;
+	}
+
+	/**
+	 * CurisException<p>
+	 * This method is used to manage the standard exception with the message, the i18 code and the cause.
+	 *
+	 * @param msg
+	 * 		The exception message
+	 * @param i18k
+	 * 		The code for localization
+	 * @param cause
+	 * 		The error cause
+	 */
+	public CurisException(String msg, String i18k, Throwable cause) {
+		this(msg, cause);
+		this.i18key = i18k;
+	}
+
+	/**
+	 * geti18key<p>
+	 * This method is used to get the i18 code for localization.
+	 *
+	 * @return i18key
+	 * 		The i18 code
+	 */
+	public String geti18key() {
+		return i18key;
+	}
+
+}
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 e44e944..5dde215 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
@@ -3,7 +3,7 @@
  */
 package net.curisit.securis.services.exception;
 
-import net.curisit.integrity.exception.CurisException;
+
 
 /**
  * SeCurisServiceException
diff --git a/securis/src/main/java/net/curisit/securis/utils/Utils.java b/securis/src/main/java/net/curisit/securis/utils/Utils.java
new file mode 100644
index 0000000..b6fac36
--- /dev/null
+++ b/securis/src/main/java/net/curisit/securis/utils/Utils.java
@@ -0,0 +1,719 @@
+/*
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
+ */
+package net.curisit.securis.utils;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.InvocationTargetException;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.commons.beanutils.BeanUtils;
+import org.apache.commons.beanutils.PropertyUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import net.curisit.integrity.exception.CurisRuntimeException;
+
+/**
+ * Utils
+ * <p>
+ * General-purpose static utilities: hashing, dates/times, collections, I/O helpers,
+ * simple email validation, reflection helpers, file tailing, etc.
+ *
+ * Author: JRA
+ * Last reviewed by JRA on Oct 6, 2025.
+ */
+public class Utils {
+
+	private static final Logger LOG = LogManager.getLogger(Utils.class);
+
+	/* --------------------- Hash functions --------------------- */
+
+	/**
+	 * md5<p>
+	 * Returns MD5 digest (hex, 32 chars) for the input text.
+	 *
+	 * @param str source text
+	 * @return lowercase hex MD5 or null on error
+	 */
+	public static String md5(String str) {
+		try {
+			MessageDigest mDigest = MessageDigest.getInstance("MD5");
+			mDigest.update(str.getBytes(), 0, str.length());
+			BigInteger i = new BigInteger(1, mDigest.digest());
+			return String.format("%1$032x", i);
+		} catch (NoSuchAlgorithmException e) {
+			LOG.error("Error generating MD5 for string: " + str, e);
+		}
+		return null;
+	}
+
+	/**
+	 * sha1<p>
+	 * Returns SHA-1 digest (hex, 40 chars) for the input text.
+	 *
+	 * @param str source text
+	 * @return lowercase hex SHA-1 or null on error
+	 */
+	public static String sha1(String str) {
+		try {
+			MessageDigest mDigest = MessageDigest.getInstance("SHA1");
+			mDigest.update(str.getBytes(), 0, str.length());
+			BigInteger i = new BigInteger(1, mDigest.digest());
+			return String.format("%1$040x", i);
+		} catch (NoSuchAlgorithmException e) {
+			LOG.error("Error generating SHA1 for string: " + str, e);
+		}
+		return null;
+	}
+
+	/**
+	 * sha256<p>
+	 * Returns SHA-256 digest (hex, 64 chars) for the input text.
+	 *
+	 * @param str source text
+	 * @return lowercase hex SHA-256 or null on error
+	 */
+	public static String sha256(String str) {
+		return sha256(str.getBytes());
+	}
+
+	/**
+	 * sha256<p>
+	 * Returns SHA-256 digest (hex) for a byte array.
+	 *
+	 * @param bytes data
+	 * @return hex SHA-256 or null on error
+	 */
+	public static String sha256(byte[] bytes) {
+		try {
+			MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
+			mDigest.update(bytes, 0, bytes.length);
+			BigInteger i = new BigInteger(1, mDigest.digest());
+			return String.format("%1$064x", i);
+		} catch (NoSuchAlgorithmException e) {
+			LOG.error("Error generating SHA-256 for bytes: " + bytes, e);
+		}
+		return null;
+	}
+
+	/**
+	 * sha256<p>
+	 * Incrementally updates SHA-256 with multiple byte arrays and returns hex digest.
+	 *
+	 * @param bytes multiple byte chunks
+	 * @return hex SHA-256 or null on error
+	 */
+	public static String sha256(byte[]... bytes) {
+		try {
+			MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
+			for (byte[] bs : bytes) {
+				mDigest.update(bs, 0, bs.length);
+			}
+			BigInteger i = new BigInteger(1, mDigest.digest());
+			return String.format("%1$064x", i);
+		} catch (NoSuchAlgorithmException e) {
+			LOG.error("Error generating SHA-256 for bytes: " + bytes, e);
+		}
+		return null;
+	}
+	
+	/* --------------------- ISO date helpers --------------------- */
+
+	private static final String ISO_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
+	private static final SimpleDateFormat sdf = new SimpleDateFormat(ISO_PATTERN);
+	static {
+		sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
+	}
+
+	/**
+	 * toIsoFormat<p>
+	 * Formats a Date to ISO-8601-like string with milliseconds in GMT.
+	 *
+	 * @param date input date
+	 * @return formatted string
+	 */
+	public static synchronized String toIsoFormat(Date date) {
+		return sdf.format(date);
+	}	
+
+	private static final String ISO_PATTERN_REDUCED_PRECISION = "yyyy-MM-dd'T'HH:mm:ssZ";
+	private static final SimpleDateFormat sdfReduced = new SimpleDateFormat(ISO_PATTERN_REDUCED_PRECISION);
+	static {
+		sdfReduced.setTimeZone(TimeZone.getTimeZone("GMT"));
+	}
+
+	/**
+	 * toIsoFormatReduced<p>
+	 * Formats a Date to ISO string without milliseconds in GMT.
+	 *
+	 * @param date input date
+	 * @return formatted string
+	 */
+	public static synchronized String toIsoFormatReduced(Date date) {
+		return sdfReduced.format(date);
+	}
+
+	/**
+	 * toDateFromIso<p>
+	 * Parses a string in {@link #ISO_PATTERN} into a Date (GMT).
+	 *
+	 * @param dateStr string to parse
+	 * @return Date or null if parsing fails
+	 */
+	public static synchronized Date toDateFromIso(String dateStr) {
+		try {
+			return sdf.parse(dateStr);
+		} catch (ParseException e) {
+			LOG.error("Error parsing string '{}' to Date object with pattern: {}", dateStr, ISO_PATTERN);
+			return null;
+		}
+	}
+
+	/* --------------------- Null-safe helpers --------------------- */
+
+	/**
+	 * nonull<p>
+	 * Returns {@code value} cast to T, or {@code defaultValue} if null.
+	 *
+	 * @param value input
+	 * @param defaultValue fallback
+	 * @return value or default
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> T nonull(Object value, T defaultValue) {
+		return value == null ? defaultValue : (T) value;
+	}
+
+	/**
+	 * nonull<p>
+	 * Returns String value or empty string if null.
+	 *
+	 * @param value input
+	 * @return non-null string
+	 */
+	public static String nonull(Object value) {
+		return nonull(value, "");
+	}
+
+	/**
+	 * value<p>
+	 * Unchecked cast helper (avoid using unless necessary).
+	 *
+	 * @param value object
+	 * @param type  target type
+	 * @return casted value
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> T value(Object value, Class<T> type) {
+		return (T) value;
+	}
+
+	/**
+	 * createMap<p>
+	 * Convenience to create a Map from varargs key/value pairs.
+	 *
+	 * @param keyValue alternating key, value
+	 * @return map with pairs
+	 */
+	@SuppressWarnings("unchecked")
+	public static <K extends Object, V extends Object> Map<K, V> createMap(Object... keyValue) {
+		Map<K, V> map = (Map<K, V>) new HashMap<Object, Object>();
+		for (int i = 0; i < keyValue.length; i += 2) {
+			((Map<Object, Object>) map).put(keyValue[i], keyValue[i + 1]);
+		}
+		return map;
+	}
+
+	/* --------------------- Date arithmetic --------------------- */
+
+	/**
+	 * addDays<p>
+	 * Adds a number of days to a Date.
+	 *
+	 * @param date base date
+	 * @param days positive/negative days
+	 * @return new Date
+	 */
+	public static Date addDays(Date date, int days) {
+		Calendar cal = Calendar.getInstance();
+		cal.setTime(date);
+		cal.add(Calendar.DATE, days);
+		return cal.getTime();
+	}
+
+	/**
+	 * addHours<p>
+	 * Adds hours to a Date.
+	 *
+	 * @param date base date
+	 * @param hours number of hours
+	 * @return new Date
+	 */
+	public static Date addHours(Date date, int hours) {
+		Calendar cal = Calendar.getInstance();
+		cal.setTime(date);
+		cal.add(Calendar.HOUR, hours);
+		return cal.getTime();
+	}
+
+	/**
+	 * addMinutes<p>
+	 * Adds minutes to a Date.
+	 *
+	 * @param date base date
+	 * @param minutes number of minutes
+	 * @return new Date
+	 */
+	public static Date addMinutes(Date date, int minutes) {
+		Calendar cal = Calendar.getInstance();
+		cal.setTime(date);
+		cal.add(Calendar.MINUTE, minutes);
+		return cal.getTime();
+	}
+
+	/**
+	 * getStrTime<p>
+	 * Formats seconds as H:MM:SS (H omitted if 0). Negative yields "??".
+	 *
+	 * @param seconds total seconds
+	 * @return formatted time
+	 */
+	public static String getStrTime(int seconds) {
+		if (seconds < 0)
+			return "??";
+		String secs = String.format("%02d", seconds % 60);
+		String hours = "";
+		String mins = "0:";
+		if (seconds > 3600) {
+			hours = String.format("%d:", seconds / 3600);
+		}
+		if (seconds > 60) {
+			mins = String.format("%02d:", (seconds / 60) % 60);
+		}
+		return hours + mins + secs;
+	}
+
+	/* --------------------- Collections & conversions --------------------- */
+
+	/**
+	 * areAllEmpty<p>
+	 * Checks if all provided collections are empty.
+	 *
+	 * @param lists vararg of collections
+	 * @return true if all empty
+	 */
+	public static boolean areAllEmpty(Collection<?>... lists) {
+		for (Collection<?> list : lists) {
+			if (!list.isEmpty())
+				return false;
+		}
+		return true;
+	}
+
+	/**
+	 * toDateFromLocalDate<p>
+	 * Converts a {@link LocalDate} to {@link Date} at system default zone start of day.
+	 *
+	 * @param ld local date
+	 * @return Date or null
+	 */
+	public static Date toDateFromLocalDate(LocalDate ld) {
+		if (ld == null)
+			return null;
+		Instant instant = ld.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
+		return Date.from(instant);
+	}
+
+	/**
+	 * toLocalDateFromDate<p>
+	 * Converts a {@link Date} to {@link LocalDate} at system default zone.
+	 *
+	 * @param date Date
+	 * @return LocalDate or null
+	 */
+	public static LocalDate toLocalDateFromDate(Date date) {
+		if (date == null)
+			return null;
+		Instant instant = Instant.ofEpochMilli(date.getTime());
+		return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).toLocalDate();
+	}
+
+	/**
+	 * compare<p>
+	 * Null-safe equals: both null → true; otherwise {@code equals()}.
+	 *
+	 * @param obj1 first object
+	 * @param obj2 second object
+	 * @return equality result
+	 */
+	public static boolean compare(Object obj1, Object obj2) {
+		if (obj1 == null) {
+			return obj2 == null;
+		} else {
+			return obj1.equals(obj2);
+		}
+	}
+
+	/* --------------------- Email validation --------------------- */
+
+	private static final String EMAIL_PATTERN_STR = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
+	private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_PATTERN_STR);
+
+	/**
+	 * isValidEmail<p>
+	 * Validates an email address format with a basic regex.
+	 *
+	 * @param email candidate value
+	 * @return valid or not
+	 */
+	public static boolean isValidEmail(final String email) {
+		if (email == null) {
+			return false;
+		}
+		return EMAIL_PATTERN.matcher(email).matches();
+	}
+
+	/* --------------------- Iterable helpers --------------------- */
+
+	/**
+	 * Reversed<p>
+	 * Iterable wrapper to iterate a List in reverse order without copying.
+	 *
+	 * @param <T> element type
+	 */
+	public static class Reversed<T> implements Iterable<T> {
+		private final List<T> original;
+
+		public Reversed(List<T> original) {
+			this.original = original;
+		}
+
+		public Iterator<T> iterator() {
+			return new Iterator<T>() {
+				int cursor = original.size();
+				public boolean hasNext() { return cursor > 0; }
+				public T next() { return original.get(--cursor); }
+				public void remove() { throw new CurisRuntimeException("ERROR removing item in read-only iterator"); }
+			};
+		}
+	}
+
+	/**
+	 * reversedList<p>
+	 * Returns a reversed-iteration view of a list.
+	 *
+	 * @param original list to view
+	 * @param <T> element type
+	 * @return iterable reverse view
+	 */
+	public static <T> Reversed<T> reversedList(List<T> original) {
+		return new Reversed<T>(original);
+	}
+
+	/**
+	 * isSorted<p>
+	 * Checks if a list of Doubles is non-decreasing.
+	 *
+	 * @param original list
+	 * @return true if sorted ascending
+	 */
+	public static boolean isSorted(List<Double> original) {
+		for (int i = 0; i < (original.size() - 1); i++) {
+			Double v0 = original.get(i);
+			Double v1 = original.get(i + 1);
+			if (v0.doubleValue() > v1.doubleValue()) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	/* --------------------- Simple file reading helpers --------------------- */
+
+	/**
+	 * readFileLines<p>
+	 * Reads a file as UTF-8 and splits each line by a separator.
+	 *
+	 * @param file source file
+	 * @param separator regex separator
+	 * @return list of line fields
+	 * @throws IOException on I/O errors
+	 */
+	public static List<List<String>> readFileLines(File file, final String separator) throws IOException {
+		List<String> lines = Files.readAllLines(Paths.get(file.toURI()), Charset.forName("utf8"));
+		return lines.stream().map(line -> Arrays.asList(line.split(separator))).collect(Collectors.toList());
+	}
+
+	/**
+	 * readStreamLines<p>
+	 * Reads an InputStream as UTF-8 and splits each line by a separator.
+	 *
+	 * @param stream input stream (caller closes it)
+	 * @param separator regex separator
+	 * @return list of line fields
+	 * @throws IOException on I/O errors
+	 */
+	public static List<List<String>> readStreamLines(InputStream stream, final String separator) throws IOException {
+		try (
+				InputStreamReader isr = new InputStreamReader(stream, Charset.forName("UTF-8"));
+				BufferedReader br = new BufferedReader(isr);
+				) {
+			String line;
+			List<List<String>> lines = new ArrayList<>(); 
+			while ((line = br.readLine()) != null) {
+				lines.add(Arrays.asList(line.split(separator))); 
+			}
+			return lines;
+		}
+	}
+
+	/**
+	 * deleteDirectoryTree<p>
+	 * Recursively deletes a directory and its contents.
+	 *
+	 * @param baseDirectory directory path
+	 * @throws IOException on failure
+	 */
+	public static void deleteDirectoryTree(Path baseDirectory) throws IOException {
+		Files.walkFileTree(baseDirectory, new SimpleFileVisitor<Path>() {
+			@Override
+			public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+				Files.deleteIfExists(file);
+				return FileVisitResult.CONTINUE;
+			}
+			@Override
+			public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+				Files.deleteIfExists(dir);
+				return FileVisitResult.CONTINUE;
+			}
+		});
+	}
+
+	/* --------------------- Directory size & tail --------------------- */
+
+	private static class LongWrapper { long num = 0L; }
+
+	/**
+	 * getDirectoryContentSize<p>
+	 * Computes total size (bytes) of files within a directory tree.
+	 *
+	 * @param baseDirectory root path
+	 * @return total bytes
+	 * @throws IOException on traversal errors
+	 */
+	public static long getDirectoryContentSize(Path baseDirectory) throws IOException {
+		final LongWrapper size = new LongWrapper();
+		Files.walkFileTree(baseDirectory, new SimpleFileVisitor<Path>() {
+			@Override
+			public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+				size.num += file.toFile().length();
+				return FileVisitResult.CONTINUE;
+			}
+			@Override
+			public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+				if (!dir.equals(baseDirectory)) {
+					size.num += dir.toFile().length();
+				}
+				return FileVisitResult.CONTINUE;
+			}
+		});
+		return size.num;
+	}
+
+	/**
+	 * waitFor<p>
+	 * Asynchronously waits until a condition is true or a timeout is reached.
+	 *
+	 * @param condition supplier that should eventually return true
+	 * @param timeout max wait (ms)
+	 * @return future completing with true if condition met
+	 * @throws TimeoutException surfaced through CompletionException if timed out
+	 */
+	public static CompletableFuture<Boolean> waitFor(Supplier<Boolean> condition, long timeout) throws TimeoutException {
+		return CompletableFuture.<Boolean>supplyAsync(() -> {
+			long t0 = System.currentTimeMillis();
+			while(!condition.get()) {
+				long t1 = System.currentTimeMillis();
+				if ((t1 - t0) > timeout) {
+					throw new CompletionException(new TimeoutException()); 
+				}
+				try {
+					Thread.sleep(100);
+				} catch (InterruptedException e) {
+					throw new CompletionException(e); 
+				}
+			}
+			return true;
+		});
+	}
+
+	private static final int BLOCK_SIZE = 4096; // 4KB
+
+	/**
+	 * getLastLines<p>
+	 * Efficiently reads the last N lines of a text file by scanning from the end in blocks.
+	 *
+	 * @param file file to read
+	 * @param numLines number of lines from the end
+	 * @return list of lines in natural order (oldest→newest among the last N)
+	 * @throws IOException on I/O errors
+	 */
+	public static List<String> getLastLines(File file, int numLines) throws IOException {
+		LinkedList<String> lines = new LinkedList<>();
+		long fileSize = file.length();
+		int chunk = (fileSize < BLOCK_SIZE) ? (int)fileSize : BLOCK_SIZE;
+		long seekPos = fileSize - chunk;
+		FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.READ);
+		final byte newLine = (byte)'\n';
+		String partialLine = null;
+		while (lines.size() < numLines) {
+			ByteBuffer dst = ByteBuffer.allocate(chunk);
+			int bytesRead = fc.read(dst, seekPos);
+			byte[] content = dst.array();
+			int lastLineIdx = bytesRead;  
+			for (int i = bytesRead - 1; i > 0 ; i--) {
+				byte b = content[i];
+				if (b == newLine) {
+					if (i < (bytesRead - 1) || partialLine != null) {
+						String line = new String(Arrays.copyOfRange(content, i, lastLineIdx)).trim();
+						if (partialLine != null) {
+							line += partialLine;
+							partialLine = null;
+						}
+						lines.addFirst(line);
+						if (lines.size() == numLines) {
+							return lines;
+						}
+					}
+					lastLineIdx = i;
+				}
+			}
+			if (lastLineIdx > 0) {
+				if (partialLine == null) {
+					partialLine = new String(Arrays.copyOfRange(content, 0, lastLineIdx)).trim();
+				} else {
+					partialLine = new String(Arrays.copyOfRange(content, 0, lastLineIdx)) + partialLine;
+				}
+			}
+			if (seekPos == 0) {
+				lines.addFirst(partialLine);
+				break;
+			}
+			if (seekPos < BLOCK_SIZE) {
+				chunk = (int)seekPos;
+				seekPos = 0;				
+			} else {
+				seekPos -= BLOCK_SIZE;
+			}
+		}
+		return lines;
+	}
+
+	/**
+	 * removeDuplicates<p>
+	 * Returns a sorted list without duplicates using a set roundtrip.
+	 *
+	 * @param list input list
+	 * @param <T> type must be Comparable
+	 * @return sorted distinct list
+	 */
+	public static <T> List<T> removeDuplicates(List<T> list) {
+		return new HashSet<>(list).stream().sorted().collect(Collectors.toList());		
+	}
+
+	/* --------------------- Reflection helpers --------------------- */
+
+	/**
+	 * setProperty<p>
+	 * Sets a bean property by name using Apache BeanUtils PropertyUtils.
+	 *
+	 * @param bean target bean
+	 * @param fieldName property name (nested supported)
+	 * @param value value to assign
+	 * @throws RuntimeException wrapping reflection errors
+	 */
+	public static void setProperty(Object bean, String fieldName, Object value) {
+		try {
+			PropertyUtils.setProperty(bean, fieldName, value);
+		} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+			LOG.error("Missing field:" + fieldName);
+			throw new RuntimeException(e);
+		}
+	}
+
+	/**
+	 * getProperty<p>
+	 * Reads a bean property by name using Apache PropertyUtils.
+	 *
+	 * @param bean target bean
+	 * @param fieldName property name
+	 * @return value
+	 * @throws RuntimeException wrapping reflection errors
+	 */
+	public static Object getProperty(Object bean, String fieldName) {
+		try {
+			return PropertyUtils.getProperty(bean, fieldName);
+		} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
+			LOG.error("Missing field:" + fieldName);
+			throw new RuntimeException(e);
+		}
+	}
+
+	/**
+	 * cloneBean<p>
+	 * Clones a bean using Apache BeanUtils' copy mechanisms (shallow copy).
+	 *
+	 * @param bean source bean
+	 * @return new cloned bean
+	 * @throws CurisRuntimeException on cloning errors
+	 */
+	public static Object cloneBean(Object bean) {
+		try {
+			return BeanUtils.cloneBean(bean);
+		} catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
+			throw new CurisRuntimeException("Error cloning bean", e);
+		}	
+	}
+}
+
diff --git a/securis/src/main/webapp/WEB-INF/web.xml b/securis/src/main/webapp/WEB-INF/web.xml
index a833683..1b5d120 100644
--- a/securis/src/main/webapp/WEB-INF/web.xml
+++ b/securis/src/main/webapp/WEB-INF/web.xml
@@ -36,11 +36,13 @@
         <param-name>resteasy.providers</param-name>
         <param-value>net.curisit.securis.DefaultExceptionHandler</param-value>
     </context-param>
+    <!-- 
 	<context-param>
 		<param-name>resteasy.injector.factory</param-name>
 		<param-value>org.jboss.resteasy.cdi.CdiInjectorFactory</param-value>
 	</context-param>
-
+     -->
+     
 	<filter>
         <filter-name>DevFilter</filter-name>
         <filter-class>
@@ -49,7 +51,7 @@
     </filter>    
     <filter-mapping>
         <filter-name>DevFilter</filter-name>
-        <url-pattern>/*</url-pattern>
+        <url-pattern>/api/*</url-pattern>
     </filter-mapping>
 
 	<filter>
@@ -63,49 +65,50 @@
         <url-pattern>/main-bundle.js</url-pattern>
     </filter-mapping>
 
-	<filter>
-        <filter-name>Resteasy</filter-name>
-        <filter-class>
-            org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
-        </filter-class>
-        <init-param>
-            <param-name>jakarta.ws.rs.Application</param-name>
-            <param-value>net.curisit.securis.RestServicesApplication</param-value>
-        </init-param>
-    </filter>
-    
-    <filter-mapping>
-        <filter-name>Resteasy</filter-name>
-        <url-pattern>/*</url-pattern>
-    </filter-mapping>
+	<servlet>
+	    <servlet-name>Resteasy</servlet-name>
+	    <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
+	    <init-param>
+	        <param-name>jakarta.ws.rs.Application</param-name>
+	        <param-value>net.curisit.securis.RestServicesApplication</param-value>
+	    </init-param>
+	    <load-on-startup>1</load-on-startup>
+	</servlet>
+	
+	<servlet-mapping>
+	    <servlet-name>Resteasy</servlet-name>
+	    <url-pattern>/api/*</url-pattern>
+	</servlet-mapping>
 
 	<welcome-file-list>
-		<welcome-file>/index.jsp</welcome-file>
+	    <welcome-file>index.jsp</welcome-file>
 	</welcome-file-list>
 
 
-  <!-- Security roles referenced by this web application -->
-  <security-role>
-    <description>
-      Advance users, customers 
-    </description>
-    <role-name>advance</role-name>
-  </security-role>
-  <security-role>
-    <description>
-      Administrator role
-    </description>
-    <role-name>admin</role-name>
-  </security-role>
+    <!-- Security roles referenced by this web application -->
+    <security-role>
+       <description>
+          Advance users, customers 
+       </description>
+       <role-name>advance</role-name>
+    </security-role>
+    <security-role>
+       <description>
+          Administrator role
+       </description>
+       <role-name>admin</role-name>
+    </security-role>
 
-<resource-env-ref>
-    <resource-env-ref-name>SeCurisDS</resource-env-ref-name>
-    <resource-env-ref-type>jakarta.sql.DataSource</resource-env-ref-type>
-</resource-env-ref>
-
-<resource-env-ref>
-   <resource-env-ref-name>BeanManager</resource-env-ref-name>
-   <resource-env-ref-type>jakarta.enterprise.inject.spi.BeanManager</resource-env-ref-type>
-</resource-env-ref>
+	<resource-env-ref>
+	    <resource-env-ref-name>SeCurisDS</resource-env-ref-name>
+	    <resource-env-ref-type>jakarta.sql.DataSource</resource-env-ref-type>
+	</resource-env-ref>
+	
+	<!-- 
+	<resource-env-ref>
+	   <resource-env-ref-name>BeanManager</resource-env-ref-name>
+	   <resource-env-ref-type>jakarta.enterprise.inject.spi.BeanManager</resource-env-ref-type>
+	</resource-env-ref>
+	 -->
 
 </web-app>
\ No newline at end of file

--
Gitblit v1.3.2