Joaquín Reñé
2026-03-27 4ee50e257b32f6ec0f72907305d1f2b1212808a4
#4479 - upgrade SecurisServer to Java 21
3 files added
7 files modified
changed files
securis/pom.xml patch | view | blame | history
securis/src/main/java/net/curisit/securis/AppVersion.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/RestServicesApplication.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/services/BasicServices.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/services/exception/CurisException.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java patch | view | blame | history
securis/src/main/java/net/curisit/securis/utils/Utils.java patch | view | blame | history
securis/src/main/webapp/WEB-INF/web.xml patch | view | blame | history
securis/pom.xml
....@@ -5,7 +5,8 @@
55 <modelVersion>4.0.0</modelVersion>
66 <groupId>net.curisit</groupId>
77 <artifactId>securis-server</artifactId>
8
- <version>2.0.2</version>
8
+ <version>3.0.0</version>
9
+ <packaging>war</packaging>
910 <name>SeCuris-Server</name>
1011
1112 <properties>
....@@ -14,11 +15,13 @@
1415 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1516 <maven.compiler.source>21</maven.compiler.source>
1617 <maven.compiler.target>21</maven.compiler.target>
18
+
1719 <resteasy.version>6.2.4.Final</resteasy.version>
1820 <hibernate.version>5.6.15.Final</hibernate.version>
1921 <jakarta.persistence.version>3.1.0</jakarta.persistence.version>
20
- <jakarta.servlet.version>6.0.0</jakarta.servlet.version>
22
+ <jakarta.servlet.version>6.1.0</jakarta.servlet.version>
2123 <jakarta.cdi.version>4.0.1</jakarta.cdi.version>
24
+ <beanutils.version>1.9.4</beanutils.version>
2225 <log4j.version>2.18.0</log4j.version>
2326 </properties>
2427
....@@ -36,12 +39,7 @@
3639 <artifactId>resteasy-core</artifactId>
3740 <version>${resteasy.version}</version>
3841 </dependency>
39
- <dependency>
40
- <groupId>org.jboss.resteasy</groupId>
41
- <artifactId>resteasy-servlet-initializer</artifactId>
42
- <version>${resteasy.version}</version>
43
- </dependency>
44
- <dependency>
42
+ <dependency>
4543 <groupId>org.jboss.resteasy</groupId>
4644 <artifactId>resteasy-multipart-provider</artifactId>
4745 <version>${resteasy.version}</version>
....@@ -83,6 +81,13 @@
8381 </dependency>
8482
8583 <!-- Hibernate 5 compatible con Jakarta Persistence -->
84
+ <!--
85
+ <dependency>
86
+ <groupId>org.hibernate</groupId>
87
+ <artifactId>hibernate-core</artifactId>
88
+ <version>${hibernate.version}</version>
89
+ </dependency>
90
+ -->
8691 <dependency>
8792 <groupId>org.hibernate</groupId>
8893 <artifactId>hibernate-core</artifactId>
....@@ -111,6 +116,14 @@
111116 <artifactId>guice</artifactId>
112117 <version>5.1.0</version>
113118 </dependency>
119
+
120
+ <!-- Bean utils -->
121
+ <dependency>
122
+ <groupId>commons-beanutils</groupId>
123
+ <artifactId>commons-beanutils</artifactId>
124
+ <version>${beanutils.version}</version>
125
+ </dependency>
126
+
114127
115128 </dependencies>
116129
securis/src/main/java/net/curisit/securis/AppVersion.java
....@@ -0,0 +1,158 @@
1
+/*
2
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
3
+ */
4
+package net.curisit.securis;
5
+
6
+import java.io.IOException;
7
+import java.text.MessageFormat;
8
+import java.util.Properties;
9
+
10
+import org.apache.logging.log4j.LogManager;
11
+import org.apache.logging.log4j.Logger;
12
+
13
+/**
14
+ * AppVersion
15
+ * <p>
16
+ * This class allows to read a version.properties file with information about
17
+ * application version This properties file is created automatically during
18
+ * compilation process. The source of this information is the version string
19
+ * inside pom.xml file. Format version has this format:
20
+ * {majorVersion}.{minorVersion}.{incrementalVersion}[-{qualifier}]
21
+ *
22
+ * @author cesar
23
+ * Last reviewed by JRA on Oct 5, 2025.
24
+ */
25
+public class AppVersion {
26
+
27
+ private static AppVersion instance;
28
+ private Properties prop;
29
+
30
+ private static final String PROPERTIES_FILE_NAME = "/version.properties";
31
+ private static final Logger LOG = LogManager.getLogger(AppVersion.class);
32
+
33
+ /**
34
+ * AppVersion<p>
35
+ * Constructor
36
+ */
37
+ private AppVersion() {
38
+ prop = new Properties();
39
+ try {
40
+ prop.load(getClass().getResourceAsStream(PROPERTIES_FILE_NAME));
41
+ } catch (IOException e) {
42
+ LOG.error("Version file is missing", e);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * getInstance<p>
48
+ * Get unique instance
49
+ *
50
+ * @return instance
51
+ */
52
+ public synchronized static AppVersion getInstance() {
53
+ if (instance == null) {
54
+ instance = new AppVersion();
55
+ }
56
+ return instance;
57
+ }
58
+
59
+ /**
60
+ * getMajorVersion<p>
61
+ * Returns the major version
62
+ *
63
+ * @return majorVersion
64
+ */
65
+ public Integer getMajorVersion() {
66
+ return getParamAsInt(Keys.MAJOR_VERSION, -1);
67
+ }
68
+
69
+ /**
70
+ * getMinorVersion<p>
71
+ * Return the minor version
72
+ *
73
+ * @return minorVersion
74
+ */
75
+ public Integer getMinorVersion() {
76
+ return getParamAsInt(Keys.MINOR_VERSION, -1);
77
+ }
78
+
79
+ /**
80
+ * getIncrementalVersion<p>
81
+ * Returns the incremental version
82
+ *
83
+ * @return incrementalVersion
84
+ */
85
+ public Integer getIncrementalVersion() {
86
+ return getParamAsInt(Keys.INCREMENTAL_VERSION, -1);
87
+ }
88
+
89
+ /**
90
+ * getQualifier<p>
91
+ * Returns qualifier if it exists
92
+ *
93
+ * @return qualifier
94
+ */
95
+ public String getQualifier() {
96
+ return getParam(Keys.QUALIFIER);
97
+ }
98
+
99
+ /**
100
+ * getCompleteVersion<p>
101
+ * Return complete version
102
+ *
103
+ * @return completeVersion
104
+ */
105
+ public String getCompleteVersion() {
106
+ String strVersion = MessageFormat.format("{0}.{1}.{2}", getMajorVersion(), getMinorVersion(), getIncrementalVersion());
107
+ if (getQualifier() != null && !getQualifier().isEmpty())
108
+ strVersion = strVersion + "-" + getQualifier();
109
+ return strVersion;
110
+ }
111
+
112
+ /*********************** Private methods *********************/
113
+
114
+ /**
115
+ * getParam<p>
116
+ * Get the parameter associated with the key
117
+ *
118
+ * @param key
119
+ * @return param
120
+ */
121
+ private String getParam(String key) {
122
+ return prop.getProperty(key, null);
123
+ }
124
+
125
+ /**
126
+ * getParamAsInt<p>
127
+ * Get the parameter as integer
128
+ *
129
+ * @param key
130
+ * @param defaulValue
131
+ * @return paramAsInt
132
+ */
133
+ private Integer getParamAsInt(String key, Integer defaulValue) {
134
+ String value = getParam(key);
135
+ try {
136
+ if (value == null) {
137
+ LOG.error("Wrong version field");
138
+ return defaulValue;
139
+ } else
140
+ return Integer.parseInt(value);
141
+ } catch (NumberFormatException e) {
142
+ LOG.error("Wrong version field");
143
+ return defaulValue;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Keys<p>
149
+ * Application version keys
150
+ */
151
+ private static class Keys {
152
+ public static final String MAJOR_VERSION = "majorVersion";
153
+ public static final String MINOR_VERSION = "minorVersion";
154
+ public static final String INCREMENTAL_VERSION = "incrementalVersion";
155
+ public static final String QUALIFIER = "qualifier";
156
+ }
157
+
158
+}
securis/src/main/java/net/curisit/securis/DefaultExceptionHandler.java
....@@ -67,8 +67,6 @@
6767 HttpServletRequest request;
6868 @Context
6969 SecurityContext bsc;
70
- @Context
71
- EntityManager em;
7270
7371 /**
7472 * toResponse
....@@ -81,20 +79,42 @@
8179 releaseEntityManager();
8280 if (e instanceof ForbiddenException) {
8381 LOG.warn("ForbiddenException: {}", e.toString());
84
- return Response.status(Status.UNAUTHORIZED).header(ERROR_CODE_MESSAGE_HEADER, ErrorCodes.INVALID_CREDENTIALS)
85
- .header(ERROR_MESSAGE_HEADER, "Unathorized access to the application").type(MediaType.APPLICATION_JSON).build();
82
+ return Response.status(Status.UNAUTHORIZED)
83
+ .header(ERROR_CODE_MESSAGE_HEADER, ErrorCodes.INVALID_CREDENTIALS)
84
+ .header(ERROR_MESSAGE_HEADER, "Unathorized access to the application")
85
+ .type(MediaType.APPLICATION_JSON)
86
+ .build();
8687 }
8788
8889 if (e instanceof SeCurisServiceException) {
8990 LOG.warn("SeCurisServiceException: {}", e.toString());
90
- return Response.status(DEFAULT_APP_ERROR_STATUS_CODE).header(ERROR_CODE_MESSAGE_HEADER, ((SeCurisServiceException) e).getStatus())
91
- .header(ERROR_MESSAGE_HEADER, e.getMessage()).type(MediaType.APPLICATION_JSON).build();
91
+ return Response.status(DEFAULT_APP_ERROR_STATUS_CODE)
92
+ .header(ERROR_CODE_MESSAGE_HEADER, ((SeCurisServiceException) e).getStatus())
93
+ .header(ERROR_MESSAGE_HEADER, e.getMessage())
94
+ .type(MediaType.APPLICATION_JSON)
95
+ .build();
9296 }
9397
98
+ String path = request != null ? request.getPathInfo() : null;
99
+ Object user = (bsc != null && bsc.getUserPrincipal() != null) ? bsc.getUserPrincipal() : null;
100
+ String host = request != null ? request.getRemoteHost() : null;
101
+ String ua = request != null ? request.getHeader("User-Agent") : null;
102
+ String url = request != null ? String.valueOf(request.getRequestURL()) : null;
103
+
104
+ LOG.error("Unexpected error accessing to '{}' by user: {}", path, user);
105
+ LOG.error("Request sent from {}, with User-Agent: {}", host, ua);
106
+ LOG.error("Request url: {}", url, e);
107
+
108
+ /**
94109 LOG.error("Unexpected error accesing to '{}' by user: {}", request.getPathInfo(), bsc.getUserPrincipal());
95110 LOG.error("Request sent from {}, with User-Agent: {}", request.getRemoteHost(), request.getHeader("User-Agent"));
96111 LOG.error("Request url: " + request.getRequestURL(), e);
97
- return Response.serverError().header(ERROR_MESSAGE_HEADER, "Unexpected error: " + e.toString()).type(MediaType.APPLICATION_JSON).build();
112
+ */
113
+
114
+ return Response.serverError()
115
+ .header(ERROR_MESSAGE_HEADER, "Unexpected error: " + e.toString())
116
+ .type(MediaType.APPLICATION_JSON)
117
+ .build();
98118 }
99119
100120 /**
....@@ -103,6 +123,8 @@
103123 * Best-effort cleanup: rollback active transaction (if joined) and close the {@link EntityManager}.
104124 */
105125 private void releaseEntityManager() {
126
+
127
+ /**
106128 try {
107129 if (em != null && em.isOpen()) {
108130 LOG.debug("CLOSING EM: {}, trans: {}", em, em.isJoinedToTransaction());
....@@ -116,5 +138,6 @@
116138 ex.printStackTrace();
117139 LOG.error("Error closing EM: {}, {}", em, ex);
118140 }
141
+ */
119142 }
120143 }
securis/src/main/java/net/curisit/securis/RestServicesApplication.java
....@@ -31,7 +31,6 @@
3131 * @author JRA
3232 * Last reviewed by JRA on Oct 5, 2025.
3333 */
34
-@ApplicationPath("/")
3534 public class RestServicesApplication extends Application {
3635
3736 private static final Logger LOG = LogManager.getLogger(RestServicesApplication.class);
securis/src/main/java/net/curisit/securis/ioc/RequestsInterceptor.java
....@@ -81,18 +81,67 @@
8181 */
8282 @Override
8383 public void filter(ContainerRequestContext requestContext) throws IOException {
84
- EntityManager em = emProvider.getEntityManager();
85
- LOG.debug("GETTING EM: {}", em);
84
+
85
+ Method method = resourceInfo != null ? resourceInfo.getResourceMethod() : null;
86
+ if (method == null) {
87
+ LOG.warn("RequestsInterceptor: resource method is null");
88
+ return;
89
+ }
8690
87
- // Store EntityManager for later retrieval (writer interceptor)
88
- requestContext.setProperty(EM_CONTEXT_PROPERTY, em);
91
+ boolean securable = method.isAnnotationPresent(Securable.class);
92
+ boolean ensureTransaction = method.isAnnotationPresent(EnsureTransaction.class);
8993
90
- Method method = resourceInfo.getResourceMethod();
94
+ // Only require injected helpers when the endpoint actually needs them
95
+ if (securable) {
96
+ if (tokenHelper == null || cache == null || emProvider == null) {
97
+ LOG.error(
98
+ "RequestsInterceptor is not fully initialized for secured endpoint '{}'. " +
99
+ "tokenHelper={}, cache={}, emProvider={}",
100
+ method.getName(), tokenHelper, cache, emProvider
101
+ );
102
+ requestContext.abortWith(
103
+ Response.status(Status.INTERNAL_SERVER_ERROR)
104
+ .entity("Security infrastructure not initialized")
105
+ .build()
106
+ );
107
+ return;
108
+ }
91109
92
- if (checkSecurableMethods(requestContext, method)) {
93
- if (method.isAnnotationPresent(EnsureTransaction.class)) {
94
- LOG.debug("Beginning transaction");
95
- em.getTransaction().begin();
110
+ if (!checkSecurableMethods(requestContext, method)) {
111
+ return;
112
+ }
113
+ }
114
+
115
+ // Only open/use EM when needed
116
+ if (ensureTransaction || securable) {
117
+ EntityManager em = getEntityManagerSafely();
118
+ if (em == null) {
119
+ LOG.error("No EntityManager available for method '{}'", method.getName());
120
+ requestContext.abortWith(
121
+ Response.status(Status.INTERNAL_SERVER_ERROR)
122
+ .entity("Persistence infrastructure not initialized")
123
+ .build()
124
+ );
125
+ return;
126
+ }
127
+
128
+ LOG.debug("GETTING EM: {}", em);
129
+ requestContext.setProperty(EM_CONTEXT_PROPERTY, em);
130
+
131
+ if (ensureTransaction) {
132
+ try {
133
+ if (!em.getTransaction().isActive()) {
134
+ LOG.debug("Beginning transaction");
135
+ em.getTransaction().begin();
136
+ }
137
+ } catch (Exception e) {
138
+ LOG.error("Error beginning transaction for method '{}'", method.getName(), e);
139
+ requestContext.abortWith(
140
+ Response.status(Status.INTERNAL_SERVER_ERROR)
141
+ .entity("Could not begin transaction")
142
+ .build()
143
+ );
144
+ }
96145 }
97146 }
98147 }
....@@ -107,9 +156,11 @@
107156 * @return true if request can proceed; false when aborted
108157 */
109158 private boolean checkSecurableMethods(ContainerRequestContext ctx, Method method) {
110
- if (!method.isAnnotationPresent(Securable.class)) return true;
159
+ if (!method.isAnnotationPresent(Securable.class)) {
160
+ return true;
161
+ }
111162
112
- String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM);
163
+ String token = servletRequest != null ? servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM) : null;
113164 if (token == null || !tokenHelper.isTokenValid(token)) {
114165 LOG.warn("Access denied, invalid token");
115166 ctx.abortWith(Response.status(Status.UNAUTHORIZED).build());
....@@ -126,11 +177,30 @@
126177 return false;
127178 }
128179
129
- BasicSecurityContext sc = new BasicSecurityContext(username, roles, servletRequest.isSecure());
180
+ boolean secure = servletRequest != null && servletRequest.isSecure();
181
+ BasicSecurityContext sc = new BasicSecurityContext(username, roles, secure);
130182 sc.setOrganizationsIds(getUserOrganizations(username));
131183 sc.setApplicationsIds(getUserApplications(username));
132184 ctx.setSecurityContext(sc);
133185 return true;
186
+ }
187
+
188
+ /**
189
+ * getEntityManagerSafely<p>
190
+ * Get the entity manager in a safely way
191
+ *
192
+ * @return entityManager
193
+ */
194
+ private EntityManager getEntityManagerSafely() {
195
+ try {
196
+ if (emProvider == null) {
197
+ return null;
198
+ }
199
+ return emProvider.getEntityManager();
200
+ } catch (Exception e) {
201
+ LOG.error("Error obtaining EntityManager from provider", e);
202
+ return null;
203
+ }
134204 }
135205
136206 // -------------------------------------------------------------
....@@ -145,18 +215,31 @@
145215 * @return userRoles
146216 */
147217 private int getUserRoles(String username) {
148
- if (username == null) return 0;
149
- Integer cached = cache.get("roles_" + username, Integer.class);
150
- if (cached != null) return cached;
218
+ if (username == null || cache == null) {
219
+ return 0;
220
+ }
151221
152
- EntityManager em = emProvider.getEntityManager();
222
+ Integer cached = cache.get("roles_" + username, Integer.class);
223
+ if (cached != null) {
224
+ return cached;
225
+ }
226
+
227
+ EntityManager em = getEntityManagerSafely();
228
+ if (em == null) {
229
+ LOG.error("Cannot resolve user roles: EntityManager is not available");
230
+ return 0;
231
+ }
232
+
153233 User user = em.find(User.class, username);
154234 int roles = 0;
155235 if (user != null) {
156236 List<Integer> r = user.getRoles();
157
- if (r != null) for (Integer role : r) roles += role;
237
+ if (r != null) {
238
+ for (Integer role : r) {
239
+ roles += role;
240
+ }
241
+ }
158242 cache.set("roles_" + username, roles, 3600);
159
- // also warm some caches
160243 cache.set("orgs_" + username, user.getOrgsIds(), 3600);
161244 }
162245 return roles;
....@@ -171,10 +254,22 @@
171254 * @return userOrganizations
172255 */
173256 private Set<Integer> getUserOrganizations(String username) {
174
- Set<Integer> cached = cache.getSet("orgs_" + username, Integer.class);
175
- if (cached != null) return cached;
257
+ if (username == null || cache == null) {
258
+ return Set.of();
259
+ }
176260
177
- User user = emProvider.getEntityManager().find(User.class, username);
261
+ Set<Integer> cached = cache.getSet("orgs_" + username, Integer.class);
262
+ if (cached != null) {
263
+ return cached;
264
+ }
265
+
266
+ EntityManager em = getEntityManagerSafely();
267
+ if (em == null) {
268
+ LOG.error("Cannot resolve user organizations: EntityManager is not available");
269
+ return Set.of();
270
+ }
271
+
272
+ User user = em.find(User.class, username);
178273 if (user != null) {
179274 Set<Integer> result = user.getAllOrgsIds();
180275 cache.set("orgs_" + username, result, 3600);
....@@ -192,10 +287,22 @@
192287 * @return userApplications
193288 */
194289 private Set<Integer> getUserApplications(String username) {
195
- Set<Integer> cached = cache.getSet("apps_" + username, Integer.class);
196
- if (cached != null) return cached;
290
+ if (username == null || cache == null) {
291
+ return Set.of();
292
+ }
197293
198
- User user = emProvider.getEntityManager().find(User.class, username);
294
+ Set<Integer> cached = cache.getSet("apps_" + username, Integer.class);
295
+ if (cached != null) {
296
+ return cached;
297
+ }
298
+
299
+ EntityManager em = getEntityManagerSafely();
300
+ if (em == null) {
301
+ LOG.error("Cannot resolve user applications: EntityManager is not available");
302
+ return Set.of();
303
+ }
304
+
305
+ User user = em.find(User.class, username);
199306 if (user != null) {
200307 Set<Integer> result = user.getAllAppsIds();
201308 cache.set("apps_" + username, result, 3600);
....@@ -218,30 +325,45 @@
218325 */
219326 @Override
220327 public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
221
- context.proceed();
222
-
223
- EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY);
224
- if (em == null) return;
225
-
226328 try {
227
- if (em.getTransaction().isActive()) {
228
- if (servletResponse.getStatus() == Status.OK.getStatusCode()) {
229
- em.getTransaction().commit();
230
- LOG.debug("Transaction committed");
231
- } else {
232
- em.getTransaction().rollback();
233
- LOG.debug("Transaction rolled back");
234
- }
235
- }
329
+ context.proceed();
236330 } finally {
237
- if (em.isOpen()) {
331
+ EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY);
332
+ if (em == null) {
333
+ return;
334
+ }
335
+
336
+ try {
337
+ if (em.getTransaction() != null && em.getTransaction().isActive()) {
338
+ int status = servletResponse != null ? servletResponse.getStatus() : Status.INTERNAL_SERVER_ERROR.getStatusCode();
339
+ if (status >= 200 && status < 300) {
340
+ em.getTransaction().commit();
341
+ LOG.debug("Transaction committed");
342
+ } else {
343
+ em.getTransaction().rollback();
344
+ LOG.debug("Transaction rolled back");
345
+ }
346
+ }
347
+ } catch (Exception e) {
348
+ LOG.error("Error finalizing transaction", e);
238349 try {
239
- em.close();
350
+ if (em.getTransaction() != null && em.getTransaction().isActive()) {
351
+ em.getTransaction().rollback();
352
+ }
353
+ } catch (Exception rollbackEx) {
354
+ LOG.error("Error rolling back transaction", rollbackEx);
355
+ }
356
+ } finally {
357
+ try {
358
+ if (em.isOpen()) {
359
+ em.close();
360
+ }
240361 } catch (Exception e) {
241362 LOG.error("Error closing EntityManager", e);
242363 }
243364 }
244365 }
245366 }
367
+
246368 }
247369
securis/src/main/java/net/curisit/securis/services/BasicServices.java
....@@ -28,8 +28,8 @@
2828 import org.apache.logging.log4j.LogManager;
2929 import org.apache.logging.log4j.Logger;
3030
31
-import net.curisit.integrity.AppVersion;
32
-import net.curisit.integrity.commons.Utils;
31
+import net.curisit.securis.AppVersion;
32
+import net.curisit.securis.utils.Utils;
3333 import net.curisit.securis.ioc.EnsureTransaction;
3434 import net.curisit.securis.security.Securable;
3535 import net.curisit.securis.utils.TokenHelper;
securis/src/main/java/net/curisit/securis/services/exception/CurisException.java
....@@ -0,0 +1,86 @@
1
+/*
2
+ * Copyright © 2015 CurisIT, S.L. All Rights Reserved.
3
+ */
4
+package net.curisit.securis.services.exception;
5
+
6
+/**
7
+ * CurisException<p>
8
+ * This class manages the standard (before or after computation) error on computation that implies that the problem is reported
9
+ * and the computation can continue.
10
+ *
11
+ * @author JRA
12
+ * Last reviewed by APB on April 05, 2022.
13
+ */
14
+public class CurisException extends Exception {
15
+
16
+ private static final long serialVersionUID = 3830386897219028662L;
17
+
18
+ // i18 code for message localization
19
+ String i18key = null;
20
+
21
+ /**
22
+ * CurisException<p>
23
+ * This method is used to manage the standard exception with the message.
24
+ *
25
+ * @param msg
26
+ * The exception message
27
+ */
28
+ public CurisException(String msg) {
29
+ super(msg);
30
+ }
31
+
32
+ /**
33
+ * CurisException<p>
34
+ * This method is used to manage the standard exception with the message and the cause.
35
+ *
36
+ * @param msg
37
+ * The exception message
38
+ * @param cause
39
+ * The error cause
40
+ */
41
+ public CurisException(String msg, Throwable cause) {
42
+ super(msg, cause);
43
+ }
44
+
45
+ /**
46
+ * CurisException<p>
47
+ * This method is used to manage the standard exception with the message and the cause.
48
+ *
49
+ * @param msg
50
+ * The exception message
51
+ * @param cause
52
+ * The error cause
53
+ */
54
+ public CurisException(String msg, String i18k) {
55
+ this(msg);
56
+ this.i18key = i18k;
57
+ }
58
+
59
+ /**
60
+ * CurisException<p>
61
+ * This method is used to manage the standard exception with the message, the i18 code and the cause.
62
+ *
63
+ * @param msg
64
+ * The exception message
65
+ * @param i18k
66
+ * The code for localization
67
+ * @param cause
68
+ * The error cause
69
+ */
70
+ public CurisException(String msg, String i18k, Throwable cause) {
71
+ this(msg, cause);
72
+ this.i18key = i18k;
73
+ }
74
+
75
+ /**
76
+ * geti18key<p>
77
+ * This method is used to get the i18 code for localization.
78
+ *
79
+ * @return i18key
80
+ * The i18 code
81
+ */
82
+ public String geti18key() {
83
+ return i18key;
84
+ }
85
+
86
+}
securis/src/main/java/net/curisit/securis/services/exception/SeCurisServiceException.java
....@@ -3,7 +3,7 @@
33 */
44 package net.curisit.securis.services.exception;
55
6
-import net.curisit.integrity.exception.CurisException;
6
+
77
88 /**
99 * SeCurisServiceException
securis/src/main/java/net/curisit/securis/utils/Utils.java
....@@ -0,0 +1,719 @@
1
+/*
2
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
3
+ */
4
+package net.curisit.securis.utils;
5
+
6
+import java.io.BufferedReader;
7
+import java.io.File;
8
+import java.io.IOException;
9
+import java.io.InputStream;
10
+import java.io.InputStreamReader;
11
+import java.lang.reflect.InvocationTargetException;
12
+import java.math.BigInteger;
13
+import java.nio.ByteBuffer;
14
+import java.nio.channels.FileChannel;
15
+import java.nio.charset.Charset;
16
+import java.nio.file.FileVisitResult;
17
+import java.nio.file.Files;
18
+import java.nio.file.Path;
19
+import java.nio.file.Paths;
20
+import java.nio.file.SimpleFileVisitor;
21
+import java.nio.file.StandardOpenOption;
22
+import java.nio.file.attribute.BasicFileAttributes;
23
+import java.security.MessageDigest;
24
+import java.security.NoSuchAlgorithmException;
25
+import java.text.ParseException;
26
+import java.text.SimpleDateFormat;
27
+import java.time.Instant;
28
+import java.time.LocalDate;
29
+import java.time.LocalDateTime;
30
+import java.time.ZoneId;
31
+import java.util.ArrayList;
32
+import java.util.Arrays;
33
+import java.util.Calendar;
34
+import java.util.Collection;
35
+import java.util.Date;
36
+import java.util.HashMap;
37
+import java.util.HashSet;
38
+import java.util.Iterator;
39
+import java.util.LinkedList;
40
+import java.util.List;
41
+import java.util.Map;
42
+import java.util.TimeZone;
43
+import java.util.concurrent.CompletableFuture;
44
+import java.util.concurrent.CompletionException;
45
+import java.util.concurrent.TimeoutException;
46
+import java.util.function.Supplier;
47
+import java.util.regex.Pattern;
48
+import java.util.stream.Collectors;
49
+
50
+import org.apache.commons.beanutils.BeanUtils;
51
+import org.apache.commons.beanutils.PropertyUtils;
52
+import org.apache.logging.log4j.LogManager;
53
+import org.apache.logging.log4j.Logger;
54
+
55
+import net.curisit.integrity.exception.CurisRuntimeException;
56
+
57
+/**
58
+ * Utils
59
+ * <p>
60
+ * General-purpose static utilities: hashing, dates/times, collections, I/O helpers,
61
+ * simple email validation, reflection helpers, file tailing, etc.
62
+ *
63
+ * Author: JRA
64
+ * Last reviewed by JRA on Oct 6, 2025.
65
+ */
66
+public class Utils {
67
+
68
+ private static final Logger LOG = LogManager.getLogger(Utils.class);
69
+
70
+ /* --------------------- Hash functions --------------------- */
71
+
72
+ /**
73
+ * md5<p>
74
+ * Returns MD5 digest (hex, 32 chars) for the input text.
75
+ *
76
+ * @param str source text
77
+ * @return lowercase hex MD5 or null on error
78
+ */
79
+ public static String md5(String str) {
80
+ try {
81
+ MessageDigest mDigest = MessageDigest.getInstance("MD5");
82
+ mDigest.update(str.getBytes(), 0, str.length());
83
+ BigInteger i = new BigInteger(1, mDigest.digest());
84
+ return String.format("%1$032x", i);
85
+ } catch (NoSuchAlgorithmException e) {
86
+ LOG.error("Error generating MD5 for string: " + str, e);
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * sha1<p>
93
+ * Returns SHA-1 digest (hex, 40 chars) for the input text.
94
+ *
95
+ * @param str source text
96
+ * @return lowercase hex SHA-1 or null on error
97
+ */
98
+ public static String sha1(String str) {
99
+ try {
100
+ MessageDigest mDigest = MessageDigest.getInstance("SHA1");
101
+ mDigest.update(str.getBytes(), 0, str.length());
102
+ BigInteger i = new BigInteger(1, mDigest.digest());
103
+ return String.format("%1$040x", i);
104
+ } catch (NoSuchAlgorithmException e) {
105
+ LOG.error("Error generating SHA1 for string: " + str, e);
106
+ }
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * sha256<p>
112
+ * Returns SHA-256 digest (hex, 64 chars) for the input text.
113
+ *
114
+ * @param str source text
115
+ * @return lowercase hex SHA-256 or null on error
116
+ */
117
+ public static String sha256(String str) {
118
+ return sha256(str.getBytes());
119
+ }
120
+
121
+ /**
122
+ * sha256<p>
123
+ * Returns SHA-256 digest (hex) for a byte array.
124
+ *
125
+ * @param bytes data
126
+ * @return hex SHA-256 or null on error
127
+ */
128
+ public static String sha256(byte[] bytes) {
129
+ try {
130
+ MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
131
+ mDigest.update(bytes, 0, bytes.length);
132
+ BigInteger i = new BigInteger(1, mDigest.digest());
133
+ return String.format("%1$064x", i);
134
+ } catch (NoSuchAlgorithmException e) {
135
+ LOG.error("Error generating SHA-256 for bytes: " + bytes, e);
136
+ }
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * sha256<p>
142
+ * Incrementally updates SHA-256 with multiple byte arrays and returns hex digest.
143
+ *
144
+ * @param bytes multiple byte chunks
145
+ * @return hex SHA-256 or null on error
146
+ */
147
+ public static String sha256(byte[]... bytes) {
148
+ try {
149
+ MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
150
+ for (byte[] bs : bytes) {
151
+ mDigest.update(bs, 0, bs.length);
152
+ }
153
+ BigInteger i = new BigInteger(1, mDigest.digest());
154
+ return String.format("%1$064x", i);
155
+ } catch (NoSuchAlgorithmException e) {
156
+ LOG.error("Error generating SHA-256 for bytes: " + bytes, e);
157
+ }
158
+ return null;
159
+ }
160
+
161
+ /* --------------------- ISO date helpers --------------------- */
162
+
163
+ private static final String ISO_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
164
+ private static final SimpleDateFormat sdf = new SimpleDateFormat(ISO_PATTERN);
165
+ static {
166
+ sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
167
+ }
168
+
169
+ /**
170
+ * toIsoFormat<p>
171
+ * Formats a Date to ISO-8601-like string with milliseconds in GMT.
172
+ *
173
+ * @param date input date
174
+ * @return formatted string
175
+ */
176
+ public static synchronized String toIsoFormat(Date date) {
177
+ return sdf.format(date);
178
+ }
179
+
180
+ private static final String ISO_PATTERN_REDUCED_PRECISION = "yyyy-MM-dd'T'HH:mm:ssZ";
181
+ private static final SimpleDateFormat sdfReduced = new SimpleDateFormat(ISO_PATTERN_REDUCED_PRECISION);
182
+ static {
183
+ sdfReduced.setTimeZone(TimeZone.getTimeZone("GMT"));
184
+ }
185
+
186
+ /**
187
+ * toIsoFormatReduced<p>
188
+ * Formats a Date to ISO string without milliseconds in GMT.
189
+ *
190
+ * @param date input date
191
+ * @return formatted string
192
+ */
193
+ public static synchronized String toIsoFormatReduced(Date date) {
194
+ return sdfReduced.format(date);
195
+ }
196
+
197
+ /**
198
+ * toDateFromIso<p>
199
+ * Parses a string in {@link #ISO_PATTERN} into a Date (GMT).
200
+ *
201
+ * @param dateStr string to parse
202
+ * @return Date or null if parsing fails
203
+ */
204
+ public static synchronized Date toDateFromIso(String dateStr) {
205
+ try {
206
+ return sdf.parse(dateStr);
207
+ } catch (ParseException e) {
208
+ LOG.error("Error parsing string '{}' to Date object with pattern: {}", dateStr, ISO_PATTERN);
209
+ return null;
210
+ }
211
+ }
212
+
213
+ /* --------------------- Null-safe helpers --------------------- */
214
+
215
+ /**
216
+ * nonull<p>
217
+ * Returns {@code value} cast to T, or {@code defaultValue} if null.
218
+ *
219
+ * @param value input
220
+ * @param defaultValue fallback
221
+ * @return value or default
222
+ */
223
+ @SuppressWarnings("unchecked")
224
+ public static <T> T nonull(Object value, T defaultValue) {
225
+ return value == null ? defaultValue : (T) value;
226
+ }
227
+
228
+ /**
229
+ * nonull<p>
230
+ * Returns String value or empty string if null.
231
+ *
232
+ * @param value input
233
+ * @return non-null string
234
+ */
235
+ public static String nonull(Object value) {
236
+ return nonull(value, "");
237
+ }
238
+
239
+ /**
240
+ * value<p>
241
+ * Unchecked cast helper (avoid using unless necessary).
242
+ *
243
+ * @param value object
244
+ * @param type target type
245
+ * @return casted value
246
+ */
247
+ @SuppressWarnings("unchecked")
248
+ public static <T> T value(Object value, Class<T> type) {
249
+ return (T) value;
250
+ }
251
+
252
+ /**
253
+ * createMap<p>
254
+ * Convenience to create a Map from varargs key/value pairs.
255
+ *
256
+ * @param keyValue alternating key, value
257
+ * @return map with pairs
258
+ */
259
+ @SuppressWarnings("unchecked")
260
+ public static <K extends Object, V extends Object> Map<K, V> createMap(Object... keyValue) {
261
+ Map<K, V> map = (Map<K, V>) new HashMap<Object, Object>();
262
+ for (int i = 0; i < keyValue.length; i += 2) {
263
+ ((Map<Object, Object>) map).put(keyValue[i], keyValue[i + 1]);
264
+ }
265
+ return map;
266
+ }
267
+
268
+ /* --------------------- Date arithmetic --------------------- */
269
+
270
+ /**
271
+ * addDays<p>
272
+ * Adds a number of days to a Date.
273
+ *
274
+ * @param date base date
275
+ * @param days positive/negative days
276
+ * @return new Date
277
+ */
278
+ public static Date addDays(Date date, int days) {
279
+ Calendar cal = Calendar.getInstance();
280
+ cal.setTime(date);
281
+ cal.add(Calendar.DATE, days);
282
+ return cal.getTime();
283
+ }
284
+
285
+ /**
286
+ * addHours<p>
287
+ * Adds hours to a Date.
288
+ *
289
+ * @param date base date
290
+ * @param hours number of hours
291
+ * @return new Date
292
+ */
293
+ public static Date addHours(Date date, int hours) {
294
+ Calendar cal = Calendar.getInstance();
295
+ cal.setTime(date);
296
+ cal.add(Calendar.HOUR, hours);
297
+ return cal.getTime();
298
+ }
299
+
300
+ /**
301
+ * addMinutes<p>
302
+ * Adds minutes to a Date.
303
+ *
304
+ * @param date base date
305
+ * @param minutes number of minutes
306
+ * @return new Date
307
+ */
308
+ public static Date addMinutes(Date date, int minutes) {
309
+ Calendar cal = Calendar.getInstance();
310
+ cal.setTime(date);
311
+ cal.add(Calendar.MINUTE, minutes);
312
+ return cal.getTime();
313
+ }
314
+
315
+ /**
316
+ * getStrTime<p>
317
+ * Formats seconds as H:MM:SS (H omitted if 0). Negative yields "??".
318
+ *
319
+ * @param seconds total seconds
320
+ * @return formatted time
321
+ */
322
+ public static String getStrTime(int seconds) {
323
+ if (seconds < 0)
324
+ return "??";
325
+ String secs = String.format("%02d", seconds % 60);
326
+ String hours = "";
327
+ String mins = "0:";
328
+ if (seconds > 3600) {
329
+ hours = String.format("%d:", seconds / 3600);
330
+ }
331
+ if (seconds > 60) {
332
+ mins = String.format("%02d:", (seconds / 60) % 60);
333
+ }
334
+ return hours + mins + secs;
335
+ }
336
+
337
+ /* --------------------- Collections & conversions --------------------- */
338
+
339
+ /**
340
+ * areAllEmpty<p>
341
+ * Checks if all provided collections are empty.
342
+ *
343
+ * @param lists vararg of collections
344
+ * @return true if all empty
345
+ */
346
+ public static boolean areAllEmpty(Collection<?>... lists) {
347
+ for (Collection<?> list : lists) {
348
+ if (!list.isEmpty())
349
+ return false;
350
+ }
351
+ return true;
352
+ }
353
+
354
+ /**
355
+ * toDateFromLocalDate<p>
356
+ * Converts a {@link LocalDate} to {@link Date} at system default zone start of day.
357
+ *
358
+ * @param ld local date
359
+ * @return Date or null
360
+ */
361
+ public static Date toDateFromLocalDate(LocalDate ld) {
362
+ if (ld == null)
363
+ return null;
364
+ Instant instant = ld.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant();
365
+ return Date.from(instant);
366
+ }
367
+
368
+ /**
369
+ * toLocalDateFromDate<p>
370
+ * Converts a {@link Date} to {@link LocalDate} at system default zone.
371
+ *
372
+ * @param date Date
373
+ * @return LocalDate or null
374
+ */
375
+ public static LocalDate toLocalDateFromDate(Date date) {
376
+ if (date == null)
377
+ return null;
378
+ Instant instant = Instant.ofEpochMilli(date.getTime());
379
+ return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()).toLocalDate();
380
+ }
381
+
382
+ /**
383
+ * compare<p>
384
+ * Null-safe equals: both null → true; otherwise {@code equals()}.
385
+ *
386
+ * @param obj1 first object
387
+ * @param obj2 second object
388
+ * @return equality result
389
+ */
390
+ public static boolean compare(Object obj1, Object obj2) {
391
+ if (obj1 == null) {
392
+ return obj2 == null;
393
+ } else {
394
+ return obj1.equals(obj2);
395
+ }
396
+ }
397
+
398
+ /* --------------------- Email validation --------------------- */
399
+
400
+ 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,})$";
401
+ private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_PATTERN_STR);
402
+
403
+ /**
404
+ * isValidEmail<p>
405
+ * Validates an email address format with a basic regex.
406
+ *
407
+ * @param email candidate value
408
+ * @return valid or not
409
+ */
410
+ public static boolean isValidEmail(final String email) {
411
+ if (email == null) {
412
+ return false;
413
+ }
414
+ return EMAIL_PATTERN.matcher(email).matches();
415
+ }
416
+
417
+ /* --------------------- Iterable helpers --------------------- */
418
+
419
+ /**
420
+ * Reversed<p>
421
+ * Iterable wrapper to iterate a List in reverse order without copying.
422
+ *
423
+ * @param <T> element type
424
+ */
425
+ public static class Reversed<T> implements Iterable<T> {
426
+ private final List<T> original;
427
+
428
+ public Reversed(List<T> original) {
429
+ this.original = original;
430
+ }
431
+
432
+ public Iterator<T> iterator() {
433
+ return new Iterator<T>() {
434
+ int cursor = original.size();
435
+ public boolean hasNext() { return cursor > 0; }
436
+ public T next() { return original.get(--cursor); }
437
+ public void remove() { throw new CurisRuntimeException("ERROR removing item in read-only iterator"); }
438
+ };
439
+ }
440
+ }
441
+
442
+ /**
443
+ * reversedList<p>
444
+ * Returns a reversed-iteration view of a list.
445
+ *
446
+ * @param original list to view
447
+ * @param <T> element type
448
+ * @return iterable reverse view
449
+ */
450
+ public static <T> Reversed<T> reversedList(List<T> original) {
451
+ return new Reversed<T>(original);
452
+ }
453
+
454
+ /**
455
+ * isSorted<p>
456
+ * Checks if a list of Doubles is non-decreasing.
457
+ *
458
+ * @param original list
459
+ * @return true if sorted ascending
460
+ */
461
+ public static boolean isSorted(List<Double> original) {
462
+ for (int i = 0; i < (original.size() - 1); i++) {
463
+ Double v0 = original.get(i);
464
+ Double v1 = original.get(i + 1);
465
+ if (v0.doubleValue() > v1.doubleValue()) {
466
+ return false;
467
+ }
468
+ }
469
+ return true;
470
+ }
471
+
472
+ /* --------------------- Simple file reading helpers --------------------- */
473
+
474
+ /**
475
+ * readFileLines<p>
476
+ * Reads a file as UTF-8 and splits each line by a separator.
477
+ *
478
+ * @param file source file
479
+ * @param separator regex separator
480
+ * @return list of line fields
481
+ * @throws IOException on I/O errors
482
+ */
483
+ public static List<List<String>> readFileLines(File file, final String separator) throws IOException {
484
+ List<String> lines = Files.readAllLines(Paths.get(file.toURI()), Charset.forName("utf8"));
485
+ return lines.stream().map(line -> Arrays.asList(line.split(separator))).collect(Collectors.toList());
486
+ }
487
+
488
+ /**
489
+ * readStreamLines<p>
490
+ * Reads an InputStream as UTF-8 and splits each line by a separator.
491
+ *
492
+ * @param stream input stream (caller closes it)
493
+ * @param separator regex separator
494
+ * @return list of line fields
495
+ * @throws IOException on I/O errors
496
+ */
497
+ public static List<List<String>> readStreamLines(InputStream stream, final String separator) throws IOException {
498
+ try (
499
+ InputStreamReader isr = new InputStreamReader(stream, Charset.forName("UTF-8"));
500
+ BufferedReader br = new BufferedReader(isr);
501
+ ) {
502
+ String line;
503
+ List<List<String>> lines = new ArrayList<>();
504
+ while ((line = br.readLine()) != null) {
505
+ lines.add(Arrays.asList(line.split(separator)));
506
+ }
507
+ return lines;
508
+ }
509
+ }
510
+
511
+ /**
512
+ * deleteDirectoryTree<p>
513
+ * Recursively deletes a directory and its contents.
514
+ *
515
+ * @param baseDirectory directory path
516
+ * @throws IOException on failure
517
+ */
518
+ public static void deleteDirectoryTree(Path baseDirectory) throws IOException {
519
+ Files.walkFileTree(baseDirectory, new SimpleFileVisitor<Path>() {
520
+ @Override
521
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
522
+ Files.deleteIfExists(file);
523
+ return FileVisitResult.CONTINUE;
524
+ }
525
+ @Override
526
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
527
+ Files.deleteIfExists(dir);
528
+ return FileVisitResult.CONTINUE;
529
+ }
530
+ });
531
+ }
532
+
533
+ /* --------------------- Directory size & tail --------------------- */
534
+
535
+ private static class LongWrapper { long num = 0L; }
536
+
537
+ /**
538
+ * getDirectoryContentSize<p>
539
+ * Computes total size (bytes) of files within a directory tree.
540
+ *
541
+ * @param baseDirectory root path
542
+ * @return total bytes
543
+ * @throws IOException on traversal errors
544
+ */
545
+ public static long getDirectoryContentSize(Path baseDirectory) throws IOException {
546
+ final LongWrapper size = new LongWrapper();
547
+ Files.walkFileTree(baseDirectory, new SimpleFileVisitor<Path>() {
548
+ @Override
549
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
550
+ size.num += file.toFile().length();
551
+ return FileVisitResult.CONTINUE;
552
+ }
553
+ @Override
554
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
555
+ if (!dir.equals(baseDirectory)) {
556
+ size.num += dir.toFile().length();
557
+ }
558
+ return FileVisitResult.CONTINUE;
559
+ }
560
+ });
561
+ return size.num;
562
+ }
563
+
564
+ /**
565
+ * waitFor<p>
566
+ * Asynchronously waits until a condition is true or a timeout is reached.
567
+ *
568
+ * @param condition supplier that should eventually return true
569
+ * @param timeout max wait (ms)
570
+ * @return future completing with true if condition met
571
+ * @throws TimeoutException surfaced through CompletionException if timed out
572
+ */
573
+ public static CompletableFuture<Boolean> waitFor(Supplier<Boolean> condition, long timeout) throws TimeoutException {
574
+ return CompletableFuture.<Boolean>supplyAsync(() -> {
575
+ long t0 = System.currentTimeMillis();
576
+ while(!condition.get()) {
577
+ long t1 = System.currentTimeMillis();
578
+ if ((t1 - t0) > timeout) {
579
+ throw new CompletionException(new TimeoutException());
580
+ }
581
+ try {
582
+ Thread.sleep(100);
583
+ } catch (InterruptedException e) {
584
+ throw new CompletionException(e);
585
+ }
586
+ }
587
+ return true;
588
+ });
589
+ }
590
+
591
+ private static final int BLOCK_SIZE = 4096; // 4KB
592
+
593
+ /**
594
+ * getLastLines<p>
595
+ * Efficiently reads the last N lines of a text file by scanning from the end in blocks.
596
+ *
597
+ * @param file file to read
598
+ * @param numLines number of lines from the end
599
+ * @return list of lines in natural order (oldest→newest among the last N)
600
+ * @throws IOException on I/O errors
601
+ */
602
+ public static List<String> getLastLines(File file, int numLines) throws IOException {
603
+ LinkedList<String> lines = new LinkedList<>();
604
+ long fileSize = file.length();
605
+ int chunk = (fileSize < BLOCK_SIZE) ? (int)fileSize : BLOCK_SIZE;
606
+ long seekPos = fileSize - chunk;
607
+ FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.READ);
608
+ final byte newLine = (byte)'\n';
609
+ String partialLine = null;
610
+ while (lines.size() < numLines) {
611
+ ByteBuffer dst = ByteBuffer.allocate(chunk);
612
+ int bytesRead = fc.read(dst, seekPos);
613
+ byte[] content = dst.array();
614
+ int lastLineIdx = bytesRead;
615
+ for (int i = bytesRead - 1; i > 0 ; i--) {
616
+ byte b = content[i];
617
+ if (b == newLine) {
618
+ if (i < (bytesRead - 1) || partialLine != null) {
619
+ String line = new String(Arrays.copyOfRange(content, i, lastLineIdx)).trim();
620
+ if (partialLine != null) {
621
+ line += partialLine;
622
+ partialLine = null;
623
+ }
624
+ lines.addFirst(line);
625
+ if (lines.size() == numLines) {
626
+ return lines;
627
+ }
628
+ }
629
+ lastLineIdx = i;
630
+ }
631
+ }
632
+ if (lastLineIdx > 0) {
633
+ if (partialLine == null) {
634
+ partialLine = new String(Arrays.copyOfRange(content, 0, lastLineIdx)).trim();
635
+ } else {
636
+ partialLine = new String(Arrays.copyOfRange(content, 0, lastLineIdx)) + partialLine;
637
+ }
638
+ }
639
+ if (seekPos == 0) {
640
+ lines.addFirst(partialLine);
641
+ break;
642
+ }
643
+ if (seekPos < BLOCK_SIZE) {
644
+ chunk = (int)seekPos;
645
+ seekPos = 0;
646
+ } else {
647
+ seekPos -= BLOCK_SIZE;
648
+ }
649
+ }
650
+ return lines;
651
+ }
652
+
653
+ /**
654
+ * removeDuplicates<p>
655
+ * Returns a sorted list without duplicates using a set roundtrip.
656
+ *
657
+ * @param list input list
658
+ * @param <T> type must be Comparable
659
+ * @return sorted distinct list
660
+ */
661
+ public static <T> List<T> removeDuplicates(List<T> list) {
662
+ return new HashSet<>(list).stream().sorted().collect(Collectors.toList());
663
+ }
664
+
665
+ /* --------------------- Reflection helpers --------------------- */
666
+
667
+ /**
668
+ * setProperty<p>
669
+ * Sets a bean property by name using Apache BeanUtils PropertyUtils.
670
+ *
671
+ * @param bean target bean
672
+ * @param fieldName property name (nested supported)
673
+ * @param value value to assign
674
+ * @throws RuntimeException wrapping reflection errors
675
+ */
676
+ public static void setProperty(Object bean, String fieldName, Object value) {
677
+ try {
678
+ PropertyUtils.setProperty(bean, fieldName, value);
679
+ } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
680
+ LOG.error("Missing field:" + fieldName);
681
+ throw new RuntimeException(e);
682
+ }
683
+ }
684
+
685
+ /**
686
+ * getProperty<p>
687
+ * Reads a bean property by name using Apache PropertyUtils.
688
+ *
689
+ * @param bean target bean
690
+ * @param fieldName property name
691
+ * @return value
692
+ * @throws RuntimeException wrapping reflection errors
693
+ */
694
+ public static Object getProperty(Object bean, String fieldName) {
695
+ try {
696
+ return PropertyUtils.getProperty(bean, fieldName);
697
+ } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
698
+ LOG.error("Missing field:" + fieldName);
699
+ throw new RuntimeException(e);
700
+ }
701
+ }
702
+
703
+ /**
704
+ * cloneBean<p>
705
+ * Clones a bean using Apache BeanUtils' copy mechanisms (shallow copy).
706
+ *
707
+ * @param bean source bean
708
+ * @return new cloned bean
709
+ * @throws CurisRuntimeException on cloning errors
710
+ */
711
+ public static Object cloneBean(Object bean) {
712
+ try {
713
+ return BeanUtils.cloneBean(bean);
714
+ } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) {
715
+ throw new CurisRuntimeException("Error cloning bean", e);
716
+ }
717
+ }
718
+}
719
+
securis/src/main/webapp/WEB-INF/web.xml
....@@ -36,11 +36,13 @@
3636 <param-name>resteasy.providers</param-name>
3737 <param-value>net.curisit.securis.DefaultExceptionHandler</param-value>
3838 </context-param>
39
+ <!--
3940 <context-param>
4041 <param-name>resteasy.injector.factory</param-name>
4142 <param-value>org.jboss.resteasy.cdi.CdiInjectorFactory</param-value>
4243 </context-param>
43
-
44
+ -->
45
+
4446 <filter>
4547 <filter-name>DevFilter</filter-name>
4648 <filter-class>
....@@ -49,7 +51,7 @@
4951 </filter>
5052 <filter-mapping>
5153 <filter-name>DevFilter</filter-name>
52
- <url-pattern>/*</url-pattern>
54
+ <url-pattern>/api/*</url-pattern>
5355 </filter-mapping>
5456
5557 <filter>
....@@ -63,49 +65,50 @@
6365 <url-pattern>/main-bundle.js</url-pattern>
6466 </filter-mapping>
6567
66
- <filter>
67
- <filter-name>Resteasy</filter-name>
68
- <filter-class>
69
- org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
70
- </filter-class>
71
- <init-param>
72
- <param-name>jakarta.ws.rs.Application</param-name>
73
- <param-value>net.curisit.securis.RestServicesApplication</param-value>
74
- </init-param>
75
- </filter>
76
-
77
- <filter-mapping>
78
- <filter-name>Resteasy</filter-name>
79
- <url-pattern>/*</url-pattern>
80
- </filter-mapping>
68
+ <servlet>
69
+ <servlet-name>Resteasy</servlet-name>
70
+ <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
71
+ <init-param>
72
+ <param-name>jakarta.ws.rs.Application</param-name>
73
+ <param-value>net.curisit.securis.RestServicesApplication</param-value>
74
+ </init-param>
75
+ <load-on-startup>1</load-on-startup>
76
+ </servlet>
77
+
78
+ <servlet-mapping>
79
+ <servlet-name>Resteasy</servlet-name>
80
+ <url-pattern>/api/*</url-pattern>
81
+ </servlet-mapping>
8182
8283 <welcome-file-list>
83
- <welcome-file>/index.jsp</welcome-file>
84
+ <welcome-file>index.jsp</welcome-file>
8485 </welcome-file-list>
8586
8687
87
- <!-- Security roles referenced by this web application -->
88
- <security-role>
89
- <description>
90
- Advance users, customers
91
- </description>
92
- <role-name>advance</role-name>
93
- </security-role>
94
- <security-role>
95
- <description>
96
- Administrator role
97
- </description>
98
- <role-name>admin</role-name>
99
- </security-role>
88
+ <!-- Security roles referenced by this web application -->
89
+ <security-role>
90
+ <description>
91
+ Advance users, customers
92
+ </description>
93
+ <role-name>advance</role-name>
94
+ </security-role>
95
+ <security-role>
96
+ <description>
97
+ Administrator role
98
+ </description>
99
+ <role-name>admin</role-name>
100
+ </security-role>
100101
101
-<resource-env-ref>
102
- <resource-env-ref-name>SeCurisDS</resource-env-ref-name>
103
- <resource-env-ref-type>jakarta.sql.DataSource</resource-env-ref-type>
104
-</resource-env-ref>
105
-
106
-<resource-env-ref>
107
- <resource-env-ref-name>BeanManager</resource-env-ref-name>
108
- <resource-env-ref-type>jakarta.enterprise.inject.spi.BeanManager</resource-env-ref-type>
109
-</resource-env-ref>
102
+ <resource-env-ref>
103
+ <resource-env-ref-name>SeCurisDS</resource-env-ref-name>
104
+ <resource-env-ref-type>jakarta.sql.DataSource</resource-env-ref-type>
105
+ </resource-env-ref>
106
+
107
+ <!--
108
+ <resource-env-ref>
109
+ <resource-env-ref-name>BeanManager</resource-env-ref-name>
110
+ <resource-env-ref-type>jakarta.enterprise.inject.spi.BeanManager</resource-env-ref-type>
111
+ </resource-env-ref>
112
+ -->
110113
111114 </web-app>