diff --git a/jpos/build.gradle b/jpos/build.gradle index c6467cff6b..c00415581b 100644 --- a/jpos/build.gradle +++ b/jpos/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation libraries.sleepycat_je implementation libraries.sshd implementation libraries.eddsa + implementation libraries.bytebuddy 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/Abort.java b/jpos/src/main/java/org/jpos/annotation/Abort.java new file mode 100644 index 0000000000..b104bad9fd --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/Abort.java @@ -0,0 +1,24 @@ +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; +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 Abort { +} 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..e484541d2a --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/AnnotatedParticipant.java @@ -0,0 +1,25 @@ +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.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/Commit.java b/jpos/src/main/java/org/jpos/annotation/Commit.java new file mode 100644 index 0000000000..f67af62d1e --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/Commit.java @@ -0,0 +1,24 @@ +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; +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 Commit { +} 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..5da70b70b1 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/ContextKey.java @@ -0,0 +1,24 @@ +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. + * + * @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 { + public String value(); +} 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 new file mode 100644 index 0000000000..84b3964582 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/Prepare.java @@ -0,0 +1,25 @@ +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; +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 { + public int result() default TransactionConstants.PREPARED; +} diff --git a/jpos/src/main/java/org/jpos/annotation/PrepareForAbort.java b/jpos/src/main/java/org/jpos/annotation/PrepareForAbort.java new file mode 100644 index 0000000000..0bfadb74a1 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/PrepareForAbort.java @@ -0,0 +1,25 @@ +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; +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 PrepareForAbort { + 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..b6baed9b96 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/Registry.java @@ -0,0 +1,23 @@ +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.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 { + 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..a25af05642 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/Return.java @@ -0,0 +1,23 @@ +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.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 { + 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..d99e6791eb --- /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(Object 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..27f90928cc --- /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(Object 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..9b37a8e1c5 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/parameters/ContextPassThruResolver.java @@ -0,0 +1,57 @@ +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; + +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) { + 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(Object participant, Context ctx) { + return (T) new ContextView(ctx, readWrite, readOnly, writeOnly); + } + + }; + } else { + return new Resolver() { + @Override + public T getValue(Object 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..5eed7857fc --- /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(Object 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/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/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..79119686d6 --- /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.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; + + +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 (ISOUtil.isEmpty(registryKey)) { + throw new ConfigurationException("Could not find Registry entry for " + f.getName()); + } + } + + @Override + public T getValue(Object 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 (!ISOUtil.isEmpty(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..234ec4b9d1 --- /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(Object 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..171a66b659 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/MultiValueContextReturnHandler.java @@ -0,0 +1,34 @@ +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(); + 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 getJPosResult(m); + }; + } +} \ 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..64d6516ceb --- /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(Object 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..0850bf83c2 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/ReturnHandlerProvider.java @@ -0,0 +1,23 @@ +package org.jpos.annotation.resolvers.response; + +import java.lang.reflect.Method; + +import org.jpos.annotation.Prepare; +import org.jpos.annotation.PrepareForAbort; +import org.jpos.annotation.resolvers.Priority; + +public interface ReturnHandlerProvider extends Priority { + boolean isMatch(Method m); + + ReturnHandler resolve(Method m); + + default int getJPosResult(Method m) { + int jPosRes = 0; + if (m.isAnnotationPresent(Prepare.class)) { + jPosRes = m.getAnnotation(Prepare.class).result(); + } else if (m.isAnnotationPresent(PrepareForAbort.class)) { + jPosRes = m.getAnnotation(PrepareForAbort.class).result(); + } + return jPosRes; + } +} \ 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..a8b36f051b --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/SingleValueContextReturnHandler.java @@ -0,0 +1,30 @@ +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]; + return (participant, ctx, res) -> { + if (res != null) { + ctx.put(key, res); + } + return getJPosResult(m); + }; + } +} \ 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..ef1bf4e830 --- /dev/null +++ b/jpos/src/main/java/org/jpos/annotation/resolvers/response/VoidContextReturnHandler.java @@ -0,0 +1,19 @@ +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) { + return (participant, ctx, res) -> getJPosResult(m); + } +} \ No newline at end of file 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. 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..2d260c28b4 --- /dev/null +++ b/jpos/src/main/java/org/jpos/transaction/AnnotatedParticipantWrapper.java @@ -0,0 +1,223 @@ +package org.jpos.transaction; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.List; + +import org.jpos.annotation.Abort; +import org.jpos.annotation.AnnotatedParticipant; +import org.jpos.annotation.Commit; +import org.jpos.annotation.Prepare; +import org.jpos.annotation.PrepareForAbort; +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 org.jpos.transaction.AnnotatedParticipantWrapper.AnnotatedMethodType; +import org.jpos.transaction.AnnotatedParticipantWrapper.MethodData; +import org.jpos.transaction.AnnotatedParticipantWrapper.TransactionParticipantHandler; + +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 { + enum AnnotatedMethodType { + PREPARE(Prepare.class, "prepare", "doPrepare"), + COMMIT(Commit.class, "commit"), + ABORT(Abort.class, "abort"), + PREPARE_FOR_ABORT(PrepareForAbort.class, "prepareForAbort"); + + Class annotation; + List methodNames = new ArrayList<>(); + + AnnotatedMethodType(Class annotation, String... methodNames) { + this.annotation = annotation; + for (String name : methodNames) { + this.methodNames.add(name); + } + } + + Class getAnnotation() { + return annotation; + } + } + + class MethodData { + AnnotatedMethodType type; + Method method; + List args = new ArrayList<>(); + ReturnHandler returnHandler; + List exceptionHandlers; + + public boolean isMatch(Method method) { + for (String name : type.methodNames) { + if (method.getName().equals(name) + && method.getParameterCount() == 2 + && long.class.isAssignableFrom(method.getParameterTypes()[0]) + && Serializable.class.isAssignableFrom(method.getParameterTypes()[1])) { + return true; + } + } + return false; + } + + int execute(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 = method.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 " + method.toString()); + ctx.log(e); + for(ReturnExceptionHandler handler: exceptionHandlers) { + if (handler.isMatch(e)) { + return handler.doReturn(participant, ctx, e); + } + } + throw new RuntimeException(e); + } + } + + protected Object participant; + protected EnumMap methods = new EnumMap<>(AnnotatedMethodType.class); + + /** + * Wraps the given {@link TransactionParticipant} object with an {@link AnnotatedParticipantWrapper}, + * enabling dynamic method invocation based on annotations. This method is designed to enhance objects + * marked with {@link AnnotatedParticipant} by providing additional functionalities + * such as customized prepared method handling. + * + * Note that the return object is a pass thru subclass of the original participant. But while the methods + * are passed thru to the underlying object, if you access field variables directly, you will be accessing + * the subclass field variables and not the original participant. For this reason, if the wrapped object is + * used directly, only access methods. + * + * @param obj the object to wrap, typically an instance of a class annotated with + * {@link AnnotatedParticipant}. + * @return an instance of {@link AnnotatedParticipantWrapper} that wraps the provided + * object, enriched with dynamic annotation processing capabilities. + */ + @SuppressWarnings("unchecked") + public static T wrap(Object participant) throws ConfigurationException { + try { + AnnotatedParticipantWrapper handler = new AnnotatedParticipantWrapper(participant); + List interfaces = new ArrayList<>(); + interfaces.add(TransactionParticipant.class); + if (handler.methods.containsKey(AnnotatedMethodType.PREPARE_FOR_ABORT)) { + interfaces.add(AbortParticipant.class); + } + return (T) new ByteBuddy() + .subclass(participant.getClass()) + .implement(interfaces.toArray(new Class[0])) + .method(ElementMatchers.any()) + .intercept(MethodDelegation.to(new TransactionParticipantHandler(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(Object participant) { + return participant.getClass().isAnnotationPresent(AnnotatedParticipant.class); + } + + public AnnotatedParticipantWrapper(Object participant) throws ConfigurationException { + this.participant = participant; + configureAnnotatedMethods(); + } + + + protected List configureParameters(Method m) throws ConfigurationException { + List args = new ArrayList<>(); + for(Parameter p: m.getParameters()) { + args.add(ResolverFactory.INSTANCE.getResolver(p)); + } + return args; + } + + protected void configureAnnotatedMethods() throws ConfigurationException { + for(Method m: participant.getClass().getMethods()) { + for (AnnotatedMethodType type : AnnotatedMethodType.values()) { + if (m.isAnnotationPresent(type.getAnnotation())) { + if (methods.containsKey(type)) { + throw new ConfigurationException("Only one method per class can be defined with the @" + + type.getAnnotation().getSimpleName() + ". " + participant.getClass().getSimpleName() + + " has multiple matches."); + } + MethodData data = new MethodData(); + data.type = type; + data.method = m; + data.exceptionHandlers = ResolverFactory.INSTANCE.getExceptionHandlers(m); + data.returnHandler = ResolverFactory.INSTANCE.getReturnHandler(m); + data.args = configureParameters(m); + methods.put(type, data); + } + } + } + + if (methods.isEmpty()) { + throw new ConfigurationException(participant.getClass().getSimpleName() + " needs one method defined with the @Prepare annotation."); + } + if (!(participant instanceof TransactionParticipant) && + !methods.keySet().containsAll( + Arrays.asList(AnnotatedMethodType.PREPARE, AnnotatedMethodType.COMMIT, AnnotatedMethodType.ABORT))) { + throw new ConfigurationException( + participant.getClass().getSimpleName() + " needs to define all of the @Prepare, @Commit, and @Abort annotations or implement TransactionParticipant."); + } + } + + public static class TransactionParticipantHandler { + + AnnotatedParticipantWrapper parent; + + private TransactionParticipantHandler(AnnotatedParticipantWrapper parent) { + this.parent = parent; + } + + @RuntimeType + public Object intercept(@Origin Method method, @AllArguments @RuntimeType Object[] args) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + for (MethodData data : parent.methods.values()) { + if (data.isMatch(method)) { + int res = data.execute((long) args[0], (Serializable) args[1]); + if (method.getReturnType().equals(void.class)) { + return null; + } else { + return res; + } + } + } + return method.invoke(parent.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..b20029db9f 100644 --- a/jpos/src/main/java/org/jpos/transaction/TransactionManager.java +++ b/jpos/src/main/java/org/jpos/transaction/TransactionManager.java @@ -805,7 +805,7 @@ public TransactionParticipant createParticipant (Element e) throws ConfigurationException { QFactory factory = getFactory(); - TransactionParticipant participant = + Object participant = factory.newInstance (QFactory.getAttributeValue (e, "class") ); factory.setLogger (participant, e); @@ -816,11 +816,17 @@ public TransactionParticipant createParticipant (Element e) realm = ":" + realm; else realm = ""; - names.put(participant, Caller.shortClassName(participant.getClass().getName())+realm); if (participant instanceof Destroyable) { destroyables.add((Destroyable) participant); } - return participant; + if (AnnotatedParticipantWrapper.isMatch(participant)) { + participant = AnnotatedParticipantWrapper.wrap(participant); + } + TransactionParticipant rParticipant = (TransactionParticipant) participant; + + names.put(rParticipant, Caller.shortClassName(participant.getClass().getName())+realm); + + return rParticipant; } @Override 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..3b61a7cdc3 --- /dev/null +++ b/jpos/src/test/java/org/jpos/transaction/AnnotatedParticipantWrapperTest.java @@ -0,0 +1,612 @@ +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.doNothing; +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.ContextKeys; +import org.jpos.annotation.Prepare; +import org.jpos.annotation.Commit; +import org.jpos.annotation.Abort; +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.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; +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 final int prepare(long id, Serializable context) { + return doPrepare(id, (Context) context); + } + + public int doPrepare(long id, Context ctx) { + 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(); + } + } + + @AnnotatedParticipant + public static class AnnotatedParticipantNoTxnSupport { + @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 void setConfiguration(Configuration cfg) {} + + @Abort + @Return("CARD") + public Object getCardAbort(Context ctx, @ContextKey("DB") Object db, @Registry Long someKey) throws Exception { + Assertions.assertNotNull(ctx); + Assertions.assertNotNull(db); + Assertions.assertEquals(2, someKey); + return new Object(); + } + + @Commit + @Return("CARD") + public Object getCardCommit(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 wrapper annotation + AnnotatedParticipantNoTxnSupport pwn = new AnnotatedParticipantNoTxnSupport(); + pwn.setConfiguration(cfg); + assertTrue(AnnotatedParticipantWrapper.isMatch(pwn)); + TransactionParticipant pt = AnnotatedParticipantWrapper.wrap(pwn); + assertAnnotatedParticipant(ctx, pt); + ctx.remove("CARD"); + pt.commit(0, ctx); + assertNotNull(ctx.get("CARD")); + } + + @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()); + doReturn(new AnnotatedParticipantNoTxnSupport()).when(f).newInstance(AnnotatedParticipantNoTxnSupport.class.getCanonicalName()); + txnMgr.setServer(q2); + txnMgr.setName("txnMgr"); + txnMgr.setConfiguration(new SimpleConfiguration()); + + String regParticipantXml = ""; + String annotatedParticipantXml = ""; + String annotatedParticipantNoTxnSupportXml = ""; + + Context ctx = new Context(); + ctx.put("DB", new Object()); + NameRegistrar.register("someKey", 2L); + + TransactionParticipant p = (TxnSupport) getParticipant(txnMgr, regParticipantXml); + assertRegularParticipant(ctx, p, false); + + p = getParticipant(txnMgr, annotatedParticipantXml); + assertAnnotatedParticipant(ctx, p); + + p = getParticipant(txnMgr, annotatedParticipantNoTxnSupportXml); + 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, (c)-> 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, (c)-> 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, (c)-> 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 { + default Object call() throws Exception { + return call(null); + } + + Object call(Context ctx) 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, (c)-> {throw new RuntimeException();}, CMF.INTERNAL_ERROR); + final Exception circularCause = new Exception() { + public synchronized Throwable getCause() { + return this; + }; + }; + testException(p, (c)-> {throw circularCause;}, CMF.INTERNAL_ERROR); + } + + @Test + public void testUncaughtExceptionHandling() throws ConfigurationException { + TransactionParticipant p = AnnotatedParticipantWrapper.wrap(new HappyPass()); + Handler h = (c)->{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"); + } + + 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, TransactionParticipant p) { + ctx.remove("CARD"); + assertEquals(TransactionConstants.PREPARED | TransactionConstants.READONLY, p.prepare(0, ctx)); + assertNotNull(ctx.get("CARD")); + } + + protected void assertRegularParticipant(Context ctx, TransactionParticipant p, boolean ignoreAnnotation) { + if (!ignoreAnnotation) { + assertFalse(AnnotatedParticipantWrapper.isMatch(p)); + } + assertEquals(TransactionConstants.ABORTED, p.prepare(0, ctx)); + assertNull(ctx.get("CARD")); + } + + 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()); + + } + +}