Skip to content

Commit

Permalink
Next stage.
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-simons committed Sep 28, 2020
1 parent 324792f commit 3a8ed37
Show file tree
Hide file tree
Showing 16 changed files with 583 additions and 148 deletions.
@@ -0,0 +1,79 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* 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.springframework.data.neo4j.core.convert;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.function.Function;

import org.apiguardian.api.API;
import org.neo4j.driver.Value;

/**
* This annotation can be used to define either custom conversions for single attributes by specifying both a writing
* and a reading converter class and if needed, a custom factory for those or it can be used to build custom meta-annotated
* annotations like {@code @org.springframework.data.neo4j.core.support.DateLong}.
*
* <p>Custom conversions are applied to both attributes of entities and parameters of repository methods that map to those
* attributes (which does apply to all derived queries and queries by example but not to string based queries).
*
* <p>Converter functions that have a default constructor don't need a dedicated factory. A dedicated factory will
* be provided with either this annotation and its values or with the meta annotated annotation, including all configuration
* available.
*
* @author Michael J. Simons
* @soundtrack Antilopen Gang - Abwasser
* @since 6.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD })
@Inherited
@Documented
@API(status = API.Status.STABLE, since = "6.0")
public @interface ConvertWith {

Class<? extends Function<?, Value>> writingConverter() default WritingPlaceholder.class;

Class<? extends Function<Value, ?>> readingConverter() default ReadingPlaceholder.class;

Class<? extends CustomConversionFactory> converterFactory() default DefaultCustomConversionFactory.class;

/**
* Indicates an unset writing converter.
*/
final class WritingPlaceholder implements Function<Object, Value> {

@Override
public Value apply(Object o) {
throw new UnsupportedOperationException();
}
}

/**
* Indicates an unset reading converter.
*/
final class ReadingPlaceholder implements Function<Value, Object> {

@Override
public Object apply(Value value) {
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* 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.springframework.data.neo4j.core.convert;

import java.util.Objects;
import java.util.function.Function;

import org.apiguardian.api.API;
import org.neo4j.driver.Value;
import org.springframework.util.Assert;

/**
* This class presents the pair of writing and reading converters for a custom conversion. Bot directions are required.
*
* @author Michael J. Simons
* @soundtrack Antilopen Gang - Adrenochrom
* @since 6.0
*/
@API(status = API.Status.STABLE, since = "6.0")
public final class CustomConversion {

private final Function<?, Value> writingConverter;

private final Function<Value, ?> readingConverter;

public CustomConversion(Function<?, Value> writingConverter, Function<Value, ?> readingConverter) {

Assert.notNull(writingConverter, "A writing converter is required.");
this.writingConverter = writingConverter;
Assert.notNull(readingConverter, "A reading converter is required.");
this.readingConverter = readingConverter;
}

public Function<?, Value> getWritingConverter() {
return writingConverter;
}

public Function<Value, ?> getReadingConverter() {
return readingConverter;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CustomConversion that = (CustomConversion) o;
return writingConverter.equals(that.writingConverter) &&
readingConverter.equals(that.readingConverter);
}

@Override
public int hashCode() {
return Objects.hash(writingConverter, readingConverter);
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* 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.springframework.data.neo4j.core.convert;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;

import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.util.ReflectionUtils;

/**
* This interface needs to be implemented to provide custom configuration for a {@link CustomConversion}. Use cases may
* be specific date formats or the like. The build method will receive the whole annotation, including all attributes.
* Classes implementing this interface <strong>must</strong> have a default constructor.
*
* @param <A> The type of the annotation
* @author Michael J. Simons
* @soundtrack Antilopen Gang - Abwasser
* @since 6.0
*/
public interface CustomConversionFactory<A extends Annotation> {

/**
* @param config The configuration for building the custom conversion
* @return The actual conversion
*/
CustomConversion buildConversion(A config, Class<?> typeOfAnnotatedElement);
}
@@ -0,0 +1,45 @@
/*
* Copyright 2011-2020 the original author or authors.
*
* 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.springframework.data.neo4j.core.convert;

import java.util.function.Function;

import org.neo4j.driver.Value;
import org.springframework.beans.BeanUtils;

/**
* @author Michael J. Simons
* @soundtrack Metallica - S&M2
* @since 6.0
*/
final class DefaultCustomConversionFactory implements CustomConversionFactory<ConvertWith> {

@Override
public CustomConversion buildConversion(ConvertWith config, Class<?> typeOfAnnotatedElement) {

if (config.writingConverter() == ConvertWith.WritingPlaceholder.class) {
throw new IllegalArgumentException("The default custom conversion factory cannot be used with a placeholder");
}

if (config.readingConverter() == ConvertWith.ReadingPlaceholder.class) {
throw new IllegalArgumentException("The default custom conversion factory cannot be used with a placeholder");
}

Function<?, Value> writingConverter = BeanUtils.instantiateClass(config.writingConverter());
Function<Value, ?> readingConverter = BeanUtils.instantiateClass(config.readingConverter());
return new CustomConversion(writingConverter, readingConverter);
}
}
Expand Up @@ -15,12 +15,13 @@
*/
package org.springframework.data.neo4j.core.mapping;

import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.function.Function;

import org.neo4j.driver.Value;
import org.springframework.beans.BeanUtils;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
Expand All @@ -30,11 +31,14 @@
import org.springframework.data.neo4j.core.schema.Relationship;
import org.springframework.data.neo4j.core.schema.RelationshipProperties;
import org.springframework.data.neo4j.core.schema.TargetNode;
import org.springframework.data.neo4j.core.support.ConvertAs;
import org.springframework.data.neo4j.core.convert.ConvertWith;
import org.springframework.data.neo4j.core.convert.CustomConversionFactory;
import org.springframework.data.neo4j.core.convert.CustomConversion;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

/**
* @author Michael J. Simons
Expand All @@ -49,42 +53,39 @@ final class DefaultNeo4jPersistentProperty extends AnnotationBasedPersistentProp

private final Neo4jMappingContext mappingContext;

private final Lazy<Function<Object, Value>> writingConverter;
private final Lazy<Function<Value, Object>> readingConverter;
private final Lazy<CustomConversion> customConversion;

/**
* Creates a new {@link AnnotationBasedPersistentProperty}.
*
* @param property must not be {@literal null}.
* @param owner must not be {@literal null}.
* @param property must not be {@literal null}.
* @param owner must not be {@literal null}.
* @param simpleTypeHolder type holder
*/
DefaultNeo4jPersistentProperty(Property property, PersistentEntity<?, Neo4jPersistentProperty> owner,
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder, boolean isEntityInRelationshipWithProperties) {
Neo4jMappingContext mappingContext, SimpleTypeHolder simpleTypeHolder,
boolean isEntityInRelationshipWithProperties) {

super(property, owner, simpleTypeHolder);
this.isEntityInRelationshipWithProperties = isEntityInRelationshipWithProperties;
this.mappingContext = mappingContext;

this.graphPropertyName = Lazy.of(this::computeGraphPropertyName);
this.isAssociation = Lazy.of(() -> {

Class<?> targetType = getActualType();
return !(simpleTypeHolder.isSimpleType(targetType) || mappingContext.hasCustomWriteTarget(targetType) || isAnnotationPresent(TargetNode.class));
return !(simpleTypeHolder.isSimpleType(targetType) || this.mappingContext.hasCustomWriteTarget(targetType)
|| isAnnotationPresent(TargetNode.class));
});
// TODO Configuration of converters needs to be accounted for.
this.writingConverter = Lazy.of(() -> {

ConvertAs annotation = findAnnotation(ConvertAs.class);
return annotation == null ? null :
(Function<Object, Value>) BeanUtils.instantiateClass(annotation.writingConverter());
});
this.readingConverter = Lazy.of(() -> {
this.customConversion = Lazy.of(() -> {

if(this.isEntity()) {
return null;
}

ConvertAs annotation = findAnnotation(ConvertAs.class);
return annotation == null ? null :
(Function<Value, Object>) BeanUtils.instantiateClass(annotation.readingConverter());
return this.mappingContext.getOptionalCustomConversionsFor(getRequiredField(), getActualType());
});
this.mappingContext = mappingContext;
}

@Override
Expand Down Expand Up @@ -166,11 +167,12 @@ public boolean isEntity() {

@Override
public Function<Object, Value> getOptionalWritingConverter() {
return isEntity() ? null : writingConverter.getNullable();
return (Function<Object, Value>) customConversion.getOptional().map(CustomConversion::getWritingConverter).orElse(null);
}

@Override
public Function<Value, Object> getOptionalReadingConverter() {
return isEntity() ? null : readingConverter.getNullable();
return (Function<Value, Object>) customConversion.getOptional().map(CustomConversion::getReadingConverter).orElse(null);
}

@Override
Expand Down

0 comments on commit 3a8ed37

Please sign in to comment.