From 33bf99dcc4f9e38683d25a4e318e7011ebb629d0 Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Thu, 14 Mar 2019 21:53:31 +0100 Subject: [PATCH] Add a minimal, Spring independent schema. --- etc/jqassistant/structure.adoc | 4 +- .../core/mapping/Neo4jMappingContext.java | 76 +++++++++++++++- .../core/mapping/Neo4jPersistentEntity.java | 7 ++ .../mapping/Neo4jPersistentEntityImpl.java | 48 ++++++++++ .../core/mapping/Neo4jPersistentProperty.java | 2 + .../mapping/Neo4jPersistentPropertyImpl.java | 59 ++++++++++++ .../schema/IllegalSchemaChangeException.java | 34 +++++++ .../neo4j/core/schema/Neo4jSimpleTypes.java | 34 +++++++ .../neo4j/core/schema/NodeDescription.java | 91 +++++++++++++++++++ .../data/neo4j/core/schema/Property.java | 9 +- .../core/schema/PropertyDescription.java | 44 +++++++++ .../data/neo4j/core/schema/Relationship.java | 4 +- .../core/schema/RelationshipDescription.java | 43 +++++++++ .../data/neo4j/core/schema/Schema.java | 84 +++++++++++++++++ ...Neo4jRepositoryConfigurationExtension.java | 16 ++++ .../core/mapping/Neo4jMappingContextTest.java | 64 ++++++++++++- .../Neo4jPersistentEntityImplTest.java | 44 +++++++++ .../data/neo4j/core/schema/SchemaTest.java | 40 ++++++++ 18 files changed, 694 insertions(+), 9 deletions(-) create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java create mode 100644 spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java create mode 100644 spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java diff --git a/etc/jqassistant/structure.adoc b/etc/jqassistant/structure.adoc index 18dd3dc66..afb12264c 100644 --- a/etc/jqassistant/structure.adoc +++ b/etc/jqassistant/structure.adoc @@ -5,11 +5,11 @@ Most of the time, the package structure under `org.springframework.data.neo4j` s [[structure:mapping]] [source,cypher,role=constraint,requiresConcepts="dependency:Package"] -.The mapping package must not depend on other SDN-RX packages. +.The mapping package must not depend on any other SDN-RX packages than `schema` ---- MATCH (a:Main:Artifact) MATCH (a) -[:CONTAINS]-> (p1:Package) -[:DEPENDS_ON]-> (p2:Package) <-[:CONTAINS]- (a) WHERE p1.fqn = 'org.springframework.data.neo4j.core.mapping' - AND NOT (p1) -[:CONTAINS]-> (p2) + AND NOT ((p1) -[:CONTAINS]-> (p2) OR p2.fqn = 'org.springframework.data.neo4j.core.schema') return p1,p2 ---- diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 2282782d0..967b0a1ba 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -18,9 +18,21 @@ */ package org.springframework.data.neo4j.core.mapping; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.SimpleAssociationHandler; +import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.AbstractMappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.PropertyDescription; +import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.data.neo4j.core.schema.RelationshipDescription; +import org.springframework.data.neo4j.core.schema.Schema; import org.springframework.data.util.TypeInformation; /** @@ -30,6 +42,8 @@ import org.springframework.data.util.TypeInformation; */ public class Neo4jMappingContext extends AbstractMappingContext, Neo4jPersistentProperty> { + private final Schema schema = new Schema(); + /* * (non-Javadoc) * @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation) @@ -37,7 +51,7 @@ public class Neo4jMappingContext extends AbstractMappingContext Neo4jPersistentEntity createPersistentEntity(TypeInformation typeInformation) { - throw new UnsupportedOperationException("Not yet implemented."); + return new Neo4jPersistentEntityImpl<>(typeInformation); } /* @@ -48,6 +62,64 @@ public class Neo4jMappingContext extends AbstractMappingContext neo4jPersistentProperties, SimpleTypeHolder simpleTypeHolder) { - throw new UnsupportedOperationException("Not yet implemented."); + return new Neo4jPersistentPropertyImpl(property, neo4jPersistentProperties, simpleTypeHolder); } + + @Override + public void initialize() { + super.initialize(); + + super.getPersistentEntities().forEach(m -> + schema.registerNodeDescription(describeAsNode(m)) + ); + } + + public Schema getSchema() { + return schema; + } + + private NodeDescription describeAsNode(Neo4jPersistentEntity entity) { + + List properties = new ArrayList<>(); + + // TODO break this up into separate methods. + entity.doWithProperties(new SimplePropertyHandler() { + @Override + public void doWithPersistentProperty(PersistentProperty persistentProperty) { + org.springframework.data.neo4j.core.schema.Property propertyAnnotation = + persistentProperty.findAnnotation(org.springframework.data.neo4j.core.schema.Property.class); + + String propertyName = persistentProperty.getName(); + if (propertyAnnotation != null && !propertyAnnotation.name().isEmpty() + && propertyAnnotation.name().trim().length() != 0) { + propertyName = propertyAnnotation.name().trim(); + } + + properties.add(new PropertyDescription(persistentProperty.getName(), propertyName)); + } + }); + + List relationships = new ArrayList<>(); + entity.doWithAssociations(new SimpleAssociationHandler() { + @Override + public void doWithAssociation(Association> association) { + + Neo4jPersistentEntity obverseOwner = Neo4jMappingContext.this + .getPersistentEntity(association.getInverse().getAssociationTargetType()); + + Relationship outgoingRelationship = association.getInverse().findAnnotation(Relationship.class); + String type; + if (outgoingRelationship != null && outgoingRelationship.type() != null) { + type = outgoingRelationship.type(); + } else { + type = association.getInverse().getName(); + } + relationships.add(new RelationshipDescription(type, obverseOwner.getPrimaryLabel())); + } + }); + + return new NodeDescription(entity.getPrimaryLabel(), properties, relationships); + } + + } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java index dadc200b2..ea6d461e0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.mapping; +import org.apiguardian.api.API; import org.springframework.data.mapping.model.MutablePersistentEntity; /** @@ -25,5 +26,11 @@ import org.springframework.data.mapping.model.MutablePersistentEntity; * * @author Michael J. Simons */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface Neo4jPersistentEntity extends MutablePersistentEntity { + + /** + * @return The primary label of this entity inside Neo4j. + */ + String getPrimaryLabel(); } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java new file mode 100644 index 000000000..ac522e3c9 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImpl.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.mapping; + +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.util.TypeInformation; + +/** + * @author Michael J. Simons + */ +class Neo4jPersistentEntityImpl extends BasicPersistentEntity + implements Neo4jPersistentEntity { + + private final String primaryLabel; + + Neo4jPersistentEntityImpl(TypeInformation information) { + super(information); + + Node nodeAnnotation = this.findAnnotation(Node.class); + if (nodeAnnotation == null || nodeAnnotation.labels().length != 1) { + primaryLabel = this.getType().getSimpleName(); + } else { + primaryLabel = nodeAnnotation.labels()[0]; + } + } + + @Override + public String getPrimaryLabel() { + return primaryLabel; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java index 356c7d496..dbc1ef891 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java @@ -18,6 +18,7 @@ */ package org.springframework.data.neo4j.core.mapping; +import org.apiguardian.api.API; import org.springframework.data.mapping.PersistentProperty; /** @@ -25,5 +26,6 @@ import org.springframework.data.mapping.PersistentProperty; * * @author Michael J. Simons */ +@API(status = API.Status.INTERNAL, since = "1.0") public interface Neo4jPersistentProperty extends PersistentProperty { } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java new file mode 100644 index 000000000..0678b8a51 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentPropertyImpl.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.mapping; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * @author Michael J. Simons + */ +class Neo4jPersistentPropertyImpl extends AnnotationBasedPersistentProperty + implements Neo4jPersistentProperty { + + /** + * Creates a new {@link AnnotationBasedPersistentProperty}. + * + * @param property must not be {@literal null}. + * @param owner must not be {@literal null}. + * @param simpleTypeHolder + */ + Neo4jPersistentPropertyImpl(Property property, + PersistentEntity owner, + SimpleTypeHolder simpleTypeHolder) { + + super(property, owner, simpleTypeHolder); + } + + @Override + protected Association createAssociation() { + + return new Association<>(this, null); + } + + @Override + public boolean isAssociation() { + return !SimpleTypeHolder.DEFAULT.isSimpleType(super.getType()); + } + + +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java new file mode 100644 index 000000000..060337971 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/IllegalSchemaChangeException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.schema; + +import org.apiguardian.api.API; + +/** + * Exception to be thrown when any change to a {@link Schema} would lead to an inconsistent state. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public class IllegalSchemaChangeException extends IllegalArgumentException { + + IllegalSchemaChangeException(String s) { + super(s); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java new file mode 100644 index 000000000..155eef31d --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Neo4jSimpleTypes.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.schema; + +import org.apiguardian.api.API; + +/** + * A list of Neo4j simple types: All attributes that can be mapped to a property. There is never a relationship + * established for attributes of a node that are simple types. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Neo4jSimpleTypes { + + private Neo4jSimpleTypes() { + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java new file mode 100644 index 000000000..0fe112b49 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/NodeDescription.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.schema; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apiguardian.api.API; + +/** + * Describes how a class is mapped to a node inside the database. It provides navigable links to relationships and + * access to the nodes properties. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class NodeDescription { + + /** + * The primary label of this node. + */ + private final String primaryLabel; + + private final Map propertiesByFieldName; + + private final List relationships; + + public NodeDescription(String primaryLabel, List properties, + List relationships) { + this.primaryLabel = primaryLabel; + this.propertiesByFieldName = properties.stream() + .collect(Collectors.toMap(PropertyDescription::getFieldName, Function + .identity())); + this.relationships = new ArrayList<>(relationships); + } + + /** + * @return The primary label of this node + */ + public String getPrimaryLabel() { + return primaryLabel; + } + + /** + * @return The properties of this node + */ + public Collection getProperties() { + return Collections.unmodifiableCollection(propertiesByFieldName.values()); + } + + /** + * Retrieves a properties description by its field name. + * + * @param fieldName The field name under which the node is described + * @return The description if any + */ + public Optional getPropertyDescription(String fieldName) { + return Optional.ofNullable(this.propertiesByFieldName.get(fieldName)); + } + + /** + * This returns the outgoing relationships this node has to other nodes directions. + * + * @return The relationships defined by instances of this node. + */ + public Collection getRelationships() { + return Collections.unmodifiableCollection(relationships); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java index 3c5e81861..28b07cde0 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Property.java @@ -26,6 +26,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.springframework.core.annotation.AliasFor; /** * The annotation to configure the mapping from a property to an attribute and vice versa. @@ -33,9 +34,15 @@ import org.apiguardian.api.API; * @author Michael J. Simons */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) +@Target(ElementType.FIELD) @Documented @Inherited @API(status = API.Status.STABLE, since = "1.0") public @interface Property { + + @AliasFor("name") + String value() default ""; + + @AliasFor("value") + String name() default ""; } diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java new file mode 100644 index 000000000..732ac4049 --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/PropertyDescription.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.schema; + +import lombok.Getter; + +import org.apiguardian.api.API; + +/** + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +@Getter +public final class PropertyDescription { + + private final String fieldName; + + private final String propertyName; + + // TODO basically all the properties from Springs PersistentProperty are needed. + // Two options: Turn that classes ending in XXXDescription into interfaces and let Neo4jPersistentProperty extend + // from it as well, or copy needed stuff into a format that fits our needs best. + + public PropertyDescription(String fieldName, String propertyName) { + this.fieldName = fieldName; + this.propertyName = propertyName; + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java index fe008577a..f6da825ff 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Relationship.java @@ -34,7 +34,7 @@ import org.springframework.core.annotation.AliasFor; * @author Michael J. Simons */ @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) +@Target(ElementType.FIELD) @Documented @Inherited @API(status = API.Status.STABLE, since = "1.0") @@ -67,6 +67,8 @@ public @interface Relationship { @AliasFor("value") String type() default ""; + String inverse() default ""; + /** * If {@code direction} is {@link Direction#OUTGOING}, than the attribute annotated with {@link Relationship} will be * the target node of the relationship and the class containing the annotated attribute will be the start node. diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java new file mode 100644 index 000000000..2f95b489c --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/RelationshipDescription.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.schema; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import org.apiguardian.api.API; + +/** + * Description of a relationship. Those descriptions always describe outgoing relationships. The inverse direction + * is maybe defined on the {@link NodeDescription} reachable in the {@link Schema} via it's primary label defined by + * {@link #target}. + * + * @author Michael J. Simons + */ +@API(status = API.Status.INTERNAL, since = "1.0") +@RequiredArgsConstructor +@ToString +@EqualsAndHashCode(of = { "type", "target" }) +public final class RelationshipDescription { + + private final String type; + + private final String target; +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java new file mode 100644 index 000000000..2de859b2e --- /dev/null +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/core/schema/Schema.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.schema; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apiguardian.api.API; + +/** + * Contains the descriptions of all nodes, their properties and relationships known to SDN-RX. + * + * The schema is currently designed to be mutual. + */ +@API(status = API.Status.INTERNAL, since = "1.0") +public final class Schema { + + private final Map nodeDescriptionsByPrimaryLabel = new HashMap<>(); + + // Just added as a reminder. Add adaquate locks + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final Lock read = lock.readLock(); + private final Lock write = lock.writeLock(); + + /** + * Registers a node description under it's primary label. + * + * @param newDescription The new node description. + * @return This schema. + * @throws IllegalSchemaChangeException when the description is already registered (under any label) + */ + public Schema registerNodeDescription(NodeDescription newDescription) { + + String primaryLabel = newDescription.getPrimaryLabel(); + if (this.nodeDescriptionsByPrimaryLabel.containsKey(primaryLabel)) { + throw new IllegalSchemaChangeException(String + .format(Locale.ENGLISH, "The schema already contains a node description under the primary label %s", + primaryLabel)); + } + + if (this.nodeDescriptionsByPrimaryLabel.containsValue(newDescription)) { + Optional label = this.nodeDescriptionsByPrimaryLabel.entrySet().stream() + .filter(e -> e.getValue().equals(newDescription)).map( + Map.Entry::getKey).findFirst(); + + throw new IllegalSchemaChangeException(String + .format(Locale.ENGLISH, "The schema already contains description %s under the primary label %s", + newDescription, label.orElse("n/a"))); + } + + this.nodeDescriptionsByPrimaryLabel.put(primaryLabel, newDescription); + return this; + } + + /** + * Retrieves a nodes description by its primary label. + * + * @param primaryLabel The primary label under which the node is described + * @return The description if any + */ + public Optional getNodeDescription(String primaryLabel) { + return Optional.ofNullable(this.nodeDescriptionsByPrimaryLabel.get(primaryLabel)); + } +} diff --git a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java index cc8b1a8b5..cecf03300 100644 --- a/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java +++ b/spring-data-neo4j/src/main/java/org/springframework/data/neo4j/repository/config/Neo4jRepositoryConfigurationExtension.java @@ -18,8 +18,14 @@ */ package org.springframework.data.neo4j.repository.config; +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; + import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.support.Neo4jRepositoryFactoryBean; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.RepositoryConfigurationSource; @@ -63,6 +69,16 @@ public class Neo4jRepositoryConfigurationExtension extends RepositoryConfigurati return MODULE_PREFIX; } + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.singleton(Node.class); + } + + @Override + protected Collection> getIdentifyingTypes() { + return Collections.singleton(Neo4jRepository.class); + } + /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#postProcess(org.springframework.beans.factory.support.BeanDefinitionBuilder, org.springframework.data.repository.config.RepositoryConfigurationSource) diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java index 4db2b0827..511177caf 100644 --- a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContextTest.java @@ -20,7 +20,19 @@ package org.springframework.data.neo4j.core.mapping; import static org.assertj.core.api.Assertions.*; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.NodeDescription; +import org.springframework.data.neo4j.core.schema.Property; +import org.springframework.data.neo4j.core.schema.PropertyDescription; +import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.data.neo4j.core.schema.RelationshipDescription; +import org.springframework.data.neo4j.core.schema.Schema; /** * @author Michael J. Simons @@ -28,9 +40,55 @@ import org.junit.jupiter.api.Test; class Neo4jMappingContextTest { @Test - void shouldCreatePersistentEntity() { + void initializationOfSchemaShouldWork() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> - new Neo4jMappingContext().createPersistentEntity(null)); + Neo4jMappingContext neo4jMappingContext = new Neo4jMappingContext(); + neo4jMappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(BikeNode.class, UserNode.class))); + neo4jMappingContext.initialize(); + + Schema schema = neo4jMappingContext.getSchema(); + + Optional optionalUserNodeDescription = schema.getNodeDescription("User"); + assertThat(optionalUserNodeDescription) + .isPresent() + .hasValueSatisfying(userNodeDescription -> { + assertThat(userNodeDescription.getProperties()) + .extracting(PropertyDescription::getFieldName) + .containsExactlyInAnyOrder("name", "first_name"); + + assertThat(userNodeDescription.getProperties()) + .extracting(PropertyDescription::getPropertyName) + .containsExactlyInAnyOrder("name", "firstName"); + }); + + Optional optionalBikeNodeDescription = schema.getNodeDescription("BikeNode"); + assertThat(optionalBikeNodeDescription) + .isPresent() + .hasValueSatisfying(bikeNodeDescription -> + assertThat(bikeNodeDescription.getRelationships()) + .containsExactlyInAnyOrder( + new RelationshipDescription("owner", "User"), + new RelationshipDescription("renter", "User"))); } + + @Node("User") + static class UserNode { + + @Relationship(type = "OWNS", inverse = "owner") + List bikes; + + String name; + + @Property(name = "firstName") + String first_name; + } + + static class BikeNode { + + UserNode owner; + + List renter; + } + + } diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java new file mode 100644 index 000000000..e3f6b6040 --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntityImplTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.mapping; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.data.neo4j.core.schema.Node; + +/** + * @author Michael J. Simons + */ +public class Neo4jPersistentEntityImplTest { + + private Neo4jMappingContext mappingContext = new Neo4jMappingContext(); + + @Test + void shouldDiscoverAnnotatedPrimaryLabel() { + + Neo4jPersistentEntity entity = mappingContext.getPersistentEntity(DummySubEntity.class); + + assertThat(entity).isNotNull(); + } + + @Node("DummySubNode") + static class DummySubEntity { + } +} diff --git a/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java new file mode 100644 index 000000000..1defe9f7e --- /dev/null +++ b/spring-data-neo4j/src/test/java/org/springframework/data/neo4j/core/schema/SchemaTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019 "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.springframework.data.neo4j.core.schema; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +/** + * @author Michael J. Simons + */ +class SchemaTest { + + @Test + void shouldGetNodeDescription() { + NodeDescription description = new NodeDescription("aLabel", Collections.emptyList(), Collections.emptyList()); + + Schema schema = new Schema().registerNodeDescription(description); + assertThat(schema.getNodeDescription("aLabel")).isPresent().contains(description); + assertThat(schema.getNodeDescription("anotherLabel")).isNotPresent(); + } +}