Skip to content

Commit ece4329

Browse files
committed
Add qrest-crud module
1 parent e8aebba commit ece4329

14 files changed

Lines changed: 1528 additions & 0 deletions

File tree

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jgroups = '5.3.11.Final'
2727
xmlBind = "4.0.1"
2828
javaxCache = '1.1.1'
2929
ehCache = '3.10.8'
30+
hibernateValidatorVersion = '6.2.1.Final'
3031

3132
[libraries]
3233
jpos = { module = "org.jpos:jpos", version.ref = "jpos" }
@@ -66,6 +67,7 @@ xmlBind = { module = 'jakarta.xml.bind:jakarta.xml.bind-api', version.ref = 'xml
6667
javaxCache = { module = 'javax.cache:cache-api', version.ref = 'javaxCache' }
6768
ehCache = { module = 'org.ehcache:ehcache', version.ref = 'ehCache' }
6869
jcache = { module = 'org.hibernate:hibernate-jcache', version.ref = 'hibernate' }
70+
hibernateValidator = { module = 'org.hibernate:hibernate-validator', version.ref = 'hibernateValidatorVersion' }
6971

7072
[bundles]
7173
jackson = [

modules/qrest-crud/build.gradle

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
description = 'jPOS-EE :: QRest-CRUD'
2+
3+
dependencies {
4+
api project(':modules:qrest')
5+
api project(':modules:txn')
6+
api libs.bundles.jackson
7+
api libs.hibernateValidator
8+
}
9+
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* jPOS Project [http://jpos.org]
3+
* Copyright (C) 2000-2025 jPOS Software SRL
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.jpos.qrest;
20+
21+
import com.fasterxml.jackson.annotation.JsonInclude;
22+
import com.fasterxml.jackson.databind.*;
23+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
24+
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
25+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
26+
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
27+
import com.fasterxml.jackson.datatype.jsr310.ser.OffsetDateTimeSerializer;
28+
29+
import java.time.*;
30+
import java.time.format.DateTimeFormatter;
31+
import java.time.format.DateTimeFormatterBuilder;
32+
import java.time.temporal.ChronoField;
33+
34+
/**
35+
* Utility class providing a shared, preconfigured Jackson {@link ObjectMapper}
36+
* and robust date/time string parsing for use in jPOS QREST modules.
37+
*
38+
* <p>
39+
* This class centralizes serialization and deserialization behavior,
40+
* especially around ISO-8601-compliant date/time formats and {@link OffsetDateTime}.
41+
* </p>
42+
*
43+
* <h2>Features</h2>
44+
* <ul>
45+
* <li>Provides a globally configured {@code ObjectMapper} with sensible defaults:
46+
* <ul>
47+
* <li>Pretty-printed output</li>
48+
* <li>Support for Java Time module</li>
49+
* <li>Non-timestamp date formatting</li>
50+
* <li>Lenient deserialization settings</li>
51+
* </ul>
52+
* </li>
53+
* <li>Parses date-time and time strings with multiple ISO-compatible formats.</li>
54+
* <li>Supplies reusable Jackson serializers/deserializers for {@link OffsetDateTime}.</li>
55+
* </ul>
56+
*
57+
* <p>
58+
* Intended for use internally by QREST participants and REST interfaces.
59+
* </p>
60+
*
61+
* @author jPOS
62+
* @since 1.9.0
63+
*/
64+
public class Converter {
65+
// Preconfigured Jackson ObjectMapper with custom settings
66+
private static ObjectMapper mapper = instantiateMapper();
67+
68+
/**
69+
* Provides a shared {@link ObjectMapper} instance used throughout the application.
70+
*
71+
* @return configured ObjectMapper with time/date support and safe defaults.
72+
*/
73+
public static ObjectMapper getMapper() {
74+
return mapper;
75+
}
76+
77+
// Flexible formatter for date-time strings, with multiple ISO-8601 variants
78+
private static final DateTimeFormatter DATE_TIME_FORMATTER = new DateTimeFormatterBuilder()
79+
.appendOptional(DateTimeFormatter.ISO_DATE_TIME)
80+
.appendOptional(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
81+
.appendOptional(DateTimeFormatter.ISO_INSTANT)
82+
.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SX"))
83+
.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX"))
84+
.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
85+
.toFormatter()
86+
.withZone(ZoneOffset.UTC);
87+
88+
/**
89+
* Parses a date-time string into an {@link OffsetDateTime}, using flexible ISO-compatible formats.
90+
*
91+
* @param str date-time string to parse.
92+
* @return corresponding OffsetDateTime.
93+
* @throws DateTimeException if the string cannot be parsed.
94+
*/
95+
public static OffsetDateTime parseDateTimeString(String str) {
96+
return ZonedDateTime.from(DATE_TIME_FORMATTER.parse(str)).toOffsetDateTime();
97+
}
98+
99+
// Flexible formatter for time-only strings, with default date values
100+
private static final DateTimeFormatter TIME_FORMATTER = new DateTimeFormatterBuilder()
101+
.appendOptional(DateTimeFormatter.ISO_TIME)
102+
.appendOptional(DateTimeFormatter.ISO_OFFSET_TIME)
103+
.parseDefaulting(ChronoField.YEAR, 2020)
104+
.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
105+
.parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
106+
.toFormatter()
107+
.withZone(ZoneOffset.UTC);
108+
109+
110+
private static StdSerializer<OffsetDateTime> offsetDateTimeSerializer(DateTimeFormatter formatter) {
111+
return new OffsetDateTimeSerializer(OffsetDateTimeSerializer.INSTANCE, false, formatter) {};
112+
}
113+
114+
private static StdDeserializer<OffsetDateTime> offsetDateTimeDeserializer(DateTimeFormatter formatter) {
115+
return new InstantDeserializer<OffsetDateTime>(InstantDeserializer.OFFSET_DATE_TIME, formatter) {};
116+
}
117+
118+
/**
119+
* Parses a time-only string into an {@link OffsetTime}.
120+
*
121+
* @param str the time string to parse.
122+
* @return corresponding OffsetTime object.
123+
* @throws DateTimeException if the string cannot be parsed.
124+
*/
125+
public static OffsetTime parseTimeString(String str) {
126+
return ZonedDateTime.from(TIME_FORMATTER.parse(str)).toOffsetDateTime().toOffsetTime();
127+
}
128+
129+
/**
130+
* Instantiates and configures the shared {@link ObjectMapper} instance.
131+
*
132+
* <p>Applies the following settings:</p>
133+
* <ul>
134+
* <li>Enables BigDecimal for float parsing.</li>
135+
* <li>Pretty-print output.</li>
136+
* <li>Ignores unknown properties.</li>
137+
* <li>Disables timestamp-based date output.</li>
138+
* <li>Registers the Java Time module.</li>
139+
* </ul>
140+
*
141+
* @return configured ObjectMapper.
142+
*/
143+
private static ObjectMapper instantiateMapper () {
144+
ObjectMapper mapper = new ObjectMapper()
145+
.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
146+
.enable(SerializationFeature.INDENT_OUTPUT)
147+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
148+
.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false)
149+
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
150+
151+
mapper.findAndRegisterModules();
152+
mapper.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
153+
mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
154+
mapper.registerModule(new JavaTimeModule());
155+
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
156+
return mapper;
157+
}
158+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* jPOS Project [http://jpos.org]
3+
* Copyright (C) 2000-2025 jPOS Software SRL
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as
7+
* published by the Free Software Foundation, either version 3 of the
8+
* License, or (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
package org.jpos.qrest;
20+
21+
import com.fasterxml.jackson.core.JsonProcessingException;
22+
import io.netty.handler.codec.http.DefaultFullHttpResponse;
23+
import io.netty.handler.codec.http.FullHttpRequest;
24+
import io.netty.handler.codec.http.HttpResponseStatus;
25+
import io.netty.handler.codec.http.HttpVersion;
26+
import io.netty.util.CharsetUtil;
27+
import org.jpos.core.Configurable;
28+
import org.jpos.core.Configuration;
29+
import org.jpos.core.ConfigurationException;
30+
import org.jpos.core.annotation.Config;
31+
import org.jpos.transaction.Context;
32+
import org.jpos.transaction.TransactionParticipant;
33+
34+
import javax.annotation.Nullable;
35+
import javax.validation.ConstraintViolation;
36+
import javax.validation.Validation;
37+
import javax.validation.Validator;
38+
import javax.validation.ValidatorFactory;
39+
import java.io.Serializable;
40+
import java.util.Set;
41+
42+
import static org.jpos.qrest.Constants.*;
43+
44+
/**
45+
* QREST transaction participant that extracts and validates a JSON object from the incoming HTTP request body.
46+
*
47+
* <p>
48+
* This participant reads the request payload (assumed to be JSON), deserializes it into a configured Java class,
49+
* validates it using Jakarta Bean Validation (JSR 380), and stores the resulting object in the transaction
50+
* {@link Context} under a configurable key.
51+
* </p>
52+
*
53+
* <h2>Configuration Parameters</h2>
54+
* <ul>
55+
* <li><strong>class</strong> — Fully qualified class name of the object to deserialize (e.g., <code>com.example.dto.UserDTO</code>).</li>
56+
* <li><strong>context-name</strong> — The key under which the resulting object will be stored in the {@link Context}.</li>
57+
* </ul>
58+
*
59+
* <h2>Behavior</h2>
60+
* <ul>
61+
* <li>Reads the raw HTTP request body from the {@code REQUEST} context key.</li>
62+
* <li>Deserializes the content into the configured target class using the shared {@link Converter#getMapper()}.</li>
63+
* <li>Performs bean validation on the resulting object.</li>
64+
* <li>If validation passes, stores the object in the context and returns {@code PREPARED | READONLY | NO_JOIN}.</li>
65+
* <li>If validation fails or deserialization fails, logs the issue and returns {@code FAIL}.</li>
66+
* </ul>
67+
*
68+
* <p>
69+
* This is commonly used in REST workflows to extract and validate DTOs prior to persistence or processing.
70+
* </p>
71+
*
72+
* <h2>Example Q2 Configuration</h2>
73+
* <pre>{@code
74+
* <participant class="org.jpos.qrest.ExtractJSONObject">
75+
* <property name="class" value="com.example.dto.CustomerDTO"/>
76+
* <property name="context-name" value="customer"/>
77+
* </participant>
78+
* }</pre>
79+
*
80+
* @author jPOS
81+
* @since 3.0.1
82+
*/
83+
84+
public class ExtractJSONObject implements TransactionParticipant, Configurable {
85+
@SuppressWarnings("rawtypes")
86+
private Class clazz;
87+
@Config("class")
88+
private String className;
89+
90+
@Config("context-name")
91+
private String contextName;
92+
93+
@Override
94+
public int prepare(long id, Serializable context) {
95+
Context ctx = (Context) context;
96+
Object obj = getObject(ctx);
97+
ctx.put (contextName, obj);
98+
return obj != null ? (PREPARED | READONLY | NO_JOIN) : FAIL;
99+
}
100+
101+
@Override
102+
public void setConfiguration (Configuration cfg) throws ConfigurationException {
103+
try {
104+
clazz = Class.forName(className);
105+
} catch (ClassNotFoundException e) {
106+
throw new ConfigurationException(e);
107+
}
108+
}
109+
110+
@SuppressWarnings("unchecked")
111+
private @Nullable Object getObject(Context ctx) {
112+
Object obj = null;
113+
try {
114+
FullHttpRequest request = ctx.get(REQUEST);
115+
String jsonRequest = request.content().toString(CharsetUtil.UTF_8);
116+
obj = Converter.getMapper().readValue(jsonRequest, clazz);
117+
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
118+
Validator validator = factory.getValidator();
119+
Set<ConstraintViolation<Object>> violations = validator.validate(obj);
120+
if (!violations.isEmpty()) {
121+
for (ConstraintViolation<Object> violation : violations) {
122+
ctx.log("Validation error: " + violation.getMessage());
123+
}
124+
ctx.put(RESPONSE, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
125+
return null;
126+
}
127+
ctx.remove(JSON_REQUEST.name()); // unclutter context
128+
} catch (JsonProcessingException e) {
129+
ctx.log (e);
130+
}
131+
return obj;
132+
}
133+
}

0 commit comments

Comments
 (0)