Skip to content

Commit 2b92cbd

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

19 files changed

Lines changed: 1873 additions & 84 deletions

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: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@
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.util.ConversionUtils;
25+
import ru.rt.restream.reindexer.convert.FieldConverter;
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;
2731
import ru.rt.restream.reindexer.vector.HnswConfig;
2832
import ru.rt.restream.reindexer.vector.HnswConfigs;
2933
import ru.rt.restream.reindexer.vector.IvfConfig;
@@ -33,11 +37,8 @@
3337

3438
import java.lang.annotation.Annotation;
3539
import java.lang.reflect.Field;
36-
import java.lang.reflect.ParameterizedType;
37-
import java.lang.reflect.Type;
3840
import java.util.ArrayList;
3941
import java.util.Arrays;
40-
import java.util.Collection;
4142
import java.util.Collections;
4243
import java.util.HashMap;
4344
import java.util.List;
@@ -341,28 +342,19 @@ private ReindexerIndex createIndex(String reindexPath, List<String> jsonPath, In
341342
}
342343

343344
private FieldInfo getFieldInfo(Field field) {
344-
Class<?> type = field.getType();
345345
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();
346+
FieldType fieldType;
347+
FieldConverter<?, ?> converter = FieldConverterRegistryFactory.INSTANCE.getFieldConverter(field);
348+
ResolvableType resolvableType = converter != null ? ConversionUtils.resolveConverterTargetType(converter.getClass())
349+
: ConversionUtils.resolveFieldType(field);
350+
fieldInfo.isArray = resolvableType.isCollectionLike();
351+
if (fieldInfo.isArray) {
352+
Class<?> componentType = getFieldType(field, resolvableType.getComponentType());
350353
fieldType = getFieldTypeByClass(componentType);
351354
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;
355+
fieldInfo.isFloatVector = resolvableType.getType().isArray() && fieldType == FLOAT;
364356
} else {
365-
fieldType = getFieldTypeByClass(type);
357+
fieldType = getFieldTypeByClass(getFieldType(field, resolvableType.getType()));
366358
}
367359

368360
if (fieldType == null) {
@@ -372,6 +364,14 @@ private FieldInfo getFieldInfo(Field field) {
372364
fieldInfo.fieldType = fieldType;
373365
return fieldInfo;
374366
}
367+
368+
private Class<?> getFieldType(Field field, Class<?> type) {
369+
if (Enum.class.isAssignableFrom(type)) {
370+
Enumerated enumerated = field.getAnnotation(Enumerated.class);
371+
return enumerated != null && enumerated.value() == EnumType.STRING ? String.class : Integer.class;
372+
}
373+
return type;
374+
}
375375

376376
private FieldType getFieldTypeByClass(Class<?> type) {
377377
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: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@
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;
2530

31+
import java.lang.reflect.Array;
2632
import java.lang.reflect.Constructor;
2733
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;
34+
import java.util.Collection;
3235
import java.util.List;
3336
import java.util.UUID;
3437

@@ -71,46 +74,45 @@ private <V> V readObject(CjsonObject cjsonObject, Class<V> itemClass) {
7174
}
7275

7376
private Object getTargetValue(Field field, CjsonElement property) {
74-
Class<?> fieldType = field.getType();
77+
FieldConverter<?, Object> converter = FieldConverterRegistryFactory.INSTANCE.getFieldConverter(field);
78+
if (converter != null) {
79+
ResolvableType resolvableType = ConversionUtils.resolveConverterTargetType(converter.getClass());
80+
return converter.convertToFieldType(getTargetValue(field, resolvableType, property));
81+
}
82+
ResolvableType resolvableType = ConversionUtils.resolveFieldType(field);
83+
return getTargetValue(field, resolvableType, property);
84+
}
85+
86+
private Object getTargetValue(Field field, ResolvableType resolvableType, CjsonElement property) {
7587
if (property.isNull()) {
76-
if (fieldType == List.class) {
77-
return new ArrayList<>();
88+
if (resolvableType.isCollectionLike()) {
89+
return resolvableType.getType().isArray() ? Array.newInstance(resolvableType.getComponentType(), 0)
90+
: CollectionUtils.createCollection(resolvableType.getType(), resolvableType.getComponentType(), 0);
7891
}
7992
return null;
8093
}
8194

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));
95+
if (resolvableType.isCollectionLike()) {
96+
List<CjsonElement> elements = property.getAsCjsonArray().list();
97+
if (resolvableType.getType().isArray()) {
98+
Object array = Array.newInstance(resolvableType.getComponentType(), elements.size());
99+
for (int i = 0; i < elements.size(); i++) {
100+
Array.set(array, i, convert(elements.get(i), resolvableType.getComponentType(), field));
101+
}
102+
return array;
89103
}
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());
104+
Collection<Object> collection = CollectionUtils
105+
.createCollection(resolvableType.getType(), resolvableType.getComponentType(), elements.size());
106+
for (CjsonElement element : elements) {
107+
collection.add(convert(element, resolvableType.getComponentType(), field));
95108
}
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;
109+
return collection;
108110
} else {
109-
return convert(property, field.getType());
111+
return convert(property, resolvableType.getType(), field);
110112
}
111113
}
112114

113-
private Object convert(CjsonElement element, Class<?> targetClass) {
115+
private Object convert(CjsonElement element, Class<?> targetClass, Field field) {
114116
if (element.isNull()) {
115117
return null;
116118
} else if (targetClass == Integer.class || targetClass == int.class) {
@@ -131,6 +133,12 @@ private Object convert(CjsonElement element, Class<?> targetClass) {
131133
return element.getAsFloat();
132134
} else if (targetClass == UUID.class) {
133135
return element.getAsUuid();
136+
} else if (Enum.class.isAssignableFrom(targetClass)) {
137+
Enumerated enumerated = field.getAnnotation(Enumerated.class);
138+
if (enumerated != null && enumerated.value() == EnumType.STRING) {
139+
return Enum.valueOf(targetClass.asSubclass(Enum.class), element.getAsString());
140+
}
141+
return targetClass.getEnumConstants()[element.getAsInteger()];
134142
} else if (element.isObject()) {
135143
return readObject(element.getAsCjsonObject(), targetClass);
136144
} else {

0 commit comments

Comments
 (0)