diff --git a/src/main/java/com/arangodb/springframework/annotation/ArangoSearch.java b/src/main/java/com/arangodb/springframework/annotation/ArangoSearch.java new file mode 100644 index 000000000..54bc31702 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/annotation/ArangoSearch.java @@ -0,0 +1,99 @@ +/* + * 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.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.arangodb.entity.arangosearch.ConsolidationType; +import com.arangodb.entity.arangosearch.StoreValuesType; + +/** + * @author Mark Vollmary + * + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface ArangoSearch { + + /** + * @return The name of the arangosearch view + */ + String value() default ""; + + /** + * @return Wait at least this many milliseconds between committing index data changes and making them visible to + * queries (default: 60000, to disable use: 0). For the case where there are a lot of inserts/updates, a + * lower value, until commit, will cause the index not to account for them and memory usage would continue + * to grow. For the case where there are a few inserts/updates, a higher value will impact performance and + * waste disk space for each commit call without any added benefits. + */ + long consolidationIntervalMsec() default -1; + + /** + * @return Wait at least this many commits between removing unused files in data directory (default: 10, to disable + * use: 0). For the case where the consolidation policies merge segments often (i.e. a lot of + * commit+consolidate), a lower value will cause a lot of disk space to be wasted. For the case where the + * consolidation policies rarely merge segments (i.e. few inserts/deletes), a higher value will impact + * performance without any added benefits. + */ + long cleanupIntervalStep() default -1; + + ConsolidationType consolidationType() default ConsolidationType.BYTES_ACCUM; + + /** + * @return Select a given segment for "consolidation" if and only if the formula based on type (as defined above) + * evaluates to true, valid value range [0.0, 1.0] (default: 0.85) + */ + double consolidationThreshold() default -1; + + /** + * @return Apply the "consolidation" operation if and only if (default: 300): {segmentThreshold} < + * number_of_segments + */ + long consolidationSegmentThreshold() default -1; + + /** + * @return The list of analyzers to be used for indexing of string values (default: ["identity"]). + */ + String[] analyzers() default {}; + + /** + * @return The flag determines whether or not to index all fields on a particular level of depth (default: false). + */ + boolean includeAllFields() default false; + + /** + * @return The flag determines whether or not values in a lists should be treated separate (default: false). + */ + boolean trackListPositions() default false; + + /** + * @return How should the view track the attribute values, this setting allows for additional value retrieval + * optimizations (default "none"). + */ + StoreValuesType storeValues() default StoreValuesType.NONE; + +} diff --git a/src/main/java/com/arangodb/springframework/annotation/ArangoSearchLinked.java b/src/main/java/com/arangodb/springframework/annotation/ArangoSearchLinked.java new file mode 100644 index 000000000..137c5aaca --- /dev/null +++ b/src/main/java/com/arangodb/springframework/annotation/ArangoSearchLinked.java @@ -0,0 +1,60 @@ +/* + * 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 com.arangodb.entity.arangosearch.StoreValuesType; + +/** + * @author Mark Vollmary + * + */ + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +public @interface ArangoSearchLinked { + + /** + * @return The list of analyzers to be used for indexing of string values (default: ["identity"]). + */ + String[] analyzers() default {}; + + /** + * @return The flag determines whether or not to index all fields on a particular level of depth (default: false). + */ + boolean includeAllFields() default false; + + /** + * @return The flag determines whether or not values in a lists should be treated separate (default: false). + */ + boolean trackListPositions() default false; + + /** + * @return How should the view track the attribute values, this setting allows for additional value retrieval + * optimizations (default "none"). + */ + StoreValuesType storeValues() default StoreValuesType.NONE; + +} diff --git a/src/main/java/com/arangodb/springframework/config/ArangoEntityClassScanner.java b/src/main/java/com/arangodb/springframework/config/ArangoEntityClassScanner.java index 8f5fb0af3..acbdc6a5c 100644 --- a/src/main/java/com/arangodb/springframework/config/ArangoEntityClassScanner.java +++ b/src/main/java/com/arangodb/springframework/config/ArangoEntityClassScanner.java @@ -31,6 +31,7 @@ import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import com.arangodb.springframework.annotation.ArangoSearch; import com.arangodb.springframework.annotation.Document; import com.arangodb.springframework.annotation.Edge; @@ -42,8 +43,9 @@ public class ArangoEntityClassScanner { @SuppressWarnings("unchecked") - private static final Class[] ENTITY_ANNOTATIONS = new Class[] { Document.class, Edge.class }; - + private static final Class[] ENTITY_ANNOTATIONS = new Class[] { Document.class, Edge.class, + ArangoSearch.class }; + @SuppressWarnings("unchecked") private static final Class[] ADDITIONAL_ANNOTATIONS = new Class[] { TypeAlias.class }; diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index ebd8b717a..7a0e21b4d 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -38,6 +38,7 @@ import com.arangodb.model.DocumentReadOptions; import com.arangodb.model.DocumentReplaceOptions; import com.arangodb.model.DocumentUpdateOptions; +import com.arangodb.model.arangosearch.ArangoSearchCreateOptions; import com.arangodb.springframework.core.convert.ArangoConverter; /** @@ -561,6 +562,12 @@ public enum UpsertStrategy { */ CollectionOperations collection(String name, CollectionCreateOptions options) throws DataAccessException; + ArangoSearchOperations arangosearch(Class entityClass) throws DataAccessException; + + ArangoSearchOperations arangosearch(String name) throws DataAccessException; + + ArangoSearchOperations arangosearch(String name, ArangoSearchCreateOptions options) throws DataAccessException; + /** * Return the operations interface for a user. The user is not created automatically if it does not exists. * diff --git a/src/main/java/com/arangodb/springframework/core/ArangoSearchOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoSearchOperations.java new file mode 100644 index 000000000..130aff0f4 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/ArangoSearchOperations.java @@ -0,0 +1,84 @@ +/* + * 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; + +import org.springframework.dao.DataAccessException; + +import com.arangodb.entity.arangosearch.ArangoSearchPropertiesEntity; +import com.arangodb.model.arangosearch.ArangoSearchPropertiesOptions; + +/** + * @author Mark Vollmary + * + */ +public interface ArangoSearchOperations { + + /** + * Return the view name + * + * @return view name + */ + String name(); + + /** + * Deletes the view from the database. + * + * @throws DataAccessException + */ + void drop() throws DataAccessException; + + /** + * Reads the properties of the specified view. + * + * @see API + * Documentation + * @return properties of the view + * @throws DataAccessException + */ + ArangoSearchPropertiesEntity getProperties() throws DataAccessException; + + /** + * Partially changes properties of the view. + * + * @see API + * Documentation + * @param options + * properties to change + * @return properties of the view + * @throws DataAccessException + */ + ArangoSearchPropertiesEntity updateProperties(ArangoSearchPropertiesOptions options) throws DataAccessException; + + /** + * Changes properties of the view. + * + * @see API + * Documentation + * @param options + * properties to change + * @return properties of the view + * @throws DataAccessException + */ + ArangoSearchPropertiesEntity replaceProperties(ArangoSearchPropertiesOptions options) throws DataAccessException; + +} 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 8784fa6ea..e00aae70c 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentEntity.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentEntity.java @@ -27,7 +27,9 @@ import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.mapping.PersistentEntity; +import com.arangodb.entity.arangosearch.CollectionLink; import com.arangodb.model.CollectionCreateOptions; +import com.arangodb.model.arangosearch.ArangoSearchCreateOptions; import com.arangodb.springframework.annotation.FulltextIndex; import com.arangodb.springframework.annotation.GeoIndex; import com.arangodb.springframework.annotation.HashIndex; @@ -46,6 +48,12 @@ public interface ArangoPersistentEntity CollectionCreateOptions getCollectionOptions(); + Optional getArangoSearch(); + + Optional getArangoSearchOptions(); + + Optional getArangoSearchLinkOptions(); + Optional getArangoIdProperty(); Optional getRevProperty(); 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 9f8fb36c7..599a34ccc 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentProperty.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/ArangoPersistentProperty.java @@ -24,6 +24,7 @@ import org.springframework.data.mapping.PersistentProperty; +import com.arangodb.springframework.annotation.ArangoSearchLinked; import com.arangodb.springframework.annotation.From; import com.arangodb.springframework.annotation.FulltextIndexed; import com.arangodb.springframework.annotation.GeoIndexed; @@ -64,4 +65,6 @@ public interface ArangoPersistentProperty extends PersistentProperty getFulltextIndexed(); + Optional getArangoSearchLinked(); + } 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 223daf575..369da0ca8 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentEntity.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentEntity.java @@ -48,7 +48,12 @@ import org.springframework.util.StringUtils; import com.arangodb.entity.CollectionType; +import com.arangodb.entity.arangosearch.CollectionLink; +import com.arangodb.entity.arangosearch.ConsolidationPolicy; +import com.arangodb.entity.arangosearch.FieldLink; import com.arangodb.model.CollectionCreateOptions; +import com.arangodb.model.arangosearch.ArangoSearchCreateOptions; +import com.arangodb.springframework.annotation.ArangoSearch; import com.arangodb.springframework.annotation.Document; import com.arangodb.springframework.annotation.Edge; import com.arangodb.springframework.annotation.FulltextIndex; @@ -72,8 +77,11 @@ public class DefaultArangoPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity persistentIndexedProperties; private final Collection geoIndexedProperties; private final Collection fulltextIndexedProperties; + private final Collection arangosearchLinkedProperties; private final CollectionCreateOptions collectionOptions; + private final Optional arangosearchOptions; + private Optional arangosearchLinkOptions; private final Map, Set> repeatableAnnotationCache; public DefaultArangoPersistentEntity(final TypeInformation information) { super(information); - collection = StringUtils.uncapitalize(information.getType().getSimpleName()); context = new StandardEvaluationContext(); hashIndexedProperties = new ArrayList<>(); skiplistIndexedProperties = new ArrayList<>(); persistentIndexedProperties = new ArrayList<>(); geoIndexedProperties = new ArrayList<>(); fulltextIndexedProperties = new ArrayList<>(); + arangosearchLinkedProperties = new ArrayList<>(); repeatableAnnotationCache = new HashMap<>(); final Document document = findAnnotation(Document.class); final Edge edge = findAnnotation(Edge.class); + final ArangoSearch as = findAnnotation(ArangoSearch.class); + final String simpleTypeName = StringUtils.uncapitalize(information.getType().getSimpleName()); if (edge != null) { - collection = StringUtils.hasText(edge.value()) ? edge.value() : collection; + collection = StringUtils.hasText(edge.value()) ? edge.value() : simpleTypeName; collectionOptions = createCollectionOptions(edge); + collectionExpression = PARSER.parseExpression(collection, ParserContext.TEMPLATE_EXPRESSION); } else if (document != null) { - collection = StringUtils.hasText(document.value()) ? document.value() : collection; + collection = StringUtils.hasText(document.value()) ? document.value() : simpleTypeName; collectionOptions = createCollectionOptions(document); - } else { + collectionExpression = PARSER.parseExpression(collection, ParserContext.TEMPLATE_EXPRESSION); + } else if (as == null) { + collection = simpleTypeName; collectionOptions = new CollectionCreateOptions().type(CollectionType.DOCUMENT); + collectionExpression = PARSER.parseExpression(collection, ParserContext.TEMPLATE_EXPRESSION); + } else { + collection = null; + collectionOptions = null; + collectionExpression = null; + } + if (as != null) { + arangosearch = StringUtils.hasText(as.value()) ? as.value() : simpleTypeName; + arangosearchExpression = PARSER.parseExpression(arangosearch, ParserContext.TEMPLATE_EXPRESSION); + arangosearchOptions = Optional.ofNullable(createArangoSearchOptions(as)); + } else { + arangosearch = null; + arangosearchExpression = null; + arangosearchOptions = Optional.empty(); } - expression = PARSER.parseExpression(collection, ParserContext.TEMPLATE_EXPRESSION); + arangosearchLinkOptions = null; } private static CollectionCreateOptions createCollectionOptions(final Document annotation) { @@ -173,7 +203,71 @@ private static CollectionCreateOptions createCollectionOptions(final Edge annota @Override public String getCollection() { - return expression != null ? expression.getValue(context, String.class) : collection; + return collectionExpression != null ? collectionExpression.getValue(context, String.class) : collection; + } + + private static ArangoSearchCreateOptions createArangoSearchOptions(final ArangoSearch arangosearch) { + final ArangoSearchCreateOptions options = new ArangoSearchCreateOptions(); + final long consolidationIntervalMsec = arangosearch.consolidationIntervalMsec(); + if (consolidationIntervalMsec > -1) { + options.consolidationIntervalMsec(consolidationIntervalMsec); + } + final long cleanupIntervalStep = arangosearch.cleanupIntervalStep(); + if (cleanupIntervalStep > -1) { + options.cleanupIntervalStep(cleanupIntervalStep); + } + final double consolidationThreshold = arangosearch.consolidationThreshold(); + final long consolidationSegmentThreshold = arangosearch.consolidationSegmentThreshold(); + if (consolidationThreshold > -1 || consolidationSegmentThreshold > -1) { + final ConsolidationPolicy consolidationPolicy = ConsolidationPolicy.of(arangosearch.consolidationType()); + options.consolidationPolicy(consolidationPolicy); + if (consolidationThreshold > -1) { + consolidationPolicy.threshold(consolidationThreshold); + } + if (consolidationSegmentThreshold > -1) { + consolidationPolicy.segmentThreshold(consolidationSegmentThreshold); + } + } + return options; + } + + private CollectionLink createArangosearchLinkOptions() { + final String collection = getCollection(); + if (collection == null) { + return null; + } + final ArangoSearch as = findAnnotation(ArangoSearch.class); + if (as == null) { + return null; + } + final CollectionLink options = CollectionLink.on(collection); + final String[] analyzers = as.analyzers(); + if (analyzers.length > 0) { + options.analyzers(analyzers); + } + options.includeAllFields(as.includeAllFields()); + options.trackListPositions(as.trackListPositions()); + options.storeValues(as.storeValues()); + arangosearchLinkedProperties.stream().forEach(property -> { + final FieldLink fieldLink = FieldLink.on(property.getFieldName()); + property.getArangoSearchLinked().ifPresent(link -> { + final String[] linkAnalyzers = link.analyzers(); + if (linkAnalyzers.length > 0) { + fieldLink.analyzers(linkAnalyzers); + } + fieldLink.includeAllFields(link.includeAllFields()); + fieldLink.trackListPositions(link.trackListPositions()); + fieldLink.storeValues(link.storeValues()); + }); + options.fields(fieldLink); + }); + return options; + } + + @Override + public Optional getArangoSearch() { + return Optional.ofNullable( + arangosearchExpression != null ? arangosearchExpression.getValue(context, String.class) : arangosearch); } @Override @@ -197,6 +291,7 @@ public void addPersistentProperty(final ArangoPersistentProperty property) { property.getPersistentIndexed().ifPresent(i -> persistentIndexedProperties.add(property)); property.getGeoIndexed().ifPresent(i -> geoIndexedProperties.add(property)); property.getFulltextIndexed().ifPresent(i -> fulltextIndexedProperties.add(property)); + property.getArangoSearchLinked().ifPresent(i -> arangosearchLinkedProperties.add(property)); } @Override @@ -214,6 +309,19 @@ public CollectionCreateOptions getCollectionOptions() { return collectionOptions; } + @Override + public Optional getArangoSearchOptions() { + return arangosearchOptions; + } + + @Override + public Optional getArangoSearchLinkOptions() { + if (arangosearchLinkOptions == null) { + arangosearchLinkOptions = Optional.ofNullable(createArangosearchLinkOptions()); + } + return arangosearchLinkOptions; + } + @Override public Collection getHashIndexes() { final Collection indexes = getIndexes(HashIndex.class); 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 6129413e5..40ed1ce6e 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentProperty.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentProperty.java @@ -32,6 +32,7 @@ import org.springframework.util.StringUtils; import com.arangodb.springframework.annotation.ArangoId; +import com.arangodb.springframework.annotation.ArangoSearchLinked; import com.arangodb.springframework.annotation.Field; import com.arangodb.springframework.annotation.From; import com.arangodb.springframework.annotation.FulltextIndexed; @@ -145,4 +146,9 @@ public Optional getFulltextIndexed() { return Optional.ofNullable(findAnnotation(FulltextIndexed.class)); } + @Override + public Optional getArangoSearchLinked() { + return Optional.ofNullable(findAnnotation(ArangoSearchLinked.class)); + } + } diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoSearchCacheValue.java b/src/main/java/com/arangodb/springframework/core/template/ArangoSearchCacheValue.java new file mode 100644 index 000000000..51854efb7 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoSearchCacheValue.java @@ -0,0 +1,31 @@ +package com.arangodb.springframework.core.template; + +import java.util.ArrayList; +import java.util.Collection; + +import com.arangodb.ArangoSearch; + +class ArangoSearchCacheValue { + + private final ArangoSearch view; + private final Collection> entities; + + public ArangoSearchCacheValue(final ArangoSearch view) { + super(); + this.view = view; + this.entities = new ArrayList<>(); + } + + public ArangoSearch getView() { + return view; + } + + public Collection> getEntities() { + return entities; + } + + public void addEntityClass(final Class entityClass) { + entities.add(entityClass); + } + +} \ No newline at end of file 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 69786e808..30d73b4dc 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -53,6 +53,7 @@ import com.arangodb.ArangoDB; import com.arangodb.ArangoDBException; import com.arangodb.ArangoDatabase; +import com.arangodb.ArangoSearch; import com.arangodb.entity.ArangoDBVersion; import com.arangodb.entity.DocumentEntity; import com.arangodb.entity.MultiDocumentEntity; @@ -69,12 +70,15 @@ import com.arangodb.model.HashIndexOptions; import com.arangodb.model.PersistentIndexOptions; import com.arangodb.model.SkiplistIndexOptions; +import com.arangodb.model.arangosearch.ArangoSearchCreateOptions; +import com.arangodb.model.arangosearch.ArangoSearchPropertiesOptions; 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.ArangoSearchOperations; import com.arangodb.springframework.core.CollectionOperations; import com.arangodb.springframework.core.UserOperations; import com.arangodb.springframework.core.convert.ArangoConverter; @@ -109,6 +113,7 @@ public class ArangoTemplate implements ArangoOperations, CollectionCallback, App private final Expression databaseExpression; private final Map databaseCache; private final Map collectionCache; + private final Map arangosearchCache; private final StandardEvaluationContext context; @@ -133,6 +138,7 @@ public ArangoTemplate(final ArangoDB arango, final String database, final Arango this.context = new StandardEvaluationContext(); // set concurrency level to 1 as writes are very rare compared to reads collectionCache = new ConcurrentHashMap<>(8, 0.9f, 1); + arangosearchCache = new ConcurrentHashMap<>(8, 0.9f, 1); databaseCache = new ConcurrentHashMap<>(8, 0.9f, 1); version = null; } @@ -188,6 +194,10 @@ private ArangoCollection _collection( if (persistentEntity != null && !entities.contains(entityClass)) { value.addEntityClass(entityClass); ensureCollectionIndexes(collection(collection), persistentEntity); + persistentEntity.getArangoSearch().ifPresent(arangosearch -> { + // also create arangosearch view if not already created + _arangosearch(arangosearch, persistentEntity, persistentEntity.getArangoSearchOptions().orElse(null)); + }); } return collection; } @@ -743,6 +753,8 @@ public void dropDatabase() throws DataAccessException { databaseCache.remove(db.name()); collectionCache.keySet().stream().filter(key -> key.getDb().equals(db.name())) .forEach(key -> collectionCache.remove(key)); + arangosearchCache.keySet().stream().filter(key -> key.getDb().equals(db.name())) + .forEach(key -> arangosearchCache.remove(key)); } @Override @@ -765,6 +777,72 @@ private CollectionOperations collection(final ArangoCollection collection) { return new DefaultCollectionOperations(collection, collectionCache, exceptionTranslator); } + @Override + public ArangoSearchOperations arangosearch(final Class entityClass) throws DataAccessException { + return arangosearch(_arangosearch(entityClass)); + } + + @Override + public ArangoSearchOperations arangosearch(final String name) throws DataAccessException { + return arangosearch(_arangosearch(name, null, null)); + } + + @Override + public ArangoSearchOperations arangosearch(final String name, final ArangoSearchCreateOptions options) + throws DataAccessException { + return arangosearch(_arangosearch(name, null, options)); + } + + private ArangoSearchOperations arangosearch(final ArangoSearch view) { + return new DefaultArangoSearchOperations(view, arangosearchCache, exceptionTranslator); + } + + private ArangoSearch _arangosearch(final Class entityClass) { + final ArangoPersistentEntity persistentEntity = converter.getMappingContext() + .getRequiredPersistentEntity(entityClass); + return persistentEntity.getArangoSearch().map(arangosearch -> { + return _arangosearch(arangosearch, persistentEntity, + persistentEntity.getArangoSearchOptions().orElse(null)); + }).orElse(null); + + } + + private ArangoSearch _arangosearch( + final String name, + final ArangoPersistentEntity persistentEntity, + final ArangoSearchCreateOptions options) { + + final ArangoDatabase db = db(); + final Class entityClass = persistentEntity != null ? persistentEntity.getType() : null; + final ArangoSearchCacheValue value = arangosearchCache.computeIfAbsent(new CollectionCacheKey(db.name(), name), + key -> { + final ArangoSearch view = db.arangoSearch(name); + if (!view.exists()) { + view.create(options); + } + return new ArangoSearchCacheValue(view); + }); + final Collection> entities = value.getEntities(); + final ArangoSearch view = value.getView(); + if (persistentEntity != null && !entities.contains(entityClass)) { + value.addEntityClass(entityClass); + ensureArangoSearchLinks(arangosearch(view), persistentEntity); + if (persistentEntity.getCollection() != null) { + // also create collection if not already created + _collection(persistentEntity.getCollection(), persistentEntity, + persistentEntity.getCollectionOptions()); + } + } + return view; + } + + private void ensureArangoSearchLinks( + final ArangoSearchOperations arangosearch, + final ArangoPersistentEntity persistentEntity) { + persistentEntity.getArangoSearchLinkOptions().ifPresent( + linkOptions -> arangosearch.updateProperties(new ArangoSearchPropertiesOptions().link(linkOptions))); + } + @Override public UserOperations user(final String username) { return new DefaultUserOperation(db(), username, exceptionTranslator, this); diff --git a/src/main/java/com/arangodb/springframework/core/template/DefaultArangoSearchOperations.java b/src/main/java/com/arangodb/springframework/core/template/DefaultArangoSearchOperations.java new file mode 100644 index 000000000..3301b3e97 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/template/DefaultArangoSearchOperations.java @@ -0,0 +1,100 @@ +/* + * 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.template; + +import java.util.Map; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; + +import com.arangodb.ArangoDBException; +import com.arangodb.ArangoSearch; +import com.arangodb.entity.arangosearch.ArangoSearchPropertiesEntity; +import com.arangodb.model.arangosearch.ArangoSearchPropertiesOptions; +import com.arangodb.springframework.core.ArangoSearchOperations; + +/** + * @author Mark Vollmary + * + */ +public class DefaultArangoSearchOperations implements ArangoSearchOperations { + + private final ArangoSearch view; + private final Map viewCache; + private final PersistenceExceptionTranslator exceptionTranslator; + + protected DefaultArangoSearchOperations(final ArangoSearch view, + final Map viewCache, + final PersistenceExceptionTranslator exceptionTranslator) { + this.view = view; + this.viewCache = viewCache; + this.exceptionTranslator = exceptionTranslator; + } + + private DataAccessException translateExceptionIfPossible(final RuntimeException exception) { + return exceptionTranslator.translateExceptionIfPossible(exception); + } + + @Override + public String name() { + return view.name(); + } + + @Override + public void drop() throws DataAccessException { + viewCache.remove(new CollectionCacheKey(view.db().name(), view.name())); + try { + view.drop(); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public ArangoSearchPropertiesEntity getProperties() throws DataAccessException { + try { + return view.getProperties(); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public ArangoSearchPropertiesEntity updateProperties(final ArangoSearchPropertiesOptions options) + throws DataAccessException { + try { + return view.updateProperties(options); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public ArangoSearchPropertiesEntity replaceProperties(final ArangoSearchPropertiesOptions options) + throws DataAccessException { + try { + return view.replaceProperties(options); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + +} diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java index 379f2142a..b99b559f3 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java @@ -21,6 +21,8 @@ package com.arangodb.springframework.repository; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import org.springframework.data.mapping.context.MappingContext; @@ -67,23 +69,42 @@ public ArangoRepositoryFactory(final ArangoOperations arangoOperations) { @SuppressWarnings("unchecked") @Override public ArangoEntityInformation getEntityInformation(final Class domainClass) { - return new ArangoPersistentEntityInformation( + return new ArangoPersistentEntityInformation<>( (ArangoPersistentEntity) context.getRequiredPersistentEntity(domainClass)); } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override protected Object getTargetRepository(final RepositoryInformation metadata) { - return new SimpleArangoRepository(arangoOperations, metadata.getDomainType()); + final Class repositoryBaseClass = metadata.getRepositoryBaseClass(); + final Repository repository; + if (repositoryBaseClass == SimpleArangoRepository.class) { + repository = new SimpleArangoRepository(arangoOperations, metadata.getDomainType()); + } else if (repositoryBaseClass == SimpleArangoSearchRepository.class) { + repository = new SimpleArangoSearchRepository(arangoOperations, metadata.getDomainType()); + } else { + repository = null; + } + return repository; } @Override protected Class getRepositoryBaseClass(final RepositoryMetadata metadata) { - return SimpleArangoRepository.class; + final Class repositoryInterface = metadata.getRepositoryInterface(); + final List> interfaces = Arrays.asList(repositoryInterface.getInterfaces()); + final Class repositoryBaseClass; + if (interfaces.contains(ArangoRepository.class)) { + repositoryBaseClass = SimpleArangoRepository.class; + } else if (interfaces.contains(ArangoSearchRepository.class)) { + repositoryBaseClass = SimpleArangoSearchRepository.class; + } else { + repositoryBaseClass = null; + } + return repositoryBaseClass; } @Override - protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { + protected RepositoryMetadata getRepositoryMetadata(final Class repositoryInterface) { Assert.notNull(repositoryInterface, "Repository interface must not be null!"); return Repository.class.isAssignableFrom(repositoryInterface) @@ -143,13 +164,13 @@ static class DefaultArangoRepositoryMetadata extends DefaultRepositoryMetadata { private final TypeInformation typeInformation; - public DefaultArangoRepositoryMetadata(Class repositoryInterface) { + public DefaultArangoRepositoryMetadata(final Class repositoryInterface) { super(repositoryInterface); typeInformation = ClassTypeInformation.from(repositoryInterface); } @Override - public Class getReturnedDomainClass(Method method) { + public Class getReturnedDomainClass(final Method method) { if (ArangoCursor.class.isAssignableFrom(method.getReturnType())) { return typeInformation.getReturnType(method).getRequiredComponentType().getType(); } else { @@ -163,13 +184,13 @@ static class AnnotationArangoRepositoryMetadata extends AnnotationRepositoryMeta private final TypeInformation typeInformation; - public AnnotationArangoRepositoryMetadata(Class repositoryInterface) { + public AnnotationArangoRepositoryMetadata(final Class repositoryInterface) { super(repositoryInterface); typeInformation = ClassTypeInformation.from(repositoryInterface); } @Override - public Class getReturnedDomainClass(Method method) { + public Class getReturnedDomainClass(final Method method) { if (ArangoCursor.class.isAssignableFrom(method.getReturnType())) { return typeInformation.getReturnType(method).getRequiredComponentType().getType(); } else { diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoSearchRepository.java b/src/main/java/com/arangodb/springframework/repository/ArangoSearchRepository.java new file mode 100644 index 000000000..09eb692aa --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/ArangoSearchRepository.java @@ -0,0 +1,99 @@ +/* + * 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.repository; + +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.QueryByExampleExecutor; + +/** + * @author Mark Vollmary + * + */ +@NoRepositoryBean +public interface ArangoSearchRepository extends QueryByExampleExecutor, Repository { + + /** + * Retrieves an entity by its id. + * + * @param id + * must not be {@literal null}. + * @return the entity with the given id or {@literal Optional#empty()} if none found + * @throws IllegalArgumentException + * if {@code id} is {@literal null}. + */ + Optional findById(String id); + + /** + * Returns whether an entity with the given id exists. + * + * @param id + * must not be {@literal null}. + * @return {@literal true} if an entity with the given id exists, {@literal false} otherwise. + * @throws IllegalArgumentException + * if {@code id} is {@literal null}. + */ + boolean existsById(String id); + + /** + * Returns all instances of the type. + * + * @return all entities + */ + Iterable findAll(); + + /** + * Returns all instances of the type with the given IDs. + * + * @param ids + * @return + */ + Iterable findAllById(Iterable ids); + + /** + * Returns the number of entities available. + * + * @return the number of entities + */ + long count(); + + /** + * Returns all entities sorted by the given options. + * + * @param sort + * @return all entities sorted by the given options + */ + Iterable findAll(Sort sort); + + /** + * Returns a {@link Page} of entities meeting the paging restriction provided in the {@code Pageable} object. + * + * @param pageable + * @return a page of entities + */ + Page findAll(Pageable pageable); + +} diff --git a/src/main/java/com/arangodb/springframework/repository/SimpleArangoSearchRepository.java b/src/main/java/com/arangodb/springframework/repository/SimpleArangoSearchRepository.java new file mode 100644 index 000000000..57259f5d0 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/SimpleArangoSearchRepository.java @@ -0,0 +1,189 @@ +/* + * 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.repository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Repository; + +import com.arangodb.ArangoCursor; +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.core.mapping.ArangoMappingContext; +import com.arangodb.springframework.core.util.AqlUtils; +import com.arangodb.util.MapBuilder; + +/** + * @author Mark Vollmary + * + */ +@Repository +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class SimpleArangoSearchRepository implements ArangoSearchRepository { + + private final ArangoExampleConverter exampleConverter; + private final ArangoOperations arangoOperations; + private final Class domainClass; + + public SimpleArangoSearchRepository(final ArangoOperations arangoOperations, final Class domainClass) { + super(); + this.arangoOperations = arangoOperations; + this.domainClass = domainClass; + this.exampleConverter = new ArangoExampleConverter( + (ArangoMappingContext) arangoOperations.getConverter().getMappingContext()); + } + + @Override + public Optional findById(final String id) { + return Optional.ofNullable(arangoOperations.query("FOR e IN @@view FILTER e._id == @id LIMIT 1 RETURN e", + new MapBuilder().put("@view", getViewName()).put("id", id).get(), domainClass).first()); + } + + @Override + public boolean existsById(final String id) { + return arangoOperations + .query("FOR e IN @@view FILTER e._id == @id LIMIT 1 RETURN true", + new MapBuilder().put("@view", getViewName()).put("id", id).get(), Boolean.class) + .first() == Boolean.TRUE; + } + + @Override + public Iterable findAll() { + return arangoOperations.query("FOR e IN @@view RETURN e", new MapBuilder().put("@view", getViewName()).get(), + domainClass); + } + + @Override + public Iterable findAllById(final Iterable ids) { + return arangoOperations.query("FOR e IN @@view FILTER e._id IN @ids RETURN e", + new MapBuilder().put("@view", getViewName()).put("ids", ids).get(), domainClass); + } + + @Override + public long count() { + final Map bindVars = new MapBuilder().put("@view", getViewName()).get(); + final String query = "FOR e IN @@view COLLECT WITH COUNT INTO length RETURN length"; + return arangoOperations.query(query, bindVars, Long.class).first(); + } + + @Override + public Iterable findAll(final Sort sort) { + return _findAll(sort, null); + } + + @Override + public Page findAll(final Pageable pageable) { + final ArangoCursor cursor = _findAll(pageable, null); + return new PageImpl<>(cursor.asListRemaining(), pageable, cursor.getStats().getFullCount()); + } + + @Override + public Optional findOne(final Example example) { + final Map bindVars = new MapBuilder().put("@view", getViewName()).get(); + final String query = "FOR e IN @@view " + buildSearchClause(example, bindVars) + " LIMIT 1 RETURN e"; + return Optional.ofNullable((S) arangoOperations.query(query, bindVars, domainClass).first()); + } + + @Override + public Iterable findAll(final Example example) { + return _findAll((Pageable) null, example); + } + + @Override + public Iterable findAll(final Example example, final Sort sort) { + return _findAll(sort, example); + } + + @Override + public Page findAll(final Example example, final Pageable pageable) { + final ArangoCursor cursor = _findAll(pageable, example); + return new PageImpl<>(cursor.asListRemaining(), pageable, cursor.getStats().getFullCount()); + } + + @Override + public long count(final Example example) { + final Map bindVars = new MapBuilder().put("@view", getViewName()).get(); + final String query = "FOR e IN @@view " + buildSearchClause(example, bindVars) + + " COLLECT WITH COUNT INTO length RETURN length"; + return arangoOperations.query(query, bindVars, Long.class).first(); + } + + @Override + public boolean exists(final Example example) { + final Map bindVars = new MapBuilder().put("@view", getViewName()).get(); + final String query = "FOR e IN @@view " + buildSearchClause(example, bindVars) + " LIMIT 1 RETURN true"; + return arangoOperations.query(query, bindVars, Boolean.class).first() == Boolean.TRUE; + } + + private ArangoCursor _findAll(final Sort sort, @Nullable final Example example) { + final Map bindVars = new HashMap<>(); + bindVars.put("@view", getViewName()); + + final String query = "FOR e IN @@view " + buildSearchClause(example, bindVars) + " " + + buildSortClause(sort, "e") + " RETURN e"; + final ArangoCursor cursor = arangoOperations.query(query, bindVars, domainClass); + return cursor; + } + + private ArangoCursor _findAll(final Pageable pageable, @Nullable final Example example) { + final Map bindVars = new HashMap<>(); + bindVars.put("@view", getViewName()); + + final String query = "FOR e IN @@view " + buildSearchClause(example, bindVars) + " " + + buildPageableClause(pageable, "e") + " RETURN e"; + final AqlQueryOptions options = new AqlQueryOptions(); + if (pageable != null && pageable.isPaged()) { + options.fullCount(true); + } + final ArangoCursor cursor = arangoOperations.query(query, bindVars, options, domainClass); + return cursor; + } + + private String buildSearchClause(final Example example, final Map bindVars) { + if (example == null) { + return ""; + } + + final String predicate = exampleConverter.convertExampleToPredicate(example, bindVars); + return predicate == null ? "" : "FILTER " + predicate; + } + + private String buildPageableClause(final Pageable pageable, final String varName) { + return pageable == null ? "" : AqlUtils.buildPageableClause(pageable, varName); + } + + private String buildSortClause(final Sort sort, final String varName) { + return sort == null ? "" : AqlUtils.buildSortClause(sort, varName); + } + + private String getViewName() { + return arangoOperations.getConverter().getMappingContext().getPersistentEntity(domainClass).getArangoSearch() + .get(); + } +} diff --git a/src/test/java/com/arangodb/springframework/repository/ArangoSearchRepositoryTest.java b/src/test/java/com/arangodb/springframework/repository/ArangoSearchRepositoryTest.java new file mode 100644 index 000000000..8b76b4b66 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/repository/ArangoSearchRepositoryTest.java @@ -0,0 +1,193 @@ +/* + * 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.repository; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +import com.arangodb.springframework.AbstractArangoTest; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.testdata.ArangoSearchTestEntity; + +/** + * @author Mark Vollmary + * + */ + +public class ArangoSearchRepositoryTest extends AbstractArangoTest { + + @Autowired + ArangoOperations operations; + + @Autowired + ArangoSearchTestRepo repository; + + public ArangoSearchRepositoryTest() { + super(); + } + + @Test + public void findById() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final Optional find = repository.findById(entity.getId()); + assertThat(find.isPresent(), is(true)); + assertThat(find.get().getValue(), is("test")); + } + + @Test + public void query() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final List find = repository.findByValue("test"); + assertThat(find.size(), is(1)); + assertThat(find.get(0).getValue(), is("test")); + } + + @Test + public void findAll() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final Iterable find = repository.findAll(); + final Iterator iterator = find.iterator(); + assertThat(iterator.next().getValue(), is("test")); + assertThat(iterator.hasNext(), is(false)); + } + + @Test + public void findAllById() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final Iterable find = repository.findAllById(Arrays.asList(entity.getId())); + final Iterator iterator = find.iterator(); + assertThat(iterator.next().getValue(), is("test")); + assertThat(iterator.hasNext(), is(false)); + } + + @Test + public void count() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final long count = repository.count(); + assertThat(count, is(1L)); + } + + @Test + public void findAllWithSort() throws InterruptedException { + operations.insert(Arrays.asList(new ArangoSearchTestEntity("test2"), new ArangoSearchTestEntity("test1")), + ArangoSearchTestEntity.class); + Thread.sleep(1000); + final Iterable find = repository.findAll(Sort.by("value")); + final Iterator iterator = find.iterator(); + assertThat(iterator.next().getValue(), is("test1")); + assertThat(iterator.hasNext(), is(true)); + assertThat(iterator.next().getValue(), is("test2")); + assertThat(iterator.hasNext(), is(false)); + } + + @Test + public void findAllWithPageable() throws InterruptedException { + operations.insert(Arrays.asList(new ArangoSearchTestEntity("test2"), new ArangoSearchTestEntity("test1")), + ArangoSearchTestEntity.class); + Thread.sleep(1000); + final Page find = repository.findAll(PageRequest.of(1, 1, Sort.by("value"))); + assertThat(find.getContent().size(), is(1)); + assertThat(find.getContent().iterator().next().getValue(), is("test2")); + } + + @Test + public void findOneByExample() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final Optional find = repository.findOne(Example.of(entity)); + assertThat(find.isPresent(), is(true)); + assertThat(find.get().getValue(), is("test")); + } + + @Test + public void findAllByExample() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final Iterable find = repository.findAll(Example.of(entity)); + final Iterator iterator = find.iterator(); + assertThat(iterator.next().getValue(), is("test")); + assertThat(iterator.hasNext(), is(false)); + } + + @Test + public void findAllByExampleWithSort() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final Iterable find = repository.findAll(Example.of(entity), Sort.by("value")); + final Iterator iterator = find.iterator(); + assertThat(iterator.next().getValue(), is("test")); + assertThat(iterator.hasNext(), is(false)); + } + + @Test + public void findAllByExampleWithPageable() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final Iterable find = repository.findAll(Example.of(entity), PageRequest.of(0, 1)); + final Iterator iterator = find.iterator(); + assertThat(iterator.next().getValue(), is("test")); + assertThat(iterator.hasNext(), is(false)); + } + + @Test + public void countByExample() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final long count = repository.count(Example.of(entity)); + assertThat(count, is(1L)); + } + + @Test + public void existsByExample() throws InterruptedException { + final ArangoSearchTestEntity entity = new ArangoSearchTestEntity("test"); + operations.insert(entity); + Thread.sleep(1000); + final boolean exists = repository.exists(Example.of(entity)); + assertThat(exists, is(true)); + } +} diff --git a/src/test/java/com/arangodb/springframework/repository/ArangoSearchTestRepo.java b/src/test/java/com/arangodb/springframework/repository/ArangoSearchTestRepo.java new file mode 100644 index 000000000..033c00fa8 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/repository/ArangoSearchTestRepo.java @@ -0,0 +1,39 @@ +/* + * 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.repository; + +import java.util.List; + +import org.springframework.data.repository.query.Param; + +import com.arangodb.springframework.annotation.Query; +import com.arangodb.springframework.testdata.ArangoSearchTestEntity; + +/** + * @author Mark Vollmary + * + */ +public interface ArangoSearchTestRepo extends ArangoSearchRepository { + + @Query("FOR i IN ArangoSearchTestEntityView SEARCH i.value == @value RETURN i") + List findByValue(@Param("value") String value); + +} diff --git a/src/test/java/com/arangodb/springframework/testdata/ArangoSearchTestEntity.java b/src/test/java/com/arangodb/springframework/testdata/ArangoSearchTestEntity.java new file mode 100644 index 000000000..e19564316 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/testdata/ArangoSearchTestEntity.java @@ -0,0 +1,66 @@ +/* + * 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.testdata; + +import com.arangodb.springframework.annotation.ArangoId; +import com.arangodb.springframework.annotation.ArangoSearch; +import com.arangodb.springframework.annotation.ArangoSearchLinked; +import com.arangodb.springframework.annotation.Document; + +/** + * @author Mark Vollmary + * + */ +@ArangoSearch("ArangoSearchTestEntityView") +@Document +public class ArangoSearchTestEntity { + + @ArangoId + private String id; + @ArangoSearchLinked + private String value; + + public ArangoSearchTestEntity() { + super(); + } + + public ArangoSearchTestEntity(final String value) { + super(); + this.value = value; + } + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(final String value) { + this.value = value; + } + +}