From 4e7ac9d935048b8a2a97436db4d1f8950a603f3f Mon Sep 17 00:00:00 2001 From: Ozzy Espaillat Date: Mon, 18 Dec 2023 17:04:12 -0800 Subject: [PATCH 1/4] master: Add annotation functionality to jPos participant. --- jpos/build.gradle | 2 + jpos/libraries.gradle | 3 +- .../jpos/annotation/AnnotatedParticipant.java | 12 + .../java/org/jpos/annotation/ContextKey.java | 12 + .../java/org/jpos/annotation/Prepare.java | 14 + .../java/org/jpos/annotation/Registry.java | 12 + .../main/java/org/jpos/annotation/Return.java | 12 + .../jpos/annotation/resolvers/Priority.java | 7 + .../annotation/resolvers/ResolverFactory.java | 63 +++ .../resolvers/ResolverProviderList.java | 41 ++ .../GenericExceptionHandlerProvider.java | 39 ++ .../exception/ReturnExceptionHandler.java | 27 ++ .../ReturnExceptionHandlerProvider.java | 9 + .../parameters/ContextPassThruResolver.java | 29 ++ .../resolvers/parameters/ContextResolver.java | 33 ++ .../parameters/RegistryResolver.java | 85 ++++ .../resolvers/parameters/Resolver.java | 13 + .../parameters/ResolverServiceProvider.java | 10 + .../IntPassthruContextReturnHandler.java | 18 + .../MultiValueContextReturnHandler.java | 35 ++ .../resolvers/response/ReturnHandler.java | 12 + .../response/ReturnHandlerProvider.java | 10 + .../SingleValueContextReturnHandler.java | 31 ++ .../response/VoidContextReturnHandler.java | 20 + .../AnnotatedParticipantWrapper.java | 127 ++++++ .../jpos/transaction/TransactionManager.java | 3 + ...s.exception.ReturnExceptionHandlerProvider | 1 + ...solvers.parameters.ResolverServiceProvider | 3 + ...n.resolvers.response.ReturnHandlerProvider | 4 + .../AnnotatedParticipantWrapperTest.java | 409 ++++++++++++++++++ 30 files changed, 1095 insertions(+), 1 deletion(-) create mode 100644 jpos/src/main/java/org/jpos/annotation/AnnotatedParticipant.java create mode 100644 jpos/src/main/java/org/jpos/annotation/ContextKey.java create mode 100644 jpos/src/main/java/org/jpos/annotation/Prepare.java create mode 100644 jpos/src/main/java/org/jpos/annotation/Registry.java create mode 100644 jpos/src/main/java/org/jpos/annotation/Return.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/Priority.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/ResolverFactory.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/ResolverProviderList.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/exception/GenericExceptionHandlerProvider.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/exception/ReturnExceptionHandler.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/exception/ReturnExceptionHandlerProvider.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextPassThruResolver.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextResolver.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/parameters/RegistryResolver.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/parameters/Resolver.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ResolverServiceProvider.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/response/IntPassthruContextReturnHandler.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/response/MultiValueContextReturnHandler.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/response/ReturnHandler.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/response/ReturnHandlerProvider.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/response/SingleValueContextReturnHandler.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/response/VoidContextReturnHandler.java create mode 100644 jpos/src/main/java/org/jpos/transaction/AnnotatedParticipantWrapper.java create mode 100644 jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.exception.ReturnExceptionHandlerProvider create mode 100644 jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.parameters.ResolverServiceProvider create mode 100644 jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.response.ReturnHandlerProvider create mode 100644 jpos/src/test/java/org/jpos/transaction/AnnotatedParticipantWrapperTest.java diff --git a/jpos/build.gradle b/jpos/build.gradle index c6467cff6b..d6356541bf 100644 --- a/jpos/build.gradle +++ b/jpos/build.gradle @@ -28,6 +28,8 @@ dependencies { implementation libraries.sleepycat_je implementation libraries.sshd implementation libraries.eddsa + implementation libraries.bytebuddy + implementation libraries.commons_lang3 testImplementation libraries.commons_lang3 testImplementation libraries.hamcrest diff --git a/jpos/libraries.gradle b/jpos/libraries.gradle index fb9de452da..faf91083dd 100644 --- a/jpos/libraries.gradle +++ b/jpos/libraries.gradle @@ -25,7 +25,8 @@ ext { slf4j_api: "org.slf4j:slf4j-api:1.7.32", slf4j_nop: "org.slf4j:slf4j-nop:1.7.32", hdrhistogram: 'org.hdrhistogram:HdrHistogram:2.1.12', - yaml: "org.yaml:snakeyaml:2.0" + yaml: "org.yaml:snakeyaml:2.0", + bytebuddy: "net.bytebuddy:byte-buddy:1.14.10" ] } diff --git a/jpos/src/main/java/org/jpos/annotation/AnnotatedParticipant.java b/jpos/src/main/java/org/jpos/annotation/AnnotatedParticipant.java new file mode 100644 index 0000000000..c43db022b3 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/AnnotatedParticipant.java @@ -0,0 +1,12 @@ +package org.jpos.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface AnnotatedParticipant{ + +} diff --git a/jpos/src/main/java/org/jpos/annotation/ContextKey.java b/jpos/src/main/java/org/jpos/annotation/ContextKey.java new file mode 100644 index 0000000000..4349b393e4 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/ContextKey.java @@ -0,0 +1,12 @@ +package org.jpos.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ContextKey { + public String value(); +} diff --git a/jpos/src/main/java/org/jpos/annotation/Prepare.java b/jpos/src/main/java/org/jpos/annotation/Prepare.java new file mode 100644 index 0000000000..2de59c9066 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/Prepare.java @@ -0,0 +1,14 @@ +package org.jpos.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.jpos.transaction.TransactionConstants; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Prepare { + public int result() default TransactionConstants.PREPARED; +} diff --git a/jpos/src/main/java/org/jpos/annotation/Registry.java b/jpos/src/main/java/org/jpos/annotation/Registry.java new file mode 100644 index 0000000000..5cd0b7b0f8 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/Registry.java @@ -0,0 +1,12 @@ +package org.jpos.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Registry { + public String value() default ""; +} diff --git a/jpos/src/main/java/org/jpos/annotation/Return.java b/jpos/src/main/java/org/jpos/annotation/Return.java new file mode 100644 index 0000000000..d087235e4d --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/Return.java @@ -0,0 +1,12 @@ +package org.jpos.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Return { + String[] value(); +} diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/Priority.java b/jpos/src/main/java/org/jpos/annotation/resolvers/Priority.java new file mode 100644 index 0000000000..95b48e9580 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/Priority.java @@ -0,0 +1,7 @@ +package org.jpos.annotation.resolvers; + +public interface Priority { + default int getPriority() { + return 10; + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/ResolverFactory.java b/jpos/src/main/java/org/jpos/annotation/resolvers/ResolverFactory.java new file mode 100644 index 0000000000..612011cfab --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/ResolverFactory.java @@ -0,0 +1,63 @@ +package org.jpos.annotation.resolvers; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.List; + +import org.jpos.annotation.resolvers.exception.ReturnExceptionHandler; +import org.jpos.annotation.resolvers.exception.ReturnExceptionHandlerProvider; +import org.jpos.annotation.resolvers.parameters.Resolver; +import org.jpos.annotation.resolvers.parameters.ResolverServiceProvider; +import org.jpos.annotation.resolvers.response.ReturnHandler; +import org.jpos.annotation.resolvers.response.ReturnHandlerProvider; +import org.jpos.core.ConfigurationException; + +public class ResolverFactory { + public static final ResolverFactory INSTANCE = new ResolverFactory(); + + protected final ResolverProviderList resolvers = new ResolverProviderList(); + + private ResolverFactory() {} + + public Resolver getResolver(Parameter p) throws ConfigurationException { + Resolver r = null; + for (ResolverServiceProvider f: resolvers.getResolvers()) { + if (f.isMatch(p)) { + r = f.resolve(p); + r.configure(p); + break; + } + } + if (r == null) { + throw new ConfigurationException("Prepare parameter " + p.getName() + " does not have the required annotation."); + } + return r; + } + + public ReturnHandler getReturnHandler(Method m) throws ConfigurationException { + ReturnHandler r = null; + for (ReturnHandlerProvider f: resolvers.getReturnHandlers()) { + if (f.isMatch(m)) { + r = f.resolve(m); + r.configure(m); + break; + } + } + if (r == null) { + throw new ConfigurationException("Could not find a valid provider for return " + m.getName()); + } + return r; + } + + public List getExceptionHandlers(Method m) { + List exceptionHandlers = new ArrayList<>(); + for(ReturnExceptionHandlerProvider p: resolvers.getExceptionResolvers()) { + ReturnExceptionHandler r = p.resolve(m); + r.configure(m); + exceptionHandlers.add(r); + } + return exceptionHandlers; + } + +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/ResolverProviderList.java b/jpos/src/main/java/org/jpos/annotation/resolvers/ResolverProviderList.java new file mode 100644 index 0000000000..906e933157 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/ResolverProviderList.java @@ -0,0 +1,41 @@ +package org.jpos.annotation.resolvers; + +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +import org.jpos.annotation.resolvers.exception.ReturnExceptionHandlerProvider; +import org.jpos.annotation.resolvers.parameters.ResolverServiceProvider; +import org.jpos.annotation.resolvers.response.ReturnHandlerProvider; + +public class ResolverProviderList { + protected final List resolvers = new ArrayList<>(); + protected final List resultHandlers = new ArrayList<>(); + protected final List exceptionHandlers = new ArrayList<>(); + + ResolverProviderList() { + loadServiceProviders(resolvers, ResolverServiceProvider.class); + loadServiceProviders(resultHandlers, ReturnHandlerProvider.class); + loadServiceProviders(exceptionHandlers, ReturnExceptionHandlerProvider.class); + } + + protected void loadServiceProviders(List list, Class svcClass) { + ServiceLoader svcLoader = ServiceLoader.load(svcClass); + for(T serviceImp: svcLoader) { + list.add(serviceImp); + } + list.sort((o1, o2) -> Integer.compare(o1.getPriority(), o2.getPriority())); + } + + + public List getReturnHandlers() { + return resultHandlers; + } + public List getExceptionResolvers() { + return exceptionHandlers; + } + + public List getResolvers() { + return resolvers; + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/exception/GenericExceptionHandlerProvider.java b/jpos/src/main/java/org/jpos/annotation/resolvers/exception/GenericExceptionHandlerProvider.java new file mode 100644 index 0000000000..adc9240f86 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/exception/GenericExceptionHandlerProvider.java @@ -0,0 +1,39 @@ +package org.jpos.annotation.resolvers.exception; + +import java.lang.reflect.Method; + +import org.jpos.rc.CMF; +import org.jpos.transaction.Context; +import org.jpos.transaction.TransactionConstants; +import org.jpos.transaction.TransactionParticipant; + +public class GenericExceptionHandlerProvider implements ReturnExceptionHandlerProvider { + static class GenericExceptionHandler implements ReturnExceptionHandler { + + @Override + public boolean isMatch(Throwable e) { + return getException(e, Exception.class) != null; + } + + @Override + public int doReturn(TransactionParticipant p, Context ctx, Throwable t) { + ctx.log("prepare exception in " + this.getClass().getName()); + ctx.log(t); + setResultCode(ctx, CMF.INTERNAL_ERROR); + + return TransactionConstants.ABORTED; + } + + } + + @Override + public ReturnExceptionHandler resolve(Method m) { + return new GenericExceptionHandler(); + } + + @Override + public int getPriority() { + return Integer.MAX_VALUE; + } + +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/exception/ReturnExceptionHandler.java b/jpos/src/main/java/org/jpos/annotation/resolvers/exception/ReturnExceptionHandler.java new file mode 100644 index 0000000000..c9ace8c39f --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/exception/ReturnExceptionHandler.java @@ -0,0 +1,27 @@ +package org.jpos.annotation.resolvers.exception; + +import java.lang.reflect.Method; + +import org.jpos.rc.IRC; +import org.jpos.transaction.Context; +import org.jpos.transaction.ContextConstants; +import org.jpos.transaction.TransactionParticipant; + +public interface ReturnExceptionHandler { + boolean isMatch(Throwable e); + int doReturn(TransactionParticipant p, Context ctx, Throwable obj); + + default void configure(Method m) {} + + default void setResultCode(Context ctx, IRC irc) { + ctx.put(ContextConstants.IRC, irc); + } + + default T getException(Throwable e, Class type) { + int stackDepth= 10; + do { + if (type.isAssignableFrom(e.getClass())) return (T) e; + } while (null != (e = e.getCause()) && stackDepth-- > 0); + return null; + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/exception/ReturnExceptionHandlerProvider.java b/jpos/src/main/java/org/jpos/annotation/resolvers/exception/ReturnExceptionHandlerProvider.java new file mode 100644 index 0000000000..53f6022b9f --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/exception/ReturnExceptionHandlerProvider.java @@ -0,0 +1,9 @@ +package org.jpos.annotation.resolvers.exception; + +import java.lang.reflect.Method; + +import org.jpos.annotation.resolvers.Priority; + +public interface ReturnExceptionHandlerProvider extends Priority { + ReturnExceptionHandler resolve(Method m); +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextPassThruResolver.java b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextPassThruResolver.java new file mode 100644 index 0000000000..fd692f34b6 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextPassThruResolver.java @@ -0,0 +1,29 @@ +package org.jpos.annotation.resolvers.parameters; + +import java.lang.reflect.Parameter; + +import org.jpos.transaction.Context; +import org.jpos.transaction.TransactionParticipant; + +public class ContextPassThruResolver implements ResolverServiceProvider { + + @Override + public boolean isMatch(Parameter p) { + return Context.class.isAssignableFrom(p.getType()); + } + + @Override + public int getPriority() { + return 1000; + } + + @Override + public Resolver resolve(Parameter p) { + return new Resolver() { + @Override + public T getValue(TransactionParticipant participant, Context ctx) { + return (T) ctx; + } + }; + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextResolver.java b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextResolver.java new file mode 100644 index 0000000000..63cbcfce05 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextResolver.java @@ -0,0 +1,33 @@ +package org.jpos.annotation.resolvers.parameters; + +import java.lang.reflect.Parameter; + +import org.jpos.annotation.ContextKey; +import org.jpos.transaction.Context; +import org.jpos.transaction.TransactionParticipant; + +public class ContextResolver implements ResolverServiceProvider { + + @Override + public boolean isMatch(Parameter p) { + return p.isAnnotationPresent(ContextKey.class); + } + + @Override + public Resolver resolve(Parameter p) { + return new Resolver() { + String ctxKey; + + @Override + public void configure(Parameter f) { + ContextKey annotation = f.getAnnotation(ContextKey.class); + ctxKey = annotation.value(); + } + + @Override + public T getValue(TransactionParticipant participant, Context ctx) { + return ctx.get(ctxKey); + } + }; + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/RegistryResolver.java b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/RegistryResolver.java new file mode 100644 index 0000000000..c08ba107d1 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/RegistryResolver.java @@ -0,0 +1,85 @@ +package org.jpos.annotation.resolvers.parameters; + +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.lang3.StringUtils; +import org.jpos.annotation.Registry; +import org.jpos.core.ConfigurationException; +import org.jpos.transaction.Context; +import org.jpos.transaction.TransactionParticipant; +import org.jpos.util.NameRegistrar; + + +public class RegistryResolver implements ResolverServiceProvider { + private static final class RegistryResolverImpl implements Resolver { + String registryKey; + + @Override + public void configure(Parameter f) throws ConfigurationException { + Registry annotation = f.getAnnotation(Registry.class); + registryKey = findKey(annotation.value(), f.getType(), NameRegistrar.getAsMap()); + if (StringUtils.isEmpty(registryKey)) { + throw new ConfigurationException("Could not find Registry entry for " + f.getName()); + } + } + + @Override + public T getValue(TransactionParticipant participant, Context ctx) { + return NameRegistrar.getIfExists(registryKey); + } + + String findKey(String key, Class type, Map entries) throws ConfigurationException { + if (entries.containsKey(key)) { + return key; + } + List typeMatches = new ArrayList<>(); + List keyMatches = new ArrayList<>(); + findPotentialMatches(key, type, entries, typeMatches, keyMatches); + return getMatch(key, typeMatches, keyMatches); + } + + protected String getMatch(String key, List typeMatches, List keyMatches) + throws ConfigurationException { + if (StringUtils.isNotBlank(key)) { + return getMatch(key, keyMatches); + } else { + return getMatch(key, typeMatches); + } + } + + protected void findPotentialMatches(String key, Class type, Map entries, List typeMatches, + List keyMatches) { + for (Entry entry: entries.entrySet()) { + String mKey = String.valueOf(entry.getKey()); + if (mKey.equalsIgnoreCase(key)) { + keyMatches.add(mKey); + } + if (type.isAssignableFrom(entry.getValue().getClass())) { + typeMatches.add(mKey); + } + } + } + + protected String getMatch(String key, List keyMatches) throws ConfigurationException { + switch(keyMatches.size()) { + case 0: return null; + case 1: return keyMatches.get(0); + default : throw new ConfigurationException("Found multiple matches for key " + key); + } + } + } + + @Override + public boolean isMatch(Parameter p) { + return p.isAnnotationPresent(org.jpos.annotation.Registry.class); + } + + @Override + public Resolver resolve(Parameter p) { + return new RegistryResolverImpl(); + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/Resolver.java b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/Resolver.java new file mode 100644 index 0000000000..8625cea29d --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/Resolver.java @@ -0,0 +1,13 @@ +package org.jpos.annotation.resolvers.parameters; + +import java.lang.reflect.Parameter; + +import org.jpos.core.ConfigurationException; +import org.jpos.transaction.Context; +import org.jpos.transaction.TransactionParticipant; + +public interface Resolver { + default void configure(Parameter f) throws ConfigurationException { + } + T getValue(TransactionParticipant participant, Context ctx); +} diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ResolverServiceProvider.java b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ResolverServiceProvider.java new file mode 100644 index 0000000000..cd350a32c9 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ResolverServiceProvider.java @@ -0,0 +1,10 @@ +package org.jpos.annotation.resolvers.parameters; + +import java.lang.reflect.Parameter; + +import org.jpos.annotation.resolvers.Priority; + +public interface ResolverServiceProvider extends Priority { + boolean isMatch(Parameter p); + Resolver resolve(Parameter p); +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/response/IntPassthruContextReturnHandler.java b/jpos/src/main/java/org/jpos/annotation/resolvers/response/IntPassthruContextReturnHandler.java new file mode 100644 index 0000000000..570b6ef1a7 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/IntPassthruContextReturnHandler.java @@ -0,0 +1,18 @@ +package org.jpos.annotation.resolvers.response; + +import java.lang.reflect.Method; + +import org.jpos.annotation.Return; + +public class IntPassthruContextReturnHandler implements ReturnHandlerProvider { + + @Override + public boolean isMatch(Method m) { + return !m.isAnnotationPresent(Return.class) && int.class.isAssignableFrom(m.getReturnType()); + } + + @Override + public ReturnHandler resolve(Method m) { + return (participant, ctx, res) -> (int) res; + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/response/MultiValueContextReturnHandler.java b/jpos/src/main/java/org/jpos/annotation/resolvers/response/MultiValueContextReturnHandler.java new file mode 100644 index 0000000000..a232608f18 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/MultiValueContextReturnHandler.java @@ -0,0 +1,35 @@ +package org.jpos.annotation.resolvers.response; + +import java.lang.reflect.Method; +import java.util.Map; + +import org.jpos.annotation.Prepare; +import org.jpos.annotation.Return; + +public class MultiValueContextReturnHandler implements ReturnHandlerProvider { + @Override + public boolean isMatch(Method m) { + if (Map.class.isAssignableFrom(m.getReturnType()) && m.isAnnotationPresent(Return.class)) { + Return r = m.getAnnotation(Return.class); + return r.value().length > 1; + } + + return false; + } + + @Override + public ReturnHandler resolve(Method m) { + Return r = m.getAnnotation(Return.class); + final String[] keys = r.value(); + final int jPosRes = m.getAnnotation(Prepare.class).result(); + return (participant, ctx, res) -> { + Map resMap = (Map) res; + for(String key: keys) { + if (resMap != null && resMap.containsKey(key)) { + ctx.put(key, resMap.get(key)); + } + } + return jPosRes; + }; + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/response/ReturnHandler.java b/jpos/src/main/java/org/jpos/annotation/resolvers/response/ReturnHandler.java new file mode 100644 index 0000000000..052adc2fdc --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/ReturnHandler.java @@ -0,0 +1,12 @@ +package org.jpos.annotation.resolvers.response; + +import java.lang.reflect.Method; + +import org.jpos.transaction.Context; +import org.jpos.transaction.TransactionParticipant; + +public interface ReturnHandler { + int doReturn(TransactionParticipant p, Context ctx, Object obj); + + default void configure(Method m) {} +} diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/response/ReturnHandlerProvider.java b/jpos/src/main/java/org/jpos/annotation/resolvers/response/ReturnHandlerProvider.java new file mode 100644 index 0000000000..8fa19814d8 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/ReturnHandlerProvider.java @@ -0,0 +1,10 @@ +package org.jpos.annotation.resolvers.response; + +import java.lang.reflect.Method; + +import org.jpos.annotation.resolvers.Priority; + +public interface ReturnHandlerProvider extends Priority { + boolean isMatch(Method m); + ReturnHandler resolve(Method m); +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/response/SingleValueContextReturnHandler.java b/jpos/src/main/java/org/jpos/annotation/resolvers/response/SingleValueContextReturnHandler.java new file mode 100644 index 0000000000..3c9050400c --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/SingleValueContextReturnHandler.java @@ -0,0 +1,31 @@ +package org.jpos.annotation.resolvers.response; + +import java.lang.reflect.Method; + +import org.jpos.annotation.Prepare; +import org.jpos.annotation.Return; + +public class SingleValueContextReturnHandler implements ReturnHandlerProvider { + @Override + public boolean isMatch(Method m) { + if (m.isAnnotationPresent(Return.class) && !Void.TYPE.equals(m.getReturnType())) { + Return r = m.getAnnotation(Return.class); + return r.value().length == 1; + } + + return false; + } + + @Override + public ReturnHandler resolve(Method m) { + Return r = m.getAnnotation(Return.class); + final String key = r.value()[0]; + final int jPosRes = m.getAnnotation(Prepare.class).result(); + return (participant, ctx, res) -> { + if (res != null) { + ctx.put(key, res); + } + return jPosRes; + }; + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/response/VoidContextReturnHandler.java b/jpos/src/main/java/org/jpos/annotation/resolvers/response/VoidContextReturnHandler.java new file mode 100644 index 0000000000..250a97d626 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/VoidContextReturnHandler.java @@ -0,0 +1,20 @@ +package org.jpos.annotation.resolvers.response; + +import java.lang.reflect.Method; + +import org.jpos.annotation.Prepare; +import org.jpos.annotation.Return; + +public class VoidContextReturnHandler implements ReturnHandlerProvider { + + @Override + public boolean isMatch(Method m) { + return Void.TYPE.equals(m.getReturnType()) && !m.isAnnotationPresent(Return.class); + } + + @Override + public ReturnHandler resolve(Method m) { + final int jPosRes = m.getAnnotation(Prepare.class).result(); + return (participant, ctx, res) -> jPosRes; + } +} \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/transaction/AnnotatedParticipantWrapper.java b/jpos/src/main/java/org/jpos/transaction/AnnotatedParticipantWrapper.java new file mode 100644 index 0000000000..7873bc52b2 --- /dev/null +++ b/jpos/src/main/java/org/jpos/transaction/AnnotatedParticipantWrapper.java @@ -0,0 +1,127 @@ +package org.jpos.transaction; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.List; + +import org.jpos.annotation.AnnotatedParticipant; +import org.jpos.annotation.Prepare; +import org.jpos.annotation.resolvers.ResolverFactory; +import org.jpos.annotation.resolvers.exception.ReturnExceptionHandler; +import org.jpos.annotation.resolvers.parameters.Resolver; +import org.jpos.annotation.resolvers.response.ReturnHandler; +import org.jpos.core.ConfigurationException; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.AllArguments; +import net.bytebuddy.implementation.bind.annotation.Origin; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.matcher.ElementMatchers; + +public class AnnotatedParticipantWrapper { + + protected TransactionParticipant participant; + protected Method prepare; + + List args = new ArrayList<>(); + ReturnHandler returnHandler; + List exceptionHandlers; + Method checkPoint; + + public static T wrap(T participant) throws ConfigurationException { + try { + AnnotatedParticipantWrapper handler = new AnnotatedParticipantWrapper(participant); + return (T) new ByteBuddy() + .subclass(participant.getClass()) + .method(ElementMatchers.any()) + .intercept(MethodDelegation.to(handler)) + .make() + .load(participant.getClass().getClassLoader()) + .getLoaded() + .getDeclaredConstructor() + .newInstance(); + } catch (Throwable e) { + throw new ConfigurationException("Cound not create the annotated wrapper", e); + } + } + + public static boolean isMatch(TransactionParticipant participant) { + return participant.getClass().isAnnotationPresent(AnnotatedParticipant.class); + } + + public AnnotatedParticipantWrapper(TransactionParticipant participant) throws ConfigurationException { + this.participant = participant; + configurePrepareMethod(); + configureReturnHandler(); + configureParameters(); + + } + + protected void configureReturnHandler() throws ConfigurationException { + returnHandler = ResolverFactory.INSTANCE.getReturnHandler(prepare); + exceptionHandlers = ResolverFactory.INSTANCE.getExceptionHandlers(prepare); + } + + protected void configureParameters() throws ConfigurationException { + for(Parameter p: prepare.getParameters()) { + args.add(ResolverFactory.INSTANCE.getResolver(p)); + } + } + + protected void configurePrepareMethod() throws ConfigurationException { + for(Method m: participant.getClass().getMethods()) { + if (m.isAnnotationPresent(Prepare.class)) { + if (prepare == null) { + prepare = m; + } else { + throw new ConfigurationException("Only one method per class can be defined with the @Prepare. " + participant.getClass().getSimpleName() + " has multiple matches."); + } + } + } + if (prepare == null) { + throw new ConfigurationException(participant.getClass().getSimpleName() + " needs one method defined with the @Prepare annotation."); + } + } + + + public final int prepare(long id, Serializable o) { + Context ctx = (Context) o; + try { + Object[] resolvedArgs = new Object[args.size()]; + int i = 0; + for(Resolver r: args) { + resolvedArgs[i++] = r.getValue(participant, ctx); + } + Object res = prepare.invoke(participant, resolvedArgs); + + return returnHandler.doReturn(participant, ctx, res); + } catch (IllegalAccessException | IllegalArgumentException e) { + return processException(ctx, e); + } catch (InvocationTargetException e) { + return processException(ctx, e.getTargetException()); + } + } + + private int processException(Context ctx, Throwable e) { + ctx.log("Failed to execute " + prepare.toString()); + ctx.log(e); + for(ReturnExceptionHandler handler: exceptionHandlers) { + if (handler.isMatch(e)) { + return handler.doReturn(participant, ctx, e); + } + } + throw new RuntimeException(e); + } + + @RuntimeType + public Object intercept(@AllArguments Object[] args, + @Origin Method method) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return method.invoke(participant, args); + } +} + + diff --git a/jpos/src/main/java/org/jpos/transaction/TransactionManager.java b/jpos/src/main/java/org/jpos/transaction/TransactionManager.java index 2fb5741c88..58d3be8cdc 100644 --- a/jpos/src/main/java/org/jpos/transaction/TransactionManager.java +++ b/jpos/src/main/java/org/jpos/transaction/TransactionManager.java @@ -820,6 +820,9 @@ public TransactionParticipant createParticipant (Element e) if (participant instanceof Destroyable) { destroyables.add((Destroyable) participant); } + if (AnnotatedParticipantWrapper.isMatch(participant)) { + participant = AnnotatedParticipantWrapper.wrap(participant); + } return participant; } diff --git a/jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.exception.ReturnExceptionHandlerProvider b/jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.exception.ReturnExceptionHandlerProvider new file mode 100644 index 0000000000..3f801d9b14 --- /dev/null +++ b/jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.exception.ReturnExceptionHandlerProvider @@ -0,0 +1 @@ +org.jpos.annotation.resolvers.exception.GenericExceptionHandlerProvider diff --git a/jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.parameters.ResolverServiceProvider b/jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.parameters.ResolverServiceProvider new file mode 100644 index 0000000000..08659c514c --- /dev/null +++ b/jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.parameters.ResolverServiceProvider @@ -0,0 +1,3 @@ +org.jpos.annotation.resolvers.parameters.ContextResolver +org.jpos.annotation.resolvers.parameters.RegistryResolver +org.jpos.annotation.resolvers.parameters.ContextPassThruResolver diff --git a/jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.response.ReturnHandlerProvider b/jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.response.ReturnHandlerProvider new file mode 100644 index 0000000000..dda90e0a42 --- /dev/null +++ b/jpos/src/main/resources/META-INF/services/org.jpos.annotation.resolvers.response.ReturnHandlerProvider @@ -0,0 +1,4 @@ +org.jpos.annotation.resolvers.response.IntPassthruContextReturnHandler +org.jpos.annotation.resolvers.response.MultiValueContextReturnHandler +org.jpos.annotation.resolvers.response.SingleValueContextReturnHandler +org.jpos.annotation.resolvers.response.VoidContextReturnHandler \ No newline at end of file diff --git a/jpos/src/test/java/org/jpos/transaction/AnnotatedParticipantWrapperTest.java b/jpos/src/test/java/org/jpos/transaction/AnnotatedParticipantWrapperTest.java new file mode 100644 index 0000000000..944cb2d797 --- /dev/null +++ b/jpos/src/test/java/org/jpos/transaction/AnnotatedParticipantWrapperTest.java @@ -0,0 +1,409 @@ +package org.jpos.transaction; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Map; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.jdom2.Document; +import org.jdom2.JDOMException; +import org.jdom2.input.SAXBuilder; +import org.jpos.annotation.AnnotatedParticipant; +import org.jpos.annotation.ContextKey; +import org.jpos.annotation.Prepare; +import org.jpos.annotation.Registry; +import org.jpos.annotation.Return; +import org.jpos.core.Configuration; +import org.jpos.core.ConfigurationException; +import org.jpos.core.SimpleConfiguration; +import org.jpos.q2.Q2; +import org.jpos.q2.QFactory; +import org.jpos.rc.IRC; +import org.jpos.rc.CMF; +import org.jpos.util.NameRegistrar; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; +import org.xml.sax.InputSource; + +public class AnnotatedParticipantWrapperTest { + + private static final String HANDLER = "handler"; + + public static class TxnSupport implements TransactionParticipant { + public int prepare(long id, Serializable context) { + return TransactionConstants.ABORTED; + } + + public void setConfiguration(Configuration cfg) {} + } + + @AnnotatedParticipant + public static class AnnotatedParticipantTest extends TxnSupport { + @Prepare(result = TransactionConstants.PREPARED | TransactionConstants.READONLY) + @Return("CARD") + public Object getCard(Context ctx, @ContextKey("DB") Object db, @Registry Long someKey) throws Exception { + Assertions.assertNotNull(ctx); + Assertions.assertNotNull(db); + Assertions.assertEquals(2, someKey); + return new Object(); + } + } + + public static class RegularParticipantTest extends TxnSupport {} + + @Test + public void testClassAnnotation() throws ConfigurationException { + Context ctx = new Context(); + ctx.put("DB", new Object()); + Configuration cfg = new SimpleConfiguration(); + NameRegistrar.register("someKey", 2L); + + // Test plain participant + TxnSupport p = new RegularParticipantTest(); + p.setConfiguration(cfg); + assertRegularParticipant(ctx, p, false); + + p = new AnnotatedParticipantTest(); + p.setConfiguration(cfg); + // Test direct participant + assertRegularParticipant(ctx, p, true); + + // Test wrapper annotation + assertTrue(AnnotatedParticipantWrapper.isMatch(p)); + TxnSupport pw = AnnotatedParticipantWrapper.wrap(p); + assertAnnotatedParticipant(ctx, pw); + + } + + @Test + public void testClassAnnotationFromTxnMgr() throws ConfigurationException, JDOMException, IOException, MalformedObjectNameException { + TransactionManager txnMgr = new TransactionManager(); + Q2 q2 = mock(Q2.class); + QFactory f = spy(new QFactory(new ObjectName("Q2:type=system,service=loader"), q2)); + when(q2.getFactory()).thenReturn(f); + doReturn(new RegularParticipantTest()).when(f).newInstance(RegularParticipantTest.class.getCanonicalName()); + doReturn(new AnnotatedParticipantTest()).when(f).newInstance(AnnotatedParticipantTest.class.getCanonicalName()); + txnMgr.setServer(q2); + txnMgr.setName("txnMgr"); + txnMgr.setConfiguration(new SimpleConfiguration()); + + String regParticipantXml = ""; + String annotatedParticipantXml = ""; + + Context ctx = new Context(); + ctx.put("DB", new Object()); + NameRegistrar.register("someKey", 2L); + + TxnSupport p = (TxnSupport) getParticipant(txnMgr, regParticipantXml); + assertRegularParticipant(ctx, p, false); + + p = (TxnSupport) getParticipant(txnMgr, annotatedParticipantXml); + assertAnnotatedParticipant(ctx, p); + } + + + @AnnotatedParticipant + public static class InvalidParticipant extends TxnSupport { + public InvalidParticipant(Object arg) {} + } + + @AnnotatedParticipant + static class InvalidParticipantInvocation extends TxnSupport { + public InvalidParticipantInvocation() throws Exception { + } + + @Prepare + public void prepare() {} + } + + @Test + public void testWrapperInstantiationFailure() { + assertThrows(ConfigurationException.class, ()-> AnnotatedParticipantWrapper.wrap(new InvalidParticipant(null) {})); + assertThrows(ConfigurationException.class, ()-> AnnotatedParticipantWrapper.wrap(new InvalidParticipant(null))); + assertThrows(ConfigurationException.class, ()-> AnnotatedParticipantWrapper.wrap(new InvalidParticipantInvocation())); + } + + public static class PassthruReturn extends TaggingAnnotatedParticipant { + @Prepare + public int doWork() { + return 5; + } + } + + public static class MapMultiReturn extends TaggingAnnotatedParticipant { + @Prepare(result = 5) + @Return({"key1", "key2", "key3"}) + public Map doWork(@ContextKey(HANDLER) Handler handler) throws Exception { + return handler == null ? null : (Map) handler.call(); + } + } + + public static class MapSingleReturn extends TaggingAnnotatedParticipant { + @Prepare(result = 5) + @Return({"key1"}) + public Map doWork(@ContextKey(HANDLER) Handler handler) throws Exception { + return handler == null ? null : (Map) handler.call(); + } + } + + @Test + public void testReturnTypes() throws ConfigurationException { + assertEquals(5, AnnotatedParticipantWrapper.wrap(new PassthruReturn()).prepare(0, new Context())); + Context ctx = new Context(); + assertEquals(5, AnnotatedParticipantWrapper.wrap(new MapMultiReturn()).prepare(0, ctx)); + assertEquals(0, ctx.getMap().size()); + testWithHandler(new MapMultiReturn(), ctx, ()-> new HashMap() {{ + put("a", "b"); + put("c", "d"); + }}); + assertEquals(1, ctx.getMap().size()); + ctx.getMap().clear(); + + assertEquals(0, ctx.getMap().size()); + testWithHandler(new MapMultiReturn(), ctx, ()-> new HashMap() {{ + put("key1", "b"); + put("c", "d"); + }}); + assertEquals(2, ctx.getMap().size()); + ctx.getMap().clear(); + + assertEquals(0, ctx.getMap().size()); + testWithHandler(new MapSingleReturn(), ctx, ()-> new HashMap() {{ + put("b", "b"); + put("c", "d"); + }}); + assertEquals(2, ctx.getMap().size()); + ctx.getMap().clear(); + + testWithHandler(new MapSingleReturn(), ctx, null); + assertEquals(1, ctx.getMap().size()); + assertTrue(ctx.getMap().containsKey(HANDLER)); + } + + void testWithHandler(TransactionParticipant p, Context ctx, Handler handler) throws ConfigurationException { + ctx.put(HANDLER, handler); + assertEquals(5, AnnotatedParticipantWrapper.wrap(p).prepare(0, ctx)); + } + + @AnnotatedParticipant + public static class TaggingAnnotatedParticipant implements TransactionParticipant { + @Override + public int prepare(long id, Serializable context) { + return TransactionConstants.ABORTED; + } + } + + public static interface Handler { + Object call() throws Exception; + } + + public static class HappyPass extends TaggingAnnotatedParticipant { + @Prepare + public void doNothing(@ContextKey(HANDLER) Handler handler, @ContextKey("DB") Object db) throws Exception { + if (handler != null) handler.call(); + } + } + + public static class DoublePrepareDefined extends TaggingAnnotatedParticipant { + @Prepare + public void doNothing() {} + @Prepare + public void doNothing1() {} + } + public static class PreparedUnboundArg extends TaggingAnnotatedParticipant { + @Prepare + public void invalidParams(Object arg) {} + } + public static class MissingReturn extends TaggingAnnotatedParticipant { + @Prepare + public Map invalidParams() { + return null; + } + } + public static class UnusedReturn extends TaggingAnnotatedParticipant { + @Prepare + @Return("key") + public void invalidParams() {} + } + + public static class TooManyKeysDefined extends TaggingAnnotatedParticipant { + @Prepare + @Return({"key", "key1"}) + public Object invalidReturn() { + return null; + } + } + + + @Test + public void testWrapperInvalidAnnotationUse() throws ConfigurationException { + assertNotNull(AnnotatedParticipantWrapper.wrap(new HappyPass())); + + assertThrows(ConfigurationException.class, ()-> { + AnnotatedParticipantWrapper.wrap(new TaggingAnnotatedParticipant()); + }); + + assertThrows(ConfigurationException.class, ()-> { + AnnotatedParticipantWrapper.wrap(new DoublePrepareDefined()); + }); + + assertThrows(ConfigurationException.class, ()-> { + AnnotatedParticipantWrapper.wrap(new PreparedUnboundArg()); + }); + + assertThrows(ConfigurationException.class, ()-> { + AnnotatedParticipantWrapper.wrap(new MissingReturn()); + }); + + assertThrows(ConfigurationException.class, ()-> { + AnnotatedParticipantWrapper.wrap(new UnusedReturn()); + }); + + assertThrows(ConfigurationException.class, ()-> { + AnnotatedParticipantWrapper.wrap(new TooManyKeysDefined()); + }); + } + + + @Test + public void testParticipantExecutionErrorHandling() throws ConfigurationException { + TransactionParticipant p = AnnotatedParticipantWrapper.wrap(new HappyPass()); + testException(p, ()-> {throw new RuntimeException();}, CMF.INTERNAL_ERROR); + final Exception circularCause = new Exception() { + public synchronized Throwable getCause() { + return this; + }; + }; + testException(p, ()-> {throw circularCause;}, CMF.INTERNAL_ERROR); + } + + @Test + public void testUncaughtExceptionHandling() throws ConfigurationException { + TransactionParticipant p = AnnotatedParticipantWrapper.wrap(new HappyPass()); + Handler h = ()->{throw new Error();}; + Context ctx = new Context(); + ctx.put(HANDLER, h); + + assertThrows(RuntimeException.class, ()->p.prepare(0, ctx)); + + } + + private void testException(TransactionParticipant p, Handler handler, IRC irc) { + Context ctx = new Context(); + ctx.put(HANDLER, handler); + assertEquals(TransactionConstants.ABORTED, p.prepare(0, ctx)); + assertEquals(irc, ctx.get(ContextConstants.IRC)); + } + + @Test + public void testInvalidArgumentBinding() throws ConfigurationException { + Context ctx = new Context(); + + TransactionParticipant p = AnnotatedParticipantWrapper.wrap(new HappyPass()); + + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + + ctx.put(HANDLER, new Object()); + assertEquals(TransactionConstants.ABORTED, p.prepare(0, ctx)); + assertEquals(CMF.INTERNAL_ERROR, ctx.get(ContextConstants.IRC)); + } + + public static class RegistryNameResolverTest extends TaggingAnnotatedParticipant { + @Prepare + public void checkRegistry(@Registry("key1") String key) { + assertEquals("key1", key); + } + } + + public static class RegistryTypeResolverTest extends TaggingAnnotatedParticipant { + @Prepare + public void checkRegistry(@Registry() String key) { + assertEquals("key1", key); + } + } + + + + @Test + public void testNamedRegistryResolver() throws ConfigurationException { + Context ctx = new Context(); + NameRegistrar.register("key1", "key1"); + TransactionParticipant p; + + p = AnnotatedParticipantWrapper.wrap(new RegistryTypeResolverTest()); + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + + p = AnnotatedParticipantWrapper.wrap(new RegistryNameResolverTest()); + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + NameRegistrar.unregister("key1"); + + assertThrows(ConfigurationException.class, ()->AnnotatedParticipantWrapper.wrap(new RegistryNameResolverTest())); + assertThrows(ConfigurationException.class, ()->AnnotatedParticipantWrapper.wrap(new RegistryTypeResolverTest())); + + NameRegistrar.register("KEY1", "key1"); + p = AnnotatedParticipantWrapper.wrap(new RegistryTypeResolverTest()); + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + + p = AnnotatedParticipantWrapper.wrap(new RegistryNameResolverTest()); + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + NameRegistrar.register("KeY1", "key1"); + assertThrows(ConfigurationException.class, ()->AnnotatedParticipantWrapper.wrap(new RegistryNameResolverTest())); + + NameRegistrar.unregister("KEY1"); + NameRegistrar.unregister("KeY1"); + + assertThrows(ConfigurationException.class, ()->AnnotatedParticipantWrapper.wrap(new RegistryNameResolverTest())); + assertThrows(ConfigurationException.class, ()->AnnotatedParticipantWrapper.wrap(new RegistryTypeResolverTest())); + + NameRegistrar.register("someKey", "key1"); + assertThrows(ConfigurationException.class, ()->AnnotatedParticipantWrapper.wrap(new RegistryNameResolverTest())); + + p = AnnotatedParticipantWrapper.wrap(new RegistryTypeResolverTest()); + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + NameRegistrar.register("someKey1", "key1"); + assertThrows(ConfigurationException.class, ()->AnnotatedParticipantWrapper.wrap(new RegistryTypeResolverTest())); + + NameRegistrar.unregister("someKey"); + NameRegistrar.unregister("someKey1"); + } + + + protected void assertAnnotatedParticipant(Context ctx, TxnSupport p) { + assertEquals(TransactionConstants.PREPARED | TransactionConstants.READONLY, p.prepare(0, ctx)); + assertNotNull(ctx.get(ContextConstants.CARD.name())); + } + + protected void assertRegularParticipant(Context ctx, TxnSupport p, boolean ignoreAnnotation) { + if (!ignoreAnnotation) { + assertFalse(AnnotatedParticipantWrapper.isMatch(p)); + } + assertEquals(TransactionConstants.ABORTED, p.prepare(0, ctx)); + assertNull(ctx.get(ContextConstants.CARD.name())); + } + + private TransactionParticipant getParticipant(TransactionManager txnMgr, String participantXml) throws JDOMException, IOException, ConfigurationException { + SAXBuilder builder = new SAXBuilder (); + builder.setFeature("http://xml.org/sax/features/namespaces", true); + builder.setFeature("http://apache.org/xml/features/xinclude", true); + Document doc = builder.build(new InputSource(new StringReader(participantXml))); + + return txnMgr.createParticipant(doc.getRootElement()); + + } + +} From 8e133087ab3c9bd1d39e2dc6cefd6c7faf7084ca Mon Sep 17 00:00:00 2001 From: Ozzy Espaillat Date: Wed, 20 Dec 2023 10:15:58 -0800 Subject: [PATCH 2/4] cla-docs: Add cla entry --- legal/cla-espaillato.txt | 138 +++++++++++++++++++++++++++++++++++++++ legal/espaillato.asc | 13 ++++ 2 files changed, 151 insertions(+) create mode 100644 legal/cla-espaillato.txt create mode 100644 legal/espaillato.asc diff --git a/legal/cla-espaillato.txt b/legal/cla-espaillato.txt new file mode 100644 index 0000000000..26594ca85a --- /dev/null +++ b/legal/cla-espaillato.txt @@ -0,0 +1,138 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +jPOS Project +Contributor License Agreement V1.0 +based on http://www.apache.org/licenses/ + +Thank you for your interest in the jPOS project by jPOS Software SRL (Uruguayan +company #217 883 210 018 - jPOS.org). In order to clarify the intellectual property +license granted with Contributions from any person or entity, jPOS Software SRL +must have a Contributor License Agreement ("CLA") that has been signed by each +Contributor, indicating agreement to the license terms below. This license is +for your protection as a Contributor as well as the protection of jPOS.org and +its users; it does not change your rights to use your own Contributions for any +other purpose. + +If you have not already done so, please complete this agreement and +commit it to the jPOS repository at +https://github.com/jpos/jPOS/tree/master/legal at legal/cla-USERNAME.txt using +your authenticated Github login. If you do not have commit +privilege to the repository, please email the file to license@jpos.org. +If possible, digitally sign the committed file, otherwise also send a +signed Agreement to jPOS.org. + +Please read this document carefully before signing and keep a copy for +your records. + + Full name: Ozzy Espaillat + E-Mail: ozzypaji@gmail.com + +You accept and agree to the following terms and conditions for Your +present and future Contributions submitted to jPOS.org. In return, +jPOS.org shall not use Your Contributions in a way that is +contrary to the software license in effect at the time of the +Contribution. Except for the license granted herein to jPOS.org +and recipients of software distributed by jPOS.org, You reserve +all right, title, and interest in and to Your Contributions. + +1. Definitions. + + "You" (or "Your") shall mean the copyright owner or legal entity + authorized by the copyright owner that is making this Agreement + with jPOS.org. For legal entities, the entity making a + Contribution and all other entities that control, are controlled + by, or are under common control with that entity are considered to + be a single Contributor. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "Contribution" shall mean any original work of authorship, + including any modifications or additions to an existing work, that + is intentionally submitted by You to jPOS.org for inclusion + in, or documentation of, any of the products owned or managed by + jPOS.org (the "Work"). For the purposes of this definition, + "submitted" means any form of electronic, verbal, or written + communication sent to jPOS.org or its representatives, + including but not limited to communication on electronic mailing + lists, source code control systems, and issue tracking systems that + are managed by, or on behalf of, jPOS.org for the purpose of + discussing and improving the Work, but excluding communication that + is conspicuously marked or otherwise designated in writing by You + as "Not a Contribution." + +2. Grant of Copyright License. Subject to the terms and conditions of + this Agreement, You hereby grant to jPOS.org and to + recipients of software distributed by jPOS.org a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare derivative works of, + publicly display, publicly perform, sublicense, and distribute Your + Contributions and such derivative works. + +3. Grant of Patent License. Subject to the terms and conditions of + this Agreement, You hereby grant to jPOS.org and to + recipients of software distributed by jPOS.org a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have + made, use, offer to sell, sell, import, and otherwise transfer the + Work, where such license applies only to those patent claims + licensable by You that are necessarily infringed by Your + Contribution(s) alone or by combination of Your Contribution(s) + with the Work to which such Contribution(s) was submitted. If any + entity institutes patent litigation against You or any other entity + (including a cross-claim or counterclaim in a lawsuit) alleging + that your Contribution, or the Work to which you have contributed, + constitutes direct or contributory patent infringement, then any + patent licenses granted to that entity under this Agreement for + that Contribution or Work shall terminate as of the date such + litigation is filed. + +4. You represent that you are legally entitled to grant the above + license. If your employer(s) has rights to intellectual property + that you create that includes your Contributions, you represent + that you have received permission to make Contributions on behalf + of that employer, that your employer has waived such rights for + your Contributions to jPOS.org, or that your employer has + executed a separate Corporate CLA with jPOS.org. + +5. You represent that each of Your Contributions is Your original + creation (see section 7 for submissions on behalf of others). You + represent that Your Contribution submissions include complete + details of any third-party license or other restriction (including, + but not limited to, related patents and trademarks) of which you + are personally aware and which are associated with any part of Your + Contributions. + +6. You are not expected to provide support for Your Contributions, + except to the extent You desire to provide support. You may provide + support for free, for a fee, or not at all. Unless required by + applicable law or agreed to in writing, You provide Your + Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + OF ANY KIND, either express or implied, including, without + limitation, any warranties or conditions of TITLE, NON- + INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. + +7. Should You wish to submit work that is not Your original creation, + You may submit it to jPOS.org separately from any + Contribution, identifying the complete details of its source and of + any license or other restriction (including, but not limited to, + related patents, trademarks, and license agreements) of which you + are personally aware, and conspicuously marking the work as + "Submitted on behalf of a third-party: [named here]". + +8. You agree to notify jPOS.org of any facts or circumstances of + which you become aware that would make these representations + inaccurate in any respect. + +Date: 12/20/2023 +Please sign: Ozzy Espaillat + +-----BEGIN PGP SIGNATURE----- + +iHUEARYKAB0WIQSexDQkxmFPNoZTGSt3V+Ng0LRTgwUCZYMtKwAKCRB3V+Ng0LRT +g3FWAP9KdLtjZCZ2OPEqzkfJpodv11sijqQgZONDBTWahZHmLAEAnRj650WTGi/b +BQBigXbwcY+//N8qdYtPDTS+B+gEEQ0= +=CEoY +-----END PGP SIGNATURE----- diff --git a/legal/espaillato.asc b/legal/espaillato.asc new file mode 100644 index 0000000000..360fa6654d --- /dev/null +++ b/legal/espaillato.asc @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZYMtFBYJKwYBBAHaRw8BAQdA56cBVnE/IJFxyJGfgoiFyKrpSHLHL3kW6Ug0 +TIX3rjm0I096enkgRXNwYWlsbGF0IDxvenp5cGFqaUBnbWFpbC5jb20+iJkEExYK +AEEWIQSexDQkxmFPNoZTGSt3V+Ng0LRTgwUCZYMtFAIbAwUJBaOagAULCQgHAgIi +AgYVCgkICwIEFgIDAQIeBwIXgAAKCRB3V+Ng0LRTg/UqAQCWOKRqySjbZUdGjfiD +JhqePhvFOSCRGR93QFf1fX6aegEA7M+IaDoYmWtfYvRgDrBiJy3IxWzlof61zc/s +kzGnUwe4OARlgy0UEgorBgEEAZdVAQUBAQdAvrkzKhUYpRFaxLTtz8icwX+pDHQ6 +NUiyrGKGssExkh4DAQgHiH4EGBYKACYWIQSexDQkxmFPNoZTGSt3V+Ng0LRTgwUC +ZYMtFAIbDAUJBaOagAAKCRB3V+Ng0LRTg6HVAP9b+TC0YQ5lAeJf3GJFMQqR+zR6 ++0p8gmZ5dgCpXksfZgEA6Fznqozft21pWl+zwPbMZ8ugnTr3Op/DwuUgkW/glAM= +=oJqW +-----END PGP PUBLIC KEY BLOCK----- From c9a5722d22578cca4bce9ad374a5310b7b8ae9ab Mon Sep 17 00:00:00 2001 From: Ozzy Espaillat Date: Thu, 28 Dec 2023 11:33:38 -0800 Subject: [PATCH 3/4] master: Remove commons lang dependency in favor of ISOUtil. --- jpos/build.gradle | 1 - .../annotation/resolvers/parameters/RegistryResolver.java | 6 +++--- jpos/src/main/java/org/jpos/iso/ISOUtil.java | 7 +++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/jpos/build.gradle b/jpos/build.gradle index d6356541bf..c00415581b 100644 --- a/jpos/build.gradle +++ b/jpos/build.gradle @@ -29,7 +29,6 @@ dependencies { implementation libraries.sshd implementation libraries.eddsa implementation libraries.bytebuddy - implementation libraries.commons_lang3 testImplementation libraries.commons_lang3 testImplementation libraries.hamcrest diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/RegistryResolver.java b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/RegistryResolver.java index c08ba107d1..a3d9e0ba4e 100644 --- a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/RegistryResolver.java +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/RegistryResolver.java @@ -6,9 +6,9 @@ import java.util.Map; import java.util.Map.Entry; -import org.apache.commons.lang3.StringUtils; import org.jpos.annotation.Registry; import org.jpos.core.ConfigurationException; +import org.jpos.iso.ISOUtil; import org.jpos.transaction.Context; import org.jpos.transaction.TransactionParticipant; import org.jpos.util.NameRegistrar; @@ -22,7 +22,7 @@ private static final class RegistryResolverImpl implements Resolver { public void configure(Parameter f) throws ConfigurationException { Registry annotation = f.getAnnotation(Registry.class); registryKey = findKey(annotation.value(), f.getType(), NameRegistrar.getAsMap()); - if (StringUtils.isEmpty(registryKey)) { + if (ISOUtil.isEmpty(registryKey)) { throw new ConfigurationException("Could not find Registry entry for " + f.getName()); } } @@ -44,7 +44,7 @@ String findKey(String key, Class type, Map entries) throws Configuratio protected String getMatch(String key, List typeMatches, List keyMatches) throws ConfigurationException { - if (StringUtils.isNotBlank(key)) { + if (!ISOUtil.isEmpty(key)) { return getMatch(key, keyMatches); } else { return getMatch(key, typeMatches); diff --git a/jpos/src/main/java/org/jpos/iso/ISOUtil.java b/jpos/src/main/java/org/jpos/iso/ISOUtil.java index d92f9264c7..af710b810c 100644 --- a/jpos/src/main/java/org/jpos/iso/ISOUtil.java +++ b/jpos/src/main/java/org/jpos/iso/ISOUtil.java @@ -1105,6 +1105,13 @@ public static boolean isZero( String s ) { public static boolean isBlank( String s ){ return s.trim().length() == 0; } + + /** + * @return true if the string is null or is blank filled (space char filled) + */ + public static boolean isEmpty(String s) { + return s == null || isBlank(s); + } /** * Return true if the string is alphanum. From e5476a9a0642f6a343af6aa374f91b2cb5ef24ff Mon Sep 17 00:00:00 2001 From: Ozzy Espaillat Date: Mon, 4 Mar 2024 09:58:48 -0800 Subject: [PATCH 4/4] master: Add ability to pass Context, either complete, or a partial view. --- .../jpos/annotation/AnnotatedParticipant.java | 13 + .../java/org/jpos/annotation/ContextKey.java | 12 + .../java/org/jpos/annotation/ContextKeys.java | 32 ++ .../java/org/jpos/annotation/Prepare.java | 11 + .../java/org/jpos/annotation/Registry.java | 11 + .../main/java/org/jpos/annotation/Return.java | 11 + .../parameters/ContextPassThruResolver.java | 34 +- .../resolvers/parameters/ContextView.java | 300 ++++++++++++++++++ .../AnnotatedParticipantWrapperTest.java | 167 +++++++++- 9 files changed, 579 insertions(+), 12 deletions(-) create mode 100644 jpos/src/main/java/org/jpos/annotation/ContextKeys.java create mode 100644 jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextView.java diff --git a/jpos/src/main/java/org/jpos/annotation/AnnotatedParticipant.java b/jpos/src/main/java/org/jpos/annotation/AnnotatedParticipant.java index c43db022b3..e484541d2a 100644 --- a/jpos/src/main/java/org/jpos/annotation/AnnotatedParticipant.java +++ b/jpos/src/main/java/org/jpos/annotation/AnnotatedParticipant.java @@ -5,6 +5,19 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.jpos.transaction.TransactionParticipant; + +/** + * Marks a {@link TransactionParticipant} a participant defined using annotations, automatically + * binding the prepare method and parameters. This annotation is used + * in convention-over-configuration scenarios to simplify the integration of components and + * make testing easier. + * + * see {@linkplain https://marqeta.atlassian.net/wiki/spaces/~62f54a31d49df231b62a575d/blog/2023/12/01/3041525965/AutoWiring+Participants+with+jPos+-+Part+I} + * + * Usage: Apply on classes extending {@link TransactionParticipant} and implementing a {@link Prepare} method instead of the prepare(long, Serializable). + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface AnnotatedParticipant{ diff --git a/jpos/src/main/java/org/jpos/annotation/ContextKey.java b/jpos/src/main/java/org/jpos/annotation/ContextKey.java index 4349b393e4..5da70b70b1 100644 --- a/jpos/src/main/java/org/jpos/annotation/ContextKey.java +++ b/jpos/src/main/java/org/jpos/annotation/ContextKey.java @@ -5,6 +5,18 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.jpos.transaction.Context; + +/** + * Used to specify a key for retrieving contextual information from a {@link Context} object via + * the annotated parameter. This facilitates dynamic access to context-specific data, + * streamlining the process of working with application contexts. + * + * @see {@linkplain https://marqeta.atlassian.net/wiki/spaces/~62f54a31d49df231b62a575d/blog/2023/12/01/3041525965/AutoWiring+Participants+with+jPos+-+Part+I} + * + * Usage: Used in conjunction with {@link Prepare} and {@link AnnotatedParticipant} annotations. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface ContextKey { diff --git a/jpos/src/main/java/org/jpos/annotation/ContextKeys.java b/jpos/src/main/java/org/jpos/annotation/ContextKeys.java new file mode 100644 index 0000000000..d0332c7c7f --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/ContextKeys.java @@ -0,0 +1,32 @@ +package org.jpos.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.jpos.transaction.Context; + +/** + * Used to specify a key for retrieving contextual information from a {@link Context} object via + * the annotated parameter. This facilitates dynamic access to context-specific data, + * streamlining the process of working with application contexts. + * + * value are in/out keys + * read only allow for read only + * write only allows for writes + * + * @see {@linkplain https://marqeta.atlassian.net/wiki/spaces/~62f54a31d49df231b62a575d/blog/2023/12/01/3041525965/AutoWiring+Participants+with+jPos+-+Part+I} + * + * Usage: Used in conjunction with {@link Prepare} and {@link AnnotatedParticipant} annotations. + * + * + * @author Ozzy Espaillat + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ContextKeys { + public String[] value() default {}; + public String[] write() default {}; + public String[] read() default {}; +} diff --git a/jpos/src/main/java/org/jpos/annotation/Prepare.java b/jpos/src/main/java/org/jpos/annotation/Prepare.java index 2de59c9066..84b3964582 100644 --- a/jpos/src/main/java/org/jpos/annotation/Prepare.java +++ b/jpos/src/main/java/org/jpos/annotation/Prepare.java @@ -6,7 +6,18 @@ import java.lang.annotation.Target; import org.jpos.transaction.TransactionConstants; +import org.jpos.transaction.TransactionParticipant; +/** + * Indicates that the annotated method is called in the preparation phase of transaction + * processing, specifying the expected outcome through the {@code result} attribute. + * This replaces the {@link TransactionParticipant} prepare method. + * + * @see {@linkplain https://marqeta.atlassian.net/wiki/spaces/~62f54a31d49df231b62a575d/blog/2023/12/01/3041525965/AutoWiring+Participants+with+jPos+-+Part+I} + * + * Usage: Used in conjunction with {@link AnnotatedParticipant} annotations. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Prepare { diff --git a/jpos/src/main/java/org/jpos/annotation/Registry.java b/jpos/src/main/java/org/jpos/annotation/Registry.java index 5cd0b7b0f8..b6baed9b96 100644 --- a/jpos/src/main/java/org/jpos/annotation/Registry.java +++ b/jpos/src/main/java/org/jpos/annotation/Registry.java @@ -5,6 +5,17 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.jpos.util.NameRegistrar; + +/** + * Marks a parameter within a method as being from + * {@link NameRegistrar} + * + * @see {@linkplain https://marqeta.atlassian.net/wiki/spaces/~62f54a31d49df231b62a575d/blog/2023/12/01/3041525965/AutoWiring+Participants+with+jPos+-+Part+I} + * + * Usage: Used in conjunction with {@link Prepare} and {@link AnnotatedParticipant} annotations. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface Registry { diff --git a/jpos/src/main/java/org/jpos/annotation/Return.java b/jpos/src/main/java/org/jpos/annotation/Return.java index d087235e4d..a25af05642 100644 --- a/jpos/src/main/java/org/jpos/annotation/Return.java +++ b/jpos/src/main/java/org/jpos/annotation/Return.java @@ -5,6 +5,17 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.jpos.transaction.TransactionParticipant; + +/** + * Specifies the return values of a method. This annotation is intended to replace + * the return type of the prepare method in the {@link TransactionParticipant} interface. + * + * @see {@linkplain https://marqeta.atlassian.net/wiki/spaces/~62f54a31d49df231b62a575d/blog/2023/12/01/3041525965/AutoWiring+Participants+with+jPos+-+Part+I} + * + * Usage: Used in conjunction with {@link Prepare} and {@link AnnotatedParticipant} annotations. + * + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Return { diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextPassThruResolver.java b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextPassThruResolver.java index fd692f34b6..6378fd6923 100644 --- a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextPassThruResolver.java +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextPassThruResolver.java @@ -1,7 +1,12 @@ package org.jpos.annotation.resolvers.parameters; import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.jpos.annotation.ContextKeys; +import org.jpos.core.ConfigurationException; import org.jpos.transaction.Context; import org.jpos.transaction.TransactionParticipant; @@ -19,11 +24,34 @@ public int getPriority() { @Override public Resolver resolve(Parameter p) { - return new Resolver() { + if ( p.isAnnotationPresent(ContextKeys.class)) { + return new Resolver() { + Set readWrite = new HashSet<>(); + Set readOnly = new HashSet<>(); + Set writeOnly = new HashSet<>(); + @Override + public void configure(Parameter f) throws ConfigurationException { + ContextKeys annotation = f.getAnnotation(ContextKeys.class); + readWrite.addAll(Arrays.asList(annotation.value())); + readOnly.addAll(Arrays.asList(annotation.read())); + writeOnly.addAll(Arrays.asList(annotation.write())); + if (readOnly.isEmpty() && writeOnly.isEmpty() && readWrite.isEmpty()) { + throw new ConfigurationException("At least one key for read or write has to be defined."); + } + } @Override public T getValue(TransactionParticipant participant, Context ctx) { - return (T) ctx; + return (T) new ContextView(ctx, readWrite, readOnly, writeOnly); } - }; + + }; + } else { + return new Resolver() { + @Override + public T getValue(TransactionParticipant participant, Context ctx) { + return (T) ctx; + } + }; + } } } \ No newline at end of file diff --git a/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextView.java b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextView.java new file mode 100644 index 0000000000..9c82a2cd90 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextView.java @@ -0,0 +1,300 @@ +package org.jpos.annotation.resolvers.parameters; + +import static org.jpos.transaction.ContextConstants.RESULT; + +import java.util.Map; +import java.util.Set; + +import org.jpos.rc.Result; +import org.jpos.transaction.Context; +import org.jpos.transaction.PausedTransaction; +import org.jpos.util.LogEvent; +import org.jpos.util.Profiler; + +class ContextView extends Context { + private final Context ctx; + private final Set readWrite; + private final Set readOnly; + private final Set writeOnly; + + enum ACCESS { + READ_ONLY(true, false), WRITE_ONLY(false, true), READ_WRITE(true, true); + + final boolean canRead; + final boolean canWrite; + + ACCESS(boolean canRead, boolean canWrite) { + this.canRead = canRead; + this.canWrite = canWrite; + } + + boolean hasAccess(ContextView ctx, Object key) { + boolean hasAccess = false; + if (canRead) { + hasAccess |= ctx.readWrite.contains(key) || ctx.readOnly.contains(key); + } + if (canWrite) { + hasAccess = ctx.readWrite.contains(key) || ctx.writeOnly.contains(key); + } + return hasAccess; + } + } + + public ContextView(Context ctx, Set readWrite, Set readOnly, Set writeOnly) { + this.ctx = ctx; + this.readWrite = readWrite; + this.readOnly = readOnly; + this.writeOnly = writeOnly; + } + + void validateKey(Object key, ACCESS access) { + if (!access.hasAccess(this, key)) { + throw new IllegalArgumentException(String.format("Can not access key %s, allowed readKeys are %s and allowed write keys are %s", key, readOnly, writeOnly)); + } + } + + /** + * puts an Object in the transient Map + */ + @Override + public void put (Object key, Object value) { + validateKey(key, ACCESS.WRITE_ONLY); + ctx.put(key, value); + } + /** + * puts an Object in the transient Map + */ + @Override + public void put (Object key, Object value, boolean persist) { + validateKey(key, ACCESS.WRITE_ONLY); + ctx.put(key, value, persist); + } + + /** + * Persists a transient entry + * @param key the key + */ + @Override + public void persist (Object key) { + ctx.persist(key); + } + + /** + * Evicts a persistent entry + * @param key the key + */ + @Override + public void evict (Object key) { + ctx.evict(key); + } + + /** + * Get object instance from transaction context. + * + * @param desired type of object instance + * @param key the key of object instance + * @return object instance if exist in context or {@code null} otherwise + */ + @Override + public T get(Object key) { + validateKey(key, ACCESS.READ_ONLY); + return ctx.get(key); + } + + /** + * Check if key present + * @param key the key + * @return true if present + */ + @Override + public boolean hasKey(Object key) { + validateKey(key, ACCESS.READ_WRITE); + return ctx.hasKey(key); + } + + /** + * Check key exists present persisted map + * @param key the key + * @return true if present + */ + @Override + public boolean hasPersistedKey(Object key) { + validateKey(key, ACCESS.READ_WRITE); + return ctx.hasPersistedKey(key); + } + + /** + * Move entry to new key name + * @param from key + * @param to key + * @return the entry's value (could be null if 'from' key not present) + */ + @Override + public synchronized T move(Object from, Object to) { + validateKey(from, ACCESS.WRITE_ONLY); + validateKey(to, ACCESS.WRITE_ONLY); + return ctx.move(from, to); + } + + /** + * Get object instance from transaction context. + * + * @param desired type of object instance + * @param key the key of object instance + * @param defValue default value returned if there is no value in context + * @return object instance if exist in context or {@code defValue} otherwise + */ + @Override + public T get(Object key, T defValue) { + validateKey(key, ACCESS.READ_ONLY); + return ctx.get(key, defValue); + } + + /** + * Transient remove + */ + @Override + public synchronized T remove(Object key) { + validateKey(key, ACCESS.WRITE_ONLY); + return ctx.remove(key); + } + + @Override + public String getString (Object key) { + validateKey(key, ACCESS.READ_ONLY); + return ctx.getString(key); + } + + @Override + public String getString (Object key, String defValue) { + validateKey(key, ACCESS.READ_ONLY); + return ctx.getString(key, defValue); + } + + /** + * persistent get with timeout + * @param key the key + * @param timeout timeout + * @return object (null on timeout) + */ + @Override + @SuppressWarnings("unchecked") + public synchronized T get (Object key, long timeout) { + validateKey(key, ACCESS.READ_ONLY); + return ctx.get(key, timeout); + } + + @Override + public Context clone() { + return new ContextView(ctx.clone(), readWrite, readOnly, writeOnly); + } + + @Override + public boolean equals(Object o) { + if (o instanceof ContextView) { + return ctx.equals(((ContextView)o).ctx); + } else { + return ctx.equals(o); + } + } + + @Override + public int hashCode() { + return ctx.hashCode(); + } + + /** + * @return transient map + */ + @Override + public synchronized Map getMap() { + throw new UnsupportedOperationException("getMap is not suported on a view"); + } + + /** + * return a LogEvent used to store trace information + * about this transaction. + * If there's no LogEvent there, it creates one. + * @return LogEvent + */ + @Override + public synchronized LogEvent getLogEvent () { + return ctx.getLogEvent(); + } + + /** + * return (or creates) a Profiler object + * @return Profiler object + */ + @Override + public synchronized Profiler getProfiler () { + return ctx.getProfiler(); + } + + /** + * return (or creates) a Resultr object + * @return Profiler object + */ + @Override + public synchronized Result getResult () { + validateKey(RESULT.toString(), ACCESS.READ_WRITE); + return ctx.getResult(); + } + + /** + * adds a trace message + * @param msg trace information + */ + @Override + public void log (Object msg) { + ctx.log(msg); + } + + /** + * add a checkpoint to the profiler + */ + @Override + public void checkPoint (String detail) { + ctx.checkPoint (detail); + } + + @Override + public void setPausedTransaction (PausedTransaction p) { + ctx.setPausedTransaction(p); + } + + @Override + public PausedTransaction getPausedTransaction() { + return ctx.getPausedTransaction(); + } + + @Override + public PausedTransaction getPausedTransaction(long timeout) { + return ctx.getPausedTransaction(timeout); + } + + @Override + public void setTimeout (long timeout) { + ctx.setTimeout(timeout); + } + + @Override + public long getTimeout () { + return ctx.getTimeout(); + } + + @Override + public synchronized void resume() { + ctx.resume(); + } + + @Override + public boolean isTrace() { + return ctx.isTrace(); + } + + @Override + public void setTrace(boolean trace) { + ctx.setTrace(trace); + } +} diff --git a/jpos/src/test/java/org/jpos/transaction/AnnotatedParticipantWrapperTest.java b/jpos/src/test/java/org/jpos/transaction/AnnotatedParticipantWrapperTest.java index 944cb2d797..dc8dd80bb1 100644 --- a/jpos/src/test/java/org/jpos/transaction/AnnotatedParticipantWrapperTest.java +++ b/jpos/src/test/java/org/jpos/transaction/AnnotatedParticipantWrapperTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -25,6 +26,7 @@ import org.jdom2.input.SAXBuilder; import org.jpos.annotation.AnnotatedParticipant; import org.jpos.annotation.ContextKey; +import org.jpos.annotation.ContextKeys; import org.jpos.annotation.Prepare; import org.jpos.annotation.Registry; import org.jpos.annotation.Return; @@ -34,6 +36,9 @@ import org.jpos.q2.Q2; import org.jpos.q2.QFactory; import org.jpos.rc.IRC; +import org.jpos.transaction.AnnotatedParticipantWrapperTest.ContextKeysResolverTest; +import org.jpos.transaction.AnnotatedParticipantWrapperTest.Handler; +import org.jpos.transaction.AnnotatedParticipantWrapperTest.TaggingAnnotatedParticipant; import org.jpos.rc.CMF; import org.jpos.util.NameRegistrar; import org.junit.jupiter.api.Test; @@ -167,7 +172,7 @@ public void testReturnTypes() throws ConfigurationException { Context ctx = new Context(); assertEquals(5, AnnotatedParticipantWrapper.wrap(new MapMultiReturn()).prepare(0, ctx)); assertEquals(0, ctx.getMap().size()); - testWithHandler(new MapMultiReturn(), ctx, ()-> new HashMap() {{ + testWithHandler(new MapMultiReturn(), ctx, (c)-> new HashMap() {{ put("a", "b"); put("c", "d"); }}); @@ -175,7 +180,7 @@ public void testReturnTypes() throws ConfigurationException { ctx.getMap().clear(); assertEquals(0, ctx.getMap().size()); - testWithHandler(new MapMultiReturn(), ctx, ()-> new HashMap() {{ + testWithHandler(new MapMultiReturn(), ctx, (c)-> new HashMap() {{ put("key1", "b"); put("c", "d"); }}); @@ -183,7 +188,7 @@ public void testReturnTypes() throws ConfigurationException { ctx.getMap().clear(); assertEquals(0, ctx.getMap().size()); - testWithHandler(new MapSingleReturn(), ctx, ()-> new HashMap() {{ + testWithHandler(new MapSingleReturn(), ctx, (c)-> new HashMap() {{ put("b", "b"); put("c", "d"); }}); @@ -209,7 +214,11 @@ public int prepare(long id, Serializable context) { } public static interface Handler { - Object call() throws Exception; + default Object call() throws Exception { + return call(null); + } + + Object call(Context ctx) throws Exception; } public static class HappyPass extends TaggingAnnotatedParticipant { @@ -283,19 +292,19 @@ public void testWrapperInvalidAnnotationUse() throws ConfigurationException { @Test public void testParticipantExecutionErrorHandling() throws ConfigurationException { TransactionParticipant p = AnnotatedParticipantWrapper.wrap(new HappyPass()); - testException(p, ()-> {throw new RuntimeException();}, CMF.INTERNAL_ERROR); + testException(p, (c)-> {throw new RuntimeException();}, CMF.INTERNAL_ERROR); final Exception circularCause = new Exception() { public synchronized Throwable getCause() { return this; }; }; - testException(p, ()-> {throw circularCause;}, CMF.INTERNAL_ERROR); + testException(p, (c)-> {throw circularCause;}, CMF.INTERNAL_ERROR); } @Test public void testUncaughtExceptionHandling() throws ConfigurationException { TransactionParticipant p = AnnotatedParticipantWrapper.wrap(new HappyPass()); - Handler h = ()->{throw new Error();}; + Handler h = (c)->{throw new Error();}; Context ctx = new Context(); ctx.put(HANDLER, h); @@ -382,10 +391,150 @@ public void testNamedRegistryResolver() throws ConfigurationException { NameRegistrar.unregister("someKey1"); } + public static class ContextKeysResolverTest extends TaggingAnnotatedParticipant { + @Prepare + public void doWork(@ContextKey(HANDLER) Handler handler, + @ContextKeys(value = {"account", "RESULT"}, write = {"RC", "EXTRC"}, read = {"CARD"} ) Context ctx, + @ContextKeys("account") Context ctx2, + @ContextKeys(write= {"RC", "EXTRC"}) Context ctx3, + @ContextKeys(read="CARD") Context ctx4, + Context ctx5 + ) throws Exception { + handler.call(ctx); + assertEquals(ctx, ctx2); + assertEquals(ctx, ctx3); + assertEquals(ctx, ctx4); + assertEquals(ctx, ctx5); + assertEquals(ctx.hashCode(), ctx2.hashCode()); + assertEquals(ctx.hashCode(), ctx3.hashCode()); + assertEquals(ctx.hashCode(), ctx4.hashCode()); + assertEquals(ctx.hashCode(), ctx5.hashCode()); + } + } + + @Test + public void assertContextKeysResolver() throws ConfigurationException { + Context ctx = new Context(); + TransactionParticipant p; + p = AnnotatedParticipantWrapper.wrap(new ContextKeysResolverTest()); + + addHandler(ctx, (c)->null); + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + + addHandler(ctx, (c)-> { + assertNull(c.get("account")); + assertNull(c.get("CARD")); + c.put("account", "account"); + assertTrue(c.hasKey("account")); + assertFalse(c.hasPersistedKey("account")); + c.persist("account"); + assertTrue(c.hasPersistedKey("account")); + c.evict("account"); + assertEquals("account", c.get("account")); + c.remove("account"); + assertNull(c.get("account")); + c.put("account", "account"); + c.put("RC", "rc"); + c.put("EXTRC", "extrarc"); + return true; + }); + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + assertEquals("rc", ctx.get("RC")); + assertEquals("extrarc", ctx.get("EXTRC")); + assertEquals("account", ctx.get("account")); + assertEquals(4, ctx.getMap().size()); + } + + @Test + public void assertContextKeysResolver2() throws ConfigurationException { + Context ctx = new Context(); + TransactionParticipant p; + p = AnnotatedParticipantWrapper.wrap(new ContextKeysResolverTest()); + ctx.put("account", "account"); + addHandler(ctx, (c)-> { + assertEquals("account", c.getString("account")); + assertEquals("account", c.getString("account", "dft")); + assertEquals("account", c.get("account")); + assertEquals("account", c.get("account", 1000L)); + assertEquals("account", c.get("account", "dft")); + assertEquals("dft", c.get("CARD", "dft")); + assertEquals("dft", c.getString("CARD", "dft")); + c.move("account", "EXTRC"); + assertNull(c.get("account")); + c.put("account", "account2", true); + c.put("RC", "rc"); + assertEquals(c, ctx); + assertEquals(c, c.clone()); + return true; + }); + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + assertEquals("rc", ctx.get("RC")); + assertEquals("account", ctx.get("EXTRC")); + assertEquals("account2", ctx.getString("account")); + assertEquals(4, ctx.getMap().size()); + } + + @Test + public void assertContextKeysResolver3() throws ConfigurationException { + Context ctx = spy(new Context()); + doNothing().when(ctx).resume(); + TransactionParticipant p; + p = AnnotatedParticipantWrapper.wrap(new ContextKeysResolverTest()); + ctx.put("account", "account"); + assertEquals(1, ctx.getMap().size()); + addHandler(ctx, (c)-> { + c.getResult(); + c.getProfiler(); + c.checkPoint("myStuff"); + c.getLogEvent(); + c.log("my log"); + c.setTimeout(1000); + c.setTrace(false); + assertNull(c.getPausedTransaction()); + c.setPausedTransaction(mock(PausedTransaction.class)); + assertNotNull(c.getPausedTransaction(1)); + c.resume(); + assertEquals(1000, c.getTimeout()); + assertFalse(c.isTrace()); + return true; + }); + assertEquals(TransactionConstants.PREPARED, p.prepare(0, ctx)); + assertEquals(6, ctx.getMap().size()); + } + + @Test + public void assertContextKeyResolverErrors() throws ConfigurationException { + TransactionParticipant p; + p = AnnotatedParticipantWrapper.wrap(new ContextKeysResolverTest()); + + testException(p, (c)-> {c.getString("account2"); return true;}, CMF.INTERNAL_ERROR); + testException(p, (c)-> {c.getString("account2", "dft"); return true;}, CMF.INTERNAL_ERROR); + + testException(p, (c)-> {c.get("account2"); return true;}, CMF.INTERNAL_ERROR); + testException(p, (c)-> {c.get("account2", "dft"); return true;}, CMF.INTERNAL_ERROR); + testException(p, (c)-> {c.get("account2", 1000L); return true;}, CMF.INTERNAL_ERROR); + + testException(p, (c)-> {c.put("account2", "acct"); return true;}, CMF.INTERNAL_ERROR); + testException(p, (c)-> {c.put("account2", "acct", false); return true;}, CMF.INTERNAL_ERROR); + + testException(p, (c)-> {c.hasKey("account2"); return true;}, CMF.INTERNAL_ERROR); + testException(p, (c)-> {c.hasPersistedKey("account2"); return true;}, CMF.INTERNAL_ERROR); + + testException(p, (c)-> {c.move("account", "account2"); return true;}, CMF.INTERNAL_ERROR); + testException(p, (c)-> {c.move("account2", "account"); return true;}, CMF.INTERNAL_ERROR); + + testException(p, (c)-> {c.remove("account2"); return true;}, CMF.INTERNAL_ERROR); + + testException(p, (c)-> {c.getMap(); return true;}, CMF.INTERNAL_ERROR); + } + + public void addHandler(Context ctx, Handler handler) { + ctx.put(HANDLER, handler); + } protected void assertAnnotatedParticipant(Context ctx, TxnSupport p) { assertEquals(TransactionConstants.PREPARED | TransactionConstants.READONLY, p.prepare(0, ctx)); - assertNotNull(ctx.get(ContextConstants.CARD.name())); + assertNotNull(ctx.get("CARD")); } protected void assertRegularParticipant(Context ctx, TxnSupport p, boolean ignoreAnnotation) { @@ -393,7 +542,7 @@ protected void assertRegularParticipant(Context ctx, TxnSupport p, boolean ignor assertFalse(AnnotatedParticipantWrapper.isMatch(p)); } assertEquals(TransactionConstants.ABORTED, p.prepare(0, ctx)); - assertNull(ctx.get(ContextConstants.CARD.name())); + assertNull(ctx.get("CARD")); } private TransactionParticipant getParticipant(TransactionManager txnMgr, String participantXml) throws JDOMException, IOException, ConfigurationException {