Skip to content

Commit 6c885de

Browse files
committed
Support custom conversions for the mapped Item fields
- Implemented FieldConverterRegistryFactory - Exposed custom conversion api in ReindexerConfiguration Closes gh-115
1 parent 693616c commit 6c885de

3 files changed

Lines changed: 215 additions & 0 deletions

File tree

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: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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.convert;
17+
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
import ru.rt.restream.reindexer.annotations.Convert;
21+
import ru.rt.restream.reindexer.convert.util.ConversionUtils;
22+
import ru.rt.restream.reindexer.convert.util.ResolvableType;
23+
import ru.rt.restream.reindexer.util.Pair;
24+
25+
import java.lang.reflect.Constructor;
26+
import java.lang.reflect.Field;
27+
import java.util.HashMap;
28+
import java.util.Map;
29+
import java.util.Objects;
30+
import java.util.concurrent.locks.ReadWriteLock;
31+
import java.util.concurrent.locks.ReentrantReadWriteLock;
32+
33+
/**
34+
* For internal use only, as this contract is likely to change.
35+
*/
36+
public enum FieldConverterRegistryFactory implements FieldConverterRegistry {
37+
INSTANCE;
38+
39+
private static final Logger LOGGER = LoggerFactory.getLogger(FieldConverterRegistryFactory.class);
40+
41+
private final ReadWriteLock lock = new ReentrantReadWriteLock();
42+
43+
private final Map<Class<?>, FieldConverter<?, ?>> converters = new HashMap<>();
44+
45+
private final Map<ResolvableType, FieldConverter<?, ?>> globalConverters = new HashMap<>();
46+
47+
private final Map<Pair<Class<?>, String>, FieldConverter<?, ?>> fieldConverters = new HashMap<>();
48+
49+
/**
50+
* Returns a {@link FieldConverter} that is mapped for the given {@code field} or {@literal null}.
51+
* If the {@code FieldConverter} is not mapped directly for the field, looks up for a global one registered via
52+
* {@link FieldConverterRegistry#registerGlobalConverter(FieldConverter)}, if global {@code FieldConverter} is found checks
53+
* the {@code converterClass} from {@link Convert} annotation if its type is not default and does not match
54+
* the global converter's type, then specified in the annotation converter takes precedence and
55+
* is created for the {@code field}.
56+
* This method returns {@literal null} if {@code Convert} annotation's attribute {@code disableConversion} is
57+
* set to {@literal true} or if none of the checks above resulted in finding an eligible field converter.
58+
* @param field the {@link Field} to use
59+
* @return the {@link FieldConverter} to use
60+
*/
61+
@SuppressWarnings("unchecked")
62+
public <X, Y> FieldConverter<X, Y> getFieldConverter(Field field) {
63+
Objects.requireNonNull(field, "field must not be null");
64+
Convert convert = field.getAnnotation(Convert.class);
65+
if (convert != null && convert.disableConversion()) {
66+
return null;
67+
}
68+
Pair<Class<?>, String> key = new Pair<>(field.getDeclaringClass(), field.getName());
69+
FieldConverter<?, ?> converter;
70+
lock.readLock().lock();
71+
try {
72+
converter = fieldConverters.get(key);
73+
} finally {
74+
lock.readLock().unlock();
75+
}
76+
if (converter != null) {
77+
/*
78+
* Return immediately if field converter has been found.
79+
* Programmatically configured converter takes precedence
80+
* over global or the one specified in @Convert annotation.
81+
*/
82+
return (FieldConverter<X, Y>) converter;
83+
}
84+
ResolvableType fieldType = ConversionUtils.resolveFieldType(field);
85+
lock.readLock().lock();
86+
try {
87+
converter = globalConverters.get(fieldType);
88+
} finally {
89+
lock.readLock().unlock();
90+
}
91+
/*
92+
* Initialize a converter instance specified in @Convert annotation
93+
* if annotation is present and converterClass is not default FieldConverter.class,
94+
* and global converter has not been found or its class does not match the one specified
95+
* in the annotation. The converter specified in the annotation always takes precedence
96+
* over the global one.
97+
*/
98+
if (convert != null && convert.converterClass() != FieldConverter.class
99+
&& (converter == null || converter.getClass() != convert.converterClass())) {
100+
lock.writeLock().lock();
101+
try {
102+
converter = fieldConverters.get(key);
103+
if (converter != null) {
104+
return (FieldConverter<X, Y>) converter;
105+
}
106+
converter = globalConverters.get(fieldType);
107+
if (converter == null || converter.getClass() != convert.converterClass()) {
108+
converter = converters.computeIfAbsent(convert.converterClass(), this::instantiateFieldConverter);
109+
fieldConverters.put(key, converter);
110+
}
111+
} finally {
112+
lock.writeLock().unlock();
113+
}
114+
}
115+
return (FieldConverter<X, Y>) converter;
116+
}
117+
118+
/**
119+
* {@inheritDoc}
120+
*/
121+
@Override
122+
public void registerFieldConverter(Class<?> itemClass, String fieldName, FieldConverter<?, ?> converter) {
123+
Objects.requireNonNull(itemClass, "itemClass must not be null");
124+
Objects.requireNonNull(fieldName, "fieldName must not be null");
125+
Objects.requireNonNull(converter, "fieldConverter must not be null");
126+
lock.writeLock().lock();
127+
try {
128+
FieldConverter<?, ?> prev = fieldConverters.put(new Pair<>(itemClass, fieldName), converter);
129+
if (LOGGER.isTraceEnabled()) {
130+
if (prev == null) {
131+
LOGGER.trace("Registered field converter {} for {}.{}", converter, itemClass.getName(), fieldName);
132+
} else {
133+
LOGGER.trace("Converter: {} was overridden to {} for {}.{}", prev, converter, itemClass.getName(), fieldName);
134+
}
135+
}
136+
} finally {
137+
lock.writeLock().unlock();
138+
}
139+
}
140+
141+
/**
142+
* {@inheritDoc}
143+
*/
144+
@Override
145+
public void registerGlobalConverter(FieldConverter<?, ?> converter) {
146+
Objects.requireNonNull(converter, "converter must not be null");
147+
Pair<ResolvableType, ResolvableType> convertiblePair = converter.getConvertiblePair();
148+
lock.writeLock().lock();
149+
try {
150+
FieldConverter<?, ?> prev = globalConverters.put(convertiblePair.getFirst(), converter);
151+
if (LOGGER.isTraceEnabled()) {
152+
if (prev == null) {
153+
LOGGER.trace("Registered global converter {} for {}", converter, convertiblePair.getFirst());
154+
} else {
155+
LOGGER.trace("Global converter: {} was overridden to {} for {}", prev, converter, convertiblePair.getFirst());
156+
}
157+
}
158+
} finally {
159+
lock.writeLock().unlock();
160+
}
161+
}
162+
163+
private FieldConverter<?, ?> instantiateFieldConverter(Class<?> converterClass) {
164+
try {
165+
Constructor<?> constructor = converterClass.getDeclaredConstructor();
166+
constructor.setAccessible(true);
167+
return (FieldConverter<?, ?>) constructor.newInstance();
168+
} catch (Exception e) {
169+
throw new RuntimeException(e);
170+
}
171+
}
172+
}

src/main/java/ru/rt/restream/reindexer/convert/util/ConversionUtils.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.apache.commons.lang3.reflect.TypeUtils;
1919
import ru.rt.restream.reindexer.util.Pair;
2020

21+
import java.lang.reflect.Field;
2122
import java.lang.reflect.Type;
2223
import java.lang.reflect.TypeVariable;
2324
import java.util.Collection;
@@ -32,6 +33,8 @@ public final class ConversionUtils {
3233

3334
private static final Map<Class<?>, Pair<ResolvableType, ResolvableType>> CONVERTIBLE_PAIR_CACHE = new ConcurrentHashMap<>();
3435

36+
private static final Map<Pair<Class<?>, String>, ResolvableType> FIELD_TYPE_CACHE = new ConcurrentHashMap<>();
37+
3538
/**
3639
* Resolves source and target types based on {@code [0]} and {@code [1]} parameters from {@code converterType} interface implementation.
3740
* If {@code converterType} source or target type is an array, the type is determined by {@code Class#getComponentType},
@@ -74,6 +77,32 @@ public static Pair<ResolvableType, ResolvableType> resolveConvertiblePair(Class<
7477
});
7578
}
7679

80+
/**
81+
* Resolves a field type. If the field type is an array, the target type is determined
82+
* by {@code Class#getComponentType}, in case of a generic return type, only {@code java.util.Collection}
83+
* assignable types are considered, nested generic types (i.e. {@code List<List<String>>})
84+
* are not supported, such types will not be recognized and IllegalArgumentException will be thrown.
85+
* @param field the field to use
86+
* @return the {@link ResolvableType} to use
87+
*/
88+
public static ResolvableType resolveFieldType(Field field) {
89+
Objects.requireNonNull(field, "field must not be null");
90+
// https://bugs.openjdk.java.net/browse/JDK-8161372
91+
Pair<Class<?>, String> key = new Pair<>(field.getDeclaringClass(), field.getName());
92+
ResolvableType resolvableType = FIELD_TYPE_CACHE.get(key);
93+
if (resolvableType != null) {
94+
return resolvableType;
95+
}
96+
return FIELD_TYPE_CACHE.computeIfAbsent(key, k -> {
97+
ResolvableType result = resolveType(field.getGenericType());
98+
if (result == null) {
99+
throw new IllegalArgumentException(String.format("Cannot resolve Field: %s.%s target type: %s",
100+
field.getDeclaringClass().getName(), field.getName(), field.getGenericType()));
101+
}
102+
return result;
103+
});
104+
}
105+
77106
private static ResolvableType resolveType(Type type) {
78107
if (TypeUtils.isAssignable(type, Collection.class)) {
79108
Type typeArgument = TypeUtils.getTypeArguments(type, Collection.class)

0 commit comments

Comments
 (0)