/**
* Copyright 2013 the original author or authors.
*
* Licensed 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 io.neba.core.resourcemodels.mapping;
import io.neba.api.resourcemodels.AnnotatedFieldMapper;
import io.neba.api.resourcemodels.Optional;
import io.neba.core.resourcemodels.mapping.testmodels.OtherTestResourceModel;
import io.neba.core.resourcemodels.mapping.testmodels.TestResourceModel;
import io.neba.core.resourcemodels.metadata.MappedFieldMetaData;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.SyntheticResource;
import org.apache.sling.api.resource.ValueMap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.LazyLoader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.*;
import static io.neba.api.resourcemodels.AnnotatedFieldMapper.OngoingMapping;
import static io.neba.core.resourcemodels.mapping.AnnotatedFieldMappers.AnnotationMapping;
import static java.lang.Boolean.FALSE;
import static java.util.Collections.emptyList;
import static org.apache.commons.lang.ClassUtils.primitiveToWrapper;
import static org.apache.commons.lang.StringUtils.substringAfterLast;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.*;
/**
* @author Olaf Otto
*/
@RunWith(MockitoJUnitRunner.class)
public class FieldValueMappingCallbackTest {
@Mock
private ValueMap valueMap;
@Mock
private ResourceResolver resourceResolver;
@Mock
private ConfigurableBeanFactory factory;
@Mock
private MappedFieldMetaData mappedFieldMetadata;
@Mock
private Factory lazyLoadingCollectionFactory;
@Mock
private AnnotatedFieldMappers annotatedFieldMappers;
@Mock
private AnnotatedFieldMapper annotatedFieldMapper;
private LazyLoader lazyLoadingCollectionCallback;
private Resource resource;
private Resource parentOfResourceTargetedByMapping;
private Resource resourceTargetedByMapping;
@SuppressWarnings("unused")
private Object mappedFieldOfTypeObject;
@SuppressWarnings("unused")
private String mappedFieldOfTypeString;
private Field mappedField;
private Object targetValue;
private Object model = this;
private OngoingMapping ongoingMapping;
@Before
public void prepareMappedField() throws Exception {
withmappedField("mappedFieldOfTypeObject");
}
@Before
public void prepareTestResource() {
withResource(mock(Resource.class));
}
@Before
public void mockLazyLoadingCollectionFactory() {
Answer<Object> loadImmediately = invocationOnMock -> {
lazyLoadingCollectionCallback = (LazyLoader) invocationOnMock.getArguments()[0];
return lazyLoadingCollectionCallback.loadObject();
};
doAnswer(loadImmediately).when(lazyLoadingCollectionFactory).newInstance(isA(LazyLoader.class));
doReturn(lazyLoadingCollectionFactory).when(this.mappedFieldMetadata).getCollectionProxyFactory();
}
@Before
public void prepareCustomFieldMappers() throws Exception {
doReturn(emptyList()).when(this.annotatedFieldMappers).get(isA(MappedFieldMetaData.class));
}
/**
* The factory must not accept null arguments to its constructor.
*/
@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullModelInConstructor() throws Exception {
new FieldValueMappingCallback(null, this.resource, this.factory, this.annotatedFieldMappers);
}
/**
* The factory must not accept null arguments to its constructor.
*/
@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullResourceInConstructor() throws Exception {
new FieldValueMappingCallback(this.model, null, this.factory, this.annotatedFieldMappers);
}
/**
* The factory must not accept null arguments to its constructor.
*/
@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullFactoryInConstructor() throws Exception {
new FieldValueMappingCallback(this.model, this.resource, null, this.annotatedFieldMappers);
}
/**
* The factory must not accept null arguments to its callback method.
*/
@Test(expected = IllegalArgumentException.class)
public void testHandlingOfNullFactoryInMapping() throws Exception {
new FieldValueMappingCallback(this.model, this.resource, this.factory, this.annotatedFieldMappers).doWith(null);
}
/**
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* private boolean someProperty;
* }
* </pre>
*/
@Test
public void testMappingOfPrimitiveBooleanField() throws Exception {
mapPropertyField(boolean.class, true);
assertFieldIsMapped();
}
/**
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* private int someProperty;
* }
* </pre>
*/
@Test
public void testMappingOfPrimitiveIntField() throws Exception {
mapPropertyField(int.class, 1);
assertFieldIsMapped();
}
/**
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* private long someProperty;
* }
* </pre>
*/
@Test
public void testMappingOfPrimitiveLongField() throws Exception {
mapPropertyField(long.class, 1L);
assertFieldIsMapped();
}
/**
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* private float someProperty;
* }
* </pre>
*/
@Test
public void testMappingOfPrimitiveFloatField() throws Exception {
mapPropertyField(float.class, 1F);
assertFieldIsMapped();
}
/**
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* private double someProperty;
* }
* </pre>
*/
@Test
public void testMappingOfPrimitiveDoubleField() throws Exception {
mapPropertyField(double.class, 1D);
assertFieldIsMapped();
}
/**
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* private short someProperty;
* }
* </pre>
*/
@Test
public void testMappingOfPrimitiveShortField() throws Exception {
mapPropertyField(short.class, (short) 1);
assertFieldIsMapped();
}
/**
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* private String someProperty;
* }
* </pre>
*/
@Test
public void testMappingOfStringField() throws Exception {
mapPropertyField(String.class, "test");
assertFieldIsMapped();
}
/**
* Tests mapping a {@link io.neba.api.annotations.ResourceModel} from a
* {@link org.apache.sling.api.resource.ResourceUtil#isSyntheticResource(org.apache.sling.api.resource.Resource) synthetic}
* resource.
*/
@Test
public void testMappingOfSyntheticResource() throws Exception {
withResource(mock(SyntheticResource.class));
withNullValueMap();
withResourceTargetedByMapping("/absolute/path");
mapComplexFieldWithPath(Resource.class, "/absolute/path");
assertMappedFieldValueIs(this.resourceTargetedByMapping);
}
/**
* It is expected that the result of a retrieval of a property
* from a value map can be <code>null</code>. This must not lead to an exception
* or an attempt to retrieve the value from a child resource.
*/
@Test
public void testMappingOfFieldWithoutValue() throws Exception {
mapPropertyField(String.class, null);
assertFieldIsFetchedFromValueMap();
assertChildResourceIsNotLoadedForField();
assertMappedFieldValueIsNull();
}
/**
* A complex value (not mapped from a resource property but a resource) can be mapped from a child resource
* who's relative path matches the field name or defined {@link io.neba.api.annotations.Path}
* If the path of the corresponding field is not an absolute path to an explicit resource.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* private OtherResourceModel child;
* }
* </pre>
*/
@Test
public void testValueRetrievalFromChildResource() throws Exception {
withResourceTargetedByMapping(child("field"));
withResourceTargetedByMappingAdaptingTo(TestResourceModel.class, new TestResourceModel());
mapChildResourceField(TestResourceModel.class);
assertFieldIsMapped();
}
/**
* A complex value (not mapped from a resource property but a resource) can be mapped from a resource
* who's path matches the absolute {@link io.neba.api.annotations.Path}.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Path}("/absolute/path")
* private OtherResourceModel someProperty;
* }
* </pre>
*/
@Test
public void testValueRetrievalFromAbsolutePath() throws Exception {
withPropertyFieldWithPath(String.class, "/absolute/path/to/value");
mapField();
assertFieldIsNotFetchedFromValueMap();
assertChildResourceIsNotLoadedForField();
}
/**
* A child resource may also mapped directly, i.e. without any adaptation but as a plain
* {@link org.apache.sling.api.resource.Resource}.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* private Resource child;
* }
* </pre>
*/
@Test
public void testDirectMappingOfChildResourceToField() throws Exception {
withResourceTargetedByMapping(child("field"));
mapChildResourceField(Resource.class);
assertMappedFieldValueIs(this.resourceTargetedByMapping);
}
/**
* The resource mapped to the current {@link io.neba.api.annotations.ResourceModel} instance
* can be obtained in the resource model using "@{@link io.neba.api.annotations.This}".
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.This}
* private Resource resource;
* }
* </pre>
*/
@Test
public void testInjectionOfResourceWithThisAnnotation() throws Exception {
mapThisReference();
assertFieldIsMapped();
}
/**
* A reference may be a property of a resource containing a path to another resource.
* Such a reference is automatically resolved and adapted in the presence of a
* {@link io.neba.api.annotations.Reference} annotation.
* For example, the current resource may have a String property called "link", containing the value
* "/path/stored/in/property". The corresponding resource is then resolved and injected.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Reference}
* private Resource link;
* }
* </pre>
*/
@Test
public void testReferenceResolution() throws Exception {
withResourceTargetedByMapping("/path/stored/in/property");
mapSingleReferenceField(Resource.class, "/path/stored/in/property");
assertMappedFieldValueIs(this.resourceTargetedByMapping);
}
/**
* A model may declare lazy 1:1 relationships to other models using the
* {@link io.neba.api.resourcemodels.Optional} interface. An implementation
* of this interface must be provided automatically and must load the
* target value when requested. Example:
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Reference}
* private {@link io.neba.api.resourcemodels.Optional}<Resource> link;
* }
* </pre>
*/
@Test
public void testLazyLoadingReferenceResolution() throws Exception {
withResourceTargetedByMapping("/path/stored/in/property");
withOptionalField();
mapSingleReferenceField(Resource.class, "/path/stored/in/property");
assertOptionalFieldHasValue(this.resourceTargetedByMapping);
}
/**
* When an {@link Optional} reference points to a non-existing target (i.e. a null value),
* invoking {@link io.neba.api.resourcemodels.Optional#get()} must result in a
* {@link java.util.NoSuchElementException}, as only {@link io.neba.api.resourcemodels.Optional#orElse(Object)}
* is null-safe.
*/
@Test(expected = NoSuchElementException.class)
public void testHandlingOfNullWhenNullIsNotAllowedInOptionalReference() throws Exception {
withOptionalField();
mapSingleReferenceField(Resource.class, "/path/stored/in/property");
assertOptionalFieldHasValue(null);
getOptionalValue();
}
/**
* When an {@link Optional} reference points to a non-existing target (i.e. a null value),
* invoking {@link io.neba.api.resourcemodels.Optional#isPresent()} must yield <code>false</code>.
*/
@Test
public void testIsPresentIsFalseWhenOptionalValueIsNull() throws Exception {
withTypeParameter(Resource.class);
withOptionalField();
mapSingleReferenceField(Optional.class, "/path/stored/in/property");
assertOptionalFieldHasValue(null);
assertOptionalValueIsNotPresent();
}
/**
* When an {@link Optional} reference points to an existing target,
* invoking {@link io.neba.api.resourcemodels.Optional#isPresent()} must yield <code>true</code>.
*/
@Test
public void testIsPresentIsFalseWhenOptionalValueIsNotNull() throws Exception {
withResourceTargetedByMapping("/path/stored/in/property");
withOptionalField();
mapSingleReferenceField(Resource.class, "/path/stored/in/property");
assertOptionalFieldHasValue(this.resourceTargetedByMapping);
assertOptionalValueIsPresent();
}
/**
* A reference may be a property of a resource containing an array of paths to other resources.
* Such a references are automatically resolved and adapted in the presence of a
* {@link io.neba.api.annotations.Reference} annotation.
* For example, the current resource may have a String[] property called "links", containing the values
* "/first/path/stored/in/property", "/second/path/stored/in/property".
* The corresponding resources are then resolved and injected.
* Here, the reference is also declared {@link io.neba.api.resourcemodels.Optional}.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Reference}
* private {@link io.neba.api.resourcemodels.Optional}<Collection<Resource>> links;
* }
* </pre>
*/
@Test
public void testOptionalCollectionOfReferencesIsExclusivelyLazyLoadedViaOptional() throws Exception {
String[] referencedResources = new String[]{"/first/path/stored/in/property", "/second/path/stored/in/property"};
withMockResources(referencedResources);
withOptionalField();
mapReferenceCollectionField(Collection.class, Resource.class, referencedResources);
assertMappedFieldValueIsOptional();
loadOptionalField();
assertMappedFieldValueIsCollectionWithResourcesWithPaths(referencedResources);
assertNoLazyLoadingProxyIsCreated();
}
/**
* Here, the above collection of references is not declared {@link io.neba.api.resourcemodels.Optional}.
* Thus, it must be provided as a lazy loading proxy instead.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Reference}
* private Collection<Resource> links;
* }
* </pre>
*/
@Test
public void testCollectionOfReferencesIsLazyLoadedViaProxy() throws Exception {
String[] referencedResources = new String[]{"/first/path/stored/in/property", "/second/path/stored/in/property"};
withMockResources(referencedResources);
mapReferenceCollectionField(Collection.class, Resource.class, referencedResources);
assertMappedFieldValueIsCollectionWithResourcesWithPaths(referencedResources);
assertLazyLoadingProxyIsCreated();
}
/**
* Test the explicitly lazy retrieval of the children of the current resources with adaptation to
* the desired target type (component type of the collection).
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Children}
* private {@link io.neba.api.resourcemodels.Optional}<List<ModelForChild>> children;
* }
* </pre>
*/
@Test
public void testOptionalCollectionOfChildrenIsExclusivelyLazyLoadedViaOptional() throws Exception {
withField(Collection.class);
withOptionalField();
withCollectionTypedField();
withInstantiableCollectionTypedField();
withTypeParameter(TestResourceModel.class);
withChildrenAnnotationPresent();
withResourceTargetedByMapping(child("field"));
withResourceTargetedByMappingAdaptingTo(TestResourceModel.class, new TestResourceModel());
mapField();
assertMappedFieldValueIsOptional();
loadOptionalField();
assertMappedFieldValueIsCollectionContainingTargetValue();
}
/**
* Tests that {@link AnnotatedFieldMapper annotated field mappers} are supported on
* {@link Optional lazy-oding} resource model fields, i.e. that these mappers are invoked when the
* lazy loading callback is triggered in a case such as this:
* <p />
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @MyCustomAnnotation
* private {@link io.neba.api.resourcemodels.Optional}<AnyType> anyField;
* }
* </pre>
*/
@Test
public void testCustomMappersAreAppliedWhenOptionalFieldsAreLoaded() throws Exception {
withField(Collection.class);
withOptionalField();
withCollectionTypedField();
withInstantiableCollectionTypedField();
withCustomFieldMapperMappingTo(new ArrayList<>());
mapField();
assertMappedFieldValueIsOptional();
assertCustomFieldMapperIsNotObtained();
assertCustomFieldMapperIsNotUsedToMapField();
loadOptionalField();
assertCustomFieldMapperIsObtained();
assertCustomFieldMapperIsUsedToMapField();
}
/**
* Test the implicitly lazy retrieval of the children of the current resources with adaptation to
* the desired target type (component type of the collection).
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Children}
* private List<ModelForChild> children;
* }
* </pre>
*/
@Test
public void testCollectionOfChildrenIsLazyLoadedViaProxy() throws Exception {
withField(Collection.class);
withCollectionTypedField();
withInstantiableCollectionTypedField();
withTypeParameter(TestResourceModel.class);
withChildrenAnnotationPresent();
withResourceTargetedByMapping(child("field"));
withResourceTargetedByMappingAdaptingTo(TestResourceModel.class, new TestResourceModel());
mapField();
assertMappedFieldValueIsCollectionContainingTargetValue();
assertLazyLoadingProxyIsCreated();
}
/**
* A {@link io.neba.api.annotations.Reference} may specify an additional
* {@link io.neba.api.annotations.Reference#append() relative path} that is appended to the reference path(s)
* prior to resolution. This way, a resource model can directly use children or parents of referenced resources
* without further programmatic steps, for instance like so:
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Reference}(append = "/jcr:content")
* @{@link io.neba.api.annotations.Path}("page")
* private ValueMap pageContent;
* }
* </pre>
*/
@Test
public void testReferenceResolutionWithAppendedRelativePath() throws Exception {
withResourceTargetedByMapping("/content/resource/child");
withAppendReferenceAppendPath("/child");
mapSingleReferenceField(Resource.class, "/content/resource");
assertMappedFieldValueIs(this.resourceTargetedByMapping);
}
/**
* A reference may be a property of a resource containing an array of paths to other resources.
* Such a references are automatically resolved and adapted in the presence of a
* {@link io.neba.api.annotations.Reference} annotation.
* For example, the current resource may have a String[] property called "links", containing the values
* "/first/path/stored/in/property", "/second/path/stored/in/property".
* The corresponding resources are then resolved and injected.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Reference}
* private Collection<Resource> links;
* }
* </pre>
*/
@Test
public void testCollectionOfReferencesResolution() throws Exception {
String[] referencedResources = new String[]{"/first/path/stored/in/property", "/second/path/stored/in/property"};
withMockResources(referencedResources);
mapReferenceCollectionField(Collection.class, Resource.class, referencedResources);
assertMappedFieldValueIsCollectionWithResourcesWithPaths(referencedResources);
}
/**
* Same as {@link #testCollectionOfReferencesResolution()}, but using a {@link java.util.Set} instead
* of a collection of references.
*/
@Test
public void testSetOfReferencesResolution() throws Exception {
String[] referencedResources = new String[]{"/first/path/stored/in/property", "/second/path/stored/in/property"};
withMockResources(referencedResources);
mapReferenceCollectionField(Set.class, Resource.class, referencedResources);
assertMappedFieldValueIsCollectionWithResourcesWithPaths(referencedResources);
}
/**
* Resources targeted by references may be unresolvable, i.e. their resolution or adaptaion results
* in a <code>null</code> value. In this case, the <code>null</code> value must not
* be stored in the injected collection of references.
*/
@Test
public void testUnresolvableResourcesInListOfReferences() throws Exception {
String[] referencedResources = new String[]{"/first/path/stored/in/property", "/second/path/stored/in/property"};
withResourceTargetedByMapping(referencedResources[0]);
mapReferenceCollectionField(Set.class, Resource.class, referencedResources);
assertMappedFieldValueIsCollectionWithResourcesWithPaths(referencedResources[0]);
}
/**
* {@link io.neba.api.annotations.Path} annotations may contain placeholders of the form
* <code>${variableName}</code>. Such placeholders must be resolved using the {@link org.springframework.context.ApplicationContext}
* of the {@link io.neba.api.annotations.ResourceModel}.
*/
@Test
public void testPlaceholderResolutionInPath() throws Exception {
withConfigurableBeanFactory();
withPlaceholderResolution("text-${language}", "text-de");
withPropertyFieldWithPath(String.class, "text-${language}");
withPathExpressionDetected();
mapField();
assertFieldMapperAttemptsToResolvePlaceholdersIn("text-${language}");
assertFieldMapperLoadsFromValueMap("text-de");
}
/**
* When no value for a placeholder in a path can be resolved, the original path including the placeholder
* shall be used.
*
* @see #testPlaceholderResolutionInPath()
*/
@Test
public void testPlaceholderResolutionWithoutSubstitution() throws Exception {
withConfigurableBeanFactory();
withPropertyFieldWithPath(String.class, "text-${language}");
withPathExpressionDetected();
mapField();
assertFieldMapperAttemptsToResolvePlaceholdersIn("text-${language}");
assertFieldMapperLoadsFromValueMap("text-${language}");
}
/**
* Placeholders can only occur if a {@link io.neba.api.annotations.Path} annotation
* was used. The mapper must thus not attempt to resolve placeholders in paths
* resolved from the field name.
*
* @see #testPlaceholderResolutionInPath()
*/
@Test
public void testPlaceholdersAreOnlyResolvedForPathAnnotationValues() throws Exception {
withConfigurableBeanFactory();
mapPropertyField(String.class, "someValue");
assertFieldMapperDoesNotAttemptToResolvePlaceholders();
}
/**
* A {@link io.neba.api.annotations.This} reference may also adapt the current resource
* to a different type.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.This}
* private OtherResourceModel resource;
* }
* </pre>
*/
@Test
public void testMappingToOtherTestModelAsThisReference() throws Exception {
OtherTestResourceModel target = new OtherTestResourceModel();
Class<OtherTestResourceModel> fieldType = OtherTestResourceModel.class;
withResourceAdaptingTo(fieldType, target);
mapThisReference(fieldType, target);
assertFieldIsMapped();
}
/**
* A {@link io.neba.api.annotations.Path} annotation may point to an absolute resource
* and include an adaptation to the annotated field type.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Path}("/another/resource")
* private OtherResourceModel resource;
* }
* </pre>
*/
@Test
public void testMappingToOtherModelByPath() throws Exception {
withResourceTargetedByMapping("/another/resource");
withResourceTargetedByMappingAdaptingTo(OtherTestResourceModel.class, new OtherTestResourceModel());
mapComplexFieldWithPath(OtherTestResourceModel.class, "/another/resource");
assertFieldIsMapped();
}
/**
* A mapped resource may not have any properties.
* It may however have children that can be mapped. Ensure that the child mapping is executed
* even if the resource's properties (value map) is null.
*/
@Test
public void testChildValuesAreStillResolvedIfResourceHasNoProperties() throws Exception {
withNullValueMap();
withResourceTargetedByMapping(child("field"));
mapChildResourceField(Resource.class);
assertMappedFieldValueIs(this.resourceTargetedByMapping);
}
/**
* Test the retrieval of the children of the current resources.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Children}
* private List<Resource> children;
* }
* </pre>
*/
@Test
public void testChildrenAnnotationOnListOfResources() throws Exception {
withField(Collection.class);
withCollectionTypedField();
withInstantiableCollectionTypedField();
withTypeParameter(Resource.class);
withChildrenAnnotationPresent();
withResourceTargetedByMapping(child("field"));
mapField();
assertMappedFieldValueIsCollectionWithResourcesWithPaths(resourceTargetedByMapping.getPath());
}
/**
* Test the retrieval of the children of the current resources with adaptation to
* the desired target type (component type of the collection).
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Children}
* private List<ModelForChild> children;
* }
* </pre>
*/
@Test
public void testChildrenAnnotationOnListOfModels() throws Exception {
withField(Collection.class);
withCollectionTypedField();
withInstantiableCollectionTypedField();
withTypeParameter(TestResourceModel.class);
withChildrenAnnotationPresent();
withResourceTargetedByMapping(child("field"));
withResourceTargetedByMappingAdaptingTo(TestResourceModel.class, new TestResourceModel());
mapField();
assertMappedFieldValueIsCollectionContainingTargetValue();
}
/**
* Test the retrieval of the children of the resource targeted by the
* relative or absolute {@link io.neba.api.annotations.Path}
* with adaptation to the desired target type (component type of the collection).
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Path}("some/path")
* @{@link io.neba.api.annotations.Children}
* private List<ModelForChild> children;
* }
* </pre>
*/
@Test
public void testChildrenAnnotationWithPathAnnotation() throws Exception {
withResourceTargetedByMapping("field/child");
withParentOfTargetResource("field");
withField(Collection.class);
withInstantiableCollectionTypedField();
withCollectionTypedField();
withTypeParameter(TestResourceModel.class);
withPathAnnotationPresent();
withChildrenAnnotationPresent();
withResourceTargetedByMappingAdaptingTo(TestResourceModel.class, new TestResourceModel());
mapField();
assertMappedFieldValueIsCollectionContainingTargetValue();
}
/**
* Test the retrieval of the children of the resource targeted by the
* {@link io.neba.api.annotations.Reference} stored in the property
* designated by the relative or absolute {@link io.neba.api.annotations.Path}
* with adaptation to the desired target type (component type of the collection).
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Path}("jcr:content/someLink")
* @{@link io.neba.api.annotations.Reference}
* @{@link io.neba.api.annotations.Children}
* private List<ModelForChild> children;
* }
* </pre>
*/
@Test
public void testChildrenAnnotationWithPathAndReferenceAnnotations() throws Exception {
withResourceTargetedByMapping("/referenced/path/child");
withParentOfTargetResource("/referenced/path");
withField(Collection.class);
withCollectionTypedField();
withInstantiableCollectionTypedField();
withTypeParameter(TestResourceModel.class);
withPathAnnotationPresent();
withReferenceAnnotationPresent();
withPropertyValue("/referenced/path");
withChildrenAnnotationPresent();
withResourceTargetedByMappingAdaptingTo(TestResourceModel.class, new TestResourceModel());
mapField();
assertFieldIsFetchedFromValueMap();
assertMappedFieldValueIsCollectionContainingTargetValue();
}
/**
* Test the retrieval of the children of the resource targeted by the
* {@link io.neba.api.annotations.Reference} with adaptation to the
* desired target type (component type of the collection).
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Reference}
* @{@link io.neba.api.annotations.Children}
* private List<ModelForChild> link;
* }
* </pre>
*/
@Test
public void testChildrenAnnotationWithReferenceAnnotation() throws Exception {
withResourceTargetedByMapping("/referenced/path/child");
withParentOfTargetResource("/referenced/path");
withField(Collection.class);
withCollectionTypedField();
withInstantiableCollectionTypedField();
withTypeParameter(TestResourceModel.class);
withReferenceAnnotationPresent();
withPropertyValue("/referenced/path");
withChildrenAnnotationPresent();
withResourceTargetedByMappingAdaptingTo(TestResourceModel.class, new TestResourceModel());
mapField();
assertFieldIsFetchedFromValueMap();
assertMappedFieldValueIsCollectionContainingTargetValue();
}
/**
* A retrieved child may be <code>null</code>, e.g. due to an unsuccessful adaptation.
* Such a <code>null</code> value must not be inserted into the injected collection of children.
*/
@Test
public void testChildrenWithNullValuesAsAdaptationResult() throws Exception {
withField(Collection.class);
withCollectionTypedField();
withInstantiableCollectionTypedField();
withTypeParameter(TestResourceModel.class);
withChildrenAnnotationPresent();
withResourceTargetedByMapping(child("field"));
mapField();
assertMappedFieldValueIsEmptyCollection();
}
/**
* A child may not be retrieved directly, but the children colleciton may also contain children of the children
* in case a {@link io.neba.api.annotations.Children#resolveBelowEveryChild()} path is specified, like so:
* <p/>
* <p>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @{@link io.neba.api.annotations.Children}(resolveBelowEveryChild = "/jcr:content")
* private List<ModelForChild> link;
* }
* </pre>
* </p>
*/
@Test
public void testChildrenWithResolveBelowEveryChildPath() throws Exception {
withField(Collection.class);
withCollectionTypedField();
withInstantiableCollectionTypedField();
withTypeParameter(Resource.class);
withChildrenAnnotationPresent();
withResolveBelowChildPathOnChildren("jcr:content");
Resource content = child(
"child",
"jcr:content");
mapField();
assertMappedFieldValueIsCollectionWithEntries(content);
}
/**
* Properties of a resource may be arrays. In this case, one may use the corresponding collection
* types instead of arrays, e.g. <code>List<String></code> instead of <code>String[]</code>.
*/
@Test
public void testMappingOfArrayPropertyToCollection() throws Exception {
String[] propertyValues = {"first value", "second value"};
withField(Collection.class);
withInstantiableCollectionTypedField();
withTypeParameter(String.class);
withPropertyTypedField();
withPropertyValue(propertyValues);
mapField();
assertFieldIsFetchedFromValueMapAs(String[].class);
assertMappedFieldValueIsCollectionWithEntries((Object[]) propertyValues);
}
/**
* Resource models can also be mapped from resources without properties - i.e. synthetic resources.
* In this case, only fields with absolute or relative mapping paths or non-property types can be resolved.
* Test that a property type is mapped from an absolute path
*/
@Test
public void testResolutionOfPropertyWithAbsolutePath() throws Exception {
withPropertyFieldWithPath(String.class, "/other/resource/propertyName");
withResourceTargetedByMapping("/other/resource/propertyName");
withResourceTargetedByMappingAdaptingTo(String.class, "propertyValue");
mapField();
assertFieldIsMapped();
}
/**
* Resource models can also be mapped from resources without properties - i.e. synthetic resources.
* In this case, only fields with absolute or relative mapping paths or non-property types can be resolved.
* Test that a non-property type is mapped, e.g. from a child resource.
*/
@Test
public void testResolutionOfChildResourceOccursEvenIfResourceHasNoProperties() throws Exception {
withNullValueMap();
withField(Resource.class);
withFieldPath("field");
withResourceTargetedByMapping(child("field"));
mapField();
assertMappedFieldValueIs(this.resourceTargetedByMapping);
}
/**
* Resource models can also be mapped from resources without properties - i.e. synthetic resources.
* In this case, only fields with absolute or relative mapping paths or non-property types can be resolved.
* Test that a property type is mapped from an absolute path
*/
@Test
public void testResolutionOfPropertyWithAbsolutePathOccursEvenIfResourceHasNoProperties() throws Exception {
withNullValueMap();
withPropertyFieldWithPath(String.class, "/other/resource/propertyName");
withResourceTargetedByMapping("/other/resource/propertyName");
withResourceTargetedByMappingAdaptingTo(String.class, "propertyValue");
mapField();
assertFieldIsMapped();
}
/**
* Resource models can also be mapped from resources without properties - i.e. synthetic resources.
* In this case, only fields with absolute or relative mapping paths or non-property types can be resolved.
* Test that a property type is mapped from a relative path
*/
@Test
public void testResolutionOfPropertyWithRelativePathOccursEvenIfResourceHasNoProperties() throws Exception {
withNullValueMap();
withPropertyFieldWithPath(String.class, "../other/resource/propertyName");
withResourceTargetedByMapping("../other/resource/propertyName");
withResourceTargetedByMappingAdaptingTo(String.class, "propertyValue");
mapField();
assertFieldIsMapped();
}
/**
* Resource models can also be mapped from resources without properties - i.e. synthetic resources.
* In this case, only fields with absolute or relative mapping paths or non-property types can be resolved.
* Test that a property type is mapped from an absolute path and that property conversion to Boolean
* occurs by retrieval of the property via the parent's {@link ValueMap} representation.
*/
@Test
public void testResolutionOfPropertyWithAbsolutePathUsesValueMapToRetrieveNonStringValues() throws Exception {
withPropertyFieldWithPath(Boolean.class, "/other/resource/propertyName");
withResourceTargetedByMapping("/other/resource/propertyName");
withParentOfTargetResource("/other/resource");
withParentOfTargetResourceProperty("propertyName", FALSE);
mapField();
assertFieldIsMapped();
}
/**
* Resource models can also be mapped from resources without properties - i.e. synthetic resources.
* In this case, only fields with absolute or relative mapping paths or non-property types can be resolved.
* Test that a property type is mapped from a relative path and that property conversion to Boolean
* occurs by retrieval of the property via the parent's {@link ValueMap} representation.
*/
@Test
public void testResolutionOfPropertyWithRelativePathUsesValueMapToRetrieveNonStringValues() throws Exception {
withPropertyFieldWithPath(Boolean.class, "../other/resource/propertyName");
withResourceTargetedByMapping("../other/resource/propertyName");
withParentOfTargetResource("../other/resource");
withParentOfTargetResourceProperty("propertyName", FALSE);
mapField();
assertFieldIsMapped();
}
/**
* Resource models can also be mapped from resources without properties - i.e. synthetic resources.
* In this case, only fields with absolute or relative mapping paths or non-property types can be resolved.
* Test that the mapping tolerates if the parent of a mapped property does not exist (e.g., mapping to root nodes)
*/
@Test
public void testResolutionOfNonStringPropertyFromForeignResourceToleratesNullParent() throws Exception {
withPropertyFieldWithPath(Boolean.class, "/other/resource/propertyName");
withResourceTargetedByMapping("/other/resource/propertyName");
mapField();
assertMappedFieldValueIsNull();
}
/**
* Resource models can also be mapped from resources without properties - i.e. synthetic resources.
* In this case, only fields with absolute or relative mapping paths or non-property types can be resolved.
* Test that the mapping tolerates if the parent of a mapped property cannot be adapted to {@link ValueMap}
* (e.g. in case of a synthetic resource).
*/
@Test
public void testResolutionOfNonStringPropertyFromForeignResourceToleratesNullValueMap() throws Exception {
withNullValueMap();
withPropertyFieldWithPath(Boolean.class, "/other/resource/propertyName");
withResourceTargetedByMapping("/other/resource/propertyName");
withParentOfTargetResource("/other/resource");
mapField();
assertMappedFieldValueIsNull();
}
/**
* Resource models can also be mapped from resources without properties - i.e. synthetic resources.
* In this case, only fields with absolute or relative mapping paths or non-property types can be resolved.
* Test that the mapping supports resolution of string arrays through direct adaptation from the
* property resource.
*/
@Test
public void testResolutionOfArrayStringPropertyFromForeignResource() throws Exception {
withPropertyFieldWithPath(String[].class, "/other/resource/propertyName");
withResourceTargetedByMapping("/other/resource/propertyName");
withParentOfTargetResource("/other/resource");
withResourceTargetedByMappingAdaptingTo(String[].class, new String[]{"first value", "second value"});
mapField();
assertFieldIsMapped();
}
/**
* It is possible for a developer to define a field
* that could be mapped from a resource property (not final, not static,
* not {@link io.neba.api.annotations.Unmapped} etc.), but with a
* type supported by neither {@link FieldValueMappingCallback} nor
* {@link org.apache.sling.api.resource.ValueMap}.
* In this case, the value must not be mapped.
*/
@Test
public void testMappingOfPropertyToUnsupportedType() throws Exception {
withField(Vector.class);
withTypeParameter(String.class);
withPropertyTypedField();
withPropertyValue(new String[]{"first value", "second value"});
mapField();
assertMappedFieldValueIsNull();
}
/**
* NEBA guarantees that Collection-typed mappable fields are not null. This
* shall hold true regardless of the field semantics.
*/
@Test
public void testPreventionOfNullValuesInReferenceCollectionFieldWithoutDefaultValue() throws Exception {
withField(Collection.class);
withInstantiableCollectionTypedField();
withReferenceAnnotationPresent();
mapField();
assertMappedFieldValueIsEmptyCollection();
}
/**
* NEBA guarantees that Collection-typed mappable fields are not null. This
* shall hold true regardless of the field semantics.
*/
@Test
public void testPreventionOfNullValuesInMappableCollectionFieldWithoutDefaultValue() throws Exception {
withField(Collection.class);
withInstantiableCollectionTypedField();
mapField();
assertMappedFieldValueIsEmptyCollection();
}
/**
* NEBA guarantees that Collection-typed mappable fields are not null. This shall not hold true for
* {@link Optional} collection-typed fields, as those have an explicit {@link Optional#isPresent() empty state}. For example.
* <p/>
* <pre>
* @{@link io.neba.api.annotations.ResourceModel}(types = ...)
* public class MyModel {
* @some.Annotation
* private Optional<List<SomeModel<< optionalList;
* }
* </pre>
*/
@Test
public void testNullValuesAreNotPreventedInOptionalCollectionTypedFields() throws Exception {
withField(Collection.class);
withOptionalField();
withInstantiableCollectionTypedField();
mapField();
assertMappedFieldValueIsOptional();
assertOptionalValueIsNotPresent();
}
/**
* NEBA guarantees that Collection-typed mappable fields are not null. This
* shall hold true regardless of the field semantics.
*/
@Test
public void testPreventionOfNullValuesInMappableCollectionFieldOfSyntheticResource() throws Exception {
withField(Collection.class);
withInstantiableCollectionTypedField();
withNullValueMap();
mapField();
assertMappedFieldValueIsEmptyCollection();
}
/**
* NEBA guarantees that Collection-typed mappable fields are not null. This
* shall hold true regardless of the field semantics. However, if the field already has a non-null default
* value, this value must not be overwritten.
*/
@Test
public void testDefaultValueOfMappableCollectionTypedFieldIsNotOverwritten() throws Exception {
withField(Collection.class);
withInstantiableCollectionTypedField();
Collection<?> defaultValue = mock(Collection.class);
withDefaultFieldValue(defaultValue);
mapField();
assertMappedFieldValueIs(defaultValue);
}
/**
* NEBA guarantees that Collection-typed mappable fields are not null. This
* shall hold true regardless of the field semantics. However, if the field already has a non-null default
* value, this value must not be overwritten.
*/
@Test
public void testDefaultValueOfMappableCollectionTypedReferenceFieldIsNotOverwritten() throws Exception {
withField(Collection.class);
withInstantiableCollectionTypedField();
withReferenceAnnotationPresent();
Collection<?> defaultValue = mock(Collection.class);
withDefaultFieldValue(defaultValue);
mapField();
assertMappedFieldValueIs(defaultValue);
}
/**
* {@link io.neba.api.resourcemodels.AnnotatedFieldMapper Field mappers}
* are applied to all field mappings after the field value was resolved by NEBA,
* i.e. they may override the resolved value. This test scenario verifies that
* a field mapper is applied and can override an already resolved value.
*/
@Test
public void testApplicationOfFieldMappersToResoledFieldValue() throws Exception {
withCustomFieldMapperMappingTo("CustomMappedValue");
mapPropertyField(String.class, "PropertyValue");
assertMappedFieldValueIs("CustomMappedValue");
}
/**
* {@link io.neba.api.resourcemodels.AnnotatedFieldMapper Field mappers} receive extensive
* contextual mapping data, such as the current field, the value that was resolved for it,
* the model, resource and so forth. This test verifies that this contextual data is correct.
*/
@Test
public void testOngoingMappingContainsAccurateMappingData() throws Exception {
withCustomFieldMapperMappingTo("CustomMappedValue");
mapPropertyField(String.class, "PropertyValue");
assertOngoingMappingDataIsAccurate();
}
/**
* To prevent implementations of field mappers from having to worry about instantiating
* suitable collection types for collection-typed fields, NEBA extends its guarantee (mappable collection-typed
* members are never null) to the {@link io.neba.api.resourcemodels.AnnotatedFieldMapper field mappers}.
* This test verifies that field mappers do not receive null for such a collection, even if no value could be resolved.
* Instead, they should receive an empty default value.
*/
@Test
public void testNullCollectionValuesAreSetToDefaultValueBeforeInvokingFieldMappers() throws Exception {
withField(Collection.class);
withInstantiableCollectionTypedField();
withPropertyTypedField();
withTypeParameter(String.class);
withCustomFieldMapperMappingTo(new ArrayList<String>());
mapField();
assertOngoingMappingsResolvedValueIsNotNull();
}
/**
* While NEBA guarantees non-null collections, no such guarantee exists for any other
* field types. This test verifies that null values for non-collection typed fields
* are passed to the {@link io.neba.api.resourcemodels.AnnotatedFieldMapper field mappers}.
*/
@Test
public void testNullNonCollectionValuesAreNullWhenInvokingFieldMappers() throws Exception {
withCustomFieldMapperMappingTo("CustomMappedValue");
mapPropertyField(String.class, null);
assertOngoingMappingsResolvedValueIsNull();
}
/**
* A {@link io.neba.api.resourcemodels.AnnotatedFieldMapper} implementation must take
* care to return an assignment-compatible value as a mapping result. However,
* there are no enforce this at compile time. This test verifies that a suitable exception
* is thrown in case a field mapper returns an incompatible value at runtime.
*/
@Test
public void testHandlingOfIncompatibleReturnValueFromCustomFieldMapper() throws Exception {
withCustomFieldMapperMappingTo(new ArrayList<String>());
withmappedField("mappedFieldOfTypeString");
Exception e = null;
try {
mapPropertyField(String.class, null);
} catch (Exception ex) {
e = ex;
}
assertThat(e)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Can not set java.lang.String field " +
"io.neba.core.resourcemodels.mapping.FieldValueMappingCallbackTest.mappedFieldOfTypeString " +
"to java.util.ArrayList");
}
private void assertOngoingMappingsResolvedValueIsNull() {
assertThat(this.ongoingMapping.getResolvedValue()).isNull();
}
private void assertOngoingMappingsResolvedValueIsNotNull() {
assertThat(this.ongoingMapping.getResolvedValue()).isNotNull();
}
private void assertOngoingMappingDataIsAccurate() {
assertThat(this.ongoingMapping.getField()).isEqualTo(this.mappedField);
assertThat(this.ongoingMapping.getRepositoryPath()).isEqualTo("field");
assertThat(this.ongoingMapping.getFieldType()).isEqualTo(this.mappedFieldMetadata.getType());
assertThat(this.ongoingMapping.getModel()).isEqualTo(this.model);
assertThat(this.ongoingMapping.getProperties()).isNotNull();
assertThat(this.ongoingMapping.getResolvedValue()).isEqualTo(this.targetValue);
assertThat(this.ongoingMapping.getResource()).isSameAs(this.resource);
assertThat(this.ongoingMapping.getFieldTypeParameter()).isSameAs(this.mappedFieldMetadata.getTypeParameter());
}
@SuppressWarnings("unchecked")
private void withCustomFieldMapperMappingTo(final Object value) {
AnnotationMapping mapping = mock(AnnotationMapping.class);
doReturn(this.annotatedFieldMapper).when(mapping).getMapper();
Collection<AnnotationMapping> mappings = new ArrayList<>();
mappings.add(mapping);
doReturn(mappings).when(this.annotatedFieldMappers).get(isA(MappedFieldMetaData.class));
Answer retainMappingContext = invocationOnMock -> {
ongoingMapping = (OngoingMapping) invocationOnMock.getArguments()[0];
return value;
};
doAnswer(retainMappingContext).when(this.annotatedFieldMapper).map(isA(OngoingMapping.class));
}
private void withmappedField(String fieldName) throws NoSuchFieldException {
this.mappedField = getClass().getDeclaredField(fieldName);
}
private void withDefaultFieldValue(Object value) {
this.mappedFieldOfTypeObject = value;
}
private void withInstantiableCollectionTypedField() {
doReturn(true).when(this.mappedFieldMetadata).isInstantiableCollectionType();
}
private void withParentOfTargetResource(String path) {
this.parentOfResourceTargetedByMapping = mock(Resource.class);
when(this.parentOfResourceTargetedByMapping.getPath()).thenReturn(path);
when(this.resourceResolver.getResource(eq(this.resource), eq(path)))
.thenReturn(this.parentOfResourceTargetedByMapping);
@SuppressWarnings("unchecked")
Iterator<Resource> it = mock(Iterator.class);
when(it.hasNext()).thenReturn(true, false);
when(it.next()).thenReturn(this.resourceTargetedByMapping).thenThrow(new IllegalStateException());
when(this.parentOfResourceTargetedByMapping.listChildren()).thenReturn(it);
when(this.resourceTargetedByMapping.getParent()).thenReturn(this.parentOfResourceTargetedByMapping);
}
@SuppressWarnings("unchecked")
private <T> void withParentOfTargetResourceProperty(String propertyName, T propertyValue) {
this.targetValue = propertyValue;
ValueMap properties = mock(ValueMap.class);
when(this.parentOfResourceTargetedByMapping.adaptTo(eq(ValueMap.class))).thenReturn(properties);
when(properties.get(eq(propertyName), eq((Class<T>) propertyValue.getClass()))).thenReturn(propertyValue);
}
private void withChildrenAnnotationPresent() {
doReturn(true).when(this.mappedFieldMetadata).isChildrenAnnotationPresent();
}
private void withResolveBelowChildPathOnChildren(String path) {
doReturn(true).when(this.mappedFieldMetadata).isResolveBelowEveryChildPathPresentOnChildren();
doReturn(path).when(this.mappedFieldMetadata).getResolveBelowEveryChildPathOnChildren();
}
private void mapPropertyField(Class<?> fieldType, Object propertyValue) throws NoSuchFieldException {
withPropertyField(fieldType, propertyValue);
mapField();
this.targetValue = propertyValue;
}
private void mapSingleReferenceField(Class<?> fieldType, String referencePath) throws NoSuchFieldException {
withPropertyField(fieldType, referencePath);
withReferenceAnnotationPresent();
mapField();
}
private void mapReferenceCollectionField(
@SuppressWarnings("rawtypes") Class<? extends Collection> collectionType,
Class<?> componentType, String[] referencePaths)
throws NoSuchFieldException {
withPropertyField(collectionType, referencePaths);
withTypeParameter(componentType);
withReferenceAnnotationPresent();
withInstantiableCollectionTypedField();
withCollectionTypedField();
mapField();
}
private void mapComplexFieldWithPath(Class<?> fieldType, String fieldPath) throws NoSuchFieldException {
withField(fieldType);
withFieldPath(fieldPath);
mapField();
}
private void withPropertyFieldWithPath(Class<?> fieldType, String fieldPath) throws NoSuchFieldException {
withField(fieldType);
withFieldPath(fieldPath);
withPathAnnotationPresent();
withPropertyTypedField();
}
private void withPathExpressionDetected() {
when(this.mappedFieldMetadata.isPathExpressionPresent()).thenReturn(true);
}
private void mapChildResourceField(Class<?> fieldType) throws NoSuchFieldException {
withField(fieldType);
mapField();
}
private void mapThisReference() throws NoSuchFieldException {
mapThisReference(Resource.class, this.resource);
}
private <T> void mapThisReference(Class<T> fieldType, T targetValue) throws NoSuchFieldException {
withField(fieldType);
withThisReferenceTypedField();
this.targetValue = targetValue;
mapField();
}
/**
* Initializes <code>resource</code> with <code>valueMap</code> and <code>resourceResolver</code>.
*/
private void withResource(final Resource mock) {
this.resource = mock;
when(this.resource.adaptTo(eq(ValueMap.class))).thenReturn(this.valueMap);
when(this.resource.getResourceResolver()).thenReturn(this.resourceResolver);
when(this.resource.getPath()).thenReturn("/test/resource/path");
}
private <T> void withResourceAdaptingTo(Class<T> type, T target) {
when(this.resource.adaptTo(eq(type))).thenReturn(target);
this.targetValue = target;
}
private void withPlaceholderResolution(String key, String value) {
when(this.factory.resolveEmbeddedValue(key)).thenReturn(value);
}
private void withConfigurableBeanFactory() {
this.factory = mock(ConfigurableBeanFactory.class);
}
private <T> void withResourceTargetedByMappingAdaptingTo(Class<T> type, T value) {
this.targetValue = value;
when(this.resourceTargetedByMapping.adaptTo(eq(type))).thenReturn(value);
}
private Resource withChildResource(Resource parent, String childName) {
Resource child = mock(Resource.class);
doReturn(child).when(parent).getChild(eq(childName));
String path = parent.getPath() + "/" + childName;
doReturn(path).when(child).getPath();
when(this.resourceResolver.getResource(eq(parent), eq(childName))).thenReturn(child);
@SuppressWarnings("unchecked")
Iterator<Resource> ci = mock(Iterator.class);
when(ci.hasNext()).thenReturn(true, false);
when(ci.next()).thenReturn(child).thenThrow(new IllegalStateException());
doReturn(ci).when(parent).listChildren();
return child;
}
private Resource child(String... childHierarchy) {
Resource currentParent = this.resource;
for (String childName : childHierarchy) {
currentParent = withChildResource(currentParent, childName);
}
return currentParent;
}
private void withOptionalField() {
doReturn(true).when(this.mappedFieldMetadata).isOptional();
}
/**
* Creates a resource mock <code>resourceTargetedByMapping</code> that can be resolved with
* <code>path</code> and returns the path.
*/
private void withResourceTargetedByMapping(String path) {
this.resourceTargetedByMapping = mock(Resource.class);
when(this.resourceTargetedByMapping.getPath()).thenReturn(path);
when(this.resourceResolver.getResource(eq(this.resource), eq(path)))
.thenReturn(this.resourceTargetedByMapping);
when(this.resourceTargetedByMapping.getName()).thenReturn(substringAfterLast(path, "/"));
}
private void withResourceTargetedByMapping(Resource resource) {
this.resourceTargetedByMapping = resource;
}
private void withMockResources(String... absoluteResourcePaths) {
for (String path : absoluteResourcePaths) {
Resource resource = mock(Resource.class);
when(resource.getPath()).thenReturn(path);
when(this.resourceResolver.getResource(eq(this.resource), eq(path)))
.thenReturn(resource);
}
}
private void withNullValueMap() {
when(this.resource.adaptTo(eq(ValueMap.class))).thenReturn(null);
}
private void withPropertyField(Class<?> fieldType, Object propertyValue) throws NoSuchFieldException {
withField(fieldType);
withPropertyTypedField();
withPropertyValue(propertyValue);
}
private void withTypeParameter(Class<?> parameter) {
doReturn(parameter).when(this.mappedFieldMetadata).getTypeParameter();
doReturn(Array.newInstance(parameter, 0).getClass()).when(this.mappedFieldMetadata).getArrayTypeOfTypeParameter();
}
private void withCollectionTypedField() {
doReturn(true).when(this.mappedFieldMetadata).isCollectionType();
}
private void withPathAnnotationPresent() {
doReturn(true).when(this.mappedFieldMetadata).isPathAnnotationPresent();
}
private void withFieldPath(String path) {
doReturn(path).when(this.mappedFieldMetadata).getPath();
}
private void withReferenceAnnotationPresent() {
doReturn(true).when(this.mappedFieldMetadata).isReference();
doReturn(true).when(this.mappedFieldMetadata).isPropertyType();
}
private void withAppendReferenceAppendPath(String relativeAppendPath) {
doReturn(true).when(this.mappedFieldMetadata).isAppendPathPresentOnReference();
doReturn(relativeAppendPath).when(this.mappedFieldMetadata).getAppendPathOnReference();
}
private void withPropertyTypedField() {
doReturn(true).when(this.mappedFieldMetadata).isPropertyType();
}
private <T> void withPropertyValue(T value) {
Class<?> type = value == null ? this.mappedFieldMetadata.getType() : value.getClass();
// primitive types are boxed before retrieval from the value map.
Class<?> retrievedType = primitiveToWrapper(type);
doReturn(value).when(this.valueMap).get(eq("field"), eq(retrievedType));
}
private <T> void withField(Class<T> fieldType) throws NoSuchFieldException {
mappedField.setAccessible(true);
doReturn(mappedField).when(this.mappedFieldMetadata).getField();
doReturn("field").when(this.mappedFieldMetadata).getPath();
doReturn(fieldType).when(this.mappedFieldMetadata).getType();
}
private void mapField() {
new FieldValueMappingCallback(this.model, this.resource, this.factory, this.annotatedFieldMappers)
.doWith(this.mappedFieldMetadata);
}
private void withThisReferenceTypedField() {
doReturn(true).when(this.mappedFieldMetadata).isThisReference();
}
private void loadOptionalField() {
this.mappedFieldOfTypeObject = ((Optional<?>) this.mappedFieldOfTypeObject).orElse(null);
}
private void assertMappedFieldValueIsOptional() {
assertThat(this.mappedFieldOfTypeObject).isInstanceOf(Optional.class);
}
private void assertNoLazyLoadingProxyIsCreated() {
verify(this.lazyLoadingCollectionFactory, never()).newInstance(isA(LazyLoader.class));
}
private void assertLazyLoadingProxyIsCreated() {
verify(this.lazyLoadingCollectionFactory).newInstance(isA(LazyLoader.class));
}
private void assertOptionalFieldHasValue(Object expected) {
assertThat(this.mappedFieldOfTypeObject).isInstanceOf(Optional.class);
assertThat(((Optional<?>) this.mappedFieldOfTypeObject).orElse(null)).isEqualTo(expected);
}
private void assertOptionalValueIsPresent() {
assertThat(((Optional<?>) this.mappedFieldOfTypeObject).isPresent()).describedAs("the optional field is present").isTrue();
}
private void assertOptionalValueIsNotPresent() {
assertThat(((Optional<?>) this.mappedFieldOfTypeObject).isPresent()).describedAs("the optional field is present").isFalse();
}
private void getOptionalValue() {
assertThat(this.mappedFieldOfTypeObject).isInstanceOf(Optional.class);
((Optional<?>) this.mappedFieldOfTypeObject).get();
}
private void assertMappedFieldValueIsCollectionWithResourcesWithPaths(String... referencedResources) {
assertThat(this.mappedFieldOfTypeObject).isInstanceOf(Collection.class);
@SuppressWarnings("unchecked")
Collection<Resource> resources = (Collection<Resource>) this.mappedFieldOfTypeObject;
assertArrayHoldsResourcesWithPaths(resources.toArray(new Resource[resources.size()]), referencedResources);
}
private void assertArrayHoldsResourcesWithPaths(Resource[] array, String... resourcePaths) {
assertThat(array).hasSize(resourcePaths.length);
for (int i = 0; i < resourcePaths.length; ++i) {
assertThat(array[i]).isNotNull();
assertThat(array[i].getPath()).isEqualTo(resourcePaths[i]);
}
}
private void assertFieldMapperDoesNotAttemptToResolvePlaceholders() {
verify(this.factory, never()).resolveEmbeddedValue(anyString());
}
private void assertChildResourceIsNotLoadedForField() {
verify(this.resource, never()).getChild(eq("field"));
}
private void assertFieldIsMapped() {
assertMappedFieldValueIs(this.targetValue);
}
private void assertMappedFieldValueIs(Object value) {
assertThat(this.mappedFieldOfTypeObject).isEqualTo(value);
}
private void assertMappedFieldValueIsNull() {
assertThat(this.mappedFieldOfTypeObject).isNull();
}
private void assertFieldIsFetchedFromValueMap() {
String fieldPath = this.mappedFieldMetadata.getPath();
verify(this.valueMap).get(eq(fieldPath), eq(String.class));
}
private void assertFieldIsNotFetchedFromValueMap() {
String fieldPath = this.mappedFieldMetadata.getPath();
verify(this.valueMap, never()).get(eq(fieldPath), eq(String.class));
}
private void assertFieldIsFetchedFromValueMapAs(Class<?> expectedPropertyType) {
String fieldPath = this.mappedFieldMetadata.getPath();
verify(this.valueMap).get(fieldPath, expectedPropertyType);
}
private void assertFieldMapperLoadsFromValueMap(String key) {
verify(this.valueMap).get(eq(key), eq(String.class));
}
private void assertFieldMapperAttemptsToResolvePlaceholdersIn(String placeholder) {
verify(this.factory).resolveEmbeddedValue(eq(placeholder));
}
private void assertMappedFieldValueIsCollectionContainingTargetValue() {
assertThat(this.mappedFieldOfTypeObject).isInstanceOf(Collection.class);
assertThat((Collection<?>) this.mappedFieldOfTypeObject).containsOnly(this.targetValue);
}
private void assertMappedFieldValueIsEmptyCollection() {
assertThat(this.mappedFieldOfTypeObject).isInstanceOf(Collection.class);
assertThat((Collection<?>) this.mappedFieldOfTypeObject).isEmpty();
}
private void assertMappedFieldValueIsCollectionWithEntries(Object... entries) {
assertThat(this.mappedFieldOfTypeObject).isInstanceOf(Collection.class);
assertThat((Collection<?>) this.mappedFieldOfTypeObject).containsOnly(entries);
}
@SuppressWarnings("unchecked")
private void assertCustomFieldMapperIsUsedToMapField() {
verify(this.annotatedFieldMapper).map(eq(this.ongoingMapping));
}
private void assertCustomFieldMapperIsObtained() {
verify(this.annotatedFieldMappers).get(eq(this.mappedFieldMetadata));
}
@SuppressWarnings("unchecked")
private void assertCustomFieldMapperIsNotUsedToMapField() {
verify(this.annotatedFieldMapper, never()).map(any());
}
private void assertCustomFieldMapperIsNotObtained() {
verify(this.annotatedFieldMappers, never()).get(any());
}
}