Skip to content

Commit

Permalink
[Refactor] Use Conversions API also for entities.
Browse files Browse the repository at this point in the history
All data will get processed by the `Conversions` API
instead of having the entity creation running in parallel.

This introduces (for now?) a cyclic dependency to the `Conversions`
infrastructure because the `EntityConverter` needs to be aware of the
available conversions.
  • Loading branch information
meistermeier committed Jan 6, 2023
1 parent 4356985 commit 7ac4144
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 82 deletions.
Expand Up @@ -16,7 +16,7 @@
package com.meistermeier.neo4j.toolbelt.conversion;

/**
* Exception to be used by the {@link TypeConverter} implementing converters.
* Exception to be used by the {@link ValueConverter} implementing converters.
*
* @author Gerrit Meier
*/
Expand Down
Expand Up @@ -16,6 +16,7 @@
package com.meistermeier.neo4j.toolbelt.conversion;

import org.neo4j.driver.Value;
import org.neo4j.driver.types.MapAccessor;

import java.util.HashSet;
import java.util.Set;
Expand All @@ -25,34 +26,33 @@
*
* @author Gerrit Meier
*/
public final class Converters implements TypeConverter {
public final class Converters {

private final Set<TypeConverter> internalTypeConverters = new HashSet<>();
private final Set<ValueConverter> internalValueConverters = new HashSet<>();
private final Set<TypeConverter<MapAccessor>> internalTypeConverters = new HashSet<>();

/**
* Convenience constructor with default converters.
*/
public Converters() {
this.internalTypeConverters.add(new DriverTypeConverter());
this.internalValueConverters.add(new DriverValueConverter());
this.internalTypeConverters.add(new EntityConverter(this));
}

@Override
public boolean canConvert(Value value, Class<?> type) {
for (TypeConverter typeConverter : internalTypeConverters) {
if (typeConverter.canConvert(value, type)) {
return true;
public <T> T convert(MapAccessor mapAccessor, Class<T> type) {
if (mapAccessor instanceof Value value) {
for (ValueConverter valueConverter : internalValueConverters) {
if (valueConverter.canConvert(value, type)) {
return valueConverter.convert(value, type);
}
}
}
return false;
}

@Override
public <T> T convert(Value value, Class<T> type) {
for (TypeConverter typeConverter : internalTypeConverters) {
if (typeConverter.canConvert(value, type)) {
return typeConverter.convert(value, type);
for (TypeConverter<MapAccessor> typeConverter : internalTypeConverters) {
if (typeConverter.canConvert(mapAccessor, type)) {
return typeConverter.convert(mapAccessor, type);
}
}
throw new ConversionException("Cannot convert %s to %s".formatted(value, type));

throw new ConversionException("Cannot convert %s to %s".formatted(mapAccessor, type));
}
}
Expand Up @@ -16,6 +16,7 @@
package com.meistermeier.neo4j.toolbelt.conversion;

import org.neo4j.driver.Value;
import org.neo4j.driver.types.Type;
import org.neo4j.driver.types.TypeSystem;

import java.time.LocalDate;
Expand All @@ -34,10 +35,11 @@
*
* @author Gerrit Meier
*/
class DriverTypeConverter implements TypeConverter {
class DriverValueConverter implements ValueConverter {

private static final TypeSystem typeSystem = TypeSystem.getDefault();
private static final List<?> SUPPORTED_SOURCE_VALUES_TYPES = List.of(typeSystem.LIST(), typeSystem.NULL());
private static final List<Type> SUPPORTED_SOURCE_VALUES_TYPES = List.of(typeSystem.LIST(), typeSystem.NULL());
private static final Map<Type, ?> SOURCE_VALUE_TARGET_COMBINATION = Map.of(typeSystem.MAP(), Map.class);

private static final Map<Class<?>, DriverTypeConversion> BASIC_CONVERSIONS = Map.of(
Long.class, conversion(Value::asObject, Long.class),
Expand All @@ -63,9 +65,10 @@ ZonedDateTime.class, conversion(Value::asZonedDateTime, ZonedDateTime.class)

@Override
public boolean canConvert(Value value, Class<?> type) {
return BASIC_CONVERSIONS.keySet().contains(type)
|| DATE_TIME_CONVERSIONS.keySet().contains(type)
|| SUPPORTED_SOURCE_VALUES_TYPES.contains(value.type());
return BASIC_CONVERSIONS.containsKey(type)
|| DATE_TIME_CONVERSIONS.containsKey(type)
|| SUPPORTED_SOURCE_VALUES_TYPES.contains(value.type())
|| (SOURCE_VALUE_TARGET_COMBINATION.containsKey(value.type()) && SOURCE_VALUE_TARGET_COMBINATION.containsValue(type));
}

@SuppressWarnings("unchecked")
Expand All @@ -74,6 +77,13 @@ public <T> T convert(Value value, Class<T> type) {
if (value.isNull()) {
return null;
}
if (typeSystem.MAP().isTypeOf(value)) {
if (type.isAssignableFrom(Map.class)) {
// convert into simple map
return (T) value.asMap(mapValue -> convert(mapValue, String.class));
}
}

if (!type.isArray() && typeSystem.LIST().isTypeOf(value)) {
return (T) value.asList(nestedValue -> convert(nestedValue, type));
}
Expand Down
@@ -0,0 +1,61 @@
/*
* Copyright 2022 Gerrit Meier
*
* 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 com.meistermeier.neo4j.toolbelt.conversion;

import org.neo4j.driver.Record;
import org.neo4j.driver.Value;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;

import java.util.Map;

/**
* Entity converter that delegates to the {@link ObjectInstantiator}
* if it can convert the incoming structure.
*
* @author Gerrit Meier
*/
public class EntityConverter implements TypeConverter<MapAccessor> {

private final TypeSystem typeSystem = TypeSystem.getDefault();
private final ObjectInstantiator objectInstantiator = new ObjectInstantiator();
private final Converters converters;

public EntityConverter(Converters converters) {
this.converters = converters;
}

@Override
public boolean canConvert(MapAccessor mapAccessor, Class<?> type) {
if (mapAccessor instanceof Value value) {
return typeSystem.NODE().isTypeOf(value) || typeSystem.MAP().isTypeOf(value);
}
return mapAccessor instanceof Record;
}

@Override
public <T> T convert(MapAccessor record, Class<T> entityClass) {
Map<String, Object> recordMap = record.asMap();
if (recordMap.size() == 1) {
Value value = record.values().iterator().next();
if (value.type().equals(typeSystem.NODE()) || value.type().equals(typeSystem.MAP())) {
return objectInstantiator.createInstance(entityClass, converters).apply(value);
}
}
// plain property based result
return objectInstantiator.createInstance(entityClass, converters).apply(record);
}
}
Expand Up @@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.meistermeier.neo4j.toolbelt.mapper;
package com.meistermeier.neo4j.toolbelt.conversion;

import com.meistermeier.neo4j.toolbelt.conversion.Converters;
import org.neo4j.driver.Value;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
Expand All @@ -29,7 +29,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
Expand All @@ -42,25 +41,37 @@
class ObjectInstantiator {

/**
* Core mapping function for class/record based mapping.
* Core entity instantiation function for class/record based mapping.
*
* @param entityClass Type to get the instance from.
* @param typeSystem Neo4j Java Driver's {@link TypeSystem}.
* @param converters Backreference to global converters for property conversion.
* @param <T> Type to process and return.
* @return New populated instance of the defined type.
*/
<T> Function<MapAccessor, T> createInstance(Class<T> entityClass, TypeSystem typeSystem, BiFunction<Value, Class<?>, Object> convertFunction) {
<T> Function<MapAccessor, T> createInstance(Class<T> entityClass, Converters converters) {
return record -> {
Constructor<T> instantiatingConstructor = determineConstructor(entityClass, record.asMap());
Map<String, Object> recordMap = record.asMap();
if (recordMap.size() == 1) {
Value value = record.values().iterator().next();
if (value.type().equals(typeSystem.NODE()) || value.type().equals(typeSystem.MAP())) {
return mapAccessorMapper(instantiatingConstructor, convertFunction).apply(value);

Parameter[] parameters = instantiatingConstructor.getParameters();
Value[] values = new Value[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
String parameterName = parameter.getName();
Value value = record.get(parameterName);
values[i] = value;
}

try {
Object[] rawValues = new Object[values.length];
for (int i = 0; i < values.length; i++) {
Value value = values[i];
rawValues[i] = converters.convert(value, getType(parameters[i]));
}
return instantiatingConstructor.newInstance(rawValues);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException |
InvocationTargetException e) {
throw new RuntimeException(e);
}
// plain property based result
return mapAccessorMapper(instantiatingConstructor, convertFunction).apply(record);
};
}

Expand Down Expand Up @@ -104,33 +115,10 @@ private int calculateIntersectionAmount(Collection<String> constructorParameterN

}

private static <T> Function<MapAccessor, T> mapAccessorMapper(Constructor<T> instantiatingConstructor, BiFunction<Value, Class<?>, Object> convertFunction) {
return record -> {

Parameter[] parameters = instantiatingConstructor.getParameters();
Value[] values = new Value[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
String parameterName = parameter.getName();
Value value = record.get(parameterName);
values[i] = value;
}

try {
Object[] rawValues = new Object[values.length];
for (int i = 0; i < values.length; i++) {
Value value = values[i];
rawValues[i] = convertFunction.apply(value, getType(parameters[i]));
}
return instantiatingConstructor.newInstance(rawValues);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException |
InvocationTargetException e) {
throw new RuntimeException(e);
}
};
}

private static Class<?> getType(Parameter parameter) throws ClassNotFoundException {
if (parameter.getType().isAssignableFrom(Map.class)) {
return Map.class;
}
return parameter.getType().getTypeParameters().length == 0
? parameter.getType()
: (Class<?>) ((ParameterizedType) parameter.getParameterizedType()).getActualTypeArguments()[0];
Expand Down
Expand Up @@ -15,32 +15,30 @@
*/
package com.meistermeier.neo4j.toolbelt.conversion;

import org.neo4j.driver.Value;

/**
* Definition of a type converter.
* An implementation must provide the methods {@code canConvert} and {@code convert}.
*
* @author Gerrit Meier
*/
public interface TypeConverter {
public interface TypeConverter<T> {

/**
* Reports if this converter can convert the given type.
*
* @param value the Java driver value
* @param value the value to convert
* @param type the field type the converter should convert the value to
* @return true, if the converter takes responsibility for this type, otherwise false
*/
boolean canConvert(Value value, Class<?> type);
boolean canConvert(T value, Class<?> type);

/**
* Converts the given driver value into the requested type.
*
* @param value the Java driver value
* @param value the value to convert
* @param type the field type the converter should convert the value to
* @param <T> Expected type of the returned object
* @param <X> Expected type of the returned object
* @return the converted value object
*/
<T> T convert(Value value, Class<T> type);
<X> X convert(T value, Class<X> type);
}
@@ -0,0 +1,28 @@
/*
* Copyright 2022 Gerrit Meier
*
* 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 com.meistermeier.neo4j.toolbelt.conversion;

import org.neo4j.driver.Value;

/**
* Definition of a Neo4j Java driver value converter.
* An implementation must provide the methods {@code canConvert} and {@code convert}.
*
* @author Gerrit Meier
*/
public interface ValueConverter extends TypeConverter<Value> {

}
Expand Up @@ -17,7 +17,6 @@

import com.meistermeier.neo4j.toolbelt.conversion.Converters;
import org.neo4j.driver.Record;
import org.neo4j.driver.Value;
import org.neo4j.driver.types.MapAccessor;
import org.neo4j.driver.types.TypeSystem;

Expand All @@ -37,7 +36,6 @@ public class Mapper {
*/
public final static Mapper INSTANCE = new Mapper();

private final ObjectInstantiator objectInstantiator = new ObjectInstantiator();
private final TypeSystem typeSystem = TypeSystem.getDefault();
private final Converters converters;

Expand Down Expand Up @@ -70,12 +68,7 @@ public <T> Function<Record, Iterable<T>> createCollectionMapperFor(Class<T> type
}

<T> T mapOne(MapAccessor mapAccessor, Class<T> type) {
if (mapAccessor instanceof Value value) {
if (converters.canConvert(value, type)) {
return converters.convert(value, type);
}
}
return (T) objectInstantiator.createInstance(type, typeSystem, this::mapOne).apply(mapAccessor);
return converters.convert(mapAccessor, type);
}

private <T> Iterable<T> mapAll(Record record, Class<T> type) {
Expand Down

0 comments on commit 7ac4144

Please sign in to comment.