From ce0a0abce1e2eb719a0073bb066bd1e20337faa6 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 8 Nov 2024 14:58:10 +0100 Subject: [PATCH] feat: Introduce `ConverterInfo` so that individual converters can get more info about the fields to be converted. Closes #1190. --- .../org/neo4j/ogm/metadata/FieldInfo.java | 23 ++++--- .../neo4j/ogm/metadata/ObjectAnnotations.java | 10 ++- .../ogm/typeconversion/ConverterInfo.java | 32 ++++++++++ .../org/neo4j/ogm/domain/gh1190/MyEntity.java | 54 ++++++++++++++++ .../ogm/domain/gh1190/Uuid2LongConverter.java | 61 +++++++++++++++++++ .../TypeConversionIntegrationTest.java | 37 +++++++++-- 6 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 core/src/main/java/org/neo4j/ogm/typeconversion/ConverterInfo.java create mode 100644 neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh1190/MyEntity.java create mode 100644 neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh1190/Uuid2LongConverter.java diff --git a/core/src/main/java/org/neo4j/ogm/metadata/FieldInfo.java b/core/src/main/java/org/neo4j/ogm/metadata/FieldInfo.java index aeb282b6d..5c4c6f6cb 100644 --- a/core/src/main/java/org/neo4j/ogm/metadata/FieldInfo.java +++ b/core/src/main/java/org/neo4j/ogm/metadata/FieldInfo.java @@ -45,6 +45,7 @@ import org.neo4j.ogm.session.Utils; import org.neo4j.ogm.typeconversion.AttributeConverter; import org.neo4j.ogm.typeconversion.CompositeAttributeConverter; +import org.neo4j.ogm.typeconversion.ConverterInfo; import org.neo4j.ogm.typeconversion.MapCompositeConverter; import org.neo4j.ogm.utils.RelationshipUtils; import org.neo4j.ogm.utils.StringUtils; @@ -114,7 +115,7 @@ public class FieldInfo { this.annotations = annotations; this.isSupportedNativeType = isSupportedNativeType.test(DescriptorMappings.getType(getTypeDescriptor())); if (!this.annotations.isEmpty()) { - Object converter = getAnnotations().getConverter(this.fieldType); + Object converter = getAnnotations().getConverter(new ConverterInfo(field, this.fieldType, this.property0())); if (converter instanceof AttributeConverter) { setPropertyConverter((AttributeConverter) converter); } else if (converter instanceof CompositeAttributeConverter) { @@ -163,17 +164,23 @@ public String getName() { // should these two methods be on PropertyReader, RelationshipReader respectively? public String property() { if (persistableAsProperty()) { - if (annotations != null) { - AnnotationInfo propertyAnnotation = annotations.get(Property.class); - if (propertyAnnotation != null) { - return propertyAnnotation.get(Property.NAME, getName()); - } - } - return getName(); + return property0(); } return null; } + // extracted here, so that it can be used in the constructor before the (composite) converters are + // assigned and #persistableAsProperty becomes possibly true + private String property0() { + if (annotations != null) { + AnnotationInfo propertyAnnotation = annotations.get(Property.class); + if (propertyAnnotation != null) { + return propertyAnnotation.get(Property.NAME, getName()); + } + } + return getName(); + } + public String relationship() { Optional localRelationshipType = relationshipType; if (!localRelationshipType.isPresent()) { diff --git a/core/src/main/java/org/neo4j/ogm/metadata/ObjectAnnotations.java b/core/src/main/java/org/neo4j/ogm/metadata/ObjectAnnotations.java index 5467af01e..f4ba56c3f 100644 --- a/core/src/main/java/org/neo4j/ogm/metadata/ObjectAnnotations.java +++ b/core/src/main/java/org/neo4j/ogm/metadata/ObjectAnnotations.java @@ -34,6 +34,7 @@ import org.neo4j.ogm.annotation.typeconversion.NumberString; import org.neo4j.ogm.config.Configuration; import org.neo4j.ogm.exception.core.MappingException; +import org.neo4j.ogm.typeconversion.ConverterInfo; import org.neo4j.ogm.typeconversion.DateLongConverter; import org.neo4j.ogm.typeconversion.DateStringConverter; import org.neo4j.ogm.typeconversion.EnumStringConverter; @@ -74,7 +75,9 @@ public boolean isEmpty() { return annotations.isEmpty(); } - Object getConverter(Class fieldType) { + Object getConverter(ConverterInfo converterInfo) { + + Class fieldType = converterInfo.fieldType(); // try to get a custom type converter AnnotationInfo customType = get(Convert.class); @@ -86,6 +89,11 @@ Object getConverter(Class fieldType) { try { Class clazz = Class.forName(classDescriptor, false, Configuration.getDefaultClassLoader()); + try { + return clazz.getDeclaredConstructor(ConverterInfo.class).newInstance(converterInfo); + } catch (NoSuchMethodException e) { + // Well, ok… + } return clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(e); diff --git a/core/src/main/java/org/neo4j/ogm/typeconversion/ConverterInfo.java b/core/src/main/java/org/neo4j/ogm/typeconversion/ConverterInfo.java new file mode 100644 index 000000000..3fdd6a47f --- /dev/null +++ b/core/src/main/java/org/neo4j/ogm/typeconversion/ConverterInfo.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2002-2024 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.ogm.typeconversion; + +import java.lang.reflect.Field; + +/** + * Converters registered via {@link org.neo4j.ogm.annotation.typeconversion.Convert @Convert} can require this information + * via a non-default, public constructor taking in one single {@link ConverterInfo argument}. + * + * @param field The field on which the converter was registered + * @param fieldType The field type that OGM does assume, might be different of what the raw field would give you + * @param defaultPropertyName The property that would be assumed for this field by OGM + */ +public record ConverterInfo(Field field, Class fieldType, String defaultPropertyName) { +} diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh1190/MyEntity.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh1190/MyEntity.java new file mode 100644 index 000000000..b2808f07a --- /dev/null +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh1190/MyEntity.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2002-2024 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.ogm.domain.gh1190; + +import java.util.UUID; + +import org.neo4j.ogm.annotation.GeneratedValue; +import org.neo4j.ogm.annotation.Id; +import org.neo4j.ogm.annotation.NodeEntity; +import org.neo4j.ogm.annotation.typeconversion.Convert; +import org.neo4j.ogm.id.UuidStrategy; + +@NodeEntity +public class MyEntity { + @Id + @GeneratedValue(strategy = UuidStrategy.class) + @Convert(Uuid2LongConverter.class) + private UUID uuid; + + @Convert(Uuid2LongConverter.class) + private UUID otherUuid; + + public UUID getOtherUuid() { + return otherUuid; + } + + public void setOtherUuid(UUID otherUuid) { + this.otherUuid = otherUuid; + } + + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } +} diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh1190/Uuid2LongConverter.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh1190/Uuid2LongConverter.java new file mode 100644 index 000000000..8f5dc379a --- /dev/null +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/domain/gh1190/Uuid2LongConverter.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2002-2024 "Neo4j," + * Neo4j Sweden AB [http://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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.neo4j.ogm.domain.gh1190; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.neo4j.ogm.typeconversion.CompositeAttributeConverter; +import org.neo4j.ogm.typeconversion.ConverterInfo; + +public class Uuid2LongConverter implements CompositeAttributeConverter { + + private final ConverterInfo converterInfo; + private final String fieldName; + + public Uuid2LongConverter(ConverterInfo converterInfo) { + this.converterInfo = converterInfo; + this.fieldName = converterInfo.field().getName(); + } + + @Override + public Map toGraphProperties(UUID value) { + + var properties = new HashMap(); + if (value != null) { + properties.put(fieldName + "_most", value.getMostSignificantBits()); + properties.put(fieldName + "_least", value.getLeastSignificantBits()); + } + + return properties; + } + + @Override + public UUID toEntityAttribute(Map value) { + var most = (Long) value.get(fieldName + "_most"); + var least = (Long) value.get(fieldName + "_least"); + + if (most != null && least != null) { + return new UUID(most, least); + } + + return null; + } +} diff --git a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/typeconversion/TypeConversionIntegrationTest.java b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/typeconversion/TypeConversionIntegrationTest.java index b8e8c9ae9..dd5079643 100644 --- a/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/typeconversion/TypeConversionIntegrationTest.java +++ b/neo4j-ogm-tests/neo4j-ogm-integration-tests/src/test/java/org/neo4j/ogm/typeconversion/TypeConversionIntegrationTest.java @@ -31,6 +31,7 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -42,6 +43,7 @@ import org.neo4j.driver.Values; import org.neo4j.ogm.domain.convertible.numbers.Account; import org.neo4j.ogm.domain.convertible.numbers.Foobar; +import org.neo4j.ogm.domain.gh1190.MyEntity; import org.neo4j.ogm.domain.music.Album; import org.neo4j.ogm.domain.music.Artist; import org.neo4j.ogm.domain.music.TimeHolder; @@ -62,7 +64,7 @@ public class TypeConversionIntegrationTest extends TestContainersTestBase { @BeforeAll public static void oneTimeSetUp() { sessionFactory = new SessionFactory(getDriver(), "org.neo4j.ogm.domain.music", - "org.neo4j.ogm.domain.convertible.numbers"); + "org.neo4j.ogm.domain.convertible.numbers", "org.neo4j.ogm.domain.gh1190"); } @BeforeEach @@ -187,7 +189,8 @@ void savedTimestampAsParameterToSimpleCreateIsReadBackAsIs() { verify(timeHolder.getGraphId(), someTime, someLocalDateTime, someLocalDate); } - private void verify(Long graphId, OffsetDateTime expectedOffsetDateTime, LocalDateTime expectedLocalDateTime, LocalDate expectedLocalDate) { + private void verify(Long graphId, OffsetDateTime expectedOffsetDateTime, LocalDateTime expectedLocalDateTime, + LocalDate expectedLocalDate) { // opening a new Session to prevent shared data TimeHolder reloaded = sessionFactory.openSession().load(TimeHolder.class, graphId); @@ -200,7 +203,7 @@ private void verify(Long graphId, OffsetDateTime expectedOffsetDateTime, LocalDa String localDateTimeValue = null; String localDateValue = null; - try(Driver driver = getNewBoltConnection()) { + try (Driver driver = getNewBoltConnection()) { try (org.neo4j.driver.Session driverSession = driver.session()) { Record record = driverSession .run("MATCH (n) WHERE id(n) = $id RETURN n", Values.parameters("id", graphId)).single(); @@ -243,8 +246,8 @@ void converterShouldBeAppliedBothWaysCorrectly() { localSession = sessionFactory.openSession(); Iterable> result = localSession.query("" - + "MATCH (a:Account) WHERE id(a) = $id " - + "RETURN a.valueA AS va, a.valueB as vb, a.listOfFoobars as listOfFoobars, a.anotherListOfFoobars as anotherListOfFoobars, a.foobar as foobar, a.notConverter as notConverter", + + "MATCH (a:Account) WHERE id(a) = $id " + + "RETURN a.valueA AS va, a.valueB as vb, a.listOfFoobars as listOfFoobars, a.anotherListOfFoobars as anotherListOfFoobars, a.foobar as foobar, a.notConverter as notConverter", Collections.singletonMap("id", account.getId()) ); assertThat(result).hasSize(1); @@ -269,4 +272,28 @@ void converterShouldBeAppliedBothWaysCorrectly() { .containsExactlyInAnyOrder("C", "D"); assertThat(account.getFoobar().getValue()).isEqualTo("Foobar"); } + + @Test // GH-1190 + void convertersShouldGetAllTheInfo() { + + var other = UUID.randomUUID(); + + var myEntity = new MyEntity(); + myEntity.setOtherUuid(other); + + session.save(myEntity); + session.clear(); + + var loaded = session.load(MyEntity.class, myEntity.getUuid()); + assertThat(loaded).isNotNull(); + assertThat(loaded.getOtherUuid()).isEqualTo(other); + + try (Driver driver = getNewBoltConnection()) { + try (org.neo4j.driver.Session driverSession = driver.session()) { + Map record = driverSession + .run("MATCH (n:MyEntity) RETURN n").single().get("n").asMap(); + assertThat(record).containsOnlyKeys("uuid_most", "uuid_least", "otherUuid_most", "otherUuid_least"); + } + } + } }