Skip to content

Commit 79dd67f

Browse files
committed
Support custom conversions for the mapped Item fields
- Reconsidered internal mappings to use custom conversion api - Implemented CollectionUtils to map various collection types Closes gh-115
1 parent 6c885de commit 79dd67f

4 files changed

Lines changed: 200 additions & 74 deletions

File tree

src/main/java/ru/rt/restream/reindexer/annotations/ReindexAnnotationScanner.java

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@
2121
import ru.rt.restream.reindexer.IndexType;
2222
import ru.rt.restream.reindexer.ReindexScanner;
2323
import ru.rt.restream.reindexer.ReindexerIndex;
24+
import ru.rt.restream.reindexer.convert.FieldConverter;
25+
import ru.rt.restream.reindexer.convert.util.ConversionUtils;
26+
import ru.rt.restream.reindexer.convert.FieldConverterRegistryFactory;
2427
import ru.rt.restream.reindexer.exceptions.IndexConflictException;
2528
import ru.rt.restream.reindexer.fulltext.FullTextConfig;
2629
import ru.rt.restream.reindexer.util.BeanPropertyUtils;
30+
import ru.rt.restream.reindexer.convert.util.ResolvableType;
31+
import ru.rt.restream.reindexer.util.Pair;
2732
import ru.rt.restream.reindexer.vector.HnswConfig;
2833
import ru.rt.restream.reindexer.vector.HnswConfigs;
2934
import ru.rt.restream.reindexer.vector.IvfConfig;
@@ -33,11 +38,8 @@
3338

3439
import java.lang.annotation.Annotation;
3540
import java.lang.reflect.Field;
36-
import java.lang.reflect.ParameterizedType;
37-
import java.lang.reflect.Type;
3841
import java.util.ArrayList;
3942
import java.util.Arrays;
40-
import java.util.Collection;
4143
import java.util.Collections;
4244
import java.util.HashMap;
4345
import java.util.List;
@@ -341,28 +343,24 @@ private ReindexerIndex createIndex(String reindexPath, List<String> jsonPath, In
341343
}
342344

343345
private FieldInfo getFieldInfo(Field field) {
344-
Class<?> type = field.getType();
345346
FieldInfo fieldInfo = new FieldInfo();
346-
fieldInfo.isArray = type.isArray() || Collection.class.isAssignableFrom(type);
347-
FieldType fieldType = null;
348-
if (type.isArray()) {
349-
Class<?> componentType = type.getComponentType();
347+
FieldType fieldType;
348+
FieldConverter<?, ?> converter = FieldConverterRegistryFactory.INSTANCE.getFieldConverter(field);
349+
ResolvableType resolvableType;
350+
if (converter != null) {
351+
Pair<ResolvableType, ResolvableType> convertiblePair = converter.getConvertiblePair();
352+
resolvableType = convertiblePair.getSecond();
353+
} else {
354+
resolvableType = ConversionUtils.resolveFieldType(field);
355+
}
356+
fieldInfo.isArray = resolvableType.isCollectionLike();
357+
if (fieldInfo.isArray) {
358+
Class<?> componentType = getFieldType(field, resolvableType.getComponentType());
350359
fieldType = getFieldTypeByClass(componentType);
351360
fieldInfo.componentType = componentType;
352-
fieldInfo.isFloatVector = (fieldType == FLOAT);
353-
} else if (field.getGenericType() instanceof ParameterizedType && fieldInfo.isArray) {
354-
ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
355-
Type typeArgument = parameterizedType.getActualTypeArguments()[0];
356-
if (typeArgument instanceof Class<?>) {
357-
Class<?> componentType = (Class<?>) typeArgument;
358-
fieldType = getFieldTypeByClass(componentType);
359-
fieldInfo.componentType = componentType;
360-
}
361-
} else if (Enum.class.isAssignableFrom(type)) {
362-
Enumerated enumerated = field.getAnnotation(Enumerated.class);
363-
fieldType = enumerated != null && enumerated.value() == EnumType.STRING ? STRING : INT;
361+
fieldInfo.isFloatVector = resolvableType.getType().isArray() && fieldType == FLOAT;
364362
} else {
365-
fieldType = getFieldTypeByClass(type);
363+
fieldType = getFieldTypeByClass(getFieldType(field, resolvableType.getType()));
366364
}
367365

368366
if (fieldType == null) {
@@ -372,6 +370,14 @@ private FieldInfo getFieldInfo(Field field) {
372370
fieldInfo.fieldType = fieldType;
373371
return fieldInfo;
374372
}
373+
374+
private Class<?> getFieldType(Field field, Class<?> type) {
375+
if (Enum.class.isAssignableFrom(type)) {
376+
Enumerated enumerated = field.getAnnotation(Enumerated.class);
377+
return enumerated != null && enumerated.value() == EnumType.STRING ? String.class : Integer.class;
378+
}
379+
return type;
380+
}
375381

376382
private FieldType getFieldTypeByClass(Class<?> type) {
377383
return MAPPED_TYPES.getOrDefault(type, COMPOSITE);

src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CJsonItemWriter.java

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@
2323
import ru.rt.restream.reindexer.binding.cproto.ByteBuffer;
2424
import ru.rt.restream.reindexer.binding.cproto.ItemWriter;
2525
import ru.rt.restream.reindexer.binding.cproto.cjson.encdec.CjsonEncoder;
26+
import ru.rt.restream.reindexer.convert.FieldConverter;
27+
import ru.rt.restream.reindexer.convert.FieldConverterRegistryFactory;
2628
import ru.rt.restream.reindexer.util.BeanPropertyUtils;
2729

30+
import java.lang.annotation.Annotation;
31+
import java.lang.reflect.Array;
2832
import java.lang.reflect.Field;
2933
import java.util.List;
3034
import java.util.UUID;
@@ -43,11 +47,11 @@ public CJsonItemWriter(CtagMatcher ctagMatcher) {
4347
@Override
4448
public void writeItem(ByteBuffer buffer, T item) {
4549
CjsonEncoder cjsonEncoder = new CjsonEncoder(ctagMatcher);
46-
byte[] itemData = cjsonEncoder.encode(toCjson(item));
50+
byte[] itemData = cjsonEncoder.encode(toCjson(item, CJsonItemWriter::defaultExtract));
4751
buffer.writeBytes(itemData);
4852
}
4953

50-
private CjsonElement toCjson(Object source) {
54+
private CjsonElement toCjson(Object source, AnnotationExtractor annotationExtractor) {
5155
if (source == null) {
5256
return CjsonNull.INSTANCE;
5357
}
@@ -70,19 +74,25 @@ private CjsonElement toCjson(Object source) {
7074
return new CjsonPrimitive((Float) source);
7175
} else if (source instanceof UUID) {
7276
return new CjsonPrimitive((UUID) source);
73-
} else if (source instanceof List) {
77+
} else if (source instanceof Enum<?>) {
78+
Enumerated enumerated = annotationExtractor.extract(Enumerated.class);
79+
if (enumerated != null && enumerated.value() == EnumType.STRING) {
80+
return new CjsonPrimitive(((Enum<?>) source).name());
81+
}
82+
int ordinal = ((Enum<?>) source).ordinal();
83+
return new CjsonPrimitive((long) ordinal);
84+
} else if (source instanceof Iterable<?>) {
7485
CjsonArray cjsonArray = new CjsonArray();
75-
List<?> sourceList = (List<?>) source;
76-
for (Object element : sourceList) {
77-
CjsonElement cjsonElement = toCjson(element);
86+
for (Object element : (Iterable<?>) source) {
87+
CjsonElement cjsonElement = toCjson(element, annotationExtractor);
7888
cjsonArray.add(cjsonElement);
7989
}
8090
return cjsonArray;
81-
} else if (source.getClass().isArray() && source.getClass().getComponentType() == float.class) {
82-
float[] floatVector = (float[]) source;
91+
} else if (source.getClass().isArray()) {
92+
int length = Array.getLength(source);
8393
CjsonArray cjsonArray = new CjsonArray();
84-
for (float el : floatVector) {
85-
cjsonArray.add(new CjsonPrimitive(el));
94+
for (int i = 0; i < length; i++) {
95+
cjsonArray.add(toCjson(Array.get(source, i), annotationExtractor));
8696
}
8797
return cjsonArray;
8898
} else {
@@ -93,22 +103,18 @@ private CjsonElement toCjson(Object source) {
93103
continue;
94104
}
95105
Object fieldValue = readFieldValue(source, field);
106+
FieldConverter<Object, ?> converter = FieldConverterRegistryFactory.INSTANCE.getFieldConverter(field);
107+
if (converter != null) {
108+
fieldValue = converter.convertToDatabaseType(fieldValue);
109+
}
96110
if (fieldValue != null) {
97111
CjsonElement cjsonElement;
98112
// hack for serialization of String field with Reindex.isUuid() == true as UUID.
99-
if (field.getType() == String.class && field.isAnnotationPresent(Reindex.class)
113+
if (fieldValue instanceof String && field.isAnnotationPresent(Reindex.class)
100114
&& field.getAnnotation(Reindex.class).isUuid()) {
101115
cjsonElement = new CjsonPrimitive(UUID.fromString((String) fieldValue));
102-
} else if (Enum.class.isAssignableFrom(field.getType())) {
103-
Enumerated enumerated = field.getAnnotation(Enumerated.class);
104-
if (enumerated != null && enumerated.value() == EnumType.STRING) {
105-
cjsonElement = new CjsonPrimitive(((Enum<?>) fieldValue).name());
106-
} else {
107-
int ordinal = ((Enum<?>) fieldValue).ordinal();
108-
cjsonElement = new CjsonPrimitive((long) ordinal);
109-
}
110116
} else {
111-
cjsonElement = toCjson(fieldValue);
117+
cjsonElement = toCjson(fieldValue, field::getAnnotation);
112118
}
113119
Json json = field.getAnnotation(Json.class);
114120
String tagName = json == null ? field.getName() : json.value();
@@ -123,4 +129,11 @@ private Object readFieldValue(Object source, Field field) {
123129
return BeanPropertyUtils.getProperty(source, field.getName());
124130
}
125131

132+
private interface AnnotationExtractor {
133+
<A extends Annotation> A extract(Class<A> annotationClass);
134+
}
135+
136+
private static <A extends Annotation> A defaultExtract(Class<A> annotationClass) {
137+
return null;
138+
}
126139
}

src/main/java/ru/rt/restream/reindexer/binding/cproto/cjson/CjsonItemReader.java

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@
2121
import ru.rt.restream.reindexer.binding.cproto.ByteBuffer;
2222
import ru.rt.restream.reindexer.binding.cproto.ItemReader;
2323
import ru.rt.restream.reindexer.binding.cproto.cjson.encdec.CjsonDecoder;
24+
import ru.rt.restream.reindexer.convert.FieldConverter;
25+
import ru.rt.restream.reindexer.convert.FieldConverterRegistryFactory;
2426
import ru.rt.restream.reindexer.util.BeanPropertyUtils;
27+
import ru.rt.restream.reindexer.convert.util.ConversionUtils;
28+
import ru.rt.restream.reindexer.convert.util.ResolvableType;
29+
import ru.rt.restream.reindexer.util.CollectionUtils;
30+
import ru.rt.restream.reindexer.util.Pair;
2531

32+
import java.lang.reflect.Array;
2633
import java.lang.reflect.Constructor;
2734
import java.lang.reflect.Field;
28-
import java.lang.reflect.ParameterizedType;
29-
import java.lang.reflect.Type;
30-
import java.util.ArrayList;
31-
import java.util.Iterator;
35+
import java.util.Collection;
3236
import java.util.List;
3337
import java.util.UUID;
3438

@@ -71,46 +75,45 @@ private <V> V readObject(CjsonObject cjsonObject, Class<V> itemClass) {
7175
}
7276

7377
private Object getTargetValue(Field field, CjsonElement property) {
74-
Class<?> fieldType = field.getType();
78+
FieldConverter<?, Object> converter = FieldConverterRegistryFactory.INSTANCE.getFieldConverter(field);
79+
if (converter != null) {
80+
Pair<ResolvableType, ResolvableType> convertiblePair = converter.getConvertiblePair();
81+
return converter.convertToFieldType(getTargetValue(field, convertiblePair.getSecond(), property));
82+
}
83+
ResolvableType resolvableType = ConversionUtils.resolveFieldType(field);
84+
return getTargetValue(field, resolvableType, property);
85+
}
86+
87+
private Object getTargetValue(Field field, ResolvableType resolvableType, CjsonElement property) {
7588
if (property.isNull()) {
76-
if (fieldType == List.class) {
77-
return new ArrayList<>();
89+
if (resolvableType.isCollectionLike()) {
90+
return resolvableType.getType().isArray() ? Array.newInstance(resolvableType.getComponentType(), 0)
91+
: CollectionUtils.createCollection(resolvableType.getType(), resolvableType.getComponentType(), 0);
7892
}
7993
return null;
8094
}
8195

82-
if (fieldType == List.class) {
83-
CjsonArray array = property.getAsCjsonArray();
84-
ArrayList<Object> elements = new ArrayList<>();
85-
ParameterizedType genericType = (ParameterizedType) field.getGenericType();
86-
Type elementType = genericType.getActualTypeArguments()[0];
87-
for (CjsonElement cjsonElement : array) {
88-
elements.add(convert(cjsonElement, (Class<?>) elementType));
96+
if (resolvableType.isCollectionLike()) {
97+
List<CjsonElement> elements = property.getAsCjsonArray().list();
98+
if (resolvableType.getType().isArray()) {
99+
Object array = Array.newInstance(resolvableType.getComponentType(), elements.size());
100+
for (int i = 0; i < elements.size(); i++) {
101+
Array.set(array, i, convert(elements.get(i), resolvableType.getComponentType(), field));
102+
}
103+
return array;
89104
}
90-
return elements;
91-
} else if (Enum.class.isAssignableFrom(fieldType)) {
92-
Enumerated enumerated = field.getAnnotation(Enumerated.class);
93-
if (enumerated != null && enumerated.value() == EnumType.STRING) {
94-
return Enum.valueOf(fieldType.asSubclass(Enum.class), property.getAsString());
105+
Collection<Object> collection = CollectionUtils
106+
.createCollection(resolvableType.getType(), resolvableType.getComponentType(), elements.size());
107+
for (CjsonElement element : elements) {
108+
collection.add(convert(element, resolvableType.getComponentType(), field));
95109
}
96-
return fieldType.getEnumConstants()[property.getAsInteger()];
97-
} else if ( fieldType.isArray() && fieldType.getComponentType() == float.class) {
98-
// float_vector
99-
CjsonArray array = property.getAsCjsonArray();
100-
int size = array.list().size();
101-
float[] elements = new float[size];
102-
int i = 0;
103-
Iterator<CjsonElement> iterator = array.iterator();
104-
while (iterator.hasNext()) {
105-
elements[i++] = iterator.next().getAsFloat();
106-
}
107-
return elements;
110+
return collection;
108111
} else {
109-
return convert(property, field.getType());
112+
return convert(property, resolvableType.getType(), field);
110113
}
111114
}
112115

113-
private Object convert(CjsonElement element, Class<?> targetClass) {
116+
private Object convert(CjsonElement element, Class<?> targetClass, Field field) {
114117
if (element.isNull()) {
115118
return null;
116119
} else if (targetClass == Integer.class || targetClass == int.class) {
@@ -131,6 +134,12 @@ private Object convert(CjsonElement element, Class<?> targetClass) {
131134
return element.getAsFloat();
132135
} else if (targetClass == UUID.class) {
133136
return element.getAsUuid();
137+
} else if (Enum.class.isAssignableFrom(targetClass)) {
138+
Enumerated enumerated = field.getAnnotation(Enumerated.class);
139+
if (enumerated != null && enumerated.value() == EnumType.STRING) {
140+
return Enum.valueOf(targetClass.asSubclass(Enum.class), element.getAsString());
141+
}
142+
return targetClass.getEnumConstants()[element.getAsInteger()];
134143
} else if (element.isObject()) {
135144
return readObject(element.getAsCjsonObject(), targetClass);
136145
} else {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2020 Restream
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package ru.rt.restream.reindexer.util;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.EnumSet;
21+
import java.util.HashSet;
22+
import java.util.LinkedHashSet;
23+
import java.util.LinkedList;
24+
import java.util.List;
25+
import java.util.NavigableSet;
26+
import java.util.Objects;
27+
import java.util.Set;
28+
import java.util.SortedSet;
29+
import java.util.TreeSet;
30+
31+
/**
32+
* This class contains utility methods to work with Java collections.
33+
*/
34+
public final class CollectionUtils {
35+
36+
/**
37+
* Creates the most appropriate collection for the given {@code collectionType}.
38+
* For {@code EnumSet} collection type the {@code elementType} must not be {@literal null}
39+
* and must be an {@code Enum} type matching to {@code E} type.
40+
* @param collectionType the target collection type to create
41+
* @param elementType the element collection type
42+
* @param capacity the collection initial capacity
43+
* @param <E> the collection type argument
44+
* @return the {@link Collection} to use
45+
*/
46+
@SuppressWarnings("unchecked")
47+
public static <E> Collection<E> createCollection(Class<?> collectionType, Class<?> elementType, int capacity) {
48+
Objects.requireNonNull(collectionType, "Collection type must not be null");
49+
if (collectionType == Collection.class ||
50+
collectionType == List.class ||
51+
collectionType == ArrayList.class) {
52+
return new ArrayList<>(capacity);
53+
}
54+
if (collectionType == Set.class ||
55+
collectionType == LinkedHashSet.class ||
56+
// Java 21 collection types.
57+
"java.util.SequencedSet".equals(collectionType.getName()) ||
58+
"java.util.SequencedCollection".equals(collectionType.getName())) {
59+
return new LinkedHashSet<>(capacity);
60+
}
61+
if (collectionType == HashSet.class) {
62+
return new HashSet<>(capacity);
63+
}
64+
if (collectionType == LinkedList.class) {
65+
return new LinkedList<>();
66+
}
67+
if (collectionType == TreeSet.class ||
68+
collectionType == NavigableSet.class ||
69+
collectionType == SortedSet.class) {
70+
return new TreeSet<>();
71+
}
72+
if (EnumSet.class.isAssignableFrom(collectionType)) {
73+
return EnumSet.noneOf(asEnumType(elementType));
74+
}
75+
if (collectionType.isInterface() || !Collection.class.isAssignableFrom(collectionType)) {
76+
throw new IllegalArgumentException("Unsupported Collection type: " + collectionType.getName());
77+
}
78+
try {
79+
return (Collection<E>) collectionType.getDeclaredConstructor().newInstance();
80+
} catch (Throwable e) {
81+
throw new IllegalArgumentException(
82+
"Could not instantiate Collection type: " + collectionType.getName(), e);
83+
}
84+
}
85+
86+
@SuppressWarnings("rawtypes")
87+
private static Class<? extends Enum> asEnumType(Class<?> enumType) {
88+
Objects.requireNonNull(enumType, "Enum type must not be null");
89+
if (!Enum.class.isAssignableFrom(enumType)) {
90+
throw new IllegalArgumentException("Supplied type is not an enum: " + enumType.getName());
91+
}
92+
return enumType.asSubclass(Enum.class);
93+
}
94+
95+
private CollectionUtils() {
96+
// utils
97+
}
98+
}

0 commit comments

Comments
 (0)