/*
* Copyright 2015-2017 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 org.springframework.data.redis.core.convert;
import static org.hamcrest.collection.IsEmptyCollection.*;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsCollectionContaining.*;
import static org.hamcrest.core.IsNull.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.data.redis.core.convert.ConversionTestEntities.*;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.hamcrest.core.IsCollectionContaining;
import org.hamcrest.core.IsInstanceOf;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.redis.core.convert.ConversionTestEntities.Address;
import org.springframework.data.redis.core.convert.ConversionTestEntities.AddressWithId;
import org.springframework.data.redis.core.convert.ConversionTestEntities.Item;
import org.springframework.data.redis.core.convert.ConversionTestEntities.Location;
import org.springframework.data.redis.core.convert.ConversionTestEntities.Person;
import org.springframework.data.redis.core.convert.ConversionTestEntities.PersonWithAddressReference;
import org.springframework.data.redis.core.convert.ConversionTestEntities.Size;
import org.springframework.data.redis.core.convert.ConversionTestEntities.TaVeren;
import org.springframework.data.redis.core.convert.ConversionTestEntities.TheWheelOfTime;
import org.springframework.data.redis.core.index.GeoIndexed;
import org.springframework.data.redis.core.index.IndexConfiguration;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.util.ClassTypeInformation;
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
@RunWith(MockitoJUnitRunner.Silent.class)
public class PathIndexResolverUnitTests {
public @Rule ExpectedException exception = ExpectedException.none();
IndexConfiguration indexConfig;
PathIndexResolver indexResolver;
@Mock PersistentProperty<?> propertyMock;
@Before
public void setUp() {
indexConfig = new IndexConfiguration();
this.indexResolver = new PathIndexResolver(
new RedisMappingContext(new MappingConfiguration(indexConfig, new KeyspaceConfiguration())));
}
@Test(expected = IllegalArgumentException.class) // DATAREDIS-425
public void shouldThrowExceptionOnNullMappingContext() {
new PathIndexResolver(null);
}
@Test // DATAREDIS-425
public void shouldResolveAnnotatedIndexOnRootWhenValueIsNotNull() {
Address address = new Address();
address.country = "andor";
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Address.class), address);
assertThat(indexes.size(), is(1));
assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(Address.class.getName(), "country", "andor")));
}
@Test // DATAREDIS-425
public void shouldNotResolveAnnotatedIndexOnRootWhenValueIsNull() {
Address address = new Address();
address.country = null;
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Address.class), address);
assertThat(indexes.size(), is(0));
}
@Test // DATAREDIS-425
public void shouldResolveAnnotatedIndexOnNestedObjectWhenValueIsNotNull() {
Person person = new Person();
person.address = new Address();
person.address.country = "andor";
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Person.class), person);
assertThat(indexes.size(), is(1));
assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "address.country", "andor")));
}
@Test // DATAREDIS-425
public void shouldResolveMultipleAnnotatedIndexesInLists() {
TheWheelOfTime twot = new TheWheelOfTime();
twot.mainCharacters = new ArrayList<Person>();
Person rand = new Person();
rand.address = new Address();
rand.address.country = "andor";
Person zarine = new Person();
zarine.address = new Address();
zarine.address.country = "saldaea";
twot.mainCharacters.add(rand);
twot.mainCharacters.add(zarine);
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TheWheelOfTime.class), twot);
assertThat(indexes.size(), is(2));
assertThat(indexes,
IsCollectionContaining.<IndexedData> hasItems(
new SimpleIndexedPropertyValue(KEYSPACE_TWOT, "mainCharacters.address.country", "andor"),
new SimpleIndexedPropertyValue(KEYSPACE_TWOT, "mainCharacters.address.country", "saldaea")));
}
@Test // DATAREDIS-425
public void shouldResolveAnnotatedIndexesInMap() {
TheWheelOfTime twot = new TheWheelOfTime();
twot.places = new LinkedHashMap<String, ConversionTestEntities.Location>();
Location stoneOfTear = new Location();
stoneOfTear.name = "Stone of Tear";
stoneOfTear.address = new Address();
stoneOfTear.address.city = "tear";
stoneOfTear.address.country = "illian";
twot.places.put("stone-of-tear", stoneOfTear);
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TheWheelOfTime.class), twot);
assertThat(indexes.size(), is(1));
assertThat(indexes,
hasItem(new SimpleIndexedPropertyValue(KEYSPACE_TWOT, "places.stone-of-tear.address.country", "illian")));
}
@Test // DATAREDIS-425
public void shouldResolveConfiguredIndexesInMapOfSimpleTypes() {
indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "physicalAttributes.eye-color"));
Person rand = new Person();
rand.physicalAttributes = new LinkedHashMap<String, String>();
rand.physicalAttributes.put("eye-color", "grey");
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Person.class), rand);
assertThat(indexes.size(), is(1));
assertThat(indexes,
hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "physicalAttributes.eye-color", "grey")));
}
@Test // DATAREDIS-425
public void shouldResolveConfiguredIndexesInMapOfComplexTypes() {
indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "relatives.father.firstname"));
Person rand = new Person();
rand.relatives = new LinkedHashMap<String, Person>();
Person janduin = new Person();
janduin.firstname = "janduin";
rand.relatives.put("father", janduin);
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Person.class), rand);
assertThat(indexes.size(), is(1));
assertThat(indexes,
hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "relatives.father.firstname", "janduin")));
}
@Test // DATAREDIS-425, DATAREDIS-471
public void shouldIgnoreConfiguredIndexesInMapWhenValueIsNull() {
indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "physicalAttributes.eye-color"));
Person rand = new Person();
rand.physicalAttributes = new LinkedHashMap<String, String>();
rand.physicalAttributes.put("eye-color", null);
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Person.class), rand);
assertThat(indexes.size(), is(1));
assertThat(indexes.iterator().next(), IsInstanceOf.instanceOf(RemoveIndexedData.class));
}
@Test // DATAREDIS-425
public void shouldNotResolveIndexOnReferencedEntity() {
PersonWithAddressReference rand = new PersonWithAddressReference();
rand.addressRef = new AddressWithId();
rand.addressRef.id = "emond_s_field";
rand.addressRef.country = "andor";
Set<IndexedData> indexes = indexResolver
.resolveIndexesFor(ClassTypeInformation.from(PersonWithAddressReference.class), rand);
assertThat(indexes.size(), is(0));
}
@Test // DATAREDIS-425
public void resolveIndexShouldReturnNullWhenNoIndexConfigured() {
when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(false);
assertThat(resolve("foo", "rand"), nullValue());
}
@Test // DATAREDIS-425
public void resolveIndexShouldReturnDataWhenIndexConfigured() {
when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(false);
indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "foo"));
assertThat(resolve("foo", "rand"), notNullValue());
}
@Test // DATAREDIS-425
public void resolveIndexShouldReturnDataWhenNoIndexConfiguredButPropertyAnnotated() {
when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true);
when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(Optional.of(createIndexedInstance()));
assertThat(resolve("foo", "rand"), notNullValue());
}
@Test // DATAREDIS-425
public void resolveIndexShouldRemovePositionIndicatorForValuesInLists() {
when(propertyMock.isCollectionLike()).thenReturn(true);
when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true);
when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(Optional.of(createIndexedInstance()));
IndexedData index = resolve("list.[0].name", "rand");
assertThat(index.getIndexName(), is("list.name"));
}
@Test // DATAREDIS-425
public void resolveIndexShouldRemoveKeyIndicatorForValuesInMap() {
when(propertyMock.isMap()).thenReturn(true);
when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true);
when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(Optional.of(createIndexedInstance()));
IndexedData index = resolve("map.[foo].name", "rand");
assertThat(index.getIndexName(), is("map.foo.name"));
}
@Test // DATAREDIS-425
public void resolveIndexShouldKeepNumericalKeyForValuesInMap() {
when(propertyMock.isMap()).thenReturn(true);
when(propertyMock.isAnnotationPresent(eq(Indexed.class))).thenReturn(true);
when(propertyMock.findAnnotation(eq(Indexed.class))).thenReturn(Optional.of(createIndexedInstance()));
IndexedData index = resolve("map.[0].name", "rand");
assertThat(index.getIndexName(), is("map.0.name"));
}
@Test // DATAREDIS-425
public void resolveIndexShouldInspectObjectTypeProperties() {
Item hat = new Item();
hat.type = "hat";
TaVeren mat = new TaVeren();
mat.feature = hat;
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat);
assertThat(indexes.size(), is(1));
assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "feature.type", "hat")));
}
@Test // DATAREDIS-425
public void resolveIndexShouldInspectObjectTypePropertiesButIgnoreNullValues() {
Item hat = new Item();
hat.description = "wide brimmed hat";
TaVeren mat = new TaVeren();
mat.feature = hat;
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat);
assertThat(indexes.size(), is(0));
}
@Test // DATAREDIS-425
public void resolveIndexShouldInspectObjectTypeValuesInMapProperties() {
Item hat = new Item();
hat.type = "hat";
TaVeren mat = new TaVeren();
mat.characteristics = new LinkedHashMap<String, Object>(2);
mat.characteristics.put("clothing", hat);
mat.characteristics.put("gambling", "owns the dark one's luck");
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat);
assertThat(indexes.size(), is(1));
assertThat(indexes,
hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "characteristics.clothing.type", "hat")));
}
@Test // DATAREDIS-425
public void resolveIndexShouldInspectObjectTypeValuesInListProperties() {
Item hat = new Item();
hat.type = "hat";
TaVeren mat = new TaVeren();
mat.items = new ArrayList<Object>(2);
mat.items.add(hat);
mat.items.add("foxhead medallion");
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat);
assertThat(indexes.size(), is(1));
assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "items.type", "hat")));
}
@Test // DATAREDIS-425
public void resolveIndexAllowCustomIndexName() {
indexConfig.addIndexDefinition(new SimpleIndexDefinition(KEYSPACE_PERSON, "items.type", "itemsType"));
Item hat = new Item();
hat.type = "hat";
TaVeren mat = new TaVeren();
mat.items = new ArrayList<Object>(2);
mat.items.add(hat);
mat.items.add("foxhead medallion");
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(TaVeren.class), mat);
assertThat(indexes.size(), is(1));
assertThat(indexes, hasItem(new SimpleIndexedPropertyValue(KEYSPACE_PERSON, "itemsType", "hat")));
}
@Test // DATAREDIS-425
public void resolveIndexForTypeThatHasNoIndexDefined() {
Size size = new Size();
size.height = 10;
size.length = 20;
size.width = 30;
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(Size.class), size);
assertThat(indexes, is(empty()));
}
@Test // DATAREDIS-425
public void resolveIndexOnMapField() {
IndexedOnMapField source = new IndexedOnMapField();
source.values = new LinkedHashMap<String, String>();
source.values.put("jon", "snow");
source.values.put("arya", "stark");
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(IndexedOnMapField.class),
source);
assertThat(indexes.size(), is(2));
assertThat(indexes,
IsCollectionContaining.<IndexedData> hasItems(
new SimpleIndexedPropertyValue(IndexedOnMapField.class.getName(), "values.jon", "snow"),
new SimpleIndexedPropertyValue(IndexedOnMapField.class.getName(), "values.arya", "stark")));
}
@Test // DATAREDIS-425
public void resolveIndexOnListField() {
IndexedOnListField source = new IndexedOnListField();
source.values = new ArrayList<String>();
source.values.add("jon");
source.values.add("arya");
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(IndexedOnListField.class),
source);
assertThat(indexes.size(), is(2));
assertThat(indexes,
IsCollectionContaining.<IndexedData> hasItems(
new SimpleIndexedPropertyValue(IndexedOnListField.class.getName(), "values", "jon"),
new SimpleIndexedPropertyValue(IndexedOnListField.class.getName(), "values", "arya")));
}
@Test // DATAREDIS-509
public void resolveIndexOnPrimitiveArrayField() {
IndexedOnPrimitiveArrayField source = new IndexedOnPrimitiveArrayField();
source.values = new int[] { 1, 2, 3 };
Set<IndexedData> indexes = indexResolver
.resolveIndexesFor(ClassTypeInformation.from(IndexedOnPrimitiveArrayField.class), source);
assertThat(indexes.size(), is(3));
assertThat(indexes,
IsCollectionContaining.<IndexedData> hasItems(
new SimpleIndexedPropertyValue(IndexedOnPrimitiveArrayField.class.getName(), "values", 1),
new SimpleIndexedPropertyValue(IndexedOnPrimitiveArrayField.class.getName(), "values", 2),
new SimpleIndexedPropertyValue(IndexedOnPrimitiveArrayField.class.getName(), "values", 3)));
}
@Test // DATAREDIS-533
public void resolveGeoIndexShouldMapNameCorrectly() {
when(propertyMock.isMap()).thenReturn(true);
when(propertyMock.isAnnotationPresent(eq(GeoIndexed.class))).thenReturn(true);
when(propertyMock.findAnnotation(eq(GeoIndexed.class))).thenReturn(Optional.of(createGeoIndexedInstance()));
IndexedData index = resolve("location", new Point(1D, 2D));
assertThat(index.getIndexName(), is("location"));
}
@Test // DATAREDIS-533
public void resolveGeoIndexShouldMapNameForNestedPropertyCorrectly() {
when(propertyMock.isMap()).thenReturn(true);
when(propertyMock.isAnnotationPresent(eq(GeoIndexed.class))).thenReturn(true);
when(propertyMock.findAnnotation(eq(GeoIndexed.class))).thenReturn(Optional.of(createGeoIndexedInstance()));
IndexedData index = resolve("property.location", new Point(1D, 2D));
assertThat(index.getIndexName(), is("property:location"));
}
@Test // DATAREDIS-533
public void resolveGeoIndexOnPointField() {
GeoIndexedOnPoint source = new GeoIndexedOnPoint();
source.location = new Point(1D, 2D);
Set<IndexedData> indexes = indexResolver.resolveIndexesFor(ClassTypeInformation.from(GeoIndexedOnPoint.class),
source);
assertThat(indexes.size(), is(1));
assertThat(indexes, IsCollectionContaining.<IndexedData> hasItems(
new GeoIndexedPropertyValue(GeoIndexedOnPoint.class.getName(), "location", source.location)));
}
@Test // DATAREDIS-533
public void resolveGeoIndexOnArrayFieldThrowsError() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("GeoIndexed property needs to be of type Point or GeoLocation");
GeoIndexedOnArray source = new GeoIndexedOnArray();
source.location = new double[] { 10D, 20D };
indexResolver.resolveIndexesFor(ClassTypeInformation.from(GeoIndexedOnArray.class), source);
}
private IndexedData resolve(String path, Object value) {
Set<IndexedData> data = indexResolver.resolveIndex(KEYSPACE_PERSON, path, propertyMock, value);
if (data.isEmpty()) {
return null;
}
assertThat(data.size(), is(1));
return data.iterator().next();
}
private Indexed createIndexedInstance() {
return new Indexed() {
@Override
public Class<? extends Annotation> annotationType() {
return Indexed.class;
}
};
}
private GeoIndexed createGeoIndexedInstance() {
return new GeoIndexed() {
@Override
public Class<? extends Annotation> annotationType() {
return GeoIndexed.class;
}
};
}
static class IndexedOnListField {
@Indexed List<String> values;
}
static class IndexedOnPrimitiveArrayField {
@Indexed int[] values;
}
static class IndexedOnMapField {
@Indexed Map<String, String> values;
}
static class GeoIndexedOnPoint {
@GeoIndexed Point location;
}
static class GeoIndexedOnArray {
@GeoIndexed double[] location;
}
}