├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ └── java │ │ └── com │ │ └── github │ │ └── leeonky │ │ └── jfactory │ │ ├── Persistable.java │ │ ├── Arguments.java │ │ ├── InvalidSpecException.java │ │ ├── DataRepository.java │ │ ├── ArgumentMapFactory.java │ │ ├── Global.java │ │ ├── Trait.java │ │ ├── PropertyAliases.java │ │ ├── ArgumentMap.java │ │ ├── ListPersistable.java │ │ ├── DefaultValueFactoryProducer.java │ │ ├── Transformer.java │ │ ├── TypeSequence.java │ │ ├── PropertyValue.java │ │ ├── PropertyAlias.java │ │ ├── UnFixedValueProducer.java │ │ ├── Factory.java │ │ ├── TypeReference.java │ │ ├── Instance.java │ │ ├── DefaultValueFactory.java │ │ ├── MemoryDataRepository.java │ │ ├── ValueCache.java │ │ ├── FixedValueProducer.java │ │ ├── LinkSpecCollection.java │ │ ├── SpecFactory.java │ │ ├── SubInstance.java │ │ ├── DefaultValueProducer.java │ │ ├── DependencyProducer.java │ │ ├── ReadOnlyProducer.java │ │ ├── CollectionInstance.java │ │ ├── SingleValueExpression.java │ │ ├── Dependency.java │ │ ├── Builder.java │ │ ├── LinkProducer.java │ │ ├── Expression.java │ │ ├── TraitsSpec.java │ │ ├── LinkSpec.java │ │ ├── DefaultArguments.java │ │ ├── BuilderValueProducer.java │ │ ├── CompositeRepository.java │ │ ├── SubObjectExpression.java │ │ ├── RootInstance.java │ │ ├── AliasSetStore.java │ │ ├── Link.java │ │ ├── Producer.java │ │ ├── SpecClassFactory.java │ │ ├── CollectionExpression.java │ │ ├── Spec.java │ │ ├── PropertyChain.java │ │ ├── KeyValueCollection.java │ │ ├── CollectionProducer.java │ │ ├── FactorySet.java │ │ ├── KeyValue.java │ │ ├── JFactory.java │ │ ├── ObjectFactory.java │ │ ├── PropertySpec.java │ │ ├── DefaultBuilder.java │ │ ├── ObjectProducer.java │ │ └── DefaultValueFactories.java └── test │ ├── resources │ └── features │ │ ├── test_case_design │ │ ├── 100-temporarily-ignored.feature │ │ └── 2-reverse-association.feature │ │ └── bug │ │ ├── dependency.feature │ │ └── link.feature │ └── java │ └── com │ └── github │ └── leeonky │ └── jfactory │ ├── cucumber │ ├── Run.java │ ├── Steps.java │ └── IntegrationTestContext.java │ ├── ExpressionTest.java │ ├── PropertySpecTest.java │ ├── JFactoryTest.java │ ├── bug │ ├── UseSubObjectSpec.java │ └── PropertyShouldOverridePropertyInSpec.java │ ├── CompositeRepositoryTest.java │ ├── DefaultBuilderTest.java │ ├── PropertyChainTest.java │ ├── DefaultValueFactoriesTest.java │ └── ProducerTest.java ├── pi-report.sh ├── .gitignore ├── .circleci └── config.yml ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeonky/jfactory/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Persistable.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | public interface Persistable { 4 | void save(Object object); 5 | } 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Arguments.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | public interface Arguments { 4 |

P param(String key); 5 | 6 |

P param(String key, P defaultValue); 7 | 8 | Arguments params(String property); 9 | } -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/InvalidSpecException.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | public class InvalidSpecException extends RuntimeException { 4 | public InvalidSpecException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/features/test_case_design/100-temporarily-ignored.feature: -------------------------------------------------------------------------------- 1 | Feature: Temporarily ignored for test case design 2 | 3 | Scenario: customize constructor - create bean use customer constructor 4 | 5 | Scenario: customized - define default value strategy by type 6 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/DataRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.Collection; 4 | 5 | public interface DataRepository extends Persistable { 6 | Collection queryAll(Class type); 7 | 8 | void clear(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/ArgumentMapFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | public class ArgumentMapFactory { 4 | public static ArgumentMap arg(String key, Object value) { 5 | ArgumentMap argumentMap = new ArgumentMap(); 6 | argumentMap.put(key, value); 7 | return argumentMap; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Global.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.TYPE; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Target(TYPE) 10 | @Retention(RUNTIME) 11 | public @interface Global { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Trait.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.METHOD) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Trait { 11 | String value() default ""; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/PropertyAliases.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.Target; 5 | 6 | import static java.lang.annotation.ElementType.TYPE; 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | @Target(TYPE) 10 | @Retention(RUNTIME) 11 | public @interface PropertyAliases { 12 | PropertyAlias[] value(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/ArgumentMap.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.LinkedHashMap; 4 | 5 | public class ArgumentMap extends LinkedHashMap { 6 | public ArgumentMap arg(String key, Object value) { 7 | ArgumentMap argumentMap = new ArgumentMap(); 8 | argumentMap.putAll(this); 9 | argumentMap.put(key, value); 10 | return argumentMap; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/ListPersistable.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | class ListPersistable implements Persistable { 7 | private List data = new ArrayList<>(); 8 | 9 | @Override 10 | public void save(Object object) { 11 | data.add(object); 12 | } 13 | 14 | public List getAll() { 15 | return data; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/DefaultValueFactoryProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | class DefaultValueFactoryProducer extends DefaultValueProducer { 6 | 7 | public DefaultValueFactoryProducer(BeanClass beanType, DefaultValueFactory factory, SubInstance instance) { 8 | super(BeanClass.create(factory.getType()), () -> factory.create(beanType, instance)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Transformer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | public interface Transformer { 4 | default boolean matches(String input) { 5 | return true; 6 | } 7 | 8 | Object transform(String input); 9 | 10 | default Object checkAndTransform(Object value) { 11 | if (value instanceof String && matches((String) value)) 12 | return transform((String) value); 13 | return value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/TypeSequence.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | class TypeSequence { 8 | private final Map, AtomicInteger> sequences = new HashMap<>(); 9 | 10 | public synchronized int generate(Class type) { 11 | return sequences.computeIfAbsent(type, k -> new AtomicInteger(0)).incrementAndGet(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/PropertyValue.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | public interface PropertyValue { 4 | 5 | static PropertyValue empty() { 6 | return new PropertyValue() { 7 | @Override 8 | public Builder setToBuilder(String property, Builder builder) { 9 | return builder; 10 | } 11 | }; 12 | } 13 | 14 | Builder setToBuilder(String property, Builder builder); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/PropertyAlias.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.lang.annotation.Repeatable; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Repeatable(PropertyAliases.class) 11 | @Target(TYPE) 12 | @Retention(RUNTIME) 13 | public @interface PropertyAlias { 14 | String alias(); 15 | 16 | String property(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/UnFixedValueProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.function.Supplier; 6 | 7 | class UnFixedValueProducer extends Producer { 8 | private final Supplier supplier; 9 | 10 | public UnFixedValueProducer(Supplier supplier, BeanClass type) { 11 | super(type); 12 | this.supplier = supplier; 13 | } 14 | 15 | @Override 16 | protected T produce() { 17 | return supplier.get(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Factory.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.function.Consumer; 6 | import java.util.function.Function; 7 | 8 | public interface Factory { 9 | Factory constructor(Function, T> constructor); 10 | 11 | Factory spec(Consumer> instance); 12 | 13 | Factory spec(String name, Consumer> instance); 14 | 15 | BeanClass getType(); 16 | 17 | Factory transformer(String property, Transformer transformer); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/TypeReference.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.GenericType; 5 | 6 | public abstract class TypeReference { 7 | @SuppressWarnings("unchecked") 8 | public BeanClass getType() { 9 | return (BeanClass) BeanClass.create(GenericType.createGenericType(getClass().getGenericSuperclass())).getTypeArguments(0) 10 | .orElseThrow(() -> new IllegalArgumentException("Cannot guess type, use new TypeReference() {}")); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Instance.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.function.Supplier; 4 | 5 | public interface Instance { 6 | int getSequence(); 7 | 8 | Spec spec(); 9 | 10 | Supplier reference(); 11 | 12 |

P param(String key); 13 | 14 |

P param(String key, P defaultValue); 15 | 16 | Arguments params(String property); 17 | 18 | Arguments params(); 19 | 20 | default int collectionSize() { 21 | return 0; 22 | } 23 | 24 | Object[] traitParams(); 25 | 26 | default Object traitParam(int index) { 27 | return traitParams()[index]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/cucumber/Run.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory.cucumber; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static com.github.leeonky.jfactory.cucumber.IntegrationTestContext.threadsCount; 6 | import static io.cucumber.core.cli.Main.run; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class Run { 10 | 11 | @Test 12 | void run_cucumber() { 13 | assertThat(run("--plugin", "pretty", "--glue", "com.github.leeonky", "--threads", 14 | String.valueOf(threadsCount("COMPILER_THREAD_SIZE", 8)), 15 | "src/test/resources/features")).isEqualTo(Byte.valueOf("0")); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/ExpressionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import org.junit.jupiter.api.Nested; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | class ExpressionTest { 9 | 10 | @Nested 11 | class Merge { 12 | 13 | @Test 14 | void single_merge_single_should_return_the_new_one() { 15 | SingleValueExpression expression1 = new SingleValueExpression<>(null, null, null); 16 | SingleValueExpression expression2 = new SingleValueExpression<>(null, null, null); 17 | 18 | assertThat(expression1.mergeTo(expression2)).isEqualTo(expression2); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/DefaultValueFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import static java.lang.String.format; 6 | 7 | public interface DefaultValueFactory { 8 | V create(BeanClass beanType, SubInstance instance); 9 | 10 | @SuppressWarnings("unchecked") 11 | default Class getType() { 12 | return (Class) BeanClass.create(getClass()).getSuper(DefaultValueFactory.class).getTypeArguments(0) 13 | .orElseThrow(() -> new IllegalStateException(format("Cannot guess type `%s` via generic type argument," 14 | + " please override DefaultValueFactory::getType", getClass().getName()))) 15 | .getType(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/MemoryDataRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.*; 4 | 5 | public class MemoryDataRepository implements DataRepository { 6 | private Map, Set> repo = new HashMap<>(); 7 | 8 | @Override 9 | public void save(Object object) { 10 | if (object != null) 11 | repo.computeIfAbsent(object.getClass(), c -> new LinkedHashSet<>()).add(object); 12 | } 13 | 14 | @Override 15 | @SuppressWarnings("unchecked") 16 | public Collection queryAll(Class type) { 17 | return (Collection) repo.getOrDefault(type, Collections.emptySet()); 18 | } 19 | 20 | @Override 21 | public void clear() { 22 | repo.clear(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/ValueCache.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Supplier; 5 | 6 | class ValueCache { 7 | private T value; 8 | private boolean cached = false; 9 | 10 | public T cache(Supplier supplier) { 11 | return cache(supplier, v -> { 12 | }); 13 | } 14 | 15 | public T cache(Supplier supplier, Consumer operation) { 16 | if (!cached) { 17 | value = supplier.get(); 18 | cached = true; 19 | operation.accept(value); 20 | } 21 | return value; 22 | } 23 | 24 | public T getValue() { 25 | if (cached) 26 | return value; 27 | throw new IllegalStateException("No value"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/FixedValueProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | class FixedValueProducer extends Producer { 6 | private final T value; 7 | 8 | public FixedValueProducer(BeanClass type, Object value) { 9 | super(type); 10 | this.value = BeanClass.getConverter().convert(type.getType(), value); 11 | } 12 | 13 | @Override 14 | protected T produce() { 15 | return value; 16 | } 17 | 18 | @Override 19 | public Producer changeTo(Producer newProducer) { 20 | if (newProducer instanceof FixedValueProducer) 21 | return newProducer; 22 | return this; 23 | } 24 | 25 | @Override 26 | protected boolean isFixed() { 27 | return true; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/LinkSpecCollection.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | class LinkSpecCollection { 7 | 8 | private final List linkSpecs = new ArrayList<>(); 9 | 10 | public void processLinks(Producer root, PropertyChain base) { 11 | linkSpecs.forEach(linkSpec -> linkSpec.process(root, base)); 12 | linkSpecs.clear(); 13 | } 14 | 15 | public void link(List properties) { 16 | if (properties.size() > 1) 17 | linkSpecs.stream().filter(linkSpec -> linkSpec.contains(properties)).findFirst().orElseGet(this::newLink).merge(properties); 18 | } 19 | 20 | private LinkSpec newLink() { 21 | LinkSpec linkSpec = new LinkSpec(); 22 | linkSpecs.add(linkSpec); 23 | return linkSpec; 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/SpecFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public class SpecFactory> extends SpecClassFactory { 6 | private final S spec; 7 | private final Consumer trait; 8 | 9 | @SuppressWarnings("unchecked") 10 | public SpecFactory(S spec, FactorySet factorySet, Consumer trait) { 11 | super((Class>) spec.getClass(), factorySet, false); 12 | this.spec = spec; 13 | this.trait = trait; 14 | } 15 | 16 | @Override 17 | protected Spec createSpec() { 18 | return spec; 19 | } 20 | 21 | @Override 22 | protected void collectSubSpec(Instance instance) { 23 | super.collectSubSpec(instance); 24 | collectClassSpec(instance, spec -> trait.accept((S) spec)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/SubInstance.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.PropertyWriter; 4 | 5 | import static java.util.Collections.emptyList; 6 | 7 | public class SubInstance extends RootInstance { 8 | private final PropertyWriter property; 9 | 10 | public SubInstance(PropertyWriter property, int sequence, Spec spec, DefaultArguments argument) { 11 | super(sequence, spec, argument); 12 | this.property = property; 13 | } 14 | 15 | String propertyInfo() { 16 | return String.format("%s#%d", property.getName(), getSequence()); 17 | } 18 | 19 | CollectionInstance inCollection() { 20 | return new CollectionInstance<>(emptyList(), property, getSequence(), spec, arguments); 21 | } 22 | 23 | public PropertyWriter getProperty() { 24 | return property; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/DefaultValueProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.function.Supplier; 6 | 7 | class DefaultValueProducer extends Producer { 8 | private final Supplier value; 9 | 10 | public DefaultValueProducer(BeanClass type, Supplier value) { 11 | super(type); 12 | this.value = value; 13 | } 14 | 15 | @Override 16 | protected V produce() { 17 | return value.get(); 18 | } 19 | 20 | @Override 21 | protected Producer changeFrom(Producer producer) { 22 | return producer.changeTo(this); 23 | } 24 | 25 | @Override 26 | protected Producer changeTo(DefaultValueProducer newProducer) { 27 | return newProducer; 28 | } 29 | 30 | @Override 31 | protected Producer changeFrom(ObjectProducer producer) { 32 | return producer; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/DependencyProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.List; 6 | import java.util.function.Function; 7 | import java.util.function.Supplier; 8 | 9 | class DependencyProducer extends Producer { 10 | private final List> dependencies; 11 | private final Function rule; 12 | 13 | public DependencyProducer(BeanClass type, List> dependencies, Function rule) { 14 | super(type); 15 | this.dependencies = dependencies; 16 | this.rule = rule; 17 | } 18 | 19 | @Override 20 | protected T produce() { 21 | return rule.apply(dependencies.stream().map(Supplier::get).toArray()); 22 | } 23 | 24 | @Override 25 | protected Producer changeFrom(ObjectProducer producer) { 26 | if (producer.isFixed()) 27 | return producer; 28 | return super.changeFrom(producer); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/ReadOnlyProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.PropertyReader; 5 | 6 | import static java.util.Optional.ofNullable; 7 | 8 | class ReadOnlyProducer extends Producer { 9 | private final PropertyReader

reader; 10 | private final Producer

parent; 11 | 12 | public ReadOnlyProducer(Producer

parent, String property) { 13 | this(parent, parent.getType().getPropertyReader(property)); 14 | } 15 | 16 | @SuppressWarnings("unchecked") 17 | private ReadOnlyProducer(Producer

parent, PropertyReader

reader) { 18 | super((BeanClass) reader.getType()); 19 | this.parent = parent; 20 | this.reader = reader; 21 | } 22 | 23 | @Override 24 | @SuppressWarnings("unchecked") 25 | protected T produce() { 26 | return (T) ofNullable(parent.getValue()).map(reader::getValue).orElseGet(() -> reader.getType().createDefault()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/PropertySpecTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class PropertySpecTest { 8 | private Spec spec = new Spec<>(); 9 | private PropertySpec propertySpec = new PropertySpec<>(spec, PropertyChain.propertyChain("p1")); 10 | 11 | @Test 12 | void method_chain() { 13 | assertThat(propertySpec.value(() -> null)).isEqualTo(spec); 14 | assertThat(propertySpec.value(1)).isEqualTo(spec); 15 | assertThat(propertySpec.is(MySpec.class)).isEqualTo(spec); 16 | assertThat(propertySpec.from(MySpec.class).and(builder -> builder.traits("trait"))).isEqualTo(spec); 17 | assertThat(propertySpec.from(MySpec.class).which(MySpec::someSpec)).isEqualTo(spec); 18 | assertThat(propertySpec.is("A", "Object")).isEqualTo(spec); 19 | } 20 | 21 | static class MySpec extends Spec { 22 | 23 | @Trait 24 | public MySpec someSpec() { 25 | return this; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /pi-report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -Euo pipefail 3 | 4 | PROJECT="github.com/leeonky/$(basename $(pwd))" 5 | VERSION=master 6 | 7 | cd build 8 | 9 | # Whenever something goes wrong, clean up the temporary file 10 | trap "rm mutation-testing-report.json" ERR 11 | 12 | # Find the report.js file 13 | reportJsLocation=$(find ./reports/pitest -name "report.js") 14 | echo Found report.js at ${reportJsLocation} 15 | # Read the file 16 | reportJsContent=$(<${reportJsLocation}) 17 | # Strip off the first 60 characters - yes, this is brittle :-) 18 | report="${reportJsContent:60}" 19 | # Store the data in a temporary file 20 | echo "${report}" > mutation-testing-report.json 21 | 22 | BASE_URL="https://dashboard.stryker-mutator.io" 23 | 24 | # Finally, upload the data using the API key that we got 25 | echo Uploading mutation-testing-report.json to ${BASE_URL}/api/reports/${PROJECT}/${VERSION} 26 | curl -X PUT \ 27 | ${BASE_URL}/api/reports/${PROJECT}/${VERSION} \ 28 | -H "Content-Type: application/json" \ 29 | -H "Host: dashboard.stryker-mutator.io" \ 30 | -H "X-Api-Key: ${PI_KEY}" \ 31 | -d @mutation-testing-report.json 32 | 33 | rm mutation-testing-report.json -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/CollectionInstance.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.PropertyWriter; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | class CollectionInstance extends SubInstance { 10 | private final List indexes; 11 | 12 | public CollectionInstance(List indexes, PropertyWriter property, int sequence, Spec spec, 13 | DefaultArguments argument) { 14 | super(property, sequence, spec, argument); 15 | this.indexes = new ArrayList<>(indexes); 16 | } 17 | 18 | @Override 19 | public String propertyInfo() { 20 | return String.format("%s%s", super.propertyInfo(), 21 | indexes.stream().map(i -> String.format("[%d]", i)).collect(Collectors.joining())); 22 | } 23 | 24 | public CollectionInstance element(int index) { 25 | CollectionInstance collection = new CollectionInstance<>(indexes, getProperty(), getSequence(), spec, arguments); 26 | collection.indexes.add(index); 27 | return collection; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/SingleValueExpression.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.CollectionHelper; 4 | import com.github.leeonky.util.Property; 5 | 6 | import java.util.Objects; 7 | 8 | class SingleValueExpression

extends Expression

{ 9 | private final Object value; 10 | private final TraitsSpec traitsSpec; 11 | 12 | public SingleValueExpression(Object value, TraitsSpec traitsSpec, Property

property) { 13 | super(property); 14 | this.value = value; 15 | this.traitsSpec = traitsSpec; 16 | } 17 | 18 | @Override 19 | protected boolean isPropertyMatch(Object propertyValue) { 20 | return property.getReaderType().isCollection() ? CollectionHelper.equals(propertyValue, value) 21 | : Objects.equals(propertyValue, property.getReader().tryConvert(value)); 22 | } 23 | 24 | @Override 25 | public Producer buildProducer(JFactory jFactory, Producer

parent) { 26 | if (intently) 27 | return traitsSpec.toBuilder(jFactory, property.getWriterType()).createProducer(); 28 | return new FixedValueProducer<>(property.getWriterType(), value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/gradle 2 | # Edit at https://www.gitignore.io/?templates=gradle 3 | 4 | ### Gradle ### 5 | .gradle 6 | /build/ 7 | 8 | # Ignore Gradle GUI config 9 | gradle-app.setting 10 | 11 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 12 | !gradle-wrapper.jar 13 | 14 | # Cache of project 15 | .gradletasknamecache 16 | 17 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 18 | # gradle/wrapper/gradle-wrapper.properties 19 | 20 | ### Gradle Patch ### 21 | **/build/ 22 | 23 | # End of https://www.gitignore.io/api/gradle 24 | 25 | 26 | # Created by https://www.gitignore.io/api/java 27 | # Edit at https://www.gitignore.io/?templates=java 28 | 29 | ### Java ### 30 | # Compiled class file 31 | *.class 32 | 33 | # Log file 34 | *.log 35 | 36 | # BlueJ files 37 | *.ctxt 38 | 39 | # Mobile Tools for Java (J2ME) 40 | .mtj.tmp/ 41 | 42 | # Package Files # 43 | *.jar 44 | *.war 45 | *.nar 46 | *.ear 47 | *.zip 48 | *.tar.gz 49 | *.rar 50 | 51 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 52 | hs_err_pid* 53 | 54 | # End of https://www.gitignore.io/api/java 55 | 56 | .idea 57 | 58 | *.iml 59 | 60 | out 61 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Dependency.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.List; 6 | import java.util.function.Function; 7 | import java.util.function.Supplier; 8 | import java.util.stream.Collectors; 9 | 10 | class Dependency { 11 | private final Function rule; 12 | private final PropertyChain property; 13 | private final List dependencies; 14 | 15 | public Dependency(PropertyChain property, List dependencies, Function rule) { 16 | this.property = property; 17 | this.dependencies = dependencies; 18 | this.rule = rule; 19 | } 20 | 21 | @SuppressWarnings("unchecked") 22 | public void process(Producer parent) { 23 | parent.changeDescendant(property, (nextToLast, property) -> new DependencyProducer<>( 24 | (BeanClass) nextToLast.getPropertyWriterType(property), suppliers(parent), rule)); 25 | } 26 | 27 | private List> suppliers(Producer producer) { 28 | return dependencies.stream().map(dependency -> (Supplier) () -> 29 | producer.descendant(dependency).getValue()).collect(Collectors.toList()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Builder.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.Collection; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public interface Builder { 10 | 11 | T create(); 12 | 13 | BeanClass getType(); 14 | 15 | default Builder property(String property, Object value) { 16 | return properties(new HashMap() {{ 17 | put(property, value); 18 | }}); 19 | } 20 | 21 | Builder properties(Map properties); 22 | 23 | default T query() { 24 | return queryAll().stream().findFirst().orElse(null); 25 | } 26 | 27 | Collection queryAll(); 28 | 29 | Builder traits(String... traits); 30 | 31 | Producer createProducer(); 32 | 33 | Builder arg(String key, Object value); 34 | 35 | Builder args(Arguments arguments); 36 | 37 | Builder args(Map args); 38 | 39 | Builder args(String property, Map args); 40 | 41 | default Builder propertyValue(String property, PropertyValue value) { 42 | return value.setToBuilder(property, this); 43 | } 44 | 45 | default Builder properties(PropertyValue value) { 46 | return value.setToBuilder("", this); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | docker: 5 | - image: openjdk:8u342 6 | steps: 7 | - add_ssh_keys: 8 | fingerprints: 9 | - '80:8f:43:71:2b:1a:2f:52:00:9c:31:8a:77:16:d1:7c' 10 | - checkout 11 | - run: 12 | name: build 13 | command: | 14 | export VERSION_SCOPE='' 15 | ./gradlew check build coveralls 16 | export PUB_VERSION=$(./gradlew currentVersion -q) 17 | if ( bash -c "git tag | grep -q \"^${PUB_VERSION}$\"" ); then 18 | echo "====== Still version ${PUB_VERSION}, CI done ======" 19 | exit 0 20 | fi 21 | echo "====== Release and tag new version $PUB_VERSION ======" 22 | echo $GPG_KEY | base64 --decode > ${SIGNING_SECRETKEYRINGFILE} 23 | ./gradlew -Dorg.gradle.project.signing.keyId="$SIGNING_KEYID" -Dorg.gradle.project.signing.password="$SIGNING_PASSWORD" -Dorg.gradle.project.signing.secretKeyRingFile="$SIGNING_SECRETKEYRINGFILE" publishToNexus 24 | git config --global user.email "leeonky@gmail.com" 25 | git config --global user.name "leeonky" 26 | git tag ${PUB_VERSION} -a -m 'CI Release' 27 | git push --tags 28 | 29 | - store_artifacts: 30 | path: /root/project/build/reports/ 31 | destination: build-reports -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/LinkProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.Optional; 6 | import java.util.stream.Stream; 7 | 8 | class LinkProducer extends Producer { 9 | private final Link.Reference linkerReference; 10 | private final Producer origin; 11 | 12 | public LinkProducer(BeanClass type, Link link, Producer origin, PropertyChain linkProperty) { 13 | super(type); 14 | linkerReference = new Link.Reference(linkProperty).setLinker(link); 15 | this.origin = origin; 16 | } 17 | 18 | @Override 19 | protected T produce() { 20 | return linkerReference.getLinker().chooseProducer().getValue(); 21 | } 22 | 23 | @Override 24 | public Stream> allLinkerReferences(Producer root, PropertyChain absoluteCurrent) { 25 | return linkerReference.getLinker().allLinkedReferences(); 26 | } 27 | 28 | @Override 29 | public Producer getLinkOrigin() { 30 | return origin; 31 | } 32 | 33 | @Override 34 | public Optional> child(String property) { 35 | return linkerReference.getLinker().chooseProducer().child(property); 36 | } 37 | 38 | @Override 39 | protected Producer changeFrom(ObjectProducer producer) { 40 | return producer.isFixed() ? producer : this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/JFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 6 | 7 | class JFactoryTest { 8 | 9 | @Test 10 | void supported_generic_signature() { 11 | JFactory jFactory = new JFactory(); 12 | Builder beanBuilder = jFactory.type(Bean.class); 13 | Builder builderInAnyType = jFactory.type((Class) Bean.class); 14 | Builder beanSpecBuilder = jFactory.spec(BeanSpec.class); 15 | Builder beanTypeReferenceBuilder = jFactory.type(new TypeReference() { 16 | }); 17 | Builder builderInAnyTypeReference = jFactory.type((TypeReference) new TypeReference() { 18 | }); 19 | } 20 | 21 | @Test 22 | void global_spec_should_not_has_super_spec() { 23 | JFactory jFactory = new JFactory(); 24 | assertThatThrownBy(() -> jFactory.register(InvalidGlobalSpec.class)).isInstanceOf(IllegalArgumentException.class) 25 | .hasMessage("Global Spec com.github.leeonky.jfactory.JFactoryTest$InvalidGlobalSpec should not have super Spec com.github.leeonky.jfactory.JFactoryTest$BeanSpec."); 26 | } 27 | 28 | public static class Bean { 29 | } 30 | 31 | public static class BeanSpec extends Spec { 32 | } 33 | 34 | @Global 35 | public static class InvalidGlobalSpec extends BeanSpec { 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Expression.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.Property; 4 | 5 | import java.util.List; 6 | 7 | abstract class Expression

{ 8 | protected final Property

property; 9 | protected boolean intently = false; 10 | 11 | public Expression(Property

property) { 12 | this.property = property; 13 | } 14 | 15 | protected String getProperty() { 16 | return property.getName(); 17 | } 18 | 19 | public boolean isMatch(P object) { 20 | return object != null && !intently && isPropertyMatch(property.getValue(object)); 21 | } 22 | 23 | protected abstract boolean isPropertyMatch(Object propertyValue); 24 | 25 | public abstract Producer buildProducer(JFactory jFactory, Producer

parent); 26 | 27 | static Expression merge(List> expressions) { 28 | return expressions.stream().reduce(Expression::mergeTo).get(); 29 | } 30 | 31 | protected Expression

mergeTo(Expression

newExpression) { 32 | return newExpression; 33 | } 34 | 35 | protected Expression

mergeFrom(SubObjectExpression

origin) { 36 | return this; 37 | } 38 | 39 | protected Expression

mergeFrom(CollectionExpression origin) { 40 | return this; 41 | } 42 | 43 | protected Expression

setIntently(boolean intently) { 44 | this.intently = intently; 45 | return this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/resources/features/bug/dependency.feature: -------------------------------------------------------------------------------- 1 | Feature: dependency 2 | 3 | Scenario: should ignore parent property dependency when specify sub property(create sub) 4 | Given the following bean class: 5 | """ 6 | public class Order { 7 | public String id; 8 | public User user; 9 | } 10 | """ 11 | And the following bean class: 12 | """ 13 | public class Recorder { 14 | public User user; 15 | public Order order; 16 | } 17 | """ 18 | And the following bean class: 19 | """ 20 | public class User { 21 | public String name; 22 | } 23 | """ 24 | And the following spec class: 25 | """ 26 | public class OneRecorder extends Spec { 27 | @Override 28 | public void main() { 29 | property("user").dependsOn("order.user", o->o); 30 | } 31 | } 32 | """ 33 | And the following spec class: 34 | """ 35 | public class OneOrder extends Spec { 36 | } 37 | """ 38 | And build: 39 | """ 40 | jFactory.spec(OneOrder.class).property("user.name", "Tom").property("id", "001").create(); 41 | """ 42 | When build: 43 | """ 44 | jFactory.spec(OneRecorder.class).property("user.name", "Lucy").property("order.id", "001").create(); 45 | """ 46 | Then the result should: 47 | """ 48 | user.name: Lucy 49 | """ 50 | 51 | # TODO Scenario: should ignore collection property link when specify sub property(create sub) 52 | -------------------------------------------------------------------------------- /src/test/resources/features/bug/link.feature: -------------------------------------------------------------------------------- 1 | Feature: link 2 | 3 | Scenario: should ignore parent property link when specify sub property(create sub) 4 | Given the following bean class: 5 | """ 6 | public class Order { 7 | public String id; 8 | public User user; 9 | } 10 | """ 11 | And the following bean class: 12 | """ 13 | public class Recorder { 14 | public User user; 15 | public Order order; 16 | } 17 | """ 18 | And the following bean class: 19 | """ 20 | public class User { 21 | public String name; 22 | } 23 | """ 24 | And the following spec class: 25 | """ 26 | public class OneRecorder extends Spec { 27 | @Override 28 | public void main() { 29 | link("order.user", "user"); 30 | } 31 | } 32 | """ 33 | And the following spec class: 34 | """ 35 | public class OneOrder extends Spec { 36 | } 37 | """ 38 | And build: 39 | """ 40 | jFactory.spec(OneOrder.class).property("user.name", "Tom").property("id", "001").create(); 41 | """ 42 | When build: 43 | """ 44 | jFactory.spec(OneRecorder.class).property("user.name", "Lucy").property("order.id", "001").create(); 45 | """ 46 | Then the result should: 47 | """ 48 | user.name: Lucy 49 | """ 50 | 51 | # TODO Scenario: should ignore collection property link when specify sub property(create sub) 52 | # TODO link / dep 53 | # TODO link merge objectproducer 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/TraitsSpec.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.LinkedHashSet; 6 | import java.util.Objects; 7 | import java.util.Set; 8 | 9 | import static java.util.Arrays.asList; 10 | 11 | class TraitsSpec { 12 | private String spec; 13 | private Set traits; 14 | 15 | public TraitsSpec(String[] traits, String spec) { 16 | this.spec = spec; 17 | this.traits = new LinkedHashSet<>(asList(traits)); 18 | } 19 | 20 | private void mergeTraits(TraitsSpec another) { 21 | traits.addAll(another.traits); 22 | } 23 | 24 | private void mergeSpec(TraitsSpec another, String property) { 25 | if (isDifferentSpec(another)) 26 | throw new IllegalArgumentException(String.format("Cannot merge different spec `%s` and `%s` for %s", 27 | spec, another.spec, property)); 28 | if (spec == null) 29 | spec = another.spec; 30 | } 31 | 32 | private boolean isDifferentSpec(TraitsSpec another) { 33 | return spec != null && another.spec != null && !Objects.equals(spec, another.spec); 34 | } 35 | 36 | public Builder toBuilder(JFactory jFactory, BeanClass propertyType) { 37 | return (spec != null ? jFactory.spec(spec) : jFactory.type(propertyType.getType())) 38 | .traits(traits.toArray(new String[0])); 39 | } 40 | 41 | public void merge(TraitsSpec another, String property) { 42 | mergeTraits(another); 43 | mergeSpec(another, property); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/LinkSpec.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.LinkedHashSet; 4 | import java.util.List; 5 | import java.util.Set; 6 | import java.util.stream.Stream; 7 | 8 | class LinkSpec { 9 | private final Set linkedProperties = new LinkedHashSet<>(); 10 | 11 | @SuppressWarnings("unchecked") 12 | public void process(Producer root, PropertyChain base) { 13 | Link link = syncLink(root, base); 14 | absoluteProperties(base).forEach(linkProperty -> root.changeDescendant(linkProperty, 15 | (objectProducer, property) -> new LinkProducer(objectProducer.getPropertyWriterType(property), link, 16 | root.descendant(linkProperty).getLinkOrigin(), linkProperty))); 17 | } 18 | 19 | private Stream absoluteProperties(PropertyChain base) { 20 | return linkedProperties.stream().map(base::concat); 21 | } 22 | 23 | @SuppressWarnings("unchecked") 24 | private Link syncLink(Producer root, PropertyChain base) { 25 | Link link = new Link<>(root); 26 | absoluteProperties(base).flatMap(property -> ((Producer) root.descendant(property)) 27 | .allLinkerReferences(root, property)).distinct().forEach(link::link); 28 | return link; 29 | } 30 | 31 | public boolean contains(List properties) { 32 | return properties.stream().anyMatch(linkedProperties::contains); 33 | } 34 | 35 | public void merge(List properties) { 36 | linkedProperties.addAll(properties); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/DefaultArguments.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | 6 | import static com.github.leeonky.jfactory.PropertyChain.propertyChain; 7 | 8 | class DefaultArguments implements Arguments { 9 | 10 | final Map params = new LinkedHashMap<>(); 11 | 12 | public void merge(DefaultArguments argument) { 13 | params.putAll(argument.params); 14 | } 15 | 16 | @Override 17 | @SuppressWarnings("unchecked") 18 | public

P param(String key) { 19 | return (P) params.get(propertyChain(key)); 20 | } 21 | 22 | @Override 23 | @SuppressWarnings("unchecked") 24 | public

P param(String key, P defaultValue) { 25 | return (P) params.getOrDefault(propertyChain(key), defaultValue); 26 | } 27 | 28 | @Override 29 | public Arguments params(String property) { 30 | return params(propertyChain(property)); 31 | } 32 | 33 | public void put(String key, Object value) { 34 | put(propertyChain(key), value); 35 | } 36 | 37 | private void put(PropertyChain key, Object value) { 38 | params.put(key, value); 39 | } 40 | 41 | public void put(String property, String key, Object value) { 42 | put(propertyChain(property).concat(key), value); 43 | } 44 | 45 | public Arguments params(PropertyChain propertyChain) { 46 | DefaultArguments defaultArguments = new DefaultArguments(); 47 | params.forEach((key, value) -> key.sub(propertyChain).ifPresent(p -> defaultArguments.put(p, value))); 48 | return defaultArguments; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/BuilderValueProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | public class BuilderValueProducer extends Producer { 6 | private final Builder builder; 7 | 8 | public BuilderValueProducer(BeanClass type, Builder builder) { 9 | super(type); 10 | this.builder = builder; 11 | } 12 | 13 | @Override 14 | protected T produce() { 15 | return BeanClass.getConverter().convert(getType().getType(), builder.query()); 16 | } 17 | 18 | @Override 19 | public Producer changeTo(Producer newProducer) { 20 | if (newProducer instanceof BuilderValueProducer) { 21 | if (builder instanceof DefaultBuilder && ((BuilderValueProducer) newProducer).builder instanceof DefaultBuilder) { 22 | DefaultBuilder marge = ((DefaultBuilder) builder).marge((DefaultBuilder) ((BuilderValueProducer) newProducer).builder); 23 | return new BuilderValueProducer<>(getType(), marge); 24 | } 25 | // TODO need test 26 | return newProducer; 27 | } 28 | if (newProducer instanceof ObjectProducer) 29 | return builder.createProducer().changeTo(newProducer); 30 | // TODO need test 31 | return this; 32 | } 33 | 34 | // TODO need test missing all test of this method() query in spec and should be created after merge input property 35 | public Producer getProducer() { 36 | Object[] objects = builder.queryAll().toArray(); 37 | if (objects.length != 0) 38 | return new FixedValueProducer<>(getType(), objects[0]); 39 | // TODO need test 40 | return builder.createProducer(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/CompositeRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import java.util.function.Predicate; 7 | 8 | public class CompositeRepository implements DataRepository { 9 | private final List repos = new ArrayList<>(); 10 | private final MemoryDataRepository defaultRepo = new MemoryDataRepository(); 11 | 12 | public CompositeRepository addRepository(Predicate> when, DataRepository repo) { 13 | repos.add(new TypedPrecedingRepo(when, repo)); 14 | return this; 15 | } 16 | 17 | private static class TypedPrecedingRepo { 18 | private final Predicate> when; 19 | private final DataRepository repository; 20 | 21 | private TypedPrecedingRepo(Predicate> when, DataRepository repository) { 22 | this.when = when; 23 | this.repository = repository; 24 | } 25 | 26 | public boolean matches(Class type) { 27 | return when.test(type); 28 | } 29 | 30 | public DataRepository getRepository() { 31 | return repository; 32 | } 33 | } 34 | 35 | @Override 36 | public Collection queryAll(Class type) { 37 | return fetchRepo(type).queryAll(type); 38 | } 39 | 40 | private DataRepository fetchRepo(Class type) { 41 | return repos.stream().filter(r -> r.matches(type)).findFirst() 42 | .map(TypedPrecedingRepo::getRepository).orElse(defaultRepo); 43 | } 44 | 45 | @Override 46 | public void save(Object object) { 47 | fetchRepo(object.getClass()).save(object); 48 | } 49 | 50 | @Override 51 | public void clear() { 52 | repos.stream().map(TypedPrecedingRepo::getRepository).forEach(DataRepository::clear); 53 | defaultRepo.clear(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/SubObjectExpression.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.Property; 5 | 6 | import java.util.Optional; 7 | 8 | class SubObjectExpression

extends Expression

{ 9 | private final KeyValueCollection properties; 10 | private final TraitsSpec traitsSpec; 11 | private final ObjectFactory objectFactory; 12 | 13 | public SubObjectExpression(KeyValueCollection properties, TraitsSpec traitsSpec, Property

property, ObjectFactory objectFactory) { 14 | super(property); 15 | this.properties = properties; 16 | this.traitsSpec = traitsSpec; 17 | this.objectFactory = objectFactory; 18 | } 19 | 20 | @Override 21 | protected boolean isPropertyMatch(Object propertyValue) { 22 | return properties.matcher(property.getReaderType(), objectFactory).matches(propertyValue); 23 | } 24 | 25 | @Override 26 | public Producer buildProducer(JFactory jFactory, Producer

parent) { 27 | Builder builder = toBuilder(jFactory, property.getWriterType()); 28 | return query(jFactory).>map(object -> new BuilderValueProducer<>(property.getWriterType(), builder)) 29 | .orElseGet(builder::createProducer); 30 | } 31 | 32 | private Optional query(JFactory jFactory) { 33 | if (intently) 34 | return Optional.empty(); 35 | return toBuilder(jFactory, property.getReaderType()).queryAll().stream().findFirst(); 36 | } 37 | 38 | private Builder toBuilder(JFactory jFactory, BeanClass propertyType) { 39 | return properties.apply(traitsSpec.toBuilder(jFactory, propertyType)); 40 | } 41 | 42 | @Override 43 | public Expression

mergeTo(Expression

newExpression) { 44 | return newExpression.mergeFrom(this); 45 | } 46 | 47 | @Override 48 | protected Expression

mergeFrom(SubObjectExpression

origin) { 49 | properties.insertAll(origin.properties); 50 | traitsSpec.merge(origin.traitsSpec, property.toString()); 51 | setIntently(intently || origin.intently); 52 | return this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/RootInstance.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.PropertyWriter; 4 | 5 | import java.util.function.Consumer; 6 | import java.util.function.Supplier; 7 | 8 | class RootInstance implements Instance { 9 | private static final Object[] NO_TRAIT_PARAMS = new Object[0]; 10 | protected final Spec spec; 11 | protected final DefaultArguments arguments; 12 | private final int sequence; 13 | private final ValueCache valueCache = new ValueCache<>(); 14 | private int collectionSize = 0; 15 | private Object[] traitParams = NO_TRAIT_PARAMS; 16 | 17 | public RootInstance(int sequence, Spec spec, DefaultArguments arguments) { 18 | this.sequence = sequence; 19 | this.spec = spec; 20 | this.arguments = arguments; 21 | } 22 | 23 | @Override 24 | public int getSequence() { 25 | return sequence; 26 | } 27 | 28 | SubInstance sub(PropertyWriter property) { 29 | return new SubInstance<>(property, sequence, spec, arguments); 30 | } 31 | 32 | @Override 33 | public Spec spec() { 34 | return spec; 35 | } 36 | 37 | @Override 38 | public Supplier reference() { 39 | return valueCache::getValue; 40 | } 41 | 42 | @Override 43 | public

P param(String key) { 44 | return arguments.param(key); 45 | } 46 | 47 | @Override 48 | public

P param(String key, P defaultValue) { 49 | return arguments.param(key, defaultValue); 50 | } 51 | 52 | @Override 53 | public Arguments params(String property) { 54 | return arguments.params(property); 55 | } 56 | 57 | @Override 58 | public Arguments params() { 59 | return arguments; 60 | } 61 | 62 | T cache(Supplier supplier, Consumer operation) { 63 | return valueCache.cache(supplier, operation); 64 | } 65 | 66 | public void setCollectionSize(int collectionSize) { 67 | this.collectionSize = collectionSize; 68 | } 69 | 70 | @Override 71 | public int collectionSize() { 72 | return collectionSize; 73 | } 74 | 75 | @Override 76 | public Object[] traitParams() { 77 | return traitParams; 78 | } 79 | 80 | void runTraitWithParams(Object[] params, Consumer> action) { 81 | traitParams = params; 82 | try { 83 | action.accept(this); 84 | } finally { 85 | traitParams = NO_TRAIT_PARAMS; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/cucumber/Steps.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory.cucumber; 2 | 3 | import io.cucumber.java.After; 4 | import io.cucumber.java.Before; 5 | import io.cucumber.java.en.And; 6 | import io.cucumber.java.en.Given; 7 | import io.cucumber.java.en.Then; 8 | import io.cucumber.java.en.When; 9 | 10 | public class Steps { 11 | private IntegrationTestContext integrationTestContext; 12 | 13 | @Before 14 | public void reset() { 15 | integrationTestContext = new IntegrationTestContext(); 16 | } 17 | 18 | @After 19 | public void releaseCompiler() { 20 | integrationTestContext.releaseCompiler(); 21 | } 22 | 23 | 24 | @Given("the following bean class:") 25 | public void the_following_bean_class(String classCode) { 26 | integrationTestContext.givenBean(classCode); 27 | } 28 | 29 | @Given("the following spec class:") 30 | public void the_following_spec_class(String specClass) { 31 | integrationTestContext.specClass(specClass); 32 | } 33 | 34 | @Given("declaration jFactory =") 35 | public void declarationJFactory(String declaration) { 36 | integrationTestContext.declare(declaration); 37 | } 38 | 39 | @Given("declaration list =") 40 | public void declarationList(String listDeclaration) { 41 | integrationTestContext.declareList(listDeclaration); 42 | } 43 | 44 | @Then("the list in repo should:") 45 | public void theListInRepoShould(String dal) { 46 | integrationTestContext.listShould(dal); 47 | } 48 | 49 | @And("register:") 50 | public void register(String factorySnippet) { 51 | integrationTestContext.register(factorySnippet); 52 | } 53 | 54 | @And("operate:") 55 | public void operate(String operateSnippet) { 56 | integrationTestContext.register(operateSnippet); 57 | } 58 | 59 | @When("build:") 60 | public void build(String builderSnippet) { 61 | integrationTestContext.build(builderSnippet); 62 | } 63 | 64 | @Then("{string} should") 65 | public void should(String code, String dal) throws Throwable { 66 | integrationTestContext.build(code + ";"); 67 | integrationTestContext.verify(dal); 68 | } 69 | 70 | @Then("the result should:") 71 | public void the_result_should(String dal) throws Throwable { 72 | integrationTestContext.verify(dal); 73 | } 74 | 75 | @Then("should raise error:") 76 | public void shouldRaiseError(String dal) { 77 | integrationTestContext.shouldThrow(dal); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/AliasSetStore.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import static com.github.leeonky.jfactory.PropertyChain.propertyChain; 9 | import static java.util.Collections.singletonList; 10 | 11 | public class AliasSetStore { 12 | private final Map, AliasSet> aliasSetMap = new HashMap<>(); 13 | private final Map, AliasSet> specAliasSetMap = new HashMap<>(); 14 | 15 | public String resolve(ObjectFactory objectFactory, String propertyChain, boolean collectionProperties) { 16 | if (objectFactory instanceof SpecClassFactory) { 17 | String evaluated = specAliasSet(((SpecClassFactory) objectFactory).getSpecClass()) 18 | .resolve(propertyChain(propertyChain), collectionProperties).toString(); 19 | if (!evaluated.equals(propertyChain)) 20 | return evaluated; 21 | return resolve(objectFactory.getBase(), propertyChain, collectionProperties); 22 | } 23 | return aliasSet(objectFactory.getType()).resolve(propertyChain(propertyChain), collectionProperties).toString(); 24 | } 25 | 26 | public AliasSet aliasSet(BeanClass type) { 27 | return aliasSetMap.computeIfAbsent(type, key -> new AliasSet()); 28 | } 29 | 30 | public > AliasSet specAliasSet(Class specClass) { 31 | return specAliasSetMap.computeIfAbsent(specClass, key -> new AliasSet()); 32 | } 33 | 34 | public static class AliasSet { 35 | private final Map aliases = new HashMap<>(); 36 | 37 | // TODO refactor 38 | public PropertyChain resolve(PropertyChain chain, boolean collectionProperties) { 39 | Object head = chain.head(); 40 | PropertyChain left = chain.removeHead(); 41 | String headString = head.toString(); 42 | boolean intently = headString.endsWith("!"); 43 | if (intently) 44 | headString = headString.substring(0, headString.length() - 1); 45 | if (aliases.containsKey(headString)) { 46 | String property = aliases.get(headString); 47 | if (property.contains("$") && !(property.contains("[$]") && collectionProperties)) { 48 | property = property.replaceFirst("\\$", left.head().toString()); 49 | left = left.removeHead(); 50 | } 51 | if (intently) 52 | property = property + "!"; 53 | return resolve(propertyChain(property), collectionProperties).concat(left); 54 | } 55 | return new PropertyChain(singletonList(head)).concat(left); 56 | } 57 | 58 | public AliasSet alias(String alias, String target) { 59 | aliases.put(alias, target); 60 | return this; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/bug/UseSubObjectSpec.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory.bug; 2 | 3 | import com.github.leeonky.jfactory.JFactory; 4 | import com.github.leeonky.jfactory.Spec; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import static com.github.leeonky.dal.Assertions.expect; 11 | 12 | public class UseSubObjectSpec { 13 | public static class Order { 14 | public String name; 15 | public String des; 16 | public List orderLines = new ArrayList<>(); 17 | } 18 | 19 | public static class OrderLine { 20 | public Order order; 21 | public String name; 22 | public String des; 23 | public Product product; 24 | } 25 | 26 | public static class Product { 27 | public Category category; 28 | public OrderLine orderLine; 29 | public String name; 30 | public String des; 31 | } 32 | 33 | public static class Category { 34 | public String name; 35 | public String des; 36 | } 37 | 38 | public static class OrderSpec extends Spec { 39 | @Override 40 | public void main() { 41 | property("orderLines").reverseAssociation("order"); 42 | property("orderLines[0]").is(OrderLineSpec.class); 43 | property("des").value("from-spec"); 44 | } 45 | } 46 | 47 | public static class EmptyOrderSpec extends Spec { 48 | @Override 49 | public void main() { 50 | property("orderLines").reverseAssociation("order"); 51 | property("des").value("from-spec"); 52 | } 53 | } 54 | 55 | public static class OrderLineSpec extends Spec { 56 | @Override 57 | public void main() { 58 | property("order").is(EmptyOrderSpec.class); 59 | property("product").is(ProductSpec.class); 60 | property("des").value("from-spec"); 61 | 62 | //property("product").reverseAssociation("orderLine"); 63 | } 64 | } 65 | 66 | public static class ProductSpec extends Spec { 67 | @Override 68 | public void main() { 69 | property("des").value("from-spec"); 70 | property("category").is(CategorySpec.class); 71 | } 72 | } 73 | 74 | public static class CategorySpec extends Spec { 75 | @Override 76 | public void main() { 77 | property("des").value("from-spec"); 78 | } 79 | } 80 | 81 | @Test 82 | void test() { 83 | JFactory jFactory = new JFactory(); 84 | // jFactory.spec(OrderLineSpec.class).property("product.name", "input").create(); 85 | // if (false) 86 | // extracted(jFactory); 87 | } 88 | 89 | private void extracted(JFactory jFactory) { 90 | expect( 91 | jFactory.spec(OrderSpec.class) 92 | .property("orderLines[0].name", "input") 93 | .create()).should("=''"); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/CompositeRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import org.junit.jupiter.api.Nested; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Collection; 7 | import java.util.function.Predicate; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.mockito.ArgumentMatchers.any; 11 | import static org.mockito.Mockito.*; 12 | 13 | class CompositeRepositoryTest { 14 | 15 | CompositeRepository compositeRepository = new CompositeRepository(); 16 | 17 | @Nested 18 | class QueryByType { 19 | 20 | @Test 21 | void default_repository_is_memory_repo_when_no_registered_repo() { 22 | compositeRepository.save("hello"); 23 | 24 | assertThat(compositeRepository.queryAll(String.class)).containsExactly("hello"); 25 | } 26 | 27 | @Test 28 | void query_by_type_and_repo() { 29 | DataRepository repo = mock(DataRepository.class); 30 | Collection collection = mock(Collection.class); 31 | when(repo.queryAll(String.class)).thenReturn(collection); 32 | 33 | Predicate predicate = mock(Predicate.class); 34 | when(predicate.test(any())).thenReturn(true); 35 | 36 | compositeRepository.addRepository(predicate, repo); 37 | 38 | assertThat(compositeRepository.queryAll(String.class)).isEqualTo(collection); 39 | verify(predicate).test(String.class); 40 | } 41 | } 42 | 43 | @Nested 44 | class SaveByType { 45 | 46 | @Test 47 | void default_repository_is_memory_repo_when_no_registered_repo() { 48 | compositeRepository.save("hello"); 49 | 50 | assertThat(compositeRepository.queryAll(String.class)).containsExactly("hello"); 51 | } 52 | 53 | @Test 54 | void query_by_type_and_repo() { 55 | DataRepository repo = mock(DataRepository.class); 56 | Predicate predicate = mock(Predicate.class); 57 | when(predicate.test(any())).thenReturn(true); 58 | compositeRepository.addRepository(predicate, repo); 59 | 60 | compositeRepository.save("string"); 61 | 62 | verify(predicate).test(String.class); 63 | verify(repo).save("string"); 64 | assertThat(compositeRepository.queryAll(String.class)).isEmpty(); 65 | } 66 | } 67 | 68 | @Nested 69 | class ClearAll { 70 | 71 | @Test 72 | void default_repository_is_memory_repo_when_no_registered_repo() { 73 | compositeRepository.save("hello"); 74 | 75 | compositeRepository.clear(); 76 | 77 | assertThat(compositeRepository.queryAll(String.class)).isEmpty(); 78 | } 79 | 80 | @Test 81 | void clear_by_type_and_repo() { 82 | compositeRepository.save("hello"); 83 | DataRepository repo = mock(DataRepository.class); 84 | 85 | compositeRepository.addRepository(type -> false, repo); 86 | compositeRepository.clear(); 87 | 88 | assertThat(compositeRepository.queryAll(String.class)).isEmpty(); 89 | verify(repo).clear(); 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/DefaultBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Nested; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class DefaultBuilderTest { 12 | private JFactory jFactory = new JFactory(); 13 | 14 | @BeforeEach 15 | void registerTrait() { 16 | jFactory.factory(Bean.class).spec("trait1", instance -> { 17 | }); 18 | 19 | jFactory.factory(Bean2.class).spec("trait1", instance -> { 20 | }).spec("trait2", instance -> { 21 | }); 22 | } 23 | 24 | private Builder builder(Class type, String value, String trait) { 25 | return jFactory.type(type).property("defaultString1", value).traits(trait); 26 | } 27 | 28 | @Getter 29 | @Setter 30 | public static class Bean { 31 | private String stringValue; 32 | } 33 | 34 | @Getter 35 | @Setter 36 | public static class Bean2 { 37 | private String stringValue; 38 | } 39 | 40 | @Nested 41 | class Hashcode { 42 | 43 | @Test 44 | void should_get_equal_hashcode_for_same_object_producer() { 45 | assertThat(builder(Bean.class, "string", "trait1").hashCode()) 46 | .isEqualTo(builder(Bean.class, "string", "trait1").hashCode()); 47 | } 48 | 49 | @Test 50 | void should_not_equal_for_different_type() { 51 | assertThat(builder(Bean.class, "string", "trait1").hashCode()) 52 | .isNotEqualTo(builder(Bean2.class, "string", "trait1").hashCode()); 53 | } 54 | 55 | @Test 56 | void should_not_equal_for_different_property() { 57 | assertThat(builder(Bean.class, "string", "trait1").hashCode()) 58 | .isNotEqualTo(builder(Bean.class, "string2", "trait1").hashCode()); 59 | } 60 | 61 | @Test 62 | void should_not_equal_for_different_trait() { 63 | assertThat(builder(Bean.class, "string", "trait1").hashCode()) 64 | .isNotEqualTo(builder(Bean.class, "string", "trait2").hashCode()); 65 | } 66 | } 67 | 68 | @Nested 69 | class Equal { 70 | 71 | @Test 72 | void should_get_equal_hashcode_for_same_object_producer() { 73 | assertThat(builder(Bean.class, "string", "trait1")) 74 | .isEqualTo(builder(Bean.class, "string", "trait1")); 75 | } 76 | 77 | @Test 78 | void should_not_equal_for_different_type() { 79 | assertThat(builder(Bean.class, "string", "trait1")) 80 | .isNotEqualTo(builder(Bean2.class, "string", "trait1")); 81 | } 82 | 83 | @Test 84 | void should_not_equal_for_different_property() { 85 | assertThat(builder(Bean.class, "string", "trait1")) 86 | .isNotEqualTo(builder(Bean.class, "string2", "trait1")); 87 | } 88 | 89 | @Test 90 | void should_not_equal_for_different_trait() { 91 | assertThat(builder(Bean.class, "string", "trait1")) 92 | .isNotEqualTo(builder(Bean.class, "string", "trait2")); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Link.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.*; 4 | import java.util.stream.Stream; 5 | 6 | import static com.github.leeonky.util.BeanClass.cast; 7 | import static java.util.Arrays.asList; 8 | import static java.util.stream.Collectors.toList; 9 | 10 | class Link { 11 | private static final List> TYPE_PRIORITY = asList( 12 | FixedValueProducer.class, 13 | ReadOnlyProducer.class, 14 | DependencyProducer.class, 15 | UnFixedValueProducer.class 16 | ); 17 | private final Set linkedAbsoluteProperties = new LinkedHashSet<>(); 18 | private final Producer root; 19 | private final Set> references = new LinkedHashSet<>(); 20 | 21 | public Link(Producer root) { 22 | this.root = root; 23 | } 24 | 25 | public Link link(PropertyChain absoluteCurrent) { 26 | linkedAbsoluteProperties.add(absoluteCurrent); 27 | return this; 28 | } 29 | 30 | private Optional> chooseProducer(Class type, Collection> linkedProducers) { 31 | Optional> fixedProducer = linkedProducers.stream().filter(Producer::isFixed).findFirst(); 32 | return fixedProducer.isPresent() ? fixedProducer : linkedProducers.stream().filter(type::isInstance).findFirst(); 33 | } 34 | 35 | @SuppressWarnings("unchecked") 36 | public Producer chooseProducer() { 37 | List> linkedProducers = linkedAbsoluteProperties.stream() 38 | .map(p -> (Producer) root.descendant(p).getLinkOrigin()) 39 | .collect(toList()); 40 | return TYPE_PRIORITY.stream().map(type -> chooseProducer(type, linkedProducers)) 41 | .filter(Optional::isPresent) 42 | .map(Optional::get) 43 | .findFirst() 44 | .orElseGet(() -> linkedProducers.iterator().next()); 45 | } 46 | 47 | public void link(Reference reference) { 48 | linkedAbsoluteProperties.addAll(reference.getLinker().linkedAbsoluteProperties); 49 | reference.setLinker(this); 50 | } 51 | 52 | public Stream> allLinkedReferences() { 53 | return references.stream(); 54 | } 55 | 56 | static class Reference { 57 | private final PropertyChain absoluteCurrent; 58 | private Link link; 59 | 60 | public Reference(PropertyChain absoluteCurrent) { 61 | this.absoluteCurrent = absoluteCurrent; 62 | } 63 | 64 | public static Reference defaultLinkerReference(Producer root, PropertyChain absoluteCurrent) { 65 | return new Reference(absoluteCurrent).setLinker(new Link(root).link(absoluteCurrent)); 66 | } 67 | 68 | public Link getLinker() { 69 | return link; 70 | } 71 | 72 | public Reference setLinker(Link link) { 73 | this.link = link; 74 | link.references.add(this); 75 | return this; 76 | } 77 | 78 | @Override 79 | public int hashCode() { 80 | return Objects.hash(Reference.class, absoluteCurrent); 81 | } 82 | 83 | @Override 84 | public boolean equals(Object obj) { 85 | return cast(obj, Reference.class) 86 | .map(another -> Objects.equals(absoluteCurrent, another.absoluteCurrent)) 87 | .orElseGet(() -> super.equals(obj)); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Producer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.PropertyWriter; 5 | 6 | import java.util.Optional; 7 | import java.util.function.BiFunction; 8 | import java.util.stream.Stream; 9 | 10 | import static com.github.leeonky.jfactory.Link.Reference.defaultLinkerReference; 11 | import static java.util.function.Function.identity; 12 | 13 | abstract class Producer { 14 | private final BeanClass type; 15 | private final ValueCache valueCache = new ValueCache<>(); 16 | 17 | protected Producer(BeanClass type) { 18 | this.type = type; 19 | } 20 | 21 | public BeanClass getType() { 22 | return type; 23 | } 24 | 25 | protected abstract T produce(); 26 | 27 | public T getValue() { 28 | return valueCache.cache(this::produce); 29 | } 30 | 31 | protected void doDependencies() { 32 | } 33 | 34 | protected void doLinks(Producer root, PropertyChain base) { 35 | } 36 | 37 | public void setChild(String property, Producer producer) { 38 | } 39 | 40 | public Optional> child(String property) { 41 | return Optional.empty(); 42 | } 43 | 44 | public Producer childOrDefault(String property) { 45 | return child(property).orElse(null); 46 | } 47 | 48 | public Producer descendant(PropertyChain property) { 49 | return property.access(this, (producer, subProperty) -> producer.child(subProperty) 50 | .orElseGet(() -> new ReadOnlyProducer<>(producer, subProperty)), identity()); 51 | } 52 | 53 | public void changeDescendant(PropertyChain property, BiFunction, String, Producer> producerFactory) { 54 | String tail = property.tail(); 55 | property.removeTail().access(this, Producer::childOrDefault, Optional::ofNullable).ifPresent(lastObjectProducer -> 56 | lastObjectProducer.changeChild(tail, producerFactory.apply(lastObjectProducer, tail))); 57 | } 58 | 59 | @SuppressWarnings("unchecked") 60 | public void changeChild(String property, Producer producer) { 61 | Producer origin = (Producer) childOrDefault(property); 62 | setChild(property, origin == null ? producer : origin.changeTo(producer)); 63 | } 64 | 65 | public BeanClass getPropertyWriterType(String property) { 66 | return getType().getPropertyWriter(property).getType(); 67 | } 68 | 69 | public Stream> allLinkerReferences(Producer root, PropertyChain absoluteCurrent) { 70 | return Stream.of(defaultLinkerReference(root, absoluteCurrent)); 71 | } 72 | 73 | public Optional subDefaultValueProducer(PropertyWriter property) { 74 | return Optional.empty(); 75 | } 76 | 77 | public Producer getLinkOrigin() { 78 | return this; 79 | } 80 | 81 | public Producer changeTo(Producer newProducer) { 82 | return newProducer.changeFrom(this); 83 | } 84 | 85 | protected Producer changeFrom(Producer producer) { 86 | return this; 87 | } 88 | 89 | protected Producer changeFrom(ObjectProducer producer) { 90 | return this; 91 | } 92 | 93 | protected Producer changeTo(DefaultValueProducer newProducer) { 94 | return this; 95 | } 96 | 97 | protected void setupAssociation(String association, RootInstance instance, ListPersistable cachedChildren) { 98 | } 99 | 100 | protected boolean isFixed() { 101 | return false; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/PropertyChainTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import org.junit.jupiter.api.Nested; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.github.leeonky.jfactory.PropertyChain.propertyChain; 7 | import static java.util.Optional.of; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | import static org.junit.jupiter.api.Assertions.assertFalse; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | class PropertyChainTest { 13 | 14 | @Test 15 | void is_chain_single() { 16 | assertTrue(propertyChain("a").isSingle()); 17 | assertTrue(propertyChain("[0]").isSingle()); 18 | assertFalse(propertyChain("a.b").isSingle()); 19 | } 20 | 21 | @Test 22 | void top_level_collection() { 23 | assertTrue(propertyChain("a[0]").isTopLevelPropertyCollection()); 24 | assertFalse(propertyChain("a[0].b").isTopLevelPropertyCollection()); 25 | assertFalse(propertyChain("a").isTopLevelPropertyCollection()); 26 | assertFalse(propertyChain("[0]").isTopLevelPropertyCollection()); 27 | assertFalse(propertyChain("a[0][1]").isTopLevelPropertyCollection()); 28 | } 29 | 30 | @Nested 31 | class ToString { 32 | 33 | @Test 34 | void single_chain() { 35 | assertToString("value"); 36 | } 37 | 38 | @Test 39 | void root_collection() { 40 | assertToString("[1]"); 41 | } 42 | 43 | @Test 44 | void full_chain() { 45 | assertToString("b[9]"); 46 | assertToString("a.b[9].c"); 47 | assertToString("b[9].c"); 48 | assertToString("b.c[9]"); 49 | } 50 | 51 | private void assertToString(String chain) { 52 | assertThat(propertyChain(chain).toString()).isEqualTo(chain); 53 | } 54 | } 55 | 56 | @Nested 57 | class HashAndEqual { 58 | 59 | @Test 60 | void equal_hash_with_equal_content() { 61 | assertHashEqual("a"); 62 | assertHashEqual("a.b"); 63 | assertHashEqual("[0]"); 64 | assertHashEqual("a.b[0].c"); 65 | assertHashEqual("b[0].c"); 66 | assertHashEqual("b[0]"); 67 | 68 | assertThat(propertyChain("p1").hashCode()).isNotEqualTo(propertyChain("p2").hashCode()); 69 | } 70 | 71 | @Test 72 | void equal_with_equal_content() { 73 | assertContentEqual("a"); 74 | assertContentEqual("a.b"); 75 | assertContentEqual("[0]"); 76 | assertContentEqual("a.b[0].c"); 77 | assertContentEqual("b[0].c"); 78 | assertContentEqual("b[0]"); 79 | 80 | assertThat(propertyChain("p1")).isNotEqualTo(propertyChain("p2")); 81 | } 82 | 83 | private void assertContentEqual(String value) { 84 | assertThat(propertyChain(value)).isEqualTo(propertyChain(value)); 85 | } 86 | 87 | private void assertHashEqual(String value) { 88 | assertThat(propertyChain(value).hashCode()).isEqualTo(propertyChain(value).hashCode()); 89 | } 90 | } 91 | 92 | @Nested 93 | class SubShould { 94 | 95 | @Test 96 | void return_empty_when_not_matches() { 97 | assertThat(propertyChain("a").sub(propertyChain("b"))).isEmpty(); 98 | assertThat(propertyChain("a.x").sub(propertyChain("a.x.y"))).isEmpty(); 99 | } 100 | 101 | @Test 102 | void return_the_left_chain() { 103 | assertThat(propertyChain("a.b").sub(propertyChain("a"))).isEqualTo(of(propertyChain("b"))); 104 | assertThat(propertyChain("a[1].b").sub(propertyChain("a[1]"))).isEqualTo(of(propertyChain("b"))); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/SpecClassFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.Converter; 5 | 6 | import java.lang.reflect.Method; 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.function.Consumer; 10 | import java.util.function.Supplier; 11 | import java.util.stream.Stream; 12 | 13 | import static com.github.leeonky.util.Classes.newInstance; 14 | import static com.github.leeonky.util.Suppressor.run; 15 | 16 | class SpecClassFactory extends ObjectFactory { 17 | private final Class> specClass; 18 | private final Supplier> base; 19 | 20 | public SpecClassFactory(Class> specClass, FactorySet factorySet, boolean globalSpec) { 21 | super(BeanClass.create(newInstance(specClass).getType()), factorySet); 22 | this.specClass = specClass; 23 | base = guessBaseFactory(factorySet, globalSpec); 24 | registerTraits(); 25 | constructor(getBase()::create); 26 | } 27 | 28 | private Supplier> guessBaseFactory(FactorySet factorySet, boolean globalSpec) { 29 | if (!globalSpec) 30 | return () -> factorySet.queryObjectFactory(getType()); 31 | ObjectFactory typeBaseFactory = factorySet.queryObjectFactory(getType()); // DO NOT INLINE 32 | return () -> typeBaseFactory; 33 | } 34 | 35 | @Override 36 | protected Spec createSpec() { 37 | return newInstance(specClass); 38 | } 39 | 40 | private void registerTraits() { 41 | Stream.of(specClass.getMethods()) 42 | .filter(this::isTraitMethod) 43 | .forEach(method -> spec(getTraitName(method), instance -> run(() -> 44 | method.invoke(instance.spec(), convertParams(method, instance.traitParams()))))); 45 | } 46 | 47 | private Object[] convertParams(Method method, Object[] traitParams) { 48 | return new ArrayList() {{ 49 | for (int i = 0; i < method.getParameterTypes().length; i++) 50 | add(Converter.getInstance().convert(method.getParameterTypes()[i], traitParams[i])); 51 | }}.toArray(); 52 | } 53 | 54 | private boolean isTraitMethod(Method method) { 55 | return method.getAnnotation(Trait.class) != null; 56 | } 57 | 58 | private String getTraitName(Method method) { 59 | Trait annotation = method.getAnnotation(Trait.class); 60 | return annotation.value().isEmpty() ? method.getName() : annotation.value(); 61 | } 62 | 63 | @Override 64 | protected void collectSubSpec(Instance instance) { 65 | getBase().collectSpec(Collections.emptyList(), instance); 66 | collectClassSpec(instance, Spec::main); 67 | } 68 | 69 | protected void collectClassSpec(Instance instance, Consumer> consumer) { 70 | if (instance.spec().getClass().equals(specClass)) 71 | consumer.accept(instance.spec()); 72 | else { 73 | Spec spec = createSpec().setInstance(instance); 74 | consumer.accept(spec); 75 | instance.spec().append(spec); 76 | } 77 | } 78 | 79 | @Override 80 | public ObjectFactory getBase() { 81 | return base.get(); 82 | } 83 | 84 | public Class> getSpecClass() { 85 | return specClass; 86 | } 87 | 88 | @Override 89 | @SuppressWarnings("unchecked") 90 | protected Supplier fallback(String name, Supplier fallback) { 91 | return () -> specClass.getSuperclass().equals(Spec.class) ? getBase().queryTransformer(name, fallback) 92 | : factorySet.querySpecClassFactory((Class>) specClass.getSuperclass()) 93 | .queryTransformer(name, fallback); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/CollectionExpression.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.CollectionHelper; 4 | import com.github.leeonky.util.Property; 5 | 6 | import java.util.ArrayList; 7 | import java.util.LinkedHashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | 12 | import static com.github.leeonky.util.BeanClass.cast; 13 | 14 | class CollectionExpression extends Expression

{ 15 | private final Map> children = new LinkedHashMap<>(); 16 | 17 | public CollectionExpression(Property

property, int index, Expression elementExpression) { 18 | super(property); 19 | children.put(index, elementExpression); 20 | } 21 | 22 | @Override 23 | protected boolean isPropertyMatch(Object collection) { 24 | if (collection == null) 25 | return false; 26 | List elements = CollectionHelper.toStream(collection).collect(Collectors.toList()); 27 | return elements.size() > children.keySet().stream().reduce(Integer::max).orElse(0) && 28 | children.entrySet().stream().allMatch(e -> isMatch(e.getValue(), elements.get(e.getKey()))); 29 | } 30 | 31 | private boolean isMatch(Expression expression, Object value) { 32 | return value != null && !expression.intently && expression.isPropertyMatch(value); 33 | } 34 | 35 | @Override 36 | @SuppressWarnings("unchecked") 37 | public Producer buildProducer(JFactory jFactory, Producer

parent) { 38 | CollectionProducer producer = cast(parent.childOrDefault(property.getName()), 39 | CollectionProducer.class).orElseThrow(IllegalArgumentException::new); 40 | groupByAdjustedPositiveAndNegativeIndexExpression(producer).forEach((index, expressions) -> 41 | producer.changeChild(index.toString(), merge(expressions).buildProducer(jFactory, producer))); 42 | return producer; 43 | } 44 | 45 | private Map>> groupByAdjustedPositiveAndNegativeIndexExpression( 46 | CollectionProducer collectionProducer) { 47 | Map>> result = new LinkedHashMap<>(); 48 | for (Map.Entry> entry : children.entrySet()) { 49 | int index = entry.getKey(); 50 | int addedProducerCount = collectionProducer.fillCollectionWithDefaultValue(index); 51 | if (index < 0) { 52 | index = collectionProducer.childrenCount() + index; 53 | result = adjustIndexByInserted(result, addedProducerCount); 54 | } 55 | result.computeIfAbsent(index, k -> new ArrayList<>()).add(entry.getValue()); 56 | } 57 | return result; 58 | } 59 | 60 | private LinkedHashMap>> adjustIndexByInserted( 61 | Map>> result, int addedProducerCount) { 62 | return result.entrySet().stream().collect(LinkedHashMap::new, 63 | (m, e) -> m.put(e.getKey() + addedProducerCount, e.getValue()), LinkedHashMap::putAll); 64 | } 65 | 66 | @Override 67 | public Expression

mergeTo(Expression

newExpression) { 68 | return newExpression.mergeFrom(this); 69 | } 70 | 71 | @Override 72 | @SuppressWarnings("unchecked") 73 | protected Expression

mergeFrom(CollectionExpression origin) { 74 | children.forEach((index, expression) -> 75 | origin.children.put(index, origin.children.containsKey(index) ? 76 | origin.children.get(index).mergeTo((Expression) expression) : (Expression) expression)); 77 | return origin; 78 | } 79 | 80 | @Override 81 | protected Expression

setIntently(boolean intently) { 82 | if (intently) 83 | children.values().forEach(c -> c.setIntently(true)); 84 | return super.setIntently(intently); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/Spec.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.ArrayList; 6 | import java.util.LinkedHashSet; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.function.BiConsumer; 10 | import java.util.stream.Collectors; 11 | 12 | import static com.github.leeonky.jfactory.PropertyChain.propertyChain; 13 | import static java.util.stream.Collectors.toList; 14 | import static java.util.stream.Stream.concat; 15 | import static java.util.stream.Stream.of; 16 | 17 | public class Spec { 18 | private final List>> operations = new ArrayList<>(); 19 | private final Set.IsSpec>> invalidIsSpecs = new LinkedHashSet<>(); 20 | 21 | private Instance instance; 22 | private Class type = null; 23 | 24 | public void main() { 25 | } 26 | 27 | public PropertySpec property(String property) { 28 | return new PropertySpec<>(this, propertyChain(property)); 29 | } 30 | 31 | Spec append(BiConsumer> operation) { 32 | operations.add(operation); 33 | return this; 34 | } 35 | 36 | void apply(JFactory jFactory, ObjectProducer producer) { 37 | operations.forEach(o -> o.accept(jFactory, producer)); 38 | type = producer.getType().getType(); 39 | if (!invalidIsSpecs.isEmpty()) 40 | throw new InvalidSpecException("Invalid property spec:\n\t" 41 | + invalidIsSpecs.stream().map(PropertySpec.IsSpec::getPosition).collect(Collectors.joining("\n\t")) 42 | + "\nShould finish method chain with `and` or `which`:\n" 43 | + "\tproperty().from().which()\n" 44 | + "\tproperty().from().and()\n" 45 | + "Or use property().is() to create object with only spec directly."); 46 | } 47 | 48 | @SuppressWarnings("unchecked") 49 | public Class getType() { 50 | return getClass().equals(Spec.class) ? type : 51 | (Class) BeanClass.create(getClass()).getSuper(Spec.class).getTypeArguments(0) 52 | .orElseThrow(() -> new IllegalStateException("Cannot guess type via generic type argument, please override Spec::getType")) 53 | .getType(); 54 | } 55 | 56 | protected String getName() { 57 | return getClass().getSimpleName(); 58 | } 59 | 60 | public Spec link(String property, String... others) { 61 | List linkProperties = concat(of(property), of(others)).map(PropertyChain::propertyChain).collect(toList()); 62 | append((jFactory, objectProducer) -> objectProducer.link(linkProperties)); 63 | return this; 64 | } 65 | 66 | Spec setInstance(Instance instance) { 67 | this.instance = instance; 68 | return this; 69 | } 70 | 71 | public

P param(String key) { 72 | return instance.param(key); 73 | } 74 | 75 | public

P param(String key, P defaultValue) { 76 | return instance.param(key, defaultValue); 77 | } 78 | 79 | public Arguments params(String property) { 80 | return instance.params(property); 81 | } 82 | 83 | public Arguments params() { 84 | return instance.params(); 85 | } 86 | 87 | public Instance instance() { 88 | return instance; 89 | } 90 | 91 | > PropertySpec.IsSpec newIsSpec(Class specClass, PropertySpec propertySpec) { 92 | PropertySpec.IsSpec isSpec = propertySpec.new IsSpec(specClass); 93 | invalidIsSpecs.add(isSpec); 94 | return isSpec; 95 | } 96 | 97 | void consume(PropertySpec.IsSpec> isSpec) { 98 | invalidIsSpecs.remove(isSpec); 99 | } 100 | 101 | void append(Spec spec) { 102 | operations.addAll(spec.operations); 103 | invalidIsSpecs.addAll(spec.invalidIsSpecs); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/PropertyChain.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import java.util.*; 4 | import java.util.function.BiFunction; 5 | import java.util.function.Function; 6 | import java.util.stream.Collectors; 7 | 8 | import static java.util.Optional.empty; 9 | import static java.util.Optional.of; 10 | import static java.util.stream.IntStream.range; 11 | 12 | //TODO alias should not use this class 13 | class PropertyChain { 14 | private final List property; 15 | 16 | private PropertyChain(String property) { 17 | // TODO refactor 18 | this.property = Arrays.stream(property.replaceAll("\\[\\$]", "###").split("[\\[\\].]")) 19 | .filter(s -> !s.isEmpty()) 20 | .map(s -> s.replaceAll("###", "[\\$]")) 21 | .map(this::tryToNumber).collect(Collectors.toList()); 22 | } 23 | 24 | public PropertyChain(List propertyChain) { 25 | property = new ArrayList<>(propertyChain); 26 | } 27 | 28 | public static PropertyChain propertyChain(String property) { 29 | return new PropertyChain(property); 30 | } 31 | 32 | private Object tryToNumber(String s) { 33 | try { 34 | return Integer.valueOf(s); 35 | } catch (Exception ignore) { 36 | return s; 37 | } 38 | } 39 | 40 | public boolean isTopLevelPropertyCollection() { 41 | return property.size() == 2 && (property.get(1) instanceof Integer); 42 | } 43 | 44 | public boolean isSingle() { 45 | return property.size() == 1; 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | ArrayList objects = new ArrayList<>(); 51 | objects.add(PropertyChain.class); 52 | objects.addAll(property); 53 | return Objects.hash(objects.toArray()); 54 | } 55 | 56 | @Override 57 | public boolean equals(Object obj) { 58 | return obj instanceof PropertyChain && Objects.equals(property, ((PropertyChain) obj).property); 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return property.stream().map(c -> { 64 | if (c instanceof Integer) 65 | return String.format("[%d]", c); 66 | return c.toString(); 67 | }).collect(Collectors.joining(".")).replace(".[", "[").replace("].!", "]!").replace("].(", "]("); 68 | } 69 | 70 | public T access(Producer producer, BiFunction, String, Producer> accessor, 71 | Function, T> returnWrapper) { 72 | if (property.isEmpty()) 73 | return returnWrapper.apply(producer); 74 | String head = property.get(0).toString(); 75 | return new PropertyChain(property.subList(1, property.size())) 76 | .access(accessor.apply(producer, head), accessor, returnWrapper); 77 | } 78 | 79 | public String tail() { 80 | return property.get(property.size() - 1).toString(); 81 | } 82 | 83 | public PropertyChain removeTail() { 84 | return new PropertyChain(property.subList(0, property.size() - 1)); 85 | } 86 | 87 | public PropertyChain concat(PropertyChain subChain) { 88 | List chain = new ArrayList<>(property); 89 | chain.addAll(subChain.property); 90 | return new PropertyChain(chain); 91 | } 92 | 93 | public PropertyChain concat(String node) { 94 | return concat(propertyChain(node)); 95 | } 96 | 97 | public Optional sub(PropertyChain propertyChain) { 98 | if (contains(propertyChain)) 99 | return of(new PropertyChain(property.subList(propertyChain.property.size(), property.size()))); 100 | return empty(); 101 | } 102 | 103 | private boolean contains(PropertyChain propertyChain) { 104 | return propertyChain.property.size() <= property.size() 105 | && range(0, propertyChain.property.size()) 106 | .allMatch(i -> Objects.equals(propertyChain.property.get(i), property.get(i))); 107 | } 108 | 109 | public Object head() { 110 | return property.get(0); 111 | } 112 | 113 | public PropertyChain removeHead() { 114 | return new PropertyChain(property.stream().skip(1).collect(Collectors.toList())); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/KeyValueCollection.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.Property; 5 | 6 | import java.util.Collection; 7 | import java.util.LinkedHashMap; 8 | import java.util.Map; 9 | import java.util.Objects; 10 | import java.util.stream.Collectors; 11 | 12 | public class KeyValueCollection { 13 | private final FactorySet factorySet; 14 | private final Map keyValues = new LinkedHashMap<>(); 15 | 16 | public KeyValueCollection(FactorySet factorySet) { 17 | this.factorySet = factorySet; 18 | } 19 | 20 | public void insertAll(KeyValueCollection another) { 21 | LinkedHashMap merged = new LinkedHashMap() {{ 22 | putAll(another.keyValues); 23 | putAll(keyValues); 24 | }}; 25 | keyValues.clear(); 26 | keyValues.putAll(merged); 27 | } 28 | 29 | public void appendAll(KeyValueCollection another) { 30 | keyValues.putAll(another.keyValues); 31 | } 32 | 33 | Builder apply(Builder builder) { 34 | for (KeyValue keyValue : keyValues.values()) 35 | builder = keyValue.apply(builder); 36 | return builder; 37 | } 38 | 39 | // TODO remove arg type 40 | Collection> expressions(BeanClass type, ObjectFactory objectFactory) { 41 | return keyValues.values().stream().map(keyValue -> keyValue.createExpression(type, objectFactory)) 42 | .collect(Collectors.groupingBy(Expression::getProperty)).values().stream() 43 | .map(Expression::merge) 44 | .collect(Collectors.toList()); 45 | } 46 | 47 | Expression createExpression(Property property, TraitsSpec traitsSpec, Property parentProperty, ObjectFactory objectFactory) { 48 | if (isSingleValue()) { 49 | Object value = transform(property, parentProperty, objectFactory); 50 | if (createOrLinkAnyExist(value)) 51 | return new SubObjectExpression<>(new KeyValueCollection(factorySet), traitsSpec, property, objectFactory); 52 | return new SingleValueExpression<>(value, traitsSpec, property); 53 | } 54 | return new SubObjectExpression<>(this, traitsSpec, property, objectFactory); 55 | } 56 | 57 | private boolean createOrLinkAnyExist(Object value) { 58 | return value instanceof Map && ((Map) value).isEmpty(); 59 | } 60 | 61 | private Object transform(Property property, Property parentProperty, ObjectFactory objectFactory) { 62 | String transformerName = parentProperty != null && property.getBeanType().isCollection() 63 | ? parentProperty.getName() + "[]" : property.getName(); 64 | return objectFactory.transform(transformerName, keyValues.values().iterator().next().getValue()); 65 | } 66 | 67 | private boolean isSingleValue() { 68 | return keyValues.size() == 1 && keyValues.values().iterator().next().nullKey(); 69 | } 70 | 71 | public KeyValueCollection append(String key, Object value) { 72 | keyValues.put(key, new KeyValue(key, value, factorySet)); 73 | return this; 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | return Objects.hash(KeyValueCollection.class, keyValues.hashCode()); 79 | } 80 | 81 | @Override 82 | public boolean equals(Object obj) { 83 | return BeanClass.cast(obj, KeyValueCollection.class) 84 | .map(keyValueCollection -> Objects.equals(keyValues, keyValueCollection.keyValues)) 85 | .orElseGet(() -> super.equals(obj)); 86 | } 87 | 88 | public Matcher matcher(BeanClass type, ObjectFactory objectFactory) { 89 | return new Matcher<>(type, objectFactory); 90 | } 91 | 92 | public class Matcher { 93 | private final Collection> expressions; 94 | 95 | Matcher(BeanClass type, ObjectFactory objectFactory) { 96 | expressions = expressions(type, objectFactory); 97 | } 98 | 99 | public boolean matches(T object) { 100 | return expressions.stream().allMatch(e -> e.isMatch(object)); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/CollectionProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.PropertyWriter; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.function.Function; 10 | 11 | import static java.lang.Integer.max; 12 | import static java.lang.Integer.valueOf; 13 | import static java.util.stream.Collectors.toList; 14 | import static java.util.stream.IntStream.range; 15 | 16 | class CollectionProducer extends Producer { 17 | private final List> children = new ArrayList<>(); 18 | private final Function> placeholderFactory; 19 | private final Function> subDefaultValueProducerFactory; 20 | 21 | public CollectionProducer(BeanClass parentType, BeanClass collectionType, 22 | SubInstance instance, FactorySet factorySet) { 23 | super(collectionType); 24 | CollectionInstance collection = instance.inCollection(); 25 | BeanClass elementType = collectionType.getElementType(); 26 | placeholderFactory = index -> new DefaultValueFactoryProducer<>(parentType, 27 | factorySet.getDefaultValueBuilder(elementType), collection.element(index)); 28 | subDefaultValueProducerFactory = index -> factorySet.queryDefaultValueBuilder(elementType) 29 | .map(builder -> new DefaultValueFactoryProducer<>(parentType, builder, collection.element(valueOf(index)))); 30 | } 31 | 32 | @Override 33 | @SuppressWarnings("unchecked") 34 | protected C produce() { 35 | return (C) getType().createCollection(children.stream().map(Producer::produce).collect(toList())); 36 | } 37 | 38 | @Override 39 | public Optional> child(String property) { 40 | int index = valueOf(property); 41 | index = transformNegativeIndex(index); 42 | return Optional.ofNullable(index < children.size() ? children.get(index) : null); 43 | } 44 | 45 | @Override 46 | public void setChild(String property, Producer producer) { 47 | int index = valueOf(property); 48 | fillCollectionWithDefaultValue(index); 49 | children.set(transformNegativeIndex(index), producer); 50 | } 51 | 52 | private int transformNegativeIndex(int index) { 53 | if (index < 0) 54 | index = children.size() + index; 55 | return index; 56 | } 57 | 58 | public int fillCollectionWithDefaultValue(int index) { 59 | int changed = 0; 60 | if (index >= 0) { 61 | for (int i = children.size(); i <= index; i++, changed++) 62 | children.add(placeholderFactory.apply(i)); 63 | } else { 64 | int count = max(children.size(), -index) - children.size(); 65 | for (int i = 0; i < count; i++, changed++) 66 | children.add(i, placeholderFactory.apply(i)); 67 | } 68 | return changed; 69 | } 70 | 71 | @Override 72 | public Producer childOrDefault(String property) { 73 | int index = valueOf(property); 74 | fillCollectionWithDefaultValue(index); 75 | return children.get(transformNegativeIndex(index)); 76 | } 77 | 78 | @Override 79 | protected void doDependencies() { 80 | children.forEach(Producer::doDependencies); 81 | } 82 | 83 | @Override 84 | protected void doLinks(Producer root, PropertyChain base) { 85 | range(0, children.size()).forEach(i -> 86 | children.get(i).doLinks(root, base.concat(String.valueOf(i)))); 87 | } 88 | 89 | @Override 90 | public Optional subDefaultValueProducer(PropertyWriter property) { 91 | return subDefaultValueProducerFactory.apply(property.getName()); 92 | } 93 | 94 | @Override 95 | protected void setupAssociation(String association, RootInstance instance, ListPersistable cachedChildren) { 96 | children.stream().filter(ObjectProducer.class::isInstance).map(ObjectProducer.class::cast).forEach(objectProducer -> 97 | objectProducer.setupAssociation(association, instance, cachedChildren)); 98 | 99 | } 100 | 101 | public int childrenCount() { 102 | return children.size(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/FactorySet.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.Classes; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Optional; 9 | import java.util.function.Consumer; 10 | 11 | class FactorySet { 12 | public final TypeSequence typeSequence = new TypeSequence(); 13 | private final DefaultValueFactories defaultValueFactories = new DefaultValueFactories(); 14 | private final Map, ObjectFactory> objectFactories = new HashMap<>(); 15 | private final Map, SpecClassFactory> specClassFactoriesWithType = new HashMap<>(); 16 | private final Map> specClassFactoriesWithName = new HashMap<>(); 17 | 18 | @SuppressWarnings("unchecked") 19 | public ObjectFactory queryObjectFactory(BeanClass type) { 20 | return (ObjectFactory) objectFactories.computeIfAbsent(type, 21 | key -> new ObjectFactory<>(key, this)); 22 | } 23 | 24 | public > void registerSpecClassFactory(Class specClass) { 25 | Spec spec = Classes.newInstance(specClass); 26 | boolean globalSpec = isGlobalSpec(specClass); 27 | SpecClassFactory specClassFactory = specClassFactoriesWithType.computeIfAbsent(specClass, 28 | type -> new SpecClassFactory<>(specClass, this, globalSpec)); 29 | specClassFactoriesWithName.put(spec.getName(), specClassFactory); 30 | if (globalSpec) 31 | registerGlobalSpec(specClassFactory); 32 | if (!specClass.getSuperclass().equals(Spec.class)) 33 | registerSpecClassFactory((Class) specClass.getSuperclass()); 34 | } 35 | 36 | private > boolean isGlobalSpec(Class specClass) { 37 | return specClass.getAnnotation(Global.class) != null; 38 | } 39 | 40 | private void registerGlobalSpec(SpecClassFactory specClassFactory) { 41 | Class> specClass = specClassFactory.getSpecClass(); 42 | if (specClassFactory.getBase() instanceof SpecClassFactory) 43 | throw new IllegalArgumentException(String.format("More than one @Global Spec class `%s` and `%s`", 44 | ((SpecClassFactory) specClassFactory.getBase()).getSpecClass().getName(), specClass.getName())); 45 | if (!specClass.getSuperclass().equals(Spec.class)) 46 | throw new IllegalArgumentException(String.format("Global Spec %s should not have super Spec %s.", 47 | specClass.getName(), specClass.getSuperclass().getName())); 48 | objectFactories.put(specClassFactory.getType(), specClassFactory); 49 | } 50 | 51 | public void removeGlobalSpec(BeanClass type) { 52 | objectFactories.computeIfPresent(type, (key, factory) -> factory.getBase()); 53 | } 54 | 55 | @SuppressWarnings("unchecked") 56 | public SpecClassFactory querySpecClassFactory(String specName) { 57 | return (SpecClassFactory) specClassFactoriesWithName.computeIfAbsent(specName, key -> { 58 | throw new IllegalArgumentException("Spec `" + specName + "` not exist"); 59 | }); 60 | } 61 | 62 | @SuppressWarnings("unchecked") 63 | public SpecClassFactory querySpecClassFactory(Class> specClass) { 64 | return (SpecClassFactory) specClassFactoriesWithType.computeIfAbsent(specClass, key -> { 65 | throw new IllegalArgumentException("Spec `" + specClass.getName() + "` not exist"); 66 | }); 67 | } 68 | 69 | public Optional> queryDefaultValueBuilder(BeanClass type) { 70 | return defaultValueFactories.query(type.getType()); 71 | } 72 | 73 | public DefaultValueFactory getDefaultValueBuilder(BeanClass type) { 74 | return queryDefaultValueBuilder(type).orElseGet(() -> new DefaultValueFactories.DefaultTypeFactory<>(type)); 75 | } 76 | 77 | public int nextSequence(Class type) { 78 | return typeSequence.generate(type); 79 | } 80 | 81 | public > SpecFactory createSpecFactory(Class specClass, Consumer trait) { 82 | return new SpecFactory<>(Classes.newInstance(specClass), this, trait); 83 | } 84 | 85 | public void registerDefaultValueFactory(Class type, DefaultValueFactory factory) { 86 | defaultValueFactories.register(type, factory); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/KeyValue.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.Property; 5 | 6 | import java.util.Objects; 7 | import java.util.Optional; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | import static java.lang.Integer.parseInt; 12 | 13 | //TODO use a parser to parse this 14 | class KeyValue { 15 | private static final String PATTERN_PROPERTY = "([^.(!\\[]+)"; 16 | private static final String PATTERN_COLLECTION_INDEX = "(\\[(-?\\d+)])?"; 17 | private static final String PATTERN_SPEC_TRAIT_WORD = "[^, )]"; 18 | private static final String PATTERN_TRAIT = "((" + PATTERN_SPEC_TRAIT_WORD + "+[, ])(" + PATTERN_SPEC_TRAIT_WORD + "+[, ])*)?"; 19 | private static final String PATTERN_SPEC = "(" + PATTERN_SPEC_TRAIT_WORD + "+)"; 20 | private static final String PATTERN_TRAIT_SPEC = "(\\(" + PATTERN_TRAIT + PATTERN_SPEC + "\\))?"; 21 | private static final String PATTERN_CLAUSE = "(\\." + "(.+)" + ")?"; 22 | private static final String PATTERN_INTENTLY = "(!)?"; 23 | private static final int GROUP_PROPERTY = 1; 24 | private static final int GROUP_COLLECTION_INDEX = 3; 25 | private static final int GROUP_TRAIT = 5; 26 | private static final int GROUP_SPEC = 8; 27 | private static final int GROUP_INTENTLY = 9; 28 | private static final int GROUP_CLAUSE = 11; 29 | private final String key; 30 | private final Object value; 31 | private final FactorySet factorySet; 32 | 33 | public KeyValue(String key, Object value, FactorySet factorySet) { 34 | this.key = key; 35 | this.value = value; 36 | this.factorySet = factorySet; 37 | } 38 | 39 | public Expression createExpression(BeanClass beanClass, ObjectFactory objectFactory) { 40 | Matcher matcher = parse(beanClass); 41 | Property property = beanClass.getProperty(matcher.group(GROUP_PROPERTY)); 42 | return hasIndex(matcher).map(index -> createCollectionExpression(matcher, property, index, objectFactory)) 43 | .orElseGet(() -> createSubExpression(matcher, property, null, objectFactory)); 44 | } 45 | 46 | private Expression createCollectionExpression(Matcher matcher, Property property, String index, ObjectFactory objectFactory) { 47 | return new CollectionExpression<>(property, parseInt(index), 48 | createSubExpression(matcher, property.getWriter().getType().getProperty(index), property, objectFactory)); 49 | } 50 | 51 | private Optional hasIndex(Matcher matcher) { 52 | return Optional.ofNullable(matcher.group(GROUP_COLLECTION_INDEX)); 53 | } 54 | 55 | private Expression createSubExpression(Matcher matcher, Property property, Property parentProperty, ObjectFactory objectFactory) { 56 | KeyValueCollection properties = new KeyValueCollection(factorySet).append(matcher.group(GROUP_CLAUSE), value); 57 | TraitsSpec traitsSpec = new TraitsSpec(matcher.group(GROUP_TRAIT) != null ? 58 | matcher.group(GROUP_TRAIT).split(", |,| ") : new String[0], matcher.group(GROUP_SPEC)); 59 | return properties.createExpression(property, traitsSpec, parentProperty, objectFactory).setIntently(matcher.group(GROUP_INTENTLY) != null); 60 | } 61 | 62 | private Matcher parse(BeanClass beanClass) { 63 | Matcher matcher = Pattern.compile(PATTERN_PROPERTY + PATTERN_COLLECTION_INDEX + 64 | PATTERN_TRAIT_SPEC + PATTERN_INTENTLY + PATTERN_CLAUSE).matcher(key); 65 | if (!matcher.matches()) 66 | throw new IllegalArgumentException(String.format("Invalid property `%s` for %s creation.", 67 | key, beanClass.getName())); 68 | return matcher; 69 | } 70 | 71 | public Builder apply(Builder builder) { 72 | return builder.property(key, value); 73 | } 74 | 75 | public boolean nullKey() { 76 | return key == null; 77 | } 78 | 79 | @Override 80 | public int hashCode() { 81 | return Objects.hash(KeyValue.class, key, value); 82 | } 83 | 84 | @Override 85 | public boolean equals(Object another) { 86 | return BeanClass.cast(another, KeyValue.class) 87 | .map(keyValue -> Objects.equals(key, keyValue.key) && Objects.equals(value, keyValue.value)) 88 | .orElseGet(() -> super.equals(another)); 89 | } 90 | 91 | public Object getValue() { 92 | return value; 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/bug/PropertyShouldOverridePropertyInSpec.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory.bug; 2 | 3 | import com.github.leeonky.jfactory.JFactory; 4 | import com.github.leeonky.jfactory.Spec; 5 | import com.github.leeonky.util.Classes; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import lombok.experimental.Accessors; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static com.github.leeonky.dal.Assertions.expect; 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | 17 | public class PropertyShouldOverridePropertyInSpec { 18 | private final JFactory jFactory = new JFactory() {{ 19 | Classes.subTypesOf(Spec.class, "com.github.leeonky.jfactory.bug").forEach(c -> register((Class) c)); 20 | }}; 21 | 22 | @Test 23 | void property_should_override_property_in_spec() { 24 | jFactory.factory(ProductPriceBook.class).spec(instance -> instance.spec().property("priceBook") 25 | .byFactory(builder -> builder.property("code", "amazon"))); 26 | 27 | ProductPriceBook productPriceBook = jFactory.type(ProductPriceBook.class).property("priceBook.code", "ebay").create(); 28 | expect(productPriceBook).should("priceBook.code: 'ebay'"); 29 | } 30 | 31 | @Test 32 | void should_merge_property_in_spec_and_input_when_property_inside_spec_is_a_query_build_and_use_merged_property_to_query() { 33 | jFactory.type(PriceBook.class).property("code", "amazon").create(); 34 | jFactory.type(PriceBook.class).property("name", "book").create(); 35 | PriceBook priceBook = jFactory.type(PriceBook.class).property("code", "amazon").property("name", "book").create(); 36 | 37 | jFactory.factory(ProductPriceBook.class).spec(instance -> instance.spec().property("priceBook") 38 | .byFactory(builder -> builder.property("code", "amazon"))); 39 | 40 | ProductPriceBook book1 = jFactory.type(ProductPriceBook.class).property("priceBook.name", "book").create(); 41 | 42 | assertThat(book1.priceBook).isSameAs(priceBook); 43 | } 44 | 45 | @Test 46 | void should_merge_property_in_spec_and_input_when_property_inside_spec_is_a_query_and_create_with_merged_property() { 47 | jFactory.factory(ProductPriceBook.class).spec(instance -> instance.spec().property("priceBook") 48 | .byFactory(builder -> builder.property("code", "amazon"))); 49 | 50 | ProductPriceBook book1 = jFactory.type(ProductPriceBook.class).property("priceBook.name", "book1").create(); 51 | ProductPriceBook book2 = jFactory.type(ProductPriceBook.class).property("priceBook.name", "book2").create(); 52 | 53 | expect(book1.priceBook).should(": {code: amazon name: book1}"); 54 | expect(book2.priceBook).should(": {code: amazon name: book2}"); 55 | } 56 | 57 | @Test 58 | void should_use_spec_in_property_override_origin_builder_spec() { 59 | jFactory.factory(ProductPriceBook.class).spec(instance -> instance.spec().property("priceBook") 60 | .from(PriceBookSpecInSpec.class).and(builder -> builder)); 61 | 62 | ProductPriceBook productPriceBook = jFactory.type(ProductPriceBook.class).property("priceBook(PriceBookSpecInProperty).rate", 10).create(); 63 | assertThat(productPriceBook.priceBook.getName()).isNotEqualTo("from-spec"); 64 | expect(productPriceBook.priceBook).match("{code: amazon rate: 10}"); 65 | } 66 | 67 | @Getter 68 | @Setter 69 | @Accessors(chain = true) 70 | public static class PriceBook { 71 | private String name, code; 72 | private int rate; 73 | } 74 | 75 | @Getter 76 | @Setter 77 | @Accessors(chain = true) 78 | public static class Product { 79 | private String productName, sku; 80 | private List productPriceBooks = new ArrayList<>(); 81 | } 82 | 83 | @Getter 84 | @Setter 85 | @Accessors(chain = true) 86 | public static class ProductPriceBook { 87 | private Product product; 88 | private PriceBook priceBook; 89 | } 90 | 91 | public static class PriceBookSpecInSpec extends Spec { 92 | 93 | @Override 94 | public void main() { 95 | property("name").value("from-spec"); 96 | } 97 | } 98 | 99 | public static class PriceBookSpecInProperty extends Spec { 100 | 101 | @Override 102 | public void main() { 103 | property("code").value("amazon"); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/JFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.PropertyWriter; 5 | 6 | import java.util.*; 7 | import java.util.function.Consumer; 8 | import java.util.function.Predicate; 9 | 10 | public class JFactory { 11 | final AliasSetStore aliasSetStore = new AliasSetStore(); 12 | private final FactorySet factorySet = new FactorySet(); 13 | private final DataRepository dataRepository; 14 | private final Set>> ignoreDefaultValues = new LinkedHashSet<>(); 15 | 16 | public JFactory() { 17 | dataRepository = new MemoryDataRepository(); 18 | } 19 | 20 | public JFactory(DataRepository dataRepository) { 21 | this.dataRepository = dataRepository; 22 | } 23 | 24 | public DataRepository getDataRepository() { 25 | return dataRepository; 26 | } 27 | 28 | public Factory factory(Class type) { 29 | return factory(BeanClass.create(type)); 30 | } 31 | 32 | public Factory factory(BeanClass type) { 33 | return factorySet.queryObjectFactory(type); 34 | } 35 | 36 | public Builder type(Class type) { 37 | return type(BeanClass.create(type)); 38 | } 39 | 40 | public Builder type(BeanClass type) { 41 | return new DefaultBuilder<>(factorySet.queryObjectFactory(type), this); 42 | } 43 | 44 | public Builder type(TypeReference type) { 45 | return type(type.getType()); 46 | } 47 | 48 | public > Builder spec(Class specClass) { 49 | return new DefaultBuilder<>((ObjectFactory) specFactory(specClass), this); 50 | } 51 | 52 | public > Builder spec(Class specClass, Consumer trait) { 53 | return new DefaultBuilder<>(factorySet.createSpecFactory(specClass, trait), this); 54 | } 55 | 56 | public > JFactory register(Class specClass) { 57 | getPropertyAliasesInSpec(specClass).stream().filter(Objects::nonNull).forEach(propertyAliases -> { 58 | if (propertyAliases.value().length > 0) { 59 | AliasSetStore.AliasSet aliasSet = aliasOfSpec(specClass); 60 | for (PropertyAlias propertyAlias : propertyAliases.value()) 61 | aliasSet.alias(propertyAlias.alias(), propertyAlias.property()); 62 | } 63 | }); 64 | factorySet.registerSpecClassFactory(specClass); 65 | return this; 66 | } 67 | 68 | private List getPropertyAliasesInSpec(Class specClass) { 69 | return new ArrayList() {{ 70 | Class superclass = specClass.getSuperclass(); 71 | if (!superclass.equals(Object.class)) 72 | addAll(getPropertyAliasesInSpec(superclass)); 73 | add(specClass.getAnnotation(PropertyAliases.class)); 74 | }}; 75 | } 76 | 77 | public Builder spec(String... traitsAndSpec) { 78 | return new DefaultBuilder<>((ObjectFactory) specFactory(traitsAndSpec[traitsAndSpec.length - 1]), this) 79 | .traits(Arrays.copyOf(traitsAndSpec, traitsAndSpec.length - 1)); 80 | } 81 | 82 | public Factory specFactory(String specName) { 83 | return factorySet.querySpecClassFactory(specName); 84 | } 85 | 86 | public > Factory specFactory(Class specClass) { 87 | register(specClass); 88 | return factorySet.querySpecClassFactory(specClass); 89 | } 90 | 91 | public T create(Class type) { 92 | return type(type).create(); 93 | } 94 | 95 | public > T createAs(Class spec) { 96 | return spec(spec).create(); 97 | } 98 | 99 | public > T createAs(Class spec, Consumer trait) { 100 | return spec(spec, trait).create(); 101 | } 102 | 103 | public T createAs(String... traitsAndSpec) { 104 | return this.spec(traitsAndSpec).create(); 105 | } 106 | 107 | public JFactory registerDefaultValueFactory(Class type, DefaultValueFactory factory) { 108 | factorySet.registerDefaultValueFactory(type, factory); 109 | return this; 110 | } 111 | 112 | public JFactory ignoreDefaultValue(Predicate> ignoreProperty) { 113 | ignoreDefaultValues.add(ignoreProperty); 114 | return this; 115 | } 116 | 117 | boolean shouldCreateDefaultValue(PropertyWriter propertyWriter) { 118 | return ignoreDefaultValues.stream().noneMatch(p -> p.test(propertyWriter)); 119 | } 120 | 121 | public AliasSetStore.AliasSet aliasOf(Class type) { 122 | return aliasSetStore.aliasSet(BeanClass.create(type)); 123 | } 124 | 125 | public > AliasSetStore.AliasSet aliasOfSpec(Class specClass) { 126 | return aliasSetStore.specAliasSet(specClass); 127 | } 128 | 129 | public JFactory removeGlobalSpec(Class type) { 130 | factorySet.removeGlobalSpec(BeanClass.create(type)); 131 | return this; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/ObjectFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.*; 6 | import java.util.function.Consumer; 7 | import java.util.function.Function; 8 | import java.util.function.Supplier; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | import java.util.stream.Collectors; 12 | 13 | class ObjectFactory implements Factory { 14 | protected final FactorySet factorySet; 15 | private final BeanClass type; 16 | private final Map>> traits = new LinkedHashMap<>(); 17 | private final Map traitPatterns = new LinkedHashMap<>(); 18 | private final Map transformers = new LinkedHashMap<>(); 19 | private final Transformer passThrough = input -> input; 20 | private Function, T> constructor = this::defaultConstruct; 21 | private Consumer> spec = (instance) -> { 22 | }; 23 | 24 | public ObjectFactory(BeanClass type, FactorySet factorySet) { 25 | this.type = type; 26 | this.factorySet = factorySet; 27 | } 28 | 29 | @SuppressWarnings("unchecked") 30 | private T defaultConstruct(Instance instance) { 31 | return getType().isCollection() 32 | ? (T) getType().createCollection(Collections.nCopies(instance.collectionSize(), null)) 33 | : getType().newInstance(); 34 | } 35 | 36 | protected Spec createSpec() { 37 | return new Spec<>(); 38 | } 39 | 40 | @Override 41 | public Factory constructor(Function, T> constructor) { 42 | this.constructor = Objects.requireNonNull(constructor); 43 | return this; 44 | } 45 | 46 | @Override 47 | public Factory spec(Consumer> spec) { 48 | this.spec = Objects.requireNonNull(spec); 49 | return this; 50 | } 51 | 52 | @Override 53 | public Factory spec(String name, Consumer> trait) { 54 | traits.put(name, Objects.requireNonNull(trait)); 55 | traitPatterns.put(name, Pattern.compile(name)); 56 | return this; 57 | } 58 | 59 | public final T create(Instance instance) { 60 | return constructor.apply(instance); 61 | } 62 | 63 | @Override 64 | public BeanClass getType() { 65 | return type; 66 | } 67 | 68 | @Override 69 | public Factory transformer(String property, Transformer transformer) { 70 | transformers.put(property, transformer); 71 | return this; 72 | } 73 | 74 | private static class TraitExecutor { 75 | private final Consumer> action; 76 | private final List args = new ArrayList<>(); 77 | 78 | public TraitExecutor(Matcher matcher, Consumer> action) { 79 | for (int i = 0; i < matcher.groupCount(); i++) 80 | args.add(matcher.group(i + 1)); 81 | this.action = action; 82 | } 83 | 84 | public TraitExecutor(Consumer> action) { 85 | this.action = action; 86 | } 87 | 88 | public void execute(Instance instance) { 89 | ((RootInstance) instance).runTraitWithParams(args.toArray(), action); 90 | } 91 | } 92 | 93 | public void collectSpec(Collection traits, Instance instance) { 94 | spec.accept(instance); 95 | collectSubSpec(instance); 96 | for (String name : traits) 97 | queryTrait(name).execute(instance); 98 | } 99 | 100 | private TraitExecutor queryTrait(String name) { 101 | Consumer> action = traits.get(name); 102 | if (action != null) 103 | return new TraitExecutor<>(action); 104 | List matchers = traitPatterns.values().stream().map(p -> p.matcher(name)).filter(Matcher::matches) 105 | .collect(Collectors.toList()); 106 | if (matchers.size() == 1) 107 | return new TraitExecutor<>(matchers.get(0), traits.get(matchers.get(0).pattern().pattern())); 108 | if (matchers.size() > 1) 109 | throw new IllegalArgumentException(String.format("Ambiguous trait pattern: %s, candidates are:\n%s", name, 110 | matchers.stream().map(p -> " " + p.pattern().pattern()).collect(Collectors.joining("\n")))); 111 | throw new IllegalArgumentException("Trait `" + name + "` not exist"); 112 | } 113 | 114 | protected void collectSubSpec(Instance instance) { 115 | } 116 | 117 | public RootInstance createInstance(DefaultArguments argument) { 118 | Spec spec = createSpec(); 119 | RootInstance rootInstance = new RootInstance<>(factorySet.nextSequence(type.getType()), spec, argument); 120 | spec.setInstance(rootInstance); 121 | return rootInstance; 122 | } 123 | 124 | public FactorySet getFactorySet() { 125 | return factorySet; 126 | } 127 | 128 | public ObjectFactory getBase() { 129 | return this; 130 | } 131 | 132 | public Object transform(String name, Object value) { 133 | return queryTransformer(name, () -> passThrough).checkAndTransform(value); 134 | } 135 | 136 | protected Transformer queryTransformer(String name, Supplier fallback) { 137 | return transformers.getOrDefault(name, fallback(name, fallback).get()); 138 | } 139 | 140 | protected Supplier fallback(String name, Supplier fallback) { 141 | return () -> type.getType().getSuperclass() == null ? fallback.get() 142 | : factorySet.queryObjectFactory(BeanClass.create(type.getType().getSuperclass())) 143 | .queryTransformer(name, fallback); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/test/resources/features/test_case_design/2-reverse-association.feature: -------------------------------------------------------------------------------- 1 | Feature: Reverse Association 2 | 3 | Rule: single object 4 | 5 | Background: 6 | Given the following bean class: 7 | """ 8 | public class Person { 9 | public Passport passport; 10 | } 11 | """ 12 | And the following bean class: 13 | """ 14 | public class Passport { 15 | public Person person; 16 | public String number; 17 | } 18 | """ 19 | 20 | Scenario: no query before create 21 | Given register: 22 | """ 23 | jFactory.factory(Person.class).spec(instance -> instance.spec() 24 | .property("passport").reverseAssociation("person")); 25 | """ 26 | When build: 27 | """ 28 | jFactory.type(Person.class).property("passport.number", "001").create(); 29 | """ 30 | Then the result should: 31 | """ 32 | : { 33 | passport.number= '001' 34 | {}= (.passport.person) 35 | } 36 | """ 37 | 38 | Scenario: should save parent bean first 39 | Given declaration list = 40 | """ 41 | new ArrayList<>(); 42 | """ 43 | And declaration jFactory = 44 | """ 45 | new JFactory(new DataRepository() { 46 | @Override 47 | public void save(Object object) { 48 | list.add(object); 49 | } 50 | @Override 51 | public Collection queryAll(Class type) { 52 | return Collections.emptyList(); 53 | } 54 | @Override 55 | public void clear() { 56 | } 57 | }); 58 | """ 59 | And register: 60 | """ 61 | jFactory.factory(Person.class).spec(instance -> instance.spec() 62 | .property("passport").reverseAssociation("person")); 63 | """ 64 | When build: 65 | """ 66 | jFactory.type(Person.class).property("passport.number", "001").create(); 67 | """ 68 | Then the list in repo should: 69 | """ 70 | class[].simpleName= [ 71 | Person 72 | Passport 73 | ] 74 | """ 75 | 76 | Scenario: query before create should not happen 77 | Given register: 78 | """ 79 | jFactory.factory(Person.class).spec(instance -> instance.spec() 80 | .property("passport").reverseAssociation("person")); 81 | """ 82 | When operate: 83 | """ 84 | jFactory.type(Person.class).property("passport.number", "001").create(); 85 | jFactory.type(Person.class).property("passport.number", "001").create(); 86 | """ 87 | Then "jFactory.type(Passport.class).queryAll()" should 88 | """ 89 | ::size= 2 90 | """ 91 | 92 | Rule: object collection 93 | 94 | Background: 95 | And the following bean class: 96 | """ 97 | public class OrderLine { 98 | public Order order; 99 | public String info; 100 | } 101 | """ 102 | 103 | Scenario Outline: no query before create 104 | Given the following bean class: 105 | """ 106 | public class Order { 107 | public orderLines; 108 | } 109 | """ 110 | And register: 111 | """ 112 | jFactory.factory(Order.class).spec(instance -> instance.spec() 113 | .property("orderLines").reverseAssociation("order")); 114 | """ 115 | When build: 116 | """ 117 | jFactory.type(Order.class).property("orderLines[0].info", "line info").create(); 118 | """ 119 | Then the result should: 120 | """ 121 | : { 122 | orderLines.info[]: ['line info'] 123 | {}= (.orderLines[0].order) 124 | } 125 | """ 126 | Examples: 127 | | listType | 128 | | OrderLine[] | 129 | | List | 130 | | Set | 131 | 132 | Scenario Outline: should save parent bean first 133 | Given the following bean class: 134 | """ 135 | public class Order { 136 | public orderLines; 137 | } 138 | """ 139 | And declaration list = 140 | """ 141 | new ArrayList<>(); 142 | """ 143 | And declaration jFactory = 144 | """ 145 | new JFactory(new DataRepository() { 146 | @Override 147 | public void save(Object object) { 148 | list.add(object); 149 | } 150 | @Override 151 | public Collection queryAll(Class type) { 152 | return Collections.emptyList(); 153 | } 154 | @Override 155 | public void clear() { 156 | } 157 | }); 158 | """ 159 | And register: 160 | """ 161 | jFactory.factory(Order.class).spec(instance -> instance.spec() 162 | .property("orderLines").reverseAssociation("order")); 163 | """ 164 | When build: 165 | """ 166 | jFactory.type(Order.class).property("orderLines[0].info", "line info").create(); 167 | """ 168 | Then the list in repo should: 169 | """ 170 | class[].simpleName= [ 171 | Order 172 | OrderLine 173 | ] 174 | """ 175 | Examples: 176 | | listType | 177 | | OrderLine[] | 178 | | List | 179 | | Set | 180 | 181 | Scenario Outline: query before create should not happen 182 | Given the following bean class: 183 | """ 184 | public class Order { 185 | public orderLines; 186 | } 187 | """ 188 | Given register: 189 | """ 190 | jFactory.factory(Order.class).spec(instance -> instance.spec() 191 | .property("orderLines").reverseAssociation("order")); 192 | """ 193 | When operate: 194 | """ 195 | jFactory.type(Order.class).property("orderLines[0].info", "line info").create(); 196 | jFactory.type(Order.class).property("orderLines[0].info", "line info").create(); 197 | """ 198 | Then "jFactory.type(OrderLine.class).queryAll()" should 199 | """ 200 | ::size= 2 201 | """ 202 | Examples: 203 | | listType | 204 | | OrderLine[] | 205 | | List | 206 | | Set | 207 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/PropertySpec.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.util.List; 6 | import java.util.function.Consumer; 7 | import java.util.function.Function; 8 | import java.util.function.Supplier; 9 | import java.util.stream.Collectors; 10 | 11 | import static java.lang.String.format; 12 | import static java.util.Collections.singletonList; 13 | 14 | public class PropertySpec { 15 | private final Spec spec; 16 | private final PropertyChain property; 17 | 18 | PropertySpec(Spec spec, PropertyChain property) { 19 | this.spec = spec; 20 | this.property = property; 21 | } 22 | 23 | public Spec value(Object value) { 24 | return value(() -> value); 25 | } 26 | 27 | @SuppressWarnings("unchecked") 28 | public Spec value(Supplier value) { 29 | if (value == null) 30 | return value(() -> null); 31 | return appendProducer((jFactory, producer, property) -> 32 | new UnFixedValueProducer<>(value, (BeanClass) producer.getPropertyWriterType(property))); 33 | } 34 | 35 | public Spec is(Class> specClass) { 36 | return appendProducer(jFactory -> createCreateProducer(jFactory.spec(specClass))); 37 | } 38 | 39 | public Spec is(String... traitsAndSpec) { 40 | return appendProducer(jFactory -> createCreateProducer(jFactory.spec(traitsAndSpec))); 41 | } 42 | 43 | public > IsSpec from(Class specClass) { 44 | return spec.newIsSpec(specClass, this); 45 | } 46 | 47 | public Spec defaultValue(Object value) { 48 | return defaultValue(() -> value); 49 | } 50 | 51 | @SuppressWarnings("unchecked") 52 | public Spec defaultValue(Supplier supplier) { 53 | if (supplier == null) 54 | return defaultValue((Object) null); 55 | return appendProducer((jFactory, producer, property) -> 56 | new DefaultValueProducer<>((BeanClass) producer.getPropertyWriterType(property), supplier)); 57 | } 58 | 59 | public Spec byFactory() { 60 | return appendProducer((jFactory, producer, property) -> 61 | producer.subDefaultValueProducer(producer.getType().getPropertyWriter(property)).orElseGet(() -> 62 | createCreateProducer(jFactory.type(producer.getPropertyWriterType(property).getType())))); 63 | } 64 | 65 | public Spec byFactory(Function, Builder> builder) { 66 | return appendProducer((jFactory, producer, property) -> 67 | producer.subDefaultValueProducer(producer.getType().getPropertyWriter(property)) 68 | .orElseGet(() -> createQueryOrCreateProducer(builder.apply(jFactory.type( 69 | producer.getPropertyWriterType(property).getType()))))); 70 | } 71 | 72 | public Spec dependsOn(String dependency, Function rule) { 73 | return dependsOn(singletonList(dependency), objects -> rule.apply(objects[0])); 74 | } 75 | 76 | public Spec dependsOn(List dependencies, Function rule) { 77 | return spec.append((jFactory, objectProducer) -> 78 | objectProducer.addDependency(property, rule, 79 | dependencies.stream().map(PropertyChain::propertyChain).collect(Collectors.toList()))); 80 | } 81 | 82 | private Spec appendProducer(Fuc, String, Producer> producerFactory) { 83 | if (property.isSingle() || property.isTopLevelPropertyCollection()) 84 | return spec.append((jFactory, objectProducer) -> objectProducer.changeDescendant(property, 85 | ((nextToLast, property) -> producerFactory.apply(jFactory, nextToLast, property)))); 86 | throw new IllegalArgumentException(format("Not support property chain '%s' in current operation", property)); 87 | } 88 | 89 | private Spec appendProducer(Function> producerFactory) { 90 | return appendProducer((jFactory, producer, s) -> producerFactory.apply(jFactory)); 91 | } 92 | 93 | @SuppressWarnings("unchecked") 94 | private Producer createQueryOrCreateProducer(Builder builder) { 95 | Builder builderWithArgs = builder.args(spec.params(property.toString())); 96 | return builderWithArgs.queryAll().stream().findFirst().>map(object -> 97 | new BuilderValueProducer<>((BeanClass) BeanClass.create(object.getClass()), builderWithArgs)) 98 | .orElseGet(builderWithArgs::createProducer); 99 | } 100 | 101 | private Producer createCreateProducer(Builder builder) { 102 | return builder.args(spec.params(property.toString())).createProducer(); 103 | } 104 | 105 | public Spec reverseAssociation(String association) { 106 | return spec.append((jFactory, producer) -> producer.appendReverseAssociation(property, association)); 107 | } 108 | 109 | public Spec ignore() { 110 | return spec.append((jFactory, objectProducer) -> objectProducer.ignoreProperty(property.toString())); 111 | } 112 | 113 | @FunctionalInterface 114 | interface Fuc { 115 | R apply(P1 p1, P2 p2, P3 p3); 116 | } 117 | 118 | public class IsSpec> { 119 | private final Class specClass; 120 | private final String position; 121 | 122 | public IsSpec(Class spec) { 123 | position = Thread.currentThread().getStackTrace()[4].toString(); 124 | specClass = spec; 125 | } 126 | 127 | public Spec which(Consumer trait) { 128 | spec.consume(this); 129 | return appendProducer(jFactory -> createCreateProducer(jFactory.spec(specClass, trait))); 130 | } 131 | 132 | public Spec and(Function, Builder> builder) { 133 | spec.consume(this); 134 | return appendProducer(jFactory -> createQueryOrCreateProducer(builder.apply(jFactory.spec(specClass)))); 135 | } 136 | 137 | public String getPosition() { 138 | return position; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/DefaultValueFactoriesTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.PropertyWriter; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.lang.annotation.Annotation; 8 | import java.math.BigDecimal; 9 | import java.math.BigInteger; 10 | import java.time.*; 11 | import java.util.Date; 12 | import java.util.UUID; 13 | import java.util.function.BiConsumer; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.junit.jupiter.api.Assertions.assertThrows; 17 | 18 | class DefaultValueFactoriesTest { 19 | 20 | @Test 21 | void default_string() { 22 | assertValue(String.class, 1, "str", "str#1"); 23 | assertValue(String.class, 3, "name", "name#3"); 24 | } 25 | 26 | @Test 27 | void default_int() { 28 | assertValue(int.class, 1, 1); 29 | assertValue(Integer.class, 3, 3); 30 | } 31 | 32 | @Test 33 | void default_short() { 34 | assertValue(short.class, 1, (short) 1); 35 | assertValue(Short.class, 2, (short) 2); 36 | } 37 | 38 | @Test 39 | void default_byte() { 40 | assertValue(byte.class, 1, (byte) 1); 41 | assertValue(Byte.class, 2, (byte) 2); 42 | } 43 | 44 | @Test 45 | void default_long() { 46 | assertValue(long.class, 1, 1L); 47 | assertValue(Long.class, 2, 2L); 48 | } 49 | 50 | @Test 51 | void default_float() { 52 | assertValue(float.class, 1, 1.0f); 53 | assertValue(Float.class, 2, 2.0f); 54 | } 55 | 56 | @Test 57 | void default_double() { 58 | assertValue(double.class, 1, 1.0); 59 | assertValue(Double.class, 2, 2.0); 60 | } 61 | 62 | @Test 63 | void should_raise_error_when_invalid_generic_args() { 64 | assertThrows(IllegalStateException.class, () -> new InvalidGenericArgDefaultValueFactory<>().getType()); 65 | } 66 | 67 | @Test 68 | void default_boolean() { 69 | assertValue(boolean.class, 1, true); 70 | assertValue(Boolean.class, 2, false); 71 | assertValue(boolean.class, 3, true); 72 | } 73 | 74 | @Test 75 | void default_big_int() { 76 | assertValue(BigInteger.class, 1, BigInteger.valueOf(1)); 77 | assertValue(BigInteger.class, 2, BigInteger.valueOf(2)); 78 | } 79 | 80 | @Test 81 | void default_big_decimal() { 82 | assertValue(BigDecimal.class, 1, BigDecimal.valueOf(1)); 83 | assertValue(BigDecimal.class, 2, BigDecimal.valueOf(2)); 84 | } 85 | 86 | @Test 87 | void default_uuid() { 88 | assertValue(UUID.class, 1, UUID.fromString("00000000-0000-0000-0000-000000000001")); 89 | assertValue(UUID.class, 2, UUID.fromString("00000000-0000-0000-0000-000000000002")); 90 | } 91 | 92 | @Test 93 | void default_instant() { 94 | assertValue(Instant.class, 1, Instant.parse("1996-01-23T00:00:01Z")); 95 | assertValue(Instant.class, 2, Instant.parse("1996-01-23T00:00:02Z")); 96 | } 97 | 98 | @Test 99 | void default_date() { 100 | assertValue(Date.class, 1, Date.from(Instant.parse("1996-01-24T00:00:00Z"))); 101 | assertValue(Date.class, 2, Date.from(Instant.parse("1996-01-25T00:00:00Z"))); 102 | } 103 | 104 | @Test 105 | void default_local_time() { 106 | assertValue(LocalTime.class, 1, LocalTime.parse("00:00:01")); 107 | assertValue(LocalTime.class, 2, LocalTime.parse("00:00:02")); 108 | } 109 | 110 | @Test 111 | void default_local_date() { 112 | assertValue(LocalDate.class, 1, LocalDate.parse("1996-01-24")); 113 | assertValue(LocalDate.class, 2, LocalDate.parse("1996-01-25")); 114 | } 115 | 116 | @Test 117 | void default_local_date_time() { 118 | assertValue(LocalDateTime.class, 1, LocalDateTime.parse("1996-01-23T00:00:01")); 119 | assertValue(LocalDateTime.class, 2, LocalDateTime.parse("1996-01-23T00:00:02")); 120 | } 121 | 122 | @Test 123 | void default_offset_date_time() { 124 | assertValue(OffsetDateTime.class, 1, Instant.parse("1996-01-23T00:00:01Z").atZone(ZoneId.systemDefault()).toOffsetDateTime()); 125 | assertValue(OffsetDateTime.class, 2, Instant.parse("1996-01-23T00:00:02Z").atZone(ZoneId.systemDefault()).toOffsetDateTime()); 126 | } 127 | 128 | @Test 129 | void default_zoned_date_time() { 130 | assertValue(ZonedDateTime.class, 1, Instant.parse("1996-01-23T00:00:01Z").atZone(ZoneId.systemDefault())); 131 | assertValue(ZonedDateTime.class, 2, Instant.parse("1996-01-23T00:00:02Z").atZone(ZoneId.systemDefault())); 132 | } 133 | 134 | private void assertValue(Class type, int sequence, String property, Object expected) { 135 | assertThat(new DefaultValueFactories().query(type).get() 136 | .create(null, new RootInstance<>(sequence, new Spec<>(), new DefaultArguments()) 137 | .sub(stubPropertyWriter(property)))).isEqualTo(expected); 138 | } 139 | 140 | private PropertyWriter stubPropertyWriter(String property) { 141 | return new PropertyWriter() { 142 | @Override 143 | public BiConsumer setter() { 144 | return null; 145 | } 146 | 147 | @Override 148 | public void setValue(Object bean, Object value) { 149 | } 150 | 151 | @Override 152 | public String getName() { 153 | return property; 154 | } 155 | 156 | @Override 157 | public Object tryConvert(Object value) { 158 | return null; 159 | } 160 | 161 | @Override 162 | public BeanClass getBeanType() { 163 | return null; 164 | } 165 | 166 | @Override 167 | public BeanClass getType() { 168 | return null; 169 | } 170 | 171 | @Override 172 | public A getAnnotation(Class annotationClass) { 173 | return null; 174 | } 175 | 176 | @Override 177 | public boolean isBeanProperty() { 178 | return true; 179 | } 180 | }; 181 | } 182 | 183 | private void assertValue(Class type, int sequence, Object expected) { 184 | assertThat(new DefaultValueFactories().query(type).get() 185 | .create(null, new RootInstance<>(sequence, new Spec<>(), new DefaultArguments()) 186 | .sub(stubPropertyWriter(null)))).isEqualTo(expected); 187 | } 188 | 189 | @Test 190 | void default_value_builder_create_default_value() { 191 | assertThat(new DefaultValueFactories.DefaultTypeFactory<>(BeanClass.create(int.class)).create(null, null)) 192 | .isInstanceOf(Integer.class); 193 | } 194 | 195 | public static class InvalidGenericArgDefaultValueFactory implements DefaultValueFactory { 196 | @Override 197 | public V create(BeanClass beanType, SubInstance instance) { 198 | return null; 199 | } 200 | } 201 | } -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/cucumber/IntegrationTestContext.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory.cucumber; 2 | 3 | import com.github.leeonky.jfactory.JFactory; 4 | import com.github.leeonky.jfactory.Spec; 5 | import com.github.leeonky.util.BeanClass; 6 | import com.github.leeonky.util.JavaCompiler; 7 | import com.github.leeonky.util.JavaCompilerPool; 8 | 9 | import java.util.*; 10 | import java.util.function.Consumer; 11 | import java.util.function.Function; 12 | import java.util.function.Supplier; 13 | import java.util.stream.Collectors; 14 | 15 | import static com.github.leeonky.dal.Assertions.expect; 16 | import static java.util.Arrays.asList; 17 | import static org.assertj.core.api.Assertions.assertThat; 18 | 19 | public class IntegrationTestContext { 20 | private final Map classCodes = new HashMap<>(); 21 | private final List registers = new ArrayList<>(); 22 | private final List classes = new ArrayList<>(); 23 | private final List register = new ArrayList<>(); 24 | 25 | { 26 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 27 | } 28 | 29 | public static int threadsCount(String env, int defaultValue) { 30 | String value = System.getenv(env); 31 | if (value == null) 32 | return defaultValue; 33 | return Integer.parseInt(value); 34 | } 35 | 36 | private static final JavaCompilerPool JAVA_COMPILER_POOL = 37 | new JavaCompilerPool(threadsCount("COMPILER_THREAD_SIZE", 8) * 2, "src.test.generate.ws"); 38 | private final JavaCompiler compiler = JAVA_COMPILER_POOL.take(); 39 | private List list; 40 | private JFactory jFactory = new JFactory(); 41 | private int snippetIndex = 0; 42 | private Object bean; 43 | private Throwable throwable; 44 | 45 | public void releaseCompiler() { 46 | JAVA_COMPILER_POOL.giveBack(compiler); 47 | } 48 | 49 | private T createProcedure(Class type, String tmpClass) { 50 | return (T) BeanClass.create(getType(tmpClass)).newInstance(); 51 | } 52 | 53 | public void givenBean(String classCode) { 54 | addClass(classCode); 55 | } 56 | 57 | private Class getType(String className) { 58 | Class type = classes.stream().filter(clazz -> clazz.getSimpleName().equals(className)) 59 | .findFirst().orElseThrow(() -> new IllegalArgumentException 60 | ("cannot find bean class: " + className + "\nclasses: " + classes)); 61 | return type; 62 | } 63 | 64 | private String jFactoryOperate(String builderSnippet) { 65 | String className = "Snip" + (snippetIndex++); 66 | String snipCode = "import java.util.function.*;\n" + 67 | "import java.util.*;\n" + 68 | "import com.github.leeonky.util.*;\n" + 69 | "import com.github.leeonky.jfactory.*;\n" + 70 | "import static com.github.leeonky.jfactory.ArgumentMapFactory.arg;\n" + 71 | "public class " + className + " implements Consumer {\n" + 72 | " @Override\n" + 73 | " public void accept(JFactory jFactory) { " + builderSnippet + ";}\n" + 74 | "}"; 75 | addClass(snipCode); 76 | return className; 77 | } 78 | 79 | private void addClass(String snipCode) { 80 | classCodes.put(JavaCompiler.guessClassName(snipCode), snipCode); 81 | } 82 | 83 | private void compileAll() { 84 | classes.clear(); 85 | classes.addAll(compiler.compileToClasses(classCodes.values().stream().map(s -> "import com.github.leeonky.jfactory.*;\n" + 86 | "import java.util.function.*;\n" + 87 | "import java.util.*;\n" + 88 | "import java.math.*;\n" + s).collect(Collectors.toList()))); 89 | classes.stream().filter(Spec.class::isAssignableFrom).forEach(jFactory::register); 90 | } 91 | 92 | public void register(String factorySnippet) { 93 | registers.add(factorySnippet); 94 | } 95 | 96 | private void create(Supplier supplier) { 97 | try { 98 | compileAll(); 99 | register.forEach(Runnable::run); 100 | bean = supplier.get(); 101 | } catch (Throwable throwable) { 102 | this.throwable = throwable; 103 | } 104 | } 105 | 106 | public void verify(String dal) throws Throwable { 107 | if (throwable != null) 108 | throw throwable; 109 | expect(bean).should(dal); 110 | } 111 | 112 | public void specClass(String specClass) { 113 | addClass(specClass); 114 | } 115 | 116 | @Deprecated 117 | public void operate(String operateSnippet) { 118 | String tmpClass = jFactoryOperate(operateSnippet); 119 | register.add(() -> createProcedure(Consumer.class, tmpClass).accept(jFactory)); 120 | } 121 | 122 | public void shouldThrow(String dal) { 123 | expect(throwable).should(dal.replace("#package#", compiler.packagePrefix())); 124 | } 125 | 126 | public void build(String builderSnippet) { 127 | String tmpClass = jFactoryAction(builderSnippet); 128 | create(() -> createProcedure(Function.class, tmpClass).apply(jFactory)); 129 | } 130 | 131 | private String jFactoryAction(String builderSnippet) { 132 | String className = "Snip" + (snippetIndex++); 133 | String snipCode = "import java.util.function.*;\n" + 134 | "import java.util.*;\n" + 135 | "import java.util.stream.*;\n" + 136 | "import com.github.leeonky.util.*;\n" + 137 | "import com.github.leeonky.jfactory.*;\n" + 138 | "import static com.github.leeonky.jfactory.ArgumentMapFactory.arg;\n" + 139 | "public class " + className + " implements Function {\n" + 140 | " @Override\n" + 141 | " public Object apply(JFactory jFactory) {\n" + 142 | String.join("\n", registers) + "\n" + 143 | " return " + builderSnippet + "}\n" + 144 | "}"; 145 | addClass(snipCode); 146 | return className; 147 | } 148 | 149 | 150 | private String createObject(String declaration) { 151 | String className = "Snip" + (snippetIndex++); 152 | return "import java.util.function.*;\n" + 153 | "import java.util.*;\n" + 154 | "import com.github.leeonky.util.*;\n" + 155 | "import com.github.leeonky.jfactory.*;\n" + 156 | "import static com.github.leeonky.jfactory.ArgumentMapFactory.arg;\n" + 157 | "public class " + className + " implements Function, Object> {\n" + 158 | " @Override\n" + 159 | " public Object apply(List list) { return " + declaration + "}\n" + 160 | "}"; 161 | } 162 | 163 | public void declare(String declaration) { 164 | jFactory = (JFactory) ((Function) BeanClass.create(compiler. 165 | compileToClasses(asList(createObject(declaration))).get(0)).newInstance()).apply(list); 166 | } 167 | 168 | public void declareList(String listDeclaration) { 169 | list = (List) ((Function) BeanClass.create(compiler. 170 | compileToClasses(asList(createObject(listDeclaration))).get(0)).newInstance()).apply(null); 171 | } 172 | 173 | public void listShould(String dal) { 174 | assertThat(throwable).isNull(); 175 | expect(list).should(dal); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/DefaultBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | import com.github.leeonky.util.CollectionHelper; 5 | 6 | import java.util.*; 7 | import java.util.stream.Collectors; 8 | 9 | import static com.github.leeonky.util.BeanClass.cast; 10 | import static java.util.Arrays.asList; 11 | import static java.util.Objects.hash; 12 | 13 | class DefaultBuilder implements Builder { 14 | private final ObjectFactory objectFactory; 15 | private final JFactory jFactory; 16 | private final Set traits = new LinkedHashSet<>(); 17 | 18 | private final KeyValueCollection properties; 19 | private final DefaultArguments arguments = new DefaultArguments(); 20 | private int collectionSize = 0; 21 | 22 | public DefaultBuilder(ObjectFactory objectFactory, JFactory jFactory) { 23 | this.jFactory = jFactory; 24 | this.objectFactory = objectFactory; 25 | properties = new KeyValueCollection(objectFactory.getFactorySet()); 26 | } 27 | 28 | @Override 29 | public T create() { 30 | return createProducer().doDependenciesAndLinks().getValue(); 31 | } 32 | 33 | @Override 34 | public BeanClass getType() { 35 | return objectFactory.getType(); 36 | } 37 | 38 | @Override 39 | public ObjectProducer createProducer() { 40 | return new ObjectProducer<>(jFactory, objectFactory, this); 41 | } 42 | 43 | @Override 44 | public Builder arg(String key, Object value) { 45 | DefaultBuilder newBuilder = clone(); 46 | newBuilder.arguments.put(key, value); 47 | return newBuilder; 48 | } 49 | 50 | @Override 51 | public Builder args(Arguments arguments) { 52 | DefaultBuilder newBuilder = clone(); 53 | newBuilder.arguments.merge((DefaultArguments) arguments); 54 | return newBuilder; 55 | } 56 | 57 | @Override 58 | public Builder args(Map args) { 59 | DefaultBuilder newBuilder = clone(); 60 | args.forEach(newBuilder.arguments::put); 61 | return newBuilder; 62 | } 63 | 64 | @Override 65 | public Builder args(String property, Map args) { 66 | DefaultBuilder newBuilder = clone(); 67 | args.forEach((key, value) -> newBuilder.arguments.put(property, key, value)); 68 | return newBuilder; 69 | } 70 | 71 | @Override 72 | public Builder traits(String... traits) { 73 | DefaultBuilder newBuilder = clone(); 74 | newBuilder.traits.addAll(asList(traits)); 75 | return newBuilder; 76 | } 77 | 78 | @Override 79 | public DefaultBuilder clone() { 80 | return clone(objectFactory); 81 | } 82 | 83 | private DefaultBuilder clone(ObjectFactory objectFactory) { 84 | DefaultBuilder builder = new DefaultBuilder<>(objectFactory, jFactory); 85 | builder.properties.appendAll(properties); 86 | builder.traits.addAll(traits); 87 | builder.arguments.merge(arguments); 88 | return builder; 89 | } 90 | 91 | @Override 92 | public Builder properties(Map properties) { 93 | DefaultBuilder newBuilder = clone(); 94 | properties.forEach((key, value) -> { 95 | String property; 96 | property = replaceStartsWithIndexBracket(jFactory.aliasSetStore.resolve( 97 | objectFactory, key, isCollection(value)), newBuilder); 98 | if (isCollection(value)) { 99 | List objects = CollectionHelper.toStream(value).collect(Collectors.toList()); 100 | if (objects.isEmpty() || !property.contains("$")) 101 | newBuilder.properties.append(trimIndexAlias(property), objects); 102 | else for (int i = 0; i < objects.size(); i++) 103 | newBuilder.properties.append(property.replaceFirst("\\$", String.valueOf(i)), objects.get(i)); 104 | } else 105 | newBuilder.properties.append(property, value); 106 | }); 107 | return newBuilder; 108 | } 109 | 110 | private String trimIndexAlias(String property) { 111 | if (property.contains("[$]")) 112 | return property.substring(0, property.indexOf("[$]")); 113 | return property; 114 | } 115 | 116 | private boolean isCollection(Object value) { 117 | return value != null && BeanClass.createFrom(value).isCollection(); 118 | } 119 | 120 | private String replaceStartsWithIndexBracket(String key, DefaultBuilder newBuilder) { 121 | if (key.startsWith("[")) { 122 | String[] indexAndSub = key.substring(1).split("]", 2); 123 | newBuilder.collectionSize = Math.max(newBuilder.collectionSize, Integer.parseInt(indexAndSub[0]) + 1); 124 | return indexAndSub[0] + indexAndSub[1]; 125 | } 126 | return key; 127 | } 128 | 129 | @Override 130 | public Collection queryAll() { 131 | KeyValueCollection.Matcher matcher = properties.matcher(objectFactory.getType(), objectFactory); 132 | return jFactory.getDataRepository().queryAll(objectFactory.getType().getType()).stream() 133 | .filter(matcher::matches).collect(Collectors.toList()); 134 | } 135 | 136 | @Override 137 | public int hashCode() { 138 | return hash(DefaultBuilder.class, objectFactory, properties, traits); 139 | } 140 | 141 | @Override 142 | public boolean equals(Object another) { 143 | return cast(another, DefaultBuilder.class) 144 | .map(builder -> objectFactory.equals(builder.objectFactory) && properties.equals(builder.properties) 145 | && traits.equals(builder.traits)) 146 | .orElseGet(() -> super.equals(another)); 147 | } 148 | 149 | public void processSpecAndInputProperty(ObjectProducer objectProducer, RootInstance instance) { 150 | collectSpec(objectProducer, instance); 151 | processInputProperty(objectProducer); 152 | instance.setCollectionSize(collectionSize); 153 | } 154 | 155 | private void collectSpec(ObjectProducer objectProducer, Instance instance) { 156 | objectFactory.collectSpec(traits, instance); 157 | instance.spec().apply(jFactory, objectProducer); 158 | objectProducer.processSpecIgnoreProperties(); 159 | } 160 | 161 | private void processInputProperty(ObjectProducer producer) { 162 | properties.expressions(objectFactory.getType(), objectFactory).forEach(exp -> producer.changeChild(exp.getProperty(), 163 | intentlyCreateWhenReverseAssociation(producer, exp).buildProducer(jFactory, producer))); 164 | } 165 | 166 | private Expression intentlyCreateWhenReverseAssociation(ObjectProducer producer, Expression exp) { 167 | return exp instanceof SingleValueExpression ? exp : 168 | producer.isReverseAssociation(exp.getProperty()) ? exp.setIntently(true) : exp; 169 | } 170 | 171 | public DefaultBuilder marge(DefaultBuilder another) { 172 | ObjectFactory objectFactory = another.objectFactory instanceof SpecClassFactory ? another.objectFactory 173 | : this.objectFactory; 174 | DefaultBuilder newBuilder = clone(objectFactory); 175 | newBuilder.properties.appendAll(another.properties); 176 | newBuilder.traits.addAll(another.traits); 177 | newBuilder.collectionSize = collectionSize; 178 | return newBuilder; 179 | } 180 | 181 | public DefaultArguments getArguments() { 182 | return arguments; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/ObjectProducer.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.PropertyWriter; 4 | 5 | import java.util.*; 6 | import java.util.function.Function; 7 | import java.util.stream.Collectors; 8 | 9 | import static com.github.leeonky.jfactory.PropertyChain.propertyChain; 10 | import static com.github.leeonky.util.BeanClass.create; 11 | import static java.util.stream.IntStream.range; 12 | 13 | class ObjectProducer extends Producer { 14 | private final ObjectFactory factory; 15 | private final JFactory jFactory; 16 | private final DefaultBuilder builder; 17 | private final RootInstance instance; 18 | private final Map> children = new HashMap<>(); 19 | private final Map> dependencies = new LinkedHashMap<>(); 20 | private final Map reverseAssociations = new LinkedHashMap<>(); 21 | private final LinkSpecCollection linkSpecCollection = new LinkSpecCollection(); 22 | private final ListPersistable cachedChildren = new ListPersistable(); 23 | private final Set ignorePropertiesInSpec = new HashSet<>(); 24 | private Persistable persistable; 25 | 26 | public ObjectProducer(JFactory jFactory, ObjectFactory factory, DefaultBuilder builder) { 27 | super(factory.getType()); 28 | this.factory = factory; 29 | this.jFactory = jFactory; 30 | this.builder = builder; 31 | instance = factory.createInstance(builder.getArguments()); 32 | persistable = jFactory.getDataRepository(); 33 | createDefaultValueProducers(); 34 | builder.processSpecAndInputProperty(this, instance); 35 | createElementDefaultValueProducers(); 36 | setupReverseAssociations(); 37 | resolveBuilderProducers(); 38 | } 39 | 40 | protected void resolveBuilderProducers() { 41 | List>> buildValueProducers = children.entrySet().stream() 42 | .filter(entry -> entry.getValue() instanceof BuilderValueProducer).collect(Collectors.toList()); 43 | buildValueProducers.forEach(e -> setChild(e.getKey(), ((BuilderValueProducer) e.getValue()).getProducer())); 44 | } 45 | 46 | private void createElementDefaultValueProducers() { 47 | range(0, instance.collectionSize()).mapToObj(String::valueOf).filter(index -> children.get(index) == null) 48 | .map(index -> getType().getPropertyWriter(index)).forEach((PropertyWriter propertyWriter) -> 49 | setChild(propertyWriter.getName(), new DefaultValueFactoryProducer<>(factory.getType(), 50 | factory.getFactorySet().getDefaultValueBuilder(propertyWriter.getType()), 51 | instance.sub(propertyWriter)))); 52 | } 53 | 54 | private void setupReverseAssociations() { 55 | reverseAssociations.forEach((child, association) -> 56 | descendant(child).setupAssociation(association, instance, cachedChildren)); 57 | } 58 | 59 | @Override 60 | public void setChild(String property, Producer producer) { 61 | children.put(property, producer); 62 | } 63 | 64 | @Override 65 | public Producer childOrDefault(String property) { 66 | Producer producer = children.get(property); 67 | if (producer == null) 68 | producer = addDefaultCollectionProducer(getType().getPropertyWriter(property)); 69 | return producer; 70 | } 71 | 72 | private Producer addDefaultCollectionProducer(PropertyWriter property) { 73 | Producer result = null; 74 | if (property.getType().isCollection()) 75 | setChild(property.getName(), result = new CollectionProducer<>(getType(), property.getType(), 76 | instance.sub(property), factory.getFactorySet())); 77 | return result; 78 | } 79 | 80 | @Override 81 | protected T produce() { 82 | return instance.cache(() -> factory.create(instance), obj -> { 83 | produceSubs(obj); 84 | persistable.save(obj); 85 | cachedChildren.getAll().forEach(persistable::save); 86 | }); 87 | } 88 | 89 | private void produceSubs(T obj) { 90 | children.entrySet().stream().filter(this::isDefaultValueProducer).forEach(e -> produceSub(obj, e)); 91 | children.entrySet().stream().filter(e -> !(isDefaultValueProducer(e))).forEach(e -> produceSub(obj, e)); 92 | } 93 | 94 | private void produceSub(T obj, Map.Entry> e) { 95 | getType().setPropertyValue(obj, e.getKey(), e.getValue().getValue()); 96 | } 97 | 98 | private boolean isDefaultValueProducer(Map.Entry> e) { 99 | return e.getValue() instanceof DefaultValueFactoryProducer; 100 | } 101 | 102 | @Override 103 | public Optional> child(String property) { 104 | return Optional.ofNullable(children.get(property)); 105 | } 106 | 107 | public void addDependency(PropertyChain property, Function rule, List dependencies) { 108 | this.dependencies.put(property, new Dependency<>(property, dependencies, rule)); 109 | } 110 | 111 | public ObjectProducer doDependenciesAndLinks() { 112 | doDependencies(); 113 | doLinks(this, propertyChain("")); 114 | return this; 115 | } 116 | 117 | @Override 118 | protected void doLinks(Producer root, PropertyChain base) { 119 | children.forEach((property, producer) -> producer.doLinks(root, base.concat(property))); 120 | linkSpecCollection.processLinks(root, base); 121 | } 122 | 123 | @Override 124 | protected void doDependencies() { 125 | children.values().forEach(Producer::doDependencies); 126 | dependencies.values().forEach(dependency -> dependency.process(this)); 127 | } 128 | 129 | public void link(List properties) { 130 | linkSpecCollection.link(properties); 131 | } 132 | 133 | private void createDefaultValueProducers() { 134 | getType().getPropertyWriters().values().stream().filter(jFactory::shouldCreateDefaultValue) 135 | .forEach(propertyWriter -> subDefaultValueProducer(propertyWriter) 136 | .ifPresent(producer -> setChild(propertyWriter.getName(), producer))); 137 | } 138 | 139 | @Override 140 | public Optional subDefaultValueProducer(PropertyWriter property) { 141 | return factory.getFactorySet().queryDefaultValueBuilder(property.getType()) 142 | .map(builder -> new DefaultValueFactoryProducer<>(getType(), builder, instance.sub(property))); 143 | } 144 | 145 | @Override 146 | public Producer changeTo(Producer newProducer) { 147 | return newProducer.changeFrom(this); 148 | } 149 | 150 | @Override 151 | protected Producer changeFrom(ObjectProducer origin) { 152 | return origin.builder.marge(builder).createProducer(); 153 | } 154 | 155 | public void appendReverseAssociation(PropertyChain property, String association) { 156 | reverseAssociations.put(property, association); 157 | } 158 | 159 | @Override 160 | protected void setupAssociation(String association, RootInstance instance, ListPersistable cachedChildren) { 161 | setChild(association, new UnFixedValueProducer<>(instance.reference(), create(instance.spec().getType()))); 162 | persistable = cachedChildren; 163 | } 164 | 165 | public boolean isReverseAssociation(String property) { 166 | return reverseAssociations.containsKey(PropertyChain.propertyChain(property)); 167 | } 168 | 169 | public void ignoreProperty(String property) { 170 | ignorePropertiesInSpec.add(property); 171 | } 172 | 173 | public void processSpecIgnoreProperties() { 174 | children.entrySet().stream().filter(e -> e.getValue() instanceof DefaultValueProducer 175 | && ignorePropertiesInSpec.contains(e.getKey())).map(Map.Entry::getKey).collect(Collectors.toList()) 176 | .forEach(children::remove); 177 | } 178 | 179 | @Override 180 | protected boolean isFixed() { 181 | return children.values().stream().anyMatch(Producer::isFixed); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/github/leeonky/jfactory/DefaultValueFactories.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import com.github.leeonky.util.BeanClass; 4 | 5 | import java.math.BigDecimal; 6 | import java.math.BigInteger; 7 | import java.time.*; 8 | import java.time.temporal.ChronoUnit; 9 | import java.util.*; 10 | 11 | import static java.util.Optional.ofNullable; 12 | 13 | public class DefaultValueFactories { 14 | private static final LocalDate LOCAL_DATE_START = LocalDate.parse("1996-01-23"); 15 | private static final LocalDateTime LOCAL_DATE_TIME_START = LocalDateTime.parse("1996-01-23T00:00:00"); 16 | private static final LocalTime LOCAL_TIME_START = LocalTime.parse("00:00:00"); 17 | private static final Instant INSTANT_START = Instant.parse("1996-01-23T00:00:00Z"); 18 | private final Map, DefaultValueFactory> defaultValueBuilders = new HashMap<>(); 19 | 20 | DefaultValueFactories() { 21 | register(String.class, new DefaultStringFactory()); 22 | register(Integer.class, new DefaultIntegerFactory()); 23 | register(int.class, defaultValueBuilders.get(Integer.class)); 24 | register(Short.class, new DefaultShortFactory()); 25 | register(short.class, defaultValueBuilders.get(Short.class)); 26 | register(Byte.class, new DefaultByteFactory()); 27 | register(byte.class, defaultValueBuilders.get(Byte.class)); 28 | register(Long.class, new DefaultLongFactory()); 29 | register(long.class, defaultValueBuilders.get(Long.class)); 30 | register(Float.class, new DefaultFloatFactory()); 31 | register(float.class, defaultValueBuilders.get(Float.class)); 32 | register(Double.class, new DefaultDoubleFactory()); 33 | register(double.class, defaultValueBuilders.get(Double.class)); 34 | register(Boolean.class, new DefaultBooleanFactory()); 35 | register(boolean.class, defaultValueBuilders.get(Boolean.class)); 36 | register(BigInteger.class, new DefaultBigIntegerFactory()); 37 | register(BigDecimal.class, new DefaultBigDecimalFactory()); 38 | register(UUID.class, new DefaultUUIDFactory()); 39 | register(Date.class, new DefaultDateFactory()); 40 | register(Instant.class, new DefaultInstantFactory()); 41 | register(LocalDate.class, new DefaultLocalDateFactory()); 42 | register(LocalTime.class, new DefaultLocalTimeFactory()); 43 | register(LocalDateTime.class, new DefaultLocalDateTimeFactory()); 44 | register(OffsetDateTime.class, new DefaultOffsetDateTimeFactory()); 45 | register(ZonedDateTime.class, new DefaultZoneDateTimeFactory()); 46 | register(Enum.class, new DefaultEnumFactory()); 47 | } 48 | 49 | public void register(Class type, DefaultValueFactory factory) { 50 | defaultValueBuilders.put(type, factory); 51 | } 52 | 53 | @SuppressWarnings("unchecked") 54 | public Optional> query(Class type) { 55 | DefaultValueFactory defaultValueFactory = defaultValueBuilders.get(type); 56 | if (type.isEnum()) 57 | defaultValueFactory = defaultValueBuilders.get(Enum.class); 58 | return ofNullable((DefaultValueFactory) defaultValueFactory); 59 | } 60 | 61 | public static class DefaultStringFactory implements DefaultValueFactory { 62 | 63 | @Override 64 | public String create(BeanClass beanType, SubInstance instance) { 65 | return instance.propertyInfo(); 66 | } 67 | } 68 | 69 | public static class DefaultLongFactory implements DefaultValueFactory { 70 | 71 | @Override 72 | public Long create(BeanClass beanType, SubInstance instance) { 73 | return (long) instance.getSequence(); 74 | } 75 | } 76 | 77 | public static class DefaultIntegerFactory implements DefaultValueFactory { 78 | 79 | @Override 80 | public Integer create(BeanClass beanType, SubInstance instance) { 81 | return instance.getSequence(); 82 | } 83 | } 84 | 85 | public static class DefaultShortFactory implements DefaultValueFactory { 86 | 87 | @Override 88 | public Short create(BeanClass beanType, SubInstance instance) { 89 | return (short) instance.getSequence(); 90 | } 91 | } 92 | 93 | public static class DefaultByteFactory implements DefaultValueFactory { 94 | 95 | @Override 96 | public Byte create(BeanClass beanType, SubInstance instance) { 97 | return (byte) instance.getSequence(); 98 | } 99 | } 100 | 101 | public static class DefaultDoubleFactory implements DefaultValueFactory { 102 | 103 | @Override 104 | public Double create(BeanClass beanType, SubInstance instance) { 105 | return (double) instance.getSequence(); 106 | } 107 | } 108 | 109 | public static class DefaultFloatFactory implements DefaultValueFactory { 110 | 111 | @Override 112 | public Float create(BeanClass beanType, SubInstance instance) { 113 | return (float) instance.getSequence(); 114 | } 115 | } 116 | 117 | public static class DefaultBooleanFactory implements DefaultValueFactory { 118 | 119 | @Override 120 | public Boolean create(BeanClass beanType, SubInstance instance) { 121 | return (instance.getSequence() % 2) == 1; 122 | } 123 | } 124 | 125 | public static class DefaultBigIntegerFactory implements DefaultValueFactory { 126 | 127 | @Override 128 | public BigInteger create(BeanClass beanType, SubInstance instance) { 129 | return BigInteger.valueOf(instance.getSequence()); 130 | } 131 | } 132 | 133 | public static class DefaultBigDecimalFactory implements DefaultValueFactory { 134 | 135 | @Override 136 | public BigDecimal create(BeanClass beanType, SubInstance instance) { 137 | return BigDecimal.valueOf(instance.getSequence()); 138 | } 139 | } 140 | 141 | public static class DefaultUUIDFactory implements DefaultValueFactory { 142 | 143 | @Override 144 | public UUID create(BeanClass beanType, SubInstance instance) { 145 | return UUID.fromString(String.format("00000000-0000-0000-0000-%012d", instance.getSequence())); 146 | } 147 | } 148 | 149 | public static class DefaultDateFactory implements DefaultValueFactory { 150 | 151 | @Override 152 | public Date create(BeanClass beanType, SubInstance instance) { 153 | return Date.from(INSTANT_START.plus(instance.getSequence(), ChronoUnit.DAYS)); 154 | } 155 | } 156 | 157 | public static class DefaultInstantFactory implements DefaultValueFactory { 158 | 159 | @Override 160 | public Instant create(BeanClass beanType, SubInstance instance) { 161 | return INSTANT_START.plusSeconds(instance.getSequence()); 162 | } 163 | } 164 | 165 | public static class DefaultLocalTimeFactory implements DefaultValueFactory { 166 | 167 | @Override 168 | public LocalTime create(BeanClass beanType, SubInstance instance) { 169 | return LOCAL_TIME_START.plusSeconds(instance.getSequence()); 170 | } 171 | } 172 | 173 | public static class DefaultLocalDateFactory implements DefaultValueFactory { 174 | 175 | @Override 176 | public LocalDate create(BeanClass beanType, SubInstance instance) { 177 | return LOCAL_DATE_START.plusDays(instance.getSequence()); 178 | } 179 | } 180 | 181 | public static class DefaultLocalDateTimeFactory implements DefaultValueFactory { 182 | 183 | @Override 184 | public LocalDateTime create(BeanClass beanType, SubInstance instance) { 185 | return LOCAL_DATE_TIME_START.plusSeconds(instance.getSequence()); 186 | } 187 | } 188 | 189 | public static class DefaultOffsetDateTimeFactory implements DefaultValueFactory { 190 | 191 | @Override 192 | public OffsetDateTime create(BeanClass beanType, SubInstance instance) { 193 | return INSTANT_START.plusSeconds(instance.getSequence()).atZone(ZoneId.systemDefault()).toOffsetDateTime(); 194 | } 195 | } 196 | 197 | public static class DefaultZoneDateTimeFactory implements DefaultValueFactory { 198 | 199 | @Override 200 | public ZonedDateTime create(BeanClass beanType, SubInstance instance) { 201 | return INSTANT_START.plusSeconds(instance.getSequence()).atZone(ZoneId.systemDefault()); 202 | } 203 | } 204 | 205 | public static class DefaultEnumFactory implements DefaultValueFactory { 206 | 207 | @Override 208 | public Object create(BeanClass beanType, SubInstance instance) { 209 | BeanClass propertyType = instance.getProperty().getType(); 210 | Object[] enumConstants = (propertyType.isCollection() ? propertyType.getElementType() : propertyType) 211 | .getType().getEnumConstants(); 212 | return enumConstants[(instance.getSequence() - 1) % enumConstants.length]; 213 | } 214 | } 215 | 216 | public static class DefaultTypeFactory implements DefaultValueFactory { 217 | private final BeanClass type; 218 | 219 | public DefaultTypeFactory(BeanClass type) { 220 | this.type = type; 221 | } 222 | 223 | @Override 224 | public V create(BeanClass beanType, SubInstance instance) { 225 | return type.createDefault(); 226 | } 227 | 228 | @Override 229 | public Class getType() { 230 | return type.getType(); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/test/java/com/github/leeonky/jfactory/ProducerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.leeonky.jfactory; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.assertj.core.api.AbstractAssert; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Nested; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.lang.reflect.Field; 11 | import java.util.Objects; 12 | 13 | import static com.github.leeonky.jfactory.PropertyChain.propertyChain; 14 | 15 | class ProducerTest { 16 | private final JFactory JFactory = new JFactory(); 17 | private ObjectProducer beanProducer; 18 | 19 | @BeforeEach 20 | void setupCollectionProducer() { 21 | JFactory.factory(Bean.class).spec(instance -> instance.spec() 22 | .property("array1[0]").byFactory() 23 | .property("array2[0]").byFactory() 24 | .property("dependency1").dependsOn("defaultString1", o -> o) 25 | .property("dependency2").dependsOn("defaultString2", o -> o) 26 | .property("unfixed1").value("") 27 | .property("unfixed2").value("") 28 | .link("link1", "link2") 29 | ); 30 | beanProducer = (ObjectProducer) JFactory.type(Bean.class) 31 | .property("inputString1", "a string") 32 | .property("inputString2", "a string") 33 | .property("subObj1.defaultString1", "1") 34 | .property("subObj2.defaultString1", "2") 35 | .createProducer(); 36 | beanProducer.doDependenciesAndLinks(); 37 | } 38 | 39 | @SuppressWarnings("unchecked") 40 | private void assertChange(String message, String from, String to, String result) { 41 | Producer producer = beanProducer.descendant(propertyChain(from)); 42 | new ProducerAssert(producer.changeTo(beanProducer.descendant(propertyChain(to)))). 43 | isSameProducer(beanProducer.descendant(propertyChain(result))); 44 | } 45 | 46 | static class ProducerAssert extends AbstractAssert> { 47 | public ProducerAssert(Producer producer) { 48 | super(producer, ProducerAssert.class); 49 | } 50 | 51 | public ProducerAssert isSameProducer(Producer another) { 52 | isNotNull(); 53 | if (another instanceof ReadOnlyProducer && actual instanceof ReadOnlyProducer) { 54 | try { 55 | Field reader = ReadOnlyProducer.class.getDeclaredField("reader"); 56 | reader.setAccessible(true); 57 | Field parent = ReadOnlyProducer.class.getDeclaredField("parent"); 58 | parent.setAccessible(true); 59 | if (Objects.equals(reader.get(another), reader.get(actual)) 60 | && Objects.equals(parent.get(another), parent.get(actual))) 61 | return this; 62 | failWithMessage("\nExpect: %s\nActual: %s", another, actual); 63 | } catch (Exception e) { 64 | throw new IllegalStateException(e); 65 | } 66 | } 67 | if (!Objects.equals(actual, another)) 68 | failWithMessage("\nExpect: %s\nActual: %s", another, actual); 69 | return this; 70 | } 71 | } 72 | 73 | @Getter 74 | @Setter 75 | public static class Bean { 76 | private String[] array1, array2; 77 | private String defaultString1, defaultString2; 78 | private String inputString1, inputString2; 79 | private Bean readonly1, readonly2; 80 | private String dependency1, dependency2; 81 | private String link1, link2; 82 | private Bean subObj1, subObj2; 83 | private String unfixed1, unfixed2; 84 | } 85 | 86 | public static class AnotherBean extends Spec { 87 | 88 | @Override 89 | public void main() { 90 | property("defaultString1").value("str1"); 91 | } 92 | } 93 | 94 | public static class ABean1 extends Spec { 95 | 96 | @Override 97 | public void main() { 98 | property("readonly1").is(AnotherBean.class); 99 | } 100 | } 101 | 102 | public static class ABean2 extends Spec { 103 | 104 | @Override 105 | public void main() { 106 | property("readonly1").is(AnotherBean.class); 107 | } 108 | } 109 | 110 | @Nested 111 | class Change { 112 | 113 | @Test 114 | void to_collection_should() { 115 | assertChange("override from collection", "array2", "array1", "array1"); 116 | assertChange("override from default value", "defaultString2", "array1", "array1"); 117 | assertChange("default override from dependency value", "dependency2", "array1", "array1"); 118 | assertChange("not override from input", "inputString2", "array1", "inputString2"); 119 | assertChange("default override from link value", "link2", "array1", "array1"); 120 | assertChange("override from object", "subObj2", "array1", "array1"); 121 | assertChange("default override from readonly", "readonly2", "array1", "array1"); 122 | assertChange("override from unfixed", "unfixed2", "array1", "array1"); 123 | } 124 | 125 | @Test 126 | void to_default_value_should() { 127 | assertChange("not override from collection", "array2", "defaultString1", "array2"); 128 | assertChange("override from default value", "defaultString2", "defaultString1", "defaultString1"); 129 | assertChange("not override from dependency value", "dependency2", "defaultString1", "dependency2"); 130 | assertChange("not override from input", "inputString2", "defaultString1", "inputString2"); 131 | assertChange("not override from link value", "link2", "defaultString1", "link2"); 132 | assertChange("not override from object", "subObj2", "defaultString1", "subObj2"); 133 | assertChange("not override from readonly", "readonly2", "defaultString1", "readonly2"); 134 | assertChange("not override from unfixed", "unfixed2", "defaultString1", "unfixed2"); 135 | } 136 | 137 | @Test 138 | void to_dependency_value_should() { 139 | assertChange("override from collection", "array2", "dependency1", "dependency1"); 140 | assertChange("override from default value", "defaultString2", "dependency1", "dependency1"); 141 | assertChange("override from dependency value", "dependency2", "dependency1", "dependency1"); 142 | assertChange("not override from input", "inputString2", "dependency1", "inputString2"); 143 | assertChange("default override from link value", "link2", "dependency1", "dependency1"); 144 | // TODO incorrect preparing data for test case 145 | // assertChange("override from object", "subObj2", "dependency1", "dependency1"); 146 | assertChange("default override from readonly", "readonly2", "dependency1", "dependency1"); 147 | assertChange("override from unfixed", "unfixed2", "dependency1", "dependency1"); 148 | } 149 | 150 | @Test 151 | void to_fixed_value_should() { 152 | assertChange("override from collection", "array2", "inputString1", "inputString1"); 153 | assertChange("override from default value", "defaultString2", "inputString1", "inputString1"); 154 | assertChange("override from dependency value", "dependency2", "inputString1", "inputString1"); 155 | assertChange("override from input", "inputString2", "inputString1", "inputString1"); 156 | assertChange("override from link value", "link2", "inputString1", "inputString1"); 157 | assertChange("override from object", "subObj2", "inputString1", "inputString1"); 158 | assertChange("default override from readonly", "readonly2", "inputString1", "inputString1"); 159 | assertChange("override from unfixed", "unfixed2", "inputString1", "inputString1"); 160 | } 161 | 162 | @Test 163 | void to_link_should() { 164 | assertChange("override from collection", "array2", "link1", "link1"); 165 | assertChange("override from default value", "defaultString2", "link1", "link1"); 166 | assertChange("override from dependency value", "dependency2", "link1", "link1"); 167 | assertChange("not override from input", "inputString2", "link1", "inputString2"); 168 | assertChange("override from link value", "link2", "link1", "link1"); 169 | // TODO incorrect preparing data for test case 170 | // assertChange("override from object", "subObj2", "link1", "link1"); 171 | assertChange("default override from readonly", "readonly2", "link1", "link1"); 172 | assertChange("override from unfixed", "unfixed2", "link1", "link1"); 173 | } 174 | 175 | @Test 176 | void to_object_should() { 177 | assertChange("default override from collection", "array2", "subObj1", "subObj1"); 178 | assertChange("override from default value", "defaultString2", "subObj1", "subObj1"); 179 | assertChange("default override from dependency value", "dependency2", "subObj1", "subObj1"); 180 | assertChange("not override from input", "inputString2", "subObj1", "inputString2"); 181 | assertChange("default override from link value", "link2", "subObj1", "subObj1"); 182 | 183 | assertChange("default override from readonly", "readonly2", "subObj1", "subObj1"); 184 | assertChange("override from unfixed", "unfixed2", "subObj1", "subObj1"); 185 | } 186 | 187 | @Test 188 | void to_readonly_should() { 189 | assertChange("default override from collection", "array2", "readonly1", "readonly1"); 190 | assertChange("default override from default value", "defaultString2", "readonly1", "readonly1"); 191 | assertChange("default override from dependency value", "dependency2", "readonly1", "readonly1"); 192 | assertChange("not override from input", "inputString2", "link1", "inputString2"); 193 | assertChange("default override from link value", "link2", "readonly1", "readonly1"); 194 | assertChange("default override from object", "subObj2", "readonly1", "readonly1"); 195 | assertChange("default override from readonly", "readonly2", "readonly1", "readonly1"); 196 | assertChange("default override from unfixed", "unfixed2", "readonly1", "readonly1"); 197 | } 198 | 199 | @Test 200 | void to_unfixed_should() { 201 | assertChange("override from collection", "array2", "unfixed1", "unfixed1"); 202 | assertChange("override from default value", "defaultString2", "unfixed1", "unfixed1"); 203 | assertChange("default override from dependency value", "dependency2", "unfixed1", "unfixed1"); 204 | assertChange("not override from input", "inputString2", "unfixed1", "inputString2"); 205 | assertChange("default override from link value", "link2", "unfixed1", "unfixed1"); 206 | assertChange("override from object", "subObj2", "unfixed1", "unfixed1"); 207 | assertChange("default override from readonly", "readonly2", "unfixed1", "unfixed1"); 208 | assertChange("override from unfixed", "unfixed2", "unfixed1", "unfixed1"); 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JFactory 2 | 3 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/leeonky/jfactory/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/leeonky/jfactory/tree/master) 4 | [![coveralls](https://img.shields.io/coveralls/github/leeonky/jfactory/master.svg)](https://coveralls.io/github/leeonky/jfactory) 5 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fleeonky%2Fjfactory%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/leeonky/jfactory/master) 6 | [![Lost commit](https://img.shields.io/github/last-commit/leeonky/jfactory.svg)](https://github.com/leeonky/jfactory) 7 | [![Maven Central](https://img.shields.io/maven-central/v/com.github.leeonky/jfactory.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.leeonky/jfactory) 8 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 9 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/6fd6832505594ed09070add129b570a6)](https://www.codacy.com/gh/leeonky/jfactory/dashboard?utm_source=github.com&utm_medium=referral&utm_content=leeonky/jfactory&utm_campaign=Badge_Grade) 10 | [![Maintainability](https://api.codeclimate.com/v1/badges/62a8a3826b05eefd1f3b/maintainability)](https://codeclimate.com/github/leeonky/jfactory/maintainability) 11 | [![Code Climate issues](https://img.shields.io/codeclimate/issues/leeonky/jfactory.svg)](https://codeclimate.com/github/leeonky/jfactory/maintainability) 12 | [![Code Climate maintainability (percentage)](https://img.shields.io/codeclimate/maintainability-percentage/leeonky/jfactory.svg)](https://codeclimate.com/github/leeonky/jfactory/maintainability) 13 | 14 | --- 15 | 16 | 通过工厂方法创建具有某些默认属性测试数据的工具库,默认数据通过预定义数据的默认属性和关联属性实现,还可以定义数据Trait,然后创建具有某些Trait的测试数据 17 | 18 | # 快速开始 19 | 创建具有默认属性值的对象,实例代码默认使用[Lombok](https://projectlombok.org/)自动生成属性访问起代码。 20 | ```java 21 | @Getter 22 | @Setter 23 | public class Bean { 24 | private String stringValue; 25 | private int intValue; 26 | } 27 | 28 | JFactory jFactory = new JFactory(); 29 | 30 | Bean bean = jFactory.create(Bean.class); 31 | // bean.stringValue: "stringValue#1" 32 | // bean.intValue: 1 33 | Bean bean2 = jFactory.create(Bean.class); 34 | // bean.stringValue: "stringValue#2" 35 | // bean.intValue: 2 36 | ``` 37 | 38 | 默认情况下属性值会根据属性名和属性所在类型的创建次数生成一个组合值,也可以在创建过程中给定一个输入值: 39 | ```java 40 | public class Bean { 41 | private String stringValue; 42 | private int intValue; 43 | } 44 | 45 | Bean bean = jFactory.type(Bean.class).property("intValue", 100).create(); 46 | // bean.intValue: 100 47 | ``` 48 | 49 | # 自定义创建 50 | 51 | ## 类型默认Spec 52 | JFactory通过Spec定义对象各个属性值的产生策略: 53 | ```java 54 | public class Bean { 55 | private String stringValue; 56 | private long nowEpochSecond; 57 | } 58 | 59 | jFactory.factory(Bean.class).spec(instance -> instance.spec() 60 | .property(stringValue).value("Hello") 61 | .property(nowEpochSecond).value(() -> Instant.now().getEpochSecond()) 62 | ); 63 | 64 | Bean bean = jFactory.create(Bean.class); 65 | // bean.stringValue: Hello 66 | // bean.nowEpochSecond is epoch second 67 | ``` 68 | 69 | Spec的详细定义都是通过如下代码实现,并且每一项配置仅表示属性的缺省值信息。 70 | ```java 71 | property(stringValue).value("Hello") 72 | ``` 73 | 之所以说是缺省值信息,是因为可以在最终创建对象时通过直接指定属性值的方式覆盖原先的任何Spec定义 74 | ```java 75 | Bean bean = jFactory.type(Bean.class).propery("stringValue", "Bye").create(); 76 | // bean.stringValue: "Bye" 77 | ``` 78 | - 指定类型的构造器 79 | 80 | 如果有些数据类型没有默认构造器,则可以提供一个对象构造器: 81 | ```java 82 | public class Bean { 83 | private int i; 84 | public Bean(int i) { 85 | this.i = i; 86 | } 87 | }; 88 | 89 | jFactory.factory(Bean.class).constructor(instance -> new Bean(instance.getSequence())) 90 | jFactory.create(Bean.class); 91 | ``` 92 | - 定义数据Trait 93 | 94 | 有时从测试的表达性而言,我们往往更关心创建具有某些特征的数据,而不是数据具有某些值的细节。比如: 95 | 96 | ```java 97 | public class Person { 98 | private int age; 99 | private String gender; 100 | }; 101 | 102 | Person person = jFactory.type(Person.class) 103 | .property("age", 20) 104 | .property("gender", "MALE").create() 105 | ``` 106 | 可以预先定义类型的一些具名Spec,然后在构造数据时组合使用: 107 | ```java 108 | jFactory.factory(Person.class) 109 | .spec("成年", instance -> instance.spec().property("age").value(20)) 110 | .spec("男性", instance -> instance.spec().property("gender").value("MALE")) 111 | ); 112 | 113 | Person person = jFactory.type(Person.class).traits("成年", "男性").create(); 114 | ``` 115 | 116 | ## 用Java类定义Spec 117 | 通过JFactory.factory(Class)定义的Spec是这个类型的默认或者说全局Spec,如果一个类型需要有多种截然不同的Spec,则可以通过一个Java类来描述这个Spec: 118 | ```java 119 | public class Person { 120 | private int age; 121 | private String gender; 122 | }; 123 | 124 | public class 女人 extends Spec { 125 | 126 | @Override 127 | public void main() { 128 | property("gender").value("FEMALE"); 129 | } 130 | 131 | @Trait 132 | public 女人 老年的() { 133 | property("age").value(80); 134 | return this; 135 | } 136 | }; 137 | 138 | public class 男人 extends Spec { 139 | 140 | @Override 141 | public void main() { 142 | property("gender").value("MALE"); 143 | } 144 | } 145 | 146 | jFactory.spec(女人.class).traits("老年的").create() 147 | ``` 148 | 其中main方法定义具体的Spec,@Trait注解表示定义Trait。JFactory提供了多种通过Spec类来创建对象的方式: 149 | ```java 150 | jFactory.createAs(女人.class); 151 | jFactory.createAs(女人.class, 女人::老年的); 152 | jFactory.spec(女人.class).create(); 153 | jFactory.spec(女人.class, 女人::老年的).create(); 154 | ``` 155 | 也可以事先注册规Spec,然后通过字符串引用: 156 | ```java 157 | jFactory.register(女人.class); 158 | 159 | jFactory.createAs("老年的", "女人"); 160 | jFactory.spec("女人").traits("老年的").create(); 161 | ``` 162 | 需要注意的是,通过类来描述Spec类似于继承,Spec类都会继承类型的默认全局Spec。比如: 163 | ```java 164 | public class Bean { 165 | private String stringValue; 166 | } 167 | 168 | public class ABean extends Spec { 169 | } 170 | 171 | jFactory.factory(Bean.class).spec(instance -> instance.spec() 172 | .property(stringValue).value("string from base") 173 | ); 174 | 175 | Bean bean = jFactory.createAs(ABean.class); 176 | // bean.stringValue: "string from base" 177 | ``` 178 | 179 | ## 保存/查询创建过的数据 180 | JFactory会用一个实现了DataRepository接口的数据库按类型存储创建过的所有数据。并且支持按照条件查找出曾经创建过的对象 181 | ```java 182 | public class Bean { 183 | private String stringValue; 184 | } 185 | 186 | Bean bean1 = jFactory.type(Bean.class).property("stringValue", "str1").create(); 187 | Bean bean2 = jFactory.type(Bean.class).property("stringValue", "str2").create(); 188 | 189 | Collection queryAll = jFactory.type(Bean.class).queryAll(); 190 | Bean query1 = jFactory.type(Bean.class).property("stringValue", "str1").query(); 191 | Bean query2 = jFactory.type(Bean.class).property("stringValue", "str1").query(); 192 | // queryAll is [bean1, bean2] 193 | // query1 == bean1 194 | // query2 == bean2 195 | 196 | jFactory.getDataRepository().clear(); 197 | // clear DB 198 | ``` 199 | 200 | ## 创建关联对象 201 | 在有些测试场景,往往需要一个相对完整,但又不太在意细节的数据,比如有如下两个类型: 202 | ```java 203 | public class Product { 204 | private String name; 205 | private String color; 206 | } 207 | 208 | public class Order { 209 | private Product product; 210 | } 211 | ``` 212 | 假如需要构造一个“有效“的订单,这里所谓有效是指Order.product属性不能为null,但其实又不关心具体是什么产品,之所以有效是为了不影响当前测试关注点以外被测系统的运行。 213 | 通常通过依次创建对象然后手动关联。 214 | ```java 215 | Product product = jFactory.create(Product.class); 216 | Order order = jFactory.type(Order.class).property("product", product).create(); 217 | ``` 218 | - 在Spec中指定子属性对象 219 | 220 | JFactroy提供了简便创建关联对象的方法可以在一个create调用中创建出具有级联关系的Order实例: 221 | ```java 222 | jFactory.factory(Order.class).spec(instance -> instance.spec() 223 | .property("product").byFactroy()); 224 | 225 | Order order = jFactory.create(Order.class); 226 | //order.product is a default sub created object 227 | ``` 228 | - 通过输入属性指定 229 | 230 | 还有一种方法是可以在创建时直接指定子对象的某个属性值来得到关联对象: 231 | ```java 232 | jFactory.type(Order.class).property("product.name", "book").create(); 233 | ``` 234 | 与前者不同的是,JFactory首先尝试在曾经创建过的所有对象中线性搜索有没有满足name为book的Product,如果有就把那个Product赋值给Order的product属性,如果没有找到就自动构建一个name为book的Product,总之一定会给Order关联到一个name为book的Product。 235 | 236 | 在指定属性值时,也可以在property中指定子属性对象创建的规格和特质: 237 | ```java 238 | public class AProduct extends Spec { 239 | 240 | @Trait 241 | public AProduct 红色的() { 242 | property("color").value("red"); 243 | } 244 | } 245 | jFactory.register(AProduct.class); 246 | jFactory.type(Order.class).property("product(红色的 AProduct).name", "book").create(); 247 | ``` 248 | 注意: 249 | 这里的Spec和Trait只能通过字符串的形式指定。在搜索已创建的对象做关联时不会比较备选对象的Spec和Trait,也就是以上代码只能保证创建出的Order的product的name属性为book。Trait和Spec要写在圆括号内,Trait写在前(可以组合写多个),Spec写在最后,中间用空格或英文逗号分割。 另外Product对象在创建后也会先于Order对象保存进数据库。 250 | 251 | 252 | - 强制创建子属性对象 253 | 254 | 可以用!强制创建子对象而不是查找关联已有对象: 255 | ```java 256 | jFactory.type(Order.class).property("product(红色的 AProduct)!.name", "book").create(); 257 | ``` 258 | 这样无论是否创建过name为book的Product,上面的代码总是会新创建一个name为book的Product,并关联到Order。 259 | 260 | - 引用当前对象 261 | 262 | 如果想在创建对象时引用对象自己,比如: 263 | ```java 264 | public class Bean { 265 | private Bean self; 266 | } 267 | ``` 268 | 想要构造出bean.self = bean的场景,需要在Spec中引用当前所要创建的对象“实例”,不过这个实例是以Supplier形式提供的: 269 | ```java 270 | jFactory.factory(Bean.class).spec(instance -> instance.spec() 271 | .property("self").value(instance.reference()) 272 | ); 273 | 274 | Bean bean = jFactory.create(Bean.class); 275 | // bean.self == bean 276 | ``` 277 | 278 | 有些有父子关系的对象需要在子对象的某个属性引用父对象。比如: 279 | ```java 280 | public class Farther { 281 | private Son son; 282 | } 283 | 284 | public class Son { 285 | private Farther farther; 286 | } 287 | ``` 288 | 289 | 如果想创建出一个Father对象father,并且father.son.father是father,就需要在son中反向引用父对象。 290 | ```java 291 | jFactory.factory(Farther.class).spec(instance -> instance.spec() 292 | .property("son").byFactroy() 293 | .property("son").reverseAssociation("father") 294 | ); 295 | ``` 296 | 并且建立反向引用后,父子对象在创建后保存到数据库的次序会发生改变,没有反向关联的对象会先保存子对象,有反向关联关系的情况下会先保存父对象,再保存子对象。 297 | 298 | ## 属性依赖 299 | 300 | 有的业务数据需要属性之间有某种依赖关系,比如: 301 | 302 | ```java 303 | public class Expression { 304 | private int number1, number2, sum; 305 | } 306 | ``` 307 | 308 | 为了不让测试意外失败,默认创建出的对象必须满足sum = number1 + number2。JFactory支持创建属性依赖Spec: 309 | 310 | ```java 311 | jFactory.factory(Expression.class).spec(instance -> instance.spec() 312 | .property("sum").dependsOn(asList("number1", "number2"), numbers -> (int)numbers[0] + (int)numbers[1]) 313 | ); 314 | 315 | Expression exp1 = jFactory.create(Expression.class); 316 | Expression exp2 = jFactory.type(Expression.class).property("number1", 100).create(); 317 | Expression exp3 = jFactory.type(Expression.class).property("number1", 100).property("number2", 200).create(); 318 | ``` 319 | 但这种依赖也不会永远有效。比如: 320 | ```java 321 | Expression exp4 = jFactory.type(Expression.class).property("sum", 300).create(); 322 | ``` 323 | 这实际上是强制指定了sum的值,因此依赖规则不再有效。 324 | 325 | ## 属性连接 326 | 327 | 有的业务需要多个数据的多个属性保持一致,比如: 328 | 329 | ```java 330 | public class Product { 331 | private int price; 332 | } 333 | 334 | public class Order { 335 | private Product product; 336 | private int total; 337 | } 338 | ``` 339 | 340 | 从有效订单的角度讲,应该最大程度的保证创建出的Order对象的total属性和product.price相等。这个特性在JFactory中可以通过连接属性Spec实现: 341 | 342 | ```java 343 | jFactory.factory(Order.class ).spec(instance -> instance.spec() 344 | .property("product").byFactroy() 345 | .link("total", "product.price") 346 | ); 347 | ``` 348 | 349 | 同样如果在创建时强制指定了不同的product.price和total属性值,这种连接也会失效。 350 | 351 | 多个属性连接后最终的值是多少会根据原先各个属性Spec按如下的优先级得出: 352 | - 创建时赋予的属性值 353 | - 只读值(关联已创建过对象的某个属性) 354 | - 属性依赖 355 | - 通过value方法给定的值 356 | - 默认策略创建的值 357 | 358 | ##### 属性依赖和连接属性都可以操作子属性,这可能会导致复杂的依赖关系甚至是循环依赖,这两个特性本身也存在部分局限,应尽量避免在多层对象中过多使用这两个特性。 359 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------