From f9453d5215880186c80160e402705c9ffda6cccd Mon Sep 17 00:00:00 2001 From: Paulo Ferreira Date: Mon, 3 Aug 2020 15:23:08 -0300 Subject: [PATCH] Find By Example With Array --- .../config/ArangoConfiguration.java | 2 +- .../core/ArangoOperations.java | 3 + .../core/convert/resolver/RefResolver.java | 4 +- .../convert/resolver/ReferenceResolver.java | 2 +- .../core/template/ArangoTemplate.java | 213 ++++++++++-------- .../repository/ArangoExampleConverter.java | 91 +++++--- .../repository/SimpleArangoRepository.java | 110 ++++----- .../repository/ArangoRepositoryTest.java | 66 ++++++ 8 files changed, 294 insertions(+), 197 deletions(-) diff --git a/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java b/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java index 1a236aae7..064cfb177 100644 --- a/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java +++ b/src/main/java/com/arangodb/springframework/config/ArangoConfiguration.java @@ -56,7 +56,7 @@ public interface ArangoConfiguration { @Bean default ArangoOperations arangoTemplate() throws Exception { - return new ArangoTemplate(arango().build(), database(), arangoConverter()); + return new ArangoTemplate(arango().build(), database(), arangoConverter(), resolverFactory()); } @Bean diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index 841231cca..83c3663f3 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -39,6 +39,7 @@ import com.arangodb.model.DocumentReplaceOptions; import com.arangodb.model.DocumentUpdateOptions; import com.arangodb.springframework.core.convert.ArangoConverter; +import com.arangodb.springframework.core.convert.resolver.ResolverFactory; /** * Interface that specifies a basic set of ArangoDB operations. @@ -581,5 +582,7 @@ public enum UpsertStrategy { Iterable getUsers() throws DataAccessException; ArangoConverter getConverter(); + + ResolverFactory getResolverFactory(); } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java index ed58eedf6..2649748b4 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/RefResolver.java @@ -62,8 +62,8 @@ public Object resolve(final String id, final TypeInformation type, final Ref } @Override - public String write(final Object source, final ArangoPersistentEntity entity, final String id, final Ref annotation) { - return MetadataUtils.createIdFromCollectionAndKey(entity.getCollection(), id); + public String write(final Object source, final ArangoPersistentEntity entity, final Object id, final Ref annotation) { + return MetadataUtils.createIdFromCollectionAndKey(entity.getCollection(), String.valueOf(id)); } diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/ReferenceResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/ReferenceResolver.java index fd985e082..667aadb5f 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/ReferenceResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/ReferenceResolver.java @@ -37,6 +37,6 @@ public interface ReferenceResolver { Object resolveMultiple(Collection ids, TypeInformation type, A annotation); - public String write(Object source, ArangoPersistentEntity entity, String id, Ref annotation); + public String write(Object source, ArangoPersistentEntity entity, Object id, Ref annotation); } 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 8db7cfc2f..445b7a5ca 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -20,25 +20,19 @@ package com.arangodb.springframework.core.template; -import com.arangodb.*; -import com.arangodb.entity.ArangoDBVersion; -import com.arangodb.entity.DocumentEntity; -import com.arangodb.entity.MultiDocumentEntity; -import com.arangodb.entity.UserEntity; -import com.arangodb.model.*; -import com.arangodb.springframework.annotation.*; -import com.arangodb.springframework.core.ArangoOperations; -import com.arangodb.springframework.core.CollectionOperations; -import com.arangodb.springframework.core.UserOperations; -import com.arangodb.springframework.core.convert.ArangoConverter; -import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; -import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; -import com.arangodb.springframework.core.mapping.event.*; -import com.arangodb.springframework.core.template.DefaultUserOperation.CollectionCallback; -import com.arangodb.springframework.core.util.ArangoExceptionTranslator; -import com.arangodb.springframework.core.util.MetadataUtils; -import com.arangodb.util.MapBuilder; -import com.arangodb.velocypack.VPackSlice; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -54,11 +48,50 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import java.util.*; -import java.util.Map.Entry; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; +import com.arangodb.ArangoCollection; +import com.arangodb.ArangoCursor; +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDBException; +import com.arangodb.ArangoDatabase; +import com.arangodb.entity.ArangoDBVersion; +import com.arangodb.entity.DocumentEntity; +import com.arangodb.entity.MultiDocumentEntity; +import com.arangodb.entity.UserEntity; +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.model.CollectionCreateOptions; +import com.arangodb.model.DocumentCreateOptions; +import com.arangodb.model.DocumentDeleteOptions; +import com.arangodb.model.DocumentReadOptions; +import com.arangodb.model.DocumentReplaceOptions; +import com.arangodb.model.DocumentUpdateOptions; +import com.arangodb.model.FulltextIndexOptions; +import com.arangodb.model.GeoIndexOptions; +import com.arangodb.model.HashIndexOptions; +import com.arangodb.model.PersistentIndexOptions; +import com.arangodb.model.SkiplistIndexOptions; +import com.arangodb.springframework.annotation.FulltextIndex; +import com.arangodb.springframework.annotation.GeoIndex; +import com.arangodb.springframework.annotation.HashIndex; +import com.arangodb.springframework.annotation.PersistentIndex; +import com.arangodb.springframework.annotation.SkiplistIndex; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.core.CollectionOperations; +import com.arangodb.springframework.core.UserOperations; +import com.arangodb.springframework.core.convert.ArangoConverter; +import com.arangodb.springframework.core.convert.resolver.ResolverFactory; +import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; +import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; +import com.arangodb.springframework.core.mapping.event.AfterDeleteEvent; +import com.arangodb.springframework.core.mapping.event.AfterLoadEvent; +import com.arangodb.springframework.core.mapping.event.AfterSaveEvent; +import com.arangodb.springframework.core.mapping.event.ArangoMappingEvent; +import com.arangodb.springframework.core.mapping.event.BeforeDeleteEvent; +import com.arangodb.springframework.core.mapping.event.BeforeSaveEvent; +import com.arangodb.springframework.core.template.DefaultUserOperation.CollectionCallback; +import com.arangodb.springframework.core.util.ArangoExceptionTranslator; +import com.arangodb.springframework.core.util.MetadataUtils; +import com.arangodb.util.MapBuilder; +import com.arangodb.velocypack.VPackSlice; /** * @author Mark Vollmary @@ -72,6 +105,7 @@ public class ArangoTemplate implements ArangoOperations, CollectionCallback, App private volatile ArangoDBVersion version; private final PersistenceExceptionTranslator exceptionTranslator; private final ArangoConverter converter; + private final ResolverFactory resolverFactory; private final ArangoDB arango; private final String databaseName; private final Expression databaseExpression; @@ -82,21 +116,19 @@ public class ArangoTemplate implements ArangoOperations, CollectionCallback, App private ApplicationEventPublisher eventPublisher; - public ArangoTemplate(final ArangoDB arango, final String database) { - this(arango, database, null); - } - - public ArangoTemplate(final ArangoDB arango, final String database, final ArangoConverter converter) { - this(arango, database, converter, new ArangoExceptionTranslator()); + public ArangoTemplate(final ArangoDB arango, final String database, final ArangoConverter converter, + final ResolverFactory resolverFactory) { + this(arango, database, converter, resolverFactory, new ArangoExceptionTranslator()); } public ArangoTemplate(final ArangoDB arango, final String database, final ArangoConverter converter, - final PersistenceExceptionTranslator exceptionTranslator) { + final ResolverFactory resolverFactory, final PersistenceExceptionTranslator exceptionTranslator) { super(); this.arango = arango._setCursorInitializer(new ArangoCursorInitializer(converter)); this.databaseName = database; this.databaseExpression = PARSER.parseExpression(databaseName, ParserContext.TEMPLATE_EXPRESSION); this.converter = converter; + this.resolverFactory = resolverFactory; this.exceptionTranslator = exceptionTranslator; this.context = new StandardEvaluationContext(); // set concurrency level to 1 as writes are very rare compared to reads @@ -136,21 +168,19 @@ private ArangoCollection _collection(final Class entityClass, final Object id return _collection(name, persistentEntity, persistentEntity.getCollectionOptions()); } - private ArangoCollection _collection( - final String name, - final ArangoPersistentEntity persistentEntity, - final CollectionCreateOptions options) { + private ArangoCollection _collection(final String name, final ArangoPersistentEntity persistentEntity, + final CollectionCreateOptions options) { final ArangoDatabase db = db(); final Class entityClass = persistentEntity != null ? persistentEntity.getType() : null; final CollectionCacheValue value = collectionCache.computeIfAbsent(new CollectionCacheKey(db.name(), name), - key -> { - final ArangoCollection collection = db.collection(name); - if (!collection.exists()) { - collection.create(options); - } - return new CollectionCacheValue(collection); - }); + key -> { + final ArangoCollection collection = db.collection(name); + if (!collection.exists()) { + collection.create(options); + } + return new CollectionCacheValue(collection); + }); final Collection> entities = value.getEntities(); final ArangoCollection collection = value.getCollection(); if (persistentEntity != null && !entities.contains(entityClass)) { @@ -160,9 +190,8 @@ private ArangoCollection _collection( return collection; } - private static void ensureCollectionIndexes( - final CollectionOperations collection, - final ArangoPersistentEntity persistentEntity) { + private static void ensureCollectionIndexes(final CollectionOperations collection, + final ArangoPersistentEntity persistentEntity) { persistentEntity.getHashIndexes().stream().forEach(index -> ensureHashIndex(collection, index)); persistentEntity.getHashIndexedProperties().stream().forEach(p -> ensureHashIndex(collection, p)); persistentEntity.getSkiplistIndexes().stream().forEach(index -> ensureSkiplistIndex(collection, index)); @@ -192,9 +221,8 @@ private static void ensureSkiplistIndex(final CollectionOperations collection, f .unique(annotation.unique()).sparse(annotation.sparse()).deduplicate(annotation.deduplicate())); } - private static void ensureSkiplistIndex( - final CollectionOperations collection, - final ArangoPersistentProperty value) { + private static void ensureSkiplistIndex(final CollectionOperations collection, + final ArangoPersistentProperty value) { final SkiplistIndexOptions options = new SkiplistIndexOptions(); value.getSkiplistIndexed() .ifPresent(i -> options.unique(i.unique()).sparse(i.sparse()).deduplicate(i.deduplicate())); @@ -203,12 +231,11 @@ private static void ensureSkiplistIndex( private static void ensurePersistentIndex(final CollectionOperations collection, final PersistentIndex annotation) { collection.ensurePersistentIndex(Arrays.asList(annotation.fields()), - new PersistentIndexOptions().unique(annotation.unique()).sparse(annotation.sparse())); + new PersistentIndexOptions().unique(annotation.unique()).sparse(annotation.sparse())); } - private static void ensurePersistentIndex( - final CollectionOperations collection, - final ArangoPersistentProperty value) { + private static void ensurePersistentIndex(final CollectionOperations collection, + final ArangoPersistentProperty value) { final PersistentIndexOptions options = new PersistentIndexOptions(); value.getPersistentIndexed().ifPresent(i -> options.unique(i.unique()).sparse(i.sparse())); collection.ensurePersistentIndex(Collections.singleton(value.getFieldName()), options); @@ -216,7 +243,7 @@ private static void ensurePersistentIndex( private static void ensureGeoIndex(final CollectionOperations collection, final GeoIndex annotation) { collection.ensureGeoIndex(Arrays.asList(annotation.fields()), - new GeoIndexOptions().geoJson(annotation.geoJson())); + new GeoIndexOptions().geoJson(annotation.geoJson())); } private static void ensureGeoIndex(final CollectionOperations collection, final ArangoPersistentProperty value) { @@ -227,12 +254,11 @@ private static void ensureGeoIndex(final CollectionOperations collection, final private static void ensureFulltextIndex(final CollectionOperations collection, final FulltextIndex annotation) { collection.ensureFulltextIndex(Collections.singleton(annotation.field()), - new FulltextIndexOptions().minLength(annotation.minLength() > -1 ? annotation.minLength() : null)); + new FulltextIndexOptions().minLength(annotation.minLength() > -1 ? annotation.minLength() : null)); } - private static void ensureFulltextIndex( - final CollectionOperations collection, - final ArangoPersistentProperty value) { + private static void ensureFulltextIndex(final CollectionOperations collection, + final ArangoPersistentProperty value) { final FulltextIndexOptions options = new FulltextIndexOptions(); value.getFulltextIndexed().ifPresent(i -> options.minLength(i.minLength() > -1 ? i.minLength() : null)); collection.ensureFulltextIndex(Collections.singleton(value.getFieldName()), options); @@ -302,11 +328,8 @@ public ArangoCursor query(final String query, final AqlQueryOptions optio } @Override - public ArangoCursor query( - final String query, - final Map bindVars, - final AqlQueryOptions options, - final Class entityClass) throws DataAccessException { + public ArangoCursor query(final String query, final Map bindVars, + final AqlQueryOptions options, final Class entityClass) throws DataAccessException { return db().query(query, bindVars == null ? null : prepareBindVars(bindVars), options, entityClass); } @@ -323,10 +346,8 @@ private Map prepareBindVars(final Map bindVars) } @Override - public MultiDocumentEntity delete( - final Iterable values, - final Class entityClass, - final DocumentDeleteOptions options) throws DataAccessException { + public MultiDocumentEntity delete(final Iterable values, + final Class entityClass, final DocumentDeleteOptions options) throws DataAccessException { potentiallyEmitBeforeDeleteEvent(values, entityClass); @@ -342,9 +363,8 @@ public MultiDocumentEntity delete( } @Override - public MultiDocumentEntity delete( - final Iterable values, - final Class entityClass) throws DataAccessException { + public MultiDocumentEntity delete(final Iterable values, + final Class entityClass) throws DataAccessException { return delete(values, entityClass, new DocumentDeleteOptions()); } @@ -371,10 +391,8 @@ public DocumentEntity delete(final Object id, final Class entityClass) throws } @Override - public MultiDocumentEntity update( - final Iterable values, - final Class entityClass, - final DocumentUpdateOptions options) throws DataAccessException { + public MultiDocumentEntity update(final Iterable values, + final Class entityClass, final DocumentUpdateOptions options) throws DataAccessException { potentiallyEmitBeforeSaveEvent(values); @@ -391,9 +409,8 @@ public MultiDocumentEntity update( } @Override - public MultiDocumentEntity update( - final Iterable values, - final Class entityClass) throws DataAccessException { + public MultiDocumentEntity update(final Iterable values, + final Class entityClass) throws DataAccessException { return update(values, entityClass, new DocumentUpdateOptions()); } @@ -406,7 +423,7 @@ public DocumentEntity update(final Object id, final Object value, final Document final DocumentEntity result; try { result = _collection(value.getClass(), id).updateDocument(determineDocumentKeyFromId(id), toVPack(value), - options); + options); } catch (final ArangoDBException e) { throw translateExceptionIfPossible(e); } @@ -422,10 +439,8 @@ public DocumentEntity update(final Object id, final Object value) throws DataAcc } @Override - public MultiDocumentEntity replace( - final Iterable values, - final Class entityClass, - final DocumentReplaceOptions options) throws DataAccessException { + public MultiDocumentEntity replace(final Iterable values, + final Class entityClass, final DocumentReplaceOptions options) throws DataAccessException { potentiallyEmitBeforeSaveEvent(values); @@ -442,9 +457,8 @@ public MultiDocumentEntity replace( } @Override - public MultiDocumentEntity replace( - final Iterable values, - final Class entityClass) throws DataAccessException { + public MultiDocumentEntity replace(final Iterable values, + final Class entityClass) throws DataAccessException { return replace(values, entityClass, new DocumentReplaceOptions()); } @@ -456,7 +470,7 @@ public DocumentEntity replace(final Object id, final Object value, final Documen final DocumentEntity result; try { result = _collection(value.getClass(), id).replaceDocument(determineDocumentKeyFromId(id), toVPack(value), - options); + options); } catch (final ArangoDBException e) { throw translateExceptionIfPossible(e); } @@ -476,7 +490,7 @@ public Optional find(final Object id, final Class entityClass, final D throws DataAccessException { try { final VPackSlice doc = _collection(entityClass, id).getDocument(determineDocumentKeyFromId(id), - VPackSlice.class, options); + VPackSlice.class, options); return Optional.ofNullable(fromVPack(entityClass, doc)); } catch (final ArangoDBException e) { throw translateExceptionIfPossible(e); @@ -509,10 +523,8 @@ public Iterable find(final Iterable ids, final Class } @Override - public MultiDocumentEntity insert( - final Iterable values, - final Class entityClass, - final DocumentCreateOptions options) throws DataAccessException { + public MultiDocumentEntity insert(final Iterable values, + final Class entityClass, final DocumentCreateOptions options) throws DataAccessException { potentiallyEmitBeforeSaveEvent(values); @@ -529,9 +541,8 @@ public MultiDocumentEntity insert( } @Override - public MultiDocumentEntity insert( - final Iterable values, - final Class entityClass) throws DataAccessException { + public MultiDocumentEntity insert(final Iterable values, + final Class entityClass) throws DataAccessException { return insert(values, entityClass, new DocumentCreateOptions()); } @@ -770,9 +781,8 @@ private void potentiallyEmitBeforeSaveEvent(final Iterable values) { } } - private void potentiallyEmitAfterSaveEvent( - final Iterable values, - final MultiDocumentEntity result) { + private void potentiallyEmitAfterSaveEvent(final Iterable values, + final MultiDocumentEntity result) { final Iterator valueIterator = values.iterator(); final Iterator documentIterator = result.getDocumentsAndErrors().iterator(); @@ -792,10 +802,8 @@ private void potentiallyEmitBeforeDeleteEvent(final Iterable values, final Cl } } - private void potentiallyEmitAfterDeleteEvent( - final Iterable values, - final Class entityClass, - final MultiDocumentEntity result) { + private void potentiallyEmitAfterDeleteEvent(final Iterable values, final Class entityClass, + final MultiDocumentEntity result) { final Iterator valueIterator = values.iterator(); final Iterator documentIterator = result.getDocumentsAndErrors().iterator(); @@ -809,4 +817,9 @@ private void potentiallyEmitAfterDeleteEvent( } } + @Override + public ResolverFactory getResolverFactory() { + return this.resolverFactory; + } + } diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoExampleConverter.java b/src/main/java/com/arangodb/springframework/repository/ArangoExampleConverter.java index e5f4b37cc..1c2eb3039 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoExampleConverter.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoExampleConverter.java @@ -20,6 +20,7 @@ package com.arangodb.springframework.repository; +import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.Map; import java.util.Optional; @@ -31,37 +32,39 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.util.Assert; +import com.arangodb.springframework.core.convert.resolver.ReferenceResolver; +import com.arangodb.springframework.core.convert.resolver.ResolverFactory; import com.arangodb.springframework.core.mapping.ArangoMappingContext; import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; /** - * Converts Example to String representing predicate expression and puts necessary bindings in the given bindVars Map + * Converts Example to String representing predicate expression and puts + * necessary bindings in the given bindVars Map */ public class ArangoExampleConverter { private final ArangoMappingContext context; + private final ResolverFactory resolverFactory; - public ArangoExampleConverter(final ArangoMappingContext context) { + public ArangoExampleConverter(final ArangoMappingContext context, final ResolverFactory resolverFactory) { this.context = context; + this.resolverFactory = resolverFactory; } public String convertExampleToPredicate(final Example example, final Map bindVars) { final StringBuilder predicateBuilder = new StringBuilder(); final ArangoPersistentEntity persistentEntity = context.getPersistentEntity(example.getProbeType()); Assert.isTrue(example.getProbe() != null, "Probe in Example cannot be null"); - traversePropertyTree(example, predicateBuilder, bindVars, "", "", persistentEntity, example.getProbe()); + final String bindEntintyName = "e"; + traversePropertyTree(example, predicateBuilder, bindVars, "", "", persistentEntity, example.getProbe(), + bindEntintyName); return predicateBuilder.toString(); } - private void traversePropertyTree( - final Example example, - final StringBuilder predicateBuilder, - final Map bindVars, - final String path, - final String javaPath, - final ArangoPersistentEntity entity, - final Object object) { + private void traversePropertyTree(final Example example, final StringBuilder predicateBuilder, + final Map bindVars, final String path, final String javaPath, + final ArangoPersistentEntity entity, final Object object, final String bindEntintyName) { final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(object); entity.doWithProperties((final ArangoPersistentProperty property) -> { if (property.getFrom().isPresent() || property.getTo().isPresent() || property.getRelations().isPresent()) { @@ -70,13 +73,35 @@ private void traversePropertyTree( final String fullPath = path + (path.length() == 0 ? "" : ".") + property.getFieldName(); final String fullJavaPath = javaPath + (javaPath.length() == 0 ? "" : ".") + property.getName(); final Object value = accessor.getProperty(property); - if (property.isEntity() && value != null) { + + if (property.isCollectionLike() && value != null) { + final ArangoPersistentEntity persistentEntity = context + .getPersistentEntity(property.getActualType()); + final StringBuilder predicateBuilderArray = new StringBuilder(); + for (Object item : (Iterable) value) { + final StringBuilder predicateBuilderArrayItem = new StringBuilder(); + traversePropertyTree(example, predicateBuilderArrayItem, bindVars, "", fullJavaPath, + persistentEntity, item, "CURRENT"); + if (predicateBuilderArray.length() > 0) { + predicateBuilderArray.append(" OR "); + } + predicateBuilderArray.append(predicateBuilderArrayItem.toString()); + } + final String delimiter = example.getMatcher().isAllMatching() ? " AND " : " OR "; + if (predicateBuilder.length() > 0) { + predicateBuilder.append(delimiter); + } + String clause = String.format("LENGTH(%s.%s[* FILTER %s ])>0", bindEntintyName, property.getName(), + predicateBuilderArray.toString()); + predicateBuilder.append(clause); + + } else if (property.isEntity() && value != null) { final ArangoPersistentEntity persistentEntity = context.getPersistentEntity(property.getType()); traversePropertyTree(example, predicateBuilder, bindVars, fullPath, fullJavaPath, persistentEntity, - value); + value, bindEntintyName); } else if (!example.getMatcher().isIgnoredPath(fullJavaPath) && (value != null || example.getMatcher().getNullHandler().equals(ExampleMatcher.NullHandler.INCLUDE))) { - addPredicate(example, predicateBuilder, bindVars, fullPath, fullJavaPath, value); + addPredicate(example, predicateBuilder, bindVars, fullPath, fullJavaPath, value, bindEntintyName); } }); @@ -91,20 +116,22 @@ private void traversePropertyTree( final ArangoPersistentEntity persistentEntity = context.getPersistentEntity(property.getType()); final PersistentPropertyAccessor associatedAccessor = persistentEntity.getPropertyAccessor(value); final Object idValue = associatedAccessor.getProperty(persistentEntity.getIdProperty()); - - String refIdValue = String.format("%s/%s", persistentEntity.getCollection(), idValue); - addPredicate(example, predicateBuilder, bindVars, fullPath, fullJavaPath, refIdValue); + String refIdValue = null; + if (property.getRef().isPresent()) { + final Optional> resolver = resolverFactory + .getReferenceResolver(property.getRef().get()); + refIdValue = resolver.get().write(value, persistentEntity, idValue, property.getRef().get()); + } else { + refIdValue = String.format("%s/%s", persistentEntity.getCollection(), idValue); + } + addPredicate(example, predicateBuilder, bindVars, fullPath, fullJavaPath, refIdValue, bindEntintyName); } }); } - private void addPredicate( - final Example example, - final StringBuilder predicateBuilder, - final Map bindVars, - final String fullPath, - final String fullJavaPath, - Object value) { + private void addPredicate(final Example example, final StringBuilder predicateBuilder, + final Map bindVars, final String fullPath, final String fullJavaPath, Object value, + final String bindEntintyName) { final String delimiter = example.getMatcher().isAllMatching() ? " AND " : " OR "; if (predicateBuilder.length() > 0) { predicateBuilder.append(delimiter); @@ -117,7 +144,7 @@ private void addPredicate( value = specifier.transformValue(Optional.of(value)).orElse(null); } if (value == null) { - clause = String.format("e.%s == null", fullPath); + clause = String.format("%s.%s == null", bindEntintyName, fullPath); } else if (String.class.isAssignableFrom(value.getClass())) { final boolean ignoreCase = specifier == null ? example.getMatcher().isIgnoreCaseEnabled() : (specifier.getIgnoreCase() == null ? false : specifier.getIgnoreCase()); @@ -127,10 +154,10 @@ private void addPredicate( : specifier.getStringMatcher(); final String string = (String) value; if (stringMatcher == ExampleMatcher.StringMatcher.REGEX) { - clause = String.format("REGEX_TEST(e.%s, @%s, %b)", fullPath, binding, ignoreCase); - } else { - clause = String.format("LIKE(e.%s, @%s, %b)", fullPath, binding, ignoreCase); - } + clause = String.format("REGEX_TEST(%s.%s, @%s, %b)", bindEntintyName, fullPath, binding, ignoreCase); + } else { + clause = String.format("LIKE(%s.%s, @%s, %b)", bindEntintyName, fullPath, binding, ignoreCase); + } switch (stringMatcher) { case STARTING: value = escape(string) + "%"; @@ -142,8 +169,8 @@ private void addPredicate( value = "%" + escape(string) + "%"; break; case REGEX: - value = escape(string); - break; + value = escape(string); + break; case DEFAULT: case EXACT: default: @@ -151,7 +178,7 @@ private void addPredicate( break; } } else { - clause = "e." + fullPath + " == @" + binding; + clause = String.format("%s.%s == @%s", bindEntintyName, fullPath, binding); } predicateBuilder.append(clause); if (value != null) { diff --git a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java index 8d618392c..397b31fa1 100644 --- a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java +++ b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java @@ -45,8 +45,9 @@ import com.arangodb.springframework.core.util.AqlUtils; /** - * The implementation of all CRUD, paging and sorting functionality in ArangoRepository from the Spring Data Commons - * CRUD repository and PagingAndSorting repository + * The implementation of all CRUD, paging and sorting functionality in + * ArangoRepository from the Spring Data Commons CRUD repository and + * PagingAndSorting repository */ @Repository @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -60,24 +61,23 @@ public class SimpleArangoRepository implements ArangoRepository { /** * - * @param arangoOperations - * The template used to execute much of the functionality of this class - * @param domainClass - * the class type of this repository + * @param arangoOperations The template used to execute much of the + * functionality of this class + * @param domainClass the class type of this repository */ public SimpleArangoRepository(final ArangoOperations arangoOperations, final Class domainClass) { super(); this.arangoOperations = arangoOperations; this.domainClass = domainClass; this.exampleConverter = new ArangoExampleConverter( - (ArangoMappingContext) arangoOperations.getConverter().getMappingContext()); + (ArangoMappingContext) arangoOperations.getConverter().getMappingContext(), + arangoOperations.getResolverFactory()); } /** * Saves the passed entity to the database using upsert from the template * - * @param entity - * the entity to be saved to the database + * @param entity the entity to be saved to the database * @return the updated entity with any id/key/rev saved */ @SuppressWarnings("deprecation") @@ -94,9 +94,9 @@ public S save(final S entity) { /** * Saves the given iterable of entities to the database * - * @param entities - * the iterable of entities to be saved to the database - * @return the iterable of updated entities with any id/key/rev saved in each entity + * @param entities the iterable of entities to be saved to the database + * @return the iterable of updated entities with any id/key/rev saved in each + * entity */ @SuppressWarnings("deprecation") @Override @@ -113,8 +113,7 @@ public Iterable saveAll(final Iterable entities) { /** * Finds if a document with the given id exists in the database * - * @param id - * the id of the document to search for + * @param id the id of the document to search for * @return the object representing the document if found */ @Override @@ -125,8 +124,7 @@ public Optional findById(final ID id) { /** * Checks if a document exists or not based on the given id or key * - * @param id - * represents either the key or id of a document to check for + * @param id represents either the key or id of a document to check for * @return returns true if the document is found, false otherwise */ @Override @@ -147,9 +145,9 @@ public Iterable findAll() { /** * Finds all documents with the an id or key in the argument * - * @param ids - * an iterable with ids/keys of documents to get - * @return an iterable with documents in the collection which have a id/key in the argument + * @param ids an iterable with ids/keys of documents to get + * @return an iterable with documents in the collection which have a id/key in + * the argument */ @Override public Iterable findAllById(final Iterable ids) { @@ -157,7 +155,8 @@ public Iterable findAllById(final Iterable ids) { } /** - * Counts the number of documents in the collection for the type of this repository + * Counts the number of documents in the collection for the type of this + * repository * * @return long with number of documents */ @@ -169,8 +168,7 @@ public long count() { /** * Deletes the document with the given id or key * - * @param id - * id or key of document to be deleted + * @param id id or key of document to be deleted */ @Override public void deleteById(final ID id) { @@ -178,10 +176,10 @@ public void deleteById(final ID id) { } /** - * Deletes document in the database representing the given object, by getting it's id + * Deletes document in the database representing the given object, by getting + * it's id * - * @param entity - * the entity to be deleted from the database + * @param entity the entity to be deleted from the database */ @Override public void delete(final T entity) { @@ -198,8 +196,7 @@ public void delete(final T entity) { /** * Deletes all the given documents from the database * - * @param entities - * iterable of entities to be deleted from the database + * @param entities iterable of entities to be deleted from the database */ @Override public void deleteAll(final Iterable entities) { @@ -215,10 +212,10 @@ public void deleteAll() { } /** - * Gets all documents in the collection for the class type of this repository, with the given sort applied + * Gets all documents in the collection for the class type of this repository, + * with the given sort applied * - * @param sort - * the sort object to use for sorting + * @param sort the sort object to use for sorting * @return an iterable with all the documents in the collection */ @Override @@ -232,10 +229,10 @@ public Iterator iterator() { } /** - * Gets all documents in the collection for the class type of this repository, with pagination + * Gets all documents in the collection for the class type of this repository, + * with pagination * - * @param pageable - * the pageable object to use for pagination of the results + * @param pageable the pageable object to use for pagination of the results * @return an iterable with all the documents in the collection */ @Override @@ -261,8 +258,7 @@ private String getCollectionName() { /** * Finds one document which matches the given example object * - * @param example - * example object to construct query with + * @param example example object to construct query with * @param * @return An object representing the example if it exists, else null */ @@ -275,8 +271,7 @@ public Optional findOne(final Example example) { /** * Finds all documents which match with the given example * - * @param example - * example object to construct query with + * @param example example object to construct query with * @param * @return iterable of all matching documents */ @@ -287,12 +282,11 @@ public Iterable findAll(final Example example) { } /** - * Finds all documents which match with the given example, then apply the given sort to results + * Finds all documents which match with the given example, then apply the given + * sort to results * - * @param example - * example object to construct query with - * @param sort - * sort object to sort results + * @param example example object to construct query with + * @param sort sort object to sort results * @param * @return sorted iterable of all matching documents */ @@ -305,10 +299,8 @@ public Iterable findAll(final Example example, final Sort so /** * Finds all documents which match with the given example, with pagination * - * @param example - * example object to construct query with - * @param pageable - * pageable object to apply pagination with + * @param example example object to construct query with + * @param pageable pageable object to apply pagination with * @param * @return iterable of all matching documents, with pagination */ @@ -320,10 +312,10 @@ public Page findAll(final Example example, final Pageable pa } /** - * Counts the number of documents in the collection which match with the given example + * Counts the number of documents in the collection which match with the given + * example * - * @param example - * example object to construct query with + * @param example example object to construct query with * @param * @return number of matching documents found */ @@ -333,7 +325,7 @@ public long count(final Example example) { final String predicate = exampleConverter.convertExampleToPredicate(example, bindVars); final String filter = predicate.length() == 0 ? "" : " FILTER " + predicate; final String query = String.format("FOR e IN %s%s COLLECT WITH COUNT INTO length RETURN length", - getCollectionName(), filter); + getCollectionName(), filter); arangoOperations.collection(domainClass); final ArangoCursor cursor = arangoOperations.query(query, bindVars, null, Long.class); return cursor.next(); @@ -351,25 +343,21 @@ public boolean exists(final Example example) { return count(example) > 0; } - private ArangoCursor findAllInternal( - final Sort sort, - @Nullable final Example example, - final Map bindVars) { + private ArangoCursor findAllInternal(final Sort sort, @Nullable final Example example, + final Map bindVars) { final String query = String.format("FOR e IN %s %s %s RETURN e", getCollectionName(), - buildFilterClause(example, bindVars), buildSortClause(sort, "e")); + buildFilterClause(example, bindVars), buildSortClause(sort, "e")); arangoOperations.collection(domainClass); return arangoOperations.query(query, bindVars, null, domainClass); } - private ArangoCursor findAllInternal( - final Pageable pageable, - @Nullable final Example example, - final Map bindVars) { + private ArangoCursor findAllInternal(final Pageable pageable, @Nullable final Example example, + final Map bindVars) { final String query = String.format("FOR e IN %s %s %s RETURN e", getCollectionName(), - buildFilterClause(example, bindVars), buildPageableClause(pageable, "e")); + buildFilterClause(example, bindVars), buildPageableClause(pageable, "e")); arangoOperations.collection(domainClass); return arangoOperations.query(query, bindVars, - pageable != null && pageable.isPaged() ? new AqlQueryOptions().fullCount(true) : null, domainClass); + pageable != null && pageable.isPaged() ? new AqlQueryOptions().fullCount(true) : null, domainClass); } private String buildFilterClause(final Example example, final Map bindVars) { diff --git a/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java b/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java index 9462a27e4..902c9dce9 100644 --- a/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java +++ b/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java @@ -7,6 +7,7 @@ import static org.junit.Assert.assertTrue; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -308,6 +309,71 @@ public void startingWithByExampleTest() { final Customer retrieved = repository.findOne(example).get(); assertEquals(check, retrieved); } + + @Test + public void findAllByExampleWithArrayTest() { + final List toBeRetrieved = new LinkedList<>(); + final Customer check = new Customer("Abba", "Bbaaaa", 100); + final Customer nested = new Customer("Bwa?[a.b]baAa", "", 67); + final Customer nested2 = new Customer("qwerty", "", 10); + check.setNestedCustomers(Arrays.asList(nested, nested2)); + toBeRetrieved.add(check); + toBeRetrieved.add(new Customer("Baabba", "", 67)); + toBeRetrieved.add(new Customer("B", "", 43)); + toBeRetrieved.add(new Customer("C", "", 76)); + repository.saveAll(toBeRetrieved); + final Customer exampleCustomer = new Customer("Abba", "Bbaaaa", 100); + final Customer exampleNested = new Customer("Bwa?[a.b]baAa", "", 67); + exampleCustomer.setNestedCustomers(Arrays.asList(exampleNested)); + final Example example = Example.of(exampleCustomer); + final Customer retrieved = repository.findOne(example).get(); + assertEquals(check, retrieved); + } + + @Test + public void findAllByExampleWithArray2Test() { + final List toBeRetrieved = new LinkedList<>(); + final Customer check = new Customer("Abba", "Bbaaaa", 100); + final Customer nested = new Customer("Bwa?[a.b]baAa", "", 67); + final Customer nested2 = new Customer("qwertyASD", "", 10); + check.setNestedCustomers(Arrays.asList(nested, nested2)); + toBeRetrieved.add(check); + toBeRetrieved.add(new Customer("Baabba", "", 67)); + toBeRetrieved.add(new Customer("B", "", 43)); + toBeRetrieved.add(new Customer("C", "", 76)); + repository.saveAll(toBeRetrieved); + final Customer exampleCustomer = new Customer(); + final Customer exampleNested = new Customer("qwertyASD", "", 10); + exampleCustomer.setNestedCustomers(Arrays.asList(exampleNested)); + final Example example = Example.of(exampleCustomer, ExampleMatcher.matching() + .withIgnoreNullValues().withIgnorePaths(new String[] { "location", "alive", "age" })); + final Customer retrieved = repository.findOne(example).get(); + assertEquals(check, retrieved); + } + + @Test + public void findAllByExampleWithArrayORTest() { + final List toBeRetrieved = new LinkedList<>(); + final Customer check = new Customer("Abba", "Bbaaaa", 100); + final Customer nested = new Customer("Bwa?[a.b]baAa", "", 67); + final Customer nested2 = new Customer("qwertyASD", "", 10); + check.setNestedCustomers(Arrays.asList(nested, nested2)); + toBeRetrieved.add(check); + toBeRetrieved.add(new Customer("Baabba", "", 67)); + toBeRetrieved.add(new Customer("B", "", 43)); + toBeRetrieved.add(new Customer("C", "", 76)); + repository.saveAll(toBeRetrieved); + final Customer exampleCustomer = new Customer(); + final Customer exampleNested = new Customer("qwertyASD", "", 10); + final Customer exampleOr = new Customer("qwertyOr", "", 10); + exampleCustomer.setNestedCustomers(Arrays.asList(exampleNested, exampleOr)); + final Example example = Example.of(exampleCustomer, ExampleMatcher.matching() + .withIgnoreNullValues().withIgnorePaths(new String[] { "location", "alive", "age" })); + final Customer retrieved = repository.findOne(example).get(); + assertEquals(check, retrieved); + } + + @Test public void endingWithByExampleNestedTest() {