blob: 0b67dedd7a1cb9ba748dd88a3d6b541f7034a407 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
*/
package org.apache.polygene.test.entity;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.apache.polygene.api.association.Association;
import org.apache.polygene.api.association.ManyAssociation;
import org.apache.polygene.api.association.NamedAssociation;
import org.apache.polygene.api.common.Optional;
import org.apache.polygene.api.common.UseDefaults;
import org.apache.polygene.api.entity.EntityBuilder;
import org.apache.polygene.api.entity.EntityComposite;
import org.apache.polygene.api.identity.Identity;
import org.apache.polygene.api.injection.scope.Service;
import org.apache.polygene.api.injection.scope.Structure;
import org.apache.polygene.api.property.Property;
import org.apache.polygene.api.structure.Module;
import org.apache.polygene.api.unitofwork.ConcurrentEntityModificationException;
import org.apache.polygene.api.unitofwork.NoSuchEntityException;
import org.apache.polygene.api.unitofwork.UnitOfWork;
import org.apache.polygene.api.unitofwork.UnitOfWorkCompletionException;
import org.apache.polygene.api.value.ValueBuilder;
import org.apache.polygene.api.value.ValueComposite;
import org.apache.polygene.bootstrap.ModuleAssembly;
import org.apache.polygene.spi.entity.EntityState;
import org.apache.polygene.spi.entitystore.EntityStore;
import org.apache.polygene.test.AbstractPolygeneTest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static java.time.ZoneOffset.UTC;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Abstract satisfiedBy with tests for the EntityStore interface.
*/
public abstract class AbstractEntityStoreTest
extends AbstractPolygeneTest
{
@Service
private EntityStore store;
@Structure
protected Module moduleInstance;
private ZonedDateTime refDate = ZonedDateTime.of( 2020, 3, 4, 13, 24, 35, 0, UTC );
@Override
public void assemble( ModuleAssembly module )
{
module.entities( TestEntity.class );
module.values( TestValue.class, TestValue2.class, TjabbaValue.class );
module.objects( getClass() );
}
@Override
@AfterEach
public void tearDown()
{
super.tearDown();
}
protected TestEntity createEntity( UnitOfWork unitOfWork )
throws UnitOfWorkCompletionException
{
// Create entity
EntityBuilder<TestEntity> builder = unitOfWork.newEntityBuilder( TestEntity.class );
builder.instance().instantValue().set( Instant.now() );
TestEntity instance = builder.newInstance();
instance.name().set( "Test" );
instance.intValue().set( 42 );
instance.longValue().set( 42L );
instance.doubleValue().set( 42D );
instance.floatValue().set( 42F );
instance.booleanValue().set( Boolean.TRUE );
instance.bigIntegerValue().set( new BigInteger( "42" ) );
instance.bigDecimalValue().set( new BigDecimal( "42" ) );
instance.instantValue().set( refDate.toInstant() );
instance.dateTimeValue().set( refDate );
instance.localDateTimeValue().set( LocalDateTime.of( 2020, 3, 4, 13, 23, 0 ) );
instance.localDateValue().set( LocalDate.of( 2020, 3, 4 ) );
instance.localTimeValue().set( LocalTime.of( 19, 20, 21 ) );
instance.duractionValue().set( Duration.between( LocalDateTime.of( 2010, 1, 2, 19, 20, 21 ),
LocalDateTime.of( 2010, 1, 2, 20, 21, 22 ) ) );
instance.periodValue().set( Period.between( LocalDate.of( 2005, 12, 21 ), LocalDate.of( 2007, 1, 23 ) ) );
instance.association().set( instance );
ValueBuilder<Tjabba> valueBuilder4 = moduleInstance.newValueBuilder( Tjabba.class );
final Tjabba prototype4 = valueBuilder4.prototype();
prototype4.bling().set( "BlinkLjus" );
// Set value
ValueBuilder<TestValue2> valueBuilder2 = moduleInstance.newValueBuilder( TestValue2.class );
TestValue2 prototype2 = valueBuilder2.prototype();
prototype2.stringValue().set( "Bar" );
Tjabba newValue = valueBuilder4.newInstance();
prototype2.anotherValue().set( newValue );
prototype2.anotherValue().set( newValue );
ValueBuilder<Tjabba> valueBuilder3 = moduleInstance.newValueBuilder( Tjabba.class );
final Tjabba prototype3 = valueBuilder3.prototype();
prototype3.bling().set( "Brakfis" );
ValueBuilder<TestValue> valueBuilder1 = moduleInstance.newValueBuilder( TestValue.class );
TestValue prototype = valueBuilder1.prototype();
prototype.enumProperty().set( TestEnum.VALUE3 );
prototype.listProperty().get().add( "Foo" );
prototype.valueProperty().set( valueBuilder2.newInstance() );
prototype.tjabbaProperty().set( valueBuilder3.newInstance() );
Map<String, String> mapValue = new HashMap<>( 1 );
mapValue.put( "foo", "bar" );
prototype.mapStringStringProperty().set( mapValue );
instance.valueProperty().set( valueBuilder1.newInstance() );
instance.manyAssociation().add( 0, instance );
instance.namedAssociation().put( "foo", instance );
instance.namedAssociation().put( "bar", instance );
return instance;
}
@Test
public void whenNewEntityThenCanFindEntityAndCorrectValues()
throws Exception
{
TestEntity instance;
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
instance = createEntity( unitOfWork );
unitOfWork.complete();
}
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
// Find entity
instance = unitOfWork.get( instance );
// Check state
assertThat( "property 'intValue' has incorrect value",
instance.intValue().get(),
equalTo( 42 ) );
assertThat( "property 'longValue' has incorrect value",
instance.longValue().get(),
equalTo( 42L ) );
assertThat( "property 'doubleValue' has incorrect value",
instance.doubleValue().get(),
equalTo( 42.0 ) );
assertThat( "property 'floatValue' has incorrect value",
instance.floatValue().get(),
equalTo( 42f ) );
assertThat( "property 'booleanValue' has incorrect value",
instance.booleanValue().get(),
equalTo( Boolean.TRUE ) );
assertThat( "property 'bigDecimal' has incorrect value",
instance.bigDecimalValue().get(),
equalTo( new BigDecimal( "42" ) ) );
assertThat( "property 'bigInteger' has incorrect value",
instance.bigIntegerValue().get(),
equalTo( new BigInteger( "42" ) ) );
assertThat( "property 'dateTimeValue' has incorrect value",
instance.dateTimeValue().get(),
equalTo( refDate ) );
assertThat( "property 'instantValue' has incorrect value",
instance.instantValue().get(),
equalTo( refDate.toInstant() ) );
assertThat( "property 'localDateTimeValue' has incorrect value",
instance.localDateTimeValue().get(),
equalTo( LocalDateTime.of( 2020, 3, 4, 13, 23, 0 ) ) );
assertThat( "property 'localDateValue' has incorrect value",
instance.localDateValue().get(),
equalTo( LocalDate.of( 2020, 3, 4 ) ) );
assertThat( "property 'localTimeValue' has incorrect value",
instance.localTimeValue().get(),
equalTo( LocalTime.of( 19, 20, 21 ) ) );
assertThat( "property 'periodValue' has incorrect value",
instance.periodValue().get(),
equalTo( Period.of( 1, 1, 2 ) ) );
assertThat( "property 'durationValue' has incorrect value",
instance.duractionValue().get(),
equalTo( Duration.ofSeconds( 3661 ) ) );
assertThat( "property 'name' has incorrect value",
instance.name().get(),
equalTo( "Test" ) );
assertThat( "property 'unsetName' has incorrect value",
instance.unsetName().get(),
equalTo( null ) );
assertThat( "property 'emptyName' has incorrect value",
instance.emptyName().get(),
equalTo( "" ) );
Property<TestValue> testValueProperty = instance.valueProperty();
TestValue testValue = testValueProperty.get();
Property<TestValue2> testValue2Property = testValue.valueProperty();
TestValue2 testValue2 = testValue2Property.get();
Property<String> stringProperty = testValue2.stringValue();
String actual = stringProperty.get();
assertThat( "property 'valueProperty.stringValue' has incorrect value",
actual,
equalTo( "Bar" ) );
assertThat( "property 'valueProperty.listProperty' has incorrect value",
instance.valueProperty().get().listProperty().get().get( 0 ),
equalTo( "Foo" ) );
assertThat( "property 'valueProperty.enumProperty' has incorrect value",
instance.valueProperty().get().enumProperty().get(),
equalTo( TestEnum.VALUE3 ) );
assertThat( "property 'valueProperty.anotherValue.bling' has incorrect value",
instance.valueProperty().get().valueProperty().get().anotherValue().get().bling().get(),
equalTo( "BlinkLjus" ) );
assertThat( "property 'valueProperty.tjabbaProperty.bling' has incorrect value",
instance.valueProperty().get().tjabbaProperty().get().bling().get(),
equalTo( "Brakfis" ) );
Map<String, String> mapValue = new HashMap<>();
mapValue.put( "foo", "bar" );
assertThat( "property 'valueProperty.mapStringStringProperty' has incorrect value",
instance.valueProperty().get().mapStringStringProperty().get(),
equalTo( mapValue ) );
assertThat( "association has incorrect value",
instance.association().get(),
equalTo( instance ) );
assertThat( "manyAssociation has incorrect value",
instance.manyAssociation().iterator().next(),
equalTo( instance ) );
assertThat( "namedAssociation has incorrect 'foo' value",
instance.namedAssociation().get( "foo" ),
equalTo( instance ) );
assertThat( "namedAssociation has incorrect 'bar' value",
instance.namedAssociation().get( "bar" ),
equalTo( instance ) );
}
}
@Test
public void whenRemovedEntityThenCannotFindEntity()
throws Exception
{
TestEntity newInstance;
Identity identity;
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
newInstance = createEntity( unitOfWork );
identity = newInstance.identity().get();
unitOfWork.complete();
}
// Remove entity
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
TestEntity instance = unitOfWork.get( newInstance );
unitOfWork.remove( instance );
unitOfWork.complete();
}
// Find entity
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
unitOfWork.get( TestEntity.class, identity );
fail( "Should not be able to find entity" );
}
catch( NoSuchEntityException e )
{
// Ok!
}
}
@Test
public void givenEntityIsNotModifiedWhenUnitOfWorkCompletesThenDontStoreState()
throws UnitOfWorkCompletionException
{
TestEntity testEntity;
String version;
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
EntityBuilder<TestEntity> builder = unitOfWork.newEntityBuilder( TestEntity.class );
testEntity = builder.newInstance();
unitOfWork.complete();
}
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
testEntity = unitOfWork.get( testEntity );
version = spi.entityStateOf( testEntity ).version();
unitOfWork.complete();
}
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
testEntity = unitOfWork.get( testEntity );
String newVersion = spi.entityStateOf( testEntity ).version();
assertThat( "version has not changed", newVersion, equalTo( version ) );
unitOfWork.complete();
}
}
@Test
public void givenPropertyIsModifiedWhenUnitOfWorkCompletesThenStoreState()
throws UnitOfWorkCompletionException
{
TestEntity testEntity;
String version;
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
EntityBuilder<TestEntity> builder = unitOfWork.newEntityBuilder( TestEntity.class );
testEntity = builder.newInstance();
unitOfWork.complete();
}
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
testEntity = unitOfWork.get( testEntity );
testEntity.name().set( "Rickard" );
version = spi.entityStateOf( testEntity ).version();
unitOfWork.complete();
}
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
testEntity = unitOfWork.get( testEntity );
String newVersion = spi.entityStateOf( testEntity ).version();
assertThat( "version has not changed", newVersion, not( equalTo( version ) ) );
unitOfWork.complete();
}
}
@Test
public void givenAssociationsModifiedWhenUnitOfWorkCompletesThenStoreState()
throws UnitOfWorkCompletionException
{
TestEntity testEntity;
String version;
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
EntityBuilder<TestEntity> builder = unitOfWork.newEntityBuilder( TestEntity.class );
testEntity = builder.newInstance();
unitOfWork.complete();
}
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
testEntity = unitOfWork.get( testEntity );
testEntity.association().set( testEntity );
testEntity.manyAssociation().add( 0, testEntity );
testEntity.namedAssociation().put( "test", testEntity );
version = spi.entityStateOf( testEntity ).version();
unitOfWork.complete();
}
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
testEntity = unitOfWork.get( testEntity );
String newVersion = spi.entityStateOf( testEntity ).version();
assertThat( "association persisted", testEntity.association().get(), equalTo( testEntity ) );
assertThat( "many association persisted", testEntity.manyAssociation().get( 0 ), equalTo( testEntity ) );
assertThat( "named association persisted", testEntity.namedAssociation().get( "test" ), equalTo( testEntity ) );
assertThat( "version has not changed", newVersion, not( equalTo( version ) ) );
testEntity.association().set( null );
testEntity.manyAssociation().clear();
testEntity.namedAssociation().clear();
unitOfWork.complete();
}
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
testEntity = unitOfWork.get( testEntity );
String newVersion = spi.entityStateOf( testEntity ).version();
assertThat( "association cleared", testEntity.association().get(), nullValue() );
assertThat( "many association cleared", testEntity.manyAssociation().count(), is( 0 ) );
assertThat( "named association cleared", testEntity.namedAssociation().count(), is( 0 ) );
assertThat( "version has not changed", newVersion, not( equalTo( version ) ) );
unitOfWork.complete();
}
}
@Test
public void givenConcurrentUnitOfWorksWhenUoWCompletesThenCheckConcurrentModification()
throws UnitOfWorkCompletionException
{
TestEntity testEntity;
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
EntityBuilder<TestEntity> builder = unitOfWork.newEntityBuilder( TestEntity.class );
testEntity = builder.newInstance();
unitOfWork.complete();
}
UnitOfWork unitOfWork1;
TestEntity testEntity1;
String version;
{
// Start working with Entity in one UoW
unitOfWork1 = unitOfWorkFactory.newUnitOfWork();
testEntity1 = unitOfWork1.get( testEntity );
version = spi.entityStateOf( testEntity1 ).version();
if( version.isEmpty() )
{
unitOfWork1.discard();
return; // Store doesn't track versions - no point in testing it
}
testEntity1.name().set( "A" );
testEntity1.unsetName().set( "A" );
}
try( UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork() )
{
// Start working with same Entity in another UoW, and complete it
TestEntity testEntity2 = unitOfWork.get( testEntity );
assertThat( "version is correct", spi.entityStateOf( testEntity1 ).version(), equalTo( version ) );
testEntity2.name().set( "B" );
unitOfWork.complete();
}
{
// Try to complete first UnitOfWork
try
{
unitOfWork1.complete();
fail( "Should have thrown concurrent modification exception" );
}
catch( ConcurrentEntityModificationException e )
{
unitOfWork1.discard();
}
}
{
// Check values
unitOfWork1 = unitOfWorkFactory.newUnitOfWork();
testEntity1 = unitOfWork1.get( testEntity );
assertThat( "property name has not been set", testEntity1.name().get(), equalTo( "B" ) );
assertThat( "version is incorrect", spi.entityStateOf( testEntity1 ).version(),
not( equalTo( version ) ) );
unitOfWork1.discard();
}
}
@Test
public void givenEntityStoredLoadedChangedWhenUnitOfWorkDiscardsThenDontStoreState()
throws UnitOfWorkCompletionException
{
UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork();
try
{
TestEntity entity1 = createEntity( unitOfWork );
Identity identity = entity1.identity().get();
unitOfWork.complete();
unitOfWork = unitOfWorkFactory.newUnitOfWork();
TestEntity entity = unitOfWork.get( TestEntity.class, identity );
assertThat( entity.intValue().get(), is( 42 ) );
entity.intValue().set( 23 );
unitOfWork.discard();
unitOfWork = unitOfWorkFactory.newUnitOfWork();
entity = unitOfWork.get( TestEntity.class, identity );
assertThat( entity.intValue().get(), is( 42 ) );
}
finally
{
unitOfWork.discard();
}
}
@Test
public void entityStatesSPI()
{
EntityStore entityStore = serviceFinder.findService( EntityStore.class ).get();
try( Stream<EntityState> states = entityStore.entityStates( module ) )
{
assertThat( states.count(), is( 0L ) );
}
UnitOfWork unitOfWork = unitOfWorkFactory.newUnitOfWork();
TestEntity newInstance = createEntity( unitOfWork );
unitOfWork.complete();
try( Stream<EntityState> states = entityStore.entityStates( module ) )
{
assertThat( states.count(), is( 1L ) );
}
unitOfWork = unitOfWorkFactory.newUnitOfWork();
TestEntity instance = unitOfWork.get( newInstance );
unitOfWork.remove( instance );
unitOfWork.complete();
try( Stream<EntityState> states = entityStore.entityStates( module ) )
{
assertThat( states.count(), is( 0L ) );
}
}
public interface TestEntity
extends EntityComposite
{
@UseDefaults
Property<Integer> intValue();
@UseDefaults
Property<Long> longValue();
@UseDefaults
Property<Double> doubleValue();
@UseDefaults
Property<Float> floatValue();
@UseDefaults
Property<Boolean> booleanValue();
@Optional
Property<BigInteger> bigIntegerValue();
@Optional
Property<BigDecimal> bigDecimalValue();
@Optional
Property<Instant> instantValue();
@Optional
Property<ZonedDateTime> dateTimeValue();
@Optional
Property<LocalDateTime> localDateTimeValue();
@Optional
Property<LocalDate> localDateValue();
@Optional
Property<LocalTime> localTimeValue();
@Optional
Property<Period> periodValue();
@Optional
Property<Duration> duractionValue();
@Optional
Property<String> name();
@Optional
Property<String> unsetName();
@UseDefaults
Property<String> emptyName();
@Optional
Property<TestValue> valueProperty();
@Optional
Association<TestEntity> association();
@Optional
Association<TestEntity> unsetAssociation();
ManyAssociation<TestEntity> manyAssociation();
NamedAssociation<TestEntity> namedAssociation();
}
public interface TjabbaValue
extends Tjabba, ValueComposite
{
}
public interface Tjabba
{
Property<String> bling();
}
public interface TestValue
extends ValueComposite
{
@UseDefaults
Property<String> stringProperty();
@UseDefaults
Property<Integer> intProperty();
@UseDefaults
Property<TestEnum> enumProperty();
@UseDefaults
Property<List<String>> listProperty();
@UseDefaults
Property<Map<String, Tjabba>> mapProperty();
Property<TestValue2> valueProperty();
Property<Tjabba> tjabbaProperty();
Property<Map<String, String>> mapStringStringProperty();
}
public interface TestValue2
extends ValueComposite
{
Property<String> stringValue();
Property<Tjabba> anotherValue();
}
public enum TestEnum
{
VALUE1, VALUE2, VALUE3
}
}