diff --git a/src/main/java/ru/rt/restream/reindexer/ReindexerConfiguration.java b/src/main/java/ru/rt/restream/reindexer/ReindexerConfiguration.java index 7d9551a..a74ab59 100644 --- a/src/main/java/ru/rt/restream/reindexer/ReindexerConfiguration.java +++ b/src/main/java/ru/rt/restream/reindexer/ReindexerConfiguration.java @@ -22,6 +22,8 @@ import ru.rt.restream.reindexer.binding.cproto.DataSourceConfiguration; import ru.rt.restream.reindexer.binding.cproto.DataSourceFactory; import ru.rt.restream.reindexer.binding.cproto.DataSourceFactoryStrategy; +import ru.rt.restream.reindexer.convert.FieldConverterRegistry; +import ru.rt.restream.reindexer.convert.FieldConverterRegistryFactory; import ru.rt.restream.reindexer.exceptions.UnimplementedException; import java.net.URI; @@ -29,6 +31,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; /** * Represents approach for bootstrapping Reindexer. @@ -103,6 +106,17 @@ public ReindexerConfiguration dataSourceFactory(DataSourceFactory dataSourceFact return this; } + /** + * Allows customizing a {@link FieldConverterRegistry}. + * + * @param customizer the {@link FieldConverterRegistry} customizer. + * @return the {@link ReindexerConfiguration} for further customizations + */ + public ReindexerConfiguration fieldConverterRegistry(Consumer customizer) { + customizer.accept(FieldConverterRegistryFactory.INSTANCE); + return this; + } + /** * Configure reindexer connection pool size. Defaults to 8. * diff --git a/src/main/java/ru/rt/restream/reindexer/annotations/Convert.java b/src/main/java/ru/rt/restream/reindexer/annotations/Convert.java new file mode 100644 index 0000000..97963ce --- /dev/null +++ b/src/main/java/ru/rt/restream/reindexer/annotations/Convert.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.annotations; + +import ru.rt.restream.reindexer.convert.FieldConverter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies how fields are converted between the Reindexer database type + * and the one used within the POJO representation. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Convert { + + /** + * Specifies a {@link FieldConverter} implementation to be used for converting + * fields between Reindexer database type and the one used within the POJO representation. + * @return the {@link FieldConverter} implementation to use + */ + Class converterClass() default FieldConverter.class; + + /** + * Specifies whether conversion should be disabled for the given field, + * useful in case of global converter should not be applied for specific fields. + * Defaults to {@literal false}. + * @return true, if conversion should be disabled for the given field, defaults to {@literal false} + */ + boolean disableConversion() default false; +} diff --git a/src/main/java/ru/rt/restream/reindexer/annotations/ReindexAnnotationScanner.java b/src/main/java/ru/rt/restream/reindexer/annotations/ReindexAnnotationScanner.java index edefaa0..25b2e2a 100644 --- a/src/main/java/ru/rt/restream/reindexer/annotations/ReindexAnnotationScanner.java +++ b/src/main/java/ru/rt/restream/reindexer/annotations/ReindexAnnotationScanner.java @@ -21,9 +21,14 @@ import ru.rt.restream.reindexer.IndexType; import ru.rt.restream.reindexer.ReindexScanner; import ru.rt.restream.reindexer.ReindexerIndex; +import ru.rt.restream.reindexer.convert.FieldConverter; +import ru.rt.restream.reindexer.convert.util.ConversionUtils; +import ru.rt.restream.reindexer.convert.FieldConverterRegistryFactory; import ru.rt.restream.reindexer.exceptions.IndexConflictException; import ru.rt.restream.reindexer.fulltext.FullTextConfig; import ru.rt.restream.reindexer.util.BeanPropertyUtils; +import ru.rt.restream.reindexer.convert.util.ResolvableType; +import ru.rt.restream.reindexer.util.Pair; import ru.rt.restream.reindexer.vector.HnswConfig; import ru.rt.restream.reindexer.vector.HnswConfigs; import ru.rt.restream.reindexer.vector.IvfConfig; @@ -33,11 +38,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -341,28 +343,24 @@ private ReindexerIndex createIndex(String reindexPath, List jsonPath, In } private FieldInfo getFieldInfo(Field field) { - Class type = field.getType(); FieldInfo fieldInfo = new FieldInfo(); - fieldInfo.isArray = type.isArray() || Collection.class.isAssignableFrom(type); - FieldType fieldType = null; - if (type.isArray()) { - Class componentType = type.getComponentType(); + FieldType fieldType; + FieldConverter converter = FieldConverterRegistryFactory.INSTANCE.getFieldConverter(field); + ResolvableType resolvableType; + if (converter != null) { + Pair convertiblePair = converter.getConvertiblePair(); + resolvableType = convertiblePair.getSecond(); + } else { + resolvableType = ConversionUtils.resolveFieldType(field); + } + fieldInfo.isArray = resolvableType.isCollectionLike(); + if (fieldInfo.isArray) { + Class componentType = getFieldType(field, resolvableType.getComponentType()); fieldType = getFieldTypeByClass(componentType); fieldInfo.componentType = componentType; - fieldInfo.isFloatVector = (fieldType == FLOAT); - } else if (field.getGenericType() instanceof ParameterizedType && fieldInfo.isArray) { - ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType(); - Type typeArgument = parameterizedType.getActualTypeArguments()[0]; - if (typeArgument instanceof Class) { - Class componentType = (Class) typeArgument; - fieldType = getFieldTypeByClass(componentType); - fieldInfo.componentType = componentType; - } - } else if (Enum.class.isAssignableFrom(type)) { - Enumerated enumerated = field.getAnnotation(Enumerated.class); - fieldType = enumerated != null && enumerated.value() == EnumType.STRING ? STRING : INT; + fieldInfo.isFloatVector = resolvableType.getType().isArray() && fieldType == FLOAT; } else { - fieldType = getFieldTypeByClass(type); + fieldType = getFieldTypeByClass(getFieldType(field, resolvableType.getType())); } if (fieldType == null) { @@ -372,6 +370,14 @@ private FieldInfo getFieldInfo(Field field) { fieldInfo.fieldType = fieldType; return fieldInfo; } + + private Class getFieldType(Field field, Class type) { + if (Enum.class.isAssignableFrom(type)) { + Enumerated enumerated = field.getAnnotation(Enumerated.class); + return enumerated != null && enumerated.value() == EnumType.STRING ? String.class : Integer.class; + } + return type; + } private FieldType getFieldTypeByClass(Class type) { return MAPPED_TYPES.getOrDefault(type, COMPOSITE); diff --git a/src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CJsonItemWriter.java b/src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CJsonItemWriter.java index e8fbad8..a9a882f 100644 --- a/src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CJsonItemWriter.java +++ b/src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CJsonItemWriter.java @@ -23,8 +23,12 @@ import ru.rt.restream.reindexer.binding.cproto.ByteBuffer; import ru.rt.restream.reindexer.binding.cproto.ItemWriter; import ru.rt.restream.reindexer.binding.cproto.cjson.encdec.CjsonEncoder; +import ru.rt.restream.reindexer.convert.FieldConverter; +import ru.rt.restream.reindexer.convert.FieldConverterRegistryFactory; import ru.rt.restream.reindexer.util.BeanPropertyUtils; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.List; import java.util.UUID; @@ -43,11 +47,11 @@ public CJsonItemWriter(CtagMatcher ctagMatcher) { @Override public void writeItem(ByteBuffer buffer, T item) { CjsonEncoder cjsonEncoder = new CjsonEncoder(ctagMatcher); - byte[] itemData = cjsonEncoder.encode(toCjson(item)); + byte[] itemData = cjsonEncoder.encode(toCjson(item, CJsonItemWriter::defaultExtract)); buffer.writeBytes(itemData); } - private CjsonElement toCjson(Object source) { + private CjsonElement toCjson(Object source, AnnotationExtractor annotationExtractor) { if (source == null) { return CjsonNull.INSTANCE; } @@ -70,19 +74,25 @@ private CjsonElement toCjson(Object source) { return new CjsonPrimitive((Float) source); } else if (source instanceof UUID) { return new CjsonPrimitive((UUID) source); - } else if (source instanceof List) { + } else if (source instanceof Enum) { + Enumerated enumerated = annotationExtractor.extract(Enumerated.class); + if (enumerated != null && enumerated.value() == EnumType.STRING) { + return new CjsonPrimitive(((Enum) source).name()); + } + int ordinal = ((Enum) source).ordinal(); + return new CjsonPrimitive((long) ordinal); + } else if (source instanceof Iterable) { CjsonArray cjsonArray = new CjsonArray(); - List sourceList = (List) source; - for (Object element : sourceList) { - CjsonElement cjsonElement = toCjson(element); + for (Object element : (Iterable) source) { + CjsonElement cjsonElement = toCjson(element, annotationExtractor); cjsonArray.add(cjsonElement); } return cjsonArray; - } else if (source.getClass().isArray() && source.getClass().getComponentType() == float.class) { - float[] floatVector = (float[]) source; + } else if (source.getClass().isArray()) { + int length = Array.getLength(source); CjsonArray cjsonArray = new CjsonArray(); - for (float el : floatVector) { - cjsonArray.add(new CjsonPrimitive(el)); + for (int i = 0; i < length; i++) { + cjsonArray.add(toCjson(Array.get(source, i), annotationExtractor)); } return cjsonArray; } else { @@ -93,22 +103,18 @@ private CjsonElement toCjson(Object source) { continue; } Object fieldValue = readFieldValue(source, field); + FieldConverter converter = FieldConverterRegistryFactory.INSTANCE.getFieldConverter(field); + if (converter != null) { + fieldValue = converter.convertToDatabaseType(fieldValue); + } if (fieldValue != null) { CjsonElement cjsonElement; // hack for serialization of String field with Reindex.isUuid() == true as UUID. - if (field.getType() == String.class && field.isAnnotationPresent(Reindex.class) + if (fieldValue instanceof String && field.isAnnotationPresent(Reindex.class) && field.getAnnotation(Reindex.class).isUuid()) { cjsonElement = new CjsonPrimitive(UUID.fromString((String) fieldValue)); - } else if (Enum.class.isAssignableFrom(field.getType())) { - Enumerated enumerated = field.getAnnotation(Enumerated.class); - if (enumerated != null && enumerated.value() == EnumType.STRING) { - cjsonElement = new CjsonPrimitive(((Enum) fieldValue).name()); - } else { - int ordinal = ((Enum) fieldValue).ordinal(); - cjsonElement = new CjsonPrimitive((long) ordinal); - } } else { - cjsonElement = toCjson(fieldValue); + cjsonElement = toCjson(fieldValue, field::getAnnotation); } Json json = field.getAnnotation(Json.class); String tagName = json == null ? field.getName() : json.value(); @@ -123,4 +129,11 @@ private Object readFieldValue(Object source, Field field) { return BeanPropertyUtils.getProperty(source, field.getName()); } + private interface AnnotationExtractor { + A extract(Class annotationClass); + } + + private static A defaultExtract(Class annotationClass) { + return null; + } } diff --git a/src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CjsonItemReader.java b/src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CjsonItemReader.java index 7515212..4de12fc 100644 --- a/src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CjsonItemReader.java +++ b/src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CjsonItemReader.java @@ -21,14 +21,18 @@ import ru.rt.restream.reindexer.binding.cproto.ByteBuffer; import ru.rt.restream.reindexer.binding.cproto.ItemReader; import ru.rt.restream.reindexer.binding.cproto.cjson.encdec.CjsonDecoder; +import ru.rt.restream.reindexer.convert.FieldConverter; +import ru.rt.restream.reindexer.convert.FieldConverterRegistryFactory; import ru.rt.restream.reindexer.util.BeanPropertyUtils; +import ru.rt.restream.reindexer.convert.util.ConversionUtils; +import ru.rt.restream.reindexer.convert.util.ResolvableType; +import ru.rt.restream.reindexer.util.CollectionUtils; +import ru.rt.restream.reindexer.util.Pair; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Iterator; +import java.util.Collection; import java.util.List; import java.util.UUID; @@ -71,46 +75,45 @@ private V readObject(CjsonObject cjsonObject, Class itemClass) { } private Object getTargetValue(Field field, CjsonElement property) { - Class fieldType = field.getType(); + FieldConverter converter = FieldConverterRegistryFactory.INSTANCE.getFieldConverter(field); + if (converter != null) { + Pair convertiblePair = converter.getConvertiblePair(); + return converter.convertToFieldType(getTargetValue(field, convertiblePair.getSecond(), property)); + } + ResolvableType resolvableType = ConversionUtils.resolveFieldType(field); + return getTargetValue(field, resolvableType, property); + } + + private Object getTargetValue(Field field, ResolvableType resolvableType, CjsonElement property) { if (property.isNull()) { - if (fieldType == List.class) { - return new ArrayList<>(); + if (resolvableType.isCollectionLike()) { + return resolvableType.getType().isArray() ? Array.newInstance(resolvableType.getComponentType(), 0) + : CollectionUtils.createCollection(resolvableType.getType(), resolvableType.getComponentType(), 0); } return null; } - if (fieldType == List.class) { - CjsonArray array = property.getAsCjsonArray(); - ArrayList elements = new ArrayList<>(); - ParameterizedType genericType = (ParameterizedType) field.getGenericType(); - Type elementType = genericType.getActualTypeArguments()[0]; - for (CjsonElement cjsonElement : array) { - elements.add(convert(cjsonElement, (Class) elementType)); + if (resolvableType.isCollectionLike()) { + List elements = property.getAsCjsonArray().list(); + if (resolvableType.getType().isArray()) { + Object array = Array.newInstance(resolvableType.getComponentType(), elements.size()); + for (int i = 0; i < elements.size(); i++) { + Array.set(array, i, convert(elements.get(i), resolvableType.getComponentType(), field)); + } + return array; } - return elements; - } else if (Enum.class.isAssignableFrom(fieldType)) { - Enumerated enumerated = field.getAnnotation(Enumerated.class); - if (enumerated != null && enumerated.value() == EnumType.STRING) { - return Enum.valueOf(fieldType.asSubclass(Enum.class), property.getAsString()); + Collection collection = CollectionUtils + .createCollection(resolvableType.getType(), resolvableType.getComponentType(), elements.size()); + for (CjsonElement element : elements) { + collection.add(convert(element, resolvableType.getComponentType(), field)); } - return fieldType.getEnumConstants()[property.getAsInteger()]; - } else if ( fieldType.isArray() && fieldType.getComponentType() == float.class) { - // float_vector - CjsonArray array = property.getAsCjsonArray(); - int size = array.list().size(); - float[] elements = new float[size]; - int i = 0; - Iterator iterator = array.iterator(); - while (iterator.hasNext()) { - elements[i++] = iterator.next().getAsFloat(); - } - return elements; + return collection; } else { - return convert(property, field.getType()); + return convert(property, resolvableType.getType(), field); } } - private Object convert(CjsonElement element, Class targetClass) { + private Object convert(CjsonElement element, Class targetClass, Field field) { if (element.isNull()) { return null; } else if (targetClass == Integer.class || targetClass == int.class) { @@ -131,6 +134,12 @@ private Object convert(CjsonElement element, Class targetClass) { return element.getAsFloat(); } else if (targetClass == UUID.class) { return element.getAsUuid(); + } else if (Enum.class.isAssignableFrom(targetClass)) { + Enumerated enumerated = field.getAnnotation(Enumerated.class); + if (enumerated != null && enumerated.value() == EnumType.STRING) { + return Enum.valueOf(targetClass.asSubclass(Enum.class), element.getAsString()); + } + return targetClass.getEnumConstants()[element.getAsInteger()]; } else if (element.isObject()) { return readObject(element.getAsCjsonObject(), targetClass); } else { diff --git a/src/main/java/ru/rt/restream/reindexer/convert/FieldConverter.java b/src/main/java/ru/rt/restream/reindexer/convert/FieldConverter.java new file mode 100644 index 0000000..5a5a1aa --- /dev/null +++ b/src/main/java/ru/rt/restream/reindexer/convert/FieldConverter.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.convert; + +import ru.rt.restream.reindexer.convert.util.ConversionUtils; +import ru.rt.restream.reindexer.convert.util.ResolvableType; +import ru.rt.restream.reindexer.util.Pair; + +/** + * An interface that can be implemented to convert field value between the Reindexer stored database type: + * {@link ru.rt.restream.reindexer.FieldType}, and the one used within the POJO representation. + * @param the field type used within the POJO + * @param the Reindexer stored database type + */ +public interface FieldConverter { + + /** + * Converts Reindexer stored database type value to the one used within the POJO representation. + * @param dbData the Reindexer stored database type value to convert + * @return the type value used within the POJO representation + */ + X convertToFieldType(Y dbData); + + /** + * Converts a POJO field type value to the one stored in Reindexer database. + * @param field the field type value used within the POJO + * @return the Reindexer stored database type value + */ + Y convertToDatabaseType(X field); + + /** + * Returns a {@link Pair} of source and target {@link ResolvableType}s. + * By default, the source and target types are determined based on {@link X} and {@link Y} parameters, + * this can be overridden to provide a custom implementation of how source and target types are determined. + * @return the {@link Pair} of source and target {@link ResolvableType}s to use + */ + default Pair getConvertiblePair() { + return ConversionUtils.resolveConvertiblePair(getClass(), FieldConverter.class); + } +} diff --git a/src/main/java/ru/rt/restream/reindexer/convert/FieldConverterRegistry.java b/src/main/java/ru/rt/restream/reindexer/convert/FieldConverterRegistry.java new file mode 100644 index 0000000..1d88a04 --- /dev/null +++ b/src/main/java/ru/rt/restream/reindexer/convert/FieldConverterRegistry.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.convert; + +/** + * A {@link FieldConverter} registry interface that allows to configure + * a custom {@link FieldConverter} implementations. + */ +public interface FieldConverterRegistry { + + /** + * Registers a {@link FieldConverter} for the given Item class and field name. + * This method allows overriding an existing mapped converter for the given field. + * + * @param itemClass the Item class to use + * @param fieldName the field name to use + * @param converter the {@link FieldConverter} to use + */ + void registerFieldConverter(Class itemClass, String fieldName, FieldConverter converter); + + /** + * Registers a global {@link FieldConverter}. + * This method allows overriding an existing global converter. + * + * @param converter the {@link FieldConverter} to use + */ + void registerGlobalConverter(FieldConverter converter); +} diff --git a/src/main/java/ru/rt/restream/reindexer/convert/FieldConverterRegistryFactory.java b/src/main/java/ru/rt/restream/reindexer/convert/FieldConverterRegistryFactory.java new file mode 100644 index 0000000..6829e90 --- /dev/null +++ b/src/main/java/ru/rt/restream/reindexer/convert/FieldConverterRegistryFactory.java @@ -0,0 +1,186 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.convert; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.rt.restream.reindexer.annotations.Convert; +import ru.rt.restream.reindexer.convert.util.ConversionUtils; +import ru.rt.restream.reindexer.convert.util.ResolvableType; +import ru.rt.restream.reindexer.util.Pair; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * For internal use only, as this contract is likely to change. + */ +public enum FieldConverterRegistryFactory implements FieldConverterRegistry { + INSTANCE; + + private static final Logger LOGGER = LoggerFactory.getLogger(FieldConverterRegistryFactory.class); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + private final Map, FieldConverter> converters = new HashMap<>(); + + private final Map> globalConverters = new HashMap<>(); + + private final Map, String>, FieldConverter> fieldConverters = new HashMap<>(); + + /** + * Returns a {@link FieldConverter} that is mapped for the given {@code field} or {@literal null}. + * If the {@code FieldConverter} is not mapped directly for the field, looks up for a global one registered via + * {@link FieldConverterRegistry#registerGlobalConverter(FieldConverter)}, if global {@code FieldConverter} is found checks + * the {@code converterClass} from {@link Convert} annotation if its type is not default and does not match + * the global converter's type, then specified in the annotation converter takes precedence and + * is created for the {@code field}. + * This method returns {@literal null} if {@code Convert} annotation's attribute {@code disableConversion} is + * set to {@literal true} or if none of the checks above resulted in finding an eligible field converter. + * @param field the {@link Field} to use + * @return the {@link FieldConverter} to use + */ + @SuppressWarnings("unchecked") + public FieldConverter getFieldConverter(Field field) { + Objects.requireNonNull(field, "field must not be null"); + Convert convert = field.getAnnotation(Convert.class); + if (convert != null && convert.disableConversion()) { + return null; + } + Pair, String> key = new Pair<>(field.getDeclaringClass(), field.getName()); + FieldConverter converter; + lock.readLock().lock(); + try { + converter = fieldConverters.get(key); + } finally { + lock.readLock().unlock(); + } + if (converter != null) { + /* + * Return immediately if field converter has been found. + * Programmatically configured converter takes precedence + * over global or the one specified in @Convert annotation. + */ + return (FieldConverter) converter; + } + ResolvableType fieldType = ConversionUtils.resolveFieldType(field); + lock.readLock().lock(); + try { + converter = globalConverters.get(fieldType); + } finally { + lock.readLock().unlock(); + } + /* + * Initialize a converter instance specified in @Convert annotation + * if annotation is present and converterClass is not default FieldConverter.class, + * and global converter has not been found or its class does not match the one specified + * in the annotation. The converter specified in the annotation always takes precedence + * over the global one. + */ + if (convert != null && convert.converterClass() != FieldConverter.class + && (converter == null || converter.getClass() != convert.converterClass())) { + lock.writeLock().lock(); + try { + converter = fieldConverters.get(key); + if (converter != null) { + return (FieldConverter) converter; + } + converter = globalConverters.get(fieldType); + if (converter == null || converter.getClass() != convert.converterClass()) { + converter = converters.computeIfAbsent(convert.converterClass(), this::instantiateFieldConverter); + fieldConverters.put(key, converter); + } + } finally { + lock.writeLock().unlock(); + } + } + return (FieldConverter) converter; + } + + /** + * {@inheritDoc} + */ + @Override + public void registerFieldConverter(Class itemClass, String fieldName, FieldConverter converter) { + Objects.requireNonNull(itemClass, "itemClass must not be null"); + Objects.requireNonNull(fieldName, "fieldName must not be null"); + Objects.requireNonNull(converter, "fieldConverter must not be null"); + lock.writeLock().lock(); + try { + FieldConverter prev = fieldConverters.put(new Pair<>(itemClass, fieldName), converter); + if (LOGGER.isTraceEnabled()) { + if (prev == null) { + LOGGER.trace("Registered field converter {} for {}.{}", converter, itemClass.getName(), fieldName); + } else { + LOGGER.trace("Converter: {} was overridden to {} for {}.{}", prev, converter, itemClass.getName(), fieldName); + } + } + } finally { + lock.writeLock().unlock(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void registerGlobalConverter(FieldConverter converter) { + Objects.requireNonNull(converter, "converter must not be null"); + Pair convertiblePair = converter.getConvertiblePair(); + lock.writeLock().lock(); + try { + FieldConverter prev = globalConverters.put(convertiblePair.getFirst(), converter); + if (LOGGER.isTraceEnabled()) { + if (prev == null) { + LOGGER.trace("Registered global converter {} for {}", converter, convertiblePair.getFirst()); + } else { + LOGGER.trace("Global converter: {} was overridden to {} for {}", prev, converter, convertiblePair.getFirst()); + } + } + } finally { + lock.writeLock().unlock(); + } + } + + /** + * For testing purposes only. + */ + void clearRegistry() { + lock.writeLock().lock(); + try { + converters.clear(); + fieldConverters.clear(); + globalConverters.clear(); + } finally { + lock.writeLock().unlock(); + } + } + + private FieldConverter instantiateFieldConverter(Class converterClass) { + try { + Constructor constructor = converterClass.getDeclaredConstructor(); + constructor.setAccessible(true); + return (FieldConverter) constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/ru/rt/restream/reindexer/convert/package-info.java b/src/main/java/ru/rt/restream/reindexer/convert/package-info.java new file mode 100644 index 0000000..5d92d69 --- /dev/null +++ b/src/main/java/ru/rt/restream/reindexer/convert/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Restream + * + * 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. + */ + +/** + * Contains classes that are used for type conversions. + */ +package ru.rt.restream.reindexer.convert; diff --git a/src/main/java/ru/rt/restream/reindexer/convert/util/ConversionUtils.java b/src/main/java/ru/rt/restream/reindexer/convert/util/ConversionUtils.java new file mode 100644 index 0000000..827b0c8 --- /dev/null +++ b/src/main/java/ru/rt/restream/reindexer/convert/util/ConversionUtils.java @@ -0,0 +1,127 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.convert.util; + +import org.apache.commons.lang3.reflect.TypeUtils; +import ru.rt.restream.reindexer.util.Pair; + +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * For internal use only, as this contract is likely to change. + */ +public final class ConversionUtils { + + private static final Map, Class>, Pair> CONVERTIBLE_PAIR_CACHE = new ConcurrentHashMap<>(); + + private static final Map, String>, ResolvableType> FIELD_TYPE_CACHE = new ConcurrentHashMap<>(); + + /** + * Resolves source and target types based on {@code [0]} and {@code [1]} parameters from {@code converterType} interface implementation. + * If {@code converterType} source or target type is an array, the type is determined by {@code Class#getComponentType}, + * in case of a generic type, only {@code java.util.Collection} assignable types are considered, nested generic types + * (i.e. {@code List>}) are not supported, such types will not be recognized and IllegalArgumentException will be thrown. + * @param converterClass the {@code converterType} interface implementation class to use + * @param converterType the converter interface to use + * @return the {@link Pair} of source and target {@link ResolvableType}s to use + */ + public static Pair resolveConvertiblePair(Class converterClass, Class converterType) { + Objects.requireNonNull(converterClass, "converterClass must not be null"); + Objects.requireNonNull(converterType, "converterType must not be null"); + Pair, Class> key = new Pair<>(converterClass, converterType); + // https://bugs.openjdk.java.net/browse/JDK-8161372 + Pair typePair = CONVERTIBLE_PAIR_CACHE.get(key); + if (typePair != null) { + return typePair; + } + TypeVariable>[] typeParameters = converterType.getTypeParameters(); + if (typeParameters.length < 2) { + throw new IllegalArgumentException( + String.format("Converter type: %s must have 2 type arguments", converterType.getName())); + } + return CONVERTIBLE_PAIR_CACHE.computeIfAbsent(key, k -> { + Map, Type> typeArguments = TypeUtils.getTypeArguments(converterClass, converterType); + Type sourceType = typeArguments.get(typeParameters[0]); + ResolvableType resolvableSourceType = resolveType(sourceType); + if (resolvableSourceType == null) { + throw new IllegalArgumentException( + String.format("Cannot resolve source type: %s of converter type: %s for converter class: %s", + sourceType, converterType.getName(), converterClass.getName())); + } + Type targetType = typeArguments.get(typeParameters[1]); + ResolvableType resolvableTargetType = resolveType(targetType); + if (resolvableTargetType == null) { + throw new IllegalArgumentException( + String.format("Cannot resolve target type: %s of converter type: %s for converter class: %s", + targetType, converterType.getName(), converterClass.getName())); + } + return new Pair<>(resolvableSourceType, resolvableTargetType); + }); + } + + /** + * Resolves a field type. If the field type is an array, the target type is determined + * by {@code Class#getComponentType}, in case of a generic return type, only {@code java.util.Collection} + * assignable types are considered, nested generic types (i.e. {@code List>}) + * are not supported, such types will not be recognized and IllegalArgumentException will be thrown. + * @param field the field to use + * @return the {@link ResolvableType} to use + */ + public static ResolvableType resolveFieldType(Field field) { + Objects.requireNonNull(field, "field must not be null"); + // https://bugs.openjdk.java.net/browse/JDK-8161372 + Pair, String> key = new Pair<>(field.getDeclaringClass(), field.getName()); + ResolvableType resolvableType = FIELD_TYPE_CACHE.get(key); + if (resolvableType != null) { + return resolvableType; + } + return FIELD_TYPE_CACHE.computeIfAbsent(key, k -> { + ResolvableType result = resolveType(field.getGenericType()); + if (result == null) { + throw new IllegalArgumentException(String.format("Cannot resolve Field: %s.%s target type: %s", + field.getDeclaringClass().getName(), field.getName(), field.getGenericType())); + } + return result; + }); + } + + private static ResolvableType resolveType(Type type) { + if (TypeUtils.isAssignable(type, Collection.class)) { + Type typeArgument = TypeUtils.getTypeArguments(type, Collection.class) + .get(Collection.class.getTypeParameters()[0]); + if (typeArgument instanceof Class) { + Class containerType = TypeUtils.getRawType(type, Collection.class); + return new ResolvableType(containerType, (Class) typeArgument, true); + } + return null; + } + if (type instanceof Class) { + Class targetType = (Class) type; + return new ResolvableType(targetType, targetType.getComponentType(), targetType.isArray()); + } + return null; + } + + private ConversionUtils() { + // utils + } +} diff --git a/src/main/java/ru/rt/restream/reindexer/convert/util/ResolvableType.java b/src/main/java/ru/rt/restream/reindexer/convert/util/ResolvableType.java new file mode 100644 index 0000000..ab1bf25 --- /dev/null +++ b/src/main/java/ru/rt/restream/reindexer/convert/util/ResolvableType.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.convert.util; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * Represents a resolved type, this includes {@code type} information, + * {@code componentType} and {@code isCollectionLike} if the {@code type} is either + * assignable of {@code java.util.Collection} or an array. + */ +@Getter +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor +public final class ResolvableType { + private final Class type; + private final Class componentType; + private final boolean isCollectionLike; +} diff --git a/src/main/java/ru/rt/restream/reindexer/util/CollectionUtils.java b/src/main/java/ru/rt/restream/reindexer/util/CollectionUtils.java new file mode 100644 index 0000000..6dd01a8 --- /dev/null +++ b/src/main/java/ru/rt/restream/reindexer/util/CollectionUtils.java @@ -0,0 +1,98 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.NavigableSet; +import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * This class contains utility methods to work with Java collections. + */ +public final class CollectionUtils { + + /** + * Creates the most appropriate collection for the given {@code collectionType}. + * For {@code EnumSet} collection type the {@code elementType} must not be {@literal null} + * and must be an {@code Enum} type matching to {@code E} type. + * @param collectionType the target collection type to create + * @param elementType the element collection type + * @param capacity the collection initial capacity + * @param the collection type argument + * @return the {@link Collection} to use + */ + @SuppressWarnings("unchecked") + public static Collection createCollection(Class collectionType, Class elementType, int capacity) { + Objects.requireNonNull(collectionType, "Collection type must not be null"); + if (collectionType == Collection.class || + collectionType == List.class || + collectionType == ArrayList.class) { + return new ArrayList<>(capacity); + } + if (collectionType == Set.class || + collectionType == LinkedHashSet.class || + // Java 21 collection types. + "java.util.SequencedSet".equals(collectionType.getName()) || + "java.util.SequencedCollection".equals(collectionType.getName())) { + return new LinkedHashSet<>(capacity); + } + if (collectionType == HashSet.class) { + return new HashSet<>(capacity); + } + if (collectionType == LinkedList.class) { + return new LinkedList<>(); + } + if (collectionType == TreeSet.class || + collectionType == NavigableSet.class || + collectionType == SortedSet.class) { + return new TreeSet<>(); + } + if (EnumSet.class.isAssignableFrom(collectionType)) { + return EnumSet.noneOf(asEnumType(elementType)); + } + if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) { + throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName()); + } + try { + return (Collection) collectionType.getDeclaredConstructor().newInstance(); + } catch (Throwable e) { + throw new IllegalArgumentException( + "Could not instantiate Collection type: " + collectionType.getName(), e); + } + } + + @SuppressWarnings("rawtypes") + private static Class asEnumType(Class enumType) { + Objects.requireNonNull(enumType, "Enum type must not be null"); + if (!Enum.class.isAssignableFrom(enumType)) { + throw new IllegalArgumentException("Supplied type is not an enum: " + enumType.getName()); + } + return enumType.asSubclass(Enum.class); + } + + private CollectionUtils() { + // utils + } +} diff --git a/src/test/java/ru/rt/restream/reindexer/connector/ReindexerTest.java b/src/test/java/ru/rt/restream/reindexer/connector/ReindexerTest.java index 99f83fe..6654144 100644 --- a/src/test/java/ru/rt/restream/reindexer/connector/ReindexerTest.java +++ b/src/test/java/ru/rt/restream/reindexer/connector/ReindexerTest.java @@ -28,16 +28,22 @@ import ru.rt.restream.reindexer.QueryResultJsonIterator; import ru.rt.restream.reindexer.ResultIterator; import ru.rt.restream.reindexer.Transaction; +import ru.rt.restream.reindexer.annotations.Convert; import ru.rt.restream.reindexer.annotations.Enumerated; import ru.rt.restream.reindexer.annotations.Reindex; import ru.rt.restream.reindexer.annotations.Serial; +import ru.rt.restream.reindexer.convert.FieldConverter; +import ru.rt.restream.reindexer.convert.FieldConverterRegistryFactory; import ru.rt.restream.reindexer.db.DbBaseTest; import ru.rt.restream.reindexer.util.JsonSerializer; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -67,6 +73,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static ru.rt.restream.reindexer.IndexType.TEXT; +import static ru.rt.restream.reindexer.Query.Condition.ALLSET; import static ru.rt.restream.reindexer.Query.Condition.EQ; import static ru.rt.restream.reindexer.Query.Condition.LE; import static ru.rt.restream.reindexer.Query.Condition.RANGE; @@ -2776,45 +2783,146 @@ public void testIsAppendableIndexItem() { public void testTestItemEnumString() { String namespaceName = "items"; db.openNamespace(namespaceName, NamespaceOptions.defaultOptions(), TestItemEnumString.class); - db.upsert(namespaceName, new TestItemEnumString(1, "TestName", TestEnum.TEST_CONSTANT)); + db.upsert(namespaceName, new TestItemEnumString(1, "TestName", TestEnum.TEST_CONSTANT_1, + Arrays.asList(TestEnum.TEST_CONSTANT_1, TestEnum.TEST_CONSTANT_2, TestEnum.TEST_CONSTANT_3))); Iterator iterator = db.query(namespaceName, TestItemEnumString.class) - .where("testEnumString", EQ, TestEnum.TEST_CONSTANT.name()) + .where("testEnumString", EQ, TestEnum.TEST_CONSTANT_1.name()) + .where("testEnumStrings", ALLSET, TestEnum.TEST_CONSTANT_1.name(), + TestEnum.TEST_CONSTANT_2.name(), TestEnum.TEST_CONSTANT_3.name()) .execute(); assertThat(iterator.hasNext(), is(true)); TestItemEnumString foundByEnumString = iterator.next(); assertThat(foundByEnumString.id, is(1)); assertThat(foundByEnumString.name, is("TestName")); - assertThat(foundByEnumString.testEnum, is(TestEnum.TEST_CONSTANT)); + assertThat(foundByEnumString.testEnum, is(TestEnum.TEST_CONSTANT_1)); + assertThat(foundByEnumString.testEnums, contains(TestEnum.TEST_CONSTANT_1, TestEnum.TEST_CONSTANT_2, + TestEnum.TEST_CONSTANT_3)); } @Test public void testTestItemEnumOrdinal() { String namespaceName = "items"; db.openNamespace(namespaceName, NamespaceOptions.defaultOptions(), TestItemEnumOrdinal.class); - db.upsert(namespaceName, new TestItemEnumOrdinal(1, "TestName", TestEnum.TEST_CONSTANT)); + db.upsert(namespaceName, new TestItemEnumOrdinal(1, "TestName", TestEnum.TEST_CONSTANT_1, + Arrays.asList(TestEnum.TEST_CONSTANT_1, TestEnum.TEST_CONSTANT_2, TestEnum.TEST_CONSTANT_3))); Iterator iterator = db.query(namespaceName, TestItemEnumOrdinal.class) - .where("testEnum", EQ, TestEnum.TEST_CONSTANT.ordinal()) + .where("testEnumOrdinal", EQ, TestEnum.TEST_CONSTANT_1.ordinal()) + .where("testEnumOrdinals", ALLSET, TestEnum.TEST_CONSTANT_1.ordinal(), + TestEnum.TEST_CONSTANT_2.ordinal(), TestEnum.TEST_CONSTANT_3.ordinal()) .execute(); assertThat(iterator.hasNext(), is(true)); TestItemEnumOrdinal foundByEnumOrdinal = iterator.next(); assertThat(foundByEnumOrdinal.id, is(1)); assertThat(foundByEnumOrdinal.name, is("TestName")); - assertThat(foundByEnumOrdinal.testEnum, is(TestEnum.TEST_CONSTANT)); + assertThat(foundByEnumOrdinal.testEnum, is(TestEnum.TEST_CONSTANT_1)); + assertThat(foundByEnumOrdinal.testEnums, contains(TestEnum.TEST_CONSTANT_1, TestEnum.TEST_CONSTANT_2, + TestEnum.TEST_CONSTANT_3)); } @Test public void testTestItemEnumDefault() { String namespaceName = "items"; db.openNamespace(namespaceName, NamespaceOptions.defaultOptions(), TestItemEnumDefault.class); - db.upsert(namespaceName, new TestItemEnumDefault(1, "TestName", TestEnum.TEST_CONSTANT)); + db.upsert(namespaceName, new TestItemEnumDefault(1, "TestName", TestEnum.TEST_CONSTANT_1, + Arrays.asList(TestEnum.TEST_CONSTANT_1, TestEnum.TEST_CONSTANT_2, TestEnum.TEST_CONSTANT_3))); Iterator iterator = db.query(namespaceName, TestItemEnumDefault.class) - .where("testEnum", EQ, TestEnum.TEST_CONSTANT.ordinal()) + .where("testEnum", EQ, TestEnum.TEST_CONSTANT_1.ordinal()) + .where("testEnums", ALLSET, TestEnum.TEST_CONSTANT_1.ordinal(), + TestEnum.TEST_CONSTANT_2.ordinal(), TestEnum.TEST_CONSTANT_3.ordinal()) .execute(); assertThat(iterator.hasNext(), is(true)); TestItemEnumDefault foundByEnumOrdinal = iterator.next(); assertThat(foundByEnumOrdinal.id, is(1)); assertThat(foundByEnumOrdinal.name, is("TestName")); - assertThat(foundByEnumOrdinal.testEnum, is(TestEnum.TEST_CONSTANT)); + assertThat(foundByEnumOrdinal.testEnum, is(TestEnum.TEST_CONSTANT_1)); + assertThat(foundByEnumOrdinal.testEnums, contains(TestEnum.TEST_CONSTANT_1, TestEnum.TEST_CONSTANT_2, + TestEnum.TEST_CONSTANT_3)); + } + + @Test + public void testTestItemEnumSet() { + String namespaceName = "items"; + db.openNamespace(namespaceName, NamespaceOptions.defaultOptions(), TestItemEnumSet.class); + db.upsert(namespaceName, new TestItemEnumSet(1, EnumSet.of(TestEnum.TEST_CONSTANT_1, + TestEnum.TEST_CONSTANT_2, TestEnum.TEST_CONSTANT_3))); + Iterator iterator = db.query(namespaceName, TestItemEnumSet.class) + .where("testEnums", ALLSET, TestEnum.TEST_CONSTANT_1.ordinal(), + TestEnum.TEST_CONSTANT_2.ordinal(), TestEnum.TEST_CONSTANT_3.ordinal()) + .execute(); + assertThat(iterator.hasNext(), is(true)); + TestItemEnumSet foundByEnumOrdinal = iterator.next(); + assertThat(foundByEnumOrdinal.id, is(1)); + assertThat(foundByEnumOrdinal.testEnums, contains(TestEnum.TEST_CONSTANT_1, TestEnum.TEST_CONSTANT_2, + TestEnum.TEST_CONSTANT_3)); + } + + @Test + public void testTestItemCustomDateConverters() { + String namespaceName = "items"; + db.openNamespace(namespaceName, NamespaceOptions.defaultOptions(), TestItemCustomDateConverters.class); + db.upsert(namespaceName, new TestItemCustomDateConverters(1, LocalDate.of(2020, 1, 1), LocalDateTime.of(2020, 1, 1, 12, 0, 0))); + Iterator iterator = db.query(namespaceName, TestItemCustomDateConverters.class) + .where("localDate", EQ, "2020-01-01") + .where("localDateTime", EQ, "2020-01-01T12:00") + .execute(); + assertThat(iterator.hasNext(), is(true)); + TestItemCustomDateConverters foundByLocalDates = iterator.next(); + assertThat(foundByLocalDates.id, is(1)); + assertThat(foundByLocalDates.localDate, is(LocalDate.of(2020, 1, 1))); + assertThat(foundByLocalDates.localDateTime, is(LocalDateTime.of(2020, 1, 1, 12, 0, 0))); + } + + @Test + public void testTestItemGlobalDateConverters() { + FieldConverterRegistryFactory.INSTANCE.registerGlobalConverter(new LocalDateStringFieldConverter()); + FieldConverterRegistryFactory.INSTANCE.registerGlobalConverter(new LocalDateTimeStringFieldConverter()); + String namespaceName = "items"; + db.openNamespace(namespaceName, NamespaceOptions.defaultOptions(), TestItemGlobalDateConverters.class); + db.upsert(namespaceName, new TestItemGlobalDateConverters(1, LocalDate.of(2020, 1, 1), LocalDateTime.of(2020, 1, 1, 12, 0, 0))); + Iterator iterator = db.query(namespaceName, TestItemGlobalDateConverters.class) + .where("localDate", EQ, "2020-01-01") + .where("localDateTime", EQ, "2020-01-01T12:00") + .execute(); + assertThat(iterator.hasNext(), is(true)); + TestItemGlobalDateConverters foundByLocalDates = iterator.next(); + assertThat(foundByLocalDates.id, is(1)); + assertThat(foundByLocalDates.localDate, is(LocalDate.of(2020, 1, 1))); + assertThat(foundByLocalDates.localDateTime, is(LocalDateTime.of(2020, 1, 1, 12, 0, 0))); + } + + @Test + public void testTestItemCustomObjectConverters() { + String namespaceName = "items"; + db.openNamespace(namespaceName, NamespaceOptions.defaultOptions(), TestItemCustomObjectConverters.class); + db.upsert(namespaceName, new TestItemCustomObjectConverters(1, new Price(100.5), "market", Arrays.asList("apple", "banana", "orange"), + Arrays.asList(new Price(101.5), new Price(102.6), new Price(103.7)))); + Iterator iterator = db.query(namespaceName, TestItemCustomObjectConverters.class) + .where("price", EQ, 100.5) + .where("name", EQ, "tekram") + .where("products", EQ, "apple,banana,orange") + .where("prices", ALLSET, Arrays.asList(101.5, 102.6, 103.7)) + .execute(); + assertThat(iterator.hasNext(), is(true)); + TestItemCustomObjectConverters foundByPriceAndProducts = iterator.next(); + assertThat(foundByPriceAndProducts.id, is(1)); + assertThat(foundByPriceAndProducts.price, notNullValue()); + assertThat(foundByPriceAndProducts.name, is("market")); + assertThat(foundByPriceAndProducts.products, contains("apple", "banana", "orange")); + } + + @Test + public void testTestItemDefaultStringConverters() { + String namespaceName = "items"; + db.openNamespace(namespaceName, NamespaceOptions.defaultOptions(), TestItemDefaultStringConverters.class); + db.upsert(namespaceName, new TestItemDefaultStringConverters(1, null, null)); + Iterator iterator = db.query(namespaceName, TestItemDefaultStringConverters.class) + .where("defaultWriting", EQ, "default") + .execute(); + assertThat(iterator.hasNext(), is(true)); + TestItemDefaultStringConverters foundByDefaultWriting = iterator.next(); + assertThat(foundByDefaultWriting.id, is(1)); + assertThat(foundByDefaultWriting.defaultReading, is("default")); + assertThat(foundByDefaultWriting.defaultWriting, is("default")); } @Getter @@ -2935,6 +3043,10 @@ public static class TestItemEnumString { @Enumerated(EnumType.STRING) @Reindex(name = "testEnumString") private TestEnum testEnum; + + @Enumerated(EnumType.STRING) + @Reindex(name = "testEnumStrings") + private List testEnums; } @Getter @@ -2951,6 +3063,10 @@ public static class TestItemEnumOrdinal { @Enumerated(EnumType.ORDINAL) @Reindex(name = "testEnumOrdinal") private TestEnum testEnum; + + @Enumerated(EnumType.ORDINAL) + @Reindex(name = "testEnumOrdinals") + private List testEnums; } @Getter @@ -2966,9 +3082,215 @@ public static class TestItemEnumDefault { @Reindex(name = "testEnum") private TestEnum testEnum; + + @Reindex(name = "testEnums") + private List testEnums; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class TestItemEnumSet { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "testEnums") + private EnumSet testEnums; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class TestItemCustomDateConverters { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "localDate") + @Convert(converterClass = LocalDateStringFieldConverter.class) + private LocalDate localDate; + + @Reindex(name = "localDateTime") + @Convert(converterClass = LocalDateTimeStringFieldConverter.class) + private LocalDateTime localDateTime; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class TestItemGlobalDateConverters { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "localDate") + private LocalDate localDate; + + @Reindex(name = "localDateTime") + private LocalDateTime localDateTime; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class TestItemCustomObjectConverters { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "price") + @Convert(converterClass = PriceDoubleFieldConverter.class) + private Price price; + + @Reindex(name = "name") + @Convert(converterClass = ReverseStringFieldConverter.class) + private String name; + + @Reindex(name = "products") + @Convert(converterClass = ListStringFieldConverter.class) + private List products; + + @Reindex(name = "prices") + @Convert(converterClass = ListPriceListDoubleFieldConverter.class) + private List prices; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class TestItemDefaultStringConverters { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "defaultReading") + @Convert(converterClass = DefaultReadingConverter.class) + private String defaultReading; + + @Reindex(name = "defaultWriting") + @Convert(converterClass = DefaultWritingConverter.class) + private String defaultWriting; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + public static class Price { + private Double value; + } + + public static class LocalDateStringFieldConverter implements FieldConverter { + + @Override + public LocalDate convertToFieldType(String localDate) { + return localDate != null ? LocalDate.parse(localDate) : null; + } + + @Override + public String convertToDatabaseType(LocalDate localDate) { + return localDate != null ? localDate.toString() : null; + } + } + + public static class LocalDateTimeStringFieldConverter implements FieldConverter { + + @Override + public LocalDateTime convertToFieldType(String localDateTime) { + return localDateTime != null ? LocalDateTime.parse(localDateTime) : null; + } + + @Override + public String convertToDatabaseType(LocalDateTime localDateTime) { + return localDateTime != null ? localDateTime.toString() : null; + } + } + + public static class PriceDoubleFieldConverter implements FieldConverter { + + @Override + public Price convertToFieldType(Double price) { + return new Price(price); + } + + @Override + public Double convertToDatabaseType(Price price) { + return price != null ? price.getValue() : null; + } + } + + public static class ReverseStringFieldConverter implements FieldConverter { + + @Override + public String convertToFieldType(String value) { + return reverse(value); + } + + @Override + public String convertToDatabaseType(String value) { + return reverse(value); + } + + private String reverse(String value) { + return value != null ? new StringBuilder(value).reverse().toString() : null; + } + } + + public static class ListStringFieldConverter implements FieldConverter, String> { + + @Override + public List convertToFieldType(String value) { + return Stream.of(value.split(",")).collect(Collectors.toList()); + } + + @Override + public String convertToDatabaseType(List values) { + return String.join(",", values); + } + } + + public static class DefaultReadingConverter implements FieldConverter { + + @Override + public String convertToFieldType(String dbData) { + return dbData != null ? dbData : "default"; + } + + @Override + public String convertToDatabaseType(String field) { + return field; + } + } + + public static class DefaultWritingConverter implements FieldConverter { + + @Override + public String convertToFieldType(String dbData) { + return dbData; + } + + @Override + public String convertToDatabaseType(String field) { + return field != null ? field : "default"; + } + } + + public static class ListPriceListDoubleFieldConverter implements FieldConverter, List> { + @Override + public List convertToFieldType(List prices) { + return prices.stream().map(Price::new).collect(Collectors.toList()); + } + + @Override + public List convertToDatabaseType(List prices) { + return prices.stream().map(Price::getValue).collect(Collectors.toList()); + } } public enum TestEnum { - TEST_CONSTANT + TEST_CONSTANT_1, + TEST_CONSTANT_2, + TEST_CONSTANT_3, } } diff --git a/src/test/java/ru/rt/restream/reindexer/convert/FieldConverterRegistryFactoryTest.java b/src/test/java/ru/rt/restream/reindexer/convert/FieldConverterRegistryFactoryTest.java new file mode 100644 index 0000000..bdbcdbb --- /dev/null +++ b/src/test/java/ru/rt/restream/reindexer/convert/FieldConverterRegistryFactoryTest.java @@ -0,0 +1,255 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.convert; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import ru.rt.restream.reindexer.annotations.Convert; + +import java.lang.reflect.Field; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for {@link FieldConverterRegistryFactory}. + */ +public class FieldConverterRegistryFactoryTest { + + private final FieldConverterRegistryFactory registry = FieldConverterRegistryFactory.INSTANCE; + + @AfterEach + void tearDown() { + registry.clearRegistry(); + } + + @Test + void getFieldConverterWhenClassSpecifiedThenCreated() { + FieldConverter converter = registry + .getFieldConverter(getField("stringLongConvertField")); + assertThat(converter, notNullValue()); + assertThat(converter, instanceOf(StringLongFieldConverter.class)); + } + + @Test + void getFieldConverterWhenClassSpecifiedConverterConfiguredThenConfiguredConverterTakesPrecedence() { + StringIntegerFieldConverter converter = new StringIntegerFieldConverter(); + registry.registerFieldConverter(TestPojo.class, "stringLongConvertField", converter); + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringLongConvertField")); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, sameInstance(converter)); + } + + @Test + void getFieldConverterWhenClassSpecifiedSameGlobalConverterConfiguredThenConverterReturned() { + StringLongFieldConverter converter = new StringLongFieldConverter(); + registry.registerGlobalConverter(converter); + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringLongConvertField")); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, sameInstance(converter)); + } + + @Test + void getFieldConverterWhenNoConvertAnnotationFieldConverterConfiguredThenConverterReturned() { + StringLongFieldConverter converter = new StringLongFieldConverter(); + registry.registerFieldConverter(TestPojo.class, "stringField", converter); + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringField")); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, sameInstance(converter)); + } + + @Test + void getFieldConverterWhenNoConvertAnnotationGlobalConverterConfiguredThenConverterReturned() { + StringLongFieldConverter converter = new StringLongFieldConverter(); + registry.registerGlobalConverter(converter); + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringField")); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, sameInstance(converter)); + } + + @Test + void getFieldConverterWhenClassSpecifiedGlobalConverterConfiguredThenSpecifiedTakesPrecedence() { + StringLongFieldConverter converter = new StringLongFieldConverter(); + registry.registerGlobalConverter(converter); + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringIntegerConvertField")); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, instanceOf(StringIntegerFieldConverter.class)); + } + + @Test + void getFieldConverterWhenNoClassSpecifiedGlobalConverterConfiguredThenConverterReturned() { + StringLongFieldConverter converter = new StringLongFieldConverter(); + registry.registerGlobalConverter(converter); + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringConvertNoClassField")); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, sameInstance(converter)); + } + + @Test + void getFieldConverterWhenNoConvertAnnotationThenNull() { + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringField")); + assertThat(fieldConverter, nullValue()); + } + + @Test + void getFieldConverterWhenDisableConversionAndGlobalConverterConfiguredThenNull() { + StringLongFieldConverter converter = new StringLongFieldConverter(); + registry.registerGlobalConverter(converter); + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringConvertDisableField")); + assertThat(fieldConverter, nullValue()); + } + + @Test + void getFieldConverterWhenDisableConversionAndFieldConverterConfiguredThenNull() { + StringLongFieldConverter converter = new StringLongFieldConverter(); + registry.registerFieldConverter(TestPojo.class, "stringConvertDisableConversionField", converter); + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringConvertDisableField")); + assertThat(fieldConverter, nullValue()); + } + + @Test + void getFieldConverterWhenDisableConversionAndClassSpecifiedThenNull() { + StringLongFieldConverter converter = new StringLongFieldConverter(); + registry.registerFieldConverter(TestPojo.class, "stringConvertDisableConversionClassSpecifiedField", converter); + FieldConverter fieldConverter = registry + .getFieldConverter(getField("stringConvertDisableClassSpecifiedField")); + assertThat(fieldConverter, nullValue()); + } + + @Test + void registerFieldConverterWhenRegisteredThenOverrides() { + FieldConverter stringLongFieldConverter = new StringLongFieldConverter(); + registry.registerFieldConverter(TestPojo.class, "stringField", stringLongFieldConverter); + Field field = getField("stringField"); + FieldConverter fieldConverter = registry.getFieldConverter(field); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, sameInstance(stringLongFieldConverter)); + StringIntegerFieldConverter stringIntegerFieldConverter = new StringIntegerFieldConverter(); + registry.registerFieldConverter(TestPojo.class, "stringField", stringIntegerFieldConverter); + fieldConverter = registry.getFieldConverter(field); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, sameInstance(stringIntegerFieldConverter)); + } + + @Test + void registerGlobalConverterWhenRegisteredThenOverrides() { + FieldConverter stringLongFieldConverter = new StringLongFieldConverter(); + registry.registerGlobalConverter(stringLongFieldConverter); + Field field = getField("stringField"); + FieldConverter fieldConverter = registry + .getFieldConverter(field); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, sameInstance(stringLongFieldConverter)); + StringIntegerFieldConverter stringIntegerFieldConverter = new StringIntegerFieldConverter(); + registry.registerGlobalConverter(stringIntegerFieldConverter); + fieldConverter = registry.getFieldConverter(field); + assertThat(fieldConverter, notNullValue()); + assertThat(fieldConverter, sameInstance(stringIntegerFieldConverter)); + } + + @Test + void getFieldConverterWhenFieldNullThenException() { + NullPointerException exception = assertThrows(NullPointerException.class, () -> registry.getFieldConverter(null)); + assertThat(exception.getMessage(), is("field must not be null")); + } + + @Test + void registerFieldConverterWhenItemClassNullThenException() { + NullPointerException exception = assertThrows(NullPointerException.class, () -> registry + .registerFieldConverter(null, "stringField", new StringLongFieldConverter())); + assertThat(exception.getMessage(), is("itemClass must not be null")); + } + + @Test + void registerFieldConverterWhenFieldNameNullThenException() { + NullPointerException exception = assertThrows(NullPointerException.class, () -> registry + .registerFieldConverter(TestPojo.class, null, new StringLongFieldConverter())); + assertThat(exception.getMessage(), is("fieldName must not be null")); + } + + @Test + void registerFieldConverterWhenConverterNullThenException() { + NullPointerException exception = assertThrows(NullPointerException.class, () -> registry + .registerFieldConverter(TestPojo.class, "stringField", null)); + assertThat(exception.getMessage(), is("fieldConverter must not be null")); + } + + @Test + void registerGlobalConverterWhenConverterNullThenException() { + NullPointerException exception = assertThrows(NullPointerException.class, () -> registry + .registerGlobalConverter(null)); + assertThat(exception.getMessage(), is("converter must not be null")); + } + + private static Field getField(String name) { + return FieldUtils.getDeclaredField(TestPojo.class, name, true); + } + + static class TestPojo { + @Convert(converterClass = StringLongFieldConverter.class) + String stringLongConvertField; + @Convert(converterClass = StringIntegerFieldConverter.class) + String stringIntegerConvertField; + @Convert + String stringConvertNoClassField; + @Convert(disableConversion = true) + String stringConvertDisableField; + @Convert(disableConversion = true, converterClass = StringLongFieldConverter.class) + String stringConvertDisableClassSpecifiedField; + String stringField; + } + + static class StringLongFieldConverter implements FieldConverter { + + @Override + public String convertToFieldType(Long dbData) { + return ""; + } + + @Override + public Long convertToDatabaseType(String field) { + return 0L; + } + } + + static class StringIntegerFieldConverter implements FieldConverter { + + @Override + public String convertToFieldType(Integer dbData) { + return ""; + } + + @Override + public Integer convertToDatabaseType(String field) { + return 0; + } + } +} diff --git a/src/test/java/ru/rt/restream/reindexer/convert/util/ConversionUtilsTest.java b/src/test/java/ru/rt/restream/reindexer/convert/util/ConversionUtilsTest.java new file mode 100644 index 0000000..5508169 --- /dev/null +++ b/src/test/java/ru/rt/restream/reindexer/convert/util/ConversionUtilsTest.java @@ -0,0 +1,570 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.convert.util; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.junit.jupiter.api.Test; +import ru.rt.restream.reindexer.convert.FieldConverter; +import ru.rt.restream.reindexer.util.Pair; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.function.Supplier; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for {@link ConversionUtils}. + */ +public class ConversionUtilsTest { + + @Test + void resolveConvertiblePairWhenClassImplementsFieldConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ClassImplementsFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(Integer.class)); + assertThat(sourceType.getComponentType(), nullValue()); + assertThat(sourceType.isCollectionLike(), is(false)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(String.class)); + assertThat(targetType.getComponentType(), nullValue()); + assertThat(targetType.isCollectionLike(), is(false)); + } + + @Test + void resolveConvertiblePairWhenClassImplementsMultipleInterfacesFieldConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ClassImplementsMultipleInterfacesFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(Integer.class)); + assertThat(sourceType.getComponentType(), nullValue()); + assertThat(sourceType.isCollectionLike(), is(false)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(String.class)); + assertThat(targetType.getComponentType(), nullValue()); + assertThat(targetType.isCollectionLike(), is(false)); + } + + @Test + void resolveConvertiblePairWhenClassExtendsClassImplementingFieldConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ClassExtendsClassImplementingFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(Integer.class)); + assertThat(sourceType.getComponentType(), nullValue()); + assertThat(sourceType.isCollectionLike(), is(false)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(String.class)); + assertThat(targetType.getComponentType(), nullValue()); + assertThat(targetType.isCollectionLike(), is(false)); + } + + @Test + void resolveConvertiblePairWhenClassExtendsClassImplementingMultipleInterfacesFieldConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ClassExtendsClassImplementingMultipleInterfacesFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(Integer.class)); + assertThat(sourceType.getComponentType(), nullValue()); + assertThat(sourceType.isCollectionLike(), is(false)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(String.class)); + assertThat(targetType.getComponentType(), nullValue()); + assertThat(targetType.isCollectionLike(), is(false)); + } + + @Test + void resolveConvertiblePairWhenClassExtendsGenericConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ClassExtendsGenericFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(Integer.class)); + assertThat(sourceType.getComponentType(), nullValue()); + assertThat(sourceType.isCollectionLike(), is(false)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(String.class)); + assertThat(targetType.getComponentType(), nullValue()); + assertThat(targetType.isCollectionLike(), is(false)); + } + + @Test + void resolveConvertiblePairWhenClassExtendsClassExtendingGenericFieldConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ClassExtendsClassExtendingGenericFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(Integer.class)); + assertThat(sourceType.getComponentType(), nullValue()); + assertThat(sourceType.isCollectionLike(), is(false)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(String.class)); + assertThat(targetType.getComponentType(), nullValue()); + assertThat(targetType.isCollectionLike(), is(false)); + } + + @Test + void resolveConvertiblePairWhenContainerTypeListFieldConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ContainerTypeListFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(List.class)); + assertThat(sourceType.getComponentType(), is(Integer.class)); + assertThat(sourceType.isCollectionLike(), is(true)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(List.class)); + assertThat(targetType.getComponentType(), is(String.class)); + assertThat(targetType.isCollectionLike(), is(true)); + } + + @Test + void resolveConvertiblePairWhenContainerTypeSetFieldConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ContainerTypeSetFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(List.class)); + assertThat(sourceType.getComponentType(), is(Integer.class)); + assertThat(sourceType.isCollectionLike(), is(true)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(Set.class)); + assertThat(targetType.getComponentType(), is(String.class)); + assertThat(targetType.isCollectionLike(), is(true)); + } + + @Test + void resolveConvertiblePairWhenContainerTypeCollectionFieldConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ContainerTypeCollectionFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(Collection.class)); + assertThat(sourceType.getComponentType(), is(Integer.class)); + assertThat(sourceType.isCollectionLike(), is(true)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(Collection.class)); + assertThat(targetType.getComponentType(), is(String.class)); + assertThat(targetType.isCollectionLike(), is(true)); + } + + @Test + void resolveConvertiblePairWhenArrayTypeFieldConverterThenResolves() { + Pair typePair = ConversionUtils + .resolveConvertiblePair(ArrayTypeFieldConverter.class, FieldConverter.class); + ResolvableType sourceType = typePair.getFirst(); + assertThat(sourceType, notNullValue()); + assertThat(sourceType.getType(), is(Integer[].class)); + assertThat(sourceType.getComponentType(), is(Integer.class)); + assertThat(sourceType.isCollectionLike(), is(true)); + ResolvableType targetType = typePair.getSecond(); + assertThat(targetType, notNullValue()); + assertThat(targetType.getType(), is(String[].class)); + assertThat(targetType.getComponentType(), is(String.class)); + assertThat(targetType.isCollectionLike(), is(true)); + } + + @Test + void resolveConvertiblePairWhenContainerSourceTypeOptionalFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerSourceTypeOptionalFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve source type: java.util.Optional of converter type: " + FieldConverter.class.getName() + + " for converter class: " + ContainerSourceTypeOptionalFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerSourceTypeRawCollectionFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerSourceTypeRawCollectionFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve source type: interface java.util.Collection of converter type: " + FieldConverter.class.getName() + + " for converter class: " + ContainerSourceTypeRawCollectionFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerSourceTypeGenericCollectionFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerSourceTypeGenericCollectionFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve source type: java.util.Collection of converter type: " + FieldConverter.class.getName() + + " for converter class: " + ContainerSourceTypeGenericCollectionFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerSourceTypeWildcardCollectionFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerSourceTypeWildcardCollectionFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve source type: java.util.Collection of converter type: " + FieldConverter.class.getName() + + " for converter class: " + ContainerSourceTypeWildcardCollectionFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerSourceTypeGenericArrayFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerSourceTypeGenericArrayFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve source type: T[] of converter type: " + FieldConverter.class.getName() + + " for converter class: " + ContainerSourceTypeGenericArrayFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerSourceTypeGenericFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerSourceTypeGenericFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve source type: T of converter type: " + FieldConverter.class.getName() + + " for converter class: " + ContainerSourceTypeGenericFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerTargetTypeOptionalFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerTypeOptionalFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve target type: java.util.Optional of converter type: " + + FieldConverter.class.getName() + " for converter class: " + + ContainerTypeOptionalFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerTargetTypeRawCollectionFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerTargetTypeRawCollectionFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve target type: interface java.util.Collection of converter type: " + + FieldConverter.class.getName() + " for converter class: " + + ContainerTargetTypeRawCollectionFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerTargetTypeGenericCollectionFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerTargetTypeGenericCollectionFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve target type: java.util.Collection of converter type: " + + FieldConverter.class.getName() + " for converter class: " + + ContainerTargetTypeGenericCollectionFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerTargetTypeWildcardCollectionFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerTargetTypeWildcardCollectionFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve target type: java.util.Collection of converter type: " + + FieldConverter.class.getName() + " for converter class: " + + ContainerTargetTypeWildcardCollectionFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerTargetTypeGenericArrayFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerTargetTypeGenericArrayFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve target type: T[] of converter type: " + + FieldConverter.class.getName() + " for converter class: " + + ContainerTargetTypeGenericArrayFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenContainerTargetTypeGenericFieldConverterThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ContainerTargetTypeGenericFieldConverter.class, FieldConverter.class)); + String expectedMessage = "Cannot resolve target type: T of converter type: " + + FieldConverter.class.getName() + " for converter class: " + + ContainerTargetTypeGenericFieldConverter.class.getName(); + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveConvertiblePairWhenConverterClassNullThenException() { + NullPointerException exception = assertThrows(NullPointerException.class, + () -> ConversionUtils.resolveConvertiblePair(null, FieldConverter.class)); + assertThat(exception.getMessage(), is("converterClass must not be null")); + } + + @Test + void resolveConvertiblePairWhenConverterTypeNullThenException() { + NullPointerException exception = assertThrows(NullPointerException.class, + () -> ConversionUtils.resolveConvertiblePair(ClassImplementsFieldConverter.class, null)); + assertThat(exception.getMessage(), is("converterType must not be null")); + } + + @Test + void resolveConvertiblePairWhenSingleTypeArgumentThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveConvertiblePair(ClassImplementsMultipleInterfacesFieldConverter.class, Callable.class)); + String expectedMessage = "Converter type: " + Callable.class.getName() + " must have 2 type arguments"; + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveFieldTypeWhenSimpleFieldThenResolves() { + ResolvableType resolvableType = ConversionUtils.resolveFieldType(getField("simpleField")); + assertThat(resolvableType, notNullValue()); + assertThat(resolvableType.getType(), is(String.class)); + assertThat(resolvableType.getComponentType(), nullValue()); + assertThat(resolvableType.isCollectionLike(), is(false)); + } + + @Test + void resolveFieldTypeWhenListFieldThenResolves() { + ResolvableType resolvableType = ConversionUtils.resolveFieldType(getField("listField")); + assertThat(resolvableType, notNullValue()); + assertThat(resolvableType.getType(), is(List.class)); + assertThat(resolvableType.getComponentType(), is(String.class)); + assertThat(resolvableType.isCollectionLike(), is(true)); + } + + @Test + void resolveFieldTypeWhenSetFieldThenResolves() { + ResolvableType resolvableType = ConversionUtils.resolveFieldType(getField("setField")); + assertThat(resolvableType, notNullValue()); + assertThat(resolvableType.getType(), is(Set.class)); + assertThat(resolvableType.getComponentType(), is(String.class)); + assertThat(resolvableType.isCollectionLike(), is(true)); + } + + @Test + void resolveFieldTypeWhenCollectionFieldThenResolves() { + ResolvableType resolvableType = ConversionUtils.resolveFieldType(getField("collectionField")); + assertThat(resolvableType, notNullValue()); + assertThat(resolvableType.getType(), is(Collection.class)); + assertThat(resolvableType.getComponentType(), is(String.class)); + assertThat(resolvableType.isCollectionLike(), is(true)); + } + + @Test + void resolveFieldTypeWhenArrayFieldThenResolves() { + ResolvableType resolvableType = ConversionUtils.resolveFieldType(getField("arrayField")); + assertThat(resolvableType, notNullValue()); + assertThat(resolvableType.getType(), is(String[].class)); + assertThat(resolvableType.getComponentType(), is(String.class)); + assertThat(resolvableType.isCollectionLike(), is(true)); + } + + @Test + void resolveFieldTypeWhenOptionalFieldThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveFieldType(getField("optionalField"))); + String expectedMessage = "Cannot resolve Field: " + TestPojo.class.getName() + ".optionalField" + + " target type: java.util.Optional"; + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveFieldTypeWhenRawCollectionFieldThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveFieldType(getField("rawCollectionField"))); + String expectedMessage = "Cannot resolve Field: " + TestPojo.class.getName() + ".rawCollectionField" + + " target type: interface java.util.Collection"; + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveFieldTypeWhenGenericCollectionFieldThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveFieldType(getField("genericCollectionField"))); + String expectedMessage = "Cannot resolve Field: " + TestPojo.class.getName() + ".genericCollectionField" + + " target type: java.util.Collection"; + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveFieldTypeWhenWildcardCollectionFieldThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveFieldType(getField("wildcardCollectionField"))); + String expectedMessage = "Cannot resolve Field: " + TestPojo.class.getName() + ".wildcardCollectionField" + + " target type: java.util.Collection"; + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveFieldTypeWhenGenericArrayFieldThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveFieldType(getField("genericArrayField"))); + String expectedMessage = "Cannot resolve Field: " + TestPojo.class.getName() + ".genericArrayField" + + " target type: T[]"; + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveFieldTypeWhenGenericFieldThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> ConversionUtils.resolveFieldType(getField("genericField"))); + String expectedMessage = "Cannot resolve Field: " + TestPojo.class.getName() + ".genericField" + + " target type: T"; + assertThat(exception.getMessage(), is(expectedMessage)); + } + + @Test + void resolveFieldTypeWhenFieldNullThenException() { + NullPointerException exception = assertThrows(NullPointerException.class, + () -> ConversionUtils.resolveFieldType(null)); + assertThat(exception.getMessage(), is("field must not be null")); + } + + private static Field getField(String name) { + return FieldUtils.getDeclaredField(TestPojo.class, name, true); + } + + static class ClassImplementsFieldConverter implements FieldConverter { + + @Override + public Integer convertToFieldType(String dbData) { + return 0; + } + + @Override + public String convertToDatabaseType(Integer field) { + return ""; + } + } + + static class ClassImplementsMultipleInterfacesFieldConverter implements Supplier, Callable, FieldConverter { + + @Override + public Integer convertToFieldType(String dbData) { + return 0; + } + + @Override + public String convertToDatabaseType(Integer field) { + return ""; + } + + @Override + public String get() { + return ""; + } + + @Override + public String call() { + return ""; + } + } + + static class ClassExtendsClassImplementingFieldConverter extends ClassImplementsFieldConverter { + } + + static class ClassExtendsClassImplementingMultipleInterfacesFieldConverter extends ClassImplementsMultipleInterfacesFieldConverter { + } + + static class GenericFieldConverter implements FieldConverter { + + @Override + public X convertToFieldType(Y dbData) { + return null; + } + + @Override + public Y convertToDatabaseType(X field) { + return null; + } + } + + static class ClassExtendsGenericFieldConverter extends GenericFieldConverter { + } + + static class ClassExtendsClassExtendingGenericFieldConverter extends ClassExtendsGenericFieldConverter { + } + + static class ContainerTypeListFieldConverter extends GenericFieldConverter, List> { + } + + static class ContainerTypeSetFieldConverter extends GenericFieldConverter, Set> { + } + + static class ContainerTypeCollectionFieldConverter extends GenericFieldConverter, Collection> { + } + + static class ArrayTypeFieldConverter extends GenericFieldConverter { + } + + static class ContainerSourceTypeOptionalFieldConverter extends GenericFieldConverter, List> { + } + + static class ContainerSourceTypeRawCollectionFieldConverter extends GenericFieldConverter> { + } + + static class ContainerSourceTypeGenericCollectionFieldConverter extends GenericFieldConverter, List> { + } + + static class ContainerSourceTypeWildcardCollectionFieldConverter extends GenericFieldConverter, List> { + } + + static class ContainerSourceTypeGenericArrayFieldConverter extends GenericFieldConverter> { + } + + static class ContainerSourceTypeGenericFieldConverter extends GenericFieldConverter> { + } + + static class ContainerTypeOptionalFieldConverter extends GenericFieldConverter, Optional> { + } + + static class ContainerTargetTypeRawCollectionFieldConverter extends GenericFieldConverter, Collection> { + } + + static class ContainerTargetTypeGenericCollectionFieldConverter extends GenericFieldConverter, Collection> { + } + + static class ContainerTargetTypeWildcardCollectionFieldConverter extends GenericFieldConverter, Collection> { + } + + static class ContainerTargetTypeGenericArrayFieldConverter extends GenericFieldConverter, T[]> { + } + + static class ContainerTargetTypeGenericFieldConverter extends GenericFieldConverter, T> { + } + + static class TestPojo { + String simpleField; + List listField; + Set setField; + Collection collectionField; + String[] arrayField; + Optional optionalField; + Collection rawCollectionField; + Collection genericCollectionField; + Collection wildcardCollectionField; + T[] genericArrayField; + T genericField; + } +} diff --git a/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerHnswIndexTest.java b/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerHnswIndexTest.java index bbe6829..3e81396 100644 --- a/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerHnswIndexTest.java +++ b/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerHnswIndexTest.java @@ -116,6 +116,16 @@ public void testNotFloatArrayField_throwsException() { assertThat(thrown.getMessage(), is("Only a float array field can have vector index")); } + @Test + public void testFloatListField_throwsException() { + RuntimeException thrown = assertThrows( + RuntimeException.class, + () -> scanner.parseIndexes(ItemWithFloatListField.class), + "Expected RuntimeException() to throw, but it didn't" + ); + assertThat(thrown.getMessage(), is("Only a float array field can have vector index")); + } + @Test public void testNoHnswAnnotation_throwsException() { RuntimeException thrown = assertThrows( @@ -171,6 +181,15 @@ static class ItemWithNotFloatArrayField { private String[] vector; } + static class ItemWithFloatListField { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "hnsw_vector", type = HNSW) + @Ivf(metric = Metric.L2, dimension = 8) + private List vector; + } + static class ItemWithoutHnswAnnotation { @Reindex(name = "id", isPrimaryKey = true) private Integer id; diff --git a/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerIvfIndexTest.java b/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerIvfIndexTest.java index aab155e..c7b8109 100644 --- a/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerIvfIndexTest.java +++ b/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerIvfIndexTest.java @@ -111,6 +111,16 @@ public void testNotFloatArrayField_throwsException() { assertThat(thrown.getMessage(), is("Only a float array field can have vector index")); } + @Test + public void testFloatListField_throwsException() { + RuntimeException thrown = assertThrows( + RuntimeException.class, + () -> scanner.parseIndexes(ItemWithFloatListField.class), + "Expected RuntimeException() to throw, but it didn't" + ); + assertThat(thrown.getMessage(), is("Only a float array field can have vector index")); + } + @Test public void testNoIvfAnnotation_throwsException() { RuntimeException thrown = assertThrows( @@ -166,6 +176,15 @@ static class ItemWithNotFloatArrayField { private String[] vector; } + static class ItemWithFloatListField { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "ivf_vector", type = IVF) + @Ivf(metric = Metric.L2, dimension = 8) + private List vector; + } + static class ItemWithoutIvfAnnotation { @Reindex(name = "id", isPrimaryKey = true) private Integer id; diff --git a/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerVecBfIndexTest.java b/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerVecBfIndexTest.java index 5e93da1..5d7d611 100644 --- a/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerVecBfIndexTest.java +++ b/src/test/java/ru/rt/restream/reindexer/fast/ReindexAnnotationScannerVecBfIndexTest.java @@ -101,6 +101,16 @@ public void testNotArrayField_throwsException() { assertThat(thrown.getMessage(), is("Only a float array field can have vector index")); } + @Test + public void testFloatListField_throwsException() { + RuntimeException thrown = assertThrows( + RuntimeException.class, + () -> scanner.parseIndexes(ItemWithFloatListField.class), + "Expected RuntimeException() to throw, but it didn't" + ); + assertThat(thrown.getMessage(), is("Only a float array field can have vector index")); + } + @Test public void testNotFloatArrayField_throwsException() { RuntimeException thrown = assertThrows( @@ -166,6 +176,15 @@ static class ItemWithNotFloatArrayField { private String[] vector; } + static class ItemWithFloatListField { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "vec_bf_vector", type = VEC_BF) + @Ivf(metric = Metric.L2, dimension = 8) + private List vector; + } + static class ItemWithoutVecBfAnnotation { @Reindex(name = "id", isPrimaryKey = true) private Integer id; diff --git a/src/test/java/ru/rt/restream/reindexer/util/CollectionUtilsTest.java b/src/test/java/ru/rt/restream/reindexer/util/CollectionUtilsTest.java new file mode 100644 index 0000000..d80b43a --- /dev/null +++ b/src/test/java/ru/rt/restream/reindexer/util/CollectionUtilsTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2020 Restream + * + * 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 ru.rt.restream.reindexer.util; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.NavigableSet; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for {@link CollectionUtils}. + */ +class CollectionUtilsTest { + + @Test + void createCollectionWhenCollectionThenArrayList() { + Collection collection = CollectionUtils.createCollection(Collection.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(ArrayList.class)); + } + + @Test + void createCollectionWhenListThenArrayList() { + Collection collection = CollectionUtils.createCollection(List.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(ArrayList.class)); + } + + @Test + void createCollectionWhenArrayListThenArrayList() { + Collection collection = CollectionUtils.createCollection(ArrayList.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(ArrayList.class)); + } + + @Test + void createCollectionWhenSetThenLinkedHashSet() { + Collection collection = CollectionUtils.createCollection(Set.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(LinkedHashSet.class)); + } + + @Test + void createCollectionWhenLinkedHashSetThenLinkedHashSet() { + Collection collection = CollectionUtils.createCollection(LinkedHashSet.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(LinkedHashSet.class)); + } + + @Test + void createCollectionWhenHashSetThenHashSet() { + Collection collection = CollectionUtils.createCollection(HashSet.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(HashSet.class)); + } + + @Test + void createCollectionWhenLinkedListThenLinkedList() { + Collection collection = CollectionUtils.createCollection(LinkedList.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(LinkedList.class)); + } + + @Test + void createCollectionWhenTreeSetThenTreeSet() { + Collection collection = CollectionUtils.createCollection(TreeSet.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(TreeSet.class)); + } + + @Test + void createCollectionWhenNavigableSetThenNavigableSet() { + Collection collection = CollectionUtils.createCollection(NavigableSet.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(TreeSet.class)); + } + + @Test + void createCollectionWhenSortedSetThenSortedSet() { + Collection collection = CollectionUtils.createCollection(SortedSet.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(TreeSet.class)); + } + + @Test + void createCollectionWhenEnumSetThenEnumSet() { + Collection collection = CollectionUtils.createCollection(EnumSet.class, TestEnum.class, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(EnumSet.class)); + } + + @Test + void createCollectionWhenArrayDequeThenArrayDeque() { + Collection collection = CollectionUtils.createCollection(ArrayDeque.class, null, 0); + assertThat(collection, notNullValue()); + assertThat(collection, instanceOf(ArrayDeque.class)); + } + + @Test + void createCollectionWhenEnumSetElementTypeNullThenException() { + NullPointerException exception = assertThrows(NullPointerException.class, + () -> CollectionUtils.createCollection(EnumSet.class, null, 0)); + assertThat(exception.getMessage(), is("Enum type must not be null")); + } + + @Test + void createCollectionWhenEnumSetElementTypeNotEnumThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> CollectionUtils.createCollection(EnumSet.class, Object.class, 0)); + assertThat(exception.getMessage(), is("Supplied type is not an enum: java.lang.Object")); + } + + @Test + void createCollectionWhenQueueThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> CollectionUtils.createCollection(Queue.class, null, 0)); + assertThat(exception.getMessage(), is("Unsupported Collection type: java.util.Queue")); + } + + @Test + void createCollectionWhenHashMapThenException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> CollectionUtils.createCollection(HashMap.class, null, 0)); + assertThat(exception.getMessage(), is("Unsupported Collection type: java.util.HashMap")); + } + + enum TestEnum { + TEST_CONSTANT + } +}