Skip to content

Commit fcb529d

Browse files
committed
Consider FieldConverter for mapped fields
Closes gh-115
1 parent 66f693d commit fcb529d

20 files changed

+2106
-84
lines changed

src/main/java/ru/rt/restream/reindexer/ReindexerConfiguration.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@
2222
import ru.rt.restream.reindexer.binding.cproto.DataSourceConfiguration;
2323
import ru.rt.restream.reindexer.binding.cproto.DataSourceFactory;
2424
import ru.rt.restream.reindexer.binding.cproto.DataSourceFactoryStrategy;
25+
import ru.rt.restream.reindexer.convert.FieldConverterRegistry;
26+
import ru.rt.restream.reindexer.convert.FieldConverterRegistryFactory;
2527
import ru.rt.restream.reindexer.exceptions.UnimplementedException;
2628

2729
import java.net.URI;
2830
import java.time.Duration;
2931
import java.util.ArrayList;
3032
import java.util.List;
3133
import java.util.Objects;
34+
import java.util.function.Consumer;
3235

3336
/**
3437
* Represents approach for bootstrapping Reindexer.
@@ -103,6 +106,17 @@ public ReindexerConfiguration dataSourceFactory(DataSourceFactory dataSourceFact
103106
return this;
104107
}
105108

109+
/**
110+
* Allows customizing a {@link FieldConverterRegistry}.
111+
*
112+
* @param customizer the {@link FieldConverterRegistry} customizer.
113+
* @return the {@link ReindexerConfiguration} for further customizations
114+
*/
115+
public ReindexerConfiguration fieldConverterRegistry(Consumer<FieldConverterRegistry> customizer) {
116+
customizer.accept(FieldConverterRegistryFactory.INSTANCE);
117+
return this;
118+
}
119+
106120
/**
107121
* Configure reindexer connection pool size. Defaults to 8.
108122
*
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.annotations;
17+
18+
import ru.rt.restream.reindexer.convert.FieldConverter;
19+
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
/**
26+
* Specifies how fields are converted between the Reindexer database type
27+
* and the one used within the POJO representation.
28+
*/
29+
@Retention(RetentionPolicy.RUNTIME)
30+
@Target(ElementType.FIELD)
31+
public @interface Convert {
32+
33+
/**
34+
* Specifies a {@link FieldConverter} implementation to be used for converting
35+
* fields between Reindexer database type and the one used within the POJO representation.
36+
* @return the {@link FieldConverter} implementation to use
37+
*/
38+
Class<? extends FieldConverter> converterClass() default FieldConverter.class;
39+
40+
/**
41+
* Specifies whether conversion should be disabled for the given field,
42+
* useful in case of global converter should not be applied for specific fields.
43+
* Defaults to {@literal false}.
44+
* @return true, if conversion should be disabled for the given field, defaults to {@literal false}
45+
*/
46+
boolean disableConversion() default false;
47+
}

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.GenericFieldConverter;
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+
GenericFieldConverter<?, ?> 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.FieldConverterRegistryFactory;
25+
import ru.rt.restream.reindexer.convert.GenericFieldConverter;
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+
GenericFieldConverter<?, 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 {

0 commit comments

Comments
 (0)