From 756f94b9488790f3c7d848c51d064e025ae79b20 Mon Sep 17 00:00:00 2001 From: pith Date: Wed, 15 Apr 2015 14:29:59 +0200 Subject: [PATCH] Added support for tuple in the assembler DSL --- .../dsl/AssemblerDslWithTupleIT.java | 46 ++++--- core/src/it/resources/logback-test.xml | 4 + .../business/core/domain/FactoryInternal.java | 11 +- .../dsl/AggAssemblerProviderImpl.java | 11 +- .../dsl/AggAssemblerWithRepoProviderImpl.java | 33 ++++- .../BaseAggAssemblerWithRepoProviderImpl.java | 119 +++++++++-------- .../dsl/InternalRegistryInternal.java | 4 +- ...TupleAggAssemblerWithRepoProviderImpl.java | 121 +++++++++++++----- .../resolver/AnnotationResolver.java | 90 +++++++------ .../resolver/ParameterHolderInternal.java | 88 +++++++------ .../business/internal/event/EventModule.java | 2 +- .../assembler/dsl/AssembleTest.java | 3 +- .../assembler/dsl/fixture/Customer.java | 11 +- .../dsl/fixture/CustomerRepository.java | 18 +++ .../fixture/CustomerRepositoryInternal.java | 56 ++++++++ .../assembler/dsl/fixture/Recipe.java | 63 +++++++++ .../resolver/AnnotationResolverTest.java | 59 ++++----- .../resolver/ParameterHolderTest.java | 85 ++++++++++++ .../assembler/resolver/sample/Case2Dto.java | 4 + .../assembler/resolver/sample/Case3Dto.java | 22 ++-- .../assembler/resolver/sample/Case4Dto.java | 24 +++- .../org/seedstack/business/api/Tuples.java | 53 ++++---- .../assembler/MatchingEntityId.java | 118 ++++++++++++----- .../assembler/MatchingFactoryParameter.java | 64 ++++++++- .../dsl/BaseAggAssemblerProvider.java | 27 +++- .../assembler/resolver/ParameterHolder.java | 59 +++++++-- ...ess.api.domain.DomainErrorCodes.properties | 4 +- .../seedstack/business/api/TuplesTest.java | 18 ++- 28 files changed, 885 insertions(+), 332 deletions(-) create mode 100644 core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/CustomerRepository.java create mode 100644 core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/CustomerRepositoryInternal.java create mode 100644 core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/Recipe.java create mode 100644 core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/ParameterHolderTest.java diff --git a/core/src/it/java/org/seedstack/business/assembler/dsl/AssemblerDslWithTupleIT.java b/core/src/it/java/org/seedstack/business/assembler/dsl/AssemblerDslWithTupleIT.java index 63fa24a9..cab93d5e 100644 --- a/core/src/it/java/org/seedstack/business/assembler/dsl/AssemblerDslWithTupleIT.java +++ b/core/src/it/java/org/seedstack/business/assembler/dsl/AssemblerDslWithTupleIT.java @@ -14,15 +14,10 @@ import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.seedstack.business.api.domain.Factory; import org.seedstack.business.api.domain.Repository; import org.seedstack.business.api.interfaces.assembler.FluentAssembler; import org.seedstack.business.api.interfaces.assembler.dsl.AggregateNotFoundException; -import org.seedstack.business.core.interfaces.assembler.dsl.fixture.Customer; -import org.seedstack.business.core.interfaces.assembler.dsl.fixture.Order; -import org.seedstack.business.core.interfaces.assembler.dsl.fixture.OrderDto; -import org.seedstack.business.core.interfaces.assembler.dsl.fixture.OrderFactory; -import org.seedstack.business.api.Tuples; +import org.seedstack.business.core.interfaces.assembler.dsl.fixture.*; import org.seedstack.seed.it.SeedITRunner; import javax.inject.Inject; @@ -41,48 +36,51 @@ public class AssemblerDslWithTupleIT { private Repository orderRepository; @Inject - private OrderFactory orderFactory; + private CustomerRepository customerRepository; @Inject - private Factory customerFactory; + private OrderFactory orderFactory; @Inject private FluentAssembler fluently; @Test - @Ignore public void testAssembleFromFactory() { - OrderDto orderDto = new OrderDto("1", "light saber"); - orderDto.setCustomerName("luke"); + Recipe recipe = new Recipe("customer1", "luke", "order1", "light saber"); - Pair orderCustomerClasses = Tuples.create(Order.class, Customer.class); - Pair orderCustomerPair = fluently.assemble().dto(orderDto).to(orderCustomerClasses).fromFactory(); + Pair orderCustomerPair = fluently.assemble().dto(recipe).>to(Order.class, Customer.class).fromFactory(); Assertions.assertThat(orderCustomerPair.getValue0()).isNotNull(); - Assertions.assertThat(orderCustomerPair.getValue0().getEntityId()).isEqualTo("1"); + Assertions.assertThat(orderCustomerPair.getValue0().getEntityId()).isEqualTo("order1"); Assertions.assertThat(orderCustomerPair.getValue0().getProduct()).isEqualTo("light saber"); // the customer name is not part of the factory parameters, so it is set by the assembler - Assertions.assertThat(orderCustomerPair.getValue1().getEntityId()).isEqualTo("luke"); + Assertions.assertThat(orderCustomerPair.getValue1().getEntityId()).isEqualTo("customer1"); + Assertions.assertThat(orderCustomerPair.getValue1().getName()).isEqualTo("luke"); } @Test - @Ignore public void testAssembleFromRepository() { - Order order = orderFactory.create("1", "death star"); + Recipe recipe = new Recipe("customer1", "luke", "order1", "light saber"); + + Order order = orderFactory.create("order1", "death star"); order.setOtherDetails("some details"); orderRepository.persist(order); - Order aggregateRoot = null; + Customer customer = new Customer("customer1"); + customerRepository.persist(customer); + + Pair orderCustomerPair = null; try { - aggregateRoot = fluently.assemble().dto(new OrderDto("1", "light saber")).to(Order.class).fromRepository().orFail(); + orderCustomerPair = fluently.assemble().dto(recipe).>to(Order.class, Customer.class).fromRepository().orFail(); } catch (AggregateNotFoundException e) { fail(); } - Assertions.assertThat(aggregateRoot).isNotNull(); - Assertions.assertThat(aggregateRoot.getEntityId()).isEqualTo("1"); - Assertions.assertThat(aggregateRoot.getProduct()).isEqualTo("light saber"); - // other details come from the aggregate loaded from the repository - Assertions.assertThat(aggregateRoot.getOtherDetails()).isEqualTo("some details"); + Assertions.assertThat(orderCustomerPair.getValue0()).isNotNull(); + Assertions.assertThat(orderCustomerPair.getValue0().getEntityId()).isEqualTo("order1"); + Assertions.assertThat(orderCustomerPair.getValue0().getProduct()).isEqualTo("light saber"); + // the customer name is not part of the factory parameters, so it is set by the assembler + Assertions.assertThat(orderCustomerPair.getValue1().getEntityId()).isEqualTo("customer1"); + Assertions.assertThat(orderCustomerPair.getValue1().getName()).isEqualTo("luke"); } @Test diff --git a/core/src/it/resources/logback-test.xml b/core/src/it/resources/logback-test.xml index f7e67518..b1217fe7 100644 --- a/core/src/it/resources/logback-test.xml +++ b/core/src/it/resources/logback-test.xml @@ -16,6 +16,10 @@ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + diff --git a/core/src/main/java/org/seedstack/business/core/domain/FactoryInternal.java b/core/src/main/java/org/seedstack/business/core/domain/FactoryInternal.java index 04167786..d3a270a8 100644 --- a/core/src/main/java/org/seedstack/business/core/domain/FactoryInternal.java +++ b/core/src/main/java/org/seedstack/business/core/domain/FactoryInternal.java @@ -23,6 +23,7 @@ import javax.inject.Inject; import java.lang.reflect.Constructor; +import java.util.Arrays; /** * FactoryInternal allows the creations of {@link org.seedstack.business.api.domain.DomainObject} objects using their constructors. @@ -79,17 +80,17 @@ public DO create(Object... args) { Constructor constructor = MethodMatcher.findMatchingConstructor(getProducedClass(), args); DO domainObject; if (constructor == null) { - throw SeedException.createNew(DomainErrorCodes.DOMAIN_OBJECT_CONSTRUCTOR_NOT_FOUND) - .put("domainObject", getProducedClass().getSimpleName()).put("parameters", args); - } - try { + throw SeedException.createNew(DomainErrorCodes.DOMAIN_OBJECT_CONSTRUCTOR_NOT_FOUND) + .put("domainObject", getProducedClass()).put("parameters", Arrays.toString(args)); + } + try { constructor.setAccessible(true); //noinspection unchecked domainObject = (DO) constructor.newInstance(args); } catch (Exception e) { throw SeedException.wrap(e, DomainErrorCodes.UNABLE_TO_INVOKE_CONSTRUCTOR).put("constructor", constructor) - .put("domainObject", getProducedClass().getSimpleName()).put("parameters", args); + .put("domainObject", getProducedClass()).put("parameters", Arrays.toString(args)); } return domainObject; } diff --git a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/AggAssemblerProviderImpl.java b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/AggAssemblerProviderImpl.java index 74dbc82e..cda868dd 100644 --- a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/AggAssemblerProviderImpl.java +++ b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/AggAssemblerProviderImpl.java @@ -10,10 +10,13 @@ package org.seedstack.business.core.interfaces.assembler.dsl; import org.javatuples.Tuple; +import org.seedstack.business.api.Tuples; import org.seedstack.business.api.domain.AggregateRoot; import org.seedstack.business.api.interfaces.assembler.Assembler; import org.seedstack.business.api.interfaces.assembler.dsl.*; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -36,8 +39,12 @@ public > AggAssemblerWithRepoProvider to(Class } @Override - public TupleAggAssemblerWithRepoProvider to(T aggregateRootTuple) { - assemblerContext.setAggregateClasses(aggregateRootTuple); + public TupleAggAssemblerWithRepoProvider to(Class> firstAggregateClass, Class> secondAggregateClass, Class>... otherAggregateClasses) { + List> aggregateRootClasses = new ArrayList>(); + aggregateRootClasses.add(firstAggregateClass); + aggregateRootClasses.add(secondAggregateClass); + aggregateRootClasses.addAll(Arrays.asList(otherAggregateClasses)); + assemblerContext.setAggregateClasses(Tuples.create((List)aggregateRootClasses)); return new TupleAggAssemblerWithRepoProviderImpl(registry, assemblerContext); } diff --git a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/AggAssemblerWithRepoProviderImpl.java b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/AggAssemblerWithRepoProviderImpl.java index 7d16ed33..d8edff11 100644 --- a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/AggAssemblerWithRepoProviderImpl.java +++ b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/AggAssemblerWithRepoProviderImpl.java @@ -11,9 +11,12 @@ import org.seedstack.business.api.domain.AggregateRoot; import org.seedstack.business.api.domain.GenericFactory; +import org.seedstack.business.api.domain.Repository; +import org.seedstack.business.api.interfaces.assembler.Assembler; import org.seedstack.business.api.interfaces.assembler.dsl.AggAssemblerWithRepoAndFactProvider; import org.seedstack.business.api.interfaces.assembler.dsl.AggAssemblerWithRepoProvider; import org.seedstack.business.api.interfaces.assembler.dsl.AggregateNotFoundException; +import org.seedstack.business.api.interfaces.assembler.resolver.ParameterHolder; /** * @author Pierre Thirouin @@ -36,10 +39,24 @@ public AggAssemblerWithRepoAndFactProvider fromRepository() { @Override public A fromFactory() { GenericFactory genericFactory = (GenericFactory) registry.genericFactoryOf(assemblerContext.getAggregateClass()); - A aggregateRoot = (A) getAggregateFromFactory(genericFactory, assemblerContext.getAggregateClass()); + ParameterHolder parameterHolder = dtoInfoResolver.resolveAggregate(assemblerContext.getDto()); + A aggregateRoot = (A) getAggregateFromFactory(genericFactory, assemblerContext.getAggregateClass(), parameterHolder.parameters()); return assembleWithDto(aggregateRoot); } + /** + * Loads an aggregate roots from a repository. + * + * @param key the aggregate roots identity + * @param the aggregate root type + * @return the loaded aggregate root + */ + protected A loadFromRepo(Object key) { + Repository repository = assemblerContext.getRepository(); + //noinspection unchecked + return (A) repository.load(key); + } + // --------------------------- AggAssemblerWithRepoAndFactProvider methods @Override @@ -69,4 +86,18 @@ public A thenFromFactory() { } } + /** + * Assemble one or a tuple of aggregate root from a dto. + * + * @param aggregateRoots the aggregate root(s) to assemble + * @param type of aggregate root(s). It could be a {@code Tuple} or an {@code AggregateRoot} + * @return the assembled aggregate root(s) + */ + protected T assembleWithDto(T aggregateRoots) { + Assembler assembler = registry.assemblerOf(assemblerContext.getAggregateClass(), assemblerContext.getDto().getClass()); + //noinspection unchecked + assembler.mergeAggregateWithDto(aggregateRoots, assemblerContext.getDto()); + return aggregateRoots; + } + } diff --git a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/BaseAggAssemblerWithRepoProviderImpl.java b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/BaseAggAssemblerWithRepoProviderImpl.java index 314dcda0..dc4a4854 100644 --- a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/BaseAggAssemblerWithRepoProviderImpl.java +++ b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/BaseAggAssemblerWithRepoProviderImpl.java @@ -9,8 +9,9 @@ */ package org.seedstack.business.core.interfaces.assembler.dsl; +import org.javatuples.Tuple; +import org.seedstack.business.api.Tuples; import org.seedstack.business.api.domain.*; -import org.seedstack.business.api.interfaces.assembler.Assembler; import org.seedstack.business.api.interfaces.assembler.resolver.DtoInfoResolver; import org.seedstack.business.api.interfaces.assembler.resolver.ParameterHolder; import org.seedstack.business.core.interfaces.assembler.resolver.AnnotationResolver; @@ -20,7 +21,9 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; /** * @author Pierre Thirouin @@ -42,88 +45,94 @@ public BaseAggAssemblerWithRepoProviderImpl(InternalRegistry registry, Assembler this.assemblerContext = assemblerContext; } - /** - * Assemble one or a tuple of aggregate root from a dto. - * - * @param aggregateRoots the aggregate root(s) to assemble - * @param type of aggregate root(s). It could be a {@code Tuple} or an {@code AggregateRoot} - * @return the assembled aggregate root(s) - */ - protected T assembleWithDto(T aggregateRoots) { - Assembler assembler = registry.assemblerOf(assemblerContext.getAggregateClass(), assemblerContext.getDto().getClass()); - //noinspection unchecked - assembler.mergeAggregateWithDto(aggregateRoots, assemblerContext.getDto()); - return aggregateRoots; - } + protected Object resolveId(Object dto, Class> aggregateRootClass) { + SeedCheckUtils.checkIfNotNull(dto); + SeedCheckUtils.checkIfNotNull(aggregateRootClass); - /** - * Loads an aggregate roots from a repository. - * - * @param key the aggregate roots identity - * @param the aggregate root type - * @return the loaded aggregate root - */ - protected A loadFromRepo(Object key) { - Repository repository = assemblerContext.getRepository(); - //noinspection unchecked - return (A) repository.load(key); + ParameterHolder parameterHolder = dtoInfoResolver.resolveId(dto); + if (parameterHolder.isEmpty()) { + throw new IllegalArgumentException("No id found in the DTO. Please check the @MatchingEntityId annotation."); + } + + return paramsToIds(aggregateRootClass, parameterHolder, -1); } - protected Object resolveId(Object dto, Class> aggregateRootClass) { - Object id; + protected Tuple resolveIds(Object dto, Tuple aggregateRootClasses) { + SeedCheckUtils.checkIfNotNull(dto); + SeedCheckUtils.checkIfNotNull(aggregateRootClasses); ParameterHolder parameterHolder = dtoInfoResolver.resolveId(dto); if (parameterHolder.isEmpty()) { throw new IllegalArgumentException("No id found in the DTO. Please check the @MatchingEntityId annotation."); } + List ids = new ArrayList(); + int aggregateIndex = 0; + for (Object aggregateRootClass : aggregateRootClasses) { + //noinspection unchecked + ids.add(paramsToIds((Class>) aggregateRootClass, parameterHolder, aggregateIndex)); + aggregateIndex++; + } + + return Tuples.create(ids); + } + + private Object paramsToIds(Class> aggregateRootClass, ParameterHolder parameterHolder, int aggregateIndex) { + Object id; + //noinspection unchecked Class aggregateIdClass = (Class) BusinessUtils.getAggregateIdClass(aggregateRootClass); - // TODO : check the case when one of the parameters is null - if (parameterHolder.first() != null && aggregateIdClass.isAssignableFrom(parameterHolder.first().getClass())) { + + Object element = parameterHolder.uniqueElementForAggregateRoot(aggregateIndex); + if (element != null && aggregateIdClass.isAssignableFrom(element.getClass())) { // The first parameter is already the id we are looking for - id = parameterHolder.first(); + id = element; } else { + if (!ValueObject.class.isAssignableFrom(aggregateIdClass)) { + throw new IllegalStateException("The " + aggregateRootClass.getCanonicalName() + "'s id is not a value object, so you don't have to specify the index in @MatchingEntityId(index = 0)"); + } // Get the "magic" factory for the aggregate id class Factory factory = registry.defaultFactoryOf(aggregateIdClass); // Create the id based on the id constructor matching the given parameters - id = factory.create(parameterHolder.parameters()); - // TODO : what if there is an actual factory for this value object ? + // TODO : check the case when one of the parameters is null + id = factory.create(parameterHolder.parametersOfAggregateRoot(aggregateIndex)); } - if (id == null) { throw new IllegalArgumentException("No id found in the DTO. Please check the @MatchingEntityId annotation."); } - return id; } - protected Object getAggregateFromFactory(GenericFactory factory, Class> aggregateClass) { + protected Object getAggregateFromFactory(GenericFactory factory, Class> aggregateClass, Object[] parameters) { SeedCheckUtils.checkIfNotNull(factory); SeedCheckUtils.checkIfNotNull(aggregateClass); + SeedCheckUtils.checkIfNotNull(parameters); - // Extract the factory parameters from the DTO (using @MatchingFactoryParameter) - ParameterHolder parameterHolder = dtoInfoResolver.resolveAggregate(assemblerContext.getDto()); - if (parameterHolder.isEmpty()) { - throw new IllegalArgumentException("No factory parameters found in the DTO. Please check the @MatchingFactoryParameter annotation."); - } - - // Find the method in the factory which match the signature determined with the previously extracted parameters - Method factoryMethod = MethodMatcher.findMatchingMethod(factory.getClass(), aggregateClass, parameterHolder.parameters()); - if (factoryMethod == null) { - throw new IllegalStateException(factory.getClass().getSimpleName() + - ": Enable to find a method matching the parameter [" + - Arrays.toString(parameterHolder.parameters()) + "]"); + if (parameters.length == 0) { + throw new IllegalArgumentException(assemblerContext.getDto().getClass() + " - No factory parameters found in the DTO. Please check the @MatchingFactoryParameter annotation."); } - // Invoke the factory to create the aggregate root - try { - //noinspection unchecked - return factoryMethod.invoke(factory, parameterHolder.parameters()); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Failed to call " + factoryMethod.getName(), e); - } catch (InvocationTargetException e) { - throw new IllegalStateException("Failed to call " + factoryMethod.getName(), e); + if (Factory.class.isAssignableFrom(factory.getClass())) { + Factory defaultFactory = (Factory) factory; + return defaultFactory.create(parameters); + } else { + // Find the method in the factory which match the signature determined with the previously extracted parameters + Method factoryMethod = MethodMatcher.findMatchingMethod(factory.getClass(), aggregateClass, parameters); + if (factoryMethod == null) { + throw new IllegalStateException(factory.getClass().getSimpleName() + + " - Enable to find a method matching the parameters " + + Arrays.toString(parameters)); + } + + // Invoke the factory to create the aggregate root + try { + //noinspection unchecked + return factoryMethod.invoke(factory, parameters); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Failed to call " + factoryMethod.getName(), e); + } catch (InvocationTargetException e) { + throw new IllegalStateException("Failed to call " + factoryMethod.getName(), e); + } } } } diff --git a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/InternalRegistryInternal.java b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/InternalRegistryInternal.java index e8cef7be..091472fd 100644 --- a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/InternalRegistryInternal.java +++ b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/InternalRegistryInternal.java @@ -73,10 +73,10 @@ public InternalRegistryInternal(Injector injector) { public Assembler tupleAssemblerOf(Tuple aggregateRootTuple, Class dto) { List>> aggregateClasses = Lists.newArrayList(); for (Object o : aggregateRootTuple) { - if (!(o instanceof AggregateRoot)) { + if (!(o instanceof Class) || !AggregateRoot.class.isAssignableFrom((Class)o)) { throw new IllegalArgumentException("The aggregateRootTuple parameter should only contain aggregates. But found " + o); } - aggregateClasses.add((Class>) o.getClass()); + aggregateClasses.add((Class>) o); } return tupleAssemblerOf(aggregateClasses, dto); } diff --git a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/TupleAggAssemblerWithRepoProviderImpl.java b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/TupleAggAssemblerWithRepoProviderImpl.java index a4e15e84..d0adbeda 100644 --- a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/TupleAggAssemblerWithRepoProviderImpl.java +++ b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/dsl/TupleAggAssemblerWithRepoProviderImpl.java @@ -9,14 +9,17 @@ */ package org.seedstack.business.core.interfaces.assembler.dsl; +import org.javatuples.Triplet; import org.javatuples.Tuple; +import org.seedstack.business.api.Tuples; import org.seedstack.business.api.domain.AggregateRoot; import org.seedstack.business.api.domain.GenericFactory; import org.seedstack.business.api.domain.Repository; +import org.seedstack.business.api.interfaces.assembler.Assembler; import org.seedstack.business.api.interfaces.assembler.dsl.AggregateNotFoundException; import org.seedstack.business.api.interfaces.assembler.dsl.TupleAggAssemblerWithRepoAndFactProvider; import org.seedstack.business.api.interfaces.assembler.dsl.TupleAggAssemblerWithRepoProvider; -import org.seedstack.business.api.Tuples; +import org.seedstack.business.api.interfaces.assembler.resolver.ParameterHolder; import java.util.ArrayList; import java.util.List; @@ -26,8 +29,6 @@ */ public class TupleAggAssemblerWithRepoProviderImpl extends BaseAggAssemblerWithRepoProviderImpl implements TupleAggAssemblerWithRepoProvider, TupleAggAssemblerWithRepoAndFactProvider { - private final List> repositories = new ArrayList>(2); - public TupleAggAssemblerWithRepoProviderImpl(InternalRegistry registry, AssemblerContext assemblerContext) { super(registry, assemblerContext); } @@ -36,9 +37,7 @@ public TupleAggAssemblerWithRepoProviderImpl(InternalRegistry registry, Assemble @Override public TupleAggAssemblerWithRepoAndFactProvider fromRepository() { - for (Object o : assemblerContext.getAggregateClasses()) { - repositories.add(registry.repositoryOf((Class>) o)); - } + // it just redirect to the good interface return this; } @@ -46,12 +45,19 @@ public TupleAggAssemblerWithRepoAndFactProvider fromRepository() { @Override public T fromFactory() { List aggregateRoots = new ArrayList(); + ParameterHolder parameterHolder = dtoInfoResolver.resolveAggregate(assemblerContext.getDto()); + int aggregateIndex = 0; for (Object o : assemblerContext.getAggregateClasses()) { if (o instanceof Class) { Class> aggregateClass = (Class>) o; GenericFactory genericFactory = registry.genericFactoryOf(aggregateClass); - aggregateRoots.add(getAggregateFromFactory(genericFactory, aggregateClass)); + Object aggregate = getAggregateFromFactory(genericFactory, aggregateClass, parameterHolder.parametersOfAggregateRoot(aggregateIndex)); + aggregateRoots.add(aggregate); + } else { + // TODO seed exception + throw new IllegalArgumentException(o + " should be a class. the .to(Tuple aggregateClasses) method only accepts tuple of aggregate root classes."); } + aggregateIndex++; } return (T) assembleWithDto(Tuples.create(aggregateRoots)); } @@ -61,41 +67,98 @@ public T fromFactory() { @SuppressWarnings("unchecked") @Override public T orFail() throws AggregateNotFoundException { - List aggregateRoots = new ArrayList(); - for (Object o : assemblerContext.getAggregateClasses()) { - Class> aggregateClass = (Class>) o; - Object id = resolveId(assemblerContext.getDto(), aggregateClass); - AggregateRoot a = loadFromRepo(id); + // list of triplet - each triplet contains the aggregate root instance, its class and its id (useful if the AR is null). + List, Object>> aggregateRootsMetadata = loadFromRepository(); - if (a == null) { - throw new AggregateNotFoundException(String.format("Unable to load aggregate %s for id: %s", aggregateClass, id)); + boolean shouldThrow = false; + + List> aggregateRoots = new ArrayList>(); + + StringBuilder stringBuilder = new StringBuilder().append("Unable to load "); + for (Triplet, Object> triplet : aggregateRootsMetadata) { + + if (triplet.getValue0() == null) { + // If at least one aggregate root is null we throw a AggregateNotFoundException + shouldThrow = true; + stringBuilder.append("aggregate: ").append(triplet.getValue1()).append(" for id: ").append(triplet.getValue2()); + } else { + aggregateRoots.add((AggregateRoot) triplet.getValue0()); } + } - aggregateRoots.add(a); + if (shouldThrow) { + throw new AggregateNotFoundException(stringBuilder.toString()); } + return (T) assembleWithDto(Tuples.create(aggregateRoots)); } @SuppressWarnings("unchecked") @Override public T thenFromFactory() { - List aggregateRoots = new ArrayList(); - for (Object o : assemblerContext.getAggregateClasses()) { - // load from the repository - Class> aggregateClass = (Class>) o; - Object id = resolveId(assemblerContext.getDto(), aggregateClass); - AggregateRoot a = loadFromRepo(id); - - if (a != null) { - // then assemble the dto in the previously created aggregate root - aggregateRoots.add(a); + // list of triplet - each triplet contains the aggregate root instance, its class and its id (useful if the AR is null). + List, Object>> aggregateRootsMetadata = loadFromRepository(); + + List> aggregateRoots = new ArrayList>(); + + StringBuilder errorMessage = new StringBuilder().append("Unable to load "); + for (Triplet, Object> triplet : aggregateRootsMetadata) { + + if (triplet.getValue0() == null) { + errorMessage.append("aggregate: ").append(triplet.getValue1()).append(" for id: ").append(triplet.getValue2()); } else { - // otherwise fallback on the factory - GenericFactory genericFactory = registry.genericFactoryOf(aggregateClass); - aggregateRoots.add(getAggregateFromFactory(genericFactory, aggregateClass)); + aggregateRoots.add((AggregateRoot) triplet.getValue0()); } } - return Tuples.create(aggregateRoots); + + T result; + if (aggregateRootsMetadata.isEmpty()) { + // should not append + result = null; + } else if (aggregateRoots.isEmpty()) { + // No aggregate root persisted -> fallback on factories + result = fromFactory(); + } else if (aggregateRootsMetadata.size() != aggregateRoots.size()) { + // data are inconsistent some required aggregate roots are persisted but not all + throw new IllegalStateException(errorMessage.toString()); + } else { + // all aggregate roots are loaded -> assemble them and return them + result = (T) assembleWithDto(Tuples.create(aggregateRoots)); + } + return result; + } + + @SuppressWarnings("unchecked") + private List, Object>> loadFromRepository() { + Tuple aggregateClasses = assemblerContext.getAggregateClasses(); + Tuple ids = resolveIds(assemblerContext.getDto(), aggregateClasses); + + List, Object>> aggregateRoots = new ArrayList, Object>>(); + for (int i = 0; i < ids.getSize(); i++) { + Class> aggregateClass = (Class>) aggregateClasses.getValue(i); + Object id = ids.getValue(i); + + Repository repository = registry.repositoryOf(aggregateClass); + AggregateRoot aggregateRoot = repository.load(id); + + aggregateRoots.add(new Triplet, Object>(aggregateRoot, aggregateClass, id)); + } + + return aggregateRoots; + } + + /** + * Assemble one or a tuple of aggregate root from a dto. + * + * @param aggregateRoots the aggregate root(s) to assemble + * @param type of aggregate root(s). It could be a {@code Tuple} or an {@code AggregateRoot} + * @return the assembled aggregate root(s) + */ + protected T assembleWithDto(T aggregateRoots) { + Assembler assembler = registry.tupleAssemblerOf(assemblerContext.getAggregateClasses(), assemblerContext.getDto().getClass()); + //noinspection unchecked + assembler.mergeAggregateWithDto(aggregateRoots, assemblerContext.getDto()); + return aggregateRoots; } } diff --git a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/resolver/AnnotationResolver.java b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/resolver/AnnotationResolver.java index ba470f63..407d50c3 100644 --- a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/resolver/AnnotationResolver.java +++ b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/resolver/AnnotationResolver.java @@ -9,79 +9,93 @@ */ package org.seedstack.business.core.interfaces.assembler.resolver; -import org.fest.reflect.core.Reflection; import org.seedstack.business.api.interfaces.assembler.MatchingEntityId; import org.seedstack.business.api.interfaces.assembler.MatchingFactoryParameter; import org.seedstack.business.api.interfaces.assembler.resolver.DtoInfoResolver; import org.seedstack.business.api.interfaces.assembler.resolver.ParameterHolder; -import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** + * Implementation of the {@link org.seedstack.business.api.interfaces.assembler.resolver.DtoInfoResolver} + * based on the MatchingFactoryParameter and MatchingFactoryParameter annotation. + *

+ * See Their respective documentation to understand {@code AnnotationResolver} implementation. + *

* @author Pierre Thirouin + * @see org.seedstack.business.api.interfaces.assembler.MatchingEntityId + * @see org.seedstack.business.api.interfaces.assembler.MatchingFactoryParameter */ public class AnnotationResolver implements DtoInfoResolver { - @Override - public ParameterHolder resolveId(Object dto) { - return resolveParameterFromDto(dto, MatchingEntityId.class); - } + public static final String MATCHING_FACT_PARAM = MatchingFactoryParameter.class.getSimpleName(); + public static final String MATCHING_ENTITY_ID = MatchingEntityId.class.getSimpleName(); @Override - public ParameterHolder resolveAggregate(Object dto) { - return resolveParameterFromDto(dto, MatchingFactoryParameter.class); - } - - private ParameterHolder resolveParameterFromDto(Object dto, Class annotationClass) { + public ParameterHolder resolveId(Object dto) { ParameterHolder parameterHolder = new ParameterHolderInternal(); - // filter methods annotated with the annotationClass Method[] methods = dto.getClass().getMethods(); for (Method method : methods) { - Annotation annotation = method.getAnnotation(annotationClass); + MatchingEntityId annotation = method.getAnnotation(MatchingEntityId.class); if (annotation != null) { - int fieldIndex = fieldIndex(annotation); - int typeIndex = typeIndex(annotation); - - if (fieldIndex == -1) { - // TODO : this case might be handle later - if (parameterHolder.first() != null) { - throw new IllegalArgumentException("Missing index on @MatchingEntityId annotation for " + method.getName()); + if (annotation.index() == -1) { + // Only if the id is not a value object ! If the constructor has one parameter set the index to 0. + // TODO test and document the behavior described above + if (parameterHolder.uniqueElement() != null) { + String message = method.toString() + " - There is already a method annotated with @" + MATCHING_ENTITY_ID + + " don't forget to specify the index to indicate the matching parameter: @" + MATCHING_ENTITY_ID + "(index = 0)"; + throw new IllegalArgumentException(message); } - parameterHolder.put(method.getName(), 0, getAttributeFromMethod(dto, method)); + parameterHolder.put(method.getName(), annotation.typeIndex(), -1, getAttributeFromMethod(dto, method)); // The index set to -1 as it is another use case + } else { - if (typeIndex == -1) { - parameterHolder.put(method.getName(), fieldIndex, getAttributeFromMethod(dto, method)); - } else { - parameterHolder.put(method.getName(), fieldIndex, typeIndex, getAttributeFromMethod(dto, method)); - } + parameterHolder.put(method.getName(), annotation.typeIndex(), annotation.index(), getAttributeFromMethod(dto, method)); } } } - // No @MatchingEntityId found + // No annotation found if (parameterHolder.isEmpty()) { - throw new IllegalArgumentException("Missing @MatchingEntityId annotation on " + dto.getClass().getSimpleName() + "'s id."); + String message = String.format("Missing %s annotation on %s's id.", MATCHING_ENTITY_ID, dto.getClass().getSimpleName()); + throw new IllegalArgumentException(message); } return parameterHolder; } - // ------ - // These two methods are needed because of the lack of interface for annotations. - // This is the contract that annotations used by the resolveParameterFromDto() method should follow. + @Override + public ParameterHolder resolveAggregate(Object dto) { + ParameterHolder parameterHolder = new ParameterHolderInternal(); - private int fieldIndex(Annotation anno) { - return Reflection.method("index").withReturnType(int.class).withParameterTypes().in(anno).invoke(); - } + Method[] methods = dto.getClass().getMethods(); + for (Method method : methods) { + MatchingFactoryParameter annotation = method.getAnnotation(MatchingFactoryParameter.class); + if (annotation != null) { + if (annotation.index() == -1) { + // If there is only one parameter in the factory method you can avoid to set the index + if (parameterHolder.uniqueElement() != null) { + String message = method.getName() + " - There is already a method annotated with @" + MATCHING_FACT_PARAM + + " don't forget to specify the index to indicate the matching parameter: @" + MATCHING_FACT_PARAM + "(index = 0)"; + throw new IllegalArgumentException(message); + } + parameterHolder.put(method.toString(), 0, getAttributeFromMethod(dto, method)); // The index set to 0 - private int typeIndex(Annotation anno) { - return Reflection.method("typeIndex").withReturnType(int.class).withParameterTypes().in(anno).invoke(); - } + } else { + parameterHolder.put(method.toString(), annotation.typeIndex(), annotation.index(), getAttributeFromMethod(dto, method)); + } + } + } - // ------ + // No annotation found + if (parameterHolder.isEmpty()) { + String message = String.format("Missing %s annotation on %s's id.", MATCHING_FACT_PARAM, dto.getClass().getSimpleName()); + throw new IllegalArgumentException(message); + } + + return parameterHolder; + } private Object getAttributeFromMethod(Object dto, Method method) { try { diff --git a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/resolver/ParameterHolderInternal.java b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/resolver/ParameterHolderInternal.java index cc64b49d..3b97b565 100644 --- a/core/src/main/java/org/seedstack/business/core/interfaces/assembler/resolver/ParameterHolderInternal.java +++ b/core/src/main/java/org/seedstack/business/core/interfaces/assembler/resolver/ParameterHolderInternal.java @@ -10,80 +10,78 @@ package org.seedstack.business.core.interfaces.assembler.resolver; import org.seedstack.business.api.interfaces.assembler.resolver.ParameterHolder; -import org.seedstack.business.api.Tuples; + +import java.util.HashMap; +import java.util.Map; /** * @author Pierre Thirouin */ public class ParameterHolderInternal implements ParameterHolder { - private Object[] parameters = new Object[2]; + private final Map> parametersByAggregate = new HashMap>(); @Override public void put(String source, int index, Object value) { - if (parameters[index] != null) { - throw new IllegalArgumentException(source + ": the index " + index + " is already used. You might have forgotten to specify the tupleIndex"); - } - parameters[index] = value; + put(source, -1, index, value); } @Override - public void put(String source, int index, int tupleIndex, Object value) { - TupleHolder tupleHolder; - Object o = parameters[index]; - if (o == null) { - tupleHolder = new TupleHolder(); - } else { - if (o instanceof TupleHolder) { - tupleHolder = (TupleHolder) o; - } else { - throw new IllegalArgumentException(source + ": the index " + index + " is already used. You might have forgotten to specify the tupleIndex"); - } + public void put(String sourceMethod, int aggregateIndex, int index, Object value) { + Map parameters = parametersByAggregate.get(aggregateIndex); + + if (parameters == null) { + parameters = new HashMap(); } - tupleHolder.put(source, tupleIndex, value); - parameters[index] = tupleHolder; + if (parameters.get(index) != null) { + String message = String.format("%s - the parameter at the index %d is already specified", sourceMethod, index); + if (aggregateIndex > -1) { + message += " for the aggregate root " + aggregateIndex; + } + throw new IllegalArgumentException(message); + } + parameters.put(index, value); + parametersByAggregate.put(aggregateIndex, parameters); } - class TupleHolder { - - private Object[] attributes = new Object[2]; + @Override + public Object[] parameters() { + return parametersOfAggregateRoot(-1); + } - public void put(String source, int tupleIndex, Object value) { - if (attributes[tupleIndex] != null) { - throw new IllegalArgumentException(source + ": the tuple index " + tupleIndex + " is already used."); - } - attributes[tupleIndex] = value; + @Override + public Object[] parametersOfAggregateRoot(int aggregateIndex) { + if (parametersByAggregate.get(aggregateIndex) != null) { + return parametersByAggregate.get(aggregateIndex).values().toArray(); + } else { + return new Object[0]; } + } - public Object[] attributes() { - return attributes; + @Override + public Object uniqueElement() { + if (parametersByAggregate.get(-1) != null) { + return parametersByAggregate.get(-1).get(-1); } + return null; } @Override - public Object[] parameters() { - return transformTupleHoldersToTuples(parameters); + public Object uniqueElementForAggregateRoot(int aggregateIndex) { + if (parametersByAggregate.get(aggregateIndex) != null) { + return parametersByAggregate.get(aggregateIndex).get(-1); + } + return null; } @Override - public Object first() { - return parameters[0]; + public boolean isEmptyForAggregateRoot(int aggregateIndex) { + return parametersByAggregate.isEmpty() || parametersByAggregate.get(aggregateIndex) == null || parametersByAggregate.get(aggregateIndex).isEmpty(); } @Override public boolean isEmpty() { - return parameters == null || parameters.length == 0; - } - - private Object[] transformTupleHoldersToTuples(Object[] parameters) { - for (int i = 0; i < parameters.length; i++) { - Object parameter = parameters[i]; - if (parameter instanceof TupleHolder) { - TupleHolder tupleHolder = (TupleHolder) parameter; - parameters[i] = Tuples.create(tupleHolder.attributes); - } - } - return parameters; + return isEmptyForAggregateRoot(-1) && isEmptyForAggregateRoot(0); } } diff --git a/core/src/main/java/org/seedstack/business/internal/event/EventModule.java b/core/src/main/java/org/seedstack/business/internal/event/EventModule.java index f384e1cf..07f8e974 100644 --- a/core/src/main/java/org/seedstack/business/internal/event/EventModule.java +++ b/core/src/main/java/org/seedstack/business/internal/event/EventModule.java @@ -65,7 +65,7 @@ class EventModule extends AbstractModule { protected void configure() { for (Class eventHandlerClass : eventHandlerClasses) { bind(eventHandlerClass); - LOGGER.info("binding {} in scope {}", eventHandlerClass, Scopes.NO_SCOPE); + LOGGER.debug("binding {} in scope {}", eventHandlerClass, Scopes.NO_SCOPE); } bind(EVENT_HANDLER_MAP_TYPE_LITERAL).toInstance(eventHandlersByEvent); bind(EventService.class).to(EventServiceInternal.class).in(Scopes.SINGLETON); diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/AssembleTest.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/AssembleTest.java index fb30a140..d7bdd9a5 100644 --- a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/AssembleTest.java +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/AssembleTest.java @@ -49,8 +49,7 @@ public void testDsl() { order = fluently.assemble().securely().dto(orderDto).to(Order.class).fromFactory(); // list of dto to tuple of aggregates - Pair aggregateClass = Tuples.create(Order.class, Customer.class); - Pair orderCustomerPair = fluently.assemble().dto(orderDto).to(aggregateClass).fromFactory(); + Pair orderCustomerPair = fluently.assemble().dto(orderDto).>to(Order.class, Customer.class).fromFactory(); // list of dtos to list of aggregates orders = fluently.assemble().dtos(dtos).to(orders); diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/Customer.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/Customer.java index db418a62..46b4578d 100644 --- a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/Customer.java +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/Customer.java @@ -20,18 +20,15 @@ public class Customer extends BaseAggregateRoot { String name; + public Customer(String id) { + this.id = id; + } + @Override public String getEntityId() { return id; } - public Customer() { - } - - public Customer(String name) { - this.name = name; - } - public String getName() { return name; } diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/CustomerRepository.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/CustomerRepository.java new file mode 100644 index 00000000..083197fc --- /dev/null +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/CustomerRepository.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.business.core.interfaces.assembler.dsl.fixture; + +import org.seedstack.business.api.domain.GenericRepository; + +/** + * @author Pierre Thirouin + */ +public interface CustomerRepository extends GenericRepository { +} diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/CustomerRepositoryInternal.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/CustomerRepositoryInternal.java new file mode 100644 index 00000000..4765abe3 --- /dev/null +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/CustomerRepositoryInternal.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.business.core.interfaces.assembler.dsl.fixture; + +import org.assertj.core.util.Maps; +import org.seedstack.business.core.domain.base.BaseRepository; + +import java.util.Map; + +/** + * @author Pierre Thirouin + */ +public class CustomerRepositoryInternal extends BaseRepository implements CustomerRepository { + + private static Map orderMap = Maps.newConcurrentHashMap(); + + @Override + protected Customer doLoad(String id) { + return orderMap.get(id); + } + + @Override + protected void doDelete(String id) { + orderMap.remove(id); + } + + @Override + protected void doDelete(Customer order) { + for (Customer order1 : orderMap.values()) { + if (order1.equals(order)) { + orderMap.remove(order.getEntityId()); + } + } + } + + @Override + protected void doPersist(Customer order) { + orderMap.put(order.getEntityId(), order); + } + + @Override + protected Customer doSave(Customer order) { + return orderMap.put(order.getEntityId(), order); + } + + public void clear() { + orderMap.clear(); + } +} diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/Recipe.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/Recipe.java new file mode 100644 index 00000000..3c65d531 --- /dev/null +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/dsl/fixture/Recipe.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.business.core.interfaces.assembler.dsl.fixture; + +import org.seedstack.business.api.interfaces.assembler.DtoOf; +import org.seedstack.business.api.interfaces.assembler.MatchingEntityId; +import org.seedstack.business.api.interfaces.assembler.MatchingFactoryParameter; + +/** + * The recipe assembled based on an order and the customer who passed the order. + * + * @author Pierre Thirouin + */ +// As no assembler implementation is provided the following annotation is required +@DtoOf({Order.class, Customer.class}) +public class Recipe { + + private String customerId; + + private String customerName; + + private String product; + + private String orderId; + + public Recipe(String customerId, String customerName, String orderId, String product) { + this.customerId = customerId; + this.customerName = customerName; + this.product = product; + this.orderId = orderId; + } + + // The order of the typeIndex depends on the position in @DtoOf + @MatchingEntityId(typeIndex = 0) // Don't specify the index as it is not a value object id + @MatchingFactoryParameter(typeIndex = 0, index = 0) + public String getOrderId() { + return orderId; + } + + @MatchingFactoryParameter(typeIndex = 0, index = 1) + public String getProduct() { + return product; + } + + @MatchingEntityId(typeIndex = 1) + @MatchingFactoryParameter(typeIndex = 1, index = 0) + public String getCustomerId() { + return customerId; + } + + // No annotation require here as the customer name is not part of + // the customer id or factory method + public String getCustomerName() { + return customerName; + } +} diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/AnnotationResolverTest.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/AnnotationResolverTest.java index 679072a2..d9a68947 100644 --- a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/AnnotationResolverTest.java +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/AnnotationResolverTest.java @@ -10,8 +10,6 @@ package org.seedstack.business.core.interfaces.assembler.resolver; import org.assertj.core.api.Assertions; -import org.assertj.core.api.SoftAssertions; -import org.javatuples.Pair; import org.junit.Before; import org.junit.Test; import org.seedstack.business.api.interfaces.assembler.resolver.DtoInfoResolver; @@ -35,10 +33,6 @@ public class AnnotationResolverTest { public static final String lastName = "doe"; private DtoInfoResolver underTest; - private Case1Dto case1Dto; - private Case2Dto case2Dto; - private Case3Dto case3Dto; - private Case4Dto case4Dto; private CaseFail1Dto caseFail1Dto; private CaseFail2Dto caseFail2Dto; private CaseFail3Dto caseFail3Dto; @@ -47,42 +41,45 @@ public class AnnotationResolverTest { public void setup() { underTest = new AnnotationResolver(); - case1Dto = new Case1Dto(1, firstName); - case2Dto = new Case2Dto(firstName, birthDate); - case3Dto = new Case3Dto(firstName, lastName); - case4Dto = new Case4Dto(firstName, lastName); caseFail1Dto = new CaseFail1Dto(firstName, lastName); caseFail2Dto = new CaseFail2Dto(firstName, lastName); caseFail3Dto = new CaseFail3Dto(firstName, lastName); } @Test - public void testResolveId() { - SoftAssertions softAssertions = new SoftAssertions(); - - ParameterHolder id1 = underTest.resolveId(case1Dto); - softAssertions.assertThat(id1).as("ResolveId() - Cas 1: the id correspond to one field").isNotNull(); - softAssertions.assertThat(id1.first()).as("Cas 1:").isEqualTo(1); + public void testSimpleId() { + ParameterHolder holder = underTest.resolveId(new Case1Dto(1, firstName)); + Assertions.assertThat(holder).isNotNull(); + Assertions.assertThat(holder.uniqueElement()).isEqualTo(1); + } - // TODO: case 2 is useless - delete it or make it work without specifying the index + @Test + public void testValueObjectId() { + ParameterHolder holder = underTest.resolveId(new Case2Dto(firstName, birthDate)); + Assertions.assertThat(holder).isNotNull(); + Assertions.assertThat(holder.parameters()).hasSize(2); + Assertions.assertThat(holder.parameters()).isEqualTo(new Object[]{firstName, birthDate}); + } - ParameterHolder id2 = underTest.resolveId(case2Dto); - softAssertions.assertThat(id2).as("ResolveId() - Cas 2: the id is composite").isNotNull(); - softAssertions.assertThat(id2.parameters()).as("Cas 2").isEqualTo(new Object[]{firstName, birthDate}); + @Test + public void testSimpleIdForTuples() { + ParameterHolder holder = underTest.resolveId(new Case3Dto(firstName, lastName)); + Assertions.assertThat(holder).isNotNull(); + Assertions.assertThat(holder.parameters()).isEmpty(); + Assertions.assertThat(holder.parametersOfAggregateRoot(0)).isEqualTo(new Object[]{firstName}); + Assertions.assertThat(holder.parametersOfAggregateRoot(1)).isEqualTo(new Object[]{lastName}); + } - ParameterHolder id3 = underTest.resolveId(case3Dto); - softAssertions.assertThat(id3).as("ResolveId() - Cas 3: the id is composite and have elements of same type").isNotNull(); - softAssertions.assertThat(id3.parameters()).as("Cas 3").isEqualTo(new Object[]{firstName, lastName}); + @Test + public void testValueObjectIdForTuples() { + ParameterHolder holder = underTest.resolveId(new Case4Dto(firstName, lastName, "oderItem", "description")); + Assertions.assertThat(holder).isNotNull(); - ParameterHolder id4 = underTest.resolveId(case4Dto); - softAssertions.assertThat(id4).as("ResolveId() - Cas 4: the id is composite and contains a value object").isNotNull(); - softAssertions.assertThat(id4.parameters()[0]).as("Cas 4").isNotNull(); - //noinspection unchecked - Pair name = (Pair) id4.first(); - softAssertions.assertThat(name.getValue0()).as("Cas 4").isEqualTo(firstName); - softAssertions.assertThat(name.getValue1()).as("Cas 4").isEqualTo(lastName); + Assertions.assertThat(holder.parametersOfAggregateRoot(0)).hasSize(2); + Assertions.assertThat(holder.parametersOfAggregateRoot(0)).isEqualTo(new Object[]{firstName, lastName}); - softAssertions.assertAll(); + Assertions.assertThat(holder.parametersOfAggregateRoot(1)).hasSize(2); + Assertions.assertThat(holder.parametersOfAggregateRoot(1)).isEqualTo(new Object[]{"oderItem", "description"}); } @Test diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/ParameterHolderTest.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/ParameterHolderTest.java new file mode 100644 index 00000000..d8bf5fc8 --- /dev/null +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/ParameterHolderTest.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.business.core.interfaces.assembler.resolver; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.seedstack.business.api.interfaces.assembler.resolver.ParameterHolder; + +/** + * Test {@link org.seedstack.business.api.interfaces.assembler.resolver.ParameterHolder} implementation. + * + * @author Pierre Thirouin + */ +public class ParameterHolderTest { + + private static final String SOURCE = "MyDto.getSomeParam()"; + private static final int param1 = 1; + private static final int param2 = 2; + private static final int param3 = 3; + + @Test + public void testPutParameter() { + ParameterHolder parameterHolder = new ParameterHolderInternal(); + + parameterHolder.put(SOURCE, 1, param2); + parameterHolder.put(SOURCE, 0, param1); + parameterHolder.put(SOURCE, 2, param3); + + Assertions.assertThat(parameterHolder.isEmpty()).isFalse(); + + Assertions.assertThat(parameterHolder.uniqueElement()).isNull(); + + Assertions.assertThat(parameterHolder.parameters()).isNotEmpty(); + Assertions.assertThat(parameterHolder.parameters()).isEqualTo(new Object[]{1,2,3}); + Assertions.assertThat(parameterHolder.parameters()).isEqualTo(parameterHolder.parametersOfAggregateRoot(-1)); + } + @Test + public void testUniqueElement() { + ParameterHolder parameterHolder = new ParameterHolderInternal(); + + parameterHolder.put(SOURCE, -1, param1); + + Assertions.assertThat(parameterHolder.uniqueElement()).isEqualTo(1); + } + + @Test(expected = IllegalArgumentException.class) + public void testPutParameterFail() { + ParameterHolder parameterHolder = new ParameterHolderInternal(); + + parameterHolder.put(SOURCE, 0, param1); + parameterHolder.put(SOURCE, 0, param2); + } + + @Test + public void testPutParameterWithMultipleAggregate() { + ParameterHolder parameterHolder = new ParameterHolderInternal(); + + parameterHolder.put(SOURCE, 0, 0, param1); + parameterHolder.put(SOURCE, 0, 1, param2); + parameterHolder.put(SOURCE, 1, 0, param3); + + Assertions.assertThat(parameterHolder.isEmptyForAggregateRoot(0)).isFalse(); + Assertions.assertThat(parameterHolder.isEmptyForAggregateRoot(1)).isFalse(); + Assertions.assertThat(parameterHolder.isEmpty()).isFalse(); + + Assertions.assertThat(parameterHolder.parameters()).isEmpty(); + Assertions.assertThat(parameterHolder.parametersOfAggregateRoot(0)).isEqualTo(new Object[]{1, 2}); + Assertions.assertThat(parameterHolder.parametersOfAggregateRoot(1)).isEqualTo(new Object[]{3}); + } + + @Test(expected = IllegalArgumentException.class) + public void testPutParameterFailWithMultipleAggregate() { + ParameterHolder parameterHolder = new ParameterHolderInternal(); + + parameterHolder.put(SOURCE, 2, 1, param1); + parameterHolder.put(SOURCE, 2, 1, param2); + } +} diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case2Dto.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case2Dto.java index 0a85208d..4e17758f 100644 --- a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case2Dto.java +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case2Dto.java @@ -36,4 +36,8 @@ public String getName() { public Date getBirthDate() { return birthDate; } + + public String otherDetails() { + return "something"; + } } diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case3Dto.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case3Dto.java index 1dbdd241..0fe1f27e 100644 --- a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case3Dto.java +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case3Dto.java @@ -16,23 +16,23 @@ */ public class Case3Dto { - String firstName; + String customerName; - String lastName; + String orderItem; - public Case3Dto(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; + public Case3Dto(String customerName, String orderItem) { + this.customerName = customerName; + this.orderItem = orderItem; } - @MatchingEntityId(index = 0) - public String getFirstName() { - return firstName; + @MatchingEntityId(typeIndex = 0) + public String getCustomerName() { + return customerName; } - @MatchingEntityId(index = 1) - public String getLastName() { - return lastName; + @MatchingEntityId(typeIndex = 1) + public String getOrderItem() { + return orderItem; } } diff --git a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case4Dto.java b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case4Dto.java index 3aa7b5ee..6bd4936f 100644 --- a/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case4Dto.java +++ b/core/src/test/java/org/seedstack/business/core/interfaces/assembler/resolver/sample/Case4Dto.java @@ -12,9 +12,9 @@ import org.seedstack.business.api.interfaces.assembler.MatchingEntityId; /** - * Case 4: The first name and last name are mapped to a {@literal Pair} in the constructor. + * Case 4: The first name and last name are mapped to a {@code Pair<String, String>} in the constructor. * - * @author Pierre Thirouin + * @author pierre.thirouin@ext.mpsa.com */ public class Case4Dto { @@ -22,9 +22,15 @@ public class Case4Dto { String lastName; - public Case4Dto(String firstName, String lastName) { + String orderItem; + + String orderDescription; + + public Case4Dto(String firstName, String lastName, String orderItem, String orderDescription) { this.firstName = firstName; this.lastName = lastName; + this.orderItem = orderItem; + this.orderDescription = orderDescription; } @MatchingEntityId(index = 0, typeIndex = 0) @@ -32,8 +38,18 @@ public String getFirstName() { return firstName; } - @MatchingEntityId(index = 0, typeIndex = 1) + @MatchingEntityId(index = 1, typeIndex = 0) public String getLastName() { return lastName; } + + @MatchingEntityId(index = 0, typeIndex = 1) + public String getOrderItem() { + return orderItem; + } + + @MatchingEntityId(index = 1, typeIndex = 1) + public String getOrderDescription() { + return orderDescription; + } } diff --git a/specs/src/main/java/org/seedstack/business/api/Tuples.java b/specs/src/main/java/org/seedstack/business/api/Tuples.java index e671b68e..77ff4b0a 100644 --- a/specs/src/main/java/org/seedstack/business/api/Tuples.java +++ b/specs/src/main/java/org/seedstack/business/api/Tuples.java @@ -16,6 +16,7 @@ import org.seedstack.seed.core.utils.SeedCheckUtils; import java.lang.reflect.ParameterizedType; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -30,18 +31,24 @@ private Tuples() { } /** - * Transforms an array of object into a tuple. Does not work array of more than ten element. + * Transforms a list of object into a tuple. Does not work array of more than ten element. * - * @param objects the array of object + * @param objects the list of object * @param the tuple type * @return a tuple * @throws org.seedstack.seed.core.api.SeedException if the array length is greater than 10 - * @deprecated Use the shorter {@code create} method instead. */ - @Deprecated - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static TUPLE createTupleFromList(Object... objects) { - return (TUPLE) create(objects); + @SuppressWarnings("unchecked") + public static TUPLE create(List objects) { + SeedCheckUtils.checkIf(objects.size() <= 10, "Can't create a Tuple of more than ten element."); + + Class tupleClass = classOfTuple(objects.toArray(new Object[objects.size()])); + + return (TUPLE) Reflection + .staticMethod("fromCollection") + .withReturnType(tupleClass) + .withParameterTypes(Collection.class) + .in(tupleClass).invoke(objects); } /** @@ -53,23 +60,25 @@ public static TUPLE createTupleFromList(Object... objects * @throws org.seedstack.seed.core.api.SeedException if the array length is greater than 10 */ @SuppressWarnings({ "unchecked", "rawtypes" }) - public static TUPLE create(Object... objects) { - return (TUPLE) createTupleFromList((List) Lists.newArrayList(objects)); + public static TUPLE create(Object firstObject, Object... objects) { + List list = Lists.newArrayList(firstObject); + list.addAll(Arrays.asList(objects)); + return (TUPLE) create(list); } /** - * Transforms a list of object into a tuple. Does not work array of more than ten element. + * Transforms an array of object into a tuple. Does not work array of more than ten element. * - * @param objects the list of object + * @param objects the array of object * @param the tuple type * @return a tuple * @throws org.seedstack.seed.core.api.SeedException if the array length is greater than 10 * @deprecated Use the shorter {@code create} method instead. */ @Deprecated - @SuppressWarnings("unchecked") - public static TUPLE createTupleFromList(List objects) { - return create(objects); + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static TUPLE createTupleFromList(Object... objects) { + return (TUPLE) create(objects); } /** @@ -79,18 +88,16 @@ public static TUPLE createTupleFromList(List obje * @param the tuple type * @return a tuple * @throws org.seedstack.seed.core.api.SeedException if the array length is greater than 10 + * @deprecated Use the shorter {@code create} method instead. */ + @Deprecated @SuppressWarnings("unchecked") - public static TUPLE create(List objects) { - SeedCheckUtils.checkIf(objects.size() <= 10, "Can't create a Tuple of more than ten element."); - - Class tupleClass = classOfTuple(objects.toArray(new Object[objects.size()])); + public static TUPLE createTupleFromList(List objects) { + return create(objects); + } - return (TUPLE) Reflection - .staticMethod("fromCollection") - .withReturnType(tupleClass) - .withParameterTypes(Collection.class) - .in(tupleClass).invoke(objects); + public static Class classOfTuple(List objects) { + return classOfTuple(objects.toArray()); } /** diff --git a/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/MatchingEntityId.java b/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/MatchingEntityId.java index 8516104e..0a9ae426 100644 --- a/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/MatchingEntityId.java +++ b/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/MatchingEntityId.java @@ -16,18 +16,85 @@ /** - * This annotation is a marker allowing {@linkplain Assemblers} to retrieve the right AggregateRoot - * when retrieving from the persistence. This describes the entityId. - *

- * When defining DTO, developers can annotate methods that match with entityId with {@link MatchingEntityId}. - *

- * Doing this will allow {@link Assemblers} to automatically retrieve the Entity by the representation of entityId. - *

- * This annotation serves as gateway between Aggregate Root and DTO. - * - * for doing the match between representation/dto methods - * to id ValueObject constructor parameters. - * + * This annotation allows the use of the {@code fromRepository()} method of the assembler DSL. If you don't use + * this DSL feature, this annotated is unnecessary. + *

+ * It binds the DTO annotated method to the aggregate root id. If the id is a value object, it binds the method + * to one of the constructor parameters. This allows the assembler DSL to find (or create) the aggregate root id + * in order to load the aggregate root from the repository. + *

+ *

+ * It also handle the case of a DTO assembled from a tuple of aggregate roots. + *

+ * Case 1: Basic use case. + *
+ * public class OrderDto {
+ *
+ *     {@literal @}MatchingEntityId
+ *     public int getId() {...}
+ * }
+ *
+ * public class OrderAssembler extends BaseTupleAssembler<Order, OrderDto> { ... }
+ *
+ * public class Order {
+ *
+ *     private int orderId;
+ *
+ *     public Integer getEntityId() {
+ *         return orderId;
+ *     }
+ * }
+ * 
+ * Case 1: Basic use case. + *
+ * public class CustomerDto {
+ *
+ *     {@literal @}MatchingEntityId(index = 0)
+ *     public String getFirstName() {...}
+ *
+ *     {@literal @}MatchingEntityId(index = 1)
+ *     public String getLastName() {...}
+ *
+ *     // No need for annotation here as the birth date is not part of the customer id
+ *     public Date getBirthDate() {...}
+ * }
+ *
+ * public class RecipeAssembler extends BaseAssembler<Customer, CustomerDto> { ... }
+ *
+ * public class CustomerId {
+ *     public CustomerId(String firstName, String lastName) {...}
+ * }
+ * 
+ * Case 2: The DTO is an assembly of multiple aggregates. + *
+ * public class RecipeDto {
+ *
+ *     {@literal @}MatchingEntityId(index = 0, typeIndex = 0)
+ *     public String getCustomerFirstName() {...}
+ *
+ *     {@literal @}MatchingEntityId(index = 1, typeIndex = 0)
+ *     public String getCustomerLastName() {...}
+ *
+ *     {@literal @}MatchingEntityId(typeIndex = 1) // no need for index as OrderId is not a value object
+ *     public int getOrderId() {...}
+ * }
+ *
+ * public class RecipeAssembler extends BaseTupleAssembler<Pair<Customer, Order>, RecipeDto> { ... }
+ *
+ * public class CustomerId {
+ *     public CustomerId(String firstName, String lastName) {...}
+ * }
+ *
+ * public class Order {
+ *
+ *     private int orderId;
+ *
+ *     public Integer getEntityId() {
+ *         return orderId;
+ *     }
+ * }
+ * 
+ * * @author epo.jemba@ext.mpsa.com * */ @@ -36,28 +103,21 @@ public @interface MatchingEntityId { /** - * The index of the current matching field. Inside a single type. + * If the aggregate root id is composite, i.e it is a value object, this method indicates + * constructor parameter of the value object associated to the annotated method. + * + * @return the parameter index in the id constructor. */ int index() default -1; /** - * In case of Tuple of Aggregate Roots, this field indicate the index number of the associated Root inside the Tuple. - *

- * For instance, in case of - *

-     * public class UseCase1Assembler extends BaseTupleAssembler, UseCase1Representation> {
-     *    ...
-     * }
-     * 
- * A field inside a DTO that matches Customer will look like: - *
-     * {@literal @}MatchingEntityId(typeIndex=1)
-     * public String getCustomer() {
-     *     return customer;
-     * }
-     * 
+ * When using a tuple assembler, i.e. when assembling a DTO to tuple of aggregate roots. + * This index indicates to which aggregate root this id belongs. * - * @return the index of the type inside Tuple starting by 0. + * @return the aggregate index + * + * @see BaseTupleAssembler */ int typeIndex() default -1; + } diff --git a/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/MatchingFactoryParameter.java b/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/MatchingFactoryParameter.java index 27955e87..20a2b792 100644 --- a/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/MatchingFactoryParameter.java +++ b/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/MatchingFactoryParameter.java @@ -16,8 +16,59 @@ /** - * This annotation serves for doing the match between representation/dto methods - * to factory create method parameters. + * This annotation allows the use of the {@code fromFactory()} method of the assembler DSL. If you don't use + * this DSL feature, this annotated is unnecessary. + *

+ * It binds the DTO's annotated method to one parameter of a factory method used to create the + * assembled aggregate. + *

+ *

+ * It also handle the case of a DTO assembled from a tuple of aggregate roots. + *

+ * Case 1: Basic use case. + *
+ * public class CustomerDto {
+ *
+ *     {@literal @}MatchingFactoryParameter(index = 0)
+ *     public String getName() {...}
+ *
+ *     {@literal @}MatchingFactoryParameter(index = 1)
+ *     public Date getBirthDate() {...}
+ *
+ *     // No need for annotation here as the address is not part of the factory method
+ *     public Address getAddress() {...}
+ * }
+ *
+ * public class RecipeAssembler extends BaseAssembler<Customer, CustomerDto> { ... }
+ *
+ * public class CustomerFactory {
+ *     public Customer createCustomer(String name, Date birthDate);
+ * }
+ * 
+ * Case 2: The DTO is an assembly of multiple aggregates. + *
+ * public class RecipeDto {
+ *
+ *     {@literal @}MatchingFactoryParameter(index = 0, typeIndex = 0)
+ *     public String getCustomerName() {...}
+ *
+ *     {@literal @}MatchingFactoryParameter(index = 1, typeIndex = 0)
+ *     public Date getCustomerBirthDate() {...}
+ *
+ *     {@literal @}MatchingFactoryParameter(index = 0, typeIndex = 1)
+ *     public int getOrderId() {...}
+ * }
+ *
+ * public class RecipeAssembler extends BaseTupleAssembler<Pair<Customer, Order>, RecipeDto> { ... }
+ *
+ * public class CustomerFactory {
+ *     Customer createCustomer(String name, Date birthDate);
+ * }
+ *
+ * public class OrderFactory {
+ *     Customer createOrder(int orderId);
+ * }
+ * 
* * @author epo.jemba@ext.mpsa.com * @@ -27,16 +78,19 @@ public @interface MatchingFactoryParameter { /** + * Indicates which factory parameter the annotated method match. + * * @return the parameter index in the factory method. */ int index() default -1; /** - * When using a BaseTupleAssembler, this type index is used to indicate to in + * When using a tuple assembler, i.e. when assembling a DTO to tuple of aggregate roots. + * This index indicates for which aggregate root this factory parameter is used. * - * @return the entity index in the tuple. + * @return the aggregate index * * @see BaseTupleAssembler */ - int typeIndex () default -1; + int typeIndex() default -1; } diff --git a/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/dsl/BaseAggAssemblerProvider.java b/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/dsl/BaseAggAssemblerProvider.java index e7a44ea0..531f736f 100644 --- a/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/dsl/BaseAggAssemblerProvider.java +++ b/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/dsl/BaseAggAssemblerProvider.java @@ -14,11 +14,32 @@ import org.seedstack.business.api.domain.AggregateRoot; /** -* @author Pierre Thirouin -*/ + * Provides methods to specify the aggregate class to which aggregate root (or tuple of aggregate root) assemble. + * + * @author Pierre Thirouin + */ public interface BaseAggAssemblerProvider { + /** + * Assembles to an aggregate root. + * + * @param aggregateRootClass the aggregate root class + * @param the type of aggregate root + * @return the assembler DSL to assemble an aggregate root + */ > AggAssemblerWithRepoProvider to(Class aggregateRootClass); - TupleAggAssemblerWithRepoProvider to(T aggregateRootTuple); + /** + * Assembles to a tuple of aggregate roots. + *

+ * The parameters are cut in three in order to not conflict with the non tuple method. + *

+ * + * @param firstAggregateClass the first aggregate root class of the tuple + * @param secondAggregateClass the second class + * @param otherAggregateClasses and the rest + * @param The tuple type + * @return the assembler DSL to assemble a tuple of aggregate roots + */ + TupleAggAssemblerWithRepoProvider to(Class> firstAggregateClass, Class> secondAggregateClass, Class>... otherAggregateClasses); } diff --git a/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/resolver/ParameterHolder.java b/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/resolver/ParameterHolder.java index a9347560..acf2975b 100644 --- a/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/resolver/ParameterHolder.java +++ b/specs/src/main/java/org/seedstack/business/api/interfaces/assembler/resolver/ParameterHolder.java @@ -13,6 +13,7 @@ * This class holds the parameter of a method. It is used to collect parameters and then match them to a method. * * @author Pierre Thirouin + * @see org.seedstack.business.api.interfaces.assembler.resolver.DtoInfoResolver */ public interface ParameterHolder { @@ -20,20 +21,20 @@ public interface ParameterHolder { * Adds a parameter value at its position in the expected method. * * @param source the source used to get the value. Used for debugging information. - * @param index the position in the method - * @param value the parameter value + * @param index the position in the method + * @param value the parameter value */ void put(String source, int index, Object value); /** - * Adds a parameter value in a tuple at its position in the expected method. + * Adds a parameter value for the specified aggregate root at its position in the expected method. * - * @param source the source used to get the value. Used for debugging information. - * @param index the position in the method - * @param tupleIndex the position in the tuple - * @param value the parameter value + * @param sourceMethod the source method used to get the value. Used for debugging information. + * @param aggregateIndex the index corresponding to the aggregate root position in the tuple of aggregate roots. + * @param index the position in the method + * @param value the parameter value */ - void put(String source, int index, int tupleIndex, Object value); + void put(String sourceMethod, int aggregateIndex, int index, Object value); /** * Gives the ordered list of parameters. @@ -43,16 +44,52 @@ public interface ParameterHolder { Object[] parameters(); /** - * Gives the ordered list of parameters. + * Gives the ordered list of parameters for specified index. + * + * @param aggregateIndex the index corresponding to the aggregate root position in the tuple of aggregate roots. + * @return the array of parameters + */ + Object[] parametersOfAggregateRoot(int aggregateIndex); + + /** + * Gives the first parameter of the first aggregate. + * + * @return array of parameters + */ + Object uniqueElement(); + + /** + * Gives the first parameter of the specified aggregate. * + * @param aggregateIndex the index corresponding to the aggregate root position in the tuple of aggregate roots. * @return array of parameters */ - Object first(); + Object uniqueElementForAggregateRoot(int aggregateIndex); /** - * Indicates whether the holder contains parameters or not. + * Indicates whether the holder contains parameters. If there + *

+ * This is equivalent to {@code isEmptyForAggregateRoot(-1) || isEmptyForAggregateRoot(0)}. + * If there is only one aggregate root, the index is -1, otherwise it starts at 0. + *

* * @return true if the holder doesn't contain parameters, false otherwise */ boolean isEmpty(); + + /** + * Indicates whether the holder contains parameters for the aggregate root at the + * aggregateIndex position in the tuple. + *

+ * It starts at the index 0, or -1 if there is only one aggregate defined. + *

+ *

+ * It checks if there is at least one aggregate defined and if the first aggregate + * as at least one parameter. + *

+ * + * @param aggregateIndex the index corresponding to the aggregate root position in the tuple of aggregate roots. + * @return true if the holder doesn't contain parameters, false otherwise + */ + boolean isEmptyForAggregateRoot(int aggregateIndex); } diff --git a/specs/src/main/resources/META-INF/errors/org.seedstack.business.api.domain.DomainErrorCodes.properties b/specs/src/main/resources/META-INF/errors/org.seedstack.business.api.domain.DomainErrorCodes.properties index 6faf565a..5bcb79bb 100644 --- a/specs/src/main/resources/META-INF/errors/org.seedstack.business.api.domain.DomainErrorCodes.properties +++ b/specs/src/main/resources/META-INF/errors/org.seedstack.business.api.domain.DomainErrorCodes.properties @@ -10,8 +10,8 @@ ENTITY_WITHOUT_IDENTITY_ISSUE.message=Missing identity for ${className} entity. AGGREGATE_ROOT_CREATION_ISSUE.message=A creation issue occurred for ${aggregateRoot}. -DOMAIN_OBJECT_CONSTRUCTOR_NOT_FOUND.message = DomainObject constructor not found for: ${domainObject}, with parameters: ${parameters} -DOMAIN_OBJECT_CONSTRUCTOR_NOT_FOUND.fix = Please check your DomainObject ${domainObject} for existing construction using parameters ${parameters} +DOMAIN_OBJECT_CONSTRUCTOR_NOT_FOUND.message = Constructor not found for: ${domainObject}, with parameters: ${parameters} +DOMAIN_OBJECT_CONSTRUCTOR_NOT_FOUND.fix = Please check ${domainObject} for existing constructor using parameters ${parameters} UNABLE_TO_INVOKE_CONSTRUCTOR.message = Unable to invoke constructor: ${constructor}, for DomainObject: ${domainObject}, using parameters ${parameters} AMBIGUOUS_CONSTRUCTOR_FOUND.message = Ambiguous constructor found for: ${constructor1}, ${constructor2} ,of object: ${object} AMBIGUOUS_CONSTRUCTOR_FOUND.fix = Please check for null parameters or ambiguous constructor with matching primitives and types for parameters: ${parameters} diff --git a/specs/src/test/java/org/seedstack/business/api/TuplesTest.java b/specs/src/test/java/org/seedstack/business/api/TuplesTest.java index 01cbe787..59dee021 100644 --- a/specs/src/test/java/org/seedstack/business/api/TuplesTest.java +++ b/specs/src/test/java/org/seedstack/business/api/TuplesTest.java @@ -9,6 +9,7 @@ */ package org.seedstack.business.api; +import com.google.common.collect.Lists; import com.google.inject.util.Types; import org.assertj.core.api.Assertions; import org.javatuples.Pair; @@ -17,6 +18,7 @@ import org.junit.Test; import java.lang.reflect.Type; +import java.util.List; /** * Tests the helper class {@link org.seedstack.business.api.Tuples}. @@ -26,10 +28,12 @@ public class TuplesTest { @Test - public void testCreateTupleFromList() { - Tuple tuple = Tuples.create(String.class, Long.class); + public void testCreateTupleFromVarArgs() { + Pair pair = Tuples.create(10, "foo"); Assertions.assertThat((Iterable) pair).isEqualTo(new Pair(10, "foo")); + + Tuple tuple = Tuples.create(String.class, Long.class); Assertions.assertThat((Iterable) tuple).isInstanceOf(Pair.class); Assertions.assertThat(tuple.containsAll(String.class, Long.class)).isTrue(); Assertions.assertThat(tuple.getSize()).isEqualTo(2); @@ -38,6 +42,16 @@ public void testCreateTupleFromList() { Assertions.assertThat((Iterable) tuple).isInstanceOf(Triplet.class); } + @Test + public void testCreateTupleFromList() { + List classes = Lists.newArrayList(String.class, Long.class); + Tuple tuple = Tuples.create(classes); + + Assertions.assertThat((Iterable) tuple).isInstanceOf(Pair.class); + Assertions.assertThat(tuple.containsAll(String.class, Long.class)).isTrue(); + Assertions.assertThat(tuple.getSize()).isEqualTo(2); + } + @Test public void testClassOfTuple() { Class type = Tuples.classOfTuple(String.class, Long.class);