Add support for CustomConversions.
This adds `Neo4jConversions`, an implementation of Spring Datas `CustomConversions`. A default instance is provided through the configuration support and also in the automatic configuration of Spring Boot. The `Neo4jConversions` is than used to create an internal `ConversionService` used by a `Neo4jConverter`, that provides methods for reading and writing values and also for decorating property accessors and parameter value providers. While writing the conversion, it turned out that we could postpone converting from drivers `Value` as late as possible. Thus all possible user provided conversions need to be conversions from `org.neo4j.driver.Value` to the desired type and back. This closes #23.
This commit is contained in:
@@ -5,12 +5,13 @@ Most of the time, the package structure under `org.neo4j.springframework.data` s
|
||||
|
||||
[[structure:mapping]]
|
||||
[source,cypher,role=constraint,requiresConcepts="dependency:Package"]
|
||||
.The mapping package must not depend on any other SDN/RX packages than `schema`
|
||||
.The mapping package must not depend on any other SDN/RX packages than `schema` and `convert`
|
||||
----
|
||||
MATCH (a:Main:Artifact)
|
||||
MATCH (a) -[:CONTAINS]-> (s:Package) WHERE s.fqn = 'org.neo4j.springframework.data.core.schema'
|
||||
OPTIONAL MATCH (a) -[:CONTAINS]-> (s:Package) WHERE s.fqn in ['org.neo4j.springframework.data.core.schema', 'org.neo4j.springframework.data.core.convert']
|
||||
WITH collect(s) as allowed, a
|
||||
MATCH (a) -[:CONTAINS]-> (p1:Package) -[:DEPENDS_ON]-> (p2:Package) <-[:CONTAINS]- (a)
|
||||
WHERE p1.fqn = 'org.neo4j.springframework.data.core.mapping'
|
||||
AND NOT (p2 = s OR (p1) -[:CONTAINS]-> (p2))
|
||||
AND NOT (p2 in allowed OR (p1) -[:CONTAINS]-> (p2))
|
||||
return p1,p2
|
||||
----
|
||||
|
||||
@@ -21,9 +21,11 @@ package org.neo4j.springframework.data.config;
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.springframework.data.core.Neo4jClient;
|
||||
import org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager;
|
||||
import org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
|
||||
/**
|
||||
* Base class for imperative SDN-RX configuration using JavaConfig.
|
||||
@@ -37,6 +39,13 @@ import org.springframework.context.annotation.Configuration;
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
|
||||
|
||||
/**
|
||||
* The driver to be used for interacting with Neo4j.
|
||||
*
|
||||
* @return the Neo4j Java driver instance to work with.
|
||||
*/
|
||||
public abstract Driver driver();
|
||||
|
||||
/**
|
||||
* The driver used here should be the driver resulting from {@link #driver()}, which is the default.
|
||||
*
|
||||
@@ -47,4 +56,16 @@ public abstract class AbstractNeo4jConfig extends Neo4jConfigurationSupport {
|
||||
public Neo4jClient neo4jClient(Driver driver) {
|
||||
return Neo4jClient.create(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}.
|
||||
*
|
||||
* @param driver The driver to synchronize against
|
||||
* @return A platform transaction manager
|
||||
*/
|
||||
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
|
||||
public PlatformTransactionManager transactionManager(Driver driver) {
|
||||
|
||||
return new Neo4jTransactionManager(driver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,21 +20,32 @@ package org.neo4j.springframework.data.config;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager;
|
||||
import org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.neo4j.springframework.data.core.ReactiveNeo4jClient;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.ReactiveTransactionManager;
|
||||
|
||||
/**
|
||||
* Base class for reactive SDN-RX configuration using JavaConfig.
|
||||
* This can be included in all scenarios in which Spring Boot is not an option.
|
||||
*
|
||||
* @author Gerrit Meier
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@Configuration
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public abstract class AbstractReactiveNeo4jConfig extends ReactiveNeo4jConfigurationSupport {
|
||||
public abstract class AbstractReactiveNeo4jConfig extends Neo4jConfigurationSupport {
|
||||
|
||||
/**
|
||||
* The driver to be used for interacting with Neo4j.
|
||||
*
|
||||
* @return the Neo4j Java driver instance to work with.
|
||||
*/
|
||||
public abstract Driver driver();
|
||||
|
||||
/**
|
||||
* The driver used here should be the driver resulting from {@link #driver()}, which is the default.
|
||||
@@ -46,4 +57,16 @@ public abstract class AbstractReactiveNeo4jConfig extends ReactiveNeo4jConfigura
|
||||
public ReactiveNeo4jClient neo4jClient(Driver driver) {
|
||||
return ReactiveNeo4jClient.create(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}.
|
||||
*
|
||||
* @param driver The driver to synchronize against
|
||||
* @return A platform transaction manager
|
||||
*/
|
||||
@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
|
||||
public ReactiveTransactionManager reactiveTransactionManager(Driver driver) {
|
||||
|
||||
return new ReactiveNeo4jTransactionManager(driver);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,37 +24,32 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.springframework.data.repository.config.Neo4jRepositoryConfigurationExtension;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
|
||||
import org.neo4j.springframework.data.core.schema.Node;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
|
||||
import org.neo4j.springframework.data.core.schema.Node;
|
||||
import org.neo4j.springframework.data.core.transaction.Neo4jTransactionManager;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Internal support class for basic configuration. The support infrastructure here is basically all around finding out about
|
||||
* which classes are to be mapped and which not. The driver is part of the configuration support, as Neo4js Java driver
|
||||
* contains both imperative and reactive components.
|
||||
* which classes are to be mapped and which not. The driver needs to be configured from a class either extending
|
||||
* {@link AbstractNeo4jConfig} for imperative or {@link AbstractReactiveNeo4jConfig} for reactive programming model.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public abstract class Neo4jConfigurationSupport {
|
||||
abstract class Neo4jConfigurationSupport {
|
||||
|
||||
/**
|
||||
* The driver to be used for interacting with Neo4j.
|
||||
*
|
||||
* @return the Neo4j Java driver instance to work with.
|
||||
*/
|
||||
public abstract Driver driver();
|
||||
@Bean
|
||||
public Neo4jConversions neo4jConversions() {
|
||||
return new Neo4jConversions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link org.neo4j.springframework.data.core.mapping.Neo4jMappingContext} equipped with entity classes
|
||||
@@ -64,26 +59,14 @@ public abstract class Neo4jConfigurationSupport {
|
||||
* @see #getMappingBasePackages()
|
||||
*/
|
||||
@Bean
|
||||
public Neo4jMappingContext neo4jMappingContext() throws ClassNotFoundException {
|
||||
public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException {
|
||||
|
||||
Neo4jMappingContext mappingContext = new Neo4jMappingContext();
|
||||
Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions);
|
||||
mappingContext.setInitialEntitySet(getInitialEntitySet());
|
||||
|
||||
return mappingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}.
|
||||
*
|
||||
* @param driver The driver to synchronize against
|
||||
* @return A platform transaction manager
|
||||
*/
|
||||
@Bean(Neo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
|
||||
public PlatformTransactionManager transactionManager(Driver driver) {
|
||||
|
||||
return new Neo4jTransactionManager(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base packages to scan for Neo4j mapped entities at startup. Will return the package name of the
|
||||
* configuration class' (the concrete class, not this one here) by default. So if you have a
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.config;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.springframework.data.repository.config.ReactiveNeo4jRepositoryConfigurationExtension;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.neo4j.springframework.data.core.mapping.Neo4jMappingContext;
|
||||
import org.neo4j.springframework.data.core.schema.Node;
|
||||
import org.neo4j.springframework.data.core.transaction.ReactiveNeo4jTransactionManager;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.ReactiveTransactionManager;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Internal support class for basic configuration. The support infrastructure here is basically all around finding out about
|
||||
* which classes are to be mapped and which not. The driver is part of the configuration support, as Neo4js Java driver
|
||||
* contains both imperative and reactive components.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @author Gerrit Meier
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public abstract class ReactiveNeo4jConfigurationSupport {
|
||||
|
||||
/**
|
||||
* The driver to be used for interacting with Neo4j.
|
||||
*
|
||||
* @return the Neo4j Java driver instance to work with.
|
||||
*/
|
||||
public abstract Driver driver();
|
||||
|
||||
/**
|
||||
* Creates a {@link Neo4jMappingContext} equipped with entity classes
|
||||
* scanned from the mapping base package.
|
||||
*
|
||||
* @return A new {@link Neo4jMappingContext} with initial classes to scan for entities set.
|
||||
* @see #getMappingBasePackages()
|
||||
*/
|
||||
@Bean
|
||||
public Neo4jMappingContext neo4jMappingContext() throws ClassNotFoundException {
|
||||
|
||||
Neo4jMappingContext mappingContext = new Neo4jMappingContext();
|
||||
mappingContext.setInitialEntitySet(getInitialEntitySet());
|
||||
|
||||
return mappingContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a {@link PlatformTransactionManager} for Neo4j based on the driver resulting from {@link #driver()}.
|
||||
*
|
||||
* @param driver The driver to synchronize against
|
||||
* @return A platform transaction manager
|
||||
*/
|
||||
@Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
|
||||
public ReactiveTransactionManager reactiveTransactionManager(Driver driver) {
|
||||
|
||||
return new ReactiveNeo4jTransactionManager(driver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base packages to scan for Neo4j mapped entities at startup. Will return the package name of the
|
||||
* configuration class' (the concrete class, not this one here) by default. So if you have a
|
||||
* {@code com.acme.AppConfig} extending {@link ReactiveNeo4jConfigurationSupport} the base package will be considered
|
||||
* {@code com.acme} unless the method is overridden to implement alternate behavior.
|
||||
*
|
||||
* @return the base packages to scan for mapped {@link Node} classes
|
||||
* or an empty collection to not enable scanning for entities.
|
||||
*/
|
||||
protected Collection<String> getMappingBasePackages() {
|
||||
|
||||
Package mappingBasePackage = getClass().getPackage();
|
||||
return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName());
|
||||
}
|
||||
|
||||
private Set<Class<?>> getInitialEntitySet() throws ClassNotFoundException {
|
||||
|
||||
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
|
||||
|
||||
for (String basePackage : getMappingBasePackages()) {
|
||||
initialEntitySet.addAll(scanForEntities(basePackage));
|
||||
}
|
||||
|
||||
return initialEntitySet;
|
||||
}
|
||||
|
||||
private Set<Class<?>> scanForEntities(String basePackage) throws ClassNotFoundException {
|
||||
|
||||
if (!StringUtils.hasText(basePackage)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
Set<Class<?>> initialEntitySet = new HashSet<Class<?>>();
|
||||
|
||||
ClassPathScanningCandidateComponentProvider componentProvider =
|
||||
new ClassPathScanningCandidateComponentProvider(false);
|
||||
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Node.class));
|
||||
|
||||
ClassLoader classLoader = ReactiveNeo4jConfigurationSupport.class.getClassLoader();
|
||||
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
|
||||
initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(), classLoader));
|
||||
}
|
||||
|
||||
return initialEntitySet;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,11 @@ import org.neo4j.driver.StatementRunner;
|
||||
import org.neo4j.driver.exceptions.NoSuchRecordException;
|
||||
import org.neo4j.driver.summary.ResultSummary;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.repository.NoResultException;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -62,11 +66,15 @@ class DefaultNeo4jClient implements Neo4jClient {
|
||||
|
||||
private final Driver driver;
|
||||
private final TypeSystem typeSystem;
|
||||
private final ConversionService conversionService;
|
||||
|
||||
DefaultNeo4jClient(Driver driver) {
|
||||
|
||||
this.driver = driver;
|
||||
this.typeSystem = driver.defaultTypeSystem();
|
||||
|
||||
this.conversionService = new DefaultConversionService();
|
||||
new Neo4jConversions().registerConvertersIn((ConverterRegistry) conversionService);
|
||||
}
|
||||
|
||||
AutoCloseableStatementRunner getStatementRunner(@Nullable final String targetDatabase) {
|
||||
@@ -244,7 +252,7 @@ class DefaultNeo4jClient implements Neo4jClient {
|
||||
public <T> MappingSpec<Optional<T>, Collection<T>, T> fetchAs(Class<T> targetClass) {
|
||||
|
||||
return new DefaultRecordFetchSpec(this.targetDatabase, this.runnableStatement,
|
||||
new SingleValueMappingFunction(targetClass));
|
||||
new SingleValueMappingFunction(conversionService, targetClass));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -41,7 +41,11 @@ import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.Neo4jClient.MappingSpec;
|
||||
import org.neo4j.springframework.data.core.Neo4jClient.OngoingBindSpec;
|
||||
import org.neo4j.springframework.data.core.Neo4jClient.RecordFetchSpec;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -58,11 +62,15 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
|
||||
|
||||
private final Driver driver;
|
||||
private final TypeSystem typeSystem;
|
||||
private final ConversionService conversionService;
|
||||
|
||||
DefaultReactiveNeo4jClient(Driver driver) {
|
||||
|
||||
this.driver = driver;
|
||||
this.typeSystem = driver.defaultTypeSystem();
|
||||
|
||||
this.conversionService = new DefaultConversionService();
|
||||
new Neo4jConversions().registerConvertersIn((ConverterRegistry) conversionService);
|
||||
}
|
||||
|
||||
Mono<RxStatementRunnerHolder> retrieveRxStatementRunnerHolder(String targetDatabase) {
|
||||
@@ -184,7 +192,7 @@ class DefaultReactiveNeo4jClient implements ReactiveNeo4jClient {
|
||||
public <R> MappingSpec<Mono<R>, Flux<R>, R> fetchAs(Class<R> targetClass) {
|
||||
|
||||
return new DefaultReactiveRecordFetchSpec<>(this.targetDatabase, this.cypherSupplier, this.parameters,
|
||||
new SingleValueMappingFunction(targetClass));
|
||||
new SingleValueMappingFunction(conversionService, targetClass));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,8 +22,9 @@ import java.util.function.BiFunction;
|
||||
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
|
||||
/**
|
||||
* Used to automatically map single valued records to a sensible Java type based on {@link Value#asObject()}.
|
||||
@@ -34,9 +35,13 @@ import org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes;
|
||||
*/
|
||||
final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Record, T> {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
private final Class<T> targetClass;
|
||||
|
||||
SingleValueMappingFunction(Class<T> targetClass) {
|
||||
SingleValueMappingFunction(ConversionService conversionService,
|
||||
Class<T> targetClass) {
|
||||
this.conversionService = conversionService;
|
||||
this.targetClass = targetClass;
|
||||
}
|
||||
|
||||
@@ -52,6 +57,7 @@ final class SingleValueMappingFunction<T> implements BiFunction<TypeSystem, Reco
|
||||
"Records with more than one value cannot be converted without a mapper.");
|
||||
}
|
||||
|
||||
return Neo4jSimpleTypes.asObject(record.get(0), targetClass);
|
||||
Value source = record.get(0);
|
||||
return source == null || source == Values.NULL ? null : conversionService.convert(source, targetClass);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import static org.springframework.data.convert.ConverterBuilder.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.exceptions.value.LossyCoercion;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Additional types that are supported out of the box.
|
||||
* Mostly all of {@link org.springframework.data.mapping.model.SimpleTypeHolder SimpleTypeHolder's} defaults.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
final class AdditionalTypes {
|
||||
|
||||
static final List<?> CONVERTERS;
|
||||
|
||||
static {
|
||||
|
||||
List<Object> hlp = new ArrayList<>();
|
||||
hlp.add(reading(Value.class, boolean[].class, AdditionalTypes::asBooleanArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Byte.class, AdditionalTypes::asByte).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, byte.class, AdditionalTypes::asByte).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, Character.class, AdditionalTypes::asCharacter).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, char.class, AdditionalTypes::asCharacter).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, char[].class, AdditionalTypes::asCharArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Date.class, AdditionalTypes::asDate).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, double[].class, AdditionalTypes::asDoubleArray).andWriting(Values::value));
|
||||
hlp.add(new EnumConverter());
|
||||
hlp.add(reading(Value.class, Float.class, AdditionalTypes::asFloat).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, float.class, AdditionalTypes::asFloat).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, float[].class, AdditionalTypes::asFloatArray).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, Integer.class, Value::asInt).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, int.class, Value::asInt).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, int[].class, AdditionalTypes::asIntArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Locale.class, AdditionalTypes::asLocale).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, long[].class, AdditionalTypes::asLongArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Short.class, AdditionalTypes::asShort).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, short.class, AdditionalTypes::asShort).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, short[].class, AdditionalTypes::asShortArray).andWriting(AdditionalTypes::value));
|
||||
hlp.add(reading(Value.class, String[].class, AdditionalTypes::asStringArray).andWriting(Values::value));
|
||||
hlp.add(
|
||||
reading(Value.class, BigDecimal.class, AdditionalTypes::asBigDecimal).andWriting(AdditionalTypes::value));
|
||||
hlp.add(
|
||||
reading(Value.class, BigInteger.class, AdditionalTypes::asBigInteger).andWriting(AdditionalTypes::value));
|
||||
hlp.add(
|
||||
reading(Value.class, TemporalAmount.class, AdditionalTypes::asTemporalAmount).andWriting(AdditionalTypes::value));
|
||||
|
||||
CONVERTERS = Collections.unmodifiableList(hlp);
|
||||
}
|
||||
|
||||
static TemporalAmount asTemporalAmount(Value value) {
|
||||
return new TemporalAmountAdapter().apply(value.asIsoDuration());
|
||||
}
|
||||
|
||||
static Value value(TemporalAmount temporalAmount) {
|
||||
return Values.value(temporalAmount);
|
||||
}
|
||||
|
||||
static BigDecimal asBigDecimal(Value value) {
|
||||
return new BigDecimal(value.asString());
|
||||
}
|
||||
|
||||
static Value value(BigDecimal bigDecimal) {
|
||||
if (bigDecimal == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(bigDecimal.toString());
|
||||
}
|
||||
|
||||
static BigInteger asBigInteger(Value value) {
|
||||
return new BigInteger(value.asString());
|
||||
}
|
||||
|
||||
static Value value(BigInteger bigInteger) {
|
||||
if (bigInteger == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(bigInteger.toString());
|
||||
}
|
||||
|
||||
static Byte asByte(Value value) {
|
||||
byte[] bytes = value.asByteArray();
|
||||
Assert.isTrue(bytes.length == 1, "Expected a byte array with exactly 1 element.");
|
||||
return bytes[0];
|
||||
}
|
||||
|
||||
static Value value(Byte aByte) {
|
||||
if (aByte == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(new Byte[] { aByte });
|
||||
}
|
||||
|
||||
static Character asCharacter(Value value) {
|
||||
char[] chars = value.asString().toCharArray();
|
||||
Assert.isTrue(chars.length == 1, "Expected a char array with exactly 1 element.");
|
||||
return chars[0];
|
||||
}
|
||||
|
||||
private static final String ISO_8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX";
|
||||
|
||||
static Date asDate(Value value) {
|
||||
try {
|
||||
return new SimpleDateFormat(ISO_8601_DATE_FORMAT).parse(value.asString());
|
||||
} catch (ParseException e) {
|
||||
throw new IllegalArgumentException("Could not parse " + value.asString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
static Value value(Date date) {
|
||||
if (date == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(new SimpleDateFormat(ISO_8601_DATE_FORMAT).format(date));
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
@WritingConverter
|
||||
static class EnumConverter implements GenericConverter {
|
||||
|
||||
private final Set<ConvertiblePair> convertiblePairs;
|
||||
|
||||
EnumConverter() {
|
||||
Set<ConvertiblePair> hlp = new HashSet<>();
|
||||
hlp.add(new ConvertiblePair(Enum.class, Value.class));
|
||||
hlp.add(new ConvertiblePair(Value.class, Enum.class));
|
||||
convertiblePairs = Collections.unmodifiableSet(hlp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return this.convertiblePairs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
Class<?> concreteTargetType = targetType.getType();
|
||||
if (source == null) {
|
||||
return concreteTargetType == Value.class ? Values.NULL : null;
|
||||
}
|
||||
|
||||
if (sourceType.getType() == Value.class) {
|
||||
return Enum.valueOf((Class<Enum>) concreteTargetType, ((Value) source).asString());
|
||||
} else {
|
||||
return Values.value(((Enum) source).name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Float asFloat(Value value) {
|
||||
return Float.parseFloat(value.asString());
|
||||
}
|
||||
|
||||
static Value value(Float aFloat) {
|
||||
if (aFloat == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(aFloat.toString());
|
||||
}
|
||||
|
||||
static Locale asLocale(Value value) {
|
||||
|
||||
return StringUtils.parseLocale(value.asString());
|
||||
}
|
||||
|
||||
static Value value(Locale locale) {
|
||||
if (locale == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(locale.toString());
|
||||
}
|
||||
|
||||
static Short asShort(Value value) {
|
||||
long val = value.asLong();
|
||||
if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
|
||||
throw new LossyCoercion(value.type().name(), "Java short");
|
||||
}
|
||||
return (short) val;
|
||||
}
|
||||
|
||||
static Value value(Short aShort) {
|
||||
if (aShort == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return Values.value(aShort.longValue());
|
||||
}
|
||||
|
||||
static boolean[] asBooleanArray(Value value) {
|
||||
boolean[] array = new boolean[value.size()];
|
||||
int i = 0;
|
||||
for (Boolean v : value.values(Value::asBoolean)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static char[] asCharArray(Value value) {
|
||||
char[] array = new char[value.size()];
|
||||
int i = 0;
|
||||
for (Character v : value.values(AdditionalTypes::asCharacter)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static String[] asStringArray(Value value) {
|
||||
String[] array = new String[value.size()];
|
||||
return value.asList(Value::asString).toArray(array);
|
||||
}
|
||||
|
||||
static double[] asDoubleArray(Value value) {
|
||||
double[] array = new double[value.size()];
|
||||
int i = 0;
|
||||
for (double v : value.values(Value::asDouble)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static float[] asFloatArray(Value value) {
|
||||
float[] array = new float[value.size()];
|
||||
int i = 0;
|
||||
for (float v : value.values(AdditionalTypes::asFloat)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static Value value(float[] aFloatArray) {
|
||||
if (aFloatArray == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
String[] values = new String[aFloatArray.length];
|
||||
int i = 0;
|
||||
for (float v : aFloatArray) {
|
||||
values[i++] = Float.toString(v);
|
||||
}
|
||||
return Values.value(values);
|
||||
}
|
||||
|
||||
static int[] asIntArray(Value value) {
|
||||
int[] array = new int[value.size()];
|
||||
int i = 0;
|
||||
for (int v : value.values(Value::asInt)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static long[] asLongArray(Value value) {
|
||||
long[] array = new long[value.size()];
|
||||
int i = 0;
|
||||
for (long v : value.values(Value::asLong)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static short[] asShortArray(Value value) {
|
||||
short[] array = new short[value.size()];
|
||||
int i = 0;
|
||||
for (short v : value.values(AdditionalTypes::asShort)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static Value value(short[] aShortArray) {
|
||||
if (aShortArray == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
long[] values = new long[aShortArray.length];
|
||||
int i = 0;
|
||||
for (short v : aShortArray) {
|
||||
values[i++] = v;
|
||||
}
|
||||
return Values.value(values);
|
||||
}
|
||||
|
||||
private AdditionalTypes() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import static org.springframework.data.convert.ConverterBuilder.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.IsoDuration;
|
||||
import org.neo4j.driver.types.Point;
|
||||
|
||||
/**
|
||||
* Conversions for all known Cypher types, diretly supported by the driver.
|
||||
* See <a href="https://neo4j.com/docs/driver-manual/current/cypher-values/">Working with Cypher values</a>.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
final class CypherTypes {
|
||||
|
||||
static final List<?> CONVERTERS;
|
||||
|
||||
static {
|
||||
|
||||
List<ConverterAware> hlp = new ArrayList<>();
|
||||
hlp.add(reading(Value.class, Void.class, v -> null).andWriting(v -> Values.NULL));
|
||||
hlp.add(reading(Value.class, void.class, v -> null).andWriting(v -> Values.NULL));
|
||||
hlp.add(reading(Value.class, Boolean.class, Value::asBoolean).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, boolean.class, Value::asBoolean).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Long.class, Value::asLong).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, long.class, Value::asLong).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Double.class, Value::asDouble).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, double.class, Value::asDouble).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, String.class, Value::asString).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, byte[].class, Value::asByteArray).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, LocalDate.class, Value::asLocalDate).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, OffsetTime.class, Value::asOffsetTime).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, LocalTime.class, Value::asLocalTime).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, ZonedDateTime.class, Value::asZonedDateTime).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, LocalDateTime.class, Value::asLocalDateTime).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, IsoDuration.class, Value::asIsoDuration).andWriting(Values::value));
|
||||
hlp.add(reading(Value.class, Point.class, Value::asPoint).andWriting(Values::value));
|
||||
|
||||
CONVERTERS = Collections.unmodifiableList(hlp);
|
||||
}
|
||||
|
||||
private CypherTypes() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import static java.util.stream.Collectors.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.springframework.core.CollectionFactory;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
import org.springframework.data.convert.CustomConversions;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack The Kleptones - A Night At The Hip-Hopera
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class Neo4jConversions extends CustomConversions {
|
||||
|
||||
private static final StoreConversions STORE_CONVERSIONS;
|
||||
private static final List<Object> STORE_CONVERTERS;
|
||||
private static final TypeDescriptor TYPE_DESCRIPTOR_OF_VALUE = TypeDescriptor.valueOf(Value.class);
|
||||
|
||||
static {
|
||||
|
||||
List<Object> converters = new ArrayList<>();
|
||||
|
||||
converters.addAll(CypherTypes.CONVERTERS);
|
||||
converters.addAll(AdditionalTypes.CONVERTERS);
|
||||
converters.addAll(SpatialTypes.CONVERTERS);
|
||||
|
||||
STORE_CONVERTERS = Collections.unmodifiableList(converters);
|
||||
STORE_CONVERSIONS = StoreConversions.of(Neo4jSimpleTypes.HOLDER, STORE_CONVERTERS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Neo4jConversions} object without custom converters.
|
||||
*/
|
||||
public Neo4jConversions() {
|
||||
this(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link CustomConversions} instance registering the given converters.
|
||||
*
|
||||
* @param converters must not be {@literal null}.
|
||||
*/
|
||||
public Neo4jConversions(Collection<?> converters) {
|
||||
super(STORE_CONVERSIONS, converters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerConvertersIn(ConverterRegistry conversionService) {
|
||||
super.registerConvertersIn(conversionService);
|
||||
|
||||
// Those can only be added at this point, as they will delegate to the target conversion service.
|
||||
conversionService.addConverter(new ValueToCollectionConverter((ConversionService) conversionService));
|
||||
conversionService.addConverter(new CollectionToValueConverter((ConversionService) conversionService));
|
||||
}
|
||||
|
||||
private static class ValueToCollectionConverter implements GenericConverter {
|
||||
|
||||
private static final Set<ConvertiblePair> CONVERTIBLE_TYPES = Collections
|
||||
.singleton(new ConvertiblePair(Value.class, Collection.class));
|
||||
private final ConversionService conversionService;
|
||||
|
||||
ValueToCollectionConverter(ConversionService conversionService) {
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return CONVERTIBLE_TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Value value = (Value) source;
|
||||
TypeDescriptor elementDesc = targetType.getElementTypeDescriptor();
|
||||
Collection<Object> target = CollectionFactory.createCollection(targetType.getType(),
|
||||
(elementDesc != null ? elementDesc.getType() : null), value.size());
|
||||
|
||||
if (elementDesc == null) {
|
||||
target.addAll(value.asList());
|
||||
} else {
|
||||
value.values().forEach(sourceElement -> target.add(this.conversionService.convert(sourceElement,
|
||||
TYPE_DESCRIPTOR_OF_VALUE, elementDesc)));
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CollectionToValueConverter implements GenericConverter {
|
||||
|
||||
private static final Set<ConvertiblePair> CONVERTIBLE_TYPES = Collections
|
||||
.singleton(new ConvertiblePair(Collection.class, Value.class));
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
CollectionToValueConverter(ConversionService conversionService) {
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||
return CONVERTIBLE_TYPES;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
Collection<?> sourceCollection = (Collection<?>) source;
|
||||
|
||||
return Values.value((sourceCollection).stream().map(v -> conversionService
|
||||
.convert(v, sourceType.elementTypeDescriptor(v), TYPE_DESCRIPTOR_OF_VALUE))
|
||||
.collect(toList()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* This orchestrates the build-in store conversions and any additional Spring converters.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack The Kleptones - A Night At The Hip-Hopera
|
||||
* @since 1.0
|
||||
*/
|
||||
public interface Neo4jConverter {
|
||||
|
||||
@Nullable
|
||||
Object readValue(@Nullable Value value, TypeInformation<?> type);
|
||||
|
||||
@Nullable
|
||||
Value writeValue(@Nullable Object value, TypeInformation<?> type);
|
||||
|
||||
/**
|
||||
* Returns a {@link PersistentPropertyAccessor} that delegates to {@code targetPropertyAccessor} and applies
|
||||
* all known conversions before returning a value.
|
||||
*
|
||||
* @param targetPropertyAccessor The property accessor to delegate to, must not be {@code null}.
|
||||
* @param <T> The type of the entity to operate on.
|
||||
* @return A {@link PersistentPropertyAccessor} guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T> PersistentPropertyAccessor<T> decoratePropertyAccessor(TypeSystem typeSystem, PersistentPropertyAccessor<T> targetPropertyAccessor);
|
||||
|
||||
/**
|
||||
* Returns a {@link ParameterValueProvider} that delegates to {@code targetParameterValueProvider} and applies
|
||||
* all known conversions before returning a value.
|
||||
*
|
||||
* @param targetParameterValueProvider The parameter value provider to delegate to, must not be {@code null}.
|
||||
* @param <T> The type of the entity to operate on.
|
||||
* @return A {@link ParameterValueProvider} guaranteed to be not {@code null}.
|
||||
*/
|
||||
<T extends PersistentProperty<T>> ParameterValueProvider<T> decorateParameterValueProvider(
|
||||
ParameterValueProvider<T> targetParameterValueProvider);
|
||||
}
|
||||
@@ -16,8 +16,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.neo4j.springframework.data.core.schema;
|
||||
package org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
@@ -26,15 +28,16 @@ import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.types.IsoDuration;
|
||||
import org.neo4j.driver.types.Path;
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.neo4j.driver.types.Relationship;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint2d;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint3d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint2d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint3d;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
|
||||
/**
|
||||
@@ -51,29 +54,32 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
@API(status = API.Status.INTERNAL, since = "1.0")
|
||||
public final class Neo4jSimpleTypes {
|
||||
|
||||
public static final Set<Class<?>> NEO4J_NATIVE_TYPES;
|
||||
private static final Set<Class<?>> NEO4J_NATIVE_TYPES;
|
||||
|
||||
static {
|
||||
Set<Class<?>> neo4jNativeTypes = new HashSet<>();
|
||||
neo4jNativeTypes.add(void.class);
|
||||
neo4jNativeTypes.add(Void.class);
|
||||
neo4jNativeTypes.add(Map.class);
|
||||
neo4jNativeTypes.add(boolean.class);
|
||||
neo4jNativeTypes.add(Boolean.class);
|
||||
neo4jNativeTypes.add(long.class);
|
||||
neo4jNativeTypes.add(Long.class);
|
||||
neo4jNativeTypes.add(String.class);
|
||||
neo4jNativeTypes.add(byte[].class);
|
||||
neo4jNativeTypes.add(LocalDate.class);
|
||||
neo4jNativeTypes.add(OffsetTime.class);
|
||||
neo4jNativeTypes.add(LocalTime.class);
|
||||
neo4jNativeTypes.add(ZonedDateTime.class);
|
||||
neo4jNativeTypes.add(LocalDateTime.class);
|
||||
|
||||
neo4jNativeTypes.add(IsoDuration.class);
|
||||
neo4jNativeTypes.add(LocalDate.class);
|
||||
neo4jNativeTypes.add(LocalDateTime.class);
|
||||
neo4jNativeTypes.add(LocalTime.class);
|
||||
neo4jNativeTypes.add(Map.class);
|
||||
neo4jNativeTypes.add(OffsetTime.class);
|
||||
neo4jNativeTypes.add(Point.class);
|
||||
neo4jNativeTypes.add(Node.class);
|
||||
neo4jNativeTypes.add(Relationship.class);
|
||||
neo4jNativeTypes.add(Path.class);
|
||||
neo4jNativeTypes.add(Void.class);
|
||||
neo4jNativeTypes.add(ZonedDateTime.class);
|
||||
neo4jNativeTypes.add(void.class);
|
||||
|
||||
neo4jNativeTypes.add(BigDecimal.class);
|
||||
neo4jNativeTypes.add(BigInteger.class);
|
||||
|
||||
neo4jNativeTypes.add(org.springframework.data.geo.Point.class);
|
||||
neo4jNativeTypes.add(GeographicPoint2d.class);
|
||||
neo4jNativeTypes.add(GeographicPoint3d.class);
|
||||
neo4jNativeTypes.add(CartesianPoint2d.class);
|
||||
neo4jNativeTypes.add(CartesianPoint3d.class);
|
||||
|
||||
neo4jNativeTypes.add(Value.class);
|
||||
|
||||
NEO4J_NATIVE_TYPES = Collections.unmodifiableSet(neo4jNativeTypes);
|
||||
}
|
||||
@@ -81,31 +87,7 @@ public final class Neo4jSimpleTypes {
|
||||
/**
|
||||
* The simple types we support plus all the simple types recognized by Spring.
|
||||
*/
|
||||
// TODO We need a conversion for some.
|
||||
// TODO Add some special treatment for Period/Duration vs IsoDuration as well as the spatial types.
|
||||
public static final SimpleTypeHolder SIMPLE_TYPE_HOLDER = new SimpleTypeHolder(NEO4J_NATIVE_TYPES, true);
|
||||
|
||||
/**
|
||||
* Converts the given Neo4j driver value into the designated type.
|
||||
*
|
||||
* @param value The value to convert.
|
||||
* @param targetClass The target class to convert into
|
||||
* @param <T> The type of the target class
|
||||
* @return The converted value or {@literal null} when the values has been {@literal null}
|
||||
* @throws IllegalArgumentException when the value cannot be converted into an object of the given target class
|
||||
*/
|
||||
public static <T> T asObject(Value value, Class<T> targetClass) {
|
||||
|
||||
Optional<Object> o = Optional.ofNullable(value).map(Value::asObject);
|
||||
if (!o.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return o.filter(v -> targetClass.isAssignableFrom(v.getClass()))
|
||||
.map(targetClass::cast)
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
String.format("%s is not assignable from %s", targetClass.getName(), o.get().getClass().getName())));
|
||||
}
|
||||
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(NEO4J_NATIVE_TYPES, true);
|
||||
|
||||
private Neo4jSimpleTypes() {
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import static org.springframework.data.convert.ConverterBuilder.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint2d;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint3d;
|
||||
import org.neo4j.springframework.data.types.Coordinate;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint2d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint3d;
|
||||
import org.neo4j.springframework.data.types.Neo4jPoint;
|
||||
import org.neo4j.springframework.data.types.PointBuilder;
|
||||
import org.springframework.data.convert.ConverterBuilder;
|
||||
import org.springframework.data.geo.Point;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Mapping of spatial types.
|
||||
* <p>
|
||||
* This replicates the behaviour of SDN+OGM. Spring Data Commons geographic points are x/y based and usually treat
|
||||
* x/y as lat/long.
|
||||
* <p>
|
||||
* Neo4j however stores x/y as long/lat when used with an Srid of 4326 or 4979 (those are geographic points). We
|
||||
* take this into account with our dedicated spatial types which can be used alternatively.
|
||||
* <p>
|
||||
* However, when converting an Spring Data Commons point to the internal value, you'll notice that we store y as x and vice versa.
|
||||
* This is intentionally. We use a hardcoded WGS-84 Srid during storage, thus you'll get back your x as latitude, y as longitude, as
|
||||
* described above.
|
||||
* <p>
|
||||
* The biggest degree of freedom will come from using an attribute of type {@link org.neo4j.driver.types.Point} directly.
|
||||
* This will be passed on as is.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
final class SpatialTypes {
|
||||
|
||||
static final List<?> CONVERTERS;
|
||||
|
||||
static {
|
||||
|
||||
List<ConverterBuilder.ConverterAware> hlp = new ArrayList<>();
|
||||
hlp.add(reading(Value.class, Point.class, SpatialTypes::asSpringDataPoint)
|
||||
.andWriting(SpatialTypes::value));
|
||||
hlp.add(reading(Value.class, Point[].class, SpatialTypes::asPointArray)
|
||||
.andWriting(SpatialTypes::value));
|
||||
|
||||
hlp.add(reading(Value.class, Neo4jPoint.class, SpatialTypes::asNeo4jPoint)
|
||||
.andWriting(SpatialTypes::value));
|
||||
|
||||
CONVERTERS = Collections.unmodifiableList(hlp);
|
||||
}
|
||||
|
||||
static Neo4jPoint asNeo4jPoint(Value value) {
|
||||
|
||||
org.neo4j.driver.types.Point point = value.asPoint();
|
||||
|
||||
Coordinate coordinate = new Coordinate(point.x(), point.y(), Double.isNaN(point.z()) ? null : point.z());
|
||||
return PointBuilder.withSrid(point.srid()).build(coordinate);
|
||||
}
|
||||
|
||||
static Value value(Neo4jPoint object) {
|
||||
|
||||
if (object instanceof CartesianPoint2d) {
|
||||
CartesianPoint2d point = (CartesianPoint2d) object;
|
||||
return Values.point(point.getSrid(), point.getX(), point.getY());
|
||||
} else if (object instanceof CartesianPoint3d) {
|
||||
CartesianPoint3d point = (CartesianPoint3d) object;
|
||||
return Values.point(point.getSrid(), point.getX(), point.getY(), point.getZ());
|
||||
} else if (object instanceof GeographicPoint2d) {
|
||||
GeographicPoint2d point = (GeographicPoint2d) object;
|
||||
return Values.point(point.getSrid(), point.getLongitude(), point.getLatitude());
|
||||
} else if (object instanceof GeographicPoint3d) {
|
||||
GeographicPoint3d point = (GeographicPoint3d) object;
|
||||
return Values.point(point.getSrid(), point.getLongitude(), point.getLatitude(),
|
||||
point.getHeight());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported point implementation: " + object.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
static Point asSpringDataPoint(Value value) {
|
||||
|
||||
org.neo4j.driver.types.Point point = value.asPoint();
|
||||
Assert.isTrue(point.srid() == 4326, "Srid must be 4326");
|
||||
|
||||
return new Point(point.y(), point.x());
|
||||
}
|
||||
|
||||
static Value value(Point point) {
|
||||
return Values.point(4326, point.getY(), point.getX());
|
||||
}
|
||||
|
||||
static Point[] asPointArray(Value value) {
|
||||
Point[] array = new Point[value.size()];
|
||||
int i = 0;
|
||||
for (Point v : value.values(SpatialTypes::asSpringDataPoint)) {
|
||||
array[i++] = v;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
static Value value(Point[] aPointArray) {
|
||||
if (aPointArray == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
Value[] values = new Value[aPointArray.length];
|
||||
int i = 0;
|
||||
for (Point v : aPointArray) {
|
||||
values[i++] = value(v);
|
||||
}
|
||||
|
||||
return Values.value(values);
|
||||
}
|
||||
|
||||
private SpatialTypes() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Period;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.time.temporal.TemporalUnit;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* This adapter maps a Driver or embedded based {@link TemporalAmount} to a valid Java temporal amount. It tries
|
||||
* to be as specific as possible: If the amount can be reliable mapped to a {@link Period}, it returns
|
||||
* a period. If only fields are present that are no estimated time unites, than it returns a {@link Duration}.
|
||||
* <br><br>
|
||||
* In cases a user has used Cypher and its <code>duration()</code> function, i.e. like so
|
||||
* <code>CREATE (s:SomeTime {isoPeriod: duration('P13Y370M45DT25H120M')}) RETURN s</code>
|
||||
* a duration object has been created that cannot be represented by either a {@link Period} or {@link Duration}. The user
|
||||
* has to map it to a plain {@link TemporalAmount} in this cases.
|
||||
* <br>
|
||||
* The Java Driver uses a <code>org.neo4j.driver.v1.types.IsoDuration</code>, embedded uses
|
||||
* <code>org.neo4j.values.storable.DurationValue</code> for representing a temporal amount, but in the end, they can be
|
||||
* treated the same.
|
||||
* However be aware that the temporal amount returned in that case may not be equal to the other one, only represents
|
||||
* the same amount after normalization.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
final class TemporalAmountAdapter implements Function<TemporalAmount, TemporalAmount> {
|
||||
|
||||
private static final int PERIOD_MASK = 0b11100;
|
||||
private static final int DURATION_MASK = 0b00011;
|
||||
private static final TemporalUnit[] SUPPORTED_UNITS = {
|
||||
ChronoUnit.YEARS,
|
||||
ChronoUnit.MONTHS,
|
||||
ChronoUnit.DAYS,
|
||||
ChronoUnit.SECONDS,
|
||||
ChronoUnit.NANOS
|
||||
};
|
||||
|
||||
private static final short FIELD_YEAR = 0;
|
||||
private static final short FIELD_MONTH = 1;
|
||||
private static final short FIELD_DAY = 2;
|
||||
private static final short FIELD_SECONDS = 3;
|
||||
private static final short FIELD_NANOS = 4;
|
||||
|
||||
private static final BiFunction<TemporalAmount, TemporalUnit, Integer> TEMPORAL_UNIT_EXTRACTOR = (d, u) -> {
|
||||
if (!d.getUnits().contains(u)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.toIntExact(d.get(u));
|
||||
};
|
||||
|
||||
@Override
|
||||
public TemporalAmount apply(TemporalAmount internalTemporalAmountRepresentation) {
|
||||
|
||||
int[] values = new int[SUPPORTED_UNITS.length];
|
||||
int type = 0;
|
||||
for (int i = 0; i < SUPPORTED_UNITS.length; ++i) {
|
||||
values[i] = TEMPORAL_UNIT_EXTRACTOR.apply(internalTemporalAmountRepresentation, SUPPORTED_UNITS[i]);
|
||||
type |= (values[i] == 0) ? 0 : (0b10000 >> i);
|
||||
}
|
||||
|
||||
boolean couldBePeriod = couldBePeriod(type);
|
||||
boolean couldBeDuration = couldBeDuration(type);
|
||||
|
||||
if (couldBePeriod && !couldBeDuration) {
|
||||
return Period.of(values[FIELD_YEAR], values[FIELD_MONTH], values[FIELD_DAY]).normalized();
|
||||
} else if (couldBeDuration && !couldBePeriod) {
|
||||
return Duration.ofSeconds(values[FIELD_SECONDS]).plusNanos(values[FIELD_NANOS]);
|
||||
} else {
|
||||
return internalTemporalAmountRepresentation;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean couldBePeriod(int type) {
|
||||
return (PERIOD_MASK & type) > 0;
|
||||
}
|
||||
|
||||
private static boolean couldBeDuration(int type) {
|
||||
return (DURATION_MASK & type) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Neo4j-specific conversion classes.
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -24,6 +24,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConverter;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
|
||||
/**
|
||||
@@ -35,8 +36,11 @@ final class DefaultNeo4jBinderFunction<T> implements Function<T, Map<String, Obj
|
||||
|
||||
private final Neo4jPersistentEntity<T> nodeDescription;
|
||||
|
||||
DefaultNeo4jBinderFunction(Neo4jPersistentEntity<T> nodeDescription) {
|
||||
private final Neo4jConverter converter;
|
||||
|
||||
DefaultNeo4jBinderFunction(Neo4jPersistentEntity<T> nodeDescription, Neo4jConverter converter) {
|
||||
this.nodeDescription = nodeDescription;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,12 +55,7 @@ final class DefaultNeo4jBinderFunction<T> implements Function<T, Map<String, Obj
|
||||
return;
|
||||
}
|
||||
|
||||
final Object value = propertyAccessor.getProperty(p);
|
||||
// Don't add null values, shave off some bytes for transport
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Object value = converter.writeValue(propertyAccessor.getProperty(p), p.getTypeInformation());
|
||||
properties.put(p.getPropertyName(), value);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.core.mapping;
|
||||
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConverter;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PreferredConstructor;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack The Kleptones - A Night At The Hip-Hopera
|
||||
* @since 1.0
|
||||
*/
|
||||
final class DefaultNeo4jConverter implements Neo4jConverter {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
DefaultNeo4jConverter(ConversionService conversionService) {
|
||||
|
||||
Assert.notNull(conversionService, "ConversionService must not be null!");
|
||||
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Object readValue(@Nullable Value value, TypeInformation<?> type) {
|
||||
|
||||
if (value == null || value == Values.NULL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return conversionService.convert(value, type.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value writeValue(@Nullable Object value, TypeInformation<?> type) {
|
||||
|
||||
if (value == null) {
|
||||
return Values.NULL;
|
||||
}
|
||||
|
||||
return conversionService.convert(value, Value.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> PersistentPropertyAccessor<T> decoratePropertyAccessor(TypeSystem typeSystem,
|
||||
PersistentPropertyAccessor<T> targetPropertyAccessor) {
|
||||
|
||||
return new ConvertingPropertyAccessor<>(targetPropertyAccessor, conversionService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends PersistentProperty<T>> ParameterValueProvider<T> decorateParameterValueProvider(
|
||||
ParameterValueProvider<T> targetParameterValueProvider) {
|
||||
|
||||
return new ParameterValueProvider<T>() {
|
||||
@Override
|
||||
public Object getParameterValue(PreferredConstructor.Parameter parameter) {
|
||||
|
||||
Object originalValue = targetParameterValueProvider.getParameterValue(parameter);
|
||||
Assert.isInstanceOf(Value.class, originalValue, "Decorated parameters other than of type Value are not supported.");
|
||||
return readValue((Value) originalValue, parameter.getType());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -32,13 +32,17 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.MapAccessor;
|
||||
import org.neo4j.driver.types.Node;
|
||||
import org.neo4j.driver.types.Relationship;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConverter;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipDescription;
|
||||
import org.neo4j.springframework.data.core.schema.SchemaUtils;
|
||||
import org.springframework.core.log.LogAccessor;
|
||||
@@ -49,7 +53,6 @@ import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PreferredConstructor;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.model.ParameterValueProvider;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* The central logic of mapping Neo4j's {@link org.neo4j.driver.Record records} to entities based on the Spring
|
||||
@@ -78,44 +81,51 @@ final class DefaultNeo4jMappingFunction<T> implements BiFunction<TypeSystem, Rec
|
||||
|
||||
private final Neo4jMappingContext mappingContext;
|
||||
|
||||
private static final Predicate<Map.Entry<String, Object>> IS_LIST = entry -> entry.getValue() instanceof List;
|
||||
private final Neo4jConverter converter;
|
||||
|
||||
DefaultNeo4jMappingFunction(Neo4jPersistentEntity<T> rootNodeDescription, Neo4jMappingContext neo4jMappingContext) {
|
||||
DefaultNeo4jMappingFunction(Neo4jPersistentEntity<T> rootNodeDescription, Neo4jMappingContext neo4jMappingContext, Neo4jConverter converter) {
|
||||
|
||||
this.rootNodeDescription = rootNodeDescription;
|
||||
this.mappingContext = neo4jMappingContext;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T apply(TypeSystem typeSystem, Record record) {
|
||||
Map<Object, Object> knownObjects = new ConcurrentHashMap<>();
|
||||
|
||||
// That would be the place to call a custom converter for the whole object, if any such thing would be
|
||||
// available (Converter<Record, DomainObject>
|
||||
try {
|
||||
Predicate<Value> isNode = v -> v.hasType(typeSystem.NODE());
|
||||
Predicate<Value> isMap = value -> value.hasType(typeSystem.MAP());
|
||||
|
||||
List<Value> recordValues = record.values();
|
||||
|
||||
String nodeLabel = rootNodeDescription.getPrimaryLabel();
|
||||
List<Node> nodes = recordValues.stream()
|
||||
.filter(isNode)
|
||||
.map(Value::asNode)
|
||||
.filter(node -> node.hasLabel(nodeLabel))
|
||||
.collect(toList());
|
||||
MapAccessor queryRoot = null;
|
||||
for (Value value : recordValues) {
|
||||
if (value.hasType(typeSystem.NODE()) && value.asNode().hasLabel(nodeLabel)) {
|
||||
if (recordValues.size() > 1) {
|
||||
queryRoot = mergeRootNodeWithRecord(value.asNode(), record);
|
||||
} else {
|
||||
queryRoot = value.asNode();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (queryRoot == null) {
|
||||
for (Value value : recordValues) {
|
||||
if (value.hasType(typeSystem.MAP())) {
|
||||
queryRoot = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// either the results will be node based
|
||||
if (!nodes.isEmpty()) {
|
||||
// todo find most fitting label value.asNode().labels().forEach();
|
||||
return nodes.stream().filter(node -> node.hasLabel(nodeLabel)).findFirst()
|
||||
.map(node -> mergeIntoMap(node, record))
|
||||
.map(mergedAttributes -> map(mergedAttributes, rootNodeDescription, knownObjects))
|
||||
.orElseGet(() -> {
|
||||
log.warn(() -> String.format("Could not find mappable nodes or relationships inside %s for %s", record, rootNodeDescription));
|
||||
return null;
|
||||
});
|
||||
} else { // or it is a mostly generated result that is represented as a map
|
||||
return recordValues.stream().filter(isMap)
|
||||
.map(map -> map(map.asMap(), rootNodeDescription, knownObjects)).findFirst().get();
|
||||
}
|
||||
if (queryRoot == null) {
|
||||
log.warn(() -> String.format("Could not find mappable nodes or relationships inside %s for %s", record,
|
||||
rootNodeDescription));
|
||||
return null;
|
||||
} else {
|
||||
return map(typeSystem, queryRoot, rootNodeDescription, knownObjects);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new MappingException("Error mapping " + record.toString(), e);
|
||||
}
|
||||
@@ -123,18 +133,20 @@ final class DefaultNeo4jMappingFunction<T> implements BiFunction<TypeSystem, Rec
|
||||
|
||||
/**
|
||||
* Merges the root node of a query and the remaining record into one map, adding the internal ID of the node, too.
|
||||
* Merge happens only when the record contains additional values.
|
||||
*
|
||||
* @param node Node whose attributes are about to be merged
|
||||
* @param record Optional record that should be merged
|
||||
* @param record Record that should be merged
|
||||
* @return
|
||||
*/
|
||||
private static Map<String, Object> mergeIntoMap(Node node, @Nullable Record record) {
|
||||
Map<String, Object> mergedAttributes = new HashMap<>(node.asMap());
|
||||
private static MapAccessor mergeRootNodeWithRecord(Node node, Record record) {
|
||||
Map<String, Object> mergedAttributes = new HashMap<>(node.size() + record.size() + 1);
|
||||
|
||||
mergedAttributes.put(NAME_OF_INTERNAL_ID, node.id());
|
||||
if (record != null) {
|
||||
mergedAttributes.putAll(record.asMap());
|
||||
}
|
||||
return mergedAttributes;
|
||||
mergedAttributes.putAll(node.asMap(Function.identity()));
|
||||
mergedAttributes.putAll(record.asMap(Function.identity()));
|
||||
|
||||
return Values.value(mergedAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,12 +156,14 @@ final class DefaultNeo4jMappingFunction<T> implements BiFunction<TypeSystem, Rec
|
||||
* @param <ET> As in entity type
|
||||
* @return
|
||||
*/
|
||||
private <ET> ET map(Map<String, Object> queryResult, Neo4jPersistentEntity<ET> nodeDescription,
|
||||
private <ET> ET map(TypeSystem typeSystem, MapAccessor queryResult,
|
||||
Neo4jPersistentEntity<ET> nodeDescription,
|
||||
Map<Object, Object> knownObjects) {
|
||||
|
||||
ET instance = instantiate(nodeDescription, queryResult);
|
||||
|
||||
PersistentPropertyAccessor<ET> propertyAccessor = nodeDescription.getPropertyAccessor(instance);
|
||||
PersistentPropertyAccessor<ET> propertyAccessor = converter
|
||||
.decoratePropertyAccessor(typeSystem, nodeDescription.getPropertyAccessor(instance));
|
||||
if (nodeDescription.requiresPropertyPopulation()) {
|
||||
|
||||
// Fill simple properties
|
||||
@@ -163,26 +177,30 @@ final class DefaultNeo4jMappingFunction<T> implements BiFunction<TypeSystem, Rec
|
||||
Function<String, Neo4jPersistentEntity<?>> relatedNodeDescriptionLookup =
|
||||
relatedLabel -> (Neo4jPersistentEntity<?>) mappingContext.getNodeDescription(relatedLabel);
|
||||
nodeDescription.doWithAssociations(
|
||||
populateFrom(queryResult, propertyAccessor, relationships, relatedNodeDescriptionLookup, knownObjects));
|
||||
populateFrom(typeSystem, queryResult, propertyAccessor, relationships, relatedNodeDescriptionLookup,
|
||||
knownObjects));
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static <ET> ET instantiate(Neo4jPersistentEntity<ET> anotherNodeDescription, Map<String, Object> values) {
|
||||
return INSTANTIATORS.getInstantiatorFor(anotherNodeDescription).createInstance(anotherNodeDescription,
|
||||
new ParameterValueProvider<Neo4jPersistentProperty>() {
|
||||
@Override
|
||||
public Object getParameterValue(PreferredConstructor.Parameter parameter) {
|
||||
private <ET> ET instantiate(Neo4jPersistentEntity<ET> anotherNodeDescription, MapAccessor values) {
|
||||
|
||||
Neo4jPersistentProperty matchingProperty = anotherNodeDescription
|
||||
.getRequiredPersistentProperty(parameter.getName());
|
||||
return extractValueOf(matchingProperty, values);
|
||||
}
|
||||
});
|
||||
ParameterValueProvider<Neo4jPersistentProperty> parameterValueProvider = new ParameterValueProvider<Neo4jPersistentProperty>() {
|
||||
@Override
|
||||
public Value getParameterValue(PreferredConstructor.Parameter parameter) {
|
||||
|
||||
Neo4jPersistentProperty matchingProperty = anotherNodeDescription
|
||||
.getRequiredPersistentProperty(parameter.getName());
|
||||
return extractValueOf(matchingProperty, values);
|
||||
}
|
||||
};
|
||||
parameterValueProvider = converter.decorateParameterValueProvider(parameterValueProvider);
|
||||
return INSTANTIATORS.getInstantiatorFor(anotherNodeDescription)
|
||||
.createInstance(anotherNodeDescription, parameterValueProvider);
|
||||
}
|
||||
|
||||
private static PropertyHandler<Neo4jPersistentProperty> populateFrom(
|
||||
Map<String, Object> queryResult,
|
||||
MapAccessor queryResult,
|
||||
PersistentPropertyAccessor<?> propertyAccessor,
|
||||
Predicate<Neo4jPersistentProperty> isConstructorParameter
|
||||
) {
|
||||
@@ -191,13 +209,14 @@ final class DefaultNeo4jMappingFunction<T> implements BiFunction<TypeSystem, Rec
|
||||
return;
|
||||
}
|
||||
|
||||
Object value = extractValueOf(property, queryResult);
|
||||
Value value = extractValueOf(property, queryResult);
|
||||
propertyAccessor.setProperty(property, value);
|
||||
};
|
||||
}
|
||||
|
||||
private AssociationHandler<Neo4jPersistentProperty> populateFrom(
|
||||
Map<String, Object> queryResult,
|
||||
TypeSystem typeSystem,
|
||||
MapAccessor queryResult,
|
||||
PersistentPropertyAccessor<?> propertyAccessor,
|
||||
Collection<RelationshipDescription> relationships,
|
||||
Function<String, Neo4jPersistentEntity<?>> relatedNodeDescriptionLookup,
|
||||
@@ -216,77 +235,60 @@ final class DefaultNeo4jMappingFunction<T> implements BiFunction<TypeSystem, Rec
|
||||
Neo4jPersistentEntity<?> targetNodeDescription = relatedNodeDescriptionLookup.apply(targetLabel);
|
||||
|
||||
List<Object> value = new ArrayList<>();
|
||||
List list = (List) queryResult.get(SchemaUtils.generateRelatedNodesCollectionName(relationship));
|
||||
Value list = queryResult.get(SchemaUtils.generateRelatedNodesCollectionName(relationship));
|
||||
|
||||
// if the list is null the mapping is based on a custom query
|
||||
if (list == null) {
|
||||
if (list == Values.NULL) {
|
||||
|
||||
Predicate<Map.Entry<String, Object>> containsOnlyRelationships = entry -> ((List) entry.getValue())
|
||||
Predicate<Value> isList = entry -> entry instanceof Value && typeSystem.LIST().isTypeOf(entry);
|
||||
|
||||
Predicate<Value> containsOnlyRelationships = entry -> entry.asList(Function.identity())
|
||||
.stream()
|
||||
.allMatch(listEntry -> {
|
||||
if (!(listEntry instanceof Relationship)) {
|
||||
return false;
|
||||
}
|
||||
.allMatch(listEntry -> typeSystem.RELATIONSHIP().isTypeOf(listEntry));
|
||||
|
||||
return ((Relationship) listEntry).type().equals(relationshipType);
|
||||
});
|
||||
|
||||
Predicate<Map.Entry<String, Object>> containsOnlyNodes = entry -> ((List) entry.getValue()).stream()
|
||||
.allMatch(listEntry -> {
|
||||
|
||||
if (!(listEntry instanceof Node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> labels = new ArrayList<>();
|
||||
(((Node) listEntry).labels()).forEach(labels::add);
|
||||
return labels.contains(targetLabel);
|
||||
});
|
||||
Predicate<Value> containsOnlyNodes = entry -> entry.asList(Function.identity())
|
||||
.stream()
|
||||
.allMatch(listEntry -> typeSystem.NODE().isTypeOf(listEntry));
|
||||
|
||||
// find relationships in the result
|
||||
// this is a List<List<Relationship>>
|
||||
List<Object> allMatchingTypeRelationshipsInResult = queryResult.entrySet().stream()
|
||||
.filter(IS_LIST.and(containsOnlyRelationships))
|
||||
.map(Map.Entry::getValue)
|
||||
List<Relationship> allMatchingTypeRelationshipsInResult = StreamSupport
|
||||
.stream(queryResult.values().spliterator(), false)
|
||||
.filter(isList.and(containsOnlyRelationships))
|
||||
.flatMap(entry -> entry.asList(Value::asRelationship).stream())
|
||||
.filter(r -> r.type().equals(relationshipType))
|
||||
.collect(toList());
|
||||
|
||||
// this is a List<List<Node>>
|
||||
List<Object> allNodesWithMatchingLabelInResult = queryResult.entrySet().stream()
|
||||
.filter(IS_LIST.and(containsOnlyNodes))
|
||||
.map(Map.Entry::getValue)
|
||||
List<Node> allNodesWithMatchingLabelInResult = StreamSupport
|
||||
.stream(queryResult.values().spliterator(), false)
|
||||
.filter(isList.and(containsOnlyNodes))
|
||||
.flatMap(entry -> entry.asList(Value::asNode).stream())
|
||||
.filter(n -> n.hasLabel(targetLabel))
|
||||
.collect(toList());
|
||||
|
||||
if (allNodesWithMatchingLabelInResult.isEmpty() && allMatchingTypeRelationshipsInResult.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Object nodeWithMatchingLabel : allNodesWithMatchingLabelInResult) {
|
||||
for (Node possibleValueNode : (List<Node>) nodeWithMatchingLabel) {
|
||||
for (Node possibleValueNode : allNodesWithMatchingLabelInResult) {
|
||||
long nodeId = possibleValueNode.id();
|
||||
|
||||
for (Object relList : allMatchingTypeRelationshipsInResult) {
|
||||
for (Relationship possibleRelationship : (List<Relationship>) relList) {
|
||||
if (possibleRelationship.endNodeId() == nodeId) {
|
||||
Map<String, Object> newPropertyMap = mergeIntoMap(possibleValueNode, null);
|
||||
value.add(map(newPropertyMap, targetNodeDescription, knownObjects));
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) {
|
||||
if (possibleRelationship.endNodeId() == nodeId) {
|
||||
value.add(map(typeSystem, possibleValueNode, targetNodeDescription, knownObjects));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (Object relatedEntity : list) {
|
||||
Map<String, Object> relatedEntityValues = (Map<String, Object>) relatedEntity;
|
||||
for (Value relatedEntity : list.asList(Function.identity())) {
|
||||
Neo4jPersistentProperty idProperty = targetNodeDescription.getRequiredIdProperty();
|
||||
|
||||
// internal (generated) id or external set
|
||||
Object idValue = idProperty.isInternalIdProperty()
|
||||
? relatedEntityValues.get(NAME_OF_INTERNAL_ID)
|
||||
: relatedEntityValues.get(idProperty.getName());
|
||||
|
||||
? relatedEntity.get(NAME_OF_INTERNAL_ID)
|
||||
: relatedEntity.get(idProperty.getName());
|
||||
Object valueEntry = knownObjects.computeIfAbsent(idValue,
|
||||
(id) -> map(relatedEntityValues, targetNodeDescription, knownObjects));
|
||||
(id) -> map(typeSystem, relatedEntity, targetNodeDescription, knownObjects));
|
||||
|
||||
value.add(valueEntry);
|
||||
}
|
||||
@@ -304,18 +306,14 @@ final class DefaultNeo4jMappingFunction<T> implements BiFunction<TypeSystem, Rec
|
||||
};
|
||||
}
|
||||
|
||||
private static Object extractValueOf(Neo4jPersistentProperty property, Map<String, Object> propertyContainer) {
|
||||
private static Value extractValueOf(Neo4jPersistentProperty property, MapAccessor propertyContainer) {
|
||||
if (property.isInternalIdProperty()) {
|
||||
return propertyContainer.get(NAME_OF_INTERNAL_ID);
|
||||
return propertyContainer instanceof Node ?
|
||||
Values.value(((Node) propertyContainer).id()) :
|
||||
propertyContainer.get(NAME_OF_INTERNAL_ID);
|
||||
} else {
|
||||
String graphPropertyName = property.getPropertyName();
|
||||
return getValueFor(graphPropertyName, propertyContainer);
|
||||
return propertyContainer.get(graphPropertyName);
|
||||
}
|
||||
}
|
||||
|
||||
private static Object getValueFor(String graphProperty, Map<String, Object> entity) {
|
||||
|
||||
// TODO conversion, Type system
|
||||
return entity.get(graphProperty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,11 @@ import java.util.function.Supplier;
|
||||
import org.apiguardian.api.API;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConverter;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jSimpleTypes;
|
||||
import org.neo4j.springframework.data.core.schema.IdDescription;
|
||||
import org.neo4j.springframework.data.core.schema.IdGenerator;
|
||||
import org.neo4j.springframework.data.core.schema.Neo4jSimpleTypes;
|
||||
import org.neo4j.springframework.data.core.schema.NodeDescription;
|
||||
import org.neo4j.springframework.data.core.schema.Relationship;
|
||||
import org.neo4j.springframework.data.core.schema.RelationshipDescription;
|
||||
@@ -54,6 +56,8 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.convert.support.ConfigurableConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.context.AbstractMappingContext;
|
||||
@@ -88,10 +92,25 @@ public final class Neo4jMappingContext
|
||||
*/
|
||||
private final Map<Class<? extends IdGenerator<?>>, IdGenerator<?>> fallbackIdGenerators = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* The converter used in this mapping context.
|
||||
*/
|
||||
private final Neo4jConverter converter;
|
||||
|
||||
private @Nullable ListableBeanFactory beanFactory;
|
||||
|
||||
public Neo4jMappingContext() {
|
||||
super.setSimpleTypeHolder(Neo4jSimpleTypes.SIMPLE_TYPE_HOLDER);
|
||||
|
||||
this(new Neo4jConversions());
|
||||
}
|
||||
|
||||
public Neo4jMappingContext(Neo4jConversions neo4jConversions) {
|
||||
|
||||
super.setSimpleTypeHolder(Neo4jSimpleTypes.HOLDER);
|
||||
|
||||
final ConfigurableConversionService conversionService = new DefaultConversionService();
|
||||
neo4jConversions.registerConvertersIn(conversionService);
|
||||
this.converter = new DefaultNeo4jConverter(conversionService);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -184,7 +203,7 @@ public final class Neo4jMappingContext
|
||||
public <T> BiFunction<TypeSystem, Record, T> getMappingFunctionFor(Class<T> targetClass) {
|
||||
if (this.hasPersistentEntityFor(targetClass)) {
|
||||
Neo4jPersistentEntity neo4jPersistentEntity = this.getPersistentEntity(targetClass);
|
||||
return new DefaultNeo4jMappingFunction<>(neo4jPersistentEntity, this);
|
||||
return new DefaultNeo4jMappingFunction<>(neo4jPersistentEntity, this, this.converter);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -198,7 +217,7 @@ public final class Neo4jMappingContext
|
||||
}
|
||||
|
||||
Neo4jPersistentEntity neo4jPersistentEntity = this.getPersistentEntity(sourceClass);
|
||||
return new DefaultNeo4jBinderFunction<T>(neo4jPersistentEntity);
|
||||
return new DefaultNeo4jBinderFunction<T>(neo4jPersistentEntity, converter);
|
||||
}
|
||||
|
||||
private Collection<RelationshipDescription> computeRelationshipsOf(String primaryLabel) {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Not part of public API, subject to change without notice.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
abstract class AbstractPoint implements Neo4jPoint {
|
||||
|
||||
protected final Coordinate coordinate;
|
||||
|
||||
private final Integer srid;
|
||||
|
||||
AbstractPoint(Coordinate coordinate, Integer srid) {
|
||||
this.coordinate = coordinate;
|
||||
this.srid = srid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Integer getSrid() {
|
||||
return srid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof AbstractPoint)) {
|
||||
return false;
|
||||
}
|
||||
AbstractPoint that = (AbstractPoint) o;
|
||||
return Objects.equals(coordinate, that.coordinate) &&
|
||||
Objects.equals(srid, that.srid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(coordinate, srid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class CartesianPoint2d extends AbstractPoint {
|
||||
|
||||
static final int SRID = 7203;
|
||||
|
||||
CartesianPoint2d(Coordinate coordinate) {
|
||||
super(coordinate, SRID);
|
||||
}
|
||||
|
||||
public CartesianPoint2d(double x, double y) {
|
||||
super(new Coordinate(x, y), SRID);
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return coordinate.getX();
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return coordinate.getY();
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "CartesianPoint2d{" +
|
||||
"x=" + getX() +
|
||||
", y=" + getY() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class CartesianPoint3d extends AbstractPoint {
|
||||
|
||||
static final int SRID = 9157;
|
||||
|
||||
CartesianPoint3d(Coordinate coordinate) {
|
||||
super(coordinate, SRID);
|
||||
}
|
||||
|
||||
public CartesianPoint3d(double x, double y, double z) {
|
||||
super(new Coordinate(x, y, z), SRID);
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return coordinate.getX();
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return coordinate.getY();
|
||||
}
|
||||
|
||||
public Double getZ() {
|
||||
return coordinate.getZ();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CartesianPoint3d{" +
|
||||
"x=" + getX() +
|
||||
", y=" + getY() +
|
||||
", z=" + getZ() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public final class Coordinate {
|
||||
private final double x;
|
||||
|
||||
private final double y;
|
||||
|
||||
private final Double z;
|
||||
|
||||
public Coordinate(double x, double y) {
|
||||
this(x, y, null);
|
||||
}
|
||||
|
||||
public Coordinate(double x, double y, Double z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
Double getZ() {
|
||||
return z;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Coordinate)) {
|
||||
return false;
|
||||
}
|
||||
Coordinate that = (Coordinate) o;
|
||||
return Double.compare(that.x, x) == 0 &&
|
||||
Double.compare(that.y, y) == 0 &&
|
||||
Objects.equals(z, that.z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y, z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class GeographicPoint2d extends AbstractPoint {
|
||||
|
||||
GeographicPoint2d(Coordinate coordinate, Integer srid) {
|
||||
super(coordinate, srid);
|
||||
}
|
||||
|
||||
public GeographicPoint2d(double latitude, double longitude) {
|
||||
super(new Coordinate(longitude, latitude), 4326);
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return coordinate.getX();
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return coordinate.getY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GeographicPoint2d{" +
|
||||
"longitude=" + getLongitude() +
|
||||
", latitude=" + getLatitude() +
|
||||
", srid=" + getSrid() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class GeographicPoint3d extends AbstractPoint {
|
||||
|
||||
GeographicPoint3d(Coordinate coordinate, Integer srid) {
|
||||
super(coordinate, srid);
|
||||
}
|
||||
|
||||
public GeographicPoint3d(double latitude, double longitude, double height) {
|
||||
super(new Coordinate(longitude, latitude, height), 4979);
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return coordinate.getX();
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return coordinate.getY();
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return coordinate.getZ();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GeographicPoint3d{" +
|
||||
"longitude=" + getLongitude() +
|
||||
", latitude=" + getLatitude() +
|
||||
", height=" + getHeight() +
|
||||
", srid=" + getSrid() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* A dedicated Neo4j point, that is aware of it's nature, either being geographic or cartesian. While you can use this
|
||||
* interface as an attribute type in your domain class, you should not mix different type of points on the same attribute
|
||||
* of the same label. Queries will lead to inconsistent results. Use one of the concrete implementations.
|
||||
* See <a href="https://neo4j.com/docs/cypher-manual/current/syntax/spatial/#cypher-spatial">Spatial values</a>.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public interface Neo4jPoint {
|
||||
|
||||
/**
|
||||
* @return The Srid identifying the Coordinate Reference Systems (CRS) used by this point.
|
||||
*/
|
||||
Integer getSrid();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import org.apiguardian.api.API;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @since 1.0
|
||||
*/
|
||||
@API(status = API.Status.STABLE, since = "1.0")
|
||||
public final class PointBuilder {
|
||||
|
||||
private final int srid;
|
||||
|
||||
public static PointBuilder withSrid(int srid) {
|
||||
return new PointBuilder(srid);
|
||||
}
|
||||
|
||||
private PointBuilder(int srid) {
|
||||
this.srid = srid;
|
||||
}
|
||||
|
||||
public AbstractPoint build(Coordinate coordinate) {
|
||||
|
||||
boolean is3d = coordinate.getZ() != null;
|
||||
|
||||
if (srid == CartesianPoint2d.SRID || srid == CartesianPoint3d.SRID) {
|
||||
return is3d ? new CartesianPoint3d(coordinate) : new CartesianPoint2d(coordinate);
|
||||
} else {
|
||||
return is3d ? new GeographicPoint3d(coordinate, srid) : new GeographicPoint2d(coordinate, srid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Additional types provided by SDN/RX.
|
||||
*/
|
||||
@NonNullApi
|
||||
package org.neo4j.springframework.data.types;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
@@ -32,6 +32,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.driver.types.TypeSystem;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.springframework.core.convert.ConversionFailedException;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.converter.ConverterRegistry;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
@@ -43,9 +48,14 @@ class SingleValueMappingFunctionTest {
|
||||
|
||||
private final Record record;
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
SingleValueMappingFunctionTest(@Mock TypeSystem typeSystem, @Mock Record record) {
|
||||
this.typeSystem = typeSystem;
|
||||
this.record = record;
|
||||
this.conversionService = new DefaultConversionService();
|
||||
new Neo4jConversions().registerConvertersIn((ConverterRegistry) this.conversionService);
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -56,7 +66,7 @@ class SingleValueMappingFunctionTest {
|
||||
|
||||
when(record.size()).thenReturn(0);
|
||||
|
||||
SingleValueMappingFunction<String> mappingFunction = new SingleValueMappingFunction<>(String.class);
|
||||
SingleValueMappingFunction<String> mappingFunction = new SingleValueMappingFunction<>(conversionService, String.class);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(typeSystem, record))
|
||||
.withMessage("Record has no elements, cannot map nothing.");
|
||||
}
|
||||
@@ -66,7 +76,7 @@ class SingleValueMappingFunctionTest {
|
||||
|
||||
when(record.size()).thenReturn(23);
|
||||
|
||||
SingleValueMappingFunction<String> mappingFunction = new SingleValueMappingFunction<>(String.class);
|
||||
SingleValueMappingFunction<String> mappingFunction = new SingleValueMappingFunction<>(conversionService, String.class);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(typeSystem, record))
|
||||
.withMessage("Records with more than one value cannot be converted without a mapper.");
|
||||
}
|
||||
@@ -78,7 +88,7 @@ class SingleValueMappingFunctionTest {
|
||||
when(record.size()).thenReturn(1);
|
||||
when(record.get(0)).thenReturn(Values.NULL);
|
||||
|
||||
SingleValueMappingFunction<String> mappingFunction = new SingleValueMappingFunction<>(String.class);
|
||||
SingleValueMappingFunction<String> mappingFunction = new SingleValueMappingFunction<>(conversionService, String.class);
|
||||
assertThat(mappingFunction.apply(typeSystem, record)).isNull();
|
||||
}
|
||||
|
||||
@@ -88,9 +98,9 @@ class SingleValueMappingFunctionTest {
|
||||
when(record.size()).thenReturn(1);
|
||||
when(record.get(0)).thenReturn(Values.value("Guten Tag."));
|
||||
|
||||
SingleValueMappingFunction<Period> mappingFunction = new SingleValueMappingFunction<>(Period.class);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> mappingFunction.apply(typeSystem, record))
|
||||
.withMessage("java.time.Period is not assignable from java.lang.String");
|
||||
SingleValueMappingFunction<Period> mappingFunction = new SingleValueMappingFunction<>(conversionService, Period.class);
|
||||
assertThatExceptionOfType(ConversionFailedException.class).isThrownBy(() -> mappingFunction.apply(typeSystem, record))
|
||||
.withMessageStartingWith("Failed to convert from type [org.neo4j.driver.internal.value.StringValue] to type [java.time.Period] for value '\"Guten Tag.\"'");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -101,7 +111,8 @@ class SingleValueMappingFunctionTest {
|
||||
when(record.size()).thenReturn(1);
|
||||
when(record.get(0)).thenReturn(Values.value(aDate));
|
||||
|
||||
SingleValueMappingFunction<LocalDate> mappingFunction = new SingleValueMappingFunction<>(LocalDate.class);
|
||||
SingleValueMappingFunction<LocalDate> mappingFunction = new SingleValueMappingFunction<>(conversionService,
|
||||
LocalDate.class);
|
||||
assertThat(mappingFunction.apply(typeSystem, record)).isEqualTo(aDate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint2d;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint3d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint2d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint3d;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
class SpatialTypesTest {
|
||||
|
||||
@Test
|
||||
void neo4jPointAsValueShouldWork() {
|
||||
|
||||
Point point;
|
||||
|
||||
point = SpatialTypes.value(new GeographicPoint2d(10, 20)).asPoint();
|
||||
assertThat(point.srid()).isEqualTo(4326);
|
||||
assertThat(point.x()).isEqualTo(20.0);
|
||||
assertThat(point.y()).isEqualTo(10.0);
|
||||
|
||||
point = SpatialTypes.value(new CartesianPoint2d(10, 20)).asPoint();
|
||||
assertThat(point.srid()).isEqualTo(7203);
|
||||
assertThat(point.x()).isEqualTo(10.0);
|
||||
assertThat(point.y()).isEqualTo(20.0);
|
||||
|
||||
point = SpatialTypes.value(new GeographicPoint3d(10.0, 20.0, 30)).asPoint();
|
||||
assertThat(point.srid()).isEqualTo(4979);
|
||||
assertThat(point.x()).isEqualTo(20.0);
|
||||
assertThat(point.y()).isEqualTo(10.0);
|
||||
assertThat(point.z()).isEqualTo(30.0);
|
||||
|
||||
point = SpatialTypes.value(new CartesianPoint3d(10.0, 20.0, 30)).asPoint();
|
||||
assertThat(point.srid()).isEqualTo(9157);
|
||||
assertThat(point.x()).isEqualTo(10.0);
|
||||
assertThat(point.y()).isEqualTo(20.0);
|
||||
assertThat(point.z()).isEqualTo(30.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.core.convert;
|
||||
|
||||
import static java.time.temporal.ChronoUnit.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Period;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.neo4j.driver.Values;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
class TemporalAmountAdapterTest {
|
||||
@Test
|
||||
public void internallyCreatedTypesShouldBeConvertedCorrect() {
|
||||
final TemporalAmountAdapter adapter = new TemporalAmountAdapter();
|
||||
|
||||
assertThat(adapter.apply(Values.isoDuration(1, 0, 0, 0).asIsoDuration())).isEqualTo(Period.ofMonths(1));
|
||||
assertThat(adapter.apply(Values.isoDuration(1, 1, 0, 0).asIsoDuration()))
|
||||
.isEqualTo(Period.ofMonths(1).plusDays(1));
|
||||
assertThat(adapter.apply(Values.isoDuration(1, 1, 1, 0).asIsoDuration()))
|
||||
.isEqualTo(Values.isoDuration(1, 1, 1, 0).asIsoDuration());
|
||||
assertThat(adapter.apply(Values.isoDuration(0, 0, 120, 1).asIsoDuration()))
|
||||
.isEqualTo(Duration.ofMinutes(2).plusNanos(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void durationsShouldStayDurations() {
|
||||
final TemporalAmountAdapter adapter = new TemporalAmountAdapter();
|
||||
|
||||
Duration duration =
|
||||
MONTHS.getDuration().multipliedBy(13).plus(DAYS.getDuration().multipliedBy(32)).plusHours(25)
|
||||
.plusMinutes(120);
|
||||
|
||||
assertThat(adapter.apply(Values.value(duration).asIsoDuration())).isEqualTo(duration);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void periodsShouldStayPeriods() {
|
||||
final TemporalAmountAdapter adapter = new TemporalAmountAdapter();
|
||||
|
||||
Period period = Period.between(LocalDate.of(2018, 11, 15), LocalDate.of(2020, 12, 24));
|
||||
|
||||
assertThat(adapter.apply(Values.value(period).asIsoDuration())).isEqualTo(period.normalized());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.integration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.DynamicTest.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.DynamicContainer;
|
||||
import org.junit.jupiter.api.DynamicNode;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.integration.shared.Neo4jConversionsITBase;
|
||||
import org.neo4j.springframework.data.test.Neo4jExtension;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.convert.ConverterBuilder;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@ExtendWith(Neo4jExtension.class)
|
||||
class Neo4jConversionsIT extends Neo4jConversionsITBase {
|
||||
|
||||
private static final TypeDescriptor TYPE_DESCRIPTOR_OF_VALUE = TypeDescriptor.valueOf(Value.class);
|
||||
private static final DefaultConversionService DEFAULT_CONVERSION_SERVICE = new DefaultConversionService();
|
||||
|
||||
@BeforeAll
|
||||
static void prepareDefaultConversionService() {
|
||||
new Neo4jConversions().registerConvertersIn(DEFAULT_CONVERSION_SERVICE);
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
@DisplayName("Objects")
|
||||
Stream<DynamicNode> objects() {
|
||||
Map<String, Map<String, Object>> supportedTypes = new HashMap<>();
|
||||
supportedTypes.put("CypherTypes", CYPHER_TYPES);
|
||||
supportedTypes.put("AdditionalTypes", ADDITIONAL_TYPES);
|
||||
supportedTypes.put("SpatialTypes", SPATIAL_TYPES);
|
||||
|
||||
return supportedTypes.entrySet().stream()
|
||||
.map(types -> {
|
||||
|
||||
DynamicContainer reads = DynamicContainer.dynamicContainer("read", types.getValue().entrySet().stream()
|
||||
.map(a -> dynamicTest(a.getKey(),
|
||||
() -> Neo4jConversionsIT.assertRead(types.getKey(), a.getKey(), a.getValue()))));
|
||||
|
||||
DynamicContainer writes = DynamicContainer.dynamicContainer("write", types.getValue().entrySet().stream()
|
||||
.map(a -> dynamicTest(a.getKey(),
|
||||
() -> Neo4jConversionsIT.assertWrite(types.getKey(), a.getKey(), a.getValue()))));
|
||||
|
||||
return DynamicContainer.dynamicContainer(types.getKey(), Arrays.asList(reads, writes));
|
||||
});
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
@DisplayName("Custom conversions")
|
||||
Stream<DynamicTest> customConversions() {
|
||||
final DefaultConversionService customConversionService = new DefaultConversionService();
|
||||
|
||||
ConverterBuilder.ConverterAware converterAware = ConverterBuilder
|
||||
.reading(Value.class, LocalDate.class, v -> {
|
||||
String s = v.asString();
|
||||
switch (s) {
|
||||
case "gestern":
|
||||
return LocalDate.now().minusDays(1);
|
||||
case "heute":
|
||||
return LocalDate.now();
|
||||
case "morgen":
|
||||
return LocalDate.now().plusDays(1);
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}).andWriting(d -> {
|
||||
if (d.isBefore(LocalDate.now())) {
|
||||
return Values.value("gestern");
|
||||
} else if (d.isAfter(LocalDate.now())) {
|
||||
return Values.value("morgen");
|
||||
} else {
|
||||
return Values.value("heute");
|
||||
}
|
||||
});
|
||||
new Neo4jConversions(converterAware.getConverters()).registerConvertersIn(customConversionService);
|
||||
|
||||
return Stream.of(
|
||||
dynamicTest("read",
|
||||
() -> assertThat(customConversionService.convert(Values.value("gestern"), LocalDate.class))
|
||||
.isEqualTo(LocalDate.now().minusDays(1))),
|
||||
dynamicTest("write",
|
||||
() -> assertThat(customConversionService.convert(LocalDate.now().plusDays(1), TYPE_DESCRIPTOR_OF_VALUE))
|
||||
.isEqualTo(Values.value("morgen")))
|
||||
);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Primitives {
|
||||
|
||||
@Test
|
||||
void cypherTypes() {
|
||||
boolean b = DEFAULT_CONVERSION_SERVICE.convert(Values.value(true), boolean.class);
|
||||
assertThat(b).isEqualTo(true);
|
||||
|
||||
long l = DEFAULT_CONVERSION_SERVICE.convert(Values.value(Long.MAX_VALUE), long.class);
|
||||
assertThat(l).isEqualTo(Long.MAX_VALUE);
|
||||
|
||||
double d = DEFAULT_CONVERSION_SERVICE.convert(Values.value(1.7976931348), double.class);
|
||||
assertThat(d).isEqualTo(1.7976931348);
|
||||
}
|
||||
|
||||
@Test
|
||||
void additionalTypes() {
|
||||
|
||||
byte b = DEFAULT_CONVERSION_SERVICE.convert(Values.value(new byte[] { 6 }), byte.class);
|
||||
assertThat(b).isEqualTo((byte) 6);
|
||||
|
||||
char c = DEFAULT_CONVERSION_SERVICE.convert(Values.value("x"), char.class);
|
||||
assertThat(c).isEqualTo('x');
|
||||
|
||||
float f = DEFAULT_CONVERSION_SERVICE.convert(Values.value("23.42"), float.class);
|
||||
assertThat(f).isEqualTo(23.42F);
|
||||
|
||||
int i = DEFAULT_CONVERSION_SERVICE.convert(Values.value(42), int.class);
|
||||
assertThat(i).isEqualTo(42);
|
||||
|
||||
short s = DEFAULT_CONVERSION_SERVICE.convert(Values.value((short) 127), short.class);
|
||||
assertThat(s).isEqualTo((short) 127);
|
||||
}
|
||||
}
|
||||
|
||||
static void assertRead(String label, String attribute, Object t) {
|
||||
try (Session session = neo4jConnectionSupport.getDriver().session()) {
|
||||
Value v = session.run("MATCH (n) WHERE labels(n) = [$label] RETURN n[$attribute] as r",
|
||||
Values.parameters("label", label, "attribute", attribute)).single().get("r");
|
||||
|
||||
Object converted = DEFAULT_CONVERSION_SERVICE.convert(v, t.getClass());
|
||||
assertThat(converted).isEqualTo(t);
|
||||
}
|
||||
}
|
||||
|
||||
static void assertWrite(String label, String attribute, Object t) {
|
||||
try (Session session = neo4jConnectionSupport.getDriver().session()) {
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("label", label);
|
||||
parameters.put("attribute", attribute);
|
||||
parameters.put("v", DEFAULT_CONVERSION_SERVICE.convert(t, TYPE_DESCRIPTOR_OF_VALUE));
|
||||
|
||||
long cnt = session
|
||||
.run("MATCH (n) WHERE labels(n) = [$label] AND n[$attribute] = $v RETURN COUNT(n) AS cnt",
|
||||
parameters)
|
||||
.single().get("cnt").asLong();
|
||||
assertThat(cnt).isEqualTo(1L);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -392,7 +392,7 @@ class RepositoryIT {
|
||||
void saveSingleEntity() {
|
||||
|
||||
PersonWithAllConstructor person = new PersonWithAllConstructor(null, "Mercury", "Freddie", "Queen", true, 1509L,
|
||||
LocalDate.of(1946, 9, 15), null, emptyList(), null);
|
||||
LocalDate.of(1946, 9, 15), null, Arrays.asList("b", "a"), null);
|
||||
PersonWithAllConstructor savedPerson = repository.save(person);
|
||||
try (Session session = driver.session()) {
|
||||
Record record = session.run("MATCH (n:PersonWithAllConstructor) WHERE n.first_name = $first_name RETURN n",
|
||||
@@ -401,6 +401,7 @@ class RepositoryIT {
|
||||
assertThat(record.containsKey("n")).isTrue();
|
||||
Node node = record.get("n").asNode();
|
||||
assertThat(savedPerson.getId()).isEqualTo(node.id());
|
||||
assertThat(node.get("things").asList()).containsExactly("b", "a");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,6 +648,8 @@ class RepositoryIT {
|
||||
PersonWithAllConstructor originalPerson = repository.findById(id1).get();
|
||||
originalPerson.setFirstName("Updated first name");
|
||||
originalPerson.setNullable("Updated nullable field");
|
||||
assertThat(originalPerson.getThings()).isNotEmpty();
|
||||
originalPerson.setThings(emptyList());
|
||||
|
||||
PersonWithAllConstructor savedPerson = repository.save(originalPerson);
|
||||
try (Session session = driver.session()) {
|
||||
@@ -660,6 +663,7 @@ class RepositoryIT {
|
||||
assertThat(node.id()).isEqualTo(savedPerson.getId());
|
||||
assertThat(node.get("first_name").asString()).isEqualTo(savedPerson.getFirstName());
|
||||
assertThat(node.get("nullable").asString()).isEqualTo(savedPerson.getNullable());
|
||||
assertThat(node.get("things").asList()).isEmpty();
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.integration.imperative;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.DynamicTest.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.DynamicContainer;
|
||||
import org.junit.jupiter.api.DynamicNode;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.springframework.data.config.AbstractNeo4jConfig;
|
||||
import org.neo4j.springframework.data.core.convert.Neo4jConversions;
|
||||
import org.neo4j.springframework.data.integration.shared.Neo4jConversionsITBase;
|
||||
import org.neo4j.springframework.data.integration.shared.ThingWithAllAdditionalTypes;
|
||||
import org.neo4j.springframework.data.integration.shared.ThingWithAllCypherTypes;
|
||||
import org.neo4j.springframework.data.integration.shared.ThingWithAllSpatialTypes;
|
||||
import org.neo4j.springframework.data.repository.Neo4jRepository;
|
||||
import org.neo4j.springframework.data.repository.config.EnableNeo4jRepositories;
|
||||
import org.neo4j.springframework.data.test.Neo4jIntegrationTest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
* @soundtrack Tool - Fear Inoculum
|
||||
*/
|
||||
@Neo4jIntegrationTest
|
||||
class TypeConversionIT extends Neo4jConversionsITBase {
|
||||
|
||||
private final Driver driver;
|
||||
|
||||
private final CypherTypesRepository cypherTypesRepository;
|
||||
|
||||
private final AdditionalTypesRepository additionalTypesRepository;
|
||||
|
||||
private final SpatialTypesRepository spatialTypesRepository;
|
||||
|
||||
private final DefaultConversionService defaultConversionService;
|
||||
|
||||
@Autowired TypeConversionIT(
|
||||
Driver driver,
|
||||
CypherTypesRepository cypherTypesRepository,
|
||||
AdditionalTypesRepository additionalTypesRepository,
|
||||
SpatialTypesRepository spatialTypesRepository,
|
||||
Neo4jConversions neo4jConversions
|
||||
) {
|
||||
this.driver = driver;
|
||||
this.cypherTypesRepository = cypherTypesRepository;
|
||||
this.additionalTypesRepository = additionalTypesRepository;
|
||||
this.spatialTypesRepository = spatialTypesRepository;
|
||||
this.defaultConversionService = new DefaultConversionService();
|
||||
neo4jConversions.registerConvertersIn(defaultConversionService);
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicNode> conversionsShouldBeAppliedToEntities() {
|
||||
|
||||
Map<String, Map<String, Object>> supportedTypes = new HashMap<>();
|
||||
supportedTypes.put("CypherTypes", CYPHER_TYPES);
|
||||
supportedTypes.put("AdditionalTypes", ADDITIONAL_TYPES);
|
||||
supportedTypes.put("SpatialTypes", SPATIAL_TYPES);
|
||||
|
||||
return supportedTypes.entrySet().stream()
|
||||
.map(entry -> {
|
||||
|
||||
Object thing;
|
||||
Object copyOfThing;
|
||||
switch (entry.getKey()) {
|
||||
case "CypherTypes":
|
||||
ThingWithAllCypherTypes hlp = cypherTypesRepository.findById(ID_OF_CYPHER_TYPES_NODE).get();
|
||||
copyOfThing = cypherTypesRepository.save(hlp.withId(null));
|
||||
thing = hlp;
|
||||
break;
|
||||
case "AdditionalTypes":
|
||||
ThingWithAllAdditionalTypes hlp2 = additionalTypesRepository
|
||||
.findById(ID_OF_ADDITIONAL_TYPES_NODE).get();
|
||||
copyOfThing = additionalTypesRepository.save(hlp2.withId(null));
|
||||
thing = hlp2;
|
||||
break;
|
||||
case "SpatialTypes":
|
||||
ThingWithAllSpatialTypes hlp3 = spatialTypesRepository.findById(ID_OF_SPATIAL_TYPES_NODE)
|
||||
.get();
|
||||
copyOfThing = spatialTypesRepository.save(hlp3.withId(null));
|
||||
thing = hlp3;
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unsupported types: " + entry.getKey());
|
||||
}
|
||||
|
||||
DynamicContainer reads = DynamicContainer.dynamicContainer("read", entry.getValue().entrySet().stream()
|
||||
.map(a -> dynamicTest(a.getKey(),
|
||||
() -> assertThat(ReflectionTestUtils.getField(thing, a.getKey())).isEqualTo(a.getValue()))));
|
||||
|
||||
DynamicContainer writes = DynamicContainer
|
||||
.dynamicContainer("write", entry.getValue().entrySet().stream()
|
||||
.map(a -> dynamicTest(a.getKey(),
|
||||
() -> assertWrite(copyOfThing, a.getKey(), defaultConversionService))));
|
||||
|
||||
return DynamicContainer.dynamicContainer(entry.getKey(), Arrays.asList(reads, writes));
|
||||
});
|
||||
}
|
||||
|
||||
void assertWrite(Object thing, String fieldName, ConversionService conversionService) {
|
||||
|
||||
long id = (long) ReflectionTestUtils.getField(thing, "id");
|
||||
Object domainValue = ReflectionTestUtils.getField(thing, fieldName);
|
||||
Value driverValue = conversionService.convert(domainValue, Value.class);
|
||||
|
||||
try (Session session = neo4jConnectionSupport.getDriver().session()) {
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("id", id);
|
||||
parameters.put("attribute", fieldName);
|
||||
parameters.put("v", driverValue);
|
||||
|
||||
long cnt = session
|
||||
.run("MATCH (n) WHERE id(n) = $id AND n[$attribute] = $v RETURN COUNT(n) AS cnt",
|
||||
parameters)
|
||||
.single().get("cnt").asLong();
|
||||
assertThat(cnt).isEqualTo(1L);
|
||||
}
|
||||
}
|
||||
|
||||
public interface CypherTypesRepository
|
||||
extends Neo4jRepository<ThingWithAllCypherTypes, Long> {
|
||||
}
|
||||
|
||||
public interface AdditionalTypesRepository
|
||||
extends Neo4jRepository<ThingWithAllAdditionalTypes, Long> {
|
||||
}
|
||||
|
||||
public interface SpatialTypesRepository
|
||||
extends Neo4jRepository<ThingWithAllSpatialTypes, Long> {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableNeo4jRepositories(considerNestedRepositories = true)
|
||||
@EnableTransactionManagement
|
||||
static class Config extends AbstractNeo4jConfig {
|
||||
|
||||
@Bean
|
||||
public Driver driver() {
|
||||
return neo4jConnectionSupport.getDriver();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<String> getMappingBasePackages() {
|
||||
return Collections.singletonList(ThingWithAllCypherTypes.class.getPackage().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.integration.shared;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetTime;
|
||||
import java.time.Period;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.neo4j.driver.Values;
|
||||
import org.neo4j.springframework.data.test.Neo4jExtension;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint2d;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint3d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint2d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint3d;
|
||||
import org.springframework.data.geo.Point;
|
||||
|
||||
/**
|
||||
* Provides some nodes spotting properties of all types we support.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
public abstract class Neo4jConversionsITBase {
|
||||
|
||||
protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport;
|
||||
|
||||
protected static final Map<String, Object> CYPHER_TYPES;
|
||||
static {
|
||||
Map<String, Object> hlp = new HashMap<>();
|
||||
hlp.put("aBoolean", true);
|
||||
hlp.put("aLong", Long.MAX_VALUE);
|
||||
hlp.put("aDouble", 1.7976931348);
|
||||
hlp.put("aString", "Hallo, Cypher");
|
||||
hlp.put("aByteArray", "A thing".getBytes());
|
||||
hlp.put("aPoint", Values.point(7203, 47, 11).asPoint());
|
||||
hlp.put("aLocalDate", LocalDate.of(2015, 7, 21));
|
||||
hlp.put("anOffsetTime", OffsetTime.of(12, 31, 0, 0, ZoneOffset.ofHours(1)));
|
||||
hlp.put("aLocalTime", LocalTime.of(12, 31, 14));
|
||||
hlp.put("aZoneDateTime", ZonedDateTime.of(2015, 7, 21, 21, 40, 32, 0, TimeZone.getTimeZone("America/New_York").toZoneId()));
|
||||
hlp.put("aLocalDateTime", LocalDateTime.of(2015, 7, 21, 21, 0));
|
||||
hlp.put("anIsoDuration", Values.isoDuration(0, 14, 58320, 0).asObject());
|
||||
CYPHER_TYPES = Collections.unmodifiableMap(hlp);
|
||||
}
|
||||
|
||||
protected static final Map<String, Object> ADDITIONAL_TYPES;
|
||||
static {
|
||||
Map<String, Object> hlp = new HashMap<>();
|
||||
hlp.put("booleanArray", new boolean[] { true, true, false });
|
||||
hlp.put("aByte", (byte) 6);
|
||||
hlp.put("aChar", 'x');
|
||||
hlp.put("charArray", new char[] { 'x', 'y', 'z' });
|
||||
hlp.put("aDate", Date.from(LocalDateTime.of(2019, 9, 21, 0, 0, 0).toInstant(ZoneOffset.UTC)));
|
||||
hlp.put("aBigDecimal", BigDecimal.valueOf(Double.MAX_VALUE).multiply(BigDecimal.TEN));
|
||||
hlp.put("aBigInteger", BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.TEN));
|
||||
hlp.put("doubleArray", new double[] { 1.1, 2.2, 3.3 });
|
||||
hlp.put("aFloat", 23.42F);
|
||||
hlp.put("floatArray", new float[] { 4.4F, 5.5F });
|
||||
hlp.put("anInt", 42);
|
||||
hlp.put("intArray", new int[] { 21, 9 });
|
||||
hlp.put("aLocale", Locale.GERMANY);
|
||||
hlp.put("longArray", new long[] { Long.MIN_VALUE, Long.MAX_VALUE });
|
||||
hlp.put("aShort", (short) 127);
|
||||
hlp.put("shortArray", new short[] { -10, 10 });
|
||||
hlp.put("aPeriod", Period.of(23, 4, 7));
|
||||
hlp.put("aDuration", Duration.ofHours(25).plusMinutes(63).plusSeconds(65));
|
||||
hlp.put("stringArray", new String[] {"Hallo", "Welt"});
|
||||
hlp.put("listOfStrings", new ArrayList<>(Arrays.asList("Hello", "World"))); // Done on purpose, otherwise the target collection cannot be determined.
|
||||
hlp.put("setOfStrings", new HashSet<>(Arrays.asList("Hallo", "wereld")));
|
||||
ADDITIONAL_TYPES = Collections.unmodifiableMap(hlp);
|
||||
}
|
||||
|
||||
@Builder
|
||||
protected static class ParamHolder {
|
||||
String name;
|
||||
double latitude;
|
||||
double longitude;
|
||||
|
||||
Map<String, Object> toParameterMap() {
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("name", this.name);
|
||||
parameters.put("latitude", this.latitude);
|
||||
parameters.put("longitude", this.longitude);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
Point asSpringPoint() {
|
||||
return new Point(latitude, longitude);
|
||||
}
|
||||
|
||||
GeographicPoint2d asGeo2d() {
|
||||
return new GeographicPoint2d(latitude, longitude);
|
||||
}
|
||||
|
||||
GeographicPoint3d asGeo3d(double height) {
|
||||
return new GeographicPoint3d(latitude, longitude, height);
|
||||
}
|
||||
}
|
||||
|
||||
private static final ParamHolder NEO_HQ = ParamHolder.builder().latitude(55.612191).longitude(12.994823)
|
||||
.name("Neo4j HQ").build();
|
||||
private static final ParamHolder CLARION = ParamHolder.builder().latitude(55.607726).longitude(12.994243)
|
||||
.name("Clarion").build();
|
||||
private static final ParamHolder MINC = ParamHolder.builder().latitude(55.611496).longitude(12.994039).name("Minc")
|
||||
.build();
|
||||
|
||||
protected static final Map<String, Object> SPATIAL_TYPES;
|
||||
static {
|
||||
Map<String, Object> hlp = new HashMap<>();
|
||||
hlp.put("sdnPoint", NEO_HQ.asSpringPoint());
|
||||
hlp.put("geo2d", MINC.asGeo2d());
|
||||
hlp.put("geo3d", CLARION.asGeo3d(27.0));
|
||||
hlp.put("car2d", new CartesianPoint2d(10, 20));
|
||||
hlp.put("car3d", new CartesianPoint3d(30, 40, 50));
|
||||
SPATIAL_TYPES = Collections.unmodifiableMap(hlp);
|
||||
}
|
||||
|
||||
protected static long ID_OF_CYPHER_TYPES_NODE;
|
||||
protected static long ID_OF_ADDITIONAL_TYPES_NODE;
|
||||
protected static long ID_OF_SPATIAL_TYPES_NODE;
|
||||
|
||||
@BeforeAll
|
||||
static void prepareData() {
|
||||
|
||||
try (Session session = neo4jConnectionSupport.getDriver().session()) {
|
||||
session.writeTransaction(w -> {
|
||||
Map<String, Object> parameters;
|
||||
|
||||
w.run("MATCH (n) detach delete n");
|
||||
|
||||
parameters = new HashMap<>();
|
||||
parameters.put("aByteArray", "A thing".getBytes());
|
||||
ID_OF_CYPHER_TYPES_NODE = w.run("CREATE (n:CypherTypes) SET "
|
||||
+ " n.aBoolean = true,"
|
||||
+ " n.aLong = 9223372036854775807,"
|
||||
+ " n.aDouble = 1.7976931348,"
|
||||
+ " n.aString = 'Hallo, Cypher',"
|
||||
+ " n.aByteArray = $aByteArray,"
|
||||
+ " n.aLocalDate = date('2015-07-21'),"
|
||||
+ " n.anOffsetTime = time({ hour:12, minute:31, timezone: '+01:00' }),"
|
||||
+ " n.aLocalTime = localtime({ hour:12, minute:31, second:14 }),"
|
||||
+ " n.aZoneDateTime = datetime('2015-07-21T21:40:32-04[America/New_York]'),"
|
||||
+ " n.aLocalDateTime = localdatetime('2015202T21'),"
|
||||
+ " n.anIsoDuration = duration('P14DT16H12M'),"
|
||||
+ " n.aPoint = point({x:47, y:11})"
|
||||
+ " RETURN id(n) AS id", parameters).single().get("id").asLong();
|
||||
|
||||
parameters = new HashMap<>();
|
||||
parameters.put("aByte", Values.value(new byte[] { 6 }));
|
||||
ID_OF_ADDITIONAL_TYPES_NODE = w.run("CREATE (n:AdditionalTypes) SET "
|
||||
+ " n.booleanArray = [true, true, false],"
|
||||
+ " n.aByte = $aByte,"
|
||||
+ " n.aChar = 'x',"
|
||||
+ " n.charArray = ['x', 'y', 'z'],"
|
||||
+ " n.aDate = '2019-09-21T02:00:00.000+02:00',"
|
||||
+ " n.doubleArray = [1.1, 2.2, 3.3],"
|
||||
+ " n.aFloat = '23.42',"
|
||||
+ " n.floatArray = ['4.4', '5.5'],"
|
||||
+ " n.anInt = 42,"
|
||||
+ " n.intArray = [21, 9],"
|
||||
+ " n.aLocale = 'de_DE',"
|
||||
+ " n.longArray = [-9223372036854775808, 9223372036854775807],"
|
||||
+ " n.aShort = 127,"
|
||||
+ " n.shortArray = [-10, 10],"
|
||||
+ " n.aBigDecimal = '1.79769313486231570E+309',"
|
||||
+ " n.aBigInteger = '92233720368547758070',"
|
||||
+ " n.aPeriod = duration('P23Y4M7D'),"
|
||||
+ " n.aDuration = duration('PT26H4M5S'),"
|
||||
+ " n.stringArray = ['Hallo', 'Welt'],"
|
||||
+ " n.listOfStrings = ['Hello', 'World'],"
|
||||
+ " n.setOfStrings = ['Hallo', 'wereld']"
|
||||
+ " RETURN id(n) AS id", parameters).single().get("id").asLong();
|
||||
|
||||
parameters = new HashMap<>();
|
||||
parameters.put("neo4j", NEO_HQ.toParameterMap());
|
||||
parameters.put("minc", MINC.toParameterMap());
|
||||
parameters.put("clarion", CLARION.toParameterMap());
|
||||
parameters.put("aByte", Values.value(new byte[] { 6 }));
|
||||
ID_OF_SPATIAL_TYPES_NODE = w.run("CREATE (n:SpatialTypes) SET "
|
||||
+ " n.sdnPoint = point({latitude: $neo4j.latitude, longitude: $neo4j.longitude}),"
|
||||
+ " n.geo2d = point({latitude: $minc.latitude, longitude: $minc.longitude}),"
|
||||
+ " n.geo3d = point({latitude: $clarion.latitude, longitude: $clarion.longitude, height: 27}),"
|
||||
+ " n.car2d = point({x: 10, y: 20}),"
|
||||
+ " n.car3d = point({x: 30, y: 40, z: 50})"
|
||||
+ " RETURN id(n) AS id", parameters).single().get("id").asLong();
|
||||
w.success();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public class PersonWithAllConstructor {
|
||||
|
||||
private String nullable;
|
||||
|
||||
private final List<String> things;
|
||||
private List<String> things;
|
||||
|
||||
private final Point place;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.integration.shared;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.time.Period;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.neo4j.springframework.data.core.schema.GeneratedValue;
|
||||
import org.neo4j.springframework.data.core.schema.Id;
|
||||
import org.neo4j.springframework.data.core.schema.Node;
|
||||
|
||||
/**
|
||||
* Contains properties of all additional types.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Node("AdditionalTypes")
|
||||
@Data
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Builder
|
||||
public class ThingWithAllAdditionalTypes {
|
||||
|
||||
@Id @GeneratedValue
|
||||
@Wither
|
||||
public final Long id;
|
||||
|
||||
private boolean[] booleanArray;
|
||||
|
||||
private byte aByte;
|
||||
|
||||
private char aChar;
|
||||
|
||||
private char[] charArray;
|
||||
|
||||
private Date aDate;
|
||||
|
||||
private BigDecimal aBigDecimal;
|
||||
|
||||
private BigInteger aBigInteger;
|
||||
|
||||
private double[] doubleArray;
|
||||
|
||||
private float aFloat;
|
||||
|
||||
private float[] floatArray;
|
||||
|
||||
private int anInt;
|
||||
|
||||
private int[] intArray;
|
||||
|
||||
private Locale aLocale;
|
||||
|
||||
private long[] longArray;
|
||||
|
||||
private short aShort;
|
||||
|
||||
private short[] shortArray;
|
||||
|
||||
private Period aPeriod;
|
||||
|
||||
private Duration aDuration;
|
||||
|
||||
private String[] stringArray;
|
||||
|
||||
private List<String> listOfStrings;
|
||||
|
||||
private Set<String> setOfStrings;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.integration.shared;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetTime;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import org.neo4j.driver.types.IsoDuration;
|
||||
import org.neo4j.driver.types.Point;
|
||||
import org.neo4j.springframework.data.core.schema.GeneratedValue;
|
||||
import org.neo4j.springframework.data.core.schema.Id;
|
||||
import org.neo4j.springframework.data.core.schema.Node;
|
||||
|
||||
/**
|
||||
* Contains properties of all cypher types.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Node("CypherTypes")
|
||||
@Data
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Builder
|
||||
public class ThingWithAllCypherTypes {
|
||||
|
||||
@Id @GeneratedValue
|
||||
@Wither
|
||||
public final Long id;
|
||||
|
||||
private boolean aBoolean;
|
||||
|
||||
private long aLong;
|
||||
|
||||
private double aDouble;
|
||||
|
||||
private String aString;
|
||||
|
||||
private byte[] aByteArray;
|
||||
|
||||
private LocalDate aLocalDate;
|
||||
|
||||
private OffsetTime anOffsetTime;
|
||||
|
||||
private LocalTime aLocalTime;
|
||||
|
||||
private ZonedDateTime aZoneDateTime;
|
||||
|
||||
private LocalDateTime aLocalDateTime;
|
||||
|
||||
private IsoDuration anIsoDuration;
|
||||
|
||||
private Point aPoint;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.integration.shared;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Wither;
|
||||
|
||||
import org.neo4j.springframework.data.core.schema.GeneratedValue;
|
||||
import org.neo4j.springframework.data.core.schema.Id;
|
||||
import org.neo4j.springframework.data.core.schema.Node;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint2d;
|
||||
import org.neo4j.springframework.data.types.CartesianPoint3d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint2d;
|
||||
import org.neo4j.springframework.data.types.GeographicPoint3d;
|
||||
import org.springframework.data.geo.Point;
|
||||
|
||||
/**
|
||||
* Contains properties of all spatial types.
|
||||
*
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
@Node("SpatialTypes")
|
||||
@Data
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Builder
|
||||
public class ThingWithAllSpatialTypes {
|
||||
|
||||
@Id @GeneratedValue
|
||||
@Wither
|
||||
public final Long id;
|
||||
|
||||
private Point sdnPoint;
|
||||
|
||||
private GeographicPoint2d geo2d;
|
||||
|
||||
private GeographicPoint3d geo3d;
|
||||
|
||||
private CartesianPoint2d car2d;
|
||||
|
||||
private CartesianPoint3d car3d;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
class GeographicPoint2dTest {
|
||||
@Test
|
||||
public void constructorShouldSetCorrectFields() {
|
||||
|
||||
double latitude = 48.793889;
|
||||
double longitude = 9.226944;
|
||||
GeographicPoint2d geographicPoint2d = new GeographicPoint2d(latitude, longitude);
|
||||
|
||||
assertThat(geographicPoint2d.getLatitude()).isEqualTo(latitude);
|
||||
assertThat(geographicPoint2d.getLongitude()).isEqualTo(longitude);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2019 "Neo4j,"
|
||||
* Neo4j Sweden AB [https://neo4j.com]
|
||||
*
|
||||
* This file is part of Neo4j.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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 org.neo4j.springframework.data.types;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Michael J. Simons
|
||||
*/
|
||||
class GeographicPoint3dTest {
|
||||
@Test
|
||||
public void constructorShouldSetCorrectFields() {
|
||||
|
||||
double latitude = 48.793889;
|
||||
double longitude = 9.226944;
|
||||
double elevation = 300.0;
|
||||
GeographicPoint3d geographicPoint = new GeographicPoint3d(latitude, longitude, elevation);
|
||||
|
||||
assertThat(geographicPoint.getLatitude()).isEqualTo(latitude);
|
||||
assertThat(geographicPoint.getLongitude()).isEqualTo(longitude);
|
||||
assertThat(geographicPoint.getHeight()).isEqualTo(elevation);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user