From 0de492eb26755d37d3d5bb39acff7643bdf0d2a1 Mon Sep 17 00:00:00 2001 From: mpv1989 Date: Mon, 23 Jul 2018 14:24:49 +0200 Subject: [PATCH] Add `@ArangoId` (representation of `_id`) --- .../springframework/annotation/ArangoId.java | 40 ++++++++++++++ .../core/convert/DefaultArangoConverter.java | 19 ++++++- .../ArangoIdPropertyIdentifierAccessor.java | 55 +++++++++++++++++++ .../core/mapping/ArangoPersistentEntity.java | 5 ++ .../mapping/ArangoPersistentProperty.java | 2 + .../DefaultArangoPersistentEntity.java | 32 +++++++++++ .../DefaultArangoPersistentProperty.java | 10 +++- .../core/template/ArangoTemplate.java | 16 +++++- .../core/mapping/GeneralMappingTest.java | 36 ++++++++++++ .../core/mapping/RefMappingTest.java | 50 +++++++++++++++++ .../core/template/ArangoTemplateTest.java | 3 + .../repository/ArangoRepositoryTest.java | 4 +- .../springframework/testdata/Customer.java | 11 ++++ 13 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/arangodb/springframework/annotation/ArangoId.java create mode 100644 src/main/java/com/arangodb/springframework/core/mapping/ArangoIdPropertyIdentifierAccessor.java diff --git a/src/main/java/com/arangodb/springframework/annotation/ArangoId.java b/src/main/java/com/arangodb/springframework/annotation/ArangoId.java new file mode 100644 index 000000000..88eef0cec --- /dev/null +++ b/src/main/java/com/arangodb/springframework/annotation/ArangoId.java @@ -0,0 +1,40 @@ +/* + * DISCLAIMER + * + * Copyright 2018 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.springframework.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.ReadOnlyProperty; + +/** + * Representation of ArangoDB document field {@code _id} + * + * @author Mark Vollmary + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +@ReadOnlyProperty +public @interface ArangoId { + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoConverter.java b/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoConverter.java index 75e557d68..057374444 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoConverter.java +++ b/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoConverter.java @@ -624,11 +624,24 @@ private void writeEntity( } }); + addKeyIfNecessary(entity, source, sink); addTypeKeyIfNecessary(definedType, source, sink); sink.close(); } + private void addKeyIfNecessary( + final ArangoPersistentEntity entity, + final Object source, + final VPackBuilder sink) { + if (!entity.hasIdProperty() || entity.getIdentifierAccessor(source).getIdentifier() == null) { + final Object id = entity.getArangoIdAccessor(source).getIdentifier(); + if (id != null) { + sink.add(_KEY, MetadataUtils.determineDocumentKeyFromId((String) id)); + } + } + } + private void writeProperty(final Object source, final VPackBuilder sink, final ArangoPersistentProperty property) { if (source == null) { return; @@ -859,7 +872,11 @@ private Optional getRefId(final Object source, final ArangoPersistentEnt } final Optional id = Optional.ofNullable(entity.getIdentifierAccessor(source).getIdentifier()); - return id.map(key -> MetadataUtils.createIdFromCollectionAndKey(entity.getCollection(), convertId(key))); + if (id.isPresent()) { + return id.map(key -> MetadataUtils.createIdFromCollectionAndKey(entity.getCollection(), convertId(key))); + } + + return Optional.ofNullable((String) entity.getArangoIdAccessor(source).getIdentifier()); } private static Collection asCollection(final Object source) { diff --git a/src/main/java/com/arangodb/springframework/core/mapping/ArangoIdPropertyIdentifierAccessor.java b/src/main/java/com/arangodb/springframework/core/mapping/ArangoIdPropertyIdentifierAccessor.java new file mode 100644 index 000000000..aa3e38e29 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/mapping/ArangoIdPropertyIdentifierAccessor.java @@ -0,0 +1,55 @@ +/* + * DISCLAIMER + * + * Copyright 2018 ArangoDB GmbH, Cologne, Germany + * + * 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. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.springframework.core.mapping; + +import java.util.Optional; + +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.TargetAwareIdentifierAccessor; +import org.springframework.util.Assert; + +/** + * @author Mark Vollmary + * + */ +public class ArangoIdPropertyIdentifierAccessor extends TargetAwareIdentifierAccessor { + + private final ArangoPersistentProperty arangoIdProperty; + private final PersistentPropertyAccessor accessor; + + public ArangoIdPropertyIdentifierAccessor(final ArangoPersistentEntity entity, final Object target) { + super(target); + + Assert.notNull(entity, "PersistentEntity must not be null!"); + final Optional aip = entity.getArangoIdProperty(); + Assert.isTrue(aip.isPresent(), "PersistentEntity must have an arango identifier property!"); + Assert.notNull(target, "Target bean must not be null!"); + + this.arangoIdProperty = aip.get(); + this.accessor = entity.getPropertyAccessor(target); + } + + @Override + public Object getIdentifier() { + return accessor.getProperty(arangoIdProperty); + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentEntity.java b/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentEntity.java index d1824d134..8784fa6ea 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentEntity.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentEntity.java @@ -24,6 +24,7 @@ import java.util.Optional; import org.springframework.context.ApplicationContextAware; +import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.PersistentEntity; import com.arangodb.model.CollectionCreateOptions; @@ -45,6 +46,8 @@ public interface ArangoPersistentEntity CollectionCreateOptions getCollectionOptions(); + Optional getArangoIdProperty(); + Optional getRevProperty(); Collection getHashIndexes(); @@ -67,4 +70,6 @@ public interface ArangoPersistentEntity Collection getFulltextIndexedProperties(); + IdentifierAccessor getArangoIdAccessor(Object bean); + } diff --git a/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentProperty.java b/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentProperty.java index b51080478..9f8fb36c7 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentProperty.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentProperty.java @@ -42,6 +42,8 @@ public interface ArangoPersistentProperty extends PersistentProperty getRef(); diff --git a/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentEntity.java b/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentEntity.java index b0306908e..f390572d4 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentEntity.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentEntity.java @@ -36,12 +36,15 @@ import org.springframework.context.expression.BeanFactoryAccessor; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.mapping.IdentifierAccessor; +import org.springframework.data.mapping.TargetAwareIdentifierAccessor; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.util.TypeInformation; import org.springframework.expression.Expression; import org.springframework.expression.ParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.arangodb.entity.CollectionType; @@ -72,6 +75,7 @@ public class DefaultArangoPersistentEntity extends BasicPersistentEntity hashIndexedProperties; private final Collection skiplistIndexedProperties; @@ -184,6 +188,9 @@ public void setApplicationContext(final ApplicationContext applicationContext) t @Override public void addPersistentProperty(final ArangoPersistentProperty property) { super.addPersistentProperty(property); + if (property.isArangoIdProperty()) { + arangoIdProperty = property; + } if (property.isRevProperty()) { revProperty = property; } @@ -194,6 +201,11 @@ public void addPersistentProperty(final ArangoPersistentProperty property) { property.getFulltextIndexed().ifPresent(i -> fulltextIndexedProperties.add(property)); } + @Override + public Optional getArangoIdProperty() { + return Optional.ofNullable(arangoIdProperty); + } + @Override public Optional getRevProperty() { return Optional.ofNullable(revProperty); @@ -278,4 +290,24 @@ public Set findAnnotations(final Class annotationTy return (Set) repeatableAnnotationCache.computeIfAbsent(annotationType, it -> AnnotatedElementUtils.findMergedRepeatableAnnotations(getType(), it)); } + + private static class AbsentAccessor extends TargetAwareIdentifierAccessor { + + public AbsentAccessor(final Object target) { + super(target); + } + + @Override + @Nullable + public Object getIdentifier() { + return null; + } + } + + @Override + public IdentifierAccessor getArangoIdAccessor(final Object bean) { + return getArangoIdProperty().isPresent() ? new ArangoIdPropertyIdentifierAccessor(this, bean) + : new AbsentAccessor(bean); + } + } diff --git a/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentProperty.java b/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentProperty.java index 86da4a0d6..6129413e5 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentProperty.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentProperty.java @@ -31,6 +31,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.util.StringUtils; +import com.arangodb.springframework.annotation.ArangoId; import com.arangodb.springframework.annotation.Field; import com.arangodb.springframework.annotation.From; import com.arangodb.springframework.annotation.FulltextIndexed; @@ -65,6 +66,11 @@ protected Association createAssociation() { return new Association<>(this, null); } + @Override + public boolean isArangoIdProperty() { + return findAnnotation(ArangoId.class) != null; + } + @Override public boolean isRevProperty() { return findAnnotation(Rev.class) != null; @@ -93,7 +99,9 @@ public Optional getTo() { @Override public String getFieldName() { final String fieldName; - if (isIdProperty()) { + if (isArangoIdProperty()) { + fieldName = "_id"; + } else if (isIdProperty()) { fieldName = "_key"; } else if (isRevProperty()) { fieldName = "_rev"; diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java index 59a34e9ab..088f7d2c0 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -557,12 +557,23 @@ public DocumentEntity insert(final String collectionName, final Object value) th return insert(collectionName, value, new DocumentCreateOptions()); } + private Object getDocumentKey(final ArangoPersistentEntity entity, final Object value) { + Object id = entity.getIdentifierAccessor(value).getIdentifier(); + if (id == null) { + final Object docId = entity.getArangoIdAccessor(value).getIdentifier(); + if (docId != null) { + id = MetadataUtils.determineDocumentKeyFromId((String) docId); + } + } + return id; + } + @Override public void upsert(final T value, final UpsertStrategy strategy) throws DataAccessException { final Class entityClass = value.getClass(); final ArangoPersistentEntity entity = getConverter().getMappingContext().getPersistentEntity(entityClass); - final Object id = entity.getIdentifierAccessor(value).getIdentifier(); + final Object id = getDocumentKey(entity, value); if (id != null && (!(value instanceof Persistable) || !Persistable.class.cast(value).isNew())) { switch (strategy) { case UPDATE: @@ -591,7 +602,7 @@ public void upsert(final Iterable value, final UpsertStrategy strategy) t final Collection withId = new ArrayList<>(); final Collection withoutId = new ArrayList<>(); for (final T e : value) { - final Object id = entity.getIdentifierAccessor(e).getIdentifier(); + final Object id = getDocumentKey(entity, e); if (id != null && (!(e instanceof Persistable) || !Persistable.class.cast(e).isNew())) { withId.add(e); continue; @@ -650,6 +661,7 @@ private void updateDBFields(final Object value, final DocumentEntity documentEnt if (idProperty != null) { accessor.setProperty(idProperty, documentEntity.getKey()); } + entity.getArangoIdProperty().ifPresent(arangoId -> accessor.setProperty(arangoId, documentEntity.getId())); entity.getRevProperty().ifPresent(rev -> accessor.setProperty(rev, documentEntity.getRev())); } diff --git a/src/test/java/com/arangodb/springframework/core/mapping/GeneralMappingTest.java b/src/test/java/com/arangodb/springframework/core/mapping/GeneralMappingTest.java index 65fadf489..df8df6094 100644 --- a/src/test/java/com/arangodb/springframework/core/mapping/GeneralMappingTest.java +++ b/src/test/java/com/arangodb/springframework/core/mapping/GeneralMappingTest.java @@ -42,8 +42,10 @@ import com.arangodb.model.AqlQueryOptions; import com.arangodb.springframework.AbstractArangoTest; import com.arangodb.springframework.ArangoTestConfiguration; +import com.arangodb.springframework.annotation.ArangoId; import com.arangodb.springframework.annotation.Document; import com.arangodb.springframework.annotation.Field; +import com.arangodb.springframework.core.ArangoOperations.UpsertStrategy; import com.arangodb.springframework.core.mapping.testdata.BasicTestEntity; import com.arangodb.springframework.testdata.Actor; import com.arangodb.springframework.testdata.Movie; @@ -374,4 +376,38 @@ public void twoTypesInSameCollection() { assertThat(findB.get().value, is("testB")); assertThat(findB.get().b, is("testB")); } + + static class ArangoIdOnlyTestEntity { + @ArangoId + private String id; + } + + @SuppressWarnings("deprecation") + @Test + public void arangoIdOnly() { + final ArangoIdOnlyTestEntity entity = new ArangoIdOnlyTestEntity(); + template.upsert(entity, UpsertStrategy.UPDATE); + assertThat(entity.id, is(notNullValue())); + final Optional find = template.find(entity.id, ArangoIdOnlyTestEntity.class); + assertThat(find.isPresent(), is(true)); + assertThat(find.get().id, is(entity.id)); + } + + static class ArangoIdAndIdTestEntity { + @ArangoId + private String arangoId; + @Id + private String id; + } + + @Test + public void arangoIdAndId() { + final ArangoIdAndIdTestEntity entity = new ArangoIdAndIdTestEntity(); + entity.arangoId = "arangoIdAndIdTestEntity/test"; + template.insert(entity); + assertThat(entity.arangoId, is("arangoIdAndIdTestEntity/test")); + assertThat(entity.id, is("test")); + assertThat(template.find(entity.id, ArangoIdAndIdTestEntity.class).isPresent(), is(true)); + assertThat(template.find(entity.arangoId, ArangoIdAndIdTestEntity.class).isPresent(), is(true)); + } } diff --git a/src/test/java/com/arangodb/springframework/core/mapping/RefMappingTest.java b/src/test/java/com/arangodb/springframework/core/mapping/RefMappingTest.java index 56655e392..cbc78a745 100644 --- a/src/test/java/com/arangodb/springframework/core/mapping/RefMappingTest.java +++ b/src/test/java/com/arangodb/springframework/core/mapping/RefMappingTest.java @@ -34,8 +34,10 @@ import java.util.stream.Collectors; import org.junit.Test; +import org.springframework.data.annotation.Id; import com.arangodb.springframework.AbstractArangoTest; +import com.arangodb.springframework.annotation.ArangoId; import com.arangodb.springframework.annotation.Document; import com.arangodb.springframework.annotation.Ref; import com.arangodb.springframework.core.mapping.testdata.BasicTestEntity; @@ -317,4 +319,52 @@ public void twoTypesInSameCollection() { } } + static class ArangoIdOnlyTestEntity { + @ArangoId + String id; + @Ref + Collection refs; + } + + @Test + public void arangoIdOnly() { + final ArangoIdOnlyTestEntity a = new ArangoIdOnlyTestEntity(); + final ArangoIdOnlyTestEntity b = new ArangoIdOnlyTestEntity(); + b.refs = Arrays.asList(a); + + template.insert(a); + template.insert(b); + + final Optional find = template.find(b.id, ArangoIdOnlyTestEntity.class); + assertThat(find.isPresent(), is(true)); + final Collection refs = find.get().refs; + assertThat(refs.size(), is(1)); + assertThat(refs.stream().findFirst().get().id, is(a.id)); + } + + static class ArangoIdAndIdTestEntity { + @ArangoId + String arangoId; + @Id + String id; + @Ref + Collection refs; + } + + @Test + public void arangoIdAndId() { + final ArangoIdAndIdTestEntity a = new ArangoIdAndIdTestEntity(); + final ArangoIdAndIdTestEntity b = new ArangoIdAndIdTestEntity(); + b.refs = Arrays.asList(a); + + template.insert(a); + a.id = null; + template.insert(b); + + final Optional find = template.find(b.arangoId, ArangoIdAndIdTestEntity.class); + assertThat(find.isPresent(), is(true)); + final Collection refs = find.get().refs; + assertThat(refs.size(), is(1)); + assertThat(refs.stream().findFirst().get().arangoId, is(a.arangoId)); + } } diff --git a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java index fe19c9fc8..ffc355de3 100644 --- a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java +++ b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java @@ -180,6 +180,9 @@ public void getDocuments() { assertThat( StreamSupport.stream(customers.spliterator(), false).map((e) -> e.getId()).collect(Collectors.toList()), hasItems(c1.getId(), c2.getId())); + for (final Customer customer : customers) { + assertThat(customer.getArangoId(), is(notNullValue())); + } } @Test diff --git a/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java b/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java index 237190a3d..e7cfcd672 100644 --- a/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java +++ b/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java @@ -317,8 +317,8 @@ public void endingWithByExampleNestedIncludeNullTest() { exampleCustomer.setNestedCustomer(nested3); final Example example = Example.of(exampleCustomer, ExampleMatcher.matching().withMatcher("nestedCustomer.name", match -> match.endsWith()) - .withIgnorePaths(new String[] { "id", "key", "rev" }).withIgnoreCase("nestedCustomer.name") - .withIncludeNullValues() + .withIgnorePaths(new String[] { "arangoId", "id", "key", "rev" }) + .withIgnoreCase("nestedCustomer.name").withIncludeNullValues() .withTransformer("nestedCustomer.age", o -> Optional.of(Integer.valueOf(o.get().toString()) + 1))); final Customer retrieved = repository.findOne(example).get(); assertEquals(check, retrieved); diff --git a/src/test/java/com/arangodb/springframework/testdata/Customer.java b/src/test/java/com/arangodb/springframework/testdata/Customer.java index c366c415f..a23f4a787 100644 --- a/src/test/java/com/arangodb/springframework/testdata/Customer.java +++ b/src/test/java/com/arangodb/springframework/testdata/Customer.java @@ -24,6 +24,7 @@ import org.springframework.data.annotation.Id; +import com.arangodb.springframework.annotation.ArangoId; import com.arangodb.springframework.annotation.Document; import com.arangodb.springframework.annotation.GeoIndexed; import com.arangodb.springframework.annotation.Ref; @@ -37,6 +38,8 @@ @Document public class Customer { + @ArangoId + private String arangoId; @Id private String id; @Rev @@ -87,6 +90,14 @@ public Customer(final String name, final String surname, final int age, final Ad this.address = address; } + public String getArangoId() { + return arangoId; + } + + public void setArangoId(final String arangoId) { + this.arangoId = arangoId; + } + public String getId() { return id; }