Skip to content

Commit f652545

Browse files
committed
Add SPI for registering external AuditLogEvent types
Replace the static @JsonSubTypes list on AuditLogEvent with AuditLogEventRegistry, which holds the built-in type ids and merges mappings contributed by AuditLogEventProvider implementations through ServiceLoader. JsonlLogWriter and the JSON/XML renderers register subtypes via the registry so external modules can ship their own events without modifying jPOS. Built-in ids and serialization are unchanged.
1 parent ebe00da commit f652545

11 files changed

Lines changed: 410 additions & 22 deletions

jpos/src/main/java/org/jpos/log/AuditLogEvent.java

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,19 @@
1818

1919
package org.jpos.log;
2020

21-
import com.fasterxml.jackson.annotation.JsonSubTypes;
2221
import com.fasterxml.jackson.annotation.JsonTypeInfo;
23-
import org.jpos.log.evt.*;
2422

23+
/**
24+
* Marker interface for log payload event records.
25+
*
26+
* <p>Concrete implementations are mapped to a stable type id through
27+
* {@link AuditLogEventRegistry}. Built-in ids are listed there; external
28+
* modules contribute their own via {@link AuditLogEventProvider} and
29+
* {@link java.util.ServiceLoader}.</p>
30+
*/
2531
@JsonTypeInfo(
2632
use = JsonTypeInfo.Id.NAME,
2733
include = JsonTypeInfo.As.PROPERTY,
2834
property = "t"
2935
)
30-
@JsonSubTypes({
31-
@JsonSubTypes.Type(value = Warning.class, name = "warn"),
32-
@JsonSubTypes.Type(value = Start.class, name = "start"),
33-
@JsonSubTypes.Type(value = Stop.class, name = "stop"),
34-
@JsonSubTypes.Type(value = Deploy.class, name = "deploy"),
35-
@JsonSubTypes.Type(value = UnDeploy.class, name = "undeploy"),
36-
@JsonSubTypes.Type(value = LogMessage.class, name = "msg"),
37-
@JsonSubTypes.Type(value = Shutdown.class, name = "shutdown"),
38-
@JsonSubTypes.Type(value = DeployActivity.class, name = "deploy-activity"),
39-
@JsonSubTypes.Type(value = ThrowableAuditLogEvent.class, name = "throwable"),
40-
@JsonSubTypes.Type(value = License.class, name = "license"),
41-
@JsonSubTypes.Type(value = SysInfo.class, name = "sysinfo"),
42-
@JsonSubTypes.Type(value = Connect.class, name = "connect"),
43-
@JsonSubTypes.Type(value = Disconnect.class, name = "disconnect"),
44-
@JsonSubTypes.Type(value = Listen.class, name = "listen"),
45-
@JsonSubTypes.Type(value = SessionStart.class, name = "session-start"),
46-
@JsonSubTypes.Type(value = SessionEnd.class, name = "session-end"),
47-
@JsonSubTypes.Type(value = Txn.class, name = "txn")
48-
})
49-
5036
public interface AuditLogEvent { }
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* jPOS Project [http://jpos.org]
3+
* Copyright (C) 2000-2026 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.log;
20+
21+
import java.util.Collection;
22+
23+
/**
24+
* SPI for contributing additional {@link AuditLogEvent} implementations.
25+
*
26+
* <p>External modules register their event classes by declaring an implementation in
27+
* {@code META-INF/services/org.jpos.log.AuditLogEventProvider}; the
28+
* {@link AuditLogEventRegistry} discovers and merges them with the built-in mappings
29+
* via {@link java.util.ServiceLoader}.</p>
30+
*
31+
* <p>Type ids must be unique. Providers must not reuse any of the built-in ids
32+
* ({@code warn}, {@code start}, {@code stop}, {@code deploy}, {@code undeploy},
33+
* {@code msg}, {@code shutdown}, {@code deploy-activity}, {@code throwable},
34+
* {@code license}, {@code sysinfo}, {@code connect}, {@code disconnect},
35+
* {@code listen}, {@code session-start}, {@code session-end}, {@code txn}).</p>
36+
*
37+
* @since 3.0.0
38+
*/
39+
public interface AuditLogEventProvider {
40+
/**
41+
* @return the type mappings contributed by this provider; must not be {@code null}.
42+
*/
43+
Collection<AuditLogEventType> types();
44+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* jPOS Project [http://jpos.org]
3+
* Copyright (C) 2000-2026 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.log;
20+
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
import com.fasterxml.jackson.databind.jsontype.NamedType;
23+
import org.jpos.log.evt.Connect;
24+
import org.jpos.log.evt.Deploy;
25+
import org.jpos.log.evt.DeployActivity;
26+
import org.jpos.log.evt.Disconnect;
27+
import org.jpos.log.evt.License;
28+
import org.jpos.log.evt.Listen;
29+
import org.jpos.log.evt.LogMessage;
30+
import org.jpos.log.evt.SessionEnd;
31+
import org.jpos.log.evt.SessionStart;
32+
import org.jpos.log.evt.Shutdown;
33+
import org.jpos.log.evt.Start;
34+
import org.jpos.log.evt.Stop;
35+
import org.jpos.log.evt.SysInfo;
36+
import org.jpos.log.evt.ThrowableAuditLogEvent;
37+
import org.jpos.log.evt.Txn;
38+
import org.jpos.log.evt.UnDeploy;
39+
import org.jpos.log.evt.Warning;
40+
41+
import java.util.Collection;
42+
import java.util.Collections;
43+
import java.util.LinkedHashMap;
44+
import java.util.List;
45+
import java.util.Map;
46+
import java.util.ServiceLoader;
47+
48+
/**
49+
* Registry of {@link AuditLogEvent} type mappings.
50+
*
51+
* <p>Holds the built-in type ids and discovers additional ones contributed by
52+
* {@link AuditLogEventProvider} implementations through
53+
* {@link ServiceLoader}. The result is a single source of truth used by every
54+
* Jackson-based renderer in jPOS to register subtypes on its {@code ObjectMapper}.</p>
55+
*
56+
* <p>The registry is initialized lazily on first use. Built-in entries are
57+
* always registered first so external providers cannot shadow them: any
58+
* provider entry that re-uses a built-in id, or that collides with another
59+
* provider entry, is rejected with {@link IllegalStateException}.</p>
60+
*
61+
* @since 3.0.0
62+
*/
63+
public final class AuditLogEventRegistry {
64+
private static final List<AuditLogEventType> BUILTINS = List.of(
65+
new AuditLogEventType("warn", Warning.class),
66+
new AuditLogEventType("start", Start.class),
67+
new AuditLogEventType("stop", Stop.class),
68+
new AuditLogEventType("deploy", Deploy.class),
69+
new AuditLogEventType("undeploy", UnDeploy.class),
70+
new AuditLogEventType("msg", LogMessage.class),
71+
new AuditLogEventType("shutdown", Shutdown.class),
72+
new AuditLogEventType("deploy-activity", DeployActivity.class),
73+
new AuditLogEventType("throwable", ThrowableAuditLogEvent.class),
74+
new AuditLogEventType("license", License.class),
75+
new AuditLogEventType("sysinfo", SysInfo.class),
76+
new AuditLogEventType("connect", Connect.class),
77+
new AuditLogEventType("disconnect", Disconnect.class),
78+
new AuditLogEventType("listen", Listen.class),
79+
new AuditLogEventType("session-start", SessionStart.class),
80+
new AuditLogEventType("session-end", SessionEnd.class),
81+
new AuditLogEventType("txn", Txn.class)
82+
);
83+
84+
private static volatile Map<String, AuditLogEventType> types;
85+
86+
private AuditLogEventRegistry() { }
87+
88+
/**
89+
* @return all known type mappings, built-ins first, then ServiceLoader-discovered.
90+
*/
91+
public static Collection<AuditLogEventType> types() {
92+
return load().values();
93+
}
94+
95+
/**
96+
* Registers every known {@link AuditLogEvent} subtype on the given
97+
* {@code ObjectMapper}. Renderers should call this once after constructing
98+
* their mapper.
99+
*
100+
* @param mapper the Jackson {@link ObjectMapper} (or subclass, e.g. {@code XmlMapper})
101+
* @return the same mapper for chaining
102+
*/
103+
public static <M extends ObjectMapper> M register(M mapper) {
104+
for (AuditLogEventType t : load().values()) {
105+
mapper.registerSubtypes(new NamedType(t.clazz(), t.name()));
106+
}
107+
return mapper;
108+
}
109+
110+
/**
111+
* Reloads the registry. Visible for testing — callers shouldn't need this in
112+
* production, since mappings discovered through {@link ServiceLoader} are
113+
* fixed for the lifetime of the JVM.
114+
*/
115+
static synchronized void reload() {
116+
types = null;
117+
load();
118+
}
119+
120+
private static Map<String, AuditLogEventType> load() {
121+
Map<String, AuditLogEventType> snapshot = types;
122+
if (snapshot != null)
123+
return snapshot;
124+
synchronized (AuditLogEventRegistry.class) {
125+
if (types != null)
126+
return types;
127+
Map<String, AuditLogEventType> map = new LinkedHashMap<>();
128+
for (AuditLogEventType t : BUILTINS)
129+
map.put(t.name(), t);
130+
for (AuditLogEventProvider provider : ServiceLoader.load(AuditLogEventProvider.class)) {
131+
Collection<AuditLogEventType> contributed = provider.types();
132+
if (contributed == null)
133+
continue;
134+
for (AuditLogEventType t : contributed) {
135+
AuditLogEventType existing = map.get(t.name());
136+
if (existing != null && !existing.equals(t)) {
137+
throw new IllegalStateException(
138+
"AuditLogEventProvider " + provider.getClass().getName()
139+
+ " attempted to register conflicting type id '" + t.name()
140+
+ "' (already mapped to " + existing.clazz().getName() + ")");
141+
}
142+
map.put(t.name(), t);
143+
}
144+
}
145+
types = Collections.unmodifiableMap(map);
146+
return types;
147+
}
148+
}
149+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* jPOS Project [http://jpos.org]
3+
* Copyright (C) 2000-2026 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.log;
20+
21+
import java.util.Objects;
22+
23+
/**
24+
* Pairs a stable type id with the {@link AuditLogEvent} implementation it identifies.
25+
*
26+
* <p>Used by {@link AuditLogEventProvider} implementations and by
27+
* {@link AuditLogEventRegistry} to register Jackson subtype mappings.</p>
28+
*
29+
* @param name stable type id used as the JSON/XML discriminator value (e.g. {@code "warn"})
30+
* @param clazz the {@link AuditLogEvent} implementation
31+
*
32+
* @since 3.0.0
33+
*/
34+
public record AuditLogEventType(String name, Class<? extends AuditLogEvent> clazz) {
35+
public AuditLogEventType {
36+
Objects.requireNonNull(name, "name");
37+
Objects.requireNonNull(clazz, "clazz");
38+
if (name.isBlank())
39+
throw new IllegalArgumentException("name must not be blank");
40+
}
41+
}

jpos/src/main/java/org/jpos/log/render/json/AuditLogEventJsonLogRenderer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.fasterxml.jackson.databind.ObjectMapper;
2323
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
2424
import org.jpos.log.AuditLogEvent;
25+
import org.jpos.log.AuditLogEventRegistry;
2526
import org.jpos.log.LogRenderer;
2627

2728
import java.io.PrintStream;
@@ -32,6 +33,7 @@ public final class AuditLogEventJsonLogRenderer implements LogRenderer<AuditLogE
3233
public AuditLogEventJsonLogRenderer () {
3334
mapper.registerModule(new JavaTimeModule());
3435
mapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
36+
AuditLogEventRegistry.register(mapper);
3537
}
3638

3739
@Override

jpos/src/main/java/org/jpos/log/render/json/LogEventJsonLogRenderer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.fasterxml.jackson.databind.module.SimpleModule;
2525
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
2626
import org.jpos.log.AuditLogEvent;
27+
import org.jpos.log.AuditLogEventRegistry;
2728

2829
import org.jpos.log.LogRenderer;
2930
import org.jpos.log.evt.LogEvt;
@@ -51,6 +52,7 @@ public LogEventJsonLogRenderer() {
5152
SimpleModule module = new SimpleModule();
5253
module.addSerializer(Throwable.class, new ThrowableSerializer());
5354
mapper.registerModule(module);
55+
AuditLogEventRegistry.register(mapper);
5456
}
5557

5658
@Override

jpos/src/main/java/org/jpos/log/render/xml/LogEventXmlLogRenderer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
2525
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
2626
import org.jpos.log.AuditLogEvent;
27+
import org.jpos.log.AuditLogEventRegistry;
2728

2829
import org.jpos.log.LogRenderer;
2930
import org.jpos.log.evt.LogEvt;
@@ -53,6 +54,7 @@ public LogEventXmlLogRenderer() {
5354
SimpleModule module = new SimpleModule();
5455
module.addSerializer(Throwable.class, new ThrowableSerializer());
5556
mapper.registerModule(module);
57+
AuditLogEventRegistry.register(mapper);
5658
}
5759

5860
@Override

jpos/src/main/java/org/jpos/util/JsonlLogWriter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.jpos.iso.ISOMsg;
3030
import org.jpos.iso.ISOUtil;
3131
import org.jpos.log.AuditLogEvent;
32+
import org.jpos.log.AuditLogEventRegistry;
3233
import org.jpos.log.evt.LogEvt;
3334
import org.jpos.log.evt.LogMessage;
3435
import org.jpos.log.evt.ThrowableAuditLogEvent;
@@ -172,6 +173,7 @@ private void initMapper() {
172173
SimpleModule module = new SimpleModule();
173174
module.addSerializer(Throwable.class, new ThrowableSerializer());
174175
mapper.registerModule(module);
176+
AuditLogEventRegistry.register(mapper);
175177
}
176178

177179
private static Set<Integer> toIntSet(String spaceSeparated) {

0 commit comments

Comments
 (0)