diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 000000000..0499ccaea --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,164 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.3.2] - 2018-09-04 + +### Fixed + +- fixed repository methods with `Example` using `StringMatcher.CONTAINING` (issue #113) + +## [1.3.1] - 2018-08-13 + +### Fixed + +- fixed a bug in derived queries when using two times `@Relations` in one entity + +## [1.3.0] - 2018-07-18 + +### Deprecated + +- deprecated `com.arangodb.springframework.annotation.Key` +- deprecated `com.arangodb.springframework.core.convert.DBEntity` +- deprecated `com.arangodb.springframework.core.convert.DBCollectionEntity` + +## [1.2.2] - 2018-07-09 + +- fixed `ArangoOperations#getVersion()` use configured database instead of \_system + +## [1.2.1] - 2018-07-03 + +### Fixed + +- fixed `ArangoOperations#upsert(T, UpsertStrategy)` (issue #92) + - Check `Persistable#isNew` +- fixed `ArangoOperations#upsert(Iterable, UpsertStrategy)` (issue #92) + - Check `Persistable#isNew` + +## [1.2.0] - 2018-07-02 + +### Added + +- added `ArangoOperations#repsert(T)` +- added `ArangoOperations#repsert(Iterable, Class)` +- added support for streaming AQL cursors + - added `QueryOptions#stream()` +- added `QueryOptions#memoryLimit()` +- added support for satellite collections + - added `@Document#satellite()` + - added `@Edge#satellite()` + +## Changed + +- upgraded dependency arangodb-java-driver 4.6.0 +- changed `SimpleArangoRepository#save()` to use `ArangoOperations#repsert()` when ArangoDB version >= 3.4.0 +- changed `SimpleArangoRepository#saveAll()` to use `ArangoOperations#repsert()` when ArangoDB version >= 3.4.0 +- changed `ArangoOperations#upsert(T, UpsertStrategy)` to work with `@Id` in addition to `@Key` +- changed `ArangoOperations#upsert(Iterable, UpsertStrategy)` to work with `@Id` in addition to `@Key` + +### Deprecated + +- deprecated `ArangoOperations#upsert(T, UpsertStrategy)` +- deprecated `ArangoOperations#upsert(Iterable, UpsertStrategy)` + +## [1.1.8] - 2018-06-26 + +### Fixed + +- fixed derived query with `containing` on `String` (issue #84) + +## [1.1.7] - 2018-06-25 + +### Changed + +- upgraded dependency arangodb-java-driver 4.5.2 + - fixed `ArangoDB#aquireHostList(true)` with authentication + - added support for custom serializer + +## [1.1.6] - 2018-06-14 + +### Changed + +- allow override of CRUD methods of `ArangoRepository` with `@Query` +- upgraded dependency arangodb-java-driver 4.5.0 + +### Fixed + +- fixed lazy use of `@Relations`/`@From`/`@To` when using a `Set<>` + +## [1.1.5] - 2018-06-07 + +### Fixed + +- fixed relation cycle (issue #43) + +## [1.1.4] - 2018-06-07 + +### Changed + +- upgraded arangodb-java-driver to 4.4.1 + +### Fixed + +- fixed relation cycle (issue #43) + +## [1.1.3] - 2018-06-04 + +### Added + +- added paging and sorting support for native queries + +### Fixed + +- fixed support for `ArangoCusor` as query-method return type + +## [1.1.2] - 2018-05-04 + +### Added + +- added support for named queries + +### Deprecated + +- deprecated `@Param` annotation, there is already such an annotation from Spring Data + +### Fixed + +- fixed floating point numbers in derived queries +- fixed distance calculation in derived geo queries + +## [1.1.1] - 2018-04-23 + +### Fixed + +- fixed serialization of enums (issue #39) + +## [1.1.0] - 2018-04-20 + +### Added + +- added `DataIntegrityViolationException` to `ExceptionTranslator` +- added type mapper implementation & custom conversions extension (issue #33) + +### Fixed + +- fixed race conditions in `ArangoTemplate` when creating database and collections (issue #35) +- fixed missing deserialization of return types of `@Query` methods(issue #21) +- fixed handling of `java.time` in converters (issue #36, #24, #25) +- fixed handling of `org.joda.time` in converters (issue #36) + +## [2.0.3] - 2018-03-23 + +### Fixed + +- fixed missing WITH information in derived query + +## [1.0.1] - 2018-01-26 + +### Fixed + +- fixed missing WITH information in AQL when resolving annotation `@Relations` (Issue #8) diff --git a/README.md b/README.md index 09d7d0426..05a4c02c7 100644 --- a/README.md +++ b/README.md @@ -16,46 +16,48 @@ Spring Data ArangoDB requires ArangoDB 3.0 or higher - which you can download [h **Note**: ArangoDB 3.0 does not support the default transport protocol [VelocyStream](https://github.com/arangodb/velocystream). A manual switch to HTTP is required. See chapter [configuration](#configuration). Also ArangoDB 3.0 does not support [geospatial queries](#geospatial-queries). ## Learn more -* [ArangoDB](https://www.arangodb.com/) -* [Demo](https://github.com/arangodb/spring-data-demo) -* [JavaDoc](http://arangodb.github.io/spring-data/javadoc-1_0/index.html) -* [JavaDoc Java driver](http://arangodb.github.io/arangodb-java-driver/javadoc-4_2/index.html) + +- [ArangoDB](https://www.arangodb.com/) +- [Demo](https://github.com/arangodb/spring-data-demo) +- [JavaDoc](http://arangodb.github.io/spring-data/javadoc-1_0/index.html) +- [JavaDoc Java driver](http://arangodb.github.io/arangodb-java-driver/javadoc-4_2/index.html) +- [Changelog](ChangeLog.md) ## Table of Contents -* [Getting Started](#getting-started) - * [Configuration](#configuration) -* [Template](#template) -* [Repositories](#repositories) - * [Introduction](#introduction) - * [Instantiating](#instantiating) - * [Return types](#return-types) - * [Query methods](#query-methods) - * [Derived queries](#derived-queries) - * [Geospatial queries](#geospatial-queries) - * [Property expression](#property-expression) - * [Special parameter handling](#special-parameter-handling) - * [Bind parameters](#bind-parameters) - * [AQL query options](#aql-query-options) -* [Mapping](#mapping) - * [Introduction](#introduction) - * [Conventions](#conventions) - * [Type conventions](#type-conventions) - * [Annotations](#annotations) - * [Annotation overview](#annotation-overview) - * [Document](#document) - * [Edge](#edge) - * [Reference](#reference) - * [Relations](#relations) - * [Document with From and To](#document-with-from-and-to) - * [Edge with From and To](#edge-with-from-and-to) - * [Index and Indexed annotations](#index-and-indexed-annotations) +- [Getting Started](#getting-started) + - [Configuration](#configuration) +- [Template](#template) +- [Repositories](#repositories) + - [Introduction](#introduction) + - [Instantiating](#instantiating) + - [Return types](#return-types) + - [Query methods](#query-methods) + - [Derived queries](#derived-queries) + - [Geospatial queries](#geospatial-queries) + - [Property expression](#property-expression) + - [Special parameter handling](#special-parameter-handling) + - [Bind parameters](#bind-parameters) + - [AQL query options](#aql-query-options) +- [Mapping](#mapping) + - [Introduction](#introduction) + - [Conventions](#conventions) + - [Type conventions](#type-conventions) + - [Annotations](#annotations) + - [Annotation overview](#annotation-overview) + - [Document](#document) + - [Edge](#edge) + - [Reference](#reference) + - [Relations](#relations) + - [Document with From and To](#document-with-from-and-to) + - [Edge with From and To](#edge-with-from-and-to) + - [Index and Indexed annotations](#index-and-indexed-annotations) # Getting Started To use Spring Data ArangoDB in your project, your build automation tool needs to be configured to include and use the Spring Data ArangoDB dependency. Example with Maven: -``` xml +```xml com.arangodb arangodb-spring-data @@ -69,7 +71,7 @@ There is a [demonstration app](https://github.com/arangodb/spring-data-demo), wh You can use Java to configure your Spring Data environment as show below. Setting up the underlying driver (`ArangoDB.Builder`) with default configuration automatically loads a properties file `arangodb.properties`, if it exists in the classpath. -``` java +```java @Configuration @EnableArangoRepositories(basePackages = { "com.company.mypackage" }) public class MyConfiguration extends AbstractArangoConfiguration { @@ -90,18 +92,18 @@ public class MyConfiguration extends AbstractArangoConfiguration { The driver is configured with some default values: -property-key | description | default value --------------|-------------|-------------- -arangodb.host | ArangoDB host | 127.0.0.1 -arangodb.port | ArangoDB port | 8529 -arangodb.timeout | socket connect timeout(millisecond) | 0 -arangodb.user | Basic Authentication User | -arangodb.password | Basic Authentication Password | -arangodb.useSsl | use SSL connection | false +| property-key | description | default value | +| ----------------- | ----------------------------------- | ------------- | +| arangodb.host | ArangoDB host | 127.0.0.1 | +| arangodb.port | ArangoDB port | 8529 | +| arangodb.timeout | socket connect timeout(millisecond) | 0 | +| arangodb.user | Basic Authentication User | +| arangodb.password | Basic Authentication Password | +| arangodb.useSsl | use SSL connection | false | To customize the configuration, the parameters can be changed in the Java code. -``` java +```java @Override public ArangoDB.Builder arango() { ArangoDB.Builder arango = new ArangoDB.Builder() @@ -112,9 +114,10 @@ public ArangoDB.Builder arango() { } ``` -In addition you can use the *arangodb.properties* or a custom properties file to supply credentials to the driver. +In addition you can use the _arangodb.properties_ or a custom properties file to supply credentials to the driver. + +_Properties file_ -*Properties file* ``` arangodb.host=127.0.0.1 arangodb.port=8529 @@ -123,8 +126,9 @@ arangodb.user=root arangodb.password= ``` -*Custom properties file* -``` java +_Custom properties file_ + +```java @Override public ArangoDB.Builder arango() { InputStream in = MyClass.class.getResourceAsStream("my.properties"); @@ -136,7 +140,7 @@ public ArangoDB.Builder arango() { **Note**: When using ArangoDB 3.0 it is required to set the transport protocol to HTTP and fetch the dependency `org.apache.httpcomponents:httpclient`. -``` java +```java @Override public ArangoDB.Builder arango() { ArangoDB.Builder arango = new ArangoDB.Builder() @@ -144,7 +148,8 @@ public ArangoDB.Builder arango() { return arango; } ``` -``` xml + +```xml org.apache.httpcomponents httpclient @@ -167,7 +172,7 @@ Spring Data Commons provides a composable repository infrastructure which Spring Instances of a Repository are created in Spring beans through the auto-wired mechanism of Spring. -``` java +```java public class MySpringBean { @Autowired @@ -189,7 +194,7 @@ There are three ways of passing bind parameters to the query in the query annota Using number matching, arguments will be substituted into the query in the order they are passed to the query method. -``` java +```java public interface MyRepository extends Repository{ @Query("FOR c IN customers FILTER c.name == @0 AND c.surname == @2 RETURN c") @@ -200,7 +205,7 @@ public interface MyRepository extends Repository{ With the `@Param` annotation, the argument will be placed in the query at the place corresponding to the value passed to the `@Param` annotation. -``` java +```java public interface MyRepository extends Repository{ @Query("FOR c IN customers FILTER c.name == @name AND c.surname == @surname RETURN c") @@ -209,9 +214,9 @@ public interface MyRepository extends Repository{ } ``` - In addition you can use a parameter of type `Map` annotated with `@BindVars` as your bind parameters. You can then fill the map with any parameter used in the query. (see [here](https://docs.arangodb.com/3.1/AQL/Fundamentals/BindParameters.html#bind-parameters) for more Information about Bind Parameters). +In addition you can use a parameter of type `Map` annotated with `@BindVars` as your bind parameters. You can then fill the map with any parameter used in the query. (see [here](https://docs.arangodb.com/3.1/AQL/Fundamentals/BindParameters.html#bind-parameters) for more Information about Bind Parameters). -``` java +```java public interface MyRepository extends Repository{ @Query("FOR c IN customers FILTER c.name == @name AND c.surname = @surname RETURN c") @@ -222,7 +227,7 @@ public interface MyRepository extends Repository{ A mixture of any of these methods can be used. Parameters with the same name from an `@Param` annotation will override those in the `bindVars`. -``` java +```java public interface MyRepository extends Repository{ @Query("FOR c IN customers FILTER c.name == @name AND c.surname = @surname RETURN c") @@ -237,33 +242,32 @@ Spring Data ArangoDB supports queries derived from methods names by splitting it The complete list of part types for derived methods is below, where doc is a document in the database -Keyword | Sample | Predicate -----------|----------------|-------- -IsGreaterThan, GreaterThan, After | findByAgeGreaterThan(int age) | doc.age > age -IsGreaterThanEqual, GreaterThanEqual | findByAgeIsGreaterThanEqual(int age) | doc.age >= age -IsLessThan, LessThan, Before | findByAgeIsLessThan(int age) | doc.age < age -IsLessThanEqualLessThanEqual | findByAgeLessThanEqual(int age) | doc.age <= age -IsBetween, Between | findByAgeBetween(int lower, int upper) | lower < doc.age < upper -IsNotNull, NotNull | findByNameNotNull() | doc.name != null -IsNull, Null | findByNameNull() | doc.name == null -IsLike, Like | findByNameLike(String name) | doc.name LIKE name -IsNotLike, NotLike | findByNameNotLike(String name) | NOT(doc.name LIKE name) -IsStartingWith, StartingWith, StartsWith | findByNameStartsWith(String prefix) | doc.name LIKE prefix -IsEndingWith, EndingWith, EndsWith | findByNameEndingWith(String suffix) | doc.name LIKE suffix -Regex, MatchesRegex, Matches | findByNameRegex(String pattern) | REGEX_TEST(doc.name, name, ignoreCase) -(No Keyword) | findByFirstName(String name) | doc.name == name -IsTrue, True | findByActiveTrue() | doc.active == true -IsFalse, False | findByActiveFalse() | doc.active == false -Is, Equals | findByAgeEquals(int age) | doc.age == age -IsNot, Not | findByAgeNot(int age) | doc.age != age -IsIn, In | findByNameIn(String[] names) | doc.name IN names -IsNotIn, NotIn | findByNameIsNotIn(String[] names) | doc.name NOT IN names -IsContaining, Containing, Contains | findByFriendsContaining(String name) | name IN doc.friends -IsNotContaining, NotContaining, NotContains | findByFriendsNotContains(String name) | name NOT IN doc.friends -Exists | findByFriendNameExists() | HAS(doc.friend, name) - - -``` java +| Keyword | Sample | Predicate | +| ------------------------------------------- | -------------------------------------- | -------------------------------------- | +| IsGreaterThan, GreaterThan, After | findByAgeGreaterThan(int age) | doc.age > age | +| IsGreaterThanEqual, GreaterThanEqual | findByAgeIsGreaterThanEqual(int age) | doc.age >= age | +| IsLessThan, LessThan, Before | findByAgeIsLessThan(int age) | doc.age < age | +| IsLessThanEqualLessThanEqual | findByAgeLessThanEqual(int age) | doc.age <= age | +| IsBetween, Between | findByAgeBetween(int lower, int upper) | lower < doc.age < upper | +| IsNotNull, NotNull | findByNameNotNull() | doc.name != null | +| IsNull, Null | findByNameNull() | doc.name == null | +| IsLike, Like | findByNameLike(String name) | doc.name LIKE name | +| IsNotLike, NotLike | findByNameNotLike(String name) | NOT(doc.name LIKE name) | +| IsStartingWith, StartingWith, StartsWith | findByNameStartsWith(String prefix) | doc.name LIKE prefix | +| IsEndingWith, EndingWith, EndsWith | findByNameEndingWith(String suffix) | doc.name LIKE suffix | +| Regex, MatchesRegex, Matches | findByNameRegex(String pattern) | REGEX_TEST(doc.name, name, ignoreCase) | +| (No Keyword) | findByFirstName(String name) | doc.name == name | +| IsTrue, True | findByActiveTrue() | doc.active == true | +| IsFalse, False | findByActiveFalse() | doc.active == false | +| Is, Equals | findByAgeEquals(int age) | doc.age == age | +| IsNot, Not | findByAgeNot(int age) | doc.age != age | +| IsIn, In | findByNameIn(String[] names) | doc.name IN names | +| IsNotIn, NotIn | findByNameIsNotIn(String[] names) | doc.name NOT IN names | +| IsContaining, Containing, Contains | findByFriendsContaining(String name) | name IN doc.friends | +| IsNotContaining, NotContaining, NotContains | findByFriendsNotContains(String name) | name NOT IN doc.friends | +| Exists | findByFriendNameExists() | HAS(doc.friend, name) | + +```java public interface MyRepository extends Repository { // FOR c IN customers FILTER c.name == @0 RETURN c @@ -284,7 +288,7 @@ public interface MyRepository extends Repository { You can apply sorting for one or multiple sort criteria by appending `OrderBy` to the method and `Asc` or `Desc` for the directions. -``` java +```java public interface MyRepository extends Repository { // FOR c IN customers @@ -306,9 +310,9 @@ Geospatial queries are a subsection of derived queries. To use a geospatial quer As a subsection of derived queries, geospatial queries support all the same return types, but also support the three return types `GeoPage, GeoResult and Georesults`. These types must be used in order to get the distance of each document as generated by the query. -There are two kinds of geospatial query, Near and Within. Near sorts documents by distance from the given point, while within both sorts and filters documents, returning those within the given distance range or shape. +There are two kinds of geospatial query, Near and Within. Near sorts documents by distance from the given point, while within both sorts and filters documents, returning those within the given distance range or shape. -``` java +```java public interface MyRepository extends Repository { GeoResult getByLocationNear(Point point); @@ -327,7 +331,7 @@ public interface MyRepository extends Repository { Property expressions can refer only to direct and nested properties of the managed domain class. The algorithm checks the domain class for the entire expression as the property. If the check fails, the algorithm splits up the expression at the camel case parts from the right and tries to find the corresponding property. -``` java +```java @Document("customers") public class Customer { private Address address; @@ -346,9 +350,9 @@ public interface MyRepository extends Repository { } ``` -It is possible for the algorithm to select the wrong property if the domain class also has a property which matches the first split of the expression. To resolve this ambiguity you can use _ as a separator inside your method-name to define traversal points. +It is possible for the algorithm to select the wrong property if the domain class also has a property which matches the first split of the expression. To resolve this ambiguity you can use \_ as a separator inside your method-name to define traversal points. -``` java +```java @Document("customers") public class Customer { private Address address; @@ -385,7 +389,7 @@ public interface MyRepository extends Repository { AQL supports the usage of [bind parameters](https://docs.arangodb.com/3.1/AQL/Fundamentals/BindParameters.html) which you can define with a method parameter named `bindVars` of type `Map`. -``` java +```java public interface MyRepository extends Repository { @Query("FOR c IN customers FILTER c[@field] == @value RETURN c") @@ -407,7 +411,7 @@ You can set additional options for the query and the created cursor over the cla The `AqlQueryOptions` allows you to set the cursor time-to-life, batch-size, caching flag and several other settings. This special parameter works with both query-methods and finder-methods. Keep in mind that some options, like time-to-life, are only effective if the method return type is`ArangoCursor` or `Iterable`. -``` java +```java public interface MyRepository extends Repository { @@ -437,67 +441,73 @@ In this section we will describe the features and conventions for mapping Java o ## Conventions -* The Java class name is mapped to the collection name -* The non-static fields of a Java object are used as fields in the stored document -* The Java field name is mapped to the stored document field name -* All nested Java object are stored as nested objects in the stored document -* The Java class needs a non parameterized constructor +- The Java class name is mapped to the collection name +- The non-static fields of a Java object are used as fields in the stored document +- The Java field name is mapped to the stored document field name +- All nested Java object are stored as nested objects in the stored document +- The Java class needs a constructor which meets the following criteria: + - in case of a single constructor: + - a non-parameterized constructor or + - a parameterized constructor + - in case of multiple constructors: + - a non-parameterized constructor or + - a parameterized constructor annotated with `@PersistenceConstructor` ## Type conventions ArangoDB uses [VelocyPack](https://github.com/arangodb/velocypack) as it's internal storage format which supports a large number of data types. In addition Spring Data ArangoDB offers - with the underlying Java driver - built-in converters to add additional types to the mapping. -Java type | VelocyPack type -----------|---------------- -java.lang.String | string -java.lang.Boolean | bool -java.lang.Integer | signed int 4 bytes, smallint -java.lang.Long | signed int 8 bytes, smallint -java.lang.Short | signed int 2 bytes, smallint -java.lang.Double | double -java.lang.Float | double -java.math.BigInteger | signed int 8 bytes, unsigned int 8 bytes -java.math.BigDecimal | double -java.lang.Number | double -java.lang.Character | string -java.util.Date | string (date-format ISO 8601) -java.sql.Date | string (date-format ISO 8601) -java.sql.Timestamp | string (date-format ISO 8601) -java.util.UUID | string -java.lang.byte[] | string (Base64) +| Java type | VelocyPack type | +| -------------------- | ---------------------------------------- | +| java.lang.String | string | +| java.lang.Boolean | bool | +| java.lang.Integer | signed int 4 bytes, smallint | +| java.lang.Long | signed int 8 bytes, smallint | +| java.lang.Short | signed int 2 bytes, smallint | +| java.lang.Double | double | +| java.lang.Float | double | +| java.math.BigInteger | signed int 8 bytes, unsigned int 8 bytes | +| java.math.BigDecimal | double | +| java.lang.Number | double | +| java.lang.Character | string | +| java.util.Date | string (date-format ISO 8601) | +| java.sql.Date | string (date-format ISO 8601) | +| java.sql.Timestamp | string (date-format ISO 8601) | +| java.util.UUID | string | +| java.lang.byte[] | string (Base64) | ## Annotations ### Annotation overview -annotation | level | description ------------|-------|------------ -@Document | class | marks this class as a candidate for mapping -@Edge | class | marks this class as a candidate for mapping -@Id | field | stores the field as the system field _id -@Key | field | stores the field as the system field _key -@Rev | field | stores the field as the system field _rev -@Field("alt-name") | field | stores the field with an alternative name -@Ref | field | stores the _id of the referenced document and not the nested document -@From | field | stores the _id of the referenced document as the system field _from -@To | field | stores the _id of the referenced document as the system field _to -@Relations | field | vertices which are connected over edges -@HashIndex | class | describes a hash index -@HashIndexed | field | describes how to index the field -@SkiplistIndex | class | describes a skiplist index -@SkiplistIndexed | field | describes how to index the field -@PersistentIndex | class | describes a persistent index -@PersistentIndexed | field | describes how to index the field -@GeoIndex | class | describes a geo index -@GeoIndexed | field | describes how to index the field -@FulltextIndex | class | describes a fulltext index -@FulltextIndexed | field | describes how to index the field +| annotation | level | description | +| ------------------ | ----- | ---------------------------------------------------------------------- | +| @Document | class | marks this class as a candidate for mapping | +| @Edge | class | marks this class as a candidate for mapping | +| @Id | field | stores the field as the system field \_id | +| @Key | field | stores the field as the system field \_key | +| @Rev | field | stores the field as the system field \_rev | +| @Field("alt-name") | field | stores the field with an alternative name | +| @Ref | field | stores the \_id of the referenced document and not the nested document | +| @From | field | stores the \_id of the referenced document as the system field \_from | +| @To | field | stores the \_id of the referenced document as the system field \_to | +| @Relations | field | vertices which are connected over edges | +| @HashIndex | class | describes a hash index | +| @HashIndexed | field | describes how to index the field | +| @SkiplistIndex | class | describes a skiplist index | +| @SkiplistIndexed | field | describes how to index the field | +| @PersistentIndex | class | describes a persistent index | +| @PersistentIndexed | field | describes how to index the field | +| @GeoIndex | class | describes a geo index | +| @GeoIndexed | field | describes how to index the field | +| @FulltextIndex | class | describes a fulltext index | +| @FulltextIndexed | field | describes how to index the field | ### Document The annotations `@Document` applied to a class marks this class as a candidate for mapping to the database. The most relevant parameter is `value` to specify the collection name in the database. The annotation `@Document` specifies the collection type to `DOCUMENT`. -``` java +```java @Document(value="persons") public class Person { ... @@ -508,7 +518,7 @@ public class Person { The annotations `@Edge` applied to a class marks this class as a candidate for mapping to the database. The most relevant parameter is `value` to specify the collection name in the database. The annotation `@Edge` specifies the collection type to `EDGE`. -``` java +```java @Edge("relations") public class Relation { ... @@ -519,7 +529,7 @@ public class Relation { With the annotation `@Ref` applied on a field the nested object isn’t stored as a nested object in the document. The `_id` field of the nested object is stored in the document and the nested object has to be stored as a separate document in another collection described in the `@Document` annotation of the nested object class. To successfully persist an instance of your object the referencing field has to be null or it's instance has to provide a field with the annotation `@Id` including a valid id. -``` java +```java @Document(value="persons") public class Person { @Ref @@ -535,7 +545,7 @@ public class Address { } ``` -The database representation of `Person` in collection *persons* looks as follow: +The database representation of `Person` in collection _persons_ looks as follow: ``` { @@ -544,7 +554,9 @@ The database representation of `Person` in collection *persons* looks as follow: "address" : "addresses/456" } ``` -and the representation of `Address` in collection *addresses*: + +and the representation of `Address` in collection _addresses_: + ``` { "_key" : "456", @@ -571,7 +583,7 @@ Without the annotation `@Ref` at the field `address`, the stored document would With the annotation `@Relations` applied on a collection or array field in a class annotated with `@Document` the nested objects are fetched from the database over a graph traversal with your current object as the starting point. The most relevant parameter is `edge`. With `edge` you define the edge collection - which should be used in the traversal - using the class type. With the parameter `depth` you can define the maximal depth for the traversal (default 1) and the parameter `direction` defines whether the traversal should follow outgoing or incoming edges (default Direction.ANY). -``` java +```java @Document(value="persons") public class Person { @Relations(edge=Relation.class, depth=1, direction=Direction.ANY) @@ -586,9 +598,9 @@ public class Relation { ### Document with From and To -With the annotations `@From` and `@To` applied on a collection or array field in a class annotated with `@Document` the nested edge objects are fetched from the database. Each of the nested edge objects has to be stored as separate edge document in the edge collection described in the `@Edge` annotation of the nested object class with the *_id* of the parent document as field *_from* or *_to*. +With the annotations `@From` and `@To` applied on a collection or array field in a class annotated with `@Document` the nested edge objects are fetched from the database. Each of the nested edge objects has to be stored as separate edge document in the edge collection described in the `@Edge` annotation of the nested object class with the _\_id_ of the parent document as field _\_from_ or _\_to_. -``` java +```java @Document("persons") public class Person { @From @@ -601,7 +613,8 @@ public class Relation { } ``` -The database representation of `Person` in collection *persons* looks as follow: +The database representation of `Person` in collection _persons_ looks as follow: + ``` { "_key" : "123", @@ -609,7 +622,8 @@ The database representation of `Person` in collection *persons* looks as follow: } ``` -and the representation of `Relation` in collection *relations*: +and the representation of `Relation` in collection _relations_: + ``` { "_key" : "456", @@ -624,14 +638,13 @@ and the representation of `Relation` in collection *relations*: "_to" : ".../..." } ... - ``` ### Edge with From and To -With the annotations `@From` and `@To` applied on a field in a class annotated with `@Edge` the nested object is fetched from the database. The nested object has to be stored as a separate document in the collection described in the `@Document` annotation of the nested object class. The *_id* field of this nested object is stored in the fields `_from` or `_to` within the edge document. +With the annotations `@From` and `@To` applied on a field in a class annotated with `@Edge` the nested object is fetched from the database. The nested object has to be stored as a separate document in the collection described in the `@Document` annotation of the nested object class. The _\_id_ field of this nested object is stored in the fields `_from` or `_to` within the edge document. -``` java +```java @Edge("relations") public class Relation { @From @@ -647,7 +660,8 @@ public class Person { } ``` -The database representation of `Relation` in collection *relations* looks as follow: +The database representation of `Relation` in collection _relations_ looks as follow: + ``` { "_key" : "123", @@ -657,7 +671,8 @@ The database representation of `Relation` in collection *relations* looks as fol } ``` -and the representation of `Person` in collection *persons*: +and the representation of `Person` in collection _persons_: + ``` { "_key" : "456", @@ -676,14 +691,16 @@ and the representation of `Person` in collection *persons*: With the `@Indexed` annotations user defined indexes can be created at a collection level by annotating single fields of a class. Possible `@Indexed` annotations are: -* `@HashIndexed` -* `@SkiplistIndexed` -* `@PersistentIndexed` -* `@GeoIndexed` -* `@FulltextIndexed` + +- `@HashIndexed` +- `@SkiplistIndexed` +- `@PersistentIndexed` +- `@GeoIndexed` +- `@FulltextIndexed` The following example creates a hash index on the field `name` and a separate hash index on the field `age`: -``` java + +```java public class Person { @HashIndexed private String name; @@ -696,7 +713,8 @@ public class Person { With the `@Indexed` annotations different indexes can be created on the same field. The following example creates a hash index and also a skiplist index on the field `name`: -``` java + +```java public class Person { @HashIndexed @SkiplistIndexed @@ -707,14 +725,16 @@ public class Person { If the index should include multiple fields the `@Index` annotations can be used on the type instead. Possible `@Index` annotations are: -* `@HashIndex` -* `@SkiplistIndex` -* `@PersistentIndex` -* `@GeoIndex` -* `@FulltextIndex` + +- `@HashIndex` +- `@SkiplistIndex` +- `@PersistentIndex` +- `@GeoIndex` +- `@FulltextIndex` The following example creates a single hash index on the fields `name` and `age`, note that if a field is renamed in the database with @Field, the new field name must be used in the index declaration: -``` java + +```java @HashIndex(fields = {"fullname", "age"}) public class Person { @Field("fullname") @@ -727,7 +747,8 @@ public class Person { The `@Index` annotations can also be used to create an index on a nested field. The following example creates a single hash index on the fields `name` and `address.country`: -``` java + +```java @HashIndex(fields = {"name", "address.country"}) public class Person { private String name; @@ -739,7 +760,8 @@ public class Person { The `@Index` annotations and the `@Indexed` annotations can be used at the same time in one class. The following example creates a hash index on the fields `name` and `age` and a separate hash index on the field `age`: -``` java + +```java @HashIndex(fields = {"name", "age"}) public class Person { private String name; @@ -752,7 +774,8 @@ public class Person { The `@Index` annotations can be used multiple times to create more than one index in this way. The following example creates a hash index on the fields `name` and `age` and a separate hash index on the fields `name` and `gender`: -``` java + +```java @HashIndex(fields = {"name", "age"}) @HashIndex(fields = {"name", "gender"}) public class Person { diff --git a/docs/Drivers/SpringData/Reference/README.md b/docs/Drivers/SpringData/Reference/README.md new file mode 100644 index 000000000..9b5ebc280 --- /dev/null +++ b/docs/Drivers/SpringData/Reference/README.md @@ -0,0 +1,797 @@ +# Spring Data ArangoDB - Reference + +# Template + +With `ArangoTemplate` Spring Data ArangoDB offers a central support for interactions with the database over a rich feature set. It mostly offers the features from the ArangoDB Java driver with additional exception translation from the drivers exceptions to the Spring Data access exceptions inheriting the `DataAccessException` class. +The `ArangoTemplate` class is the default implementation of the operations interface `ArangoOperations` which developers of Spring Data are encouraged to code against. + +# Repositories + +## Introduction + +Spring Data Commons provides a composable repository infrastructure which Spring Data ArangoDB is built on. These allow for interface-based composition of repositories consisting of provided default implementations for certain interfaces (like `CrudRepository`) and custom implementations for other methods. + +## Instantiating + +Instances of a Repository are created in Spring beans through the auto-wired mechanism of Spring. + +```java +public class MySpringBean { + + @Autowired + private MyRepository rep; + +} +``` + +## Return types + +The method return type for single results can be a primitive type, a domain class, `Map`, `BaseDocument`, `BaseEdgeDocument`, `Optional`, `GeoResult`. +The method return type for multiple results can additionally be `ArangoCursor`, `Iterable`, `Collection`, `List`, `Set`, `Page`, `Slice`, `GeoPage`, `GeoResults` where Type can be everything a single result can be. + +## Query methods + +Queries using [ArangoDB Query Language (AQL)](https://docs.arangodb.com/current/AQL/index.html) can be supplied with the `@Query` annotation on methods. `AqlQueryOptions` can also be passed to the driver, as an argument anywhere in the method signature. + +There are three ways of passing bind parameters to the query in the query annotation. + +Using number matching, arguments will be substituted into the query in the order they are passed to the query method. + +```java +public interface MyRepository extends Repository{ + + @Query("FOR c IN customers FILTER c.name == @0 AND c.surname == @2 RETURN c") + ArangoCursor query(String name, AqlQueryOptions options, String surname); + +} +``` + +With the `@Param` annotation, the argument will be placed in the query at the place corresponding to the value passed to the `@Param` annotation. + +```java +public interface MyRepository extends Repository{ + + @Query("FOR c IN customers FILTER c.name == @name AND c.surname == @surname RETURN c") + ArangoCursor query(@Param("name") String name, @Param("surname") String surname); + +} +``` + +In addition you can use a parameter of type `Map` annotated with `@BindVars` as your bind parameters. You can then fill the map with any parameter used in the query. (see [here](https://docs.arangodb.com/3.1/AQL/Fundamentals/BindParameters.html#bind-parameters) for more Information about Bind Parameters). + +```java +public interface MyRepository extends Repository{ + + @Query("FOR c IN customers FILTER c.name == @name AND c.surname = @surname RETURN c") + ArangoCursor query(@BindVars Map bindVars); + +} +``` + +A mixture of any of these methods can be used. Parameters with the same name from an `@Param` annotation will override those in the `bindVars`. + +```java +public interface MyRepository extends Repository{ + + @Query("FOR c IN customers FILTER c.name == @name AND c.surname = @surname RETURN c") + ArangoCursor query(@BindVars Map bindVars, @Param("name") String name); + +} +``` + +## Named queries + +An alternative to using the `@Query` annotation on methods is specifying them in a separate `.properties` file. The default path for the file is `META-INF/arango-named-queries.properties` and can be changed with the `EnableArangoRepositories#namedQueriesLocation()` setting. The entries in the properties file must adhere to the following convention: `{simple entity name}.{method name} = {query}`. Let's assume we have the following repository interface: + +```java +package com.arangodb.repository; + +public interface CustomerRepository extends ArangoRepository { + Customer findByUsername(@Param("username") String username); +} +``` + +The corresponding `arango-named-queries.properties` file looks like this: + +```properties +Customer.findByUsername = FOR c IN customers FILTER c.username == @username RETURN c +``` + +The queries specified in the properties file are no different than the queries that can be defined with the `@Query` annotation. The only difference is that the queries are in one place. If there is a `@Query` annotation present and a named query defined, the query in the `@Query` annotation takes precedence. + +## Derived queries + +Spring Data ArangoDB supports queries derived from methods names by splitting it into its semantic parts and converting into AQL. The mechanism strips the prefixes `find..By`, `get..By`, `query..By`, `read..By`, `stream..By`, `count..By`, `exists..By`, `delete..By`, `remove..By` from the method and parses the rest. The By acts as a separator to indicate the start of the criteria for the query to be built. You can define conditions on entity properties and concatenate them with `And` and `Or`. + +The complete list of part types for derived methods is below, where doc is a document in the database + +| Keyword | Sample | Predicate | +| ------------------------------------------- | -------------------------------------- | -------------------------------------- | +| IsGreaterThan, GreaterThan, After | findByAgeGreaterThan(int age) | doc.age > age | +| IsGreaterThanEqual, GreaterThanEqual | findByAgeIsGreaterThanEqual(int age) | doc.age >= age | +| IsLessThan, LessThan, Before | findByAgeIsLessThan(int age) | doc.age < age | +| IsLessThanEqualLessThanEqual | findByAgeLessThanEqual(int age) | doc.age <= age | +| IsBetween, Between | findByAgeBetween(int lower, int upper) | lower < doc.age < upper | +| IsNotNull, NotNull | findByNameNotNull() | doc.name != null | +| IsNull, Null | findByNameNull() | doc.name == null | +| IsLike, Like | findByNameLike(String name) | doc.name LIKE name | +| IsNotLike, NotLike | findByNameNotLike(String name) | NOT(doc.name LIKE name) | +| IsStartingWith, StartingWith, StartsWith | findByNameStartsWith(String prefix) | doc.name LIKE prefix | +| IsEndingWith, EndingWith, EndsWith | findByNameEndingWith(String suffix) | doc.name LIKE suffix | +| Regex, MatchesRegex, Matches | findByNameRegex(String pattern) | REGEX_TEST(doc.name, name, ignoreCase) | +| (No Keyword) | findByFirstName(String name) | doc.name == name | +| IsTrue, True | findByActiveTrue() | doc.active == true | +| IsFalse, False | findByActiveFalse() | doc.active == false | +| Is, Equals | findByAgeEquals(int age) | doc.age == age | +| IsNot, Not | findByAgeNot(int age) | doc.age != age | +| IsIn, In | findByNameIn(String[] names) | doc.name IN names | +| IsNotIn, NotIn | findByNameIsNotIn(String[] names) | doc.name NOT IN names | +| IsContaining, Containing, Contains | findByFriendsContaining(String name) | name IN doc.friends | +| IsNotContaining, NotContaining, NotContains | findByFriendsNotContains(String name) | name NOT IN doc.friends | +| Exists | findByFriendNameExists() | HAS(doc.friend, name) | + +```java +public interface MyRepository extends Repository { + + // FOR c IN customers FILTER c.name == @0 RETURN c + ArangoCursor findByName(String name); + ArangoCursor getByName(String name); + + // FOR c IN customers + // FILTER c.name == @0 && c.age == @1 + // RETURN c + ArangoCursor findByNameAndAge(String name, int age); + + // FOR c IN customers + // FILTER c.name == @0 || c.age == @1 + // RETURN c + ArangoCursor findByNameOrAge(String name, int age); +} +``` + +You can apply sorting for one or multiple sort criteria by appending `OrderBy` to the method and `Asc` or `Desc` for the directions. + +```java +public interface MyRepository extends Repository { + + // FOR c IN customers + // FILTER c.name == @0 + // SORT c.age DESC RETURN c + ArangoCursor getByNameOrderByAgeDesc(String name); + + // FOR c IN customers + // FILTER c.name = @0 + // SORT c.name ASC, c.age DESC RETURN c + ArangoCursor findByNameOrderByNameAscAgeDesc(String name); + +} +``` + +### Geospatial queries + +Geospatial queries are a subsection of derived queries. To use a geospatial query on a collection, a geo index must exist on that collection. A geo index can be created on a field which is a two element array, corresponding to latitude and longitude coordinates. + +As a subsection of derived queries, geospatial queries support all the same return types, but also support the three return types `GeoPage, GeoResult and Georesults`. These types must be used in order to get the distance of each document as generated by the query. + +There are two kinds of geospatial query, Near and Within. Near sorts documents by distance from the given point, while within both sorts and filters documents, returning those within the given distance range or shape. + +```java +public interface MyRepository extends Repository { + + GeoResult getByLocationNear(Point point); + + GeoResults findByLocationWithinOrLocationWithin(Box box, Polygon polygon); + + //Equivalent queries + GeoResults findByLocationWithinOrLocationWithin(Point point, int distance); + GeoResults findByLocationWithinOrLocationWithin(Point point, Distance distance); + GeoResults findByLocationWithinOrLocationWithin(Circle circle); + +} +``` + +## Property expression + +Property expressions can refer only to direct and nested properties of the managed domain class. The algorithm checks the domain class for the entire expression as the property. If the check fails, the algorithm splits up the expression at the camel case parts from the right and tries to find the corresponding property. + +```java +@Document("customers") +public class Customer { + private Address address; +} + +public class Address { + private ZipCode zipCode; +} + +public interface MyRepository extends Repository { + + // 1. step: search domain class for a property "addressZipCode" + // 2. step: search domain class for "addressZip.code" + // 3. step: search domain class for "address.zipCode" + ArangoCursor findByAddressZipCode(ZipCode zipCode); +} +``` + +It is possible for the algorithm to select the wrong property if the domain class also has a property which matches the first split of the expression. To resolve this ambiguity you can use \_ as a separator inside your method-name to define traversal points. + +```java +@Document("customers") +public class Customer { + private Address address; + private AddressZip addressZip; +} + +public class Address { + private ZipCode zipCode; +} + +public class AddressZip { + private String code; +} + +public interface MyRepository extends Repository { + + // 1. step: search domain class for a property "addressZipCode" + // 2. step: search domain class for "addressZip.code" + // creates query with "x.addressZip.code" + ArangoCursor findByAddressZipCode(ZipCode zipCode); + + // 1. step: search domain class for a property "addressZipCode" + // 2. step: search domain class for "addressZip.code" + // 3. step: search domain class for "address.zipCode" + // creates query with "x.address.zipCode" + ArangoCursor findByAddress_ZipCode(ZipCode zipCode); + +} +``` + +## Special parameter handling + +### Bind parameters + +AQL supports the usage of [bind parameters](https://docs.arangodb.com/3.1/AQL/Fundamentals/BindParameters.html) which you can define with a method parameter annotated with `@BindVars` of type `Map`. + +```java +public interface MyRepository extends Repository { + + @Query("FOR c IN customers FILTER c[@field] == @value RETURN c") + ArangoCursor query(Map bindVars); + +} + +Map bindVars = new HashMap(); +bindVars.put("field", "name"); +bindVars.put("value", "john"; + +// will execute query "FOR c IN customers FILTER c.name == "john" RETURN c" +ArangoCursor cursor = myRepo.query(bindVars); +``` + +### AQL query options + +You can set additional options for the query and the created cursor over the class `AqlQueryOptions` which you can simply define as a method parameter without a specific name. AqlQuery options can also be defined with the `@QueryOptions` annotation, as shown below. AqlQueryOptions from an annotation and those from an argument are merged if both exist, with those in the argument taking precedence. + +The `AqlQueryOptions` allows you to set the cursor time-to-live, batch-size, caching flag and several other settings. This special parameter works with both query-methods and finder-methods. Keep in mind that some options, like time-to-live, are only effective if the method return type is`ArangoCursor` or `Iterable`. + +```java +public interface MyRepository extends Repository { + + + @Query("FOR c IN customers FILTER c.name == @0 RETURN c") + Iterable query(String name, AqlQueryOptions options); + + + Iterable findByName(String name, AqlQueryOptions options); + + + @QueryOptions(maxPlans = 1000, ttl = 128) + ArangoCursor findByAddressZipCode(ZipCode zipCode); + + + @Query("FOR c IN customers FILTER c[@field] == @value RETURN c") + @QueryOptions(cache = true, ttl = 128) + ArangoCursor query(Map bindVars, AqlQueryOptions options); + +} +``` + +### Paging and sorting + +Spring Data ArangoDB supports Spring Data's `Pageable` and `Sort` parameters for repository query methods. If these parameters are used together with a native query, either through `@Query` annotation or named queries, a placeholder must be specified: + +* `#pageable` for `Pageable` parameter +* `#sort` for `Sort` parameter + +Sort properties or paths are attributes separated by dots (e.g. `customer.age`). Some rules apply for them: + +* they must not begin or end with a dot (e.g. `.customer.age`) +* dots in attributes are supported, but the whole attribute must be enclosed by backticks (e.g. `` customer.`attr.with.dots` `` +* backticks in attributes are supported, but they must be escaped with a backslash (e.g. `` customer.attr_with\` ``) +* any backslashes (that do not escape a backtick) are escaped (e.g. `customer\` => `customer\\`) + +``` +just.`some`.`attributes.that`.`form\``.a path\`.\ is converted to +`just`.`some`.`attributes.that`.`form\``.`a path\``.`\\` +``` + +Native queries example: + +```java +public interface CustomerRepository extends ArangoRepository { + + @Query("FOR c IN customer FILTER c.name == @1 #pageable RETURN c") + Page findByNameNative(Pageable pageable, String name); + + @Query("FOR c IN customer FILTER c.name == @1 #sort RETURN c") + List findByNameNative(Sort sort, String name); +} + +// don't forget to specify the var name of the document +final Pageable page = PageRequest.of(1, 10, Sort.by("c.age")); +repository.findByNameNative(page, "Matt"); + +final Sort sort = Sort.by(Direction.DESC, "c.age"); +repository.findByNameNative(sort, "Tony"); +``` + +Derived queries example: + +```java +public interface CustomerRepository extends ArangoRepository { + + Page findByName(Pageable pageable, String name); + + List findByName(Sort sort, String name); +} + +// no var name is necessary for derived queries +final Pageable page = PageRequest.of(1, 10, Sort.by("age")); +repository.findByName(page, "Matt"); + +final Sort sort = Sort.by(Direction.DESC, "age"); +repository.findByName(sort, "Tony"); +``` + +# Mapping + +## Introduction + +In this section we will describe the features and conventions for mapping Java objects to documents and how to override those conventions with annotation based mapping metadata. + +## Conventions + +* The Java class name is mapped to the collection name +* The non-static fields of a Java object are used as fields in the stored document +* The Java field name is mapped to the stored document field name +* All nested Java object are stored as nested objects in the stored document +* The Java class needs a constructor which meets the following criteria: + * in case of a single constructor: + * a non-parameterized constructor or + * a parameterized constructor + * in case of multiple constructors: + * a non-parameterized constructor or + * a parameterized constructor annotated with `@PersistenceConstructor` + +## Type conventions + +ArangoDB uses [VelocyPack](https://github.com/arangodb/velocypack) as it's internal storage format which supports a large number of data types. In addition Spring Data ArangoDB offers - with the underlying Java driver - built-in converters to add additional types to the mapping. + +| Java type | VelocyPack type | +| -------------------- | ---------------------------------------- | +| java.lang.String | string | +| java.lang.Boolean | bool | +| java.lang.Integer | signed int 4 bytes, smallint | +| java.lang.Long | signed int 8 bytes, smallint | +| java.lang.Short | signed int 2 bytes, smallint | +| java.lang.Double | double | +| java.lang.Float | double | +| java.math.BigInteger | signed int 8 bytes, unsigned int 8 bytes | +| java.math.BigDecimal | double | +| java.lang.Number | double | +| java.lang.Character | string | +| java.util.Date | string (date-format ISO 8601) | +| java.sql.Date | string (date-format ISO 8601) | +| java.sql.Timestamp | string (date-format ISO 8601) | +| java.util.UUID | string | +| java.lang.byte[] | string (Base64) | + +## Type mapping + +As collections in ArangoDB can contain documents of various types, a mechanism to retrieve the correct Java class is required. The type information of properties declared in a class may not be enough to restore the original class (due to inheritance). If the declared complex type and the actual type do not match, information about the actual type is stored together with the document. This is necessary to restore the correct type when reading from the DB. Consider the following example: + +```java +public class Person { + private String name; + private Address homeAddress; + // ... + + // getters and setters omitted +} + +public class Employee extends Person { + private Address workAddress; + // ... + + // getters and setters omitted +} + +public class Address { + private final String street; + private final String number; + // ... + + public Address(String street, String number) { + this.street = street; + this.number = number; + } + + // getters omitted +} + +@Document +public class Company { + @Key + private String key; + private Person manager; + + // getters and setters omitted +} + +Employee manager = new Employee(); +manager.setName("Jane Roberts"); +manager.setHomeAddress(new Address("Park Avenue", "432/64")); +manager.setWorkAddress(new Address("Main Street", "223")); +Company comp = new Company(); +comp.setManager(manager); +``` + +The serialized document for the DB looks like this: + +```json +{ + "manager": { + "name": "Jane Roberts", + "homeAddress": { + "street": "Park Avenue", + "number": "432/64" + }, + "workAddress": { + "street": "Main Street", + "number": "223" + }, + "_class": "com.arangodb.Employee" + }, + "_class": "com.arangodb.Company" +} +``` + +Type hints are written for top-level documents (as a collection can contain different document types) as well as for every value if it's a complex type and a sub-type of the property type declared. `Map`s and `Collection`s are excluded from type mapping. Without the additional information about the concrete classes used, the document couldn't be restored in Java. The type information of the `manager` property is not enough to determine the `Employee` type. The `homeAddress` and `workAddress` properties have the same actual and defined type, thus no type hint is needed. + +### Customizing type mapping + +By default, the fully qualified class name is stored in the documents as a type hint. A custom type hint can be set with the `@TypeAlias("my-alias")` annotation on an entity. Make sure that it is an unique identifier across all entities. If we would add a `TypeAlias("employee")` annotation to the `Employee` class above, it would be persisted as `"_class": "employee"`. + +The default type key is `_class` and can be changed by overriding the `typeKey()` method of the `AbstractArangoConfiguration` class. + +If you need to further customize the type mapping process, the `arangoTypeMapper()` method of the configuration class can be overridden. The included `DefaultArangoTypeMapper` can be customized by providing a list of [`TypeInformationMapper`](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/convert/TypeInformationMapper.html)s that create aliases from types and vice versa. + +In order to fully customize the type mapping process you can provide a custom type mapper implementation by extending the `DefaultArangoTypeMapper` class. + +### Deactivating type mapping + +To deactivate the type mapping process, you can return `null` from the `typeKey()` method of the `AbstractArangoConfiguration` class. No type hints are stored in the documents with this setting. If you make sure that each defined type corresponds to the actual type, you can disable the type mapping, otherwise it can lead to exceptions when reading the entities from the DB. + +## Annotations + +### Annotation overview + +| annotation | level | description | +| ----------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| @Document | class | marks this class as a candidate for mapping | +| @Edge | class | marks this class as a candidate for mapping | +| @Id | field | stores the field as the system field \_id | +| @Key | field | stores the field as the system field \_key | +| @Rev | field | stores the field as the system field \_rev | +| @Field("alt-name") | field | stores the field with an alternative name | +| @Ref | field | stores the \_id of the referenced document and not the nested document | +| @From | field | stores the \_id of the referenced document as the system field \_from | +| @To | field | stores the \_id of the referenced document as the system field \_to | +| @Relations | field | vertices which are connected over edges | +| @Transient | field, method, annotation | marks a field to be transient for the mapping framework, thus the property will not be persisted and not further inspected by the mapping framework | +| @PersistenceConstructor | constructor | marks a given constructor - even a package protected one - to use when instantiating the object from the database | +| @TypeAlias("alias") | class | set a type alias for the class when persisted to the DB | +| @HashIndex | class | describes a hash index | +| @HashIndexed | field | describes how to index the field | +| @SkiplistIndex | class | describes a skiplist index | +| @SkiplistIndexed | field | describes how to index the field | +| @PersistentIndex | class | describes a persistent index | +| @PersistentIndexed | field | describes how to index the field | +| @GeoIndex | class | describes a geo index | +| @GeoIndexed | field | describes how to index the field | +| @FulltextIndex | class | describes a fulltext index | +| @FulltextIndexed | field | describes how to index the field | + +### Document + +The annotations `@Document` applied to a class marks this class as a candidate for mapping to the database. The most relevant parameter is `value` to specify the collection name in the database. The annotation `@Document` specifies the collection type to `DOCUMENT`. + +```java +@Document(value="persons") +public class Person { + ... +} +``` + +### Edge + +The annotations `@Edge` applied to a class marks this class as a candidate for mapping to the database. The most relevant parameter is `value` to specify the collection name in the database. The annotation `@Edge` specifies the collection type to `EDGE`. + +```java +@Edge("relations") +public class Relation { + ... +} +``` + +### Reference + +With the annotation `@Ref` applied on a field the nested object isn’t stored as a nested object in the document. The `_id` field of the nested object is stored in the document and the nested object has to be stored as a separate document in another collection described in the `@Document` annotation of the nested object class. To successfully persist an instance of your object the referencing field has to be null or it's instance has to provide a field with the annotation `@Id` including a valid id. + +```java +@Document(value="persons") +public class Person { + @Ref + private Address address; +} + +@Document("addresses") +public class Address { + @Id + private String id; + private String country; + private String street; +} +``` + +The database representation of `Person` in collection _persons_ looks as follow: + +``` +{ + "_key" : "123", + "_id" : "persons/123", + "address" : "addresses/456" +} +``` + +and the representation of `Address` in collection _addresses_: + +``` +{ + "_key" : "456", + "_id" : "addresses/456", + "country" : "...", + "street" : "..." +} +``` + +Without the annotation `@Ref` at the field `address`, the stored document would look: + +``` +{ + "_key" : "123", + "_id" : "persons/123", + "address" : { + "country" : "...", + "street" : "..." + } +} +``` + +### Relations + +With the annotation `@Relations` applied on a collection or array field in a class annotated with `@Document` the nested objects are fetched from the database over a graph traversal with your current object as the starting point. The most relevant parameter is `edge`. With `edge` you define the edge collection - which should be used in the traversal - using the class type. With the parameter `depth` you can define the maximal depth for the traversal (default 1) and the parameter `direction` defines whether the traversal should follow outgoing or incoming edges (default Direction.ANY). + +```java +@Document(value="persons") +public class Person { + @Relations(edge=Relation.class, depth=1, direction=Direction.ANY) + private List friends; +} + +@Edge(name="relations") +public class Relation { + +} +``` + +### Document with From and To + +With the annotations `@From` and `@To` applied on a collection or array field in a class annotated with `@Document` the nested edge objects are fetched from the database. Each of the nested edge objects has to be stored as separate edge document in the edge collection described in the `@Edge` annotation of the nested object class with the _\_id_ of the parent document as field _\_from_ or _\_to_. + +```java +@Document("persons") +public class Person { + @From + private List relations; +} + +@Edge(name="relations") +public class Relation { + ... +} +``` + +The database representation of `Person` in collection _persons_ looks as follow: + +``` +{ + "_key" : "123", + "_id" : "persons/123" +} +``` + +and the representation of `Relation` in collection _relations_: + +``` +{ + "_key" : "456", + "_id" : "relations/456", + "_from" : "persons/123" + "_to" : ".../..." +} +{ + "_key" : "789", + "_id" : "relations/456", + "_from" : "persons/123" + "_to" : ".../..." +} +... +``` + +### Edge with From and To + +With the annotations `@From` and `@To` applied on a field in a class annotated with `@Edge` the nested object is fetched from the database. The nested object has to be stored as a separate document in the collection described in the `@Document` annotation of the nested object class. The _\_id_ field of this nested object is stored in the fields `_from` or `_to` within the edge document. + +```java +@Edge("relations") +public class Relation { + @From + private Person c1; + @To + private Person c2; +} + +@Document(value="persons") +public class Person { + @Id + private String id; +} +``` + +The database representation of `Relation` in collection _relations_ looks as follow: + +``` +{ + "_key" : "123", + "_id" : "relations/123", + "_from" : "persons/456", + "_to" : "persons/789" +} +``` + +and the representation of `Person` in collection _persons_: + +``` +{ + "_key" : "456", + "_id" : "persons/456", +} +{ + "_key" : "789", + "_id" : "persons/789", +} +``` + +**Note:** If you want to save an instance of `Relation`, both `Person` objects (from & to) already have to be persisted and the class `Person` needs a field with the annotation `@Id` so it can hold the persisted `_id` from the database. + +### Index and Indexed annotations + +With the `@Indexed` annotations user defined indexes can be created at a collection level by annotating single fields of a class. + +Possible `@Indexed` annotations are: + +* `@HashIndexed` +* `@SkiplistIndexed` +* `@PersistentIndexed` +* `@GeoIndexed` +* `@FulltextIndexed` + +The following example creates a hash index on the field `name` and a separate hash index on the field `age`: + +```java +public class Person { + @HashIndexed + private String name; + + @HashIndexed + private int age; +} +``` + +With the `@Indexed` annotations different indexes can be created on the same field. + +The following example creates a hash index and also a skiplist index on the field `name`: + +```java +public class Person { + @HashIndexed + @SkiplistIndexed + private String name; +} +``` + +If the index should include multiple fields the `@Index` annotations can be used on the type instead. + +Possible `@Index` annotations are: + +* `@HashIndex` +* `@SkiplistIndex` +* `@PersistentIndex` +* `@GeoIndex` +* `@FulltextIndex` + +The following example creates a single hash index on the fields `name` and `age`, note that if a field is renamed in the database with @Field, the new field name must be used in the index declaration: + +```java +@HashIndex(fields = {"fullname", "age"}) +public class Person { + @Field("fullname") + private String name; + + private int age; +} +``` + +The `@Index` annotations can also be used to create an index on a nested field. + +The following example creates a single hash index on the fields `name` and `address.country`: + +```java +@HashIndex(fields = {"name", "address.country"}) +public class Person { + private String name; + + private Address address; +} +``` + +The `@Index` annotations and the `@Indexed` annotations can be used at the same time in one class. + +The following example creates a hash index on the fields `name` and `age` and a separate hash index on the field `age`: + +```java +@HashIndex(fields = {"name", "age"}) +public class Person { + private String name; + + @HashIndexed + private int age; +} +``` + +The `@Index` annotations can be used multiple times to create more than one index in this way. + +The following example creates a hash index on the fields `name` and `age` and a separate hash index on the fields `name` and `gender`: + +```java +@HashIndex(fields = {"name", "age"}) +@HashIndex(fields = {"name", "gender"}) +public class Person { + private String name; + + private int age; + + private Gender gender +} +``` diff --git a/pom.xml b/pom.xml index 38720f521..04beae57b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,359 +1,360 @@ - - 4.0.0 - - com.arangodb - arangodb-spring-data - 1.0.0-SNAPSHOT - 2017 - jar - - arangodb-spring-data - ArangoDB Spring Data - - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - repo - - - - - UTF-8 - 1.1.3 - 1.3 - 4.12 - 4.3.2 - 4.3.13.RELEASE - ${spring.version} - ${spring.version} - ${spring.version} - ${spring.version} - ${spring.version} - ${spring.version} - 1.13.9.RELEASE - - - - - mpv1989 - Mark - https://github.com/mpv1989 - - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - - arangodb-snapshots - https://oss.sonatype.org/content/groups/staging - - - spring-libs-milestone - https://repo.spring.io/libs-milestone - - - spring-libs-snapshot - https://repo.spring.io/libs-snapshot - - - - - - doclint-java8-disable - - [1.8,) - - - -Xdoclint:none - - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.5 - true - - ossrh - https://oss.sonatype.org/ - 84aff6e87e214c - false - - - - - org.apache.maven.plugins - maven-assembly-plugin - 2.4.1 - - - assembly - package - - single - - - - - - ${project.artifactId}-${project.version}-standalone - - false - false - - jar-with-dependencies - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.2 - - 1.8 - 1.8 - - - - - - org.apache.maven.plugins - maven-resources-plugin - 2.7 - - UTF-8 - - - - - org.apache.maven.plugins - maven-source-plugin - 2.4 - - - - jar - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - - attach-javadocs - - jar - - - ${javadoc.opts} - - - - - - - maven-surefire-plugin - 2.19.1 - - - **/*Test.java - **/*Example.java - - - - - - maven-deploy-plugin - 2.8.2 - - false - 10 - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - - - - - - - com.arangodb - arangodb-java-driver - - - com.arangodb - velocypack-module-jdk8 - - - com.arangodb - velocypack-module-joda - - - - org.springframework - spring-tx - - - org.springframework - spring-context - - - org.springframework - spring-beans - - - org.springframework - spring-core - - - org.springframework - spring-expression - - - - org.springframework.data - spring-data-commons - - - - org.springframework - spring-test - test - - - ch.qos.logback - logback-classic - test - - - junit - junit - test - - - org.hamcrest - hamcrest-all - test - - - - - - - com.arangodb - arangodb-java-driver - ${arangodb-java-driver.version} - - - com.arangodb - velocypack-module-jdk8 - 1.0.2 - - - com.arangodb - velocypack-module-joda - 1.0.0 - - - - org.springframework - spring-tx - ${spring-tx.version} - - - org.springframework - spring-context - ${spring-context.version} - - - org.springframework - spring-beans - ${spring-beans.version} - - - org.springframework - spring-core - ${spring-core.version} - - - org.springframework - spring-expression - ${spring-expression.version} - - - - org.springframework.data - spring-data-commons - ${spring-data-commons.version} - - - - org.springframework - spring-test - ${spring-test.version} - - - ch.qos.logback - logback-classic - ${logback-classic.version} - - - junit - junit - ${junit.version} - - - org.hamcrest - hamcrest-all - ${hamcrest-all.version} - - - - - - https://github.com/arangodb/spring-data - scm:git:git://github.com/arangodb/spring-data.git - scm:git:git://github.com/arangodb/spring-data.git - - - - ArangoDB GmbH - https://www.arangodb.com - - - + + 4.0.0 + + com.arangodb + arangodb-spring-data + 1.3.3-SNAPSHOT + 2017 + jar + + arangodb-spring-data + ArangoDB Spring Data + http://maven.apache.org + + + + Apache License 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + + UTF-8 + 1.1.3 + 1.3 + 4.12 + 4.6.0 + 4.3.13.RELEASE + ${spring.version} + ${spring.version} + ${spring.version} + ${spring.version} + ${spring.version} + ${spring.version} + 1.13.9.RELEASE + + + + + mpv1989 + Mark Vollmary + https://github.com/mpv1989 + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + arangodb-snapshots + https://oss.sonatype.org/content/groups/staging + + + spring-libs-milestone + https://repo.spring.io/libs-milestone + + + spring-libs-snapshot + https://repo.spring.io/libs-snapshot + + + + + + doclint-java8-disable + + [1.8,) + + + -Xdoclint:none + + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.5 + true + + ossrh + https://oss.sonatype.org/ + 84aff6e87e214c + false + + + + + org.apache.maven.plugins + maven-assembly-plugin + 2.4.1 + + + assembly + package + + single + + + + + + ${project.artifactId}-${project.version}-standalone + + false + false + + jar-with-dependencies + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + 1.8 + 1.8 + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.7 + + UTF-8 + + + + + org.apache.maven.plugins + maven-source-plugin + 2.4 + + + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + + attach-javadocs + + jar + + + ${javadoc.opts} + + + + + + + maven-surefire-plugin + 2.19.1 + + + **/*Test.java + **/*Example.java + + + + + + maven-deploy-plugin + 2.8.2 + + false + 10 + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + + + + + + + com.arangodb + arangodb-java-driver + + + com.arangodb + velocypack-module-jdk8 + + + com.arangodb + velocypack-module-joda + + + + org.springframework + spring-tx + + + org.springframework + spring-context + + + org.springframework + spring-beans + + + org.springframework + spring-core + + + org.springframework + spring-expression + + + + org.springframework.data + spring-data-commons + + + + org.springframework + spring-test + test + + + ch.qos.logback + logback-classic + test + + + junit + junit + test + + + org.hamcrest + hamcrest-all + test + + + + + + + com.arangodb + arangodb-java-driver + ${arangodb-java-driver.version} + + + com.arangodb + velocypack-module-jdk8 + 1.1.0 + + + com.arangodb + velocypack-module-joda + 1.1.1 + + + + org.springframework + spring-tx + ${spring-tx.version} + + + org.springframework + spring-context + ${spring-context.version} + + + org.springframework + spring-beans + ${spring-beans.version} + + + org.springframework + spring-core + ${spring-core.version} + + + org.springframework + spring-expression + ${spring-expression.version} + + + + org.springframework.data + spring-data-commons + ${spring-data-commons.version} + + + + org.springframework + spring-test + ${spring-test.version} + + + ch.qos.logback + logback-classic + ${logback-classic.version} + + + junit + junit + ${junit.version} + + + org.hamcrest + hamcrest-all + ${hamcrest-all.version} + + + + + + https://github.com/arangodb/spring-data + scm:git:git://github.com/arangodb/spring-data.git + scm:git:git://github.com/arangodb/spring-data.git + + + + ArangoDB GmbH + https://www.arangodb.com + + + diff --git a/src/main/java/com/arangodb/springframework/annotation/Document.java b/src/main/java/com/arangodb/springframework/annotation/Document.java index 5f0e00859..6439524b7 100644 --- a/src/main/java/com/arangodb/springframework/annotation/Document.java +++ b/src/main/java/com/arangodb/springframework/annotation/Document.java @@ -56,11 +56,17 @@ * of k means that k-1 replicas are kept. Any two copies reside on different DBServers. Replication between * them is synchronous, that is, every write operation to the "leader" copy will be replicated to all * "follower" replicas, before the write operation is reported successful. If a server fails, this is - * detected automatically and one of the servers holding copies take over, usually without an error being + * detected automatically and one of the servers holding copies takes over, usually without an error being * reported. */ int replicationFactor() default -1; + /** + * @return If true the collection is created as a satellite collection. In this case {@link #replicationFactor()} is + * ignored. + */ + boolean satellite() default false; + /** * @return If true then the data is synchronized to disk before returning from a document create, update, replace or * removal operation. (default: false) @@ -107,7 +113,7 @@ boolean isSystem() default false; /** - * @return The: number of buckets into which indexes using a hash table are split. The default is 16 and this number + * @return The number of buckets into which indexes using a hash table are split. The default is 16 and this number * has to be a power of 2 and less than or equal to 1024. For very large collections one should increase * this to avoid long pauses when the hash table has to be initially built or resized, since buckets are * resized individually and can be initially built in parallel. For example, 64 might be a sensible value diff --git a/src/main/java/com/arangodb/springframework/annotation/Edge.java b/src/main/java/com/arangodb/springframework/annotation/Edge.java index ac45a685b..a87fb92be 100644 --- a/src/main/java/com/arangodb/springframework/annotation/Edge.java +++ b/src/main/java/com/arangodb/springframework/annotation/Edge.java @@ -56,11 +56,17 @@ * of k means that k-1 replicas are kept. Any two copies reside on different DBServers. Replication between * them is synchronous, that is, every write operation to the "leader" copy will be replicated to all * "follower" replicas, before the write operation is reported successful. If a server fails, this is - * detected automatically and one of the servers holding copies take over, usually without an error being + * detected automatically and one of the servers holding copies takes over, usually without an error being * reported. */ int replicationFactor() default -1; + /** + * @return If true the collection is created as a satellite collection. In this case {@link #replicationFactor()} is + * ignored. + */ + boolean satellite() default false; + /** * @return If true then the data is synchronized to disk before returning from a document create, update, replace or * removal operation. (default: false) @@ -107,7 +113,7 @@ boolean isSystem() default false; /** - * @return The: number of buckets into which indexes using a hash table are split. The default is 16 and this number + * @return The number of buckets into which indexes using a hash table are split. The default is 16 and this number * has to be a power of 2 and less than or equal to 1024. For very large collections one should increase * this to avoid long pauses when the hash table has to be initially built or resized, since buckets are * resized individually and can be initially built in parallel. For example, 64 might be a sensible value diff --git a/src/main/java/com/arangodb/springframework/annotation/EnableArangoRepositories.java b/src/main/java/com/arangodb/springframework/annotation/EnableArangoRepositories.java index 12fa3d74a..6f917f63e 100644 --- a/src/main/java/com/arangodb/springframework/annotation/EnableArangoRepositories.java +++ b/src/main/java/com/arangodb/springframework/annotation/EnableArangoRepositories.java @@ -29,12 +29,17 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; import com.arangodb.springframework.repository.ArangoRepositoriesRegistrar; import com.arangodb.springframework.repository.ArangoRepositoryFactoryBean; /** - * Created by F625633 on 07/07/2017. + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @@ -53,10 +58,23 @@ ComponentScan.Filter[] excludeFilters() default {}; - String repositoryImplementationPostfix() default ""; + /** + * Returns the postfix to be used for custom repository implementations. Defaults to {@literal Impl}. + */ + String repositoryImplementationPostfix() default "Impl"; Class repositoryFactoryBeanClass() default ArangoRepositoryFactoryBean.class; + /** + * Configures the location of the Spring Data named queries properties file. Defaults to + * {@code META-INF/arango-named-queries.properties}. + */ String namedQueriesLocation() default ""; + /** + * Returns the key of the {@link QueryLookupStrategy} that should be used to lookup queries for query methods. + * Currently only the default {@link Key#CREATE_IF_NOT_FOUND} is supported. + */ + Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; + } diff --git a/src/main/java/com/arangodb/springframework/annotation/From.java b/src/main/java/com/arangodb/springframework/annotation/From.java index d78b09f23..fb7f44a3b 100644 --- a/src/main/java/com/arangodb/springframework/annotation/From.java +++ b/src/main/java/com/arangodb/springframework/annotation/From.java @@ -34,7 +34,7 @@ public @interface From { /** - * @return whether the entity should be loaded lazy + * @return whether the entity should be loaded lazily */ boolean lazy() default false; diff --git a/src/main/java/com/arangodb/springframework/annotation/Key.java b/src/main/java/com/arangodb/springframework/annotation/Key.java index e2bb130f9..10cfaddc7 100644 --- a/src/main/java/com/arangodb/springframework/annotation/Key.java +++ b/src/main/java/com/arangodb/springframework/annotation/Key.java @@ -1,36 +1,39 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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; - -/** - * @author Mark Vollmary - * - */ -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.FIELD }) -public @interface Key { - -} +/* + * DISCLAIMER + * + * Copyright 2017 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; + +/** + * @deprecated Represents the document field {@code _key}. Will be removed in 3.0.0, please use + * {@link org.springframework.data.annotation.Id} then instead. + * + * @author Mark Vollmary + */ +@Deprecated +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD }) +public @interface Key { + +} diff --git a/src/main/java/com/arangodb/springframework/annotation/Param.java b/src/main/java/com/arangodb/springframework/annotation/Param.java index f7c036aa0..36208a546 100644 --- a/src/main/java/com/arangodb/springframework/annotation/Param.java +++ b/src/main/java/com/arangodb/springframework/annotation/Param.java @@ -26,8 +26,14 @@ import java.lang.annotation.Target; /** - * Created by F625633 on 13/07/2017. + * @deprecated. Will be removed in 3.0.0, please use {@link org.springframework.data.repository.query.Param} instead. + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner */ +@Deprecated @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface Param { diff --git a/src/main/java/com/arangodb/springframework/annotation/Query.java b/src/main/java/com/arangodb/springframework/annotation/Query.java index de49de1de..5a3dce99a 100644 --- a/src/main/java/com/arangodb/springframework/annotation/Query.java +++ b/src/main/java/com/arangodb/springframework/annotation/Query.java @@ -25,11 +25,18 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.data.annotation.QueryAnnotation; + /** - * Created by F625633 on 12/07/2017. + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) +@QueryAnnotation public @interface Query { /** diff --git a/src/main/java/com/arangodb/springframework/annotation/QueryOptions.java b/src/main/java/com/arangodb/springframework/annotation/QueryOptions.java index 6c2fdd237..3fd744ef3 100644 --- a/src/main/java/com/arangodb/springframework/annotation/QueryOptions.java +++ b/src/main/java/com/arangodb/springframework/annotation/QueryOptions.java @@ -87,4 +87,26 @@ */ int ttl() default -1; + /** + * @since ArangoDB 3.4.0 + * @return Specify true and the query will be executed in a streaming fashion. The query result is not stored on the + * server, but calculated on the fly. Beware: long-running queries will need to hold the collection locks + * for as long as the query cursor exists. When set to false a query will be executed right away in its + * entirety. In that case query results are either returned right away (if the resultset is small enough), + * or stored on the arangod instance and accessible via the cursor API (with respect to the ttl). It is + * advisable to only use this option on short-running queries or without exclusive locks (write-locks on + * MMFiles). Please note that the query options cache, count and fullCount will not work on streaming + * queries. Additionally query statistics, warnings and profiling data will only be available after the + * query is finished. The default value is false + */ + boolean stream() default false; + + /** + * @since ArangoDB 3.1.0 + * @return the maximum number of memory (measured in bytes) that the query is allowed to use. If set, then the query + * will fail with error "resource limit exceeded" in case it allocates too much memory. A value of 0 + * indicates that there is no memory limit. + */ + long memoryLimit() default -1; + } diff --git a/src/main/java/com/arangodb/springframework/annotation/Ref.java b/src/main/java/com/arangodb/springframework/annotation/Ref.java index 5c2d4ef52..bac07edd3 100644 --- a/src/main/java/com/arangodb/springframework/annotation/Ref.java +++ b/src/main/java/com/arangodb/springframework/annotation/Ref.java @@ -37,7 +37,7 @@ public @interface Ref { /** - * @return whether the entity should be loaded lazy + * @return whether the entity should be loaded lazily */ boolean lazy() default false; diff --git a/src/main/java/com/arangodb/springframework/annotation/Relations.java b/src/main/java/com/arangodb/springframework/annotation/Relations.java index 6c40e3ffe..404ba042f 100644 --- a/src/main/java/com/arangodb/springframework/annotation/Relations.java +++ b/src/main/java/com/arangodb/springframework/annotation/Relations.java @@ -58,7 +58,7 @@ public enum Direction { Direction direction() default Direction.ANY; /** - * @return whether the entity should be loaded lazy + * @return whether the entity should be loaded lazily */ boolean lazy() default false; diff --git a/src/main/java/com/arangodb/springframework/annotation/To.java b/src/main/java/com/arangodb/springframework/annotation/To.java index d5686b765..fc39b9d86 100644 --- a/src/main/java/com/arangodb/springframework/annotation/To.java +++ b/src/main/java/com/arangodb/springframework/annotation/To.java @@ -34,7 +34,7 @@ public @interface To { /** - * @return whether the entity should be loaded lazy + * @return whether the entity should be loaded lazily */ boolean lazy() default false; diff --git a/src/main/java/com/arangodb/springframework/config/AbstractArangoConfiguration.java b/src/main/java/com/arangodb/springframework/config/AbstractArangoConfiguration.java index 5ffc43c12..d4a717228 100644 --- a/src/main/java/com/arangodb/springframework/config/AbstractArangoConfiguration.java +++ b/src/main/java/com/arangodb/springframework/config/AbstractArangoConfiguration.java @@ -1,141 +1,159 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.config; - -import java.lang.annotation.Annotation; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.mapping.model.FieldNamingStrategy; -import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; - -import com.arangodb.ArangoDB; -import com.arangodb.ArangoDBException; -import com.arangodb.springframework.annotation.From; -import com.arangodb.springframework.annotation.Ref; -import com.arangodb.springframework.annotation.Relations; -import com.arangodb.springframework.annotation.To; -import com.arangodb.springframework.core.ArangoOperations; -import com.arangodb.springframework.core.convert.ArangoConverter; -import com.arangodb.springframework.core.convert.ArangoCustomConversions; -import com.arangodb.springframework.core.convert.CustomConversions; -import com.arangodb.springframework.core.convert.DefaultArangoConverter; -import com.arangodb.springframework.core.convert.resolver.FromResolver; -import com.arangodb.springframework.core.convert.resolver.RefResolver; -import com.arangodb.springframework.core.convert.resolver.ReferenceResolver; -import com.arangodb.springframework.core.convert.resolver.RelationResolver; -import com.arangodb.springframework.core.convert.resolver.RelationsResolver; -import com.arangodb.springframework.core.convert.resolver.ResolverFactory; -import com.arangodb.springframework.core.convert.resolver.ToResolver; -import com.arangodb.springframework.core.mapping.ArangoMappingContext; -import com.arangodb.springframework.core.template.ArangoTemplate; -import com.arangodb.velocypack.module.jdk8.VPackJdk8Module; -import com.arangodb.velocypack.module.joda.VPackJodaModule; - -/** - * @author Mark Vollmary - * - */ -@Configuration -public abstract class AbstractArangoConfiguration { - - @Bean - public abstract ArangoDB.Builder arango(); - - @Bean - public abstract String database(); - - private ArangoDB.Builder configure(final ArangoDB.Builder arango) { - return arango.registerModules(new VPackJdk8Module(), new VPackJodaModule()); - } - - @Bean - public ArangoOperations arangoTemplate() throws Exception { - return new ArangoTemplate(configure(arango()), database(), arangoConverter()); - } - - @Bean - public ArangoMappingContext arangoMappingContext() throws Exception { - final ArangoMappingContext context = new ArangoMappingContext(); - context.setInitialEntitySet(getInitialEntitySet()); - context.setFieldNamingStrategy(fieldNamingStrategy()); - context.setSimpleTypeHolder(customConversions().getSimpleTypeHolder()); - return context; - } - - @Bean - public CustomConversions customConversions() { - return new ArangoCustomConversions(Collections.emptyList()); - } - - private Set> getInitialEntitySet() throws ClassNotFoundException { - return ArangoEntityClassScanner.scanForEntities(getEntityBasePackages()); - } - - protected String[] getEntityBasePackages() { - return new String[] { getClass().getPackage().getName() }; - } - - protected FieldNamingStrategy fieldNamingStrategy() { - return PropertyNameFieldNamingStrategy.INSTANCE; - } - - @Bean - public ArangoConverter arangoConverter() throws Exception { - return new DefaultArangoConverter(arangoMappingContext(), customConversions(), new ResolverFactory() { - @SuppressWarnings("unchecked") - @Override - public Optional> getReferenceResolver(final A annotation) { - ReferenceResolver resolver = null; - try { - if (annotation instanceof Ref) { - resolver = (ReferenceResolver) new RefResolver(arangoTemplate()); - } - } catch (final Exception e) { - throw new ArangoDBException(e); - } - return Optional.ofNullable(resolver); - } - - @SuppressWarnings("unchecked") - @Override - public Optional> getRelationResolver(final A annotation) { - RelationResolver resolver = null; - try { - if (annotation instanceof From) { - resolver = (RelationResolver) new FromResolver(arangoTemplate()); - } else if (annotation instanceof To) { - resolver = (RelationResolver) new ToResolver(arangoTemplate()); - } else if (annotation instanceof Relations) { - resolver = (RelationResolver) new RelationsResolver(arangoTemplate()); - } - } catch (final Exception e) { - throw new ArangoDBException(e); - } - return Optional.ofNullable(resolver); - } - }); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.config; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mapping.model.FieldNamingStrategy; +import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; + +import com.arangodb.ArangoDB; +import com.arangodb.ArangoDBException; +import com.arangodb.springframework.annotation.From; +import com.arangodb.springframework.annotation.Ref; +import com.arangodb.springframework.annotation.Relations; +import com.arangodb.springframework.annotation.To; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.core.convert.ArangoConverter; +import com.arangodb.springframework.core.convert.ArangoCustomConversions; +import com.arangodb.springframework.core.convert.ArangoTypeMapper; +import com.arangodb.springframework.core.convert.CustomConversions; +import com.arangodb.springframework.core.convert.DBEntityModule; +import com.arangodb.springframework.core.convert.DefaultArangoConverter; +import com.arangodb.springframework.core.convert.DefaultArangoTypeMapper; +import com.arangodb.springframework.core.convert.resolver.FromResolver; +import com.arangodb.springframework.core.convert.resolver.RefResolver; +import com.arangodb.springframework.core.convert.resolver.ReferenceResolver; +import com.arangodb.springframework.core.convert.resolver.RelationResolver; +import com.arangodb.springframework.core.convert.resolver.RelationsResolver; +import com.arangodb.springframework.core.convert.resolver.ResolverFactory; +import com.arangodb.springframework.core.convert.resolver.ToResolver; +import com.arangodb.springframework.core.mapping.ArangoMappingContext; +import com.arangodb.springframework.core.template.ArangoTemplate; +import com.arangodb.velocypack.module.jdk8.VPackJdk8Module; +import com.arangodb.velocypack.module.joda.VPackJodaModule; + +/** + * @author Mark Vollmary + * @author Christian Lechner + * + */ +@Configuration +public abstract class AbstractArangoConfiguration { + + protected abstract ArangoDB.Builder arango(); + + protected abstract String database(); + + private ArangoDB.Builder configure(final ArangoDB.Builder arango) { + return arango.registerModules(new VPackJdk8Module(), new VPackJodaModule(), new DBEntityModule()); + } + + @Bean + public ArangoOperations arangoTemplate() throws Exception { + return new ArangoTemplate(configure(arango()), database(), arangoConverter()); + } + + @Bean + public ArangoMappingContext arangoMappingContext() throws Exception { + final ArangoMappingContext context = new ArangoMappingContext(); + context.setInitialEntitySet(getInitialEntitySet()); + context.setFieldNamingStrategy(fieldNamingStrategy()); + context.setSimpleTypeHolder(customConversions().getSimpleTypeHolder()); + return context; + } + + @Bean + public ArangoConverter arangoConverter() throws Exception { + return new DefaultArangoConverter(arangoMappingContext(), customConversions(), resolverFactory(), + arangoTypeMapper()); + } + + protected CustomConversions customConversions() { + return new ArangoCustomConversions(Collections.emptyList()); + } + + private Set> getInitialEntitySet() throws ClassNotFoundException { + return ArangoEntityClassScanner.scanForEntities(getEntityBasePackages()); + } + + protected String[] getEntityBasePackages() { + return new String[] { getClass().getPackage().getName() }; + } + + protected FieldNamingStrategy fieldNamingStrategy() { + return PropertyNameFieldNamingStrategy.INSTANCE; + } + + protected String typeKey() { + return DefaultArangoTypeMapper.DEFAULT_TYPE_KEY; + } + + protected ArangoTypeMapper arangoTypeMapper() throws Exception { + return new DefaultArangoTypeMapper(typeKey(), arangoMappingContext()); + } + + protected ResolverFactory resolverFactory() { + return new ResolverFactory() { + @SuppressWarnings("unchecked") + @Override + public Optional> getReferenceResolver(final A annotation) { + ReferenceResolver resolver = null; + try { + if (annotation instanceof Ref) { + resolver = (ReferenceResolver) new RefResolver(arangoTemplate(), + arangoConverter().getConversionService()); + } + } catch (final Exception e) { + throw new ArangoDBException(e); + } + return Optional.ofNullable(resolver); + } + + @SuppressWarnings("unchecked") + @Override + public Optional> getRelationResolver(final A annotation) { + RelationResolver resolver = null; + try { + if (annotation instanceof From) { + resolver = (RelationResolver) new FromResolver(arangoTemplate(), + arangoConverter().getConversionService()); + } else if (annotation instanceof To) { + resolver = (RelationResolver) new ToResolver(arangoTemplate(), + arangoConverter().getConversionService()); + } else if (annotation instanceof Relations) { + resolver = (RelationResolver) new RelationsResolver(arangoTemplate(), + arangoConverter().getConversionService()); + } + } catch (final Exception e) { + throw new ArangoDBException(e); + } + return Optional.ofNullable(resolver); + } + }; + } + +} diff --git a/src/main/java/com/arangodb/springframework/config/ArangoEntityClassScanner.java b/src/main/java/com/arangodb/springframework/config/ArangoEntityClassScanner.java index 0ba4e7257..8f5fb0af3 100644 --- a/src/main/java/com/arangodb/springframework/config/ArangoEntityClassScanner.java +++ b/src/main/java/com/arangodb/springframework/config/ArangoEntityClassScanner.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.data.annotation.TypeAlias; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -35,12 +36,16 @@ /** * @author Mark Vollmary + * @author Christian Lechner * */ public class ArangoEntityClassScanner { @SuppressWarnings("unchecked") private static final Class[] ENTITY_ANNOTATIONS = new Class[] { Document.class, Edge.class }; + + @SuppressWarnings("unchecked") + private static final Class[] ADDITIONAL_ANNOTATIONS = new Class[] { TypeAlias.class }; public static Set> scanForEntities(final String... basePackages) throws ClassNotFoundException { final Set> entities = new HashSet<>(); @@ -58,6 +63,9 @@ public static Set> scanForEntities(final String basePackage) throws Cla for (final Class annotationType : ENTITY_ANNOTATIONS) { componentProvider.addIncludeFilter(new AnnotationTypeFilter(annotationType)); } + for (final Class annotationType : ADDITIONAL_ANNOTATIONS) { + componentProvider.addIncludeFilter(new AnnotationTypeFilter(annotationType)); + } for (final BeanDefinition definition : componentProvider.findCandidateComponents(basePackage)) { entities.add(ClassUtils.forName(definition.getBeanClassName(), null)); } diff --git a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java index 80cfd1168..b762a142a 100644 --- a/src/main/java/com/arangodb/springframework/core/ArangoOperations.java +++ b/src/main/java/com/arangodb/springframework/core/ArangoOperations.java @@ -67,7 +67,7 @@ public interface ArangoOperations { * @param query * contains the query string to be executed * @param bindVars - * key/value pairs representing the bind parameters + * key/value pairs representing the bind parameters, can be null * @param options * Additional options, can be null * @param entityClass @@ -78,6 +78,20 @@ public interface ArangoOperations { ArangoCursor query(String query, Map bindVars, AqlQueryOptions options, Class entityClass) throws DataAccessException; + /** + * Create a cursor and return the first results. For queries without bind parameters. + * + * @param query + * contains the query string to be executed + * @param options + * Additional options, can be null + * @param entityClass + * The entity type of the result + * @return cursor of the results + * @throws DataAccessException + */ + ArangoCursor query(String query, AqlQueryOptions options, Class entityClass) throws DataAccessException; + /** * Removes multiple document * @@ -354,6 +368,36 @@ MultiDocumentEntity insert(Iterable values, Cla */ DocumentEntity insert(T value) throws DataAccessException; + /** + * Creates a new document from the given document, unless there is already a document with the _key given. If no + * _key is given, a new unique _key is generated automatically. + * + * @param collectionName + * Name of the collection in which the new document should be inserted + * @param value + * A representation of a single document + * @param options + * Additional options, can be null + * @return information about the document + * @throws DataAccessException + */ + DocumentEntity insert(String collectionName, Object value, DocumentCreateOptions options) + throws DataAccessException; + + /** + * + * Creates a new document from the given document, unless there is already a document with the _key given. If no + * _key is given, a new unique _key is generated automatically. + * + * @param collectionName + * Name of the collection in which the new document should be inserted + * @param value + * A representation of a single document + * @return information about the document + * @throws DataAccessException + */ + DocumentEntity insert(String collectionName, Object value) throws DataAccessException; + public enum UpsertStrategy { REPLACE, UPDATE } @@ -362,26 +406,54 @@ public enum UpsertStrategy { * Creates a new document from the given document, unless there is already a document with the id given. In that * case it updates or replaces the document, depending on the chosen strategy. * + * @deprecated use {@link #repsert(Object)} instead * @param value * A representation of a single document * @param strategy * The strategy to use when not inserting the document * @throws DataAccessException */ + @Deprecated void upsert(T value, UpsertStrategy strategy) throws DataAccessException; /** * Creates new documents from the given documents, unless there already exists. In that case it updates or replaces * the documents, depending on the chosen strategy. * + * @deprecated use {@link #repsert(Iterable)} instead * @param value * A List of documents * @param strategy * The strategy to use when not inserting the document * @throws DataAccessException */ + @Deprecated void upsert(Iterable value, UpsertStrategy strategy) throws DataAccessException; + /** + * Creates a new document from the given document, unless there is already a document with the id given. In that + * case it replaces the document. + * + * @param value + * A representation of a single document + * @throws DataAccessException + * @since ArangoDB 3.4 + */ + void repsert(T value) throws DataAccessException; + + /** + * Creates new documents from the given documents, unless there already exists. In that case it replaces the + * documents. + * + * @param value + * A List of documents + * @param entityClass + * The entity type of the documents + * @throws DataAccessException + * @since ArangoDB 3.4 + */ + void repsert(Iterable value, Class entityClass) throws DataAccessException; + /** * Checks whether the document exists by reading a single document head * diff --git a/src/main/java/com/arangodb/springframework/core/convert/ArangoConverter.java b/src/main/java/com/arangodb/springframework/core/convert/ArangoConverter.java index f18cd86cc..3f09042c2 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/ArangoConverter.java +++ b/src/main/java/com/arangodb/springframework/core/convert/ArangoConverter.java @@ -1,43 +1,44 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.data.mapping.context.MappingContext; - -import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; -import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; - -/** - * @author Mark Vollmary - * - */ -public interface ArangoConverter extends ArangoEntityReader, ArangoEntityWriter { - - MappingContext, ArangoPersistentProperty> getMappingContext(); - - boolean isCollectionType(Class type); - - boolean isEntityType(Class type); - - GenericConversionService getConversionService(); - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import org.springframework.data.convert.EntityConverter; + +import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; +import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; + +/** + * @author Mark Vollmary + * @author Christian Lechner + * + */ +@SuppressWarnings("deprecation") +public interface ArangoConverter + extends EntityConverter, ArangoPersistentProperty, Object, DBEntity>, + ArangoEntityReader, ArangoEntityWriter { + + boolean isCollectionType(Class type); + + boolean isEntityType(Class type); + + ArangoTypeMapper getTypeMapper(); + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/ArangoCustomConversions.java b/src/main/java/com/arangodb/springframework/core/convert/ArangoCustomConversions.java index b0b449ebc..6773a4679 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/ArangoCustomConversions.java +++ b/src/main/java/com/arangodb/springframework/core/convert/ArangoCustomConversions.java @@ -24,17 +24,11 @@ /** * @author Mark Vollmary + * @author Christian Lechner * */ public class ArangoCustomConversions extends CustomConversions { - // private static final StoreConversions STORE_CONVERSIONS; - - // static { - // final Collection converters = new ArrayList<>(); - // STORE_CONVERSIONS = StoreConversions.of(SimpleTypeHolder.DEFAULT, converters); - // } - public ArangoCustomConversions(final Collection converters) { super(converters); } diff --git a/src/main/java/com/arangodb/springframework/core/convert/ArangoEntityReader.java b/src/main/java/com/arangodb/springframework/core/convert/ArangoEntityReader.java index 19c1b0a54..98be945d4 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/ArangoEntityReader.java +++ b/src/main/java/com/arangodb/springframework/core/convert/ArangoEntityReader.java @@ -1,31 +1,32 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -import org.springframework.data.convert.EntityReader; - -/** - * @author Mark Vollmary - * - */ -public interface ArangoEntityReader extends EntityReader { - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import org.springframework.data.convert.EntityReader; + +/** + * @author Mark Vollmary + * + */ +@SuppressWarnings("deprecation") +public interface ArangoEntityReader extends EntityReader { + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/ArangoEntityWriter.java b/src/main/java/com/arangodb/springframework/core/convert/ArangoEntityWriter.java index 0adde9b2f..5ebb92b8a 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/ArangoEntityWriter.java +++ b/src/main/java/com/arangodb/springframework/core/convert/ArangoEntityWriter.java @@ -1,31 +1,32 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -import org.springframework.data.convert.EntityWriter; - -/** - * @author Mark Vollmary - * - */ -public interface ArangoEntityWriter extends EntityWriter { - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import org.springframework.data.convert.EntityWriter; + +/** + * @author Mark Vollmary + * + */ +@SuppressWarnings("deprecation") +public interface ArangoEntityWriter extends EntityWriter { + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/ArangoSimpleTypeConverters.java b/src/main/java/com/arangodb/springframework/core/convert/ArangoSimpleTypeConverters.java new file mode 100644 index 000000000..b26f1f9fe --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/convert/ArangoSimpleTypeConverters.java @@ -0,0 +1,106 @@ +/* + * 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.convert; + +import java.sql.Timestamp; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; + +import javax.xml.bind.DatatypeConverter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; + +import com.arangodb.velocypack.internal.util.DateUtil; + +/** + * This class contains converters that are necessary to restore the original types defined in entities. Normally the + * Java driver would do this, but he has no type information available (since he is deserializing to + * {@link com.arangodb.springframework.core.convert.DBEntity} objects). + * + * @author Christian Lechner + * + */ +public class ArangoSimpleTypeConverters { + + public static Collection> getConvertersToRegister() { + final List> converters = new ArrayList<>(); + + converters.add(Base64StringToByteArrayConverter.INSTANCE); + converters.add(StringToDateConverter.INSTANCE); + converters.add(StringToSqlDateConverter.INSTANCE); + converters.add(StringToSqlTimestampConverter.INSTANCE); + + return converters; + } + + private static Date parse(final String source) { + try { + return DateUtil.parse(source); + } catch (final ParseException e) { + throw new IllegalArgumentException(e); + } + } + + @ReadingConverter + public static enum Base64StringToByteArrayConverter implements Converter { + INSTANCE; + + @Override + public byte[] convert(final String source) { + return DatatypeConverter.parseBase64Binary(source); + } + } + + @ReadingConverter + public static enum StringToDateConverter implements Converter { + INSTANCE; + + @Override + public Date convert(final String source) { + return parse(source); + } + } + + @ReadingConverter + public static enum StringToSqlDateConverter implements Converter { + INSTANCE; + + @Override + public java.sql.Date convert(final String source) { + return new java.sql.Date(parse(source).getTime()); + } + } + + @ReadingConverter + public static enum StringToSqlTimestampConverter implements Converter { + INSTANCE; + + @Override + public Timestamp convert(final String source) { + return new Timestamp(parse(source).getTime()); + } + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/ArangoTypeMapper.java b/src/main/java/com/arangodb/springframework/core/convert/ArangoTypeMapper.java new file mode 100644 index 000000000..a82caadd2 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/convert/ArangoTypeMapper.java @@ -0,0 +1,34 @@ +/* + * 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.convert; + +import org.springframework.data.convert.TypeMapper; + +/** + * @author Christian Lechner + * + */ +@SuppressWarnings("deprecation") +public interface ArangoTypeMapper extends TypeMapper { + + boolean isTypeKey(String key); + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/CustomConversions.java b/src/main/java/com/arangodb/springframework/core/convert/CustomConversions.java index 4526dc25a..2d842c93a 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/CustomConversions.java +++ b/src/main/java/com/arangodb/springframework/core/convert/CustomConversions.java @@ -41,7 +41,6 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.data.convert.JodaTimeConverters; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; import org.springframework.data.mapping.model.SimpleTypeHolder; @@ -83,9 +82,9 @@ protected CustomConversions(final Collection converters) { // Add user provided converters to make sure they can override the defaults toRegister.addAll(converters); toRegister.add(CustomToStringConverter.INSTANCE); - toRegister.addAll(JodaTimeConverters.getConvertersToRegister()); toRegister.addAll(TimeStringConverters.getConvertersToRegister()); toRegister.addAll(JodaTimeStringConverters.getConvertersToRegister()); + toRegister.addAll(ArangoSimpleTypeConverters.getConvertersToRegister()); for (final Object c : toRegister) { registerConversion(c); diff --git a/src/main/java/com/arangodb/springframework/core/convert/DBCollectionEntity.java b/src/main/java/com/arangodb/springframework/core/convert/DBCollectionEntity.java index ba8194807..ab28adc43 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/DBCollectionEntity.java +++ b/src/main/java/com/arangodb/springframework/core/convert/DBCollectionEntity.java @@ -1,52 +1,54 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -import java.util.ArrayList; -import java.util.Collection; - -/** - * @author Mark Vollmary - * - */ -public class DBCollectionEntity extends ArrayList implements DBEntity { - - private static final long serialVersionUID = -2068955559598596722L; - - public DBCollectionEntity() { - super(); - } - - public DBCollectionEntity(final Collection c) { - super(c); - } - - @Override - public Object put(final String key, final Object value) { - throw new UnsupportedOperationException(); - } - - @Override - public Object get(final Object key) { - throw new UnsupportedOperationException(); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * @deprecated Will be removed in 3.0.0 + * + * @author Mark Vollmary + */ +@Deprecated +public class DBCollectionEntity extends ArrayList implements DBEntity { + + private static final long serialVersionUID = -2068955559598596722L; + + public DBCollectionEntity() { + super(); + } + + public DBCollectionEntity(final Collection c) { + super(c); + } + + @Override + public Object put(final String key, final Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Object get(final Object key) { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/DBDocumentEntity.java b/src/main/java/com/arangodb/springframework/core/convert/DBDocumentEntity.java index 0f2a3f062..f8d4b7c9b 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/DBDocumentEntity.java +++ b/src/main/java/com/arangodb/springframework/core/convert/DBDocumentEntity.java @@ -1,47 +1,52 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author Mark Vollmary - * - */ -public class DBDocumentEntity extends HashMap implements DBEntity { - - private static final long serialVersionUID = -7251842887063588024L; - - public DBDocumentEntity() { - super(); - } - - public DBDocumentEntity(final Map m) { - super(m); - } - - @Override - public boolean add(final Object value) { - throw new UnsupportedOperationException(); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Mark Vollmary + * + */ +@SuppressWarnings("deprecation") +public class DBDocumentEntity extends HashMap implements DBEntity { + + private static final long serialVersionUID = -7251842887063588024L; + + public DBDocumentEntity() { + super(); + } + + public DBDocumentEntity(final Map m) { + super(m); + } + + /** + * @deprecated Will be removed in 3.0.0 + */ + @Override + @Deprecated + public boolean add(final Object value) { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/DBEntity.java b/src/main/java/com/arangodb/springframework/core/convert/DBEntity.java index 2107750f4..696f6dd6c 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/DBEntity.java +++ b/src/main/java/com/arangodb/springframework/core/convert/DBEntity.java @@ -1,35 +1,39 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -/** - * @author Mark Vollmary - * - */ -public interface DBEntity { - - Object put(String key, Object value); - - Object get(Object key); - - boolean add(Object value); - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import com.arangodb.entity.DocumentEntity; + +/** + * @deprecated Will be removed in 3.0.0, please use {@link DocumentEntity} instead + * + * @author Mark Vollmary + */ +@Deprecated +public interface DBEntity { + + Object put(String key, Object value); + + Object get(Object key); + + boolean add(Object value); + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/DBEntityDeserializer.java b/src/main/java/com/arangodb/springframework/core/convert/DBEntityDeserializer.java index 131d0e7c0..f2dad0587 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/DBEntityDeserializer.java +++ b/src/main/java/com/arangodb/springframework/core/convert/DBEntityDeserializer.java @@ -1,43 +1,44 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -import com.arangodb.velocypack.VPackDeserializationContext; -import com.arangodb.velocypack.VPackDeserializer; -import com.arangodb.velocypack.VPackSlice; -import com.arangodb.velocypack.exception.VPackException; - -/** - * @author Mark Vollmary - * - */ -public class DBEntityDeserializer implements VPackDeserializer { - - @Override - public DBEntity deserialize( - final VPackSlice parent, - final VPackSlice vpack, - final VPackDeserializationContext context) throws VPackException { - final Class type = vpack.isObject() ? DBDocumentEntity.class : DBCollectionEntity.class; - return (DBEntity) context.deserialize(vpack, type); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import com.arangodb.velocypack.VPackDeserializationContext; +import com.arangodb.velocypack.VPackDeserializer; +import com.arangodb.velocypack.VPackSlice; +import com.arangodb.velocypack.exception.VPackException; + +/** + * @author Mark Vollmary + * + */ +@SuppressWarnings("deprecation") +public class DBEntityDeserializer implements VPackDeserializer { + + @Override + public DBEntity deserialize( + final VPackSlice parent, + final VPackSlice vpack, + final VPackDeserializationContext context) throws VPackException { + final Class type = vpack.isObject() ? DBDocumentEntity.class : DBCollectionEntity.class; + return (DBEntity) context.deserialize(vpack, type); + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/DBEntityModule.java b/src/main/java/com/arangodb/springframework/core/convert/DBEntityModule.java new file mode 100644 index 000000000..9d4ba45d0 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/convert/DBEntityModule.java @@ -0,0 +1,62 @@ +/* + * 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.convert; + +import java.util.Collection; +import java.util.Map; + +import com.arangodb.velocypack.VPackInstanceCreator; +import com.arangodb.velocypack.VPackModule; +import com.arangodb.velocypack.VPackSetupContext; + +/** + * @author Christian Lechner + * + */ +@SuppressWarnings("deprecation") +public class DBEntityModule implements VPackModule { + + @Override + public > void setup(final C context) { + context.registerInstanceCreator(Map.class, new DBDocumentEntityInstantiator()) + .registerInstanceCreator(Collection.class, new DBCollectionEntityInstantiator()) + .registerDeserializer(DBEntity.class, new DBEntityDeserializer()); + } + + public static class DBDocumentEntityInstantiator implements VPackInstanceCreator> { + + @Override + public Map createInstance() { + return new DBDocumentEntity(); + } + + } + + public static class DBCollectionEntityInstantiator implements VPackInstanceCreator> { + + @Override + public Collection createInstance() { + return new DBCollectionEntity(); + } + + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoConverter.java b/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoConverter.java index 0ab508489..71a97aff1 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoConverter.java +++ b/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoConverter.java @@ -1,517 +1,551 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.springframework.core.CollectionFactory; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.data.convert.EntityInstantiator; -import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; -import org.springframework.data.mapping.model.MappingException; -import org.springframework.data.mapping.model.ParameterValueProvider; -import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; -import org.springframework.data.mapping.model.PropertyValueProvider; -import org.springframework.data.util.ClassTypeInformation; -import org.springframework.data.util.TypeInformation; -import org.springframework.util.CollectionUtils; - -import com.arangodb.entity.BaseDocument; -import com.arangodb.entity.BaseEdgeDocument; -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.velocypack.VPackSlice; - -/** - * @author Mark Vollmary - * - */ -public class DefaultArangoConverter implements ArangoConverter { - - private static final String _ID = "_id"; - private static final String _KEY = "_key"; - private final MappingContext, ArangoPersistentProperty> context; - private final CustomConversions conversions; - private final GenericConversionService conversionService; - private final EntityInstantiators instantiators; - private final ResolverFactory resolverFactory; - - public DefaultArangoConverter( - final MappingContext, ArangoPersistentProperty> context, - final CustomConversions conversions, final ResolverFactory resolverFactory) { - super(); - this.context = context; - this.conversions = conversions; - this.resolverFactory = resolverFactory; - conversionService = new DefaultConversionService(); - conversions.registerConvertersIn(conversionService); - instantiators = new EntityInstantiators(); - } - - @Override - public MappingContext, ArangoPersistentProperty> getMappingContext() { - return context; - } - - @SuppressWarnings("unchecked") - @Override - public R read(final Class type, final DBEntity source) { - return (R) read(ClassTypeInformation.from(type), source); - } - - private Object read(final TypeInformation type, final DBEntity source) { - if (source == null) { - return null; - } - if (conversions.hasCustomReadTarget(type.getType(), type.getType())) { - return conversionService.convert(source, type.getType()); - } - if (isMapType(type.getType()) && source instanceof DBDocumentEntity) { - return readMap(type, DBDocumentEntity.class.cast(source)); - } - if (type.isCollectionLike() && source instanceof DBCollectionEntity) { - return readCollection(type, DBCollectionEntity.class.cast(source)); - } - final Optional> entity = Optional - .ofNullable(context.getPersistentEntity(type.getType())); - return read(type, source, entity); - } - - @SuppressWarnings("unchecked") - private Object readMap(final TypeInformation type, final DBDocumentEntity source) { - final Class keyType = type.getComponentType().getType(); - final TypeInformation valueType = type.getMapValueType(); - final Map map = CollectionFactory.createMap(type.getType(), keyType, source.size()); - for (final Map.Entry entry : source.entrySet()) { - final Object key = conversionService.convert(entry.getKey(), keyType); - final Object value = entry.getValue(); - if (value instanceof DBEntity) { - map.put(key, read(valueType, (DBEntity) value)); - } else if (value instanceof Map) { - map.put(key, read(valueType, new DBDocumentEntity((Map) value))); - } else if (value instanceof Collection) { - map.put(key, read(valueType, new DBCollectionEntity((Collection) value))); - } else if (isSimpleType(valueType.getType())) { - final Optional> customWriteTarget = Optional - .ofNullable(conversions.getCustomWriteTarget(valueType.getType())); - final Class targetType = customWriteTarget.orElseGet(() -> valueType.getType()); - map.put(key, conversionService.convert(value, targetType)); - } else { - map.put(key, value); - } - } - return map; - } - - @SuppressWarnings("unchecked") - private Object readCollection(final TypeInformation type, final DBCollectionEntity source) { - final Class collectionType = Collection.class.isAssignableFrom(type.getType()) ? type.getType() : List.class; - final TypeInformation componentType = getComponentType(type); - final Collection entries = type.getType().isArray() ? new ArrayList<>() - : CollectionFactory.createCollection(collectionType, componentType.getType(), source.size()); - for (final Object entry : source) { - if (entry instanceof DBEntity) { - entries.add(read(componentType, (DBEntity) entry)); - } else if (entry instanceof Map) { - entries.add(read(componentType, new DBDocumentEntity((Map) entry))); - } else if (entry instanceof Collection) { - entries.add(read(componentType, new DBCollectionEntity((Collection) entry))); - } else if (isSimpleType(componentType.getType())) { - final Optional> customWriteTarget = Optional - .ofNullable(conversions.getCustomWriteTarget(componentType.getType())); - final Class targetType = customWriteTarget.orElseGet(() -> componentType.getType()); - entries.add(conversionService.convert(entry, targetType)); - } else { - entries.add(entry); - } - } - return entries; - } - - private Object read( - final TypeInformation type, - final DBEntity source, - final Optional> persistentEntity) { - final ArangoPersistentEntity entity = persistentEntity.orElseThrow( - () -> new MappingException("No mapping metadata found for type " + type.getType().getName())); - final EntityInstantiator instantiatorFor = instantiators.getInstantiatorFor(entity); - final ParameterValueProvider provider = getParameterProvider(entity, source); - final Object instance = instantiatorFor.createInstance(entity, provider); - final ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(instance), - conversionService); - - entity.doWithProperties((final ArangoPersistentProperty property) -> { - readProperty(source.get(_ID), accessor, source.get(property.getFieldName()), property); - }); - entity.doWithAssociations((final Association association) -> { - final ArangoPersistentProperty property = association.getInverse(); - readProperty(source.get(_ID), accessor, source.get(property.getFieldName()), property); - }); - return instance; - } - - private ParameterValueProvider getParameterProvider( - final ArangoPersistentEntity entity, - final DBEntity source) { - final PropertyValueProvider provider = new ArangoPropertyValueProvider(source); - return new PersistentEntityParameterValueProvider<>(entity, provider, null); - } - - private class ArangoPropertyValueProvider implements PropertyValueProvider { - private final DBEntity source; - - public ArangoPropertyValueProvider(final DBEntity source) { - super(); - this.source = source; - } - - @SuppressWarnings("unchecked") - @Override - public T getPropertyValue(final ArangoPersistentProperty property) { - final Optional referenceOrRelation = readReferenceOrRelation(source.get(_ID), - source.get(property.getFieldName()), property); - return (T) referenceOrRelation.orElseGet(() -> convertIfNecessary( - read(source.get(property.getFieldName()), property.getTypeInformation()), property.getType())); - } - - } - - private void readProperty( - final Object parentId, - final ConvertingPropertyAccessor accessor, - final Object source, - final ArangoPersistentProperty property) { - final Optional referenceOrRelation = readReferenceOrRelation(parentId, source, property); - accessor.setProperty(property, - referenceOrRelation.orElseGet(() -> (read(source, property.getTypeInformation())))); - } - - private Optional readReferenceOrRelation( - final Object parentId, - final Object source, - final ArangoPersistentProperty property) { - Optional tmp = Optional.empty(); - if (source != null) { - if (!tmp.isPresent()) { - final Optional ref = property.getRef() - .flatMap(annotation -> readReference(source, property, annotation)); - if (ref.isPresent()) { - tmp = ref; - } - } - } - for (final Optional annotation : Arrays.asList(property.getRelations(), - property.getFrom(), property.getTo())) { - final Optional relation = annotation.flatMap(a -> readRelation(parentId, source, property, a)); - if (relation.isPresent()) { - tmp = relation; - break; - } - } - return tmp; - } - - @SuppressWarnings("unchecked") - private Optional readReference( - final Object source, - final ArangoPersistentProperty property, - final Annotation annotation) { - return resolverFactory.getReferenceResolver(annotation).flatMap(resolver -> { - if (property.isCollectionLike()) { - final Collection ids; - try { - ids = (Collection) asCollection(source); - } catch (final Exception e) { - throw new MappingException( - "Collection of Type String expected for references but found type " + source.getClass()); - } - return Optional.ofNullable(resolver.resolveMultiple(ids, - getComponentType(property.getTypeInformation()).getType(), annotation)); - } else { - if (!(source instanceof String)) { - throw new MappingException( - "Type String expected for reference but found type " + source.getClass()); - } - return Optional.ofNullable( - resolver.resolveOne(source.toString(), property.getTypeInformation().getType(), annotation)); - } - }); - } - - private Optional readRelation( - final Object parentId, - final Object source, - final ArangoPersistentProperty property, - final A annotation) { - return resolverFactory.getRelationResolver(annotation).flatMap(resolver -> { - if (property.isCollectionLike() && parentId != null) { - return Optional.of(resolver.resolveMultiple(parentId.toString(), - getComponentType(property.getTypeInformation()).getType(), annotation)); - } else if (source != null) { - return Optional.of( - resolver.resolveOne(source.toString(), property.getTypeInformation().getType(), annotation)); - } - return Optional.empty(); - }); - } - - @SuppressWarnings("unchecked") - private T read(final Object source, final TypeInformation type) { - if (source == null) { - return null; - } - if (conversions.hasCustomReadTarget(source.getClass(), type.getType())) { - return (T) conversionService.convert(source, type.getType()); - } - if (isMapType(source.getClass())) { - return (T) read(type, new DBDocumentEntity((Map) source)); - } - if (isCollectionType(source.getClass())) { - return (T) readCollection(type, new DBCollectionEntity((Collection) source)); - } - return (T) source; - } - - @Override - public void write(final Object source, final DBEntity sink) { - if (source == null) { - return; - } - write(source, ClassTypeInformation.from(source.getClass()), sink); - } - - @SuppressWarnings("unchecked") - private void write(final Object source, final TypeInformation type, final DBEntity sink) { - if (isMapType(type.getType())) { - writeMap((Map) source, sink); - return; - } - if (isCollectionType(type.getType())) { - writeCollection(source, sink); - return; - } - write(source, sink, Optional.ofNullable(context.getPersistentEntity(type))); - } - - private void write( - final Object source, - final DBEntity sink, - final Optional> entityC) { - final ArangoPersistentEntity entity = entityC.orElseThrow( - () -> new MappingException("No mapping metadata found for type " + source.getClass().getName())); - - final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); - - entity.doWithProperties((final ArangoPersistentProperty property) -> { - if (!property.isWritable()) { - return; - } - final Object propertyObj = accessor.getProperty(property); - if (propertyObj != null) { - writeProperty(propertyObj, sink, property); - } - }); - entity.doWithAssociations((final Association association) -> { - final ArangoPersistentProperty inverse = association.getInverse(); - final Object property = accessor.getProperty(inverse); - if (property != null) { - writeProperty(property, sink, inverse); - } - }); - final Object id = sink.get(_ID); - if (id != null && sink.get(_KEY) == null) { - sink.put(_KEY, determineDocumentKeyFromId(id.toString())); - } - } - - @SuppressWarnings("unchecked") - private void writeProperty(final Object source, final DBEntity sink, final ArangoPersistentProperty property) { - if (source == null) { - return; - } - final String fieldName = property.getFieldName(); - final TypeInformation valueType = ClassTypeInformation.from(source.getClass()); - if (property.getRef().isPresent()) { - if (valueType.isCollectionLike()) { - final Collection ids = new ArrayList<>(); - for (final Object ref : createCollection(asCollection(source), property)) { - getId(ref, property).ifPresent(id -> ids.add(id)); - } - sink.put(fieldName, ids); - } else { - getId(source, property).ifPresent(id -> sink.put(fieldName, id)); - } - return; - } - if (property.getRelations().isPresent()) { - return; - } - if (property.getFrom().isPresent() || property.getTo().isPresent()) { - if (!valueType.isCollectionLike()) { - getId(source, property).ifPresent(id -> sink.put(fieldName, id)); - } - return; - } - if (valueType.isCollectionLike()) { - final DBEntity collection = new DBCollectionEntity(); - writeCollection(source, collection); - sink.put(fieldName, collection); - return; - } - if (valueType.isMap()) { - final DBEntity map = new DBDocumentEntity(); - writeMap((Map) source, map); - sink.put(fieldName, map); - return; - } - final Optional> customWriteTarget = Optional - .ofNullable(conversions.getCustomWriteTarget(source.getClass())); - final Class targetType = customWriteTarget.orElseGet(() -> property.getTypeInformation().getType()); - final DBEntity document = new DBDocumentEntity(); - final Optional> persistentEntity = Optional - .ofNullable(context.getPersistentEntity(targetType)); - if (persistentEntity.isPresent()) { - write(source, document, persistentEntity); - sink.put(fieldName, document); - } else { - sink.put(fieldName, conversionService.convert(source, targetType)); - } - return; - } - - private void writeMap(final Map source, final DBEntity sink) { - for (final Entry entry : source.entrySet()) { - final Object key = entry.getKey(); - if (!conversions.isSimpleType(key.getClass())) { - throw new MappingException( - "Complexe type as Map key value is not allowed! fount type " + key.getClass()); - } - final Object value = entry.getValue(); - final Class valueType = value.getClass(); - if (conversions.isSimpleType(valueType)) { - final Optional> customWriteTarget = Optional - .ofNullable(conversions.getCustomWriteTarget(valueType)); - final Class targetType = customWriteTarget.orElseGet(() -> valueType); - sink.put(key.toString(), conversionService.convert(value, targetType)); - } else { - final DBEntity entity = createDBEntity(valueType); - write(value, ClassTypeInformation.from(valueType), entity); - sink.put(key.toString(), entity); - } - } - } - - private void writeCollection(final Object source, final DBEntity sink) { - for (final Object entry : asCollection(source)) { - final Class valueType = entry.getClass(); - if (conversions.isSimpleType(valueType)) { - final Optional> customWriteTarget = Optional - .ofNullable(conversions.getCustomWriteTarget(valueType)); - final Class targetType = customWriteTarget.orElseGet(() -> valueType); - sink.add(conversionService.convert(entry, targetType)); - } else { - final DBEntity entity = createDBEntity(valueType); - write(entry, ClassTypeInformation.from(valueType), entity); - sink.add(entity); - } - } - } - - private Optional getId(final Object source, final ArangoPersistentProperty property) { - return Optional.ofNullable(context.getPersistentEntity(property)).flatMap(entity -> getId(source, entity)); - } - - private Optional getId(final Object source, final ArangoPersistentEntity entity) { - return Optional.ofNullable(entity.getIdProperty()).map(p -> entity.getPropertyAccessor(source).getProperty(p)); - } - - private Collection createCollection(final Collection source, final ArangoPersistentProperty property) { - return source.stream() - .map(s -> conversionService.convert(s, getComponentType(property.getTypeInformation()).getType())) - .collect(Collectors.toList()); - } - - private TypeInformation getComponentType(final TypeInformation type) { - return Optional.ofNullable(type.getComponentType()) - .orElseThrow(() -> new MappingException("Can not determine collection component type")); - } - - private static Collection asCollection(final Object source) { - return (source instanceof Collection) ? Collection.class.cast(source) - : source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); - } - - private DBEntity createDBEntity(final Class type) { - return isCollectionType(type) ? new DBCollectionEntity() : new DBDocumentEntity(); - } - - private boolean isSimpleType(final Class type) { - return conversions.isSimpleType(type); - } - - @Override - public boolean isCollectionType(final Class type) { - return type.isArray() || Iterable.class.equals(type) || Collection.class.isAssignableFrom(type); - } - - private boolean isMapType(final Class type) { - return Map.class.isAssignableFrom(type); - } - - @Override - public GenericConversionService getConversionService() { - return conversionService; - } - - @Override - public boolean isEntityType(final Class type) { - return !isSimpleType(type) && !isMapType(type) && !isCollectionType(type) - && !BaseDocument.class.isAssignableFrom(type) && !BaseEdgeDocument.class.isAssignableFrom(type) - && !VPackSlice.class.isAssignableFrom(type); - } - - @SuppressWarnings("unchecked") - private T convertIfNecessary(final Object source, final Class type) { - return (T) (source == null ? source - : type.isAssignableFrom(source.getClass()) ? source : conversionService.convert(source, type)); - } - - private String determineDocumentKeyFromId(final String id) { - final String[] split = id.split("/"); - return split[split.length - 1]; - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.EntityInstantiator; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mapping.model.MappingException; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; +import org.springframework.data.mapping.model.PropertyValueProvider; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +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.ArangoSimpleTypes; + +/** + * @author Mark Vollmary + * @author Christian Lechner + * + */ +@SuppressWarnings("deprecation") +public class DefaultArangoConverter implements ArangoConverter { + + private static final String _ID = "_id"; + private static final String _KEY = "_key"; + private final MappingContext, ArangoPersistentProperty> context; + private final CustomConversions conversions; + private final GenericConversionService conversionService; + private final EntityInstantiators instantiators; + private final ResolverFactory resolverFactory; + private final ArangoTypeMapper typeMapper; + + public DefaultArangoConverter( + final MappingContext, ArangoPersistentProperty> context, + final CustomConversions conversions, final ResolverFactory resolverFactory, final ArangoTypeMapper typeMapper) { + super(); + this.context = context; + this.conversions = conversions; + this.resolverFactory = resolverFactory; + this.typeMapper = typeMapper; + conversionService = new DefaultConversionService(); + conversions.registerConvertersIn(conversionService); + instantiators = new EntityInstantiators(); + } + + @Override + public MappingContext, ArangoPersistentProperty> getMappingContext() { + return context; + } + + @Override + public ArangoTypeMapper getTypeMapper() { + return typeMapper; + } + + @SuppressWarnings("unchecked") + @Override + public R read(final Class type, final DBEntity source) { + return (R) read(ClassTypeInformation.from(type), source); + } + + private Object read(final TypeInformation type, final DBEntity source) { + if (source == null) { + return null; + } + + final TypeInformation typeToUse = typeMapper.readType(source, type); + + if (conversions.hasCustomReadTarget(source.getClass(), typeToUse.getType())) { + return conversionService.convert(source, typeToUse.getType()); + } + + if (DBEntity.class.isAssignableFrom(typeToUse.getType())) { + return source; + } + + if (typeToUse.isMap()) { + return readMap(typeToUse, DBDocumentEntity.class.cast(source)); + } + if (typeToUse.isCollectionLike()) { + return readCollection(typeToUse, DBCollectionEntity.class.cast(source)); + } + + // no type information available => stick to the given type of the source + if (typeToUse.equals(ClassTypeInformation.OBJECT)) { + if (source instanceof DBDocumentEntity) { + return readMap(ClassTypeInformation.MAP, DBDocumentEntity.class.cast(source)); + } else if (source instanceof DBCollectionEntity) { + return readCollection(ClassTypeInformation.LIST, DBCollectionEntity.class.cast(source)); + } + return source; + } + + final ArangoPersistentEntity entity = context.getPersistentEntity(typeToUse.getType()); + return read(typeToUse, source, entity); + } + + private Object readMap(final TypeInformation type, final DBDocumentEntity source) { + final Class keyType = getNonNullComponentType(type).getType(); + final TypeInformation valueType = getNonNullMapValueType(type); + final Map map = CollectionFactory.createMap(type.getType(), keyType, source.size()); + for (final Map.Entry entry : source.entrySet()) { + if (typeMapper.isTypeKey(entry.getKey())) { + continue; + } + final Object key = convertIfNecessary(entry.getKey(), keyType); + final Object value = entry.getValue(); + if (value instanceof DBEntity) { + map.put(key, read(valueType, (DBEntity) value)); + } else { + map.put(key, convertIfNecessary(value, valueType.getType())); + } + } + return map; + } + + private Object readCollection(final TypeInformation type, final DBCollectionEntity source) { + final Class collectionType = Collection.class.isAssignableFrom(type.getType()) ? type.getType() : List.class; + final TypeInformation componentType = getNonNullComponentType(type); + final Collection entries = type.getType().isArray() ? new ArrayList<>() + : CollectionFactory.createCollection(collectionType, componentType.getType(), source.size()); + for (final Object entry : source) { + if (entry instanceof DBEntity) { + entries.add(read(componentType, (DBEntity) entry)); + } else { + entries.add(convertIfNecessary(entry, componentType.getType())); + } + } + return entries; + } + + private Object read(final TypeInformation type, final DBEntity source, final ArangoPersistentEntity entity) { + final EntityInstantiator instantiatorFor = instantiators.getInstantiatorFor(entity); + final ParameterValueProvider provider = getParameterProvider(entity, source); + final Object instance = instantiatorFor.createInstance(entity, provider); + final ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(instance), + conversionService); + + entity.doWithProperties((final ArangoPersistentProperty property) -> { + if (!entity.isConstructorArgument(property)) { + readProperty(source.get(_ID), accessor, source.get(property.getFieldName()), property); + } + }); + entity.doWithAssociations((final Association association) -> { + final ArangoPersistentProperty property = association.getInverse(); + if (!entity.isConstructorArgument(property)) { + readProperty(source.get(_ID), accessor, source.get(property.getFieldName()), property); + } + }); + return instance; + } + + private ParameterValueProvider getParameterProvider( + final ArangoPersistentEntity entity, + final DBEntity source) { + final PropertyValueProvider provider = new ArangoPropertyValueProvider(source); + return new PersistentEntityParameterValueProvider<>(entity, provider, null); + } + + private class ArangoPropertyValueProvider implements PropertyValueProvider { + private final DBEntity source; + + public ArangoPropertyValueProvider(final DBEntity source) { + super(); + this.source = source; + } + + @SuppressWarnings("unchecked") + @Override + public T getPropertyValue(final ArangoPersistentProperty property) { + final Optional referenceOrRelation = readReferenceOrRelation(source.get(_ID), + source.get(property.getFieldName()), property); + return (T) referenceOrRelation.orElseGet(() -> convertIfNecessary( + read(source.get(property.getFieldName()), property.getTypeInformation()), property.getType())); + } + + } + + private void readProperty( + final Object parentId, + final ConvertingPropertyAccessor accessor, + final Object source, + final ArangoPersistentProperty property) { + final Optional referenceOrRelation = readReferenceOrRelation(parentId, source, property); + accessor.setProperty(property, + referenceOrRelation.orElseGet(() -> read(source, property.getTypeInformation()))); + } + + private Optional readReferenceOrRelation( + final Object parentId, + final Object source, + final ArangoPersistentProperty property) { + Optional tmp = Optional.empty(); + if (source != null) { + if (!tmp.isPresent()) { + final Optional ref = property.getRef() + .flatMap(annotation -> readReference(source, property, annotation)); + if (ref.isPresent()) { + tmp = ref; + } + } + } + for (final Optional annotation : Arrays.asList(property.getRelations(), + property.getFrom(), property.getTo())) { + final Optional relation = annotation.flatMap(a -> readRelation(parentId, source, property, a)); + if (relation.isPresent()) { + tmp = relation; + break; + } + } + return tmp; + } + + @SuppressWarnings("unchecked") + private Optional readReference( + final Object source, + final ArangoPersistentProperty property, + final Annotation annotation) { + return resolverFactory.getReferenceResolver(annotation).flatMap(resolver -> { + if (property.isCollectionLike()) { + final Collection ids; + try { + ids = (Collection) asCollection(source); + } catch (final Exception e) { + throw new MappingException( + "Collection of type String expected for references but found type " + source.getClass()); + } + return Optional.ofNullable(resolver.resolveMultiple(ids, property.getTypeInformation(), annotation)); + } else { + if (!(source instanceof String)) { + throw new MappingException( + "Type String expected for reference but found type " + source.getClass()); + } + return Optional + .ofNullable(resolver.resolveOne(source.toString(), property.getTypeInformation(), annotation)); + } + }); + } + + private Optional readRelation( + final Object parentId, + final Object source, + final ArangoPersistentProperty property, + final A annotation) { + return resolverFactory.getRelationResolver(annotation).flatMap(resolver -> { + if (property.isCollectionLike() && parentId != null) { + return Optional + .of(resolver.resolveMultiple(parentId.toString(), property.getTypeInformation(), annotation)); + } else if (source != null) { + return Optional.of(resolver.resolveOne(source.toString(), property.getTypeInformation(), annotation)); + } + return Optional.empty(); + }); + } + + @SuppressWarnings("unchecked") + private T read(final Object source, final TypeInformation type) { + if (source == null) { + return null; + } + if (conversions.hasCustomReadTarget(source.getClass(), type.getType())) { + return (T) conversionService.convert(source, type.getType()); + } + if (source instanceof DBEntity) { + return (T) read(type, DBEntity.class.cast(source)); + } + return (T) source; + } + + @Override + public void write(final Object source, final DBEntity sink) { + if (source == null) { + return; + } + + if (sink instanceof DBDocumentEntity + && conversions.hasCustomWriteTarget(source.getClass(), DBDocumentEntity.class)) { + final DBDocumentEntity result = conversionService.convert(source, DBDocumentEntity.class); + ((DBDocumentEntity) sink).putAll(result); + } + + final TypeInformation type = ClassTypeInformation.from(ClassUtils.getUserClass(source.getClass())); + final TypeInformation definedType = ClassTypeInformation.OBJECT; + + write(source, type, sink, definedType); + } + + @SuppressWarnings("unchecked") + private void write( + final Object source, + final TypeInformation type, + final DBEntity sink, + final TypeInformation definedType) { + + if (type.isMap()) { + writeMap((Map) source, sink, definedType); + return; + } + if (type.isCollectionLike()) { + writeCollection(source, sink, definedType); + return; + } + write(source, sink, context.getPersistentEntity(type)); + addTypeKeyIfNecessary(definedType, source, sink); + } + + private void write(final Object source, final DBEntity sink, final ArangoPersistentEntity entity) { + + final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(source); + + entity.doWithProperties((final ArangoPersistentProperty property) -> { + if (!property.isWritable()) { + return; + } + final Object propertyObj = accessor.getProperty(property); + if (propertyObj != null) { + writeProperty(propertyObj, sink, property); + } + }); + entity.doWithAssociations((final Association association) -> { + final ArangoPersistentProperty inverse = association.getInverse(); + final Object property = accessor.getProperty(inverse); + if (property != null) { + writeProperty(property, sink, inverse); + } + }); + final Object id = sink.get(_ID); + if (id != null && sink.get(_KEY) == null) { + sink.put(_KEY, determineDocumentKeyFromId(id.toString())); + } + } + + @SuppressWarnings("unchecked") + private void writeProperty(final Object source, final DBEntity sink, final ArangoPersistentProperty property) { + if (source == null) { + return; + } + final String fieldName = property.getFieldName(); + final TypeInformation valueType = ClassTypeInformation.from(source.getClass()); + if (property.getRef().isPresent()) { + if (valueType.isCollectionLike()) { + final Collection ids = new ArrayList<>(); + for (final Object ref : createCollection(asCollection(source), property)) { + getId(ref).ifPresent(id -> ids.add(id)); + } + sink.put(fieldName, ids); + } else { + getId(source).ifPresent(id -> sink.put(fieldName, id)); + } + return; + } + if (property.getRelations().isPresent()) { + return; + } + if (property.getFrom().isPresent() || property.getTo().isPresent()) { + if (!valueType.isCollectionLike()) { + getId(source).ifPresent(id -> sink.put(fieldName, id)); + } + return; + } + if (conversions.isSimpleType(valueType.getType())) { + final Optional> customWriteTarget = Optional + .ofNullable(conversions.getCustomWriteTarget(source.getClass())); + final Class targetType = customWriteTarget.orElseGet(() -> valueType.getType()); + sink.put(fieldName, conversionService.convert(source, targetType)); + return; + } + if (valueType.isCollectionLike()) { + final DBEntity collection = new DBCollectionEntity(); + writeCollection(source, collection, property.getTypeInformation()); + sink.put(fieldName, collection); + return; + } + if (valueType.isMap()) { + final DBEntity map = new DBDocumentEntity(); + writeMap((Map) source, map, property.getTypeInformation()); + sink.put(fieldName, map); + return; + } + final ArangoPersistentEntity persistentEntity = context.getPersistentEntity(valueType); + final DBEntity document = new DBDocumentEntity(); + write(source, document, persistentEntity); + addTypeKeyIfNecessary(property.getTypeInformation(), source, document); + sink.put(fieldName, document); + return; + } + + private void writeMap(final Map source, final DBEntity sink, final TypeInformation definedType) { + for (final Entry entry : source.entrySet()) { + final Object key = entry.getKey(); + if (!conversions.isSimpleType(key.getClass()) || key instanceof DBEntity) { + throw new MappingException( + "Complex type " + key.getClass().getName() + " is not allowed as a map key!"); + } + final Object value = entry.getValue(); + final Class valueType = value.getClass(); + if (conversions.isSimpleType(valueType)) { + final Optional> customWriteTarget = Optional + .ofNullable(conversions.getCustomWriteTarget(valueType)); + final Class targetType = customWriteTarget.orElseGet(() -> valueType); + sink.put(convertMapKey(key), conversionService.convert(value, targetType)); + } else { + final DBEntity entity = createDBEntity(valueType); + write(value, ClassTypeInformation.from(valueType), entity, getNonNullMapValueType(definedType)); + sink.put(convertMapKey(key), entity); + } + } + } + + private void writeCollection(final Object source, final DBEntity sink, final TypeInformation definedType) { + for (final Object entry : asCollection(source)) { + final Class valueType = entry.getClass(); + if (conversions.isSimpleType(valueType)) { + final Optional> customWriteTarget = Optional + .ofNullable(conversions.getCustomWriteTarget(valueType)); + final Class targetType = customWriteTarget.orElseGet(() -> valueType); + sink.add(conversionService.convert(entry, targetType)); + } else { + final DBEntity entity = createDBEntity(valueType); + write(entry, ClassTypeInformation.from(valueType), entity, getNonNullComponentType(definedType)); + sink.add(entity); + } + } + } + + private Optional getId(final Object source) { + return getId(source, context.getPersistentEntity(source.getClass())); + } + + private Optional getId(final Object source, final ArangoPersistentEntity entity) { + return Optional.ofNullable(entity.getIdentifierAccessor(source).getIdentifier()); + } + + private Collection createCollection(final Collection source, final ArangoPersistentProperty property) { + return source.stream() + .map( + s -> conversionService.convert(s, getNonNullComponentType(property.getTypeInformation()).getType())) + .collect(Collectors.toList()); + } + + private static Collection asCollection(final Object source) { + return (source instanceof Collection) ? Collection.class.cast(source) + : source.getClass().isArray() ? CollectionUtils.arrayToList(source) : Collections.singleton(source); + } + + private DBEntity createDBEntity(final Class type) { + return isCollectionType(type) ? new DBCollectionEntity() : new DBDocumentEntity(); + } + + private boolean isArangoSimpleType(final Class type) { + return ArangoSimpleTypes.HOLDER.isSimpleType(type); + } + + @Override + public boolean isCollectionType(final Class type) { + return type.isArray() || Iterable.class.equals(type) || Collection.class.isAssignableFrom(type); + } + + private boolean isMapType(final Class type) { + return Map.class.isAssignableFrom(type); + } + + @Override + public GenericConversionService getConversionService() { + return conversionService; + } + + @Override + public boolean isEntityType(final Class type) { + return !isArangoSimpleType(type) && !isMapType(type) && !isCollectionType(type); + } + + @SuppressWarnings("unchecked") + private T convertIfNecessary(final Object source, final Class type) { + return (T) (source == null ? source + : type.isAssignableFrom(source.getClass()) ? source : conversionService.convert(source, type)); + } + + private String determineDocumentKeyFromId(final String id) { + final String[] split = id.split("/"); + return split[split.length - 1]; + } + + private void addTypeKeyIfNecessary(final TypeInformation definedType, final Object value, final DBEntity sink) { + final Class referenceType = definedType != null ? definedType.getType() : Object.class; + final Class valueType = ClassUtils.getUserClass(value.getClass()); + if (!valueType.equals(referenceType)) { + typeMapper.writeType(valueType, sink); + } + } + + private String convertMapKey(final Object key) { + if (key instanceof String) { + return (String) key; + } + final boolean hasCustomConverter = conversions.hasCustomWriteTarget(key.getClass(), String.class); + return hasCustomConverter ? conversionService.convert(key, String.class) : key.toString(); + } + + private TypeInformation getNonNullComponentType(final TypeInformation type) { + final TypeInformation compType = type.getComponentType(); + return compType != null ? compType : ClassTypeInformation.OBJECT; + } + + private TypeInformation getNonNullMapValueType(final TypeInformation type) { + final TypeInformation valueType = type.getMapValueType(); + return valueType != null ? valueType : ClassTypeInformation.OBJECT; + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoTypeMapper.java b/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoTypeMapper.java new file mode 100644 index 000000000..651a910ec --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/convert/DefaultArangoTypeMapper.java @@ -0,0 +1,105 @@ +/* + * 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.convert; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.data.convert.DefaultTypeMapper; +import org.springframework.data.convert.SimpleTypeInformationMapper; +import org.springframework.data.convert.TypeAliasAccessor; +import org.springframework.data.convert.TypeInformationMapper; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.context.MappingContext; + +/** + * @author Christian Lechner + * + */ +@SuppressWarnings("deprecation") +public class DefaultArangoTypeMapper extends DefaultTypeMapper implements ArangoTypeMapper { + + public static final String DEFAULT_TYPE_KEY = "_class"; + + private final String typeKey; + + public DefaultArangoTypeMapper() { + this(DEFAULT_TYPE_KEY); + } + + public DefaultArangoTypeMapper(final String typeKey) { + this(typeKey, Arrays.asList(new SimpleTypeInformationMapper())); + } + + public DefaultArangoTypeMapper(final String typeKey, + final MappingContext, ?> mappingContext) { + this(typeKey, new DocumentTypeAliasAccessor(typeKey), mappingContext, + Arrays.asList(new SimpleTypeInformationMapper())); + } + + public DefaultArangoTypeMapper(final String typeKey, final List mappers) { + this(typeKey, new DocumentTypeAliasAccessor(typeKey), null, mappers); + } + + private DefaultArangoTypeMapper(final String typeKey, final TypeAliasAccessor accessor, + final MappingContext, ?> mappingContext, + final List mappers) { + + super(accessor, mappingContext, mappers); + this.typeKey = typeKey; + } + + @Override + public boolean isTypeKey(final String key) { + return typeKey == null ? false : typeKey.equals(key); + } + + public static final class DocumentTypeAliasAccessor implements TypeAliasAccessor { + + private final String typeKey; + + public DocumentTypeAliasAccessor(final String typeKey) { + this.typeKey = typeKey; + } + + @Override + public Object readAliasFrom(final DBEntity source) { + if (source instanceof DBCollectionEntity) { + return null; + } + + if (source instanceof DBDocumentEntity) { + return source.get(this.typeKey); + } + + throw new IllegalArgumentException("Cannot read alias from " + source.getClass()); + } + + @Override + public void writeTypeTo(final DBEntity sink, final Object alias) { + if (this.typeKey != null && sink instanceof DBDocumentEntity) { + sink.put(this.typeKey, alias); + } + } + + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/JodaTimeStringConverters.java b/src/main/java/com/arangodb/springframework/core/convert/JodaTimeStringConverters.java index 05576e43c..32bad0704 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/JodaTimeStringConverters.java +++ b/src/main/java/com/arangodb/springframework/core/convert/JodaTimeStringConverters.java @@ -1,145 +1,144 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import org.joda.time.DateTime; -import org.joda.time.Instant; -import org.joda.time.LocalDate; -import org.joda.time.LocalDateTime; -import org.springframework.core.convert.converter.Converter; -import org.springframework.util.ClassUtils; - -import com.arangodb.ArangoDBException; -import com.arangodb.velocypack.internal.util.DateUtil; - -/** - * @author Mark Vollmary - * - */ -public class JodaTimeStringConverters { - - private static final boolean JODA_TIME_IS_PRESENT = ClassUtils.isPresent("org.joda.time.LocalDate", null); - - public static Collection> getConvertersToRegister() { - if (!JODA_TIME_IS_PRESENT) { - return Collections.emptySet(); - } - final List> converters = new ArrayList<>(); - converters.add(InstantToStringConverter.INSTANCE); - converters.add(DateTimeToStringConverter.INSTANCE); - converters.add(LocalDateToStringConverter.INSTANCE); - converters.add(LocalDateTimeToStringConverter.INSTANCE); - - converters.add(StringToInstantConverter.INSTANCE); - converters.add(StringToDateTimeConverter.INSTANCE); - converters.add(StringToLocalDateConverter.INSTANCE); - converters.add(StringToLocalDateTimeConverter.INSTANCE); - return converters; - } - - private static Date parse(final String source) { - try { - return DateUtil.parse(source); - } catch (final ParseException e) { - throw new ArangoDBException(e); - } - } - - public static enum InstantToStringConverter implements Converter { - INSTANCE; - - @Override - public String convert(final Instant source) { - return source == null ? null : DateUtil.format(source.toDate()); - } - } - - public static enum DateTimeToStringConverter implements Converter { - INSTANCE; - - @Override - public String convert(final DateTime source) { - return source == null ? null : DateUtil.format(source.toDate()); - } - } - - public static enum LocalDateToStringConverter implements Converter { - INSTANCE; - - @Override - public String convert(final LocalDate source) { - return source == null ? null : DateUtil.format(source.toDate()); - } - } - - public static enum LocalDateTimeToStringConverter implements Converter { - INSTANCE; - - @Override - public String convert(final LocalDateTime source) { - return source == null ? null : DateUtil.format(source.toDate()); - } - } - - public static enum StringToInstantConverter implements Converter { - INSTANCE; - - @Override - public Instant convert(final String source) { - return source == null ? null : new Instant(parse(source).getTime()); - } - } - - public static enum StringToDateTimeConverter implements Converter { - INSTANCE; - - @Override - public DateTime convert(final String source) { - return source == null ? null : new DateTime(parse(source).getTime()); - } - } - - public static enum StringToLocalDateConverter implements Converter { - INSTANCE; - - @Override - public LocalDate convert(final String source) { - return source == null ? null : new LocalDate(parse(source).getTime()); - } - } - - public static enum StringToLocalDateTimeConverter implements Converter { - INSTANCE; - - @Override - public LocalDateTime convert(final String source) { - return source == null ? null : new LocalDateTime(parse(source).getTime()); - } - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.joda.time.DateTime; +import org.joda.time.Instant; +import org.joda.time.LocalDate; +import org.joda.time.LocalDateTime; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.util.ClassUtils; + +import com.arangodb.velocypack.module.joda.internal.util.JodaTimeUtil; + +/** + * @author Mark Vollmary + * + */ +public class JodaTimeStringConverters { + + private static final boolean JODA_TIME_IS_PRESENT = ClassUtils.isPresent("org.joda.time.LocalDate", null); + + public static Collection> getConvertersToRegister() { + if (!JODA_TIME_IS_PRESENT) { + return Collections.emptySet(); + } + final List> converters = new ArrayList<>(); + converters.add(InstantToStringConverter.INSTANCE); + converters.add(DateTimeToStringConverter.INSTANCE); + converters.add(LocalDateToStringConverter.INSTANCE); + converters.add(LocalDateTimeToStringConverter.INSTANCE); + + converters.add(StringToInstantConverter.INSTANCE); + converters.add(StringToDateTimeConverter.INSTANCE); + converters.add(StringToLocalDateConverter.INSTANCE); + converters.add(StringToLocalDateTimeConverter.INSTANCE); + return converters; + } + + @WritingConverter + public static enum InstantToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(final Instant source) { + return source == null ? null : JodaTimeUtil.format(source); + } + } + + @WritingConverter + public static enum DateTimeToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(final DateTime source) { + return source == null ? null : JodaTimeUtil.format(source); + } + } + + @WritingConverter + public static enum LocalDateToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(final LocalDate source) { + return source == null ? null : JodaTimeUtil.format(source); + } + } + + @WritingConverter + public static enum LocalDateTimeToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(final LocalDateTime source) { + return source == null ? null : JodaTimeUtil.format(source); + } + } + + @ReadingConverter + public static enum StringToInstantConverter implements Converter { + INSTANCE; + + @Override + public Instant convert(final String source) { + return source == null ? null : JodaTimeUtil.parseInstant(source); + } + } + + @ReadingConverter + public static enum StringToDateTimeConverter implements Converter { + INSTANCE; + + @Override + public DateTime convert(final String source) { + return source == null ? null : JodaTimeUtil.parseDateTime(source); + } + } + + @ReadingConverter + public static enum StringToLocalDateConverter implements Converter { + INSTANCE; + + @Override + public LocalDate convert(final String source) { + return source == null ? null : JodaTimeUtil.parseLocalDate(source); + } + } + + @ReadingConverter + public static enum StringToLocalDateTimeConverter implements Converter { + INSTANCE; + + @Override + public LocalDateTime convert(final String source) { + return source == null ? null : JodaTimeUtil.parseLocalDateTime(source); + } + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/TimeStringConverters.java b/src/main/java/com/arangodb/springframework/core/convert/TimeStringConverters.java index ce1a1b48e..61bc01687 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/TimeStringConverters.java +++ b/src/main/java/com/arangodb/springframework/core/convert/TimeStringConverters.java @@ -1,127 +1,161 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert; - -import java.text.ParseException; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.util.ClassUtils; - -import com.arangodb.ArangoDBException; -import com.arangodb.velocypack.internal.util.DateUtil; - -/** - * @author Mark Vollmary - * - */ -public class TimeStringConverters { - - private static final boolean JODA_TIME_IS_PRESENT = ClassUtils.isPresent("org.joda.time.LocalDate", null); - - public static Collection> getConvertersToRegister() { - if (!JODA_TIME_IS_PRESENT) { - return Collections.emptySet(); - } - final List> converters = new ArrayList<>(); - converters.add(InstantToStringConverter.INSTANCE); - converters.add(LocalDateToStringConverter.INSTANCE); - converters.add(LocalDateTimeToStringConverter.INSTANCE); - - converters.add(StringToInstantConverter.INSTANCE); - converters.add(StringToLocalDateConverter.INSTANCE); - converters.add(StringToLocalDateTimeConverter.INSTANCE); - return converters; - } - - private static Date parse(final String source) { - try { - return DateUtil.parse(source); - } catch (final ParseException e) { - throw new ArangoDBException(e); - } - } - - public static enum InstantToStringConverter implements Converter { - INSTANCE; - - @Override - public String convert(final Instant source) { - return source == null ? null : DateUtil.format(Date.from(source)); - } - } - - public static enum LocalDateToStringConverter implements Converter { - INSTANCE; - - @Override - public String convert(final LocalDate source) { - return source == null ? null - : DateUtil.format(Date.from(source.atStartOfDay(ZoneId.systemDefault()).toInstant())); - } - } - - public static enum LocalDateTimeToStringConverter implements Converter { - INSTANCE; - - @Override - public String convert(final LocalDateTime source) { - return source == null ? null - : DateUtil.format(Date.from(source.atZone(ZoneId.systemDefault()).toInstant())); - } - } - - public static enum StringToInstantConverter implements Converter { - INSTANCE; - - @Override - public Instant convert(final String source) { - return source == null ? null : parse(source).toInstant(); - } - } - - public static enum StringToLocalDateConverter implements Converter { - INSTANCE; - - @Override - public LocalDate convert(final String source) { - return source == null ? null : parse(source).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); - } - } - - public static enum StringToLocalDateTimeConverter implements Converter { - INSTANCE; - - @Override - public LocalDateTime convert(final String source) { - return source == null ? null : parse(source).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); - } - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; + +import com.arangodb.velocypack.module.jdk8.internal.util.JavaTimeUtil; + +/** + * @author Mark Vollmary + * @author Christian Lechner + * + */ +public class TimeStringConverters { + + public static Collection> getConvertersToRegister() { + final List> converters = new ArrayList<>(); + converters.add(InstantToStringConverter.INSTANCE); + converters.add(LocalDateToStringConverter.INSTANCE); + converters.add(LocalDateTimeToStringConverter.INSTANCE); + converters.add(OffsetDateTimeToStringConverter.INSTANCE); + converters.add(ZonedDateTimeToStringConverter.INSTANCE); + + converters.add(StringToInstantConverter.INSTANCE); + converters.add(StringToLocalDateConverter.INSTANCE); + converters.add(StringToLocalDateTimeConverter.INSTANCE); + converters.add(StringToOffsetDateTimeConverter.INSTANCE); + converters.add(StringToZonedDateTimeConverter.INSTANCE); + return converters; + } + + @WritingConverter + public static enum InstantToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(final Instant source) { + return source == null ? null : JavaTimeUtil.format(source); + } + } + + @WritingConverter + public static enum LocalDateToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(final LocalDate source) { + return source == null ? null : JavaTimeUtil.format(source); + } + } + + @WritingConverter + public static enum LocalDateTimeToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(final LocalDateTime source) { + return source == null ? null : JavaTimeUtil.format(source); + } + } + + @WritingConverter + public static enum OffsetDateTimeToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(final OffsetDateTime source) { + return source == null ? null : JavaTimeUtil.format(source); + } + } + + @WritingConverter + public static enum ZonedDateTimeToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(final ZonedDateTime source) { + return source == null ? null : JavaTimeUtil.format(source); + } + } + + @ReadingConverter + public static enum StringToInstantConverter implements Converter { + INSTANCE; + + @Override + public Instant convert(final String source) { + return source == null ? null : JavaTimeUtil.parseInstant(source); + } + } + + @ReadingConverter + public static enum StringToLocalDateConverter implements Converter { + INSTANCE; + + @Override + public LocalDate convert(final String source) { + return source == null ? null : JavaTimeUtil.parseLocalDate(source); + } + } + + @ReadingConverter + public static enum StringToLocalDateTimeConverter implements Converter { + INSTANCE; + + @Override + public LocalDateTime convert(final String source) { + return source == null ? null : JavaTimeUtil.parseLocalDateTime(source); + } + } + + @ReadingConverter + public static enum StringToOffsetDateTimeConverter implements Converter { + INSTANCE; + + @Override + public OffsetDateTime convert(final String source) { + return source == null ? null : JavaTimeUtil.parseOffsetDateTime(source); + } + } + + @ReadingConverter + public static enum StringToZonedDateTimeConverter implements Converter { + INSTANCE; + + @Override + public ZonedDateTime convert(final String source) { + return source == null ? null : JavaTimeUtil.parseZonedDateTime(source); + } + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java index d3d08a6ff..f5443fb4f 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/AbstractResolver.java @@ -1,125 +1,143 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert.resolver; - -import java.io.Serializable; -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; - -import org.aopalliance.intercept.MethodInvocation; -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.cglib.proxy.Callback; -import org.springframework.cglib.proxy.Enhancer; -import org.springframework.cglib.proxy.Factory; -import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.objenesis.ObjenesisStd; - -/** - * @author Mark Vollmary - * - */ -public abstract class AbstractResolver { - - private final ObjenesisStd objenesis; - - protected AbstractResolver() { - super(); - this.objenesis = new ObjenesisStd(true); - } - - static interface ResolverCallback { - - Object resolve(String id, Class type, A annotation); - - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - protected Object proxy( - final String id, - final Class type, - final A annotation, - final ResolverCallback callback) { - final ProxyInterceptor interceptor = new ProxyInterceptor(id, type, annotation, callback); - if (type.isInterface()) { - final ProxyFactory proxyFactory = new ProxyFactory(new Class[] { type }); - for (final Class interf : type.getInterfaces()) { - proxyFactory.addInterface(interf); - } - proxyFactory.addAdvice(interceptor); - return proxyFactory.getProxy(); - } else { - final Factory factory = (Factory) objenesis.newInstance(enhancedTypeFor(type)); - factory.setCallbacks(new Callback[] { interceptor }); - return factory; - } - } - - private Class enhancedTypeFor(final Class type) { - final Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(type); - enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); - return enhancer.createClass(); - } - - static class ProxyInterceptor implements Serializable, - org.springframework.cglib.proxy.MethodInterceptor, org.aopalliance.intercept.MethodInterceptor { - - private static final long serialVersionUID = -6722757823918987065L; - private final String id; - private final Class type; - private final A annotation; - private final ResolverCallback callback; - private volatile boolean resolved; - private Object result; - - public ProxyInterceptor(final String id, final Class type, final A annotation, - final ResolverCallback callback) { - super(); - this.id = id; - this.type = type; - this.annotation = annotation; - this.callback = callback; - result = null; - resolved = false; - } - - @Override - public Object invoke(final MethodInvocation invocation) throws Throwable { - return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null); - } - - @Override - public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) - throws Throwable { - final Object result = resolve(); - return result == null ? null : method.invoke(result, args); - } - - private synchronized Object resolve() { - if (!resolved) { - result = callback.resolve(id, type, annotation); - resolved = true; - } - return result; - } - - } -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert.resolver; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.cglib.proxy.Callback; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.Factory; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.objenesis.ObjenesisStd; + +/** + * @author Mark Vollmary + * + */ +public abstract class AbstractResolver { + + private final ObjenesisStd objenesis; + private final ConversionService conversionService; + + protected AbstractResolver(final ConversionService conversionService) { + super(); + this.conversionService = conversionService; + this.objenesis = new ObjenesisStd(true); + } + + static interface ResolverCallback { + + Object resolve(String id, TypeInformation type, A annotation); + + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected Object proxy( + final String id, + final TypeInformation type, + final A annotation, + final ResolverCallback callback) { + final ProxyInterceptor interceptor = new ProxyInterceptor(id, type, annotation, callback, conversionService); + if (type.getType().isInterface()) { + final ProxyFactory proxyFactory = new ProxyFactory(new Class[] { type.getType() }); + for (final Class interf : type.getType().getInterfaces()) { + proxyFactory.addInterface(interf); + } + proxyFactory.addAdvice(interceptor); + return proxyFactory.getProxy(); + } else { + final Factory factory = (Factory) objenesis.newInstance(enhancedTypeFor(type.getType())); + factory.setCallbacks(new Callback[] { interceptor }); + return factory; + } + } + + private Class enhancedTypeFor(final Class type) { + final Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(type); + enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); + return enhancer.createClass(); + } + + static class ProxyInterceptor implements Serializable, + org.springframework.cglib.proxy.MethodInterceptor, org.aopalliance.intercept.MethodInterceptor { + + private static final long serialVersionUID = -6722757823918987065L; + private final String id; + final TypeInformation type; + private final A annotation; + private final ResolverCallback callback; + private volatile boolean resolved; + private Object result; + private final ConversionService conversionService; + + public ProxyInterceptor(final String id, final TypeInformation type, final A annotation, + final ResolverCallback callback, final ConversionService conversionService) { + super(); + this.id = id; + this.type = type; + this.annotation = annotation; + this.callback = callback; + this.conversionService = conversionService; + result = null; + resolved = false; + } + + @Override + public Object invoke(final MethodInvocation invocation) throws Throwable { + return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null); + } + + @Override + public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) + throws Throwable { + final Object result = resolve(); + return result == null ? null : method.invoke(result, args); + } + + private synchronized Object resolve() { + if (!resolved) { + result = convertIfNecessary(callback.resolve(id, type, annotation), type.getType()); + resolved = true; + } + return result; + } + + @SuppressWarnings("unchecked") + private T convertIfNecessary(final Object source, final Class type) { + return (T) (source == null ? null + : type.isAssignableFrom(source.getClass()) ? source : conversionService.convert(source, type)); + } + } + + protected static TypeInformation getNonNullComponentType(final TypeInformation type) { + final TypeInformation compType = type.getComponentType(); + return compType != null ? compType : ClassTypeInformation.OBJECT; + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/FromResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/FromResolver.java index 4685a4c8b..12aa2d67a 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/FromResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/FromResolver.java @@ -1,67 +1,66 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert.resolver; - -import java.util.Collection; - -import com.arangodb.model.AqlQueryOptions; -import com.arangodb.springframework.annotation.From; -import com.arangodb.springframework.core.ArangoOperations; -import com.arangodb.util.MapBuilder; - -/** - * @author Mark Vollmary - * - */ -public class FromResolver extends AbstractResolver implements RelationResolver { - - private final ArangoOperations template; - - public FromResolver(final ArangoOperations template) { - super(); - this.template = template; - } - - @Override - public Object resolveOne(final String id, final Class type, final From annotation) { - return annotation.lazy() ? proxy(id, type, annotation, (i, t, a) -> internalResolveOne(i, t)) - : internalResolveOne(id, type); - } - - private Object internalResolveOne(final String id, final Class type) { - return template.find(id, type).get(); - } - - @Override - public Object resolveMultiple(final String id, final Class type, final From annotation) { - return annotation.lazy() - ? proxy(id, Collection.class, annotation, (i, t, a) -> internalResolveMultiple(i, type)) - : internalResolveMultiple(id, type); - } - - private Object internalResolveMultiple(final String id, final Class type) { - return template - .query("FOR e IN @@edge FILTER e._from == @id RETURN e", - new MapBuilder().put("@edge", type).put("id", id).get(), new AqlQueryOptions(), type) - .asListRemaining(); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert.resolver; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.util.TypeInformation; + +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.annotation.From; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.util.MapBuilder; + +/** + * @author Mark Vollmary + * + */ +public class FromResolver extends AbstractResolver implements RelationResolver { + + private final ArangoOperations template; + + public FromResolver(final ArangoOperations template, final ConversionService conversionService) { + super(conversionService); + this.template = template; + } + + @Override + public Object resolveOne(final String id, final TypeInformation type, final From annotation) { + return annotation.lazy() ? proxy(id, type, annotation, (i, t, a) -> internalResolveOne(i, t)) + : internalResolveOne(id, type); + } + + private Object internalResolveOne(final String id, final TypeInformation type) { + return template.find(id, type.getType()).get(); + } + + @Override + public Object resolveMultiple(final String id, final TypeInformation type, final From annotation) { + return annotation.lazy() ? proxy(id, type, annotation, (i, t, a) -> internalResolveMultiple(i, t)) + : internalResolveMultiple(id, type); + } + + private Object internalResolveMultiple(final String id, final TypeInformation type) { + final Class t = getNonNullComponentType(type).getType(); + return template.query("FOR e IN @@edge FILTER e._from == @id RETURN e", + new MapBuilder().put("@edge", t).put("id", id).get(), new AqlQueryOptions(), t).asListRemaining(); + } + +} 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 0863e607c..4ab5f2eaa 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 @@ -1,58 +1,62 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert.resolver; - -import java.util.Collection; -import java.util.stream.Collectors; - -import com.arangodb.springframework.annotation.Ref; -import com.arangodb.springframework.core.ArangoOperations; - -/** - * @author Mark Vollmary - * - */ -public class RefResolver extends AbstractResolver - implements ReferenceResolver, AbstractResolver.ResolverCallback { - - private final ArangoOperations template; - - public RefResolver(final ArangoOperations template) { - super(); - this.template = template; - } - - @Override - public Object resolveOne(final String id, final Class type, final Ref annotation) { - return annotation.lazy() ? proxy(id, type, annotation, this) : resolve(id, type, annotation); - } - - @Override - public Object resolveMultiple(final Collection ids, final Class type, final Ref annotation) { - return ids.stream().map(id -> resolveOne(id, type, annotation)).collect(Collectors.toList()); - } - - @Override - public Object resolve(final String id, final Class type, final Ref annotation) { - return template.find(id, type).get(); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert.resolver; + +import java.util.Collection; +import java.util.stream.Collectors; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.util.TypeInformation; + +import com.arangodb.springframework.annotation.Ref; +import com.arangodb.springframework.core.ArangoOperations; + +/** + * @author Mark Vollmary + * + */ +public class RefResolver extends AbstractResolver + implements ReferenceResolver, AbstractResolver.ResolverCallback { + + private final ArangoOperations template; + + public RefResolver(final ArangoOperations template, final ConversionService conversionService) { + super(conversionService); + this.template = template; + } + + @Override + public Object resolveOne(final String id, final TypeInformation type, final Ref annotation) { + return annotation.lazy() ? proxy(id, type, annotation, this) : resolve(id, type, annotation); + } + + @Override + public Object resolveMultiple(final Collection ids, final TypeInformation type, final Ref annotation) { + return ids.stream().map(id -> resolveOne(id, getNonNullComponentType(type), annotation)) + .collect(Collectors.toList()); + } + + @Override + public Object resolve(final String id, final TypeInformation type, final Ref annotation) { + return template.find(id, type.getType()).get(); + } + +} 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 6a066fede..be370a66e 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 @@ -1,36 +1,38 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert.resolver; - -import java.lang.annotation.Annotation; -import java.util.Collection; - -/** - * @author Mark Vollmary - * - */ -public interface ReferenceResolver { - - Object resolveOne(String id, Class type, A annotation); - - Object resolveMultiple(Collection ids, Class type, A annotation); - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert.resolver; + +import java.lang.annotation.Annotation; +import java.util.Collection; + +import org.springframework.data.util.TypeInformation; + +/** + * @author Mark Vollmary + * + */ +public interface ReferenceResolver { + + Object resolveOne(String id, TypeInformation type, A annotation); + + Object resolveMultiple(Collection ids, TypeInformation type, A annotation); + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationResolver.java index 0804e8cdd..363fd3b59 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationResolver.java @@ -1,35 +1,37 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert.resolver; - -import java.lang.annotation.Annotation; - -/** - * @author Mark Vollmary - * - */ -public interface RelationResolver { - - Object resolveOne(String id, Class type, A annotation); - - Object resolveMultiple(String id, Class type, A annotation); - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert.resolver; + +import java.lang.annotation.Annotation; + +import org.springframework.data.util.TypeInformation; + +/** + * @author Mark Vollmary + * + */ +public interface RelationResolver { + + Object resolveOne(String id, TypeInformation type, A annotation); + + Object resolveMultiple(String id, TypeInformation type, A annotation); + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java index 551f2df1c..21b33d121 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/RelationsResolver.java @@ -1,72 +1,73 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert.resolver; - -import java.util.Arrays; -import java.util.Collection; - -import com.arangodb.model.AqlQueryOptions; -import com.arangodb.springframework.annotation.Relations; -import com.arangodb.springframework.core.ArangoOperations; -import com.arangodb.util.MapBuilder; - -/** - * @author Mark Vollmary - * - */ -public class RelationsResolver extends AbstractResolver - implements RelationResolver, AbstractResolver.ResolverCallback { - - private final ArangoOperations template; - - public RelationsResolver(final ArangoOperations template) { - super(); - this.template = template; - } - - @Override - public Object resolveOne(final String id, final Class type, final Relations annotation) { - throw new UnsupportedOperationException(); - } - - @Override - public Object resolveMultiple(final String id, final Class type, final Relations annotation) { - return annotation.lazy() ? proxy(id, Collection.class, annotation, (i, t, a) -> resolve(i, type, a)) - : resolve(id, type, annotation); - } - - @Override - public Object resolve(final String id, final Class type, final Relations annotation) { - return template - .query( - "WITH @@edges FOR v IN " + Math.max(1, annotation.minDepth()) + ".." - + Math.max(1, annotation.maxDepth()) + " " + annotation.direction() - + " @start @@edges OPTIONS {bfs: true, uniqueVertices: \"global\"} RETURN v", - new MapBuilder().put("start", id) - .put("@edges", - Arrays.asList(annotation.edges()).stream().map((e) -> template.collection(e).name()) - .reduce((a, b) -> a + ", " + b).get()) - .get(), - new AqlQueryOptions(), type) - .asListRemaining(); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert.resolver; + +import java.util.Arrays; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.util.TypeInformation; + +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.annotation.Relations; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.util.MapBuilder; + +/** + * @author Mark Vollmary + * @author Christian Lechner + * + */ +public class RelationsResolver extends AbstractResolver + implements RelationResolver, AbstractResolver.ResolverCallback { + + private final ArangoOperations template; + + public RelationsResolver(final ArangoOperations template, final ConversionService conversionService) { + super(conversionService); + this.template = template; + } + + @Override + public Object resolveOne(final String id, final TypeInformation type, final Relations annotation) { + throw new UnsupportedOperationException(); + } + + @Override + public Object resolveMultiple(final String id, final TypeInformation type, final Relations annotation) { + return annotation.lazy() ? proxy(id, type, annotation, this) : resolve(id, type, annotation); + } + + @Override + public Object resolve(final String id, final TypeInformation type, final Relations annotation) { + final Class t = getNonNullComponentType(type).getType(); + return template.query( + "WITH @@vertex FOR v IN " + Math.max(1, annotation.minDepth()) + ".." + Math.max(1, annotation.maxDepth()) + + " " + annotation.direction() + + " @start @@edges OPTIONS {bfs: true, uniqueVertices: \"global\"} RETURN v", + new MapBuilder().put("start", id) + .put("@edges", + Arrays.asList(annotation.edges()).stream().map((e) -> template.collection(e).name()) + .reduce((a, b) -> a + ", " + b).get()) + .put("@vertex", t).get(), + new AqlQueryOptions(), t).asListRemaining(); + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/convert/resolver/ToResolver.java b/src/main/java/com/arangodb/springframework/core/convert/resolver/ToResolver.java index 59126447e..280ae5935 100644 --- a/src/main/java/com/arangodb/springframework/core/convert/resolver/ToResolver.java +++ b/src/main/java/com/arangodb/springframework/core/convert/resolver/ToResolver.java @@ -1,67 +1,66 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.convert.resolver; - -import java.util.Collection; - -import com.arangodb.model.AqlQueryOptions; -import com.arangodb.springframework.annotation.To; -import com.arangodb.springframework.core.ArangoOperations; -import com.arangodb.util.MapBuilder; - -/** - * @author Mark Vollmary - * - */ -public class ToResolver extends AbstractResolver implements RelationResolver { - - private final ArangoOperations template; - - public ToResolver(final ArangoOperations template) { - super(); - this.template = template; - } - - @Override - public Object resolveOne(final String id, final Class type, final To annotation) { - return annotation.lazy() ? proxy(id, type, annotation, (i, t, a) -> internalResolveOne(i, t)) - : internalResolveOne(id, type); - } - - private Object internalResolveOne(final String id, final Class type) { - return template.find(id, type).get(); - } - - @Override - public Object resolveMultiple(final String id, final Class type, final To annotation) { - return annotation.lazy() - ? proxy(id, Collection.class, annotation, (i, t, a) -> internalResolveMultiple(i, type)) - : internalResolveMultiple(id, type); - } - - private Object internalResolveMultiple(final String id, final Class type) { - return template - .query("FOR e IN @@edge FILTER e._to == @id RETURN e", - new MapBuilder().put("@edge", type).put("id", id).get(), new AqlQueryOptions(), type) - .asListRemaining(); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.convert.resolver; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.util.TypeInformation; + +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.annotation.To; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.util.MapBuilder; + +/** + * @author Mark Vollmary + * + */ +public class ToResolver extends AbstractResolver implements RelationResolver { + + private final ArangoOperations template; + + public ToResolver(final ArangoOperations template, final ConversionService conversionService) { + super(conversionService); + this.template = template; + } + + @Override + public Object resolveOne(final String id, final TypeInformation type, final To annotation) { + return annotation.lazy() ? proxy(id, type, annotation, (i, t, a) -> internalResolveOne(i, t)) + : internalResolveOne(id, type); + } + + private Object internalResolveOne(final String id, final TypeInformation type) { + return template.find(id, type.getType()).get(); + } + + @Override + public Object resolveMultiple(final String id, final TypeInformation type, final To annotation) { + return annotation.lazy() ? proxy(id, type, annotation, (i, t, a) -> internalResolveMultiple(i, t)) + : internalResolveMultiple(id, type); + } + + private Object internalResolveMultiple(final String id, final TypeInformation type) { + final Class t = getNonNullComponentType(type).getType(); + return template.query("FOR e IN @@edge FILTER e._to == @id RETURN e", + new MapBuilder().put("@edge", t).put("id", id).get(), new AqlQueryOptions(), t).asListRemaining(); + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/mapping/ArangoSimpleTypes.java b/src/main/java/com/arangodb/springframework/core/mapping/ArangoSimpleTypes.java new file mode 100644 index 000000000..3cee178b2 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/mapping/ArangoSimpleTypes.java @@ -0,0 +1,112 @@ +/* + * DISCLAIMER + * + * Copyright 2018 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.springframework.core.mapping; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Timestamp; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import org.springframework.data.mapping.model.SimpleTypeHolder; + +import com.arangodb.entity.BaseDocument; +import com.arangodb.entity.BaseEdgeDocument; +import com.arangodb.springframework.core.convert.DBEntity; +import com.arangodb.velocypack.VPackSlice; + +/** + * This class contains all types that are directly supported by the Java driver (through java-velocypack). + * + * @author Christian Lechner + * + */ +@SuppressWarnings("deprecation") +public abstract class ArangoSimpleTypes { + + private static final Set> ARANGO_SIMPLE_TYPES; + + static { + final Set> simpleTypes = new HashSet<>(); + + // com.arangodb.* + simpleTypes.add(DBEntity.class); + simpleTypes.add(BaseDocument.class); + simpleTypes.add(BaseEdgeDocument.class); + simpleTypes.add(VPackSlice.class); + + // primitives + simpleTypes.add(boolean.class); + simpleTypes.add(byte.class); + simpleTypes.add(char.class); + simpleTypes.add(short.class); + simpleTypes.add(int.class); + simpleTypes.add(long.class); + simpleTypes.add(float.class); + simpleTypes.add(double.class); + + // primitive arrays + simpleTypes.add(boolean[].class); + simpleTypes.add(byte[].class); + simpleTypes.add(char[].class); + simpleTypes.add(short[].class); + simpleTypes.add(int[].class); + simpleTypes.add(long[].class); + simpleTypes.add(float[].class); + simpleTypes.add(double[].class); + + // java.lang.* + simpleTypes.add(Boolean.class); + simpleTypes.add(Byte.class); + simpleTypes.add(Character.class); + simpleTypes.add(Short.class); + simpleTypes.add(Integer.class); + simpleTypes.add(Long.class); + simpleTypes.add(Float.class); + simpleTypes.add(Double.class); + simpleTypes.add(Number.class); + simpleTypes.add(String.class); + simpleTypes.add(Enum.class); + + // java.math.* + simpleTypes.add(BigInteger.class); + simpleTypes.add(BigDecimal.class); + + // java.util.* + simpleTypes.add(UUID.class); + simpleTypes.add(Date.class); + + // java.sql.* + simpleTypes.add(java.sql.Date.class); + simpleTypes.add(Timestamp.class); + + ARANGO_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); + } + + public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(ARANGO_SIMPLE_TYPES, false); + + private ArangoSimpleTypes() { + } + +} \ No newline at end of file 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 26b88e8ae..f45884441 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentEntity.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentEntity.java @@ -125,6 +125,9 @@ private static CollectionCreateOptions createCollectionOptions(final Document an if (annotation.replicationFactor() > -1) { options.replicationFactor(annotation.replicationFactor()); } + if (annotation.satellite()) { + options.satellite(annotation.satellite()); + } final String[] shardKeys = annotation.shardKeys(); if (shardKeys.length > 1 || (shardKeys.length > 0 && StringUtils.hasText(shardKeys[0]))) { options.shardKeys(shardKeys); 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 02af1c38d..1812c61c3 100644 --- a/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentProperty.java +++ b/src/main/java/com/arangodb/springframework/core/mapping/DefaultArangoPersistentProperty.java @@ -1,148 +1,149 @@ -/* - * DISCLAIMER - * - * Copyright 2017 ArangoDB GmbH, Cologne, Germany - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Copyright holder is ArangoDB GmbH, Cologne, Germany - */ - -package com.arangodb.springframework.core.mapping; - -import java.beans.PropertyDescriptor; -import java.util.Optional; - -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; -import org.springframework.data.mapping.model.FieldNamingStrategy; -import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; -import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.util.StringUtils; - -import com.arangodb.springframework.annotation.Field; -import com.arangodb.springframework.annotation.From; -import com.arangodb.springframework.annotation.FulltextIndexed; -import com.arangodb.springframework.annotation.GeoIndexed; -import com.arangodb.springframework.annotation.HashIndexed; -import com.arangodb.springframework.annotation.Key; -import com.arangodb.springframework.annotation.PersistentIndexed; -import com.arangodb.springframework.annotation.Ref; -import com.arangodb.springframework.annotation.Relations; -import com.arangodb.springframework.annotation.Rev; -import com.arangodb.springframework.annotation.SkiplistIndexed; -import com.arangodb.springframework.annotation.To; - -/** - * @author Mark Vollmary - * - */ -public class DefaultArangoPersistentProperty extends AnnotationBasedPersistentProperty - implements ArangoPersistentProperty { - - private final FieldNamingStrategy fieldNamingStrategy; - - public DefaultArangoPersistentProperty(final java.lang.reflect.Field field, - final PropertyDescriptor propertyDescriptor, final PersistentEntity owner, - final SimpleTypeHolder simpleTypeHolder, final FieldNamingStrategy fieldNamingStrategy) { - super(field, propertyDescriptor, owner, simpleTypeHolder); - this.fieldNamingStrategy = fieldNamingStrategy != null ? fieldNamingStrategy - : PropertyNameFieldNamingStrategy.INSTANCE; - } - - @Override - protected Association createAssociation() { - return new Association<>(this, null); - } - - @Override - public boolean isKeyProperty() { - return findAnnotation(Key.class) != null; - } - - @Override - public boolean isRevProperty() { - return findAnnotation(Rev.class) != null; - } - - @Override - public Optional getRef() { - return Optional.ofNullable(findAnnotation(Ref.class)); - } - - @Override - public Optional getRelations() { - return Optional.ofNullable(findAnnotation(Relations.class)); - } - - @Override - public Optional getFrom() { - return Optional.ofNullable(findAnnotation(From.class)); - } - - @Override - public Optional getTo() { - return Optional.ofNullable(findAnnotation(To.class)); - } - - @Override - public String getFieldName() { - final String fieldName; - if (isIdProperty()) { - fieldName = "_id"; - } else if (isKeyProperty()) { - fieldName = "_key"; - } else if (isRevProperty()) { - fieldName = "_rev"; - } else if (getFrom().isPresent()) { - fieldName = "_from"; - } else if (getTo().isPresent()) { - fieldName = "_to"; - } else { - fieldName = getAnnotatedFieldName().orElse(fieldNamingStrategy.getFieldName(this)); - } - return fieldName; - } - - private Optional getAnnotatedFieldName() { - return Optional.ofNullable(findAnnotation(Field.class)) - .map(f -> StringUtils.hasText(f.value()) ? f.value() : null); - } - - @Override - public Optional getHashIndexed() { - return Optional.ofNullable(findAnnotation(HashIndexed.class)); - } - - @Override - public Optional getSkiplistIndexed() { - return Optional.ofNullable(findAnnotation(SkiplistIndexed.class)); - } - - @Override - public Optional getPersistentIndexed() { - return Optional.ofNullable(findAnnotation(PersistentIndexed.class)); - } - - @Override - public Optional getGeoIndexed() { - return Optional.ofNullable(findAnnotation(GeoIndexed.class)); - } - - @Override - public Optional getFulltextIndexed() { - return Optional.ofNullable(findAnnotation(FulltextIndexed.class)); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.springframework.core.mapping; + +import java.beans.PropertyDescriptor; +import java.util.Optional; + +import org.springframework.data.mapping.Association; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.model.FieldNamingStrategy; +import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.util.StringUtils; + +import com.arangodb.springframework.annotation.Field; +import com.arangodb.springframework.annotation.From; +import com.arangodb.springframework.annotation.FulltextIndexed; +import com.arangodb.springframework.annotation.GeoIndexed; +import com.arangodb.springframework.annotation.HashIndexed; +import com.arangodb.springframework.annotation.Key; +import com.arangodb.springframework.annotation.PersistentIndexed; +import com.arangodb.springframework.annotation.Ref; +import com.arangodb.springframework.annotation.Relations; +import com.arangodb.springframework.annotation.Rev; +import com.arangodb.springframework.annotation.SkiplistIndexed; +import com.arangodb.springframework.annotation.To; + +/** + * @author Mark Vollmary + * + */ +@SuppressWarnings("deprecation") +public class DefaultArangoPersistentProperty extends AnnotationBasedPersistentProperty + implements ArangoPersistentProperty { + + private final FieldNamingStrategy fieldNamingStrategy; + + public DefaultArangoPersistentProperty(final java.lang.reflect.Field field, + final PropertyDescriptor propertyDescriptor, final PersistentEntity owner, + final SimpleTypeHolder simpleTypeHolder, final FieldNamingStrategy fieldNamingStrategy) { + super(field, propertyDescriptor, owner, simpleTypeHolder); + this.fieldNamingStrategy = fieldNamingStrategy != null ? fieldNamingStrategy + : PropertyNameFieldNamingStrategy.INSTANCE; + } + + @Override + protected Association createAssociation() { + return new Association<>(this, null); + } + + @Override + public boolean isKeyProperty() { + return findAnnotation(Key.class) != null; + } + + @Override + public boolean isRevProperty() { + return findAnnotation(Rev.class) != null; + } + + @Override + public Optional getRef() { + return Optional.ofNullable(findAnnotation(Ref.class)); + } + + @Override + public Optional getRelations() { + return Optional.ofNullable(findAnnotation(Relations.class)); + } + + @Override + public Optional getFrom() { + return Optional.ofNullable(findAnnotation(From.class)); + } + + @Override + public Optional getTo() { + return Optional.ofNullable(findAnnotation(To.class)); + } + + @Override + public String getFieldName() { + final String fieldName; + if (isIdProperty()) { + fieldName = "_id"; + } else if (isKeyProperty()) { + fieldName = "_key"; + } else if (isRevProperty()) { + fieldName = "_rev"; + } else if (getFrom().isPresent()) { + fieldName = "_from"; + } else if (getTo().isPresent()) { + fieldName = "_to"; + } else { + fieldName = getAnnotatedFieldName().orElse(fieldNamingStrategy.getFieldName(this)); + } + return fieldName; + } + + private Optional getAnnotatedFieldName() { + return Optional.ofNullable(findAnnotation(Field.class)) + .map(f -> StringUtils.hasText(f.value()) ? f.value() : null); + } + + @Override + public Optional getHashIndexed() { + return Optional.ofNullable(findAnnotation(HashIndexed.class)); + } + + @Override + public Optional getSkiplistIndexed() { + return Optional.ofNullable(findAnnotation(SkiplistIndexed.class)); + } + + @Override + public Optional getPersistentIndexed() { + return Optional.ofNullable(findAnnotation(PersistentIndexed.class)); + } + + @Override + public Optional getGeoIndexed() { + return Optional.ofNullable(findAnnotation(GeoIndexed.class)); + } + + @Override + public Optional getFulltextIndexed() { + return Optional.ofNullable(findAnnotation(FulltextIndexed.class)); + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoCursorInitializer.java b/src/main/java/com/arangodb/springframework/core/template/ArangoCursorInitializer.java index 9c21afe07..7e2a4af38 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoCursorInitializer.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoCursorInitializer.java @@ -41,7 +41,7 @@ public ArangoCursorInitializer(final ArangoConverter converter) { @Override public ArangoCursor createInstance( - final InternalArangoDatabase db, + final InternalArangoDatabase db, final ArangoCursorExecute execute, final Class type, final CursorEntity result) { diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoExtCursor.java b/src/main/java/com/arangodb/springframework/core/template/ArangoExtCursor.java index 797013ddb..fc70591d9 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoExtCursor.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoExtCursor.java @@ -1,53 +1,52 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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 com.arangodb.ArangoCursor; -import com.arangodb.entity.CursorEntity; -import com.arangodb.internal.ArangoCursorExecute; -import com.arangodb.internal.ArangoCursorIterator; -import com.arangodb.internal.InternalArangoDatabase; -import com.arangodb.internal.net.HostHandle; -import com.arangodb.springframework.core.convert.ArangoConverter; - -/** - * @author Mark Vollmary - * @param - * - */ -class ArangoExtCursor extends ArangoCursor { - - protected ArangoExtCursor(final InternalArangoDatabase db, final ArangoCursorExecute execute, - final Class type, final CursorEntity result, final ArangoConverter converter) { - super(db, execute, type, result); - ArangoExtCursorIterator.class.cast(iterator).setConverter(converter); - } - - @Override - protected ArangoCursorIterator createIterator( - final ArangoCursor cursor, - final InternalArangoDatabase db, - final ArangoCursorExecute execute, - final CursorEntity result, - final HostHandle hostHandle) { - return new ArangoExtCursorIterator<>(cursor, db, execute, result, hostHandle); - } -} +/* + * DISCLAIMER + * + * Copyright 2017 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 com.arangodb.ArangoCursor; +import com.arangodb.entity.CursorEntity; +import com.arangodb.internal.ArangoCursorExecute; +import com.arangodb.internal.ArangoCursorImpl; +import com.arangodb.internal.ArangoCursorIterator; +import com.arangodb.internal.InternalArangoDatabase; +import com.arangodb.springframework.core.convert.ArangoConverter; + +/** + * @author Mark Vollmary + * @param + * + */ +class ArangoExtCursor extends ArangoCursorImpl { + + protected ArangoExtCursor(final InternalArangoDatabase db, final ArangoCursorExecute execute, + final Class type, final CursorEntity result, final ArangoConverter converter) { + super(db, execute, type, result); + ArangoExtCursorIterator.class.cast(iterator).setConverter(converter); + } + + @Override + protected ArangoCursorIterator createIterator( + final ArangoCursor cursor, + final InternalArangoDatabase db, + final ArangoCursorExecute execute, + final CursorEntity result) { + return new ArangoExtCursorIterator<>(cursor, db, execute, result); + } +} diff --git a/src/main/java/com/arangodb/springframework/core/template/ArangoExtCursorIterator.java b/src/main/java/com/arangodb/springframework/core/template/ArangoExtCursorIterator.java index 6afc5a8bd..15ec217e0 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoExtCursorIterator.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoExtCursorIterator.java @@ -1,57 +1,90 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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 com.arangodb.ArangoCursor; -import com.arangodb.entity.CursorEntity; -import com.arangodb.internal.ArangoCursorExecute; -import com.arangodb.internal.ArangoCursorIterator; -import com.arangodb.internal.InternalArangoDatabase; -import com.arangodb.internal.net.HostHandle; -import com.arangodb.springframework.core.convert.ArangoConverter; -import com.arangodb.springframework.core.convert.DBEntity; -import com.arangodb.velocypack.VPackSlice; - -/** - * @author Mark Vollmary - * @param - * - */ -class ArangoExtCursorIterator extends ArangoCursorIterator { - - private ArangoConverter converter; - - protected ArangoExtCursorIterator(final ArangoCursor cursor, final InternalArangoDatabase db, - final ArangoCursorExecute execute, final CursorEntity result, final HostHandle hostHandle) { - super(cursor, execute, db, result, hostHandle); - } - - public void setConverter(final ArangoConverter converter) { - this.converter = converter; - } - - @Override - protected R deserialize(final VPackSlice result, final Class type) { - return !converter.isEntityType(type) ? super.deserialize(result, type) - : converter.read(type, super.deserialize(result, DBEntity.class)); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.mapping.model.SimpleTypeHolder; + +import com.arangodb.ArangoCursor; +import com.arangodb.entity.CursorEntity; +import com.arangodb.internal.ArangoCursorExecute; +import com.arangodb.internal.ArangoCursorIterator; +import com.arangodb.internal.InternalArangoDatabase; +import com.arangodb.springframework.core.convert.ArangoConverter; +import com.arangodb.springframework.core.convert.DBEntity; +import com.arangodb.velocypack.VPackSlice; + +/** + * @author Mark Vollmary + * @author Christian Lechner + * + * @param + * + */ +@SuppressWarnings("deprecation") +class ArangoExtCursorIterator extends ArangoCursorIterator { + + private static final SimpleTypeHolder ADDITIONAL_DESERIALIZABLE_TYPES; + + static { + final Set> simpleTypes = new HashSet<>(); + + // the following types apply only if the VPackJdk8Module is present on the + // ArangoDB Java driver, + // but there is no possibility to check + simpleTypes.add(Instant.class); + simpleTypes.add(LocalDate.class); + simpleTypes.add(LocalDateTime.class); + simpleTypes.add(OffsetDateTime.class); + simpleTypes.add(ZonedDateTime.class); + + ADDITIONAL_DESERIALIZABLE_TYPES = new SimpleTypeHolder(simpleTypes, false); + } + + private ArangoConverter converter; + + protected ArangoExtCursorIterator(final ArangoCursor cursor, final InternalArangoDatabase db, + final ArangoCursorExecute execute, final CursorEntity result) { + super(cursor, execute, db, result); + } + + public void setConverter(final ArangoConverter converter) { + this.converter = converter; + } + + @Override + protected R deserialize(final VPackSlice result, final Class type) { + return canDeserializeDirectly(type) ? super.deserialize(result, type) + : converter.read(type, super.deserialize(result, DBEntity.class)); + } + + private boolean canDeserializeDirectly(final Class type) { + return !converter.isEntityType(type) || ADDITIONAL_DESERIALIZABLE_TYPES.isSimpleType(type); + } + +} 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 f83a907cc..49491d5b1 100644 --- a/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java +++ b/src/main/java/com/arangodb/springframework/core/template/ArangoTemplate.java @@ -1,679 +1,719 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.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.Optional; -import java.util.stream.Collectors; - -import org.springframework.dao.DataAccessException; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.dao.support.PersistenceExceptionTranslator; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; - -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.DBCollectionEntity; -import com.arangodb.springframework.core.convert.DBDocumentEntity; -import com.arangodb.springframework.core.convert.DBEntity; -import com.arangodb.springframework.core.convert.DBEntityDeserializer; -import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; -import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; -import com.arangodb.springframework.core.template.DefaultUserOperation.CollectionCallback; -import com.arangodb.springframework.core.util.ArangoExceptionTranslator; -import com.arangodb.util.MapBuilder; - -/** - * @author Mark Vollmary - * - */ -public class ArangoTemplate implements ArangoOperations, CollectionCallback { - - private ArangoDBVersion version; - private final PersistenceExceptionTranslator exceptionTranslator; - private final ArangoConverter converter; - private final ArangoDB arango; - private ArangoDatabase database; - private final String databaseName; - private final Map collectionCache; - - public ArangoTemplate(final ArangoDB.Builder arango, final String database) { - this(arango, database, null); - } - - public ArangoTemplate(final ArangoDB.Builder arango, final String database, final ArangoConverter converter) { - this(arango, database, converter, new ArangoExceptionTranslator()); - } - - public ArangoTemplate(final ArangoDB.Builder arango, final String database, final ArangoConverter converter, - final PersistenceExceptionTranslator exceptionTranslator) { - super(); - this.arango = arango.registerDeserializer(DBEntity.class, new DBEntityDeserializer()).build() - ._setCursorInitializer( - new com.arangodb.springframework.core.template.ArangoCursorInitializer(converter)); - this.databaseName = database; - this.converter = converter; - this.exceptionTranslator = exceptionTranslator; - collectionCache = new HashMap<>(); - version = null; - } - - private ArangoDatabase db() { - if (database == null) { - database = arango.db(databaseName); - try { - database.getInfo(); - } catch (final ArangoDBException e) { - if (new Integer(404).equals(e.getResponseCode())) { - try { - arango.createDatabase(databaseName); - } catch (final ArangoDBException e1) { - throw translateExceptionIfPossible(e1); - } - } else { - throw translateExceptionIfPossible(e); - } - } - } - return database; - } - - private DataAccessException translateExceptionIfPossible(final RuntimeException exception) { - return exceptionTranslator.translateExceptionIfPossible(exception); - } - - private ArangoCollection _collection(final String name) { - return _collection(name, null, null); - } - - private ArangoCollection _collection(final Class entityClass) { - return _collection(entityClass, null); - } - - private ArangoCollection _collection(final Class entityClass, final String id) { - final String name = determineCollectionFromId(Optional.ofNullable(id)) - .orElse(getPersistentEntity(entityClass).getCollection()); - final ArangoPersistentEntity persistentEntity = getPersistentEntity(entityClass); - return _collection(name, persistentEntity, persistentEntity.getCollectionOptions()); - } - - private ArangoCollection _collection( - final String name, - final ArangoPersistentEntity persistentEntity, - final CollectionCreateOptions options) { - ArangoCollection collection = collectionCache.get(name); - if (collection == null) { - collection = db().collection(name); - try { - collection.getInfo(); - } catch (final ArangoDBException e) { - if (new Integer(404).equals(e.getResponseCode())) { - try { - db().createCollection(name, options); - } catch (final ArangoDBException e1) { - throw translateExceptionIfPossible(e1); - } - } else { - throw translateExceptionIfPossible(e); - } - } - collectionCache.put(name, collection); - if (persistentEntity != null) { - ensureCollectionIndexes(collection(collection), persistentEntity); - } - } - return collection; - } - - 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)); - persistentEntity.getSkiplistIndexedProperties().stream().forEach(p -> ensureSkiplistIndex(collection, p)); - persistentEntity.getPersistentIndexes().stream().forEach(index -> ensurePersistentIndex(collection, index)); - persistentEntity.getPersistentIndexedProperties().stream().forEach(p -> ensurePersistentIndex(collection, p)); - persistentEntity.getGeoIndexes().stream().forEach(index -> ensureGeoIndex(collection, index)); - persistentEntity.getGeoIndexedProperties().stream().forEach(p -> ensureGeoIndex(collection, p)); - persistentEntity.getFulltextIndexes().stream().forEach(index -> ensureFulltextIndex(collection, index)); - persistentEntity.getFulltextIndexedProperties().stream().forEach(p -> ensureFulltextIndex(collection, p)); - } - - private static void ensureHashIndex(final CollectionOperations collection, final HashIndex annotation) { - collection.ensureHashIndex(Arrays.asList(annotation.fields()), new HashIndexOptions() - .unique(annotation.unique()).sparse(annotation.sparse()).deduplicate(annotation.deduplicate())); - } - - private static void ensureHashIndex(final CollectionOperations collection, final ArangoPersistentProperty value) { - final HashIndexOptions options = new HashIndexOptions(); - value.getHashIndexed() - .ifPresent(i -> options.unique(i.unique()).sparse(i.sparse()).deduplicate(i.deduplicate())); - collection.ensureHashIndex(Collections.singleton(value.getFieldName()), options); - } - - private static void ensureSkiplistIndex(final CollectionOperations collection, final SkiplistIndex annotation) { - collection.ensureSkiplistIndex(Arrays.asList(annotation.fields()), new SkiplistIndexOptions() - .unique(annotation.unique()).sparse(annotation.sparse()).deduplicate(annotation.deduplicate())); - } - - 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())); - collection.ensureSkiplistIndex(Collections.singleton(value.getFieldName()), options); - } - - private static void ensurePersistentIndex(final CollectionOperations collection, final PersistentIndex annotation) { - collection.ensurePersistentIndex(Arrays.asList(annotation.fields()), - new PersistentIndexOptions().unique(annotation.unique()).sparse(annotation.sparse())); - } - - 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); - } - - private static void ensureGeoIndex(final CollectionOperations collection, final GeoIndex annotation) { - collection.ensureGeoIndex(Arrays.asList(annotation.fields()), - new GeoIndexOptions().geoJson(annotation.geoJson())); - } - - private static void ensureGeoIndex(final CollectionOperations collection, final ArangoPersistentProperty value) { - final GeoIndexOptions options = new GeoIndexOptions(); - value.getGeoIndexed().ifPresent(i -> options.geoJson(i.geoJson())); - collection.ensureGeoIndex(Collections.singleton(value.getFieldName()), options); - } - - 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)); - } - - 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); - } - - private ArangoPersistentEntity getPersistentEntity(final Class entityClass) { - final ArangoPersistentEntity persistentEntity = converter.getMappingContext() - .getPersistentEntity(entityClass); - if (persistentEntity == null) { - new InvalidDataAccessApiUsageException( - "No persistent entity information found for the type " + entityClass.getName()); - } - return persistentEntity; - } - - private Optional determineCollectionFromId(final Optional id) { - return id.map(i -> { - final String[] split = i.split("/"); - return split.length == 2 ? split[0] : null; - }); - } - - private String determineDocumentKeyFromId(final String id) { - final String[] split = id.split("/"); - return split[split.length - 1]; - } - - private DBEntity toDBEntity(final Object value) { - final DBEntity entity = converter.isCollectionType(value.getClass()) ? new DBCollectionEntity() - : new DBDocumentEntity(); - converter.write(value, entity); - return entity; - } - - private T fromDBEntity(final Class entityClass, final DBEntity doc) { - return converter.read(entityClass, doc); - } - - @Override - public ArangoDB driver() { - return arango; - } - - @Override - public ArangoDBVersion getVersion() throws DataAccessException { - try { - if (version == null) { - version = arango.getVersion(); - } - return version; - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public ArangoCursor query( - final String query, - final Map bindVars, - final AqlQueryOptions options, - final Class entityClass) throws DataAccessException { - return db().query(query, DBDocumentEntity.class.cast(toDBEntity(prepareBindVars(bindVars))), options, - entityClass); - } - - private Map prepareBindVars(final Map bindVars) { - for (final Map.Entry entry : new HashMap<>(bindVars).entrySet()) { - if (entry.getKey().startsWith("@") && entry.getValue() instanceof Class) { - bindVars.put(entry.getKey(), _collection((Class) entry.getValue()).name()); - } - } - return bindVars; - } - - @Override - public MultiDocumentEntity delete( - final Iterable values, - final Class entityClass, - final DocumentDeleteOptions options) throws DataAccessException { - try { - return _collection(entityClass).deleteDocuments(DBCollectionEntity.class.cast(toDBEntity(values)), - entityClass, options); - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public MultiDocumentEntity delete( - final Iterable values, - final Class entityClass) throws DataAccessException { - return delete(values, entityClass, new DocumentDeleteOptions()); - } - - @Override - public DocumentEntity delete(final String id, final Class entityClass, final DocumentDeleteOptions options) - throws DataAccessException { - try { - return _collection(entityClass, id).deleteDocument(determineDocumentKeyFromId(id), entityClass, options); - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public DocumentEntity delete(final String id, final Class entityClass) throws DataAccessException { - return delete(id, entityClass, new DocumentDeleteOptions()); - } - - @Override - public MultiDocumentEntity update( - final Iterable values, - final Class entityClass, - final DocumentUpdateOptions options) throws DataAccessException { - try { - final MultiDocumentEntity res = _collection(entityClass) - .updateDocuments(DBCollectionEntity.class.cast(toDBEntity(values)), options); - updateDBFields(values, entityClass, res); - return res; - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public MultiDocumentEntity update( - final Iterable values, - final Class entityClass) throws DataAccessException { - return update(values, entityClass, new DocumentUpdateOptions()); - } - - @Override - public DocumentEntity update(final String id, final Object value, final DocumentUpdateOptions options) - throws DataAccessException { - try { - final DocumentEntity res = _collection(value.getClass(), id).updateDocument(determineDocumentKeyFromId(id), - toDBEntity(value)); - updateDBFields(value, res); - return res; - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public DocumentEntity update(final String id, final Object value) throws DataAccessException { - return update(id, value, new DocumentUpdateOptions()); - } - - @Override - public MultiDocumentEntity replace( - final Iterable values, - final Class entityClass, - final DocumentReplaceOptions options) throws DataAccessException { - try { - final MultiDocumentEntity res = _collection(entityClass) - .replaceDocuments(DBCollectionEntity.class.cast(toDBEntity(values)), options); - updateDBFields(values, entityClass, res); - return res; - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public MultiDocumentEntity replace( - final Iterable values, - final Class entityClass) throws DataAccessException { - return replace(values, entityClass, new DocumentReplaceOptions()); - } - - @Override - public DocumentEntity replace(final String id, final Object value, final DocumentReplaceOptions options) - throws DataAccessException { - try { - final DocumentEntity res = _collection(value.getClass(), id).replaceDocument(determineDocumentKeyFromId(id), - toDBEntity(value), options); - updateDBFields(value, res); - return res; - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public DocumentEntity replace(final String id, final Object value) throws DataAccessException { - return replace(id, value, new DocumentReplaceOptions()); - } - - @Override - public Optional find(final String id, final Class entityClass, final DocumentReadOptions options) - throws DataAccessException { - try { - final DBEntity doc = _collection(entityClass, id).getDocument(determineDocumentKeyFromId(id), - DBEntity.class, options); - return Optional.ofNullable(fromDBEntity(entityClass, doc)); - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public Optional find(final String id, final Class entityClass) throws DataAccessException { - return find(id, entityClass, new DocumentReadOptions()); - } - - @Override - public Iterable findAll(final Class entityClass) throws DataAccessException { - final String query = "FOR entity IN @@col RETURN entity"; - return new Iterable() { - @Override - public Iterator iterator() { - return query(query, new MapBuilder().put("@col", entityClass).get(), null, entityClass); - } - }; - } - - @Override - public Iterable find(final Iterable ids, final Class entityClass) throws DataAccessException { - try { - final Collection keys = new ArrayList<>(); - ids.forEach(id -> keys.add(determineDocumentKeyFromId(id))); - final MultiDocumentEntity docs = _collection(entityClass).getDocuments(keys, DBEntity.class); - return docs.getDocuments().stream().map(doc -> fromDBEntity(entityClass, doc)).collect(Collectors.toList()); - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public MultiDocumentEntity insert( - final Iterable values, - final Class entityClass, - final DocumentCreateOptions options) throws DataAccessException { - try { - final MultiDocumentEntity res = _collection(entityClass) - .insertDocuments(DBCollectionEntity.class.cast(toDBEntity(values)), options); - updateDBFields(values, entityClass, res); - return res; - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public MultiDocumentEntity insert( - final Iterable values, - final Class entityClass) throws DataAccessException { - return insert(values, entityClass, new DocumentCreateOptions()); - } - - @Override - public DocumentEntity insert(final Object value, final DocumentCreateOptions options) throws DataAccessException { - try { - final DocumentEntity res = _collection(value.getClass()).insertDocument(toDBEntity(value)); - updateDBFields(value, res); - return res; - } catch (final ArangoDBException e) { - throw exceptionTranslator.translateExceptionIfPossible(e); - } - } - - @Override - public DocumentEntity insert(final Object value) throws DataAccessException { - return insert(value, new DocumentCreateOptions()); - } - - @Override - public void upsert(final T value, final UpsertStrategy strategy) throws DataAccessException { - final Class entityClass = value.getClass(); - final ArangoPersistentEntity entity = getConverter().getMappingContext().getPersistentEntity(entityClass); - final ArangoPersistentProperty idProperty = entity.getIdProperty(); - if (idProperty != null) { - final ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor( - entity.getPropertyAccessor(value), converter.getConversionService()); - final Object id = accessor.getProperty(idProperty); - if (id != null) { - switch (strategy) { - case UPDATE: - update(id.toString(), value); - break; - case REPLACE: - default: - replace(id.toString(), value); - break; - } - return; - } - } - insert(value); - } - - @SuppressWarnings("unchecked") - @Override - public void upsert(final Iterable value, final UpsertStrategy strategy) throws DataAccessException { - Class entityClass = null; - ArangoPersistentEntity entity = null; - ArangoPersistentProperty idProperty = null; - final Collection withId = new ArrayList<>(); - final Collection withoutId = new ArrayList<>(); - for (final T e : value) { - if (entityClass == null) { - entityClass = (Class) e.getClass(); - entity = getConverter().getMappingContext().getPersistentEntity(entityClass); - idProperty = entity.getIdProperty(); - } - if (idProperty != null) { - final ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor( - entity.getPropertyAccessor(e), converter.getConversionService()); - final Object id = accessor.getProperty(idProperty); - if (id != null) { - withId.add(e); - continue; - } - } - withoutId.add(e); - } - if (!withoutId.isEmpty()) { - insert(withoutId, entityClass); - } - if (!withId.isEmpty()) { - switch (strategy) { - case UPDATE: - update(withId, entityClass); - break; - case REPLACE: - default: - replace(withId, entityClass); - break; - } - } - } - - private void updateDBFields( - final Iterable values, - final Class entityClass, - final MultiDocumentEntity res) { - final Iterator valueIterator = values.iterator(); - if (res.getErrors().isEmpty()) { - final Iterator documentIterator = res.getDocuments().iterator(); - for (; valueIterator.hasNext() && documentIterator.hasNext();) { - updateDBFields(valueIterator.next(), documentIterator.next()); - } - } else { - final Iterator documentIterator = res.getDocumentsAndErrors().iterator(); - for (; valueIterator.hasNext() && documentIterator.hasNext();) { - final Object nextDoc = documentIterator.next(); - final Object nextValue = valueIterator.next(); - if (nextDoc instanceof DocumentEntity) { - updateDBFields(nextValue, (DocumentEntity) nextDoc); - } - } - } - } - - private void updateDBFields(final Object value, final DocumentEntity documentEntity) { - final ArangoPersistentEntity entity = converter.getMappingContext().getPersistentEntity(value.getClass()); - final ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(value), - converter.getConversionService()); - final ArangoPersistentProperty idProperty = entity.getIdProperty(); - if (idProperty != null) { - accessor.setProperty(idProperty, documentEntity.getId()); - } - entity.getKeyProperty().ifPresent(key -> accessor.setProperty(key, documentEntity.getKey())); - entity.getRevProperty().ifPresent(rev -> accessor.setProperty(rev, documentEntity.getRev())); - } - - @Override - public boolean exists(final String id, final Class entityClass) throws DataAccessException { - try { - return _collection(entityClass).documentExists(determineDocumentKeyFromId(id)); - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public void dropDatabase() throws DataAccessException { - try { - db().drop(); - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - database = null; - collectionCache.clear(); - } - - @Override - public CollectionOperations collection(final Class entityClass) throws DataAccessException { - return collection(_collection(entityClass)); - } - - @Override - public CollectionOperations collection(final String name) throws DataAccessException { - return collection(_collection(name)); - } - - @Override - public CollectionOperations collection(final String name, final CollectionCreateOptions options) - throws DataAccessException { - return collection(_collection(name, null, options)); - } - - private CollectionOperations collection(final ArangoCollection collection) { - return new DefaultCollectionOperations(collection, collectionCache, exceptionTranslator); - } - - @Override - public UserOperations user(final String username) { - return new DefaultUserOperation(db(), username, exceptionTranslator, this); - } - - @Override - public Iterable getUsers() throws DataAccessException { - try { - return arango.getUsers(); - } catch (final ArangoDBException e) { - throw translateExceptionIfPossible(e); - } - } - - @Override - public ArangoConverter getConverter() { - return this.converter; - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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.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.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.springframework.dao.DataAccessException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.data.domain.Persistable; +import org.springframework.data.mapping.PersistentPropertyAccessor; + +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.DBCollectionEntity; +import com.arangodb.springframework.core.convert.DBDocumentEntity; +import com.arangodb.springframework.core.convert.DBEntity; +import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; +import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; +import com.arangodb.springframework.core.template.DefaultUserOperation.CollectionCallback; +import com.arangodb.springframework.core.util.ArangoExceptionTranslator; +import com.arangodb.util.MapBuilder; + +/** + * @author Mark Vollmary + * @author Christian Lechner + * + */ +@SuppressWarnings("deprecation") +public class ArangoTemplate implements ArangoOperations, CollectionCallback { + + private volatile ArangoDBVersion version; + private final PersistenceExceptionTranslator exceptionTranslator; + private final ArangoConverter converter; + private final ArangoDB arango; + private volatile ArangoDatabase database; + private final String databaseName; + private final Map collectionCache; + + public ArangoTemplate(final ArangoDB.Builder arango, final String database) { + this(arango, database, null); + } + + public ArangoTemplate(final ArangoDB.Builder arango, final String database, final ArangoConverter converter) { + this(arango, database, converter, new ArangoExceptionTranslator()); + } + + public ArangoTemplate(final ArangoDB.Builder arango, final String database, final ArangoConverter converter, + final PersistenceExceptionTranslator exceptionTranslator) { + super(); + this.arango = arango.build()._setCursorInitializer( + new com.arangodb.springframework.core.template.ArangoCursorInitializer(converter)); + this.databaseName = database; + this.converter = converter; + this.exceptionTranslator = exceptionTranslator; + // set concurrency level to 1 as writes are very rare compared to reads + collectionCache = new ConcurrentHashMap<>(8, 0.9f, 1); + version = null; + } + + private ArangoDatabase db() { + // guard against NPE because database can be set to null by dropDatabase() by + // another thread + ArangoDatabase db = database; + if (db != null) { + return db; + } + // make sure the database is only created once + synchronized (this) { + db = database; + if (db != null) { + return db; + } + db = arango.db(databaseName); + try { + db.getInfo(); + } catch (final ArangoDBException e) { + if (new Integer(404).equals(e.getResponseCode())) { + try { + arango.createDatabase(databaseName); + } catch (final ArangoDBException e1) { + throw translateExceptionIfPossible(e1); + } + } else { + throw translateExceptionIfPossible(e); + } + } + database = db; + return db; + } + } + + private DataAccessException translateExceptionIfPossible(final RuntimeException exception) { + return exceptionTranslator.translateExceptionIfPossible(exception); + } + + private ArangoCollection _collection(final String name) { + return _collection(name, null, null); + } + + private ArangoCollection _collection(final Class entityClass) { + return _collection(entityClass, null); + } + + private ArangoCollection _collection(final Class entityClass, final String id) { + final ArangoPersistentEntity persistentEntity = converter.getMappingContext() + .getPersistentEntity(entityClass); + final String name = determineCollectionFromId(Optional.ofNullable(id)).orElse(persistentEntity.getCollection()); + return _collection(name, persistentEntity, persistentEntity.getCollectionOptions()); + } + + private ArangoCollection _collection( + final String name, + final ArangoPersistentEntity persistentEntity, + final CollectionCreateOptions options) { + + return collectionCache.computeIfAbsent(name, collName -> { + final ArangoCollection collection = db().collection(collName); + try { + collection.getInfo(); + } catch (final ArangoDBException e) { + if (new Integer(404).equals(e.getResponseCode())) { + try { + db().createCollection(collName, options); + } catch (final ArangoDBException e1) { + throw translateExceptionIfPossible(e1); + } + } else { + throw translateExceptionIfPossible(e); + } + } + if (persistentEntity != null) { + ensureCollectionIndexes(collection(collection), persistentEntity); + } + return collection; + }); + } + + 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)); + persistentEntity.getSkiplistIndexedProperties().stream().forEach(p -> ensureSkiplistIndex(collection, p)); + persistentEntity.getPersistentIndexes().stream().forEach(index -> ensurePersistentIndex(collection, index)); + persistentEntity.getPersistentIndexedProperties().stream().forEach(p -> ensurePersistentIndex(collection, p)); + persistentEntity.getGeoIndexes().stream().forEach(index -> ensureGeoIndex(collection, index)); + persistentEntity.getGeoIndexedProperties().stream().forEach(p -> ensureGeoIndex(collection, p)); + persistentEntity.getFulltextIndexes().stream().forEach(index -> ensureFulltextIndex(collection, index)); + persistentEntity.getFulltextIndexedProperties().stream().forEach(p -> ensureFulltextIndex(collection, p)); + } + + private static void ensureHashIndex(final CollectionOperations collection, final HashIndex annotation) { + collection.ensureHashIndex(Arrays.asList(annotation.fields()), new HashIndexOptions() + .unique(annotation.unique()).sparse(annotation.sparse()).deduplicate(annotation.deduplicate())); + } + + private static void ensureHashIndex(final CollectionOperations collection, final ArangoPersistentProperty value) { + final HashIndexOptions options = new HashIndexOptions(); + value.getHashIndexed() + .ifPresent(i -> options.unique(i.unique()).sparse(i.sparse()).deduplicate(i.deduplicate())); + collection.ensureHashIndex(Collections.singleton(value.getFieldName()), options); + } + + private static void ensureSkiplistIndex(final CollectionOperations collection, final SkiplistIndex annotation) { + collection.ensureSkiplistIndex(Arrays.asList(annotation.fields()), new SkiplistIndexOptions() + .unique(annotation.unique()).sparse(annotation.sparse()).deduplicate(annotation.deduplicate())); + } + + 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())); + collection.ensureSkiplistIndex(Collections.singleton(value.getFieldName()), options); + } + + private static void ensurePersistentIndex(final CollectionOperations collection, final PersistentIndex annotation) { + collection.ensurePersistentIndex(Arrays.asList(annotation.fields()), + new PersistentIndexOptions().unique(annotation.unique()).sparse(annotation.sparse())); + } + + 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); + } + + private static void ensureGeoIndex(final CollectionOperations collection, final GeoIndex annotation) { + collection.ensureGeoIndex(Arrays.asList(annotation.fields()), + new GeoIndexOptions().geoJson(annotation.geoJson())); + } + + private static void ensureGeoIndex(final CollectionOperations collection, final ArangoPersistentProperty value) { + final GeoIndexOptions options = new GeoIndexOptions(); + value.getGeoIndexed().ifPresent(i -> options.geoJson(i.geoJson())); + collection.ensureGeoIndex(Collections.singleton(value.getFieldName()), options); + } + + 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)); + } + + 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); + } + + private Optional determineCollectionFromId(final Optional id) { + return id.map(i -> { + final String[] split = i.split("/"); + return split.length == 2 ? split[0] : null; + }); + } + + private String determineDocumentKeyFromId(final String id) { + final String[] split = id.split("/"); + return split[split.length - 1]; + } + + private DBEntity toDBEntity(final Object value) { + final DBEntity entity = converter.isCollectionType(value.getClass()) ? new DBCollectionEntity() + : new DBDocumentEntity(); + converter.write(value, entity); + return entity; + } + + private T fromDBEntity(final Class entityClass, final DBEntity doc) { + return converter.read(entityClass, doc); + } + + @Override + public ArangoDB driver() { + return arango; + } + + @Override + public ArangoDBVersion getVersion() throws DataAccessException { + try { + if (version == null) { + version = db().getVersion(); + } + return version; + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public ArangoCursor query(final String query, final AqlQueryOptions options, final Class entityClass) + throws DataAccessException { + return db().query(query, null, options, entityClass); + } + + @Override + public ArangoCursor query( + final String query, + final Map bindVars, + final AqlQueryOptions options, + final Class entityClass) throws DataAccessException { + return db().query(query, + bindVars == null ? null : DBDocumentEntity.class.cast(toDBEntity(prepareBindVars(bindVars))), options, + entityClass); + } + + private Map prepareBindVars(final Map bindVars) { + for (final Map.Entry entry : new HashMap<>(bindVars).entrySet()) { + if (entry.getKey().startsWith("@") && entry.getValue() instanceof Class) { + bindVars.put(entry.getKey(), _collection((Class) entry.getValue()).name()); + } + } + return bindVars; + } + + @Override + public MultiDocumentEntity delete( + final Iterable values, + final Class entityClass, + final DocumentDeleteOptions options) throws DataAccessException { + try { + return _collection(entityClass).deleteDocuments(DBCollectionEntity.class.cast(toDBEntity(values)), + entityClass, options); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public MultiDocumentEntity delete( + final Iterable values, + final Class entityClass) throws DataAccessException { + return delete(values, entityClass, new DocumentDeleteOptions()); + } + + @Override + public DocumentEntity delete(final String id, final Class entityClass, final DocumentDeleteOptions options) + throws DataAccessException { + try { + return _collection(entityClass, id).deleteDocument(determineDocumentKeyFromId(id), entityClass, options); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public DocumentEntity delete(final String id, final Class entityClass) throws DataAccessException { + return delete(id, entityClass, new DocumentDeleteOptions()); + } + + @Override + public MultiDocumentEntity update( + final Iterable values, + final Class entityClass, + final DocumentUpdateOptions options) throws DataAccessException { + try { + final MultiDocumentEntity res = _collection(entityClass) + .updateDocuments(DBCollectionEntity.class.cast(toDBEntity(values)), options); + updateDBFields(values, res); + return res; + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public MultiDocumentEntity update( + final Iterable values, + final Class entityClass) throws DataAccessException { + return update(values, entityClass, new DocumentUpdateOptions()); + } + + @Override + public DocumentEntity update(final String id, final Object value, final DocumentUpdateOptions options) + throws DataAccessException { + try { + final DocumentEntity res = _collection(value.getClass(), id).updateDocument(determineDocumentKeyFromId(id), + toDBEntity(value), options); + updateDBFields(value, res); + return res; + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public DocumentEntity update(final String id, final Object value) throws DataAccessException { + return update(id, value, new DocumentUpdateOptions()); + } + + @Override + public MultiDocumentEntity replace( + final Iterable values, + final Class entityClass, + final DocumentReplaceOptions options) throws DataAccessException { + try { + final MultiDocumentEntity res = _collection(entityClass) + .replaceDocuments(DBCollectionEntity.class.cast(toDBEntity(values)), options); + updateDBFields(values, res); + return res; + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public MultiDocumentEntity replace( + final Iterable values, + final Class entityClass) throws DataAccessException { + return replace(values, entityClass, new DocumentReplaceOptions()); + } + + @Override + public DocumentEntity replace(final String id, final Object value, final DocumentReplaceOptions options) + throws DataAccessException { + try { + final DocumentEntity res = _collection(value.getClass(), id).replaceDocument(determineDocumentKeyFromId(id), + toDBEntity(value), options); + updateDBFields(value, res); + return res; + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public DocumentEntity replace(final String id, final Object value) throws DataAccessException { + return replace(id, value, new DocumentReplaceOptions()); + } + + @Override + public Optional find(final String id, final Class entityClass, final DocumentReadOptions options) + throws DataAccessException { + try { + final DBEntity doc = _collection(entityClass, id).getDocument(determineDocumentKeyFromId(id), + DBEntity.class, options); + return Optional.ofNullable(fromDBEntity(entityClass, doc)); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public Optional find(final String id, final Class entityClass) throws DataAccessException { + return find(id, entityClass, new DocumentReadOptions()); + } + + @Override + public Iterable findAll(final Class entityClass) throws DataAccessException { + final String query = "FOR entity IN @@col RETURN entity"; + return new Iterable() { + @Override + public Iterator iterator() { + return query(query, new MapBuilder().put("@col", entityClass).get(), null, entityClass); + } + }; + } + + @Override + public Iterable find(final Iterable ids, final Class entityClass) throws DataAccessException { + try { + final Collection keys = new ArrayList<>(); + ids.forEach(id -> keys.add(determineDocumentKeyFromId(id))); + final MultiDocumentEntity docs = _collection(entityClass).getDocuments(keys, DBEntity.class); + return docs.getDocuments().stream().map(doc -> fromDBEntity(entityClass, doc)).collect(Collectors.toList()); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public MultiDocumentEntity insert( + final Iterable values, + final Class entityClass, + final DocumentCreateOptions options) throws DataAccessException { + try { + final MultiDocumentEntity res = _collection(entityClass) + .insertDocuments(DBCollectionEntity.class.cast(toDBEntity(values)), options); + updateDBFields(values, res); + return res; + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public MultiDocumentEntity insert( + final Iterable values, + final Class entityClass) throws DataAccessException { + return insert(values, entityClass, new DocumentCreateOptions()); + } + + @Override + public DocumentEntity insert(final Object value, final DocumentCreateOptions options) throws DataAccessException { + try { + final DocumentEntity res = _collection(value.getClass()).insertDocument(toDBEntity(value), options); + updateDBFields(value, res); + return res; + } catch (final ArangoDBException e) { + throw exceptionTranslator.translateExceptionIfPossible(e); + } + } + + @Override + public DocumentEntity insert(final Object value) throws DataAccessException { + return insert(value, new DocumentCreateOptions()); + } + + @Override + public DocumentEntity insert(final String collectionName, final Object value, final DocumentCreateOptions options) + throws DataAccessException { + try { + final DocumentEntity res = _collection(collectionName).insertDocument(toDBEntity(value), options); + updateDBFields(value, res); + return res; + } catch (final ArangoDBException e) { + throw exceptionTranslator.translateExceptionIfPossible(e); + } + } + + @Override + public DocumentEntity insert(final String collectionName, final Object value) throws DataAccessException { + return insert(collectionName, value, new DocumentCreateOptions()); + } + + @Override + public void upsert(final T value, final UpsertStrategy strategy) throws DataAccessException { + final Class entityClass = value.getClass(); + final ArangoPersistentEntity entity = getConverter().getMappingContext().getPersistentEntity(entityClass); + + final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); + final Object id = entity.getKeyProperty().map(property -> accessor.getProperty(property)).orElseGet(() -> { + return entity.getIdProperty() != null ? accessor.getProperty(entity.getIdProperty()) : null; + }); + if (id != null && (!(value instanceof Persistable) || !Persistable.class.cast(value).isNew())) { + switch (strategy) { + case UPDATE: + update(id.toString(), value); + break; + case REPLACE: + default: + replace(id.toString(), value); + break; + } + return; + } + insert(value); + } + + @SuppressWarnings("unchecked") + @Override + public void upsert(final Iterable value, final UpsertStrategy strategy) throws DataAccessException { + final Optional first = StreamSupport.stream(value.spliterator(), false).findFirst(); + if (!first.isPresent()) { + return; + } + final Class entityClass = (Class) first.get().getClass(); + final ArangoPersistentEntity entity = getConverter().getMappingContext().getPersistentEntity(entityClass); + final ArangoPersistentProperty idProperty = entity.getIdProperty(); + final Optional keyProperty = entity.getKeyProperty(); + + final Collection withId = new ArrayList<>(); + final Collection withoutId = new ArrayList<>(); + for (final T e : value) { + if (keyProperty.isPresent() || idProperty != null) { + final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(e); + final Object id = keyProperty.map(property -> accessor.getProperty(property)).orElseGet(() -> { + return idProperty != null ? accessor.getProperty(entity.getIdProperty()) : null; + }); + if (id != null && (!(e instanceof Persistable) || !Persistable.class.cast(e).isNew())) { + withId.add(e); + continue; + } + } + withoutId.add(e); + } + if (!withoutId.isEmpty()) { + insert(withoutId, entityClass); + } + if (!withId.isEmpty()) { + switch (strategy) { + case UPDATE: + update(withId, entityClass); + break; + case REPLACE: + default: + replace(withId, entityClass); + break; + } + } + } + + private void updateDBFields(final Iterable values, final MultiDocumentEntity res) { + final Iterator valueIterator = values.iterator(); + if (res.getErrors().isEmpty()) { + final Iterator documentIterator = res.getDocuments().iterator(); + for (; valueIterator.hasNext() && documentIterator.hasNext();) { + updateDBFields(valueIterator.next(), documentIterator.next()); + } + } else { + final Iterator documentIterator = res.getDocumentsAndErrors().iterator(); + for (; valueIterator.hasNext() && documentIterator.hasNext();) { + final Object nextDoc = documentIterator.next(); + final Object nextValue = valueIterator.next(); + if (nextDoc instanceof DocumentEntity) { + updateDBFields(nextValue, (DocumentEntity) nextDoc); + } + } + } + } + + @Override + public void repsert(final T value) throws DataAccessException { + insert(value, new DocumentCreateOptions().overwrite(true)); + } + + @Override + public void repsert(final Iterable value, final Class entityClass) throws DataAccessException { + insert(value, entityClass, new DocumentCreateOptions().overwrite(true)); + } + + private void updateDBFields(final Object value, final DocumentEntity documentEntity) { + final ArangoPersistentEntity entity = converter.getMappingContext().getPersistentEntity(value.getClass()); + final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); + final ArangoPersistentProperty idProperty = entity.getIdProperty(); + if (idProperty != null) { + accessor.setProperty(idProperty, documentEntity.getId()); + } + entity.getKeyProperty().ifPresent(key -> accessor.setProperty(key, documentEntity.getKey())); + entity.getRevProperty().ifPresent(rev -> accessor.setProperty(rev, documentEntity.getRev())); + } + + @Override + public boolean exists(final String id, final Class entityClass) throws DataAccessException { + try { + return _collection(entityClass).documentExists(determineDocumentKeyFromId(id)); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public void dropDatabase() throws DataAccessException { + // guard against NPE because another thread could also call dropDatabase() + ArangoDatabase db = database; + if (db == null) { + db = arango.db(databaseName); + } + try { + db.drop(); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + database = null; + collectionCache.clear(); + } + + @Override + public CollectionOperations collection(final Class entityClass) throws DataAccessException { + return collection(_collection(entityClass)); + } + + @Override + public CollectionOperations collection(final String name) throws DataAccessException { + return collection(_collection(name)); + } + + @Override + public CollectionOperations collection(final String name, final CollectionCreateOptions options) + throws DataAccessException { + return collection(_collection(name, null, options)); + } + + private CollectionOperations collection(final ArangoCollection collection) { + return new DefaultCollectionOperations(collection, collectionCache, exceptionTranslator); + } + + @Override + public UserOperations user(final String username) { + return new DefaultUserOperation(db(), username, exceptionTranslator, this); + } + + @Override + public Iterable getUsers() throws DataAccessException { + try { + return arango.getUsers(); + } catch (final ArangoDBException e) { + throw translateExceptionIfPossible(e); + } + } + + @Override + public ArangoConverter getConverter() { + return this.converter; + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/util/AqlUtils.java b/src/main/java/com/arangodb/springframework/core/util/AqlUtils.java new file mode 100644 index 000000000..8d32b7a8e --- /dev/null +++ b/src/main/java/com/arangodb/springframework/core/util/AqlUtils.java @@ -0,0 +1,196 @@ +/* + * 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.util; + +import java.util.StringJoiner; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.util.StringUtils; + +/** + * + * @author Christian Lechner + */ +public final class AqlUtils { + + private AqlUtils() { + + } + + public static String buildLimitClause(final Pageable pageable) { + if (pageable == null) { + return ""; + } + + final StringJoiner clause = new StringJoiner(", ", "LIMIT ", ""); + clause.add(String.valueOf(pageable.getOffset())); + clause.add(String.valueOf(pageable.getPageSize())); + return clause.toString(); + } + + public static String buildPageableClause(final Pageable pageable) { + return buildPageableClause(pageable, null); + } + + public static String buildPageableClause(final Pageable pageable, final String varName) { + return buildPageableClause(pageable, varName, new StringBuilder()).toString(); + } + + private static StringBuilder buildPageableClause( + final Pageable pageable, + final String varName, + final StringBuilder clause) { + + if (pageable == null) { + return clause; + } + + final Sort sort = pageable.getSort(); + buildSortClause(sort, varName, clause); + + if (sort != null) { + clause.append(' '); + } + + clause.append("LIMIT ").append(pageable.getOffset()).append(", ").append(pageable.getPageSize()); + return clause; + } + + public static String buildSortClause(final Sort sort) { + return buildSortClause(sort, null); + } + + public static String buildSortClause(final Sort sort, final String varName) { + return buildSortClause(sort, varName, new StringBuilder()).toString(); + } + + private static StringBuilder buildSortClause(final Sort sort, final String varName, final StringBuilder clause) { + + if (sort == null) { + return clause; + } + + final String prefix = StringUtils.hasText(varName) ? escapeSortProperty(varName) : null; + clause.append("SORT "); + boolean first = true; + + for (final Sort.Order order : sort) { + if (!first) { + clause.append(", "); + } else { + first = false; + } + + if (prefix != null) { + clause.append(prefix).append('.'); + } + final String escapedProperty = escapeSortProperty(order.getProperty()); + clause.append(escapedProperty).append(' ').append(order.getDirection()); + } + return clause; + + } + + private static String escapeSortProperty(final String str) { + // dots are not allowed at start/end + if (str.charAt(0) == '.' || str.charAt(str.length() - 1) == '.') { + throw new IllegalArgumentException("Sort properties must not begin or end with a dot!"); + } + + final StringBuilder escaped = new StringBuilder(); + escaped.append('`'); + + // keep track if we are inside an escaped sequence + boolean inEscapedSeq = false; + + for (int i = 0; i < str.length(); ++i) { + final char currChar = str.charAt(i); + final boolean hasNext = (i + 1) < str.length(); + final char nextChar = hasNext ? str.charAt(i + 1) : '\0'; + + if (currChar == '\\') { + // keep escaped backticks + if (nextChar == '`') { + escaped.append("\\`"); + ++i; + } + // escape backslashes + else { + escaped.append("\\\\"); + } + } + + // current char is an unescaped backtick + else if (currChar == '`') { + inEscapedSeq = !inEscapedSeq; + + final boolean isStartOrEnd = i == 0 || !hasNext; + final boolean isNextCharDotOutsideEscapedSeq = nextChar == '.' && !inEscapedSeq; + + // unescaped backticks are only allowed at start/end of attributes + if (!isStartOrEnd && !isNextCharDotOutsideEscapedSeq) { + throw new IllegalArgumentException( + "Sort properties must only contain backticks at beginning/end of attributes or when escaped."); + } + } + + else if (currChar == '.') { + // the dot is part of an attribute name when inside escaped sequence + if (inEscapedSeq) { + // add dot without escaping + escaped.append('.'); + } + + else { + // properties can only contain 2+ dots in escaped sequences + if (nextChar == '.') { + throw new IllegalArgumentException( + "Sort properties may not contain 2+ consecutive dots when outside a backtick escape sequence!"); + } + // consume optional backtick + else if (nextChar == '`') { + inEscapedSeq = !inEscapedSeq; + ++i; + } + + // close previous escape sequence and open new one + escaped.append("`.`"); + } + } + + // keep others + else { + escaped.append(currChar); + } + } + + // check for an open escape sequence + if (inEscapedSeq) { + throw new IllegalArgumentException( + "A sort property contains an unclosed backtick escape sequence! The cause may be a missing backtick."); + } + + escaped.append('`'); + return escaped.toString(); + } + +} diff --git a/src/main/java/com/arangodb/springframework/core/util/ArangoErrors.java b/src/main/java/com/arangodb/springframework/core/util/ArangoErrors.java index 196a63b14..fb105d9a7 100644 --- a/src/main/java/com/arangodb/springframework/core/util/ArangoErrors.java +++ b/src/main/java/com/arangodb/springframework/core/util/ArangoErrors.java @@ -22,6 +22,7 @@ /** * @author Mark Vollmary + * @author Christian Lechner * */ public class ArangoErrors { @@ -51,6 +52,11 @@ public class ArangoErrors { */ public static final int ERROR_HTTP_METHOD_NOT_ALLOWED = 405; + /** + * conflict. Will be raised when a conflict is encountered. + */ + public static final int ERROR_HTTP_CONFLICT = 409; + /** * precondition failed. Will be raised when a precondition for an HTTP request is not met. */ diff --git a/src/main/java/com/arangodb/springframework/core/util/ArangoExceptionTranslator.java b/src/main/java/com/arangodb/springframework/core/util/ArangoExceptionTranslator.java index 85f466363..67247abe8 100644 --- a/src/main/java/com/arangodb/springframework/core/util/ArangoExceptionTranslator.java +++ b/src/main/java/com/arangodb/springframework/core/util/ArangoExceptionTranslator.java @@ -22,6 +22,7 @@ import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.PermissionDeniedDataAccessException; @@ -32,6 +33,7 @@ /** * @author Mark Vollmary + * @author Christian Lechner * */ public class ArangoExceptionTranslator implements PersistenceExceptionTranslator { @@ -57,6 +59,9 @@ public DataAccessException translateExceptionIfPossible(final RuntimeException e case ArangoErrors.ERROR_HTTP_NOT_FOUND: dae = new InvalidDataAccessResourceUsageException(exception.getMessage(), exception); break; + case ArangoErrors.ERROR_HTTP_CONFLICT: + dae = new DataIntegrityViolationException(exception.getMessage(), exception); + break; case ArangoErrors.ERROR_HTTP_PRECONDITION_FAILED: case ArangoErrors.ERROR_HTTP_SERVICE_UNAVAILABLE: dae = new DataAccessResourceFailureException(exception.getMessage(), exception); diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoEntityInformation.java b/src/main/java/com/arangodb/springframework/repository/ArangoEntityInformation.java new file mode 100644 index 000000000..3e07489d9 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/ArangoEntityInformation.java @@ -0,0 +1,35 @@ +/* + * 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.io.Serializable; + +import org.springframework.data.repository.core.EntityInformation; + +/** + * + * @author Christian Lechner + */ +public interface ArangoEntityInformation extends EntityInformation { + + String getCollection(); + +} \ No newline at end of file diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoExampleConverter.java b/src/main/java/com/arangodb/springframework/repository/ArangoExampleConverter.java index fcaa8c23f..0faa048b5 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoExampleConverter.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoExampleConverter.java @@ -114,6 +114,9 @@ private void addPredicate( case ENDING: value = escape(string) + "$"; break; + case CONTAINING: + value = escape(string); + break; case DEFAULT: case EXACT: default: diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoPersistentEntityInformation.java b/src/main/java/com/arangodb/springframework/repository/ArangoPersistentEntityInformation.java new file mode 100644 index 000000000..ebe96d1c1 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/ArangoPersistentEntityInformation.java @@ -0,0 +1,48 @@ +/* + * 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 org.springframework.data.repository.core.support.PersistentEntityInformation; + +import java.io.Serializable; + +import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; + +/** + * + * @author Christian Lechner + */ +public class ArangoPersistentEntityInformation extends PersistentEntityInformation + implements ArangoEntityInformation { + + private final ArangoPersistentEntity persistentEntity; + + public ArangoPersistentEntityInformation(final ArangoPersistentEntity entity) { + super(entity); + this.persistentEntity = entity; + } + + @Override + public String getCollection() { + return persistentEntity.getCollection(); + } + +} diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryConfigurationExtension.java b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryConfigurationExtension.java index 7cc9b59d8..88d28c00e 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryConfigurationExtension.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryConfigurationExtension.java @@ -20,19 +20,48 @@ package com.arangodb.springframework.repository; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import com.arangodb.springframework.annotation.Document; +import com.arangodb.springframework.annotation.Edge; + /** - * Created by F625633 on 07/07/2017. + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner */ public class ArangoRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { + + @Override + public String getModuleName() { + return "ArangoDB"; + } + @Override protected String getModulePrefix() { - return null; + return "arango"; } @Override public String getRepositoryFactoryClassName() { return ArangoRepositoryFactoryBean.class.getName(); } + + @Override + protected Collection> getIdentifyingAnnotations() { + return Arrays.asList(Document.class, Edge.class); + } + + @Override + protected Collection> getIdentifyingTypes() { + return Collections.singleton(ArangoRepository.class); + } + } diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java index d22673ba3..2781dc509 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactory.java @@ -21,31 +21,55 @@ package com.arangodb.springframework.repository; import java.io.Serializable; +import java.lang.reflect.Method; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import com.arangodb.ArangoCursor; import com.arangodb.springframework.core.ArangoOperations; -import com.arangodb.springframework.repository.query.ArangoQueryLookupStrategy; +import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; +import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; +import com.arangodb.springframework.repository.query.ArangoQueryMethod; +import com.arangodb.springframework.repository.query.DerivedArangoQuery; +import com.arangodb.springframework.repository.query.StringBasedArangoQuery; /** - * Created by F625633 on 06/07/2017. + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner */ public class ArangoRepositoryFactory extends RepositoryFactorySupport { private final ArangoOperations arangoOperations; + private final MappingContext, ArangoPersistentProperty> context; public ArangoRepositoryFactory(final ArangoOperations arangoOperations) { this.arangoOperations = arangoOperations; + this.context = arangoOperations.getConverter().getMappingContext(); } + @SuppressWarnings("unchecked") @Override public EntityInformation getEntityInformation(final Class domainClass) { - return null; + return new ArangoPersistentEntityInformation( + (ArangoPersistentEntity) context.getPersistentEntity(domainClass)); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -60,10 +84,96 @@ protected Class getRepositoryBaseClass(final RepositoryMetadata metadata) { } @Override - protected QueryLookupStrategy getQueryLookupStrategy( - final QueryLookupStrategy.Key key, - final EvaluationContextProvider evaluationContextProvider) { - return new ArangoQueryLookupStrategy(arangoOperations); + protected RepositoryMetadata getRepositoryMetadata(Class repositoryInterface) { + Assert.notNull(repositoryInterface, "Repository interface must not be null!"); + + return Repository.class.isAssignableFrom(repositoryInterface) + ? new DefaultArangoRepositoryMetadata(repositoryInterface) + : new AnnotationArangoRepositoryMetadata(repositoryInterface); + } + + @Override + protected QueryLookupStrategy getQueryLookupStrategy(final QueryLookupStrategy.Key key, + final EvaluationContextProvider evaluationContextProvider) { + + QueryLookupStrategy strategy = null; + switch (key) { + case CREATE_IF_NOT_FOUND: + strategy = new DefaultArangoQueryLookupStrategy(arangoOperations); + break; + case CREATE: + break; + case USE_DECLARED_QUERY: + break; + } + return strategy; + } + + static class DefaultArangoQueryLookupStrategy implements QueryLookupStrategy { + + private final ArangoOperations operations; + + public DefaultArangoQueryLookupStrategy(final ArangoOperations operations) { + this.operations = operations; + } + + @Override + public RepositoryQuery resolveQuery(final Method method, final RepositoryMetadata metadata, + final ProjectionFactory factory, final NamedQueries namedQueries) { + + final ArangoQueryMethod queryMethod = new ArangoQueryMethod(method, metadata, factory); + final String namedQueryName = queryMethod.getNamedQueryName(); + + if (namedQueries.hasQuery(namedQueryName)) { + final String namedQuery = namedQueries.getQuery(namedQueryName); + return new StringBasedArangoQuery(namedQuery, queryMethod, operations); + } else if (queryMethod.hasAnnotatedQuery()) { + return new StringBasedArangoQuery(queryMethod, operations); + } else { + return new DerivedArangoQuery(queryMethod, operations); + } + } + + } + + static class DefaultArangoRepositoryMetadata extends DefaultRepositoryMetadata { + + private final TypeInformation typeInformation; + + public DefaultArangoRepositoryMetadata(Class repositoryInterface) { + super(repositoryInterface); + typeInformation = ClassTypeInformation.from(repositoryInterface); + } + + @Override + public Class getReturnedDomainClass(Method method) { + if (ArangoCursor.class.isAssignableFrom(method.getReturnType())) { + return typeInformation.getReturnType(method).getComponentType().getType(); + } else { + return super.getReturnedDomainClass(method); + } + } + + } + + static class AnnotationArangoRepositoryMetadata extends AnnotationRepositoryMetadata { + + private final TypeInformation typeInformation; + + public AnnotationArangoRepositoryMetadata(Class repositoryInterface) { + super(repositoryInterface); + typeInformation = ClassTypeInformation.from(repositoryInterface); + } + + @Override + public Class getReturnedDomainClass(Method method) { + if (ArangoCursor.class.isAssignableFrom(method.getReturnType())) { + return typeInformation.getReturnType(method).getComponentType().getType(); + } else { + return super.getReturnedDomainClass(method); + } + } + } } diff --git a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java index 77349ac00..962aa14c9 100644 --- a/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java +++ b/src/main/java/com/arangodb/springframework/repository/ArangoRepositoryFactoryBean.java @@ -20,6 +20,7 @@ package com.arangodb.springframework.repository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -32,11 +33,16 @@ */ public class ArangoRepositoryFactoryBean, S> extends RepositoryFactoryBeanSupport { - private final ArangoOperations arangoOperations; - public ArangoRepositoryFactoryBean(final Class repositoryInterface, - final ArangoOperations arangoOperations) { + private ArangoOperations arangoOperations; + + @Autowired + public ArangoRepositoryFactoryBean(final Class repositoryInterface) { super(repositoryInterface); + } + + @Autowired + public void setArangoOperations(final ArangoOperations arangoOperations) { this.arangoOperations = arangoOperations; } diff --git a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java index 809a0d467..5708fa11b 100644 --- a/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java +++ b/src/main/java/com/arangodb/springframework/repository/SimpleArangoRepository.java @@ -24,6 +24,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.stream.StreamSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,11 +40,12 @@ import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.core.ArangoOperations.UpsertStrategy; import com.arangodb.springframework.core.mapping.ArangoMappingContext; -import com.arangodb.springframework.repository.query.derived.DerivedQueryCreator; +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" }) @@ -57,10 +59,9 @@ 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(); @@ -73,36 +74,43 @@ public SimpleArangoRepository(final ArangoOperations arangoOperations, final Cla /** * 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 */ - // TODO refactor once template.upsert() is implemented + @SuppressWarnings("deprecation") @Override public S save(final S entity) { - arangoOperations.upsert(entity, UpsertStrategy.UPDATE); + if (arangoOperations.getVersion().getVersion().compareTo("3.4.0") < 0) { + arangoOperations.upsert(entity, UpsertStrategy.UPDATE); + } else { + arangoOperations.repsert(entity); + } return 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 */ - // TODO refactor once template.upsert() is implemented + @SuppressWarnings("deprecation") @Override public Iterable save(final Iterable entities) { - arangoOperations.upsert(entities, UpsertStrategy.UPDATE); + if (arangoOperations.getVersion().getVersion().compareTo("3.4.0") < 0) { + arangoOperations.upsert(entities, UpsertStrategy.UPDATE); + } else { + final S first = StreamSupport.stream(entities.spliterator(), false).findFirst().get(); + arangoOperations.repsert(entities, (Class) first.getClass()); + } return 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 @@ -113,8 +121,7 @@ public T findOne(final String id) { /** * Checks if a document exists or not based on the given id or key * - * @param s - * represents either the key or id of a document to check for + * @param s represents either the key or id of a document to check for * @return returns true if the document is found, false otherwise */ @Override @@ -135,9 +142,9 @@ public Iterable findAll() { /** * Finds all documents with the an id or key in the argument * - * @param strings - * 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 strings 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 findAll(final Iterable strings) { @@ -145,7 +152,8 @@ public Iterable findAll(final Iterable strings) { } /** - * 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 */ @@ -157,8 +165,7 @@ public long count() { /** * Deletes the document with the given id or key * - * @param s - * id or key of document to be deleted + * @param s id or key of document to be deleted */ @Override public void delete(final String s) { @@ -166,17 +173,17 @@ public void delete(final String s) { } /** - * 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) { String id = null; try { - id = (String) arangoOperations.getConverter().getMappingContext().getPersistentEntity(domainClass) - .getIdProperty().getField().get(entity); + id = (String) arangoOperations.getConverter().getMappingContext().getPersistentEntity(domainClass).getIdProperty() + .getField().get(entity); } catch (final IllegalAccessException e) { e.printStackTrace(); } @@ -186,8 +193,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 */ // TODO refactor if this can be done through one database call @Override @@ -204,28 +210,27 @@ 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 public Iterable findAll(final Sort sort) { - final String sortString = DerivedQueryCreator.buildSortString(sort); return new Iterable() { @Override public Iterator iterator() { - return execute("", sortString, "", new HashMap<>()); + return findAllInternal(sort, null, new HashMap<>()); } }; } /** - * 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 @@ -233,16 +238,15 @@ public Page findAll(final Pageable pageable) { if (pageable == null) { LOGGER.debug("Pageable in findAll(Pageable) is null"); } - final String sort = DerivedQueryCreator.buildSortString(pageable.getSort()); - final String limit = String.format(" LIMIT %d, %d", pageable.getOffset(), pageable.getPageSize()); - final ArangoCursor result = execute("", sort, limit, new HashMap<>()); + + final ArangoCursor result = findAllInternal(pageable, null, new HashMap<>()); final List content = result.asListRemaining(); return new PageImpl<>(content, pageable, result.getStats().getFullCount()); } /** * Gets the name of the collection for this repository - * + * * @return the name of the collection */ private String getCollectionName() { @@ -252,85 +256,65 @@ private String getCollectionName() { /** * Finds one document which matches the given example object * - * @param example - * example object to construct query with - * @param + * @param example example object to construct query with + * @param * @return An object representing the example if it exists, else null */ @Override public S findOne(final Example example) { - final Map bindVars = new HashMap<>(); - final String predicate = exampleConverter.convertExampleToPredicate(example, bindVars); - final String filter = predicate.length() == 0 ? "" : " FILTER " + predicate; - final ArangoCursor cursor = execute(filter, "", "", bindVars); + final ArangoCursor cursor = findAllInternal((Pageable) null, example, new HashMap()); return cursor.hasNext() ? (S) cursor.next() : null; } /** * Finds all documents which match with the given example * - * @param example - * example object to construct query with - * @param + * @param example example object to construct query with + * @param * @return iterable of all matching documents */ @Override public Iterable findAll(final Example example) { - final Map bindVars = new HashMap<>(); - final String predicate = exampleConverter.convertExampleToPredicate(example, bindVars); - final String filter = predicate.length() == 0 ? "" : " FILTER " + predicate; - final ArangoCursor cursor = execute(filter, "", "", bindVars); + final ArangoCursor cursor = findAllInternal((Pageable) null, example, new HashMap<>()); return cursor; } /** - * 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 + * @param example example object to construct query with + * @param sort sort object to sort results + * @param * @return sorted iterable of all matching documents */ @Override public Iterable findAll(final Example example, final Sort sort) { - final Map bindVars = new HashMap<>(); - final String predicate = exampleConverter.convertExampleToPredicate(example, bindVars); - final String filter = predicate.length() == 0 ? "" : " FILTER " + predicate; - final String sortString = DerivedQueryCreator.buildSortString(sort); - final ArangoCursor cursor = execute(filter, sortString, "", bindVars); + final ArangoCursor cursor = findAllInternal(sort, example, new HashMap()); return cursor; } /** * 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 + * @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 */ @Override public Page findAll(final Example example, final Pageable pageable) { - final Map bindVars = new HashMap<>(); - final String predicate = exampleConverter.convertExampleToPredicate(example, bindVars); - final String filter = predicate.length() == 0 ? "" : " FILTER " + predicate; - final String sortString = DerivedQueryCreator.buildSortString(pageable.getSort()); - final String limit = String.format(" LIMIT %d, %d", pageable.getOffset(), pageable.getPageSize()); - final ArangoCursor cursor = execute(filter, sortString, limit, bindVars); + final ArangoCursor cursor = findAllInternal(pageable, example, new HashMap()); final List content = cursor.asListRemaining(); return new PageImpl<>((List) content, pageable, cursor.getStats().getFullCount()); } /** - * Counts the number of documents in the collection which match with the given example - * - * @param example - * example object to construct query with - * @param + * Counts the number of documents in the collection which match with the given + * example + * + * @param example example object to construct query with + * @param * @return number of matching documents found */ @Override @@ -339,16 +323,16 @@ 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); final ArangoCursor cursor = arangoOperations.query(query, bindVars, null, Long.class); return cursor.next(); } /** * Checks if any documents match with the given example - * + * * @param example - * @param + * @param * @return true if any matches are found, else false */ @Override @@ -356,26 +340,38 @@ public boolean exists(final Example example) { return count(example) > 0; } - /** - * Execute a query to the database - * - * @param filter - * filter statement to be put in query - * @param sort - * any sort to be applied - * @param limit - * a limit if one exists - * @param bindVars - * bindVars for the query being executed - * @return ArangoCursor with the results of the executed query - */ - private ArangoCursor execute( - final String filter, - final String sort, - final String limit, - final Map bindVars) { - final String query = String.format("FOR e IN %s%s%s%s RETURN e", getCollectionName(), filter, sort, limit); - return arangoOperations.query(query, bindVars, - limit.length() == 0 ? null : new AqlQueryOptions().fullCount(true), domainClass); + private ArangoCursor findAllInternal(final Sort sort, 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")); + return arangoOperations.query(query, bindVars, null, domainClass); + } + + private ArangoCursor findAllInternal(final Pageable pageable, 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")); + + return arangoOperations.query(query, bindVars, pageable != null ? new AqlQueryOptions().fullCount(true) : null, + domainClass); + } + + private String buildFilterClause(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); } } diff --git a/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java new file mode 100644 index 000000000..c4f912b7f --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/query/AbstractArangoQuery.java @@ -0,0 +1,180 @@ +/* + * 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.query; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.util.Assert; + +import com.arangodb.ArangoCursor; +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.core.ArangoOperations; + +/** + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner + */ +public abstract class AbstractArangoQuery implements RepositoryQuery { + + protected final ArangoQueryMethod method; + protected final ArangoOperations operations; + protected final Class domainClass; + + public AbstractArangoQuery(final ArangoQueryMethod method, final ArangoOperations operations) { + Assert.notNull(method, "ArangoQueryMethod must not be null!"); + Assert.notNull(operations, "ArangoOperations must not be null!"); + this.method = method; + this.operations = operations; + this.domainClass = method.getEntityInformation().getJavaType(); + } + + @Override + public Object execute(final Object[] parameters) { + final ArangoParameterAccessor accessor = new ArangoParametersParameterAccessor(method, parameters); + final Map bindVars = new HashMap<>(); + + AqlQueryOptions options = mergeQueryOptions(method.getAnnotatedQueryOptions(), accessor.getQueryOptions()); + if (options == null) { + options = new AqlQueryOptions(); + } + + if (method.isPageQuery()) { + options.fullCount(true); + } + + final String query = createQuery(accessor, bindVars, options); + + final ResultProcessor processor = method.getResultProcessor().withDynamicProjection(accessor); + final Class typeToRead = getTypeToRead(processor); + + final ArangoCursor result = operations.query(query, bindVars, options, typeToRead); + return processor.processResult(convertResult(result, accessor)); + } + + @Override + public ArangoQueryMethod getQueryMethod() { + return method; + } + + /** + * Implementations should create an AQL query with the given + * {@link com.arangodb.springframework.repository.query.ArangoParameterAccessor} and set necessary binding + * parameters and query options. + * + * @param accessor + * provides access to the actual arguments + * @param bindVars + * the binding parameter map + * @param options + * contains the merged {@link com.arangodb.model.AqlQueryOptions} + * @return the created AQL query + */ + protected abstract String createQuery( + ArangoParameterAccessor accessor, + Map bindVars, + AqlQueryOptions options); + + protected abstract boolean isCountQuery(); + + protected abstract boolean isExistsQuery(); + + /** + * Merges AqlQueryOptions derived from @QueryOptions with dynamically passed AqlQueryOptions which takes priority + * + * @param oldStatic + * @param newDynamic + * @return + */ + protected AqlQueryOptions mergeQueryOptions(final AqlQueryOptions oldStatic, final AqlQueryOptions newDynamic) { + if (oldStatic == null) { + return newDynamic; + } + if (newDynamic == null) { + return oldStatic; + } + final Integer batchSize = newDynamic.getBatchSize(); + if (batchSize != null) { + oldStatic.batchSize(batchSize); + } + final Integer maxPlans = newDynamic.getMaxPlans(); + if (maxPlans != null) { + oldStatic.maxPlans(maxPlans); + } + final Integer ttl = newDynamic.getTtl(); + if (ttl != null) { + oldStatic.ttl(ttl); + } + final Boolean cache = newDynamic.getCache(); + if (cache != null) { + oldStatic.cache(cache); + } + final Boolean count = newDynamic.getCount(); + if (count != null) { + oldStatic.count(count); + } + final Boolean fullCount = newDynamic.getFullCount(); + if (fullCount != null) { + oldStatic.fullCount(fullCount); + } + final Boolean profile = newDynamic.getProfile(); + if (profile != null) { + oldStatic.profile(profile); + } + final Collection rules = newDynamic.getRules(); + if (rules != null) { + oldStatic.rules(rules); + } + return oldStatic; + } + + private Class getTypeToRead(final ResultProcessor processor) { + if (isExistsQuery()) { + return Integer.class; + } + + if (method.isGeoQuery()) { + return Map.class; + } + + final Class typeToRead = processor.getReturnedType().getTypeToRead(); + return typeToRead != null ? typeToRead : Map.class; + } + + private Object convertResult(final ArangoCursor result, final ArangoParameterAccessor accessor) { + if (isExistsQuery()) { + if (!result.hasNext()) { + return false; + } + return (Integer) result.next() > 0; + } + final ArangoResultConverter resultConverter = new ArangoResultConverter(accessor, result, operations, + domainClass); + return resultConverter.convertResult(method.getReturnType().getType()); + } + +} diff --git a/src/main/java/com/arangodb/springframework/repository/query/ArangoAqlQuery.java b/src/main/java/com/arangodb/springframework/repository/query/ArangoAqlQuery.java deleted file mode 100644 index 9259b4889..000000000 --- a/src/main/java/com/arangodb/springframework/repository/query/ArangoAqlQuery.java +++ /dev/null @@ -1,370 +0,0 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.query; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.Page; -import org.springframework.data.geo.GeoPage; -import org.springframework.data.geo.GeoResult; -import org.springframework.data.geo.GeoResults; -import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.RepositoryQuery; -import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.util.Assert; - -import com.arangodb.ArangoCursor; -import com.arangodb.entity.BaseDocument; -import com.arangodb.entity.BaseEdgeDocument; -import com.arangodb.entity.IndexType; -import com.arangodb.model.AqlQueryOptions; -import com.arangodb.springframework.annotation.BindVars; -import com.arangodb.springframework.annotation.Param; -import com.arangodb.springframework.annotation.Query; -import com.arangodb.springframework.annotation.QueryOptions; -import com.arangodb.springframework.core.ArangoOperations; -import com.arangodb.springframework.core.mapping.ArangoMappingContext; -import com.arangodb.springframework.repository.query.derived.DerivedQueryCreator; - -/** - * Implements execute(Object[]) method which is called every time a user-defined AQL or derived method is called - */ -public class ArangoAqlQuery implements RepositoryQuery { - - private static final Set> GEO_RETURN_TYPES = new HashSet<>(); - private static final Set> DESERIALIZABLE_TYPES = new HashSet<>(); - - static { - GEO_RETURN_TYPES.add(GeoResult.class); - GEO_RETURN_TYPES.add(GeoResults.class); - GEO_RETURN_TYPES.add(GeoPage.class); - - DESERIALIZABLE_TYPES.add(Map.class); - DESERIALIZABLE_TYPES.add(BaseDocument.class); - DESERIALIZABLE_TYPES.add(BaseEdgeDocument.class); - } - - private static final Logger LOGGER = LoggerFactory.getLogger(ArangoAqlQuery.class); - - private final ArangoOperations operations; - private final Class domainClass; - private final Method method; - private final RepositoryMetadata metadata; - private ArangoParameterAccessor accessor; - private boolean isCountProjection = false; - private boolean isExistsProjection = false; - private final ProjectionFactory factory; - - public ArangoAqlQuery(final Class domainClass, final Method method, final RepositoryMetadata metadata, - final ArangoOperations operations, final ProjectionFactory factory) { - this.domainClass = domainClass; - this.method = method; - this.metadata = metadata; - this.operations = operations; - this.factory = factory; - } - - @Override - public QueryMethod getQueryMethod() { - return new ArangoQueryMethod(method, metadata, factory); - } - - /** - * This method contains main logic showing how all user-defined methods are implemented - * - * @param arguments - * @return - */ - @SuppressWarnings("unchecked") - @Override - public Object execute(final Object[] arguments) { - Map bindVars = new HashMap<>(); - String query = getQueryAnnotationValue(); - AqlQueryOptions options = getAqlQueryOptions(); - boolean optionsFound = false; - if (query == null) { // derived method - final PartTree tree = new PartTree(method.getName(), domainClass); - isCountProjection = tree.isCountProjection(); - isExistsProjection = tree.isExistsProjection(); - accessor = new ArangoParameterAccessor(new ArangoParameters(method), arguments); - options = updateAqlQueryOptions(options, accessor.getAqlQueryOptions()); - if (Page.class.isAssignableFrom(method.getReturnType())) { - if (options == null) { - options = new AqlQueryOptions().fullCount(true); - } else { - options = options.fullCount(true); - } - } - final List geoFields = new LinkedList<>(); - if (GEO_RETURN_TYPES.contains(method.getReturnType())) { - operations.collection( - operations.getConverter().getMappingContext().getPersistentEntity(domainClass).getCollection()) - .getIndexes().forEach(i -> { - if ((i.getType() == IndexType.geo1) && geoFields.isEmpty()) { - i.getFields().forEach(f -> geoFields.add(f)); - } - }); - } - query = new DerivedQueryCreator((ArangoMappingContext) operations.getConverter().getMappingContext(), - domainClass, tree, accessor, bindVars, geoFields, - operations.getVersion().getVersion().compareTo("3.2.0") < 0).createQuery(); - } else if (arguments != null) { // AQL query method - final Set bindings = getBindings(query); - final Annotation[][] annotations = method.getParameterAnnotations(); - Assert.isTrue(arguments.length == annotations.length, "arguments.length != annotations.length"); - final Map bindVarsLocal = new HashMap<>(); - boolean bindVarsFound = false; - for (int i = 0; i < arguments.length; ++i) { - if (arguments[i] instanceof AqlQueryOptions) { - Assert.isTrue(!optionsFound, "AqlQueryOptions are already set"); - optionsFound = true; - options = updateAqlQueryOptions(options, (AqlQueryOptions) arguments[i]); - continue; - } - String parameter = null; - final Annotation specialAnnotation = getSpecialAnnotation(annotations[i]); - if (specialAnnotation != null) { - if (specialAnnotation.annotationType() == Param.class) { - parameter = ((Param) specialAnnotation).value(); - } else if (specialAnnotation.annotationType() == BindVars.class) { - Assert.isTrue(arguments[i] instanceof Map, "@BindVars must be a Map"); - Assert.isTrue(!bindVarsFound, "@BindVars duplicated"); - bindVars = (Map) arguments[i]; - bindVarsFound = true; - continue; - } - } - if (parameter == null) { - final String key = String.format("%d", i); - if (bindings.contains(key)) { - Assert.isTrue(!bindVarsLocal.containsKey(key), "duplicate parameter name"); - bindVarsLocal.put(key, arguments[i]); - } else if (bindings.contains("@" + key)) { - Assert.isTrue(!bindVarsLocal.containsKey("@" + key), "duplicate parameter name"); - bindVarsLocal.put("@" + key, arguments[i]); - } else { - LOGGER.debug("Local parameter '@{}' is not used in the query", key); - } - } else { - Assert.isTrue(!bindVarsLocal.containsKey(parameter), "duplicate parameter name"); - bindVarsLocal.put(parameter, arguments[i]); - } - } - mergeBindVars(bindVars, bindVarsLocal); - } - return convertResult(operations.query(query, bindVars, options, getResultClass())); - } - - /** - * Merges AqlQueryOptions derived from @QueryOptions with dynamically passed AqlQueryOptions which takes priority - * - * @param oldStatic - * @param newDynamic - * @return - */ - private AqlQueryOptions updateAqlQueryOptions(final AqlQueryOptions oldStatic, final AqlQueryOptions newDynamic) { - if (oldStatic == null) { - return newDynamic; - } - final Integer batchSize = newDynamic.getBatchSize(); - if (batchSize != null) { - oldStatic.batchSize(batchSize); - } - final Integer maxPlans = newDynamic.getMaxPlans(); - if (maxPlans != null) { - oldStatic.maxPlans(maxPlans); - } - final Integer ttl = newDynamic.getTtl(); - if (ttl != null) { - oldStatic.ttl(ttl); - } - final Boolean cache = newDynamic.getCache(); - if (cache != null) { - oldStatic.cache(cache); - } - final Boolean count = newDynamic.getCount(); - if (count != null) { - oldStatic.count(count); - } - final Boolean fullCount = newDynamic.getFullCount(); - if (fullCount != null) { - oldStatic.fullCount(fullCount); - } - final Boolean profile = newDynamic.getProfile(); - if (profile != null) { - oldStatic.profile(profile); - } - final Collection rules = newDynamic.getRules(); - if (rules != null) { - oldStatic.rules(rules); - } - return oldStatic; - } - - private AqlQueryOptions getAqlQueryOptions() { - final QueryOptions queryOptions = method.getAnnotation(QueryOptions.class); - if (queryOptions == null) { - return null; - } - final AqlQueryOptions options = new AqlQueryOptions(); - final int batchSize = queryOptions.batchSize(); - if (batchSize != -1) { - options.batchSize(batchSize); - } - final int maxPlans = queryOptions.maxPlans(); - if (maxPlans != -1) { - options.maxPlans(maxPlans); - } - final int ttl = queryOptions.ttl(); - if (ttl != -1) { - options.ttl(ttl); - } - options.cache(queryOptions.cache()); - options.count(queryOptions.count()); - options.fullCount(queryOptions.fullCount()); - options.profile(queryOptions.profile()); - options.rules(Arrays.asList(queryOptions.rules())); - return options; - } - - private Class getResultClass() { - if (isCountProjection || isExistsProjection) { - return Integer.class; - } - if (GEO_RETURN_TYPES.contains(method.getReturnType())) { - return Object.class; - } - if (DESERIALIZABLE_TYPES.contains(method.getReturnType())) { - return method.getReturnType(); - } - return domainClass; - } - - /** - * Returns Param or BindVars Annotation if it is present in the given array or null otherwise - * - * @param annotations - * @return - */ - private Annotation getSpecialAnnotation(final Annotation[] annotations) { - Annotation specialAnnotation = null; - for (final Annotation annotation : annotations) { - if (annotation.annotationType() == BindVars.class || annotation.annotationType() == Param.class) { - Assert.isTrue(specialAnnotation == null, "@BindVars or @Param should be used only once per parameter"); - specialAnnotation = annotation; - } - } - return specialAnnotation; - } - - private String getQueryAnnotationValue() { - final Query query = method.getAnnotation(Query.class); - return query == null ? null : query.value(); - } - - /** - * Merges bindVars Map passed by a user with a Map created from the rest of the arguments which take priority - * - * @param bindVars - * @param bindVarsLocal - */ - private void mergeBindVars(final Map bindVars, final Map bindVarsLocal) { - for (final String key : bindVarsLocal.keySet()) { - if (bindVars.containsKey(key)) { - LOGGER.debug("Local parameter '{}' overrides @BindVars Map", key); - } - bindVars.put(key, bindVarsLocal.get(key)); - } - } - - private Object convertResult(final ArangoCursor result) { - if (isExistsProjection) { - if (!result.hasNext()) { - return false; - } - return Integer.valueOf(result.next().toString()) > 0; - } - final ArangoResultConverter resultConverter = new ArangoResultConverter(accessor, result, operations, - domainClass); - return resultConverter.convertResult(method.getReturnType()); - } - - private String removeAqlStringLiterals(final String query) { - final StringBuilder fixedQuery = new StringBuilder(); - for (int i = 0; i < query.length(); ++i) { - if (query.charAt(i) == '"') { - for (++i; i < query.length(); ++i) { - if (query.charAt(i) == '"') { - ++i; - break; - } - if (query.charAt(i) == '\\') { - ++i; - } - } - } else if (query.charAt(i) == '\'') { - for (++i; i < query.length(); ++i) { - if (query.charAt(i) == '\'') { - ++i; - break; - } - if (query.charAt(i) == '\\') { - ++i; - } - } - } - fixedQuery.append(query.charAt(i)); - } - return fixedQuery.toString(); - } - - /** - * Returns all bindings used in AQL query String including bindings prefixed with both single and double '@' - * character ignoring AQL string literals - * - * @param query - * @return - */ - private Set getBindings(final String query) { - final String fixedQuery = removeAqlStringLiterals(query); - final Set bindings = new HashSet<>(); - final Matcher matcher = Pattern.compile("@\\S+").matcher(fixedQuery); - while (matcher.find()) { - bindings.add(matcher.group().substring(1)); - } - return bindings; - } -} diff --git a/src/main/java/com/arangodb/springframework/repository/query/ArangoParameterAccessor.java b/src/main/java/com/arangodb/springframework/repository/query/ArangoParameterAccessor.java index 0eac6522d..5277ad3d4 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/ArangoParameterAccessor.java +++ b/src/main/java/com/arangodb/springframework/repository/query/ArangoParameterAccessor.java @@ -1,7 +1,7 @@ /* * DISCLAIMER * - * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * 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. @@ -20,77 +20,22 @@ package com.arangodb.springframework.repository.query; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; +import java.util.Map; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.util.Assert; import com.arangodb.model.AqlQueryOptions; /** - * The main class used to access parameters of a user-defined method. It wraps ParametersParameterAccessor which catches - * special parameters Sort and Pageable, and catches Arango-specific parameters e.g. AqlQueryOptions + * + * @author Christian Lechner */ -public class ArangoParameterAccessor implements ParameterAccessor { - private final ParametersParameterAccessor accessor; - private final List bindableArguments; - private AqlQueryOptions options = null; - - public ArangoParameterAccessor(final ArangoParameters parameters, final Object[] arguments) { - accessor = new ParametersParameterAccessor(parameters, arguments); - this.bindableArguments = createBindableArguments(arguments); - } - - AqlQueryOptions getAqlQueryOptions() { - return options; - } - - @Override - public Pageable getPageable() { - return accessor.getPageable(); - } - - @Override - public Sort getSort() { - return accessor.getSort(); - } - - @Override - public Class getDynamicProjection() { - return accessor.getDynamicProjection(); - } +public interface ArangoParameterAccessor extends ParameterAccessor { - @Override - public Object getBindableValue(final int index) { - return accessor.getBindableValue(index); - } + ArangoParameters getParameters(); - @Override - public boolean hasBindableNullValue() { - return accessor.hasBindableNullValue(); - } + AqlQueryOptions getQueryOptions(); - @Override - public Iterator iterator() { - return bindableArguments.iterator(); - } + Map getBindVars(); - private List createBindableArguments(final Object[] arguments) { - final List bindableArguments = new LinkedList<>(); - for (final Parameter parameter : accessor.getParameters().getBindableParameters()) { - if (parameter.getType() == AqlQueryOptions.class) { - Assert.isTrue(options == null, "AqlQueryOptions duplicated"); - options = (AqlQueryOptions) arguments[parameter.getIndex()]; - } else { - bindableArguments.add(arguments[parameter.getIndex()]); - } - } - return bindableArguments; - } } diff --git a/src/main/java/com/arangodb/springframework/repository/query/ArangoParameters.java b/src/main/java/com/arangodb/springframework/repository/query/ArangoParameters.java index cec4bd6ed..6414bd4ec 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/ArangoParameters.java +++ b/src/main/java/com/arangodb/springframework/repository/query/ArangoParameters.java @@ -1,58 +1,221 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.query; - -import java.lang.reflect.Method; -import java.util.List; - -import org.springframework.core.MethodParameter; -import org.springframework.data.repository.query.Parameter; -import org.springframework.data.repository.query.Parameters; - -/** - * Created by F625633 on 12/07/2017. - */ -public class ArangoParameters extends Parameters { - - public ArangoParameters(final Method method) { - super(method); - } - - public ArangoParameters(final List parameters) { - super(parameters); - } - - @Override - protected ArangoParameter createParameter(final MethodParameter parameter) { - return new ArangoParameter(parameter); - } - - @Override - protected ArangoParameters createFrom(final List parameters) { - return new ArangoParameters(parameters); - } - - protected static class ArangoParameter extends Parameter { - public ArangoParameter(final MethodParameter parameter) { - super(parameter); - } - } -} +/* + * DISCLAIMER + * + * Copyright 2017 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.query; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import org.springframework.core.MethodParameter; +import org.springframework.data.repository.query.Param; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; +import org.springframework.util.Assert; + +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.annotation.BindVars; + +/** + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner + */ +public class ArangoParameters extends Parameters { + + private final int queryOptionsIndex; + private final int bindVarsIndex; + + public ArangoParameters(final Method method) { + super(method); + assertSingleSpecialParameter(ArangoParameter::isQueryOptions, + "Multiple AqlQueryOptions parameters are not allowed! Offending method: " + method); + assertSingleSpecialParameter(ArangoParameter::isBindVars, + "Multiple @BindVars parameters are not allowed! Offending method: " + method); + assertNonDuplicateParamNames(method); + this.queryOptionsIndex = getIndexOfSpecialParameter(ArangoParameter::isQueryOptions); + this.bindVarsIndex = getIndexOfSpecialParameter(ArangoParameter::isBindVars); + } + + private ArangoParameters(final List parameters, final int queryOptionsIndex, + final int bindVarsIndex) { + super(parameters); + this.queryOptionsIndex = queryOptionsIndex; + this.bindVarsIndex = bindVarsIndex; + } + + @Override + protected ArangoParameter createParameter(final MethodParameter parameter) { + return new ArangoParameter(parameter); + } + + @Override + protected ArangoParameters createFrom(final List parameters) { + return new ArangoParameters(parameters, this.queryOptionsIndex, this.bindVarsIndex); + } + + public boolean hasQueryOptions() { + return this.queryOptionsIndex != -1; + } + + public int getQueryOptionsIndex() { + return this.queryOptionsIndex; + } + + public boolean hasBindVars() { + return this.bindVarsIndex != -1; + } + + public int getBindVarsIndex() { + return this.bindVarsIndex; + } + + private int getIndexOfSpecialParameter(final Predicate condition) { + for (int index = 0; index < getNumberOfParameters(); ++index) { + final ArangoParameter param = getParameter(index); + if (condition.test(param)) { + return index; + } + } + return -1; + } + + private void assertSingleSpecialParameter(final Predicate condition, final String message) { + boolean found = false; + for (int index = 0; index < getNumberOfParameters(); ++index) { + final ArangoParameter param = getParameter(index); + if (condition.test(param)) { + Assert.isTrue(!found, message); + found = true; + } + } + } + + private void assertNonDuplicateParamNames(final Method method) { + final ArangoParameters bindableParams = getBindableParameters(); + final int bindableParamsSize = bindableParams.getNumberOfParameters(); + final Set paramNames = new HashSet<>(bindableParamsSize); + for (int i = 0; i < bindableParamsSize; ++i) { + final ArangoParameter param = bindableParams.getParameter(i); + final String name = param.getName(); + if (name != null) { + Assert.isTrue(!paramNames.contains(name), "Duplicate parameter name! Offending method: " + method); + paramNames.add(name); + } + } + } + + static class ArangoParameter extends Parameter { + + private static final Pattern BIND_PARAM_PATTERN = Pattern.compile("^@?[A-Za-z0-9][A-Za-z0-9_]*$"); + private static final String NAMED_PARAMETER_TEMPLATE = "@%s"; + private static final String POSITION_PARAMETER_TEMPLATE = "@%d"; + + private final MethodParameter parameter; + + public ArangoParameter(final MethodParameter parameter) { + super(parameter); + this.parameter = parameter; + assertCorrectBindParamPattern(); + assertCorrectBindVarsType(); + } + + @Override + public boolean isSpecialParameter() { + return super.isSpecialParameter() || isQueryOptions() || isBindVars(); + } + + public boolean isQueryOptions() { + return AqlQueryOptions.class.isAssignableFrom(parameter.getParameterType()); + } + + public boolean isBindVars() { + return parameter.hasParameterAnnotation(BindVars.class); + } + + @SuppressWarnings("deprecation") + @Override + public String getName() { + final Param annotation = parameter.getParameterAnnotation(Param.class); + // we need to support the old @Param annotation + final com.arangodb.springframework.annotation.Param oldAnnotation = parameter + .getParameterAnnotation(com.arangodb.springframework.annotation.Param.class); + return annotation == null ? (oldAnnotation == null ? null : oldAnnotation.value()) : annotation.value(); + } + + @SuppressWarnings("deprecation") + @Override + public boolean isExplicitlyNamed() { + // we need to support the old @Param annotation + return super.isExplicitlyNamed() + || parameter.hasMethodAnnotation(com.arangodb.springframework.annotation.Param.class); + } + + @Override + public String getPlaceholder() { + if (isNamedParameter()) { + return String.format(NAMED_PARAMETER_TEMPLATE, getName()); + } else { + return String.format(POSITION_PARAMETER_TEMPLATE, getIndex()); + } + } + + private void assertCorrectBindParamPattern() { + if (isExplicitlyNamed()) { + final String name = getName(); + final boolean matches = BIND_PARAM_PATTERN.matcher(name).matches(); + Assert.isTrue(matches, "@Param has invalid format! Offending parameter: parameter " + + parameter.getParameterIndex() + " on method " + parameter.getMethod()); + } + } + + private void assertCorrectBindVarsType() { + final String errorMsg = "@BindVars parameter must be of type Map! Offending parameter: parameter " + + parameter.getParameterIndex() + " on method " + parameter.getMethod(); + + if (isBindVars()) { + Assert.isTrue(Map.class.equals(parameter.getParameterType()), errorMsg); + + final Type type = parameter.getGenericParameterType(); + Assert.isTrue(ParameterizedType.class.isInstance(type), errorMsg); + + final Type[] genericTypes = ((ParameterizedType) type).getActualTypeArguments(); + Assert.isTrue(genericTypes.length == 2, errorMsg); + + final Type keyType = genericTypes[0]; + final Type valueType = genericTypes[1]; + + Assert.isTrue(Class.class.isInstance(keyType), errorMsg); + Assert.isTrue(Class.class.isInstance(valueType), errorMsg); + Assert.isTrue(String.class.equals(keyType), errorMsg); + Assert.isTrue(Object.class.equals(valueType), errorMsg); + } + } + + } +} diff --git a/src/main/java/com/arangodb/springframework/repository/query/ArangoParametersParameterAccessor.java b/src/main/java/com/arangodb/springframework/repository/query/ArangoParametersParameterAccessor.java new file mode 100644 index 000000000..24818431b --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/query/ArangoParametersParameterAccessor.java @@ -0,0 +1,63 @@ +/* + * DISCLAIMER + * + * Copyright 2017 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.query; + +import java.util.Map; + +import org.springframework.data.repository.query.ParametersParameterAccessor; + +import com.arangodb.model.AqlQueryOptions; + +/** + * This class provides access to parameters of a user-defined method. It wraps ParametersParameterAccessor which catches + * special parameters Sort and Pageable, and catches Arango-specific parameters e.g. AqlQueryOptions. + * + * @author Audrius Malele + * @author Mark Vollmary + * @author Christian Lechner + */ +public class ArangoParametersParameterAccessor extends ParametersParameterAccessor implements ArangoParameterAccessor { + + private final ArangoParameters parameters; + + public ArangoParametersParameterAccessor(ArangoQueryMethod method, Object[] values) { + super(method.getParameters(), values); + this.parameters = method.getParameters(); + } + + @Override + public ArangoParameters getParameters() { + return parameters; + } + + @Override + public AqlQueryOptions getQueryOptions() { + final int optionsIndex = parameters.getQueryOptionsIndex(); + return optionsIndex == -1 ? null : getValue(optionsIndex); + } + + @Override + public Map getBindVars() { + final int bindVarsIndex = parameters.getBindVarsIndex(); + return bindVarsIndex == -1 ? null : getValue(bindVarsIndex); + } + +} diff --git a/src/main/java/com/arangodb/springframework/repository/query/ArangoQueryLookupStrategy.java b/src/main/java/com/arangodb/springframework/repository/query/ArangoQueryLookupStrategy.java deleted file mode 100644 index 73b0527be..000000000 --- a/src/main/java/com/arangodb/springframework/repository/query/ArangoQueryLookupStrategy.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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.query; - -import java.lang.reflect.Method; - -import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.query.QueryLookupStrategy; -import org.springframework.data.repository.query.RepositoryQuery; - -import com.arangodb.springframework.core.ArangoOperations; - -/** - * Created by F625633 on 12/07/2017. - */ -public class ArangoQueryLookupStrategy implements QueryLookupStrategy { - - private final ArangoOperations operations; - - public ArangoQueryLookupStrategy(final ArangoOperations operations) { - this.operations = operations; - } - - @Override - public RepositoryQuery resolveQuery( - final Method method, - final RepositoryMetadata metadata, - final ProjectionFactory factory, - final NamedQueries namedQueries) { - return new ArangoAqlQuery(metadata.getDomainType(), method, metadata, operations, factory); - } -} diff --git a/src/main/java/com/arangodb/springframework/repository/query/ArangoQueryMethod.java b/src/main/java/com/arangodb/springframework/repository/query/ArangoQueryMethod.java index a26c0a866..342af90e8 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/ArangoQueryMethod.java +++ b/src/main/java/com/arangodb/springframework/repository/query/ArangoQueryMethod.java @@ -21,28 +21,127 @@ package com.arangodb.springframework.repository.query; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.geo.GeoPage; +import org.springframework.data.geo.GeoResult; +import org.springframework.data.geo.GeoResults; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.StringUtils; + +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.annotation.Query; +import com.arangodb.springframework.annotation.QueryOptions; /** - * Created by F625633 on 12/07/2017. + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner */ public class ArangoQueryMethod extends QueryMethod { + private static final List> GEO_TYPES = Arrays.asList(GeoResult.class, GeoResults.class, GeoPage.class); + + private final Method method; + private final TypeInformation returnType; + public ArangoQueryMethod(final Method method, final RepositoryMetadata metadata, final ProjectionFactory factory) { super(method, metadata, factory); + this.method = method; + this.returnType = ClassTypeInformation.from(metadata.getRepositoryInterface()).getReturnType(method); } @Override - public Parameters getParameters() { - return super.getParameters(); + public ArangoParameters getParameters() { + return (ArangoParameters) super.getParameters(); } @Override - public Parameters createParameters(final Method method) { + public ArangoParameters createParameters(final Method method) { return new ArangoParameters(method); } + + public boolean hasAnnotatedQuery() { + return getQueryAnnotationValue().isPresent(); + } + + public String getAnnotatedQuery() { + return getQueryAnnotationValue().orElse(null); + } + + public Query getQueryAnnotation() { + return AnnotatedElementUtils.findMergedAnnotation(method, Query.class); + } + + private Optional getQueryAnnotationValue() { + return Optional.ofNullable(getQueryAnnotation()) // + .map(q -> q.value()) // + .filter(StringUtils::hasText); + } + + public boolean hasAnnotatedQueryOptions() { + return getQueryOptionsAnnotation() != null; + } + + public AqlQueryOptions getAnnotatedQueryOptions() { + final QueryOptions queryOptions = getQueryOptionsAnnotation(); + if (queryOptions == null) { + return null; + } + final AqlQueryOptions options = new AqlQueryOptions(); + final int batchSize = queryOptions.batchSize(); + if (batchSize != -1) { + options.batchSize(batchSize); + } + final int maxPlans = queryOptions.maxPlans(); + if (maxPlans != -1) { + options.maxPlans(maxPlans); + } + final int ttl = queryOptions.ttl(); + if (ttl != -1) { + options.ttl(ttl); + } + options.cache(queryOptions.cache()); + options.count(queryOptions.count()); + options.fullCount(queryOptions.fullCount()); + options.profile(queryOptions.profile()); + options.rules(Arrays.asList(queryOptions.rules())); + final boolean stream = queryOptions.stream(); + if (stream) { + options.stream(stream); + } + final long memoryLimit = queryOptions.memoryLimit(); + if (memoryLimit != -1) { + options.memoryLimit(memoryLimit); + } + return options; + } + + public QueryOptions getQueryOptionsAnnotation() { + return AnnotatedElementUtils.findMergedAnnotation(method, QueryOptions.class); + } + + public TypeInformation getReturnType() { + return returnType; + } + + public boolean isGeoQuery() { + final Class returnType = method.getReturnType(); + for (final Class type : GEO_TYPES) { + if (type.isAssignableFrom(returnType)) { + return true; + } + } + return false; + } + } diff --git a/src/main/java/com/arangodb/springframework/repository/query/ArangoResultConverter.java b/src/main/java/com/arangodb/springframework/repository/query/ArangoResultConverter.java index bea32eb74..beb57d6c8 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/ArangoResultConverter.java +++ b/src/main/java/com/arangodb/springframework/repository/query/ArangoResultConverter.java @@ -40,16 +40,25 @@ import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metrics; +import org.springframework.util.Assert; import com.arangodb.ArangoCursor; import com.arangodb.springframework.core.ArangoOperations; import com.arangodb.springframework.core.convert.DBDocumentEntity; /** - * Class used to convert the result returned from the ArangoDB java driver from ArangoCursor to the desired type + * Converts the result returned from the ArangoDB Java driver to the desired type. + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner */ public class ArangoResultConverter { + private final static String MISSING_FULL_COUNT = "Query result does not contain the full result count! " + + "The most likely cause is a forgotten LIMIT clause in the query."; + private final ArangoParameterAccessor accessor; private final ArangoCursor result; private final ArangoOperations operations; @@ -202,6 +211,7 @@ public List convertList() { } public PageImpl convertPage() { + Assert.notNull(result.getStats().getFullCount(), MISSING_FULL_COUNT); return new PageImpl<>(result.asListRemaining(), accessor.getPageable(), result.getStats().getFullCount()); } @@ -222,6 +232,7 @@ public GeoResults convertGeoResults() { } public GeoPage convertGeoPage() { + Assert.notNull(result.getStats().getFullCount(), MISSING_FULL_COUNT); return new GeoPage<>(buildGeoResults(result), accessor.getPageable(), result.getStats().getFullCount()); } diff --git a/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java new file mode 100644 index 000000000..86f19ccd5 --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/query/DerivedArangoQuery.java @@ -0,0 +1,91 @@ +/* + * 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.query; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.parser.PartTree; + +import com.arangodb.entity.IndexEntity; +import com.arangodb.entity.IndexType; +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; +import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; +import com.arangodb.springframework.repository.query.derived.DerivedQueryCreator; + +/** + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner + */ +public class DerivedArangoQuery extends AbstractArangoQuery { + + private final PartTree tree; + private final MappingContext, ArangoPersistentProperty> context; + private final List geoFields; + + public DerivedArangoQuery(final ArangoQueryMethod method, final ArangoOperations operations) { + super(method, operations); + this.tree = new PartTree(method.getName(), this.domainClass); + this.context = operations.getConverter().getMappingContext(); + this.geoFields = getGeoFields(); + } + + @Override + protected String createQuery( + final ArangoParameterAccessor accessor, + final Map bindVars, + final AqlQueryOptions options) { + + return new DerivedQueryCreator(context, domainClass, tree, accessor, bindVars, geoFields, + operations.getVersion().getVersion().compareTo("3.2.0") < 0).createQuery(); + } + + @Override + protected boolean isCountQuery() { + return tree.isCountProjection(); + } + + @Override + protected boolean isExistsQuery() { + return tree.isExistsProjection(); + } + + private List getGeoFields() { + final List geoFields = new LinkedList<>(); + if (method.isGeoQuery()) { + for (final IndexEntity index : operations.collection(domainClass).getIndexes()) { + final IndexType type = index.getType(); + if (type == IndexType.geo || type == IndexType.geo1 || type == IndexType.geo2) { + geoFields.addAll(index.getFields()); + } + } + } + return geoFields; + } + +} diff --git a/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java new file mode 100644 index 000000000..1aca16ece --- /dev/null +++ b/src/main/java/com/arangodb/springframework/repository/query/StringBasedArangoQuery.java @@ -0,0 +1,196 @@ +/* + * 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.query; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; + +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.core.ArangoOperations; +import com.arangodb.springframework.core.util.AqlUtils; +import com.arangodb.springframework.repository.query.ArangoParameters.ArangoParameter; + +/** + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner + */ +public class StringBasedArangoQuery extends AbstractArangoQuery { + + private static final String PAGEABLE_PLACEHOLDER = "#pageable"; + + private static final String SORT_PLACEHOLDER = "#sort"; + + private static final Pattern BIND_PARAM_PATTERN = Pattern.compile("@(@?[A-Za-z0-9][A-Za-z0-9_]*)"); + + private static final Pattern PAGEABLE_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(PAGEABLE_PLACEHOLDER)); + + private static final Pattern SORT_PLACEHOLDER_PATTERN = Pattern.compile(Pattern.quote(SORT_PLACEHOLDER)); + + private final String query; + private final Set queryBindParams; + + public StringBasedArangoQuery(ArangoQueryMethod method, ArangoOperations operations) { + this(method.getAnnotatedQuery(), method, operations); + } + + public StringBasedArangoQuery(String query, ArangoQueryMethod method, ArangoOperations operations) { + super(method, operations); + Assert.notNull(query, "Query must not be null!"); + + this.query = query; + + assertSinglePageablePlaceholder(); + assertSingleSortPlaceholder(); + + this.queryBindParams = getBindParamsInQuery(); + } + + @Override + protected String createQuery(final ArangoParameterAccessor accessor, final Map bindVars, + final AqlQueryOptions options) { + + extractBindVars(accessor, bindVars); + + return prepareQuery(accessor); + } + + @Override + protected boolean isCountQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + private String prepareQuery(final ArangoParameterAccessor accessor) { + if (accessor.getParameters().hasPageableParameter()) { + final String pageableClause = AqlUtils.buildPageableClause(accessor.getPageable()); + return PAGEABLE_PLACEHOLDER_PATTERN.matcher(query).replaceFirst(pageableClause); + } + + if (accessor.getParameters().hasSortParameter()) { + final String sortClause = AqlUtils.buildSortClause(accessor.getSort()); + return SORT_PLACEHOLDER_PATTERN.matcher(query).replaceFirst(sortClause); + } + + return query; + } + + private void extractBindVars(final ArangoParameterAccessor accessor, final Map bindVars) { + final Map bindVarsInParams = accessor.getBindVars(); + if (bindVarsInParams != null) { + bindVars.putAll(bindVarsInParams); + } + + final ArangoParameters bindableParams = accessor.getParameters().getBindableParameters(); + final int bindableParamsSize = bindableParams.getNumberOfParameters(); + + for (int i = 0; i < bindableParamsSize; ++i) { + final ArangoParameter param = bindableParams.getParameter(i); + final Object value = accessor.getBindableValue(i); + if (param.isNamedParameter()) { + bindVars.put(param.getName(), value); + } else { + final String key = String.valueOf(param.getIndex()); + final String collectionKey = "@" + key; + if (queryBindParams.contains(collectionKey)) { + bindVars.put(collectionKey, value); + } else { + bindVars.put(key, value); + } + } + } + } + + private Set getBindParamsInQuery() { + final String fixedQuery = removeAqlStringLiterals(query); + final Set bindings = new HashSet<>(); + final Matcher matcher = BIND_PARAM_PATTERN.matcher(fixedQuery); + while (matcher.find()) { + bindings.add(matcher.group(1)); + } + return bindings; + } + + private String removeAqlStringLiterals(final String query) { + final StringBuilder fixedQuery = new StringBuilder(); + for (int i = 0; i < query.length(); ++i) { + if (query.charAt(i) == '"') { + for (++i; i < query.length(); ++i) { + if (query.charAt(i) == '"') { + ++i; + break; + } + if (query.charAt(i) == '\\') { + ++i; + } + } + } else if (query.charAt(i) == '\'') { + for (++i; i < query.length(); ++i) { + if (query.charAt(i) == '\'') { + ++i; + break; + } + if (query.charAt(i) == '\\') { + ++i; + } + } + } + fixedQuery.append(query.charAt(i)); + } + return fixedQuery.toString(); + } + + private void assertSinglePageablePlaceholder() { + if (method.getParameters().hasPageableParameter()) { + int firstOccurrence = query.indexOf(PAGEABLE_PLACEHOLDER); + int secondOccurrence = query.indexOf(PAGEABLE_PLACEHOLDER, firstOccurrence + PAGEABLE_PLACEHOLDER.length()); + + Assert.isTrue(firstOccurrence > -1 && secondOccurrence < 0, + String.format( + "Native query with Pageable param must contain exactly one pageable placeholder (%s)! Offending method: %s", + PAGEABLE_PLACEHOLDER, method)); + } + } + + private void assertSingleSortPlaceholder() { + if (method.getParameters().hasSortParameter()) { + int firstOccurrence = query.indexOf(SORT_PLACEHOLDER); + int secondOccurrence = query.indexOf(SORT_PLACEHOLDER, firstOccurrence + SORT_PLACEHOLDER.length()); + + Assert.isTrue(firstOccurrence > -1 && secondOccurrence < 0, + String.format( + "Native query with Sort param must contain exactly one sort placeholder (%s)! Offending method: %s", + SORT_PLACEHOLDER, method)); + } + } + +} diff --git a/src/main/java/com/arangodb/springframework/repository/query/derived/Conjunction.java b/src/main/java/com/arangodb/springframework/repository/query/derived/Conjunction.java index 691d5279c..05f6355a5 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/derived/Conjunction.java +++ b/src/main/java/com/arangodb/springframework/repository/query/derived/Conjunction.java @@ -20,6 +20,8 @@ package com.arangodb.springframework.repository.query.derived; +import java.util.Collection; + /** * Created by F625633 on 24/07/2017. */ @@ -27,10 +29,12 @@ public class Conjunction { private final String array; private final String predicate; + private final Collection> with; - public Conjunction(final String array, final String predicate) { + public Conjunction(final String array, final String predicate, final Collection> with) { this.array = array; this.predicate = predicate; + this.with = with; } public String getArray() { @@ -52,4 +56,9 @@ public boolean hasPredicate() { public boolean isComposite() { return isArray() && hasPredicate(); } + + public Collection> getWith() { + return with; + } + } diff --git a/src/main/java/com/arangodb/springframework/repository/query/derived/ConjunctionBuilder.java b/src/main/java/com/arangodb/springframework/repository/query/derived/ConjunctionBuilder.java index 3efa68311..157c39848 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/derived/ConjunctionBuilder.java +++ b/src/main/java/com/arangodb/springframework/repository/query/derived/ConjunctionBuilder.java @@ -20,6 +20,9 @@ package com.arangodb.springframework.repository.query.derived; +import java.util.ArrayList; +import java.util.Collection; + /** * Created by F625633 on 24/07/2017. */ @@ -28,11 +31,20 @@ public class ConjunctionBuilder { private static final String ARRAY_DELIMITER = ", "; private static final String PREDICATE_DELIMITER = " AND "; - private final StringBuilder arrayStringBuilder = new StringBuilder(); - private final StringBuilder predicateStringBuilder = new StringBuilder(); + private final StringBuilder arrayStringBuilder; + private final StringBuilder predicateStringBuilder; + private final Collection> with; private int arrays = 0; + public ConjunctionBuilder() { + super(); + arrayStringBuilder = new StringBuilder(); + predicateStringBuilder = new StringBuilder(); + with = new ArrayList<>(); + arrays = 0; + } + public void add(final PartInformation partInformation) { if (partInformation.isArray()) { ++arrays; @@ -42,6 +54,9 @@ public void add(final PartInformation partInformation) { predicateStringBuilder.append( (predicateStringBuilder.length() == 0 ? "" : PREDICATE_DELIMITER) + partInformation.getClause()); } + if (partInformation.getWith() != null) { + with.addAll(partInformation.getWith()); + } } private String buildArrayString() { @@ -52,6 +67,6 @@ private String buildArrayString() { } public Conjunction build() { - return new Conjunction(buildArrayString(), predicateStringBuilder.toString()); + return new Conjunction(buildArrayString(), predicateStringBuilder.toString(), with); } } diff --git a/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java b/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java index f23e4688e..baef94d8c 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java +++ b/src/main/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreator.java @@ -20,12 +20,17 @@ package com.arangodb.springframework.repository.query.derived; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +42,8 @@ import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentPropertyPath; import org.springframework.data.repository.query.parser.AbstractQueryCreator; import org.springframework.data.repository.query.parser.Part; @@ -44,9 +51,9 @@ import org.springframework.util.Assert; import com.arangodb.springframework.annotation.Relations; -import com.arangodb.springframework.core.mapping.ArangoMappingContext; import com.arangodb.springframework.core.mapping.ArangoPersistentEntity; import com.arangodb.springframework.core.mapping.ArangoPersistentProperty; +import com.arangodb.springframework.core.util.AqlUtils; import com.arangodb.springframework.repository.query.ArangoParameterAccessor; import com.arangodb.springframework.repository.query.derived.geo.Ring; @@ -69,7 +76,7 @@ public class DerivedQueryCreator extends AbstractQueryCreator, ArangoPersistentProperty> context; private final String collectionName; private final PartTree tree; private final Map bindVars; @@ -80,19 +87,15 @@ public class DerivedQueryCreator extends AbstractQueryCreator domainClass, final PartTree tree, - final ArangoParameterAccessor accessor, final Map bindVars, final List geoFields, - final boolean useFunctions) { + public DerivedQueryCreator( + final MappingContext, ArangoPersistentProperty> context, + final Class domainClass, final PartTree tree, final ArangoParameterAccessor accessor, + final Map bindVars, final List geoFields, final boolean useFunctions) { super(tree, accessor); this.context = context; - String collectionName = context.getPersistentEntity(domainClass).getCollection(); - if (collectionName.split("-").length > 1) { - collectionName = "`" + collectionName + "`"; - } - this.collectionName = collectionName; + this.collectionName = collectionName(context.getPersistentEntity(domainClass).getCollection()); this.tree = tree; this.bindVars = bindVars; this.accessor = accessor; @@ -100,6 +103,10 @@ public DerivedQueryCreator(final ArangoMappingContext context, final Class do this.useFunctions = useFunctions; } + private static String collectionName(final String collection) { + return collection.contains("-") ? "`" + collection + "`" : collection; + } + public String getCollectionName() { return collectionName; } @@ -138,7 +145,7 @@ protected ConjunctionBuilder or(final ConjunctionBuilder base, final Conjunction /** * Builds a full AQL query from a built Disjunction, additional information from PartTree and special parameters * caught by ArangoParameterAccessor - * + * * @param criteria * @param sort * @return @@ -155,58 +162,41 @@ protected String complete(final ConjunctionBuilder criteria, final Sort sort) { final String array = disjunction.getArray().length() == 0 ? collectionName : disjunction.getArray(); final String predicate = disjunction.getPredicate().length() == 0 ? "" : " FILTER " + disjunction.getPredicate(); - final String queryTemplate = "FOR e IN %s%s%s%s%s%s%s"; // collection predicate count sort limit pageable - // queryType + final String queryTemplate = "%sFOR e IN %s%s%s%s%s%s%s"; // collection predicate count sort limit pageable + // queryType final String count = (tree.isCountProjection() || tree.isExistsProjection()) ? ((tree.isDistinct() ? " COLLECT entity = e" : "") + " COLLECT WITH COUNT INTO length") : ""; - final String limit = tree.isLimiting() ? String.format(" LIMIT %d", tree.getMaxResults()) : ""; + final String limit = tree.isLimiting() ? format(" LIMIT %d", tree.getMaxResults()) : ""; final String pageable = accessor.getPageable() == null ? "" - : String.format(" LIMIT %d, %d", accessor.getPageable().getOffset(), - accessor.getPageable().getPageSize()); - final String geoFields = String.format("%s[0], %s[1]", uniqueLocation, uniqueLocation); + : format(" LIMIT %d, %d", accessor.getPageable().getOffset(), accessor.getPageable().getPageSize()); + final String geoFields = format("%s[0], %s[1]", uniqueLocation, uniqueLocation); final String distanceAdjusted = getGeoFields().isEmpty() ? "e" - : String.format("MERGE(e, { '_distance': distance(%s, %f, %f) })", geoFields, getUniquePoint()[0], + : format("MERGE(e, { '_distance': distance(%s, %f, %f) })", geoFields, getUniquePoint()[0], getUniquePoint()[1]); final String type = tree.isDelete() ? (" REMOVE e IN " + collectionName) : ((tree.isCountProjection() || tree.isExistsProjection()) ? " RETURN length" - : String.format(" RETURN %s", distanceAdjusted)); - String sortString = buildSortString(sort); + : format(" RETURN %s", distanceAdjusted)); + String sortString = " " + AqlUtils.buildSortClause(sort, "e"); if ((!this.geoFields.isEmpty() || isUnique != null && isUnique) && !tree.isDelete() && !tree.isCountProjection() && !tree.isExistsProjection()) { - final String distanceSortKey = String.format(" SORT distance(%s, %f, %f)", geoFields, getUniquePoint()[0], + final String distanceSortKey = format(" SORT distance(%s, %f, %f)", geoFields, getUniquePoint()[0], getUniquePoint()[1]); - if (sortString.length() == 0) { + if (sort == null) { sortString = distanceSortKey; } else { sortString = distanceSortKey + ", " + sortString.substring(5, sortString.length()); } } - return String.format(queryTemplate, array, predicate, count, sortString, limit, pageable, type); - } - - /** - * Builds a String representing SORT statement from a given Sort object - * - * @param sort - * @return - */ - public static String buildSortString(final Sort sort) { - if (sort == null) { - LOGGER.debug("Sort in findAll(Sort) is null"); - } - final StringBuilder sortBuilder = new StringBuilder(sort == null ? "" : " SORT"); - if (sort != null) { - for (final Sort.Order order : sort) { - sortBuilder.append( - (sortBuilder.length() == 5 ? " " : ", ") + "e." + order.getProperty() + " " + order.getDirection()); - } - } - return sortBuilder.toString(); + final String withCollections = disjunction.getWith().stream() + .map(c -> collectionName(context.getPersistentEntity(c).getCollection())).distinct() + .collect(Collectors.joining(", ")); + final String with = withCollections.isEmpty() ? "" : format("WITH %s ", withCollections); + return format(queryTemplate, with, array, predicate, count, sortString, limit, pageable, type); } /** * Escapes special characters which could be used in an operand of LIKE operator - * + * * @param string * @return */ @@ -228,7 +218,7 @@ private String ignorePropertyCase(final Part part) { /** * Wrapps property expression in order to lower case. Only properties of type String or Iterable are lowered - * + * * @param part * @param property * @return @@ -240,12 +230,12 @@ private String ignorePropertyCase(final Part part, final String property) { if (!part.getProperty().getLeafProperty().isCollection()) { return "LOWER(" + property + ")"; } - return String.format("(FOR i IN TO_ARRAY(%s) RETURN LOWER(i))", property); + return format("(FOR i IN TO_ARRAY(%s) RETURN LOWER(i))", property); } /** * Returns a String representing a full propertyPath e.g. "e.product.name" - * + * * @param part * @return */ @@ -258,11 +248,12 @@ private String getProperty(final Part part) { * Creates a predicate template with one String placeholder for a Part-specific predicate expression from properties * in PropertyPath which represent references or collections, and, also, returns a 2nd String representing property * to be used in a Part-specific predicate expression - * + * * @param part + * @param varsUsed * @return */ - private String[] createPredicateTemplateAndPropertyString(final Part part) { + private String[] createPredicateTemplateAndPropertyString(final Part part, int varsUsed) { final String PREDICATE_TEMPLATE = "(%s FILTER %%s RETURN 1)[0] == 1"; final PersistentPropertyPath persistentPropertyPath = context.getPersistentPropertyPath(part.getProperty()); StringBuilder simpleProperties = new StringBuilder(); @@ -281,7 +272,7 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { final String nested = simpleProperties.toString(); final Relations relations = property.getRelations().get(); final String direction = relations.direction().name(); - final String depths = String.format("%s..%d", relations.minDepth(), relations.maxDepth()); + final String depths = format("%s..%d", relations.minDepth(), relations.maxDepth()); final Class[] edgeClasses = relations.edges(); final StringBuilder edgesBuilder = new StringBuilder(); for (final Class edge : edgeClasses) { @@ -295,10 +286,9 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { final String entity = "e" + Integer.toString(++varsUsed); final String edges = edgesBuilder.toString(); simpleProperties = new StringBuilder(); - final String iteration = String.format(TEMPLATE, entity, depths, direction, prevEntity, nested, edges); - final String predicate = String.format(PREDICATE_TEMPLATE, iteration); - predicateTemplate = predicateTemplate.length() == 0 ? predicate - : String.format(predicateTemplate, predicate); + final String iteration = format(TEMPLATE, entity, depths, direction, prevEntity, nested, edges); + final String predicate = format(PREDICATE_TEMPLATE, iteration); + predicateTemplate = predicateTemplate.length() == 0 ? predicate : format(predicateTemplate, predicate); } else if (property.isCollectionLike()) { if (property.getRef().isPresent()) { // collection of references @@ -311,10 +301,10 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { } final String name = simpleProperties.toString() + "." + property.getFieldName(); simpleProperties = new StringBuilder(); - final String iteration = String.format(TEMPLATE, entity, collection, entity, prevEntity, name); - final String predicate = String.format(PREDICATE_TEMPLATE, iteration); + final String iteration = format(TEMPLATE, entity, collection, entity, prevEntity, name); + final String predicate = format(PREDICATE_TEMPLATE, iteration); predicateTemplate = predicateTemplate.length() == 0 ? predicate - : String.format(predicateTemplate, predicate); + : format(predicateTemplate, predicate); } else { // collection final String TEMPLATE = "FOR %s IN TO_ARRAY(%s%s)"; @@ -322,10 +312,10 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { final String entity = "e" + Integer.toString(++varsUsed); final String name = simpleProperties.toString() + "." + property.getFieldName(); simpleProperties = new StringBuilder(); - final String iteration = String.format(TEMPLATE, entity, prevEntity, name); - final String predicate = String.format(PREDICATE_TEMPLATE, iteration); + final String iteration = format(TEMPLATE, entity, prevEntity, name); + final String predicate = format(PREDICATE_TEMPLATE, iteration); predicateTemplate = predicateTemplate.length() == 0 ? predicate - : String.format(predicateTemplate, predicate); + : format(predicateTemplate, predicate); } } else { if (property.getRef().isPresent() || property.getFrom().isPresent() || property.getTo().isPresent()) { @@ -339,10 +329,10 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { } final String name = simpleProperties.toString() + "." + property.getFieldName(); simpleProperties = new StringBuilder(); - final String iteration = String.format(TEMPLATE, entity, collection, entity, prevEntity, name); - final String predicate = String.format(PREDICATE_TEMPLATE, iteration); + final String iteration = format(TEMPLATE, entity, collection, entity, prevEntity, name); + final String predicate = format(PREDICATE_TEMPLATE, iteration); predicateTemplate = predicateTemplate.length() == 0 ? predicate - : String.format(predicateTemplate, predicate); + : format(predicateTemplate, predicate); } else { // simple property simpleProperties.append("." + property.getFieldName()); @@ -355,7 +345,7 @@ private String[] createPredicateTemplateAndPropertyString(final Part part) { /** * Lowers case of a given argument if its type is String, Iterable or String[] if shouldIgnoreCase is true - * + * * @param argument * @param shouldIgnoreCase * @return @@ -386,7 +376,7 @@ private Object ignoreArgumentCase(final Object argument, final boolean shouldIgn /** * Determines whether the case for a Part should be ignored based on property type and IgnoreCase keywords in the * method name - * + * * @param part * @return */ @@ -404,7 +394,7 @@ private boolean shouldIgnoreCase(final Part part) { /** * Puts actual arguments in bindVars Map based on Part-specific information and types of arguments. - * + * * @param iterator * @param shouldIgnoreCase * @param arguments @@ -497,7 +487,7 @@ private ArgumentProcessingResult bindArguments( /** * Ensures that Points used in geospatial parts of non-nested properties are the same in case geospatial return type * is expected - * + * * @param point */ private void checkUniquePoint(final Point point) { @@ -534,7 +524,7 @@ private double convertDistanceToMeters(final Distance distance) { /** * Ensures that the same geo fields are used in geospatial parts of non-nested properties are the same in case * geospatial return type is expected - * + * * @param part */ private void checkUniqueLocation(final Part part) { @@ -552,13 +542,14 @@ private void checkUniqueLocation(final Part part) { /** * Creates a PartInformation containing a String representing either a predicate or array expression, and binds * arguments from Iterator for a given Part - * + * * @param part * @param iterator * @return */ private PartInformation createPartInformation(final Part part, final Iterator iterator) { - final String[] templateAndProperty = createPredicateTemplateAndPropertyString(part); + int varsUsed = 0; + final String[] templateAndProperty = createPredicateTemplateAndPropertyString(part, varsUsed); final String template = templateAndProperty[0]; final String property = templateAndProperty[1]; boolean isArray = false; @@ -573,120 +564,125 @@ private PartInformation createPartInformation(final Part part, final Iterator 1) { collectionName = "`" + collectionName + "`"; } - // TODO possibly refactor in the future if the complexity of this block does not increase + // TODO possibly refactor in the future if the complexity of this block does not + // increase switch (part.getType()) { case SIMPLE_PROPERTY: isArray = false; - clause = String.format("%s == @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s == @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case NEGATING_SIMPLE_PROPERTY: isArray = false; - clause = String.format("%s != @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s != @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case TRUE: isArray = false; - clause = String.format("%s == true", ignorePropertyCase(part, property)); + clause = format("%s == true", ignorePropertyCase(part, property)); break; case FALSE: isArray = false; - clause = String.format("%s == false", ignorePropertyCase(part, property)); + clause = format("%s == false", ignorePropertyCase(part, property)); break; case IS_NULL: isArray = false; - clause = String.format("%s == null", ignorePropertyCase(part, property)); + clause = format("%s == null", ignorePropertyCase(part, property)); break; case IS_NOT_NULL: isArray = false; - clause = String.format("%s != null", ignorePropertyCase(part, property)); + clause = format("%s != null", ignorePropertyCase(part, property)); break; case EXISTS: isArray = false; - clause = String.format("HAS(%s, '%s')", property.substring(0, property.lastIndexOf(".")), + clause = format("HAS(%s, '%s')", property.substring(0, property.lastIndexOf(".")), property.substring(property.lastIndexOf(".") + 1, property.length())); break; case BEFORE: case LESS_THAN: isArray = false; - clause = String.format("%s < @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s < @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case AFTER: case GREATER_THAN: isArray = false; - clause = String.format("%s > @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s > @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case LESS_THAN_EQUAL: isArray = false; - clause = String.format("%s <= @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s <= @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case GREATER_THAN_EQUAL: isArray = false; - clause = String.format("%s >= @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s >= @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case BETWEEN: isArray = false; - clause = String.format("@%d <= %s AND %s <= @%d", bindingCounter, ignorePropertyCase(part, property), + clause = format("@%d <= %s AND %s <= @%d", bindingCounter, ignorePropertyCase(part, property), ignorePropertyCase(part, property), bindingCounter + 1); arguments = 2; break; case LIKE: isArray = false; - clause = String.format("%s LIKE @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s LIKE @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case NOT_LIKE: isArray = false; - clause = String.format("NOT(%s LIKE @%d)", ignorePropertyCase(part, property), bindingCounter); + clause = format("NOT(%s LIKE @%d)", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case STARTING_WITH: isArray = false; - clause = String.format("%s LIKE @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s LIKE @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; borderStatus = true; break; case ENDING_WITH: isArray = false; - clause = String.format("%s LIKE @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s LIKE @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; borderStatus = false; break; case REGEX: isArray = false; - clause = String.format("REGEX_TEST(%s, @%d, %b)", ignorePropertyCase(part, property), bindingCounter, + clause = format("REGEX_TEST(%s, @%d, %b)", ignorePropertyCase(part, property), bindingCounter, shouldIgnoreCase(part)); arguments = 1; break; case IN: isArray = false; - clause = String.format("%s IN @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s IN @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case NOT_IN: isArray = false; - clause = String.format("%s NOT IN @%d", ignorePropertyCase(part, property), bindingCounter); + clause = format("%s NOT IN @%d", ignorePropertyCase(part, property), bindingCounter); arguments = 1; break; case CONTAINING: isArray = false; - clause = String.format("@%d IN %s", bindingCounter, ignorePropertyCase(part, property)); + if (part.getProperty().getOwningType().getProperty(part.getProperty().getSegment()).isCollectionLike()) { + clause = format("@%d IN %s", bindingCounter, ignorePropertyCase(part, property)); + } else { + clause = format("CONTAINS(%s, @%d)", ignorePropertyCase(part, property), bindingCounter); + } arguments = 1; break; case NOT_CONTAINING: isArray = false; - clause = String.format("@%d NOT IN %s", bindingCounter, ignorePropertyCase(part, property)); + clause = format("@%d NOT IN %s", bindingCounter, ignorePropertyCase(part, property)); arguments = 1; break; case NEAR: checkUniqueLocation(part); if (useFunctions) { isArray = true; - clause = String.format("NEAR(%s, @%d, @%d, COUNT(%s), '_distance')", collectionName, bindingCounter, + clause = format("NEAR(%s, @%d, @%d, COUNT(%s), '_distance')", collectionName, bindingCounter, bindingCounter + 1, collectionName); if (geoFields.isEmpty()) { clause = unsetDistance(clause); @@ -700,20 +696,20 @@ private PartInformation createPartInformation(final Part part, final Iterator> with = new ArrayList<>(); + PropertyPath pp = part.getProperty(); + do { + Optional.ofNullable(pp.isCollection() ? pp.getOwningType().getComponentType() : pp.getOwningType()) + .filter(t -> context.getPersistentEntity(t) != null).map(t -> t.getType()) + .ifPresent(t -> with.add(t)); + } while ((pp = pp.next()) != null); + return clause == null ? null : new PartInformation(isArray, clause, with); } private String unsetDistance(final String clause) { - return String.format("(FOR u IN %s RETURN UNSET(u, '_distance'))", clause); + return format("(FOR u IN %s RETURN UNSET(u, '_distance'))", clause); + } + + private String format(final String format, final Object... args) { + return String.format(Locale.ENGLISH, format, args); } /** diff --git a/src/main/java/com/arangodb/springframework/repository/query/derived/Disjunction.java b/src/main/java/com/arangodb/springframework/repository/query/derived/Disjunction.java index 258f2a680..0dd97d15b 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/derived/Disjunction.java +++ b/src/main/java/com/arangodb/springframework/repository/query/derived/Disjunction.java @@ -20,6 +20,8 @@ package com.arangodb.springframework.repository.query.derived; +import java.util.Collection; + /** * Created by F625633 on 24/07/2017. */ @@ -27,10 +29,12 @@ public class Disjunction { private final String array; private final String predicate; + private final Collection> with; - public Disjunction(final String array, final String predicate) { + public Disjunction(final String array, final String predicate, final Collection> with) { this.array = array; this.predicate = predicate; + this.with = with; } public String getArray() { @@ -40,4 +44,9 @@ public String getArray() { public String getPredicate() { return predicate; } + + public Collection> getWith() { + return with; + } + } diff --git a/src/main/java/com/arangodb/springframework/repository/query/derived/DisjunctionBuilder.java b/src/main/java/com/arangodb/springframework/repository/query/derived/DisjunctionBuilder.java index f92187b0c..c1ebc68fa 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/derived/DisjunctionBuilder.java +++ b/src/main/java/com/arangodb/springframework/repository/query/derived/DisjunctionBuilder.java @@ -20,6 +20,8 @@ package com.arangodb.springframework.repository.query.derived; +import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedList; /** @@ -33,15 +35,22 @@ public class DisjunctionBuilder { private final DerivedQueryCreator queryCreator; - private final LinkedList conjunctions = new LinkedList<>(); + private final LinkedList conjunctions; - private final StringBuilder arrayStringBuilder = new StringBuilder(); - private final StringBuilder predicateStringBuilder = new StringBuilder(); + private final StringBuilder arrayStringBuilder; + private final StringBuilder predicateStringBuilder; + private final Collection> with; - private int arrays = 0; + private int arrays; public DisjunctionBuilder(final DerivedQueryCreator queryCreator) { + super(); this.queryCreator = queryCreator; + conjunctions = new LinkedList<>(); + arrayStringBuilder = new StringBuilder(); + predicateStringBuilder = new StringBuilder(); + with = new ArrayList<>(); + arrays = 0; } public void add(final Conjunction conjunction) { @@ -56,6 +65,7 @@ public void add(final Conjunction conjunction) { predicateStringBuilder.append( (predicateStringBuilder.length() == 0 ? "" : PREDICATE_DELIMITER) + conjunction.getPredicate()); } + with.addAll(conjunction.getWith()); } private String buildArrayString() { @@ -90,6 +100,6 @@ private String buildPredicateSring() { public Disjunction build() { final String arrayString = String.format(buildArrayString(), queryCreator.getUniquePoint()[0], queryCreator.getUniquePoint()[1]); - return new Disjunction(arrayString, buildPredicateSring()); + return new Disjunction(arrayString, buildPredicateSring(), with); } } diff --git a/src/main/java/com/arangodb/springframework/repository/query/derived/PartInformation.java b/src/main/java/com/arangodb/springframework/repository/query/derived/PartInformation.java index 264d6fd71..9f09cd6e8 100644 --- a/src/main/java/com/arangodb/springframework/repository/query/derived/PartInformation.java +++ b/src/main/java/com/arangodb/springframework/repository/query/derived/PartInformation.java @@ -20,6 +20,8 @@ package com.arangodb.springframework.repository.query.derived; +import java.util.Collection; + /** * Created by F625633 on 24/07/2017. */ @@ -27,10 +29,12 @@ public class PartInformation { private final boolean isArray; private final String clause; + private final Collection> with; - public PartInformation(final boolean isArray, final String clause) { + public PartInformation(final boolean isArray, final String clause, final Collection> with) { this.isArray = isArray; this.clause = clause; + this.with = with; } public boolean isArray() { @@ -40,4 +44,9 @@ public boolean isArray() { public String getClause() { return clause; } + + public Collection> getWith() { + return with; + } + } diff --git a/src/test/java/com/arangodb/springframework/ArangoTestConfiguration.java b/src/test/java/com/arangodb/springframework/ArangoTestConfiguration.java index 09b220aad..f2f639b92 100644 --- a/src/test/java/com/arangodb/springframework/ArangoTestConfiguration.java +++ b/src/test/java/com/arangodb/springframework/ArangoTestConfiguration.java @@ -21,17 +21,18 @@ package com.arangodb.springframework; import org.springframework.context.annotation.Configuration; - import com.arangodb.ArangoDB; import com.arangodb.springframework.annotation.EnableArangoRepositories; import com.arangodb.springframework.config.AbstractArangoConfiguration; /** + * * @author Mark Vollmary - * + * @author Christian Lechner */ @Configuration -@EnableArangoRepositories(basePackages = { "com.arangodb.springframework.repository" }) +@EnableArangoRepositories(basePackages = { + "com.arangodb.springframework.repository" }, namedQueriesLocation = "classpath*:arango-named-queries-test.properties") public class ArangoTestConfiguration extends AbstractArangoConfiguration { public static final String DB = "spring-test-db"; diff --git a/src/test/java/com/arangodb/springframework/core/mapping/ArangoMappingTest.java b/src/test/java/com/arangodb/springframework/core/mapping/ArangoMappingTest.java index 44c6e09ab..ec1bb30fc 100644 --- a/src/test/java/com/arangodb/springframework/core/mapping/ArangoMappingTest.java +++ b/src/test/java/com/arangodb/springframework/core/mapping/ArangoMappingTest.java @@ -1,1050 +1,1423 @@ -/* - * DISCLAIMER - * - * Copyright 2017 ArangoDB GmbH, Cologne, Germany - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Copyright holder is ArangoDB GmbH, Cologne, Germany - */ - -package com.arangodb.springframework.core.mapping; - -import static org.hamcrest.Matchers.hasItems; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isOneOf; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.data.annotation.Id; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import com.arangodb.entity.DocumentEntity; -import com.arangodb.model.AqlQueryOptions; -import com.arangodb.springframework.AbstractArangoTest; -import com.arangodb.springframework.ArangoTestConfiguration; -import com.arangodb.springframework.annotation.Document; -import com.arangodb.springframework.annotation.Edge; -import com.arangodb.springframework.annotation.Field; -import com.arangodb.springframework.annotation.From; -import com.arangodb.springframework.annotation.Key; -import com.arangodb.springframework.annotation.Ref; -import com.arangodb.springframework.annotation.Relations; -import com.arangodb.springframework.annotation.Rev; -import com.arangodb.springframework.annotation.To; -import com.arangodb.util.MapBuilder; -import com.arangodb.velocypack.VPackSlice; - -/** - * @author Mark Vollmary - * - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = { ArangoTestConfiguration.class }) -public class ArangoMappingTest extends AbstractArangoTest { - - @Document - public static class BasicTestEntity { - - @Id - String id; - @Key - String key; - @Rev - String rev; - - public BasicTestEntity() { - super(); - } - - public String getId() { - return id; - } - - public void setId(final String id) { - this.id = id; - } - - public String getKey() { - return key; - } - - public void setKey(final String key) { - this.key = key; - } - - public String getRev() { - return rev; - } - - public void setRev(final String rev) { - this.rev = rev; - } - - } - - @Test - public void idKeyRev() { - final DocumentEntity ref = template.insert(new BasicTestEntity()); - final BasicTestEntity entity = template.find(ref.getId(), BasicTestEntity.class).get(); - assertThat(entity, is(notNullValue())); - assertThat(entity.getId(), is(ref.getId())); - assertThat(entity.getKey(), is(ref.getKey())); - assertThat(entity.getRev(), is(ref.getRev())); - } - - public static class OnlyIdTestEntity { - @Id - private String id; - } - - @Test - public void supplementKey() { - final OnlyIdTestEntity value = new OnlyIdTestEntity(); - template.insert(value); - final List result = template.query("RETURN @doc", new MapBuilder().put("doc", value).get(), - new AqlQueryOptions(), BasicTestEntity.class).asListRemaining(); - assertThat(result.size(), is(1)); - assertThat(result.get(0).getId(), is(value.id)); - assertThat(result.get(0).getKey(), is(value.id.split("/")[1])); - assertThat(result.get(0).getRev(), is(nullValue())); - } - - public static class FieldNameTestEntity extends BasicTestEntity { - @Field("alt-test") - private String test; - } - - @Test - public void fieldNameAnnotation() { - final FieldNameTestEntity entity = new FieldNameTestEntity(); - entity.test = "1234"; - final DocumentEntity res = template.insert(entity); - final VPackSlice slice = template.driver().db(ArangoTestConfiguration.DB).getDocument(res.getId(), - VPackSlice.class); - assertThat(slice, is(notNullValue())); - assertThat(slice.get("alt-test").isString(), is(true)); - assertThat(slice.get("alt-test").getAsString(), is(entity.test)); - } - - public static class SingleNestedDocumentTestEntity extends BasicTestEntity { - private NestedDocumentTestEntity entity; - } - - public static class NestedDocumentTestEntity { - private String test; - - public NestedDocumentTestEntity() { - super(); - } - - public NestedDocumentTestEntity(final String test) { - super(); - this.test = test; - } - } - - @Test - public void singleNestedDocument() { - final SingleNestedDocumentTestEntity entity = new SingleNestedDocumentTestEntity(); - entity.entity = new NestedDocumentTestEntity("test"); - template.insert(entity); - final SingleNestedDocumentTestEntity document = template.find(entity.id, SingleNestedDocumentTestEntity.class) - .get(); - assertThat(document, is(notNullValue())); - assertThat(document.entity, is(notNullValue())); - assertThat(document.entity.test, is("test")); - } - - public static class MultipleNestedDocumentTestEntity extends BasicTestEntity { - private Collection entities; - } - - @Test - public void multipleNestedDocuments() { - final MultipleNestedDocumentTestEntity entity = new MultipleNestedDocumentTestEntity(); - entity.entities = new ArrayList<>(Arrays.asList(new NestedDocumentTestEntity("0"), - new NestedDocumentTestEntity("1"), new NestedDocumentTestEntity("2"))); - template.insert(entity); - final MultipleNestedDocumentTestEntity document = template - .find(entity.id, MultipleNestedDocumentTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(3)); - assertThat(document.entities.stream().map(e -> e.test).collect(Collectors.toList()), hasItems("0", "1", "2")); - } - - public static class MultipleNestedCollectionsTestEntity extends BasicTestEntity { - private Collection> entities; - } - - @Test - public void multipleNestedCollections() { - final MultipleNestedCollectionsTestEntity entity = new MultipleNestedCollectionsTestEntity(); - entity.entities = new ArrayList<>(Arrays.asList( - new ArrayList<>(Arrays.asList(new NestedDocumentTestEntity("00"), new NestedDocumentTestEntity("01"), - new NestedDocumentTestEntity("02"))), - new ArrayList<>(Arrays.asList(new NestedDocumentTestEntity("10"), new NestedDocumentTestEntity("11"), - new NestedDocumentTestEntity("12"))))); - template.insert(entity); - final MultipleNestedCollectionsTestEntity document = template - .find(entity.id, MultipleNestedCollectionsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(2)); - final List> collect = document.entities.stream() - .map(c -> c.stream().map(cc -> cc.test).collect(Collectors.toList())).collect(Collectors.toList()); - for (int i = 0; i < collect.size(); i++) { - for (int j = 0; j < collect.get(i).size(); j++) { - assertThat(collect.get(i).get(j), is(i + "" + j)); - } - } - } - - public static class SingleNestedMapTestEntity extends BasicTestEntity { - private Map entities; - } - - @Test - public void singleNestedMap() { - final SingleNestedMapTestEntity entity = new SingleNestedMapTestEntity(); - entity.entities = new HashMap<>(); - entity.entities.put("0", new NestedDocumentTestEntity("0")); - entity.entities.put("1", new NestedDocumentTestEntity("1")); - entity.entities.put("2", new NestedDocumentTestEntity("2")); - template.insert(entity); - final SingleNestedMapTestEntity document = template.find(entity.id, SingleNestedMapTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - final Map collect = document.entities.entrySet().stream() - .map(e -> new String[] { e.getKey(), e.getValue().test }) - .collect(Collectors.toMap(k -> k[0], v -> v[1])); - for (int i = 0; i <= 2; i++) { - assertThat(collect.get(String.valueOf(i)), is(String.valueOf(i))); - } - } - - public static class MultipleNestedMapTestEntity extends BasicTestEntity { - private Map> entities; - } - - @Test - public void multipleNestedMaps() { - final MultipleNestedMapTestEntity entity = new MultipleNestedMapTestEntity(); - entity.entities = new HashMap<>(); - final Map m0 = new HashMap<>(); - m0.put("0", new NestedDocumentTestEntity("00")); - m0.put("1", new NestedDocumentTestEntity("01")); - m0.put("2", new NestedDocumentTestEntity("02")); - entity.entities.put("0", m0); - final Map m1 = new HashMap<>(); - m1.put("0", new NestedDocumentTestEntity("10")); - m1.put("1", new NestedDocumentTestEntity("11")); - m1.put("2", new NestedDocumentTestEntity("12")); - entity.entities.put("1", m1); - template.insert(entity); - final MultipleNestedMapTestEntity document = template.find(entity.id, MultipleNestedMapTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - for (int i = 0; i <= 1; i++) { - for (int j = 0; j <= 2; j++) { - assertThat(document.entities.get(String.valueOf(i)).get(String.valueOf(j)).test, is(i + "" + j)); - } - } - } - - public static class SingleReferenceTestEntity extends BasicTestEntity { - @Ref - private BasicTestEntity entity; - } - - @Test - public void singleRef() { - final BasicTestEntity e1 = new BasicTestEntity(); - template.insert(e1); - final SingleReferenceTestEntity e0 = new SingleReferenceTestEntity(); - e0.entity = e1; - template.insert(e0); - final SingleReferenceTestEntity document = template.find(e0.id, SingleReferenceTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entity, is(notNullValue())); - assertThat(document.entity.id, is(e1.id)); - } - - public static class SingleReferenceLazyTestEntity extends BasicTestEntity { - @Ref(lazy = true) - private BasicTestEntity entity; - } - - @Test - public void singleRefLazy() { - final BasicTestEntity e1 = new BasicTestEntity(); - template.insert(e1); - final SingleReferenceLazyTestEntity e0 = new SingleReferenceLazyTestEntity(); - e0.entity = e1; - template.insert(e0); - final SingleReferenceLazyTestEntity document = template.find(e0.id, SingleReferenceLazyTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entity, is(notNullValue())); - assertThat(document.entity, instanceOf(BasicTestEntity.class)); - assertThat(document.entity.getId(), is(e1.getId())); - } - - public static class MultiReferenceTestEntity extends BasicTestEntity { - @Ref - private Collection entities; - } - - @Test - public void multiRef() { - final BasicTestEntity e1 = new BasicTestEntity(); - template.insert(e1); - final BasicTestEntity e2 = new BasicTestEntity(); - template.insert(e2); - final MultiReferenceTestEntity e0 = new MultiReferenceTestEntity(); - e0.entities = Arrays.asList(e1, e2); - template.insert(e0); - final MultiReferenceTestEntity document = template.find(e0.id, MultiReferenceTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(2)); - for (final BasicTestEntity e : document.entities) { - assertThat(e, instanceOf(BasicTestEntity.class)); - assertThat(e.getId(), is(notNullValue())); - assertThat(e.getId(), is(isOneOf(e1.getId(), e2.getId()))); - } - } - - public static class MultiReferenceLazyTestEntity extends BasicTestEntity { - @Ref(lazy = true) - private Collection entities; - } - - @Test - public void multiRefLazy() { - final BasicTestEntity e1 = new BasicTestEntity(); - template.insert(e1); - final BasicTestEntity e2 = new BasicTestEntity(); - template.insert(e2); - final MultiReferenceLazyTestEntity e0 = new MultiReferenceLazyTestEntity(); - e0.entities = Arrays.asList(e1, e2); - template.insert(e0); - final MultiReferenceLazyTestEntity document = template.find(e0.id, MultiReferenceLazyTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(2)); - for (final BasicTestEntity e : document.entities) { - assertThat(e, instanceOf(BasicTestEntity.class)); - assertThat(e.getId(), is(notNullValue())); - assertThat(e.getId(), is(isOneOf(e1.getId(), e2.getId()))); - } - } - - public static class NestedReferenceTestEntity extends BasicTestEntity { - private NestedReferenceSubTestEntity sub; - } - - public static class NestedReferenceSubTestEntity { - @Ref - private Collection entities; - } - - @Test - public void testNestedRef() { - final NestedReferenceTestEntity o = new NestedReferenceTestEntity(); - o.sub = new NestedReferenceSubTestEntity(); - o.sub.entities = new ArrayList<>(); - final BasicTestEntity e = new BasicTestEntity(); - o.sub.entities.add(e); - template.insert(e); - template.insert(o); - final NestedReferenceTestEntity document = template.find(o.id, NestedReferenceTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.sub, is(notNullValue())); - assertThat(document.sub.entities, is(notNullValue())); - assertThat(document.sub.entities.size(), is(1)); - assertThat(document.sub.entities.iterator().next().id, is(e.id)); - } - - @Edge - public static class BasicEdgeTestEntity extends BasicTestEntity { - @From - BasicTestEntity from; - @To - BasicTestEntity to; - - public BasicEdgeTestEntity() { - super(); - } - - public BasicEdgeTestEntity(final BasicTestEntity from, final BasicTestEntity to) { - super(); - this.from = from; - this.to = to; - } - - public BasicTestEntity getFrom() { - return from; - } - - public void setFrom(final BasicTestEntity from) { - this.from = from; - } - - public BasicTestEntity getTo() { - return to; - } - - public void setTo(final BasicTestEntity to) { - this.to = to; - } - - } - - @Test - public void edgeFromTo() { - final BasicTestEntity e1 = new BasicTestEntity(); - template.insert(e1); - final BasicTestEntity e2 = new BasicTestEntity(); - template.insert(e2); - final BasicEdgeTestEntity e0 = new BasicEdgeTestEntity(e1, e2); - template.insert(e0); - final BasicEdgeTestEntity document = template.find(e0.id, BasicEdgeTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.getFrom(), is(notNullValue())); - assertThat(document.getFrom().getId(), is(e1.getId())); - assertThat(document.getTo(), is(notNullValue())); - assertThat(document.getTo().getId(), is(e2.getId())); - } - - @Edge - public static class BasicEdgeLazyTestEntity extends BasicTestEntity { - @From(lazy = true) - BasicTestEntity from; - @To(lazy = true) - BasicTestEntity to; - - public BasicEdgeLazyTestEntity() { - super(); - } - - public BasicEdgeLazyTestEntity(final BasicTestEntity from, final BasicTestEntity to) { - super(); - this.from = from; - this.to = to; - } - - public BasicTestEntity getFrom() { - return from; - } - - public void setFrom(final BasicTestEntity from) { - this.from = from; - } - - public BasicTestEntity getTo() { - return to; - } - - public void setTo(final BasicTestEntity to) { - this.to = to; - } - - } - - @Test - public void edgeFromToLazy() { - final BasicTestEntity e1 = new BasicTestEntity(); - template.insert(e1); - final BasicTestEntity e2 = new BasicTestEntity(); - template.insert(e2); - final BasicEdgeLazyTestEntity e0 = new BasicEdgeLazyTestEntity(e1, e2); - template.insert(e0); - final BasicEdgeLazyTestEntity document = template.find(e0.id, BasicEdgeLazyTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.getFrom(), is(notNullValue())); - assertThat(document.getFrom().getId(), is(e1.getId())); - assertThat(document.getTo(), is(notNullValue())); - assertThat(document.getTo().getId(), is(e2.getId())); - } - - public static class DocumentFromTestEntity extends BasicTestEntity { - @From - private Collection entities; - } - - @Test - public void documentFrom() { - final DocumentFromTestEntity e0 = new DocumentFromTestEntity(); - template.insert(e0); - final DocumentFromTestEntity e1 = new DocumentFromTestEntity(); - template.insert(e1); - final BasicEdgeLazyTestEntity edge0 = new BasicEdgeLazyTestEntity(e0, e1); - template.insert(edge0); - final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(e0, e1); - template.insert(edge1); - final DocumentFromTestEntity document = template.find(e0.id, DocumentFromTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(2)); - for (final BasicEdgeLazyTestEntity e : document.entities) { - assertThat(e, instanceOf(BasicEdgeLazyTestEntity.class)); - assertThat(e.getId(), is(notNullValue())); - assertThat(e.getId(), is(isOneOf(edge0.getId(), edge1.getId()))); - assertThat(e.getFrom(), is(notNullValue())); - assertThat(e.getFrom().getId(), is(notNullValue())); - assertThat(e.getFrom().getId(), is(e0.getId())); - } - } - - public static class DocumentFromLazyTestEntity extends BasicTestEntity { - @From(lazy = true) - private Collection entities; - } - - @Test - public void documentFromLazy() { - final DocumentFromLazyTestEntity e0 = new DocumentFromLazyTestEntity(); - template.insert(e0); - final DocumentFromLazyTestEntity e1 = new DocumentFromLazyTestEntity(); - template.insert(e1); - final BasicEdgeLazyTestEntity edge0 = new BasicEdgeLazyTestEntity(e0, e1); - template.insert(edge0); - final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(e0, e1); - template.insert(edge1); - final DocumentFromLazyTestEntity document = template.find(e0.id, DocumentFromLazyTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(2)); - for (final BasicEdgeLazyTestEntity e : document.entities) { - assertThat(e, instanceOf(BasicEdgeLazyTestEntity.class)); - assertThat(e.getId(), is(notNullValue())); - assertThat(e.getId(), is(isOneOf(edge0.getId(), edge1.getId()))); - assertThat(e.getFrom(), is(notNullValue())); - assertThat(e.getFrom().getId(), is(notNullValue())); - assertThat(e.getFrom().getId(), is(e0.getId())); - } - } - - public static class DocumentToTestEntity extends BasicTestEntity { - @To - private Collection entities; - } - - @Test - public void documentTo() { - final DocumentToTestEntity e0 = new DocumentToTestEntity(); - template.insert(e0); - final DocumentToTestEntity e1 = new DocumentToTestEntity(); - template.insert(e1); - final BasicEdgeLazyTestEntity edge0 = new BasicEdgeLazyTestEntity(e1, e0); - template.insert(edge0); - final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(e1, e0); - template.insert(edge1); - final DocumentToTestEntity document = template.find(e0.id, DocumentToTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(2)); - for (final BasicEdgeLazyTestEntity e : document.entities) { - assertThat(e, instanceOf(BasicEdgeLazyTestEntity.class)); - assertThat(e.getId(), is(notNullValue())); - assertThat(e.getId(), is(isOneOf(edge0.getId(), edge1.getId()))); - assertThat(e.getTo(), is(notNullValue())); - assertThat(e.getTo().getId(), is(notNullValue())); - assertThat(e.getTo().getId(), is(e0.getId())); - } - } - - public static class DocumentToLazyTestEntity extends BasicTestEntity { - @To(lazy = true) - private Collection entities; - } - - @Test - public void documentToLazy() { - final DocumentToLazyTestEntity e0 = new DocumentToLazyTestEntity(); - template.insert(e0); - final DocumentToLazyTestEntity e1 = new DocumentToLazyTestEntity(); - template.insert(e1); - final BasicEdgeLazyTestEntity edge0 = new BasicEdgeLazyTestEntity(e1, e0); - template.insert(edge0); - final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(e1, e0); - template.insert(edge1); - final DocumentToLazyTestEntity document = template.find(e0.id, DocumentToLazyTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(2)); - for (final BasicEdgeLazyTestEntity e : document.entities) { - assertThat(e, instanceOf(BasicEdgeLazyTestEntity.class)); - assertThat(e.getId(), is(notNullValue())); - assertThat(e.getId(), is(isOneOf(edge0.getId(), edge1.getId()))); - assertThat(e.getTo(), is(notNullValue())); - assertThat(e.getTo().getId(), is(notNullValue())); - assertThat(e.getTo().getId(), is(e0.getId())); - } - } - - public static class RelationsTestEntity extends BasicTestEntity { - @Relations(edges = BasicEdgeTestEntity.class) - private Collection entities; - } - - @Test - public void relations() { - final BasicTestEntity e1 = new BasicTestEntity(); - template.insert(e1); - final BasicTestEntity e2 = new BasicTestEntity(); - template.insert(e2); - final RelationsTestEntity e0 = new RelationsTestEntity(); - template.insert(e0); - template.insert(new BasicEdgeTestEntity(e0, e1)); - template.insert(new BasicEdgeTestEntity(e0, e2)); - - final RelationsTestEntity document = template.find(e0.id, RelationsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(2)); - for (final BasicTestEntity e : document.entities) { - assertThat(e, instanceOf(BasicTestEntity.class)); - assertThat(e.getId(), is(notNullValue())); - assertThat(e.getId(), is(isOneOf(e1.getId(), e2.getId()))); - } - } - - public static class RelationsLazyTestEntity extends BasicTestEntity { - @Relations(edges = BasicEdgeTestEntity.class, lazy = true) - private Collection entities; - } - - @Test - public void relationsLazy() { - final BasicTestEntity e1 = new BasicTestEntity(); - template.insert(e1); - final BasicTestEntity e2 = new BasicTestEntity(); - template.insert(e2); - final RelationsTestEntity e0 = new RelationsTestEntity(); - template.insert(e0); - template.insert(new BasicEdgeTestEntity(e0, e1)); - template.insert(new BasicEdgeTestEntity(e0, e2)); - - final RelationsLazyTestEntity document = template.find(e0.id, RelationsLazyTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.entities, is(notNullValue())); - assertThat(document.entities.size(), is(2)); - for (final BasicTestEntity e : document.entities) { - assertThat(e, instanceOf(BasicTestEntity.class)); - assertThat(e.getId(), is(notNullValue())); - assertThat(e.getId(), is(isOneOf(e1.getId(), e2.getId()))); - } - } - - public static class ConstructorWithParamTestEntity extends BasicTestEntity { - private final String value; - - public ConstructorWithParamTestEntity(final String value) { - super(); - this.value = value; - } - } - - @Test - public void constructorWithParam() { - final ConstructorWithParamTestEntity entity = new ConstructorWithParamTestEntity("test"); - template.insert(entity); - final ConstructorWithParamTestEntity document = template - .find(entity.getId(), ConstructorWithParamTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value, is(entity.value)); - } - - public static class ConstructorWithMultipleParamsTestEntity extends BasicTestEntity { - private final String value1; - private final boolean value2; - private final double value3; - private final long value4; - private final int value5; - private final String[] value6; - - public ConstructorWithMultipleParamsTestEntity(final String value1, final boolean value2, final double value3, - final long value4, final int value5, final String[] value6) { - super(); - this.value1 = value1; - this.value2 = value2; - this.value3 = value3; - this.value4 = value4; - this.value5 = value5; - this.value6 = value6; - } - - } - - @Test - public void constructorWithMultipleParams() { - final ConstructorWithMultipleParamsTestEntity entity = new ConstructorWithMultipleParamsTestEntity("test", true, - 3.5, 13L, 69, new String[] { "a", "b" }); - template.insert(entity); - final ConstructorWithMultipleParamsTestEntity document = template - .find(entity.getId(), ConstructorWithMultipleParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value1, is(entity.value1)); - assertThat(document.value2, is(entity.value2)); - assertThat(document.value3, is(entity.value3)); - assertThat(document.value4, is(entity.value4)); - assertThat(document.value5, is(entity.value5)); - assertThat(document.value6, is(entity.value6)); - } - - public static class ConstructorWithRefParamsTestEntity extends BasicTestEntity { - @Ref - private final BasicTestEntity value1; - @Ref - private final Collection value2; - - public ConstructorWithRefParamsTestEntity(final BasicTestEntity value1, - final Collection value2) { - super(); - this.value1 = value1; - this.value2 = value2; - } - } - - @Test - public void constructorWithRefParams() { - final BasicTestEntity value1 = new BasicTestEntity(); - final BasicTestEntity value2 = new BasicTestEntity(); - final BasicTestEntity value3 = new BasicTestEntity(); - template.insert(value1); - template.insert(value2); - template.insert(value3); - final ConstructorWithRefParamsTestEntity entity = new ConstructorWithRefParamsTestEntity(value1, - Arrays.asList(value2, value3)); - template.insert(entity); - final ConstructorWithRefParamsTestEntity document = template - .find(entity.id, ConstructorWithRefParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value1.id, is(value1.id)); - assertThat(document.value2.size(), is(2)); - assertThat(document.value2.stream().map((e) -> e.id).collect(Collectors.toList()), - hasItems(value2.id, value3.id)); - } - - public static class ConstructorWithRefLazyParamsTestEntity extends BasicTestEntity { - @Ref(lazy = true) - private final BasicTestEntity value1; - @Ref(lazy = true) - private final Collection value2; - - public ConstructorWithRefLazyParamsTestEntity(final BasicTestEntity value1, - final Collection value2) { - super(); - this.value1 = value1; - this.value2 = value2; - } - } - - @Test - public void constructorWithRefLazyParams() { - final BasicTestEntity value1 = new BasicTestEntity(); - final BasicTestEntity value2 = new BasicTestEntity(); - final BasicTestEntity value3 = new BasicTestEntity(); - template.insert(value1); - template.insert(value2); - template.insert(value3); - final ConstructorWithRefLazyParamsTestEntity entity = new ConstructorWithRefLazyParamsTestEntity(value1, - Arrays.asList(value2, value3)); - template.insert(entity); - final ConstructorWithRefLazyParamsTestEntity document = template - .find(entity.id, ConstructorWithRefLazyParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value1.getId(), is(value1.id)); - assertThat(document.value2.size(), is(2)); - assertThat(document.value2.stream().map((e) -> e.getId()).collect(Collectors.toList()), - hasItems(value2.id, value3.id)); - } - - public static class ConstructorWithRelationsParamsTestEntity extends BasicTestEntity { - @Relations(edges = BasicEdgeTestEntity.class) - private final Collection value; - - public ConstructorWithRelationsParamsTestEntity(final Collection value) { - super(); - this.value = value; - } - } - - @Test - public void constructorWithRelationsParams() { - final BasicTestEntity vertex1 = new BasicTestEntity(); - final BasicTestEntity vertex2 = new BasicTestEntity(); - template.insert(vertex1); - template.insert(vertex2); - final ConstructorWithRelationsParamsTestEntity entity = new ConstructorWithRelationsParamsTestEntity( - Arrays.asList(vertex1, vertex2)); - template.insert(entity); - template.insert(new BasicEdgeTestEntity(entity, vertex1)); - template.insert(new BasicEdgeTestEntity(entity, vertex2)); - final ConstructorWithRelationsParamsTestEntity document = template - .find(entity.id, ConstructorWithRelationsParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value.stream().map((e) -> e.id).collect(Collectors.toList()), - hasItems(vertex1.id, vertex2.id)); - } - - public static class ConstructorWithRelationsLazyParamsTestEntity extends BasicTestEntity { - @Relations(edges = BasicEdgeTestEntity.class, lazy = true) - private final Collection value; - - public ConstructorWithRelationsLazyParamsTestEntity(final Collection value) { - super(); - this.value = value; - } - } - - @Test - public void constructorWithRelationsLazyParams() { - final BasicTestEntity vertex1 = new BasicTestEntity(); - final BasicTestEntity vertex2 = new BasicTestEntity(); - template.insert(vertex1); - template.insert(vertex2); - final ConstructorWithRelationsLazyParamsTestEntity entity = new ConstructorWithRelationsLazyParamsTestEntity( - Arrays.asList(vertex1, vertex2)); - template.insert(entity); - template.insert(new BasicEdgeTestEntity(entity, vertex1)); - template.insert(new BasicEdgeTestEntity(entity, vertex2)); - final ConstructorWithRelationsLazyParamsTestEntity document = template - .find(entity.id, ConstructorWithRelationsLazyParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value.stream().map((e) -> e.id).collect(Collectors.toList()), - hasItems(vertex1.id, vertex2.id)); - } - - public static class ConstructorWithFromParamsTestEntity extends BasicTestEntity { - @From - private final Collection value; - - public ConstructorWithFromParamsTestEntity(final Collection value) { - super(); - this.value = value; - } - } - - @Test - public void constructorWithFromParams() { - final ConstructorWithFromParamsTestEntity entity = new ConstructorWithFromParamsTestEntity(null); - template.insert(entity); - final BasicTestEntity to = new BasicTestEntity(); - template.insert(to); - final BasicEdgeTestEntity edge1 = new BasicEdgeTestEntity(entity, to); - final BasicEdgeTestEntity edge2 = new BasicEdgeTestEntity(entity, to); - template.insert(edge1); - template.insert(edge2); - final ConstructorWithFromParamsTestEntity document = template - .find(entity.id, ConstructorWithFromParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value.stream().map((e) -> e.id).collect(Collectors.toList()), hasItems(edge1.id, edge2.id)); - } - - public static class ConstructorWithFromLazyParamsTestEntity extends BasicTestEntity { - @From(lazy = true) - private final Collection value; - - public ConstructorWithFromLazyParamsTestEntity(final Collection value) { - super(); - this.value = value; - } - } - - @Test - public void constructorWithFromLazyParams() { - final ConstructorWithFromLazyParamsTestEntity entity = new ConstructorWithFromLazyParamsTestEntity(null); - template.insert(entity); - final BasicTestEntity to = new BasicTestEntity(); - template.insert(to); - final BasicEdgeTestEntity edge1 = new BasicEdgeTestEntity(entity, to); - final BasicEdgeTestEntity edge2 = new BasicEdgeTestEntity(entity, to); - template.insert(edge1); - template.insert(edge2); - final ConstructorWithFromLazyParamsTestEntity document = template - .find(entity.id, ConstructorWithFromLazyParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value.stream().map((e) -> e.getId()).collect(Collectors.toList()), - hasItems(edge1.id, edge2.id)); - } - - public static class ConstructorWithToParamsTestEntity extends BasicTestEntity { - @To - private final Collection value; - - public ConstructorWithToParamsTestEntity(final Collection value) { - super(); - this.value = value; - } - } - - @Test - public void constructorWithToParams() { - final ConstructorWithToParamsTestEntity entity = new ConstructorWithToParamsTestEntity(null); - template.insert(entity); - final BasicTestEntity from = new BasicTestEntity(); - template.insert(from); - final BasicEdgeTestEntity edge1 = new BasicEdgeTestEntity(from, entity); - final BasicEdgeTestEntity edge2 = new BasicEdgeTestEntity(from, entity); - template.insert(edge1); - template.insert(edge2); - final ConstructorWithToParamsTestEntity document = template - .find(entity.id, ConstructorWithToParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value.stream().map((e) -> e.id).collect(Collectors.toList()), hasItems(edge1.id, edge2.id)); - } - - public static class ConstructorWithToLazyParamsTestEntity extends BasicTestEntity { - @To(lazy = true) - private final Collection value; - - public ConstructorWithToLazyParamsTestEntity(final Collection value) { - super(); - this.value = value; - } - } - - @Test - public void constructorWithToLazyParams() { - final ConstructorWithToLazyParamsTestEntity entity = new ConstructorWithToLazyParamsTestEntity(null); - template.insert(entity); - final BasicTestEntity from = new BasicTestEntity(); - template.insert(from); - final BasicEdgeTestEntity edge1 = new BasicEdgeTestEntity(from, entity); - final BasicEdgeTestEntity edge2 = new BasicEdgeTestEntity(from, entity); - template.insert(edge1); - template.insert(edge2); - final ConstructorWithToLazyParamsTestEntity document = template - .find(entity.id, ConstructorWithToLazyParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value.stream().map((e) -> e.getId()).collect(Collectors.toList()), - hasItems(edge1.id, edge2.id)); - } - - public static class EdgeConstructorWithFromToParamsTestEntity extends BasicEdgeTestEntity { - @From - private final BasicTestEntity from; - @To - private final BasicTestEntity to; - - public EdgeConstructorWithFromToParamsTestEntity(final BasicTestEntity from, final BasicTestEntity to) { - super(); - this.from = from; - this.to = to; - } - } - - @Test - public void edgeConstructorWithFromToParams() { - final BasicTestEntity from = new BasicTestEntity(); - final BasicTestEntity to = new BasicTestEntity(); - template.insert(from); - template.insert(to); - final EdgeConstructorWithFromToParamsTestEntity edge = new EdgeConstructorWithFromToParamsTestEntity(from, to); - template.insert(edge); - final EdgeConstructorWithFromToParamsTestEntity document = template - .find(edge.id, EdgeConstructorWithFromToParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.from.id, is(from.id)); - assertThat(document.to.id, is(to.id)); - } - - public static class EdgeConstructorWithFromToLazyParamsTestEntity extends BasicEdgeTestEntity { - @From - private final BasicTestEntity from; - @To - private final BasicTestEntity to; - - public EdgeConstructorWithFromToLazyParamsTestEntity(final BasicTestEntity from, final BasicTestEntity to) { - super(); - this.from = from; - this.to = to; - } - } - - @Test - public void edgeConstructorWithFromToLazyParams() { - final BasicTestEntity from = new BasicTestEntity(); - final BasicTestEntity to = new BasicTestEntity(); - template.insert(from); - template.insert(to); - final EdgeConstructorWithFromToLazyParamsTestEntity edge = new EdgeConstructorWithFromToLazyParamsTestEntity( - from, to); - template.insert(edge); - final EdgeConstructorWithFromToLazyParamsTestEntity document = template - .find(edge.id, EdgeConstructorWithFromToLazyParamsTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.from.getId(), is(from.id)); - assertThat(document.to.getId(), is(to.id)); - } - - public static class JodaTestEntity extends BasicTestEntity { - private org.joda.time.DateTime value1; - private org.joda.time.Instant value2; - private org.joda.time.LocalDate value3; - private org.joda.time.LocalDateTime value4; - } - - @Test - public void jodaMapping() { - final JodaTestEntity entity = new JodaTestEntity(); - entity.value1 = org.joda.time.DateTime.now(); - entity.value2 = org.joda.time.Instant.now(); - entity.value3 = org.joda.time.LocalDate.now(); - entity.value4 = org.joda.time.LocalDateTime.now(); - template.insert(entity); - final JodaTestEntity document = template.find(entity.getId(), JodaTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value1, is(entity.value1)); - assertThat(document.value2, is(entity.value2)); - assertThat(document.value3, is(entity.value3)); - assertThat(document.value4, is(entity.value4)); - } - - public static class Java8TimeTestEntity extends BasicTestEntity { - private java.time.Instant value1; - private java.time.LocalDate value2; - private java.time.LocalDateTime value3; - } - - @Test - public void timeMapping() { - final Java8TimeTestEntity entity = new Java8TimeTestEntity(); - entity.value1 = java.time.Instant.now(); - entity.value2 = java.time.LocalDate.now(); - entity.value3 = java.time.LocalDateTime.now(); - template.insert(entity); - final Java8TimeTestEntity document = template.find(entity.getId(), Java8TimeTestEntity.class).get(); - assertThat(document, is(notNullValue())); - assertThat(document.value1, is(entity.value1)); - assertThat(document.value2, is(entity.value2)); - assertThat(document.value3, is(entity.value3)); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 ArangoDB GmbH, Cologne, Germany + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright holder is ArangoDB GmbH, Cologne, Germany + */ + +package com.arangodb.springframework.core.mapping; + +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isOneOf; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.joda.time.DateTimeZone; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.annotation.Id; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.arangodb.entity.DocumentEntity; +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.AbstractArangoTest; +import com.arangodb.springframework.ArangoTestConfiguration; +import com.arangodb.springframework.annotation.Document; +import com.arangodb.springframework.annotation.Edge; +import com.arangodb.springframework.annotation.Field; +import com.arangodb.springframework.annotation.From; +import com.arangodb.springframework.annotation.Key; +import com.arangodb.springframework.annotation.Ref; +import com.arangodb.springframework.annotation.Relations; +import com.arangodb.springframework.annotation.Rev; +import com.arangodb.springframework.annotation.To; +import com.arangodb.util.MapBuilder; +import com.arangodb.velocypack.VPackSlice; + +/** + * @author Mark Vollmary + * @author Christian Lechner + * + */ +@SuppressWarnings("deprecation") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { ArangoTestConfiguration.class }) +public class ArangoMappingTest extends AbstractArangoTest { + + @Document + public static class BasicTestEntity { + + @Id + String id; + @Key + String key; + @Rev + String rev; + + public BasicTestEntity() { + super(); + } + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(final String key) { + this.key = key; + } + + public String getRev() { + return rev; + } + + public void setRev(final String rev) { + this.rev = rev; + } + + } + + @Test + public void idKeyRev() { + final DocumentEntity ref = template.insert(new BasicTestEntity()); + final BasicTestEntity entity = template.find(ref.getId(), BasicTestEntity.class).get(); + assertThat(entity, is(notNullValue())); + assertThat(entity.getId(), is(ref.getId())); + assertThat(entity.getKey(), is(ref.getKey())); + assertThat(entity.getRev(), is(ref.getRev())); + } + + public static class OnlyIdTestEntity { + @Id + private String id; + } + + @Test + public void supplementKey() { + final OnlyIdTestEntity value = new OnlyIdTestEntity(); + template.insert(value); + final List result = template.query("RETURN @doc", new MapBuilder().put("doc", value).get(), + new AqlQueryOptions(), BasicTestEntity.class).asListRemaining(); + assertThat(result.size(), is(1)); + assertThat(result.get(0).getId(), is(value.id)); + assertThat(result.get(0).getKey(), is(value.id.split("/")[1])); + assertThat(result.get(0).getRev(), is(nullValue())); + } + + public static class FieldNameTestEntity extends BasicTestEntity { + @Field("alt-test") + private String test; + } + + @Test + public void fieldNameAnnotation() { + final FieldNameTestEntity entity = new FieldNameTestEntity(); + entity.test = "1234"; + final DocumentEntity res = template.insert(entity); + final VPackSlice slice = template.driver().db(ArangoTestConfiguration.DB).getDocument(res.getId(), + VPackSlice.class); + assertThat(slice, is(notNullValue())); + assertThat(slice.get("alt-test").isString(), is(true)); + assertThat(slice.get("alt-test").getAsString(), is(entity.test)); + } + + public static class SingleNestedDocumentTestEntity extends BasicTestEntity { + private NestedDocumentTestEntity entity; + } + + public static class NestedDocumentTestEntity { + private String test; + + public NestedDocumentTestEntity() { + super(); + } + + public NestedDocumentTestEntity(final String test) { + super(); + this.test = test; + } + } + + @Test + public void singleNestedDocument() { + final SingleNestedDocumentTestEntity entity = new SingleNestedDocumentTestEntity(); + entity.entity = new NestedDocumentTestEntity("test"); + template.insert(entity); + final SingleNestedDocumentTestEntity document = template.find(entity.id, SingleNestedDocumentTestEntity.class) + .get(); + assertThat(document, is(notNullValue())); + assertThat(document.entity, is(notNullValue())); + assertThat(document.entity.test, is("test")); + } + + public static class MultipleNestedDocumentTestEntity extends BasicTestEntity { + private Collection entities; + } + + @Test + public void multipleNestedDocuments() { + final MultipleNestedDocumentTestEntity entity = new MultipleNestedDocumentTestEntity(); + entity.entities = new ArrayList<>(Arrays.asList(new NestedDocumentTestEntity("0"), + new NestedDocumentTestEntity("1"), new NestedDocumentTestEntity("2"))); + template.insert(entity); + final MultipleNestedDocumentTestEntity document = template + .find(entity.id, MultipleNestedDocumentTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(3)); + assertThat(document.entities.stream().map(e -> e.test).collect(Collectors.toList()), hasItems("0", "1", "2")); + } + + public static class MultipleNestedCollectionsTestEntity extends BasicTestEntity { + private Collection> entities; + } + + @Test + public void multipleNestedCollections() { + final MultipleNestedCollectionsTestEntity entity = new MultipleNestedCollectionsTestEntity(); + entity.entities = new ArrayList<>(Arrays.asList( + new ArrayList<>(Arrays.asList(new NestedDocumentTestEntity("00"), new NestedDocumentTestEntity("01"), + new NestedDocumentTestEntity("02"))), + new ArrayList<>(Arrays.asList(new NestedDocumentTestEntity("10"), new NestedDocumentTestEntity("11"), + new NestedDocumentTestEntity("12"))))); + template.insert(entity); + final MultipleNestedCollectionsTestEntity document = template + .find(entity.id, MultipleNestedCollectionsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(2)); + final List> collect = document.entities.stream() + .map(c -> c.stream().map(cc -> cc.test).collect(Collectors.toList())).collect(Collectors.toList()); + for (int i = 0; i < collect.size(); i++) { + for (int j = 0; j < collect.get(i).size(); j++) { + assertThat(collect.get(i).get(j), is(i + "" + j)); + } + } + } + + public static class SingleNestedMapTestEntity extends BasicTestEntity { + private Map entities; + } + + @Test + public void singleNestedMap() { + final SingleNestedMapTestEntity entity = new SingleNestedMapTestEntity(); + entity.entities = new HashMap<>(); + entity.entities.put("0", new NestedDocumentTestEntity("0")); + entity.entities.put("1", new NestedDocumentTestEntity("1")); + entity.entities.put("2", new NestedDocumentTestEntity("2")); + template.insert(entity); + final SingleNestedMapTestEntity document = template.find(entity.id, SingleNestedMapTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + final Map collect = document.entities.entrySet().stream() + .map(e -> new String[] { e.getKey(), e.getValue().test }) + .collect(Collectors.toMap(k -> k[0], v -> v[1])); + for (int i = 0; i <= 2; i++) { + assertThat(collect.get(String.valueOf(i)), is(String.valueOf(i))); + } + } + + public static class MultipleNestedMapTestEntity extends BasicTestEntity { + private Map> entities; + } + + @Test + public void multipleNestedMaps() { + final MultipleNestedMapTestEntity entity = new MultipleNestedMapTestEntity(); + entity.entities = new HashMap<>(); + final Map m0 = new HashMap<>(); + m0.put("0", new NestedDocumentTestEntity("00")); + m0.put("1", new NestedDocumentTestEntity("01")); + m0.put("2", new NestedDocumentTestEntity("02")); + entity.entities.put("0", m0); + final Map m1 = new HashMap<>(); + m1.put("0", new NestedDocumentTestEntity("10")); + m1.put("1", new NestedDocumentTestEntity("11")); + m1.put("2", new NestedDocumentTestEntity("12")); + entity.entities.put("1", m1); + template.insert(entity); + final MultipleNestedMapTestEntity document = template.find(entity.id, MultipleNestedMapTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + for (int i = 0; i <= 1; i++) { + for (int j = 0; j <= 2; j++) { + assertThat(document.entities.get(String.valueOf(i)).get(String.valueOf(j)).test, is(i + "" + j)); + } + } + } + + public static class SingleReferenceTestEntity extends BasicTestEntity { + @Ref + private BasicTestEntity entity; + } + + @Test + public void singleRef() { + final BasicTestEntity e1 = new BasicTestEntity(); + template.insert(e1); + final SingleReferenceTestEntity e0 = new SingleReferenceTestEntity(); + e0.entity = e1; + template.insert(e0); + final SingleReferenceTestEntity document = template.find(e0.id, SingleReferenceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entity, is(notNullValue())); + assertThat(document.entity.id, is(e1.id)); + } + + public static class SingleReferenceLazyTestEntity extends BasicTestEntity { + @Ref(lazy = true) + private BasicTestEntity entity; + } + + @Test + public void singleRefLazy() { + final BasicTestEntity e1 = new BasicTestEntity(); + template.insert(e1); + final SingleReferenceLazyTestEntity e0 = new SingleReferenceLazyTestEntity(); + e0.entity = e1; + template.insert(e0); + final SingleReferenceLazyTestEntity document = template.find(e0.id, SingleReferenceLazyTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entity, is(notNullValue())); + assertThat(document.entity, instanceOf(BasicTestEntity.class)); + assertThat(document.entity.getId(), is(e1.getId())); + } + + public static class MultiReferenceTestEntity extends BasicTestEntity { + @Ref + private Collection entities; + } + + @Test + public void multiRef() { + final BasicTestEntity e1 = new BasicTestEntity(); + template.insert(e1); + final BasicTestEntity e2 = new BasicTestEntity(); + template.insert(e2); + final MultiReferenceTestEntity e0 = new MultiReferenceTestEntity(); + e0.entities = Arrays.asList(e1, e2); + template.insert(e0); + final MultiReferenceTestEntity document = template.find(e0.id, MultiReferenceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(2)); + for (final BasicTestEntity e : document.entities) { + assertThat(e, instanceOf(BasicTestEntity.class)); + assertThat(e.getId(), is(notNullValue())); + assertThat(e.getId(), is(isOneOf(e1.getId(), e2.getId()))); + } + } + + public static class MultiReferenceLazyTestEntity extends BasicTestEntity { + @Ref(lazy = true) + private Collection entities; + } + + @Test + public void multiRefLazy() { + final BasicTestEntity e1 = new BasicTestEntity(); + template.insert(e1); + final BasicTestEntity e2 = new BasicTestEntity(); + template.insert(e2); + final MultiReferenceLazyTestEntity e0 = new MultiReferenceLazyTestEntity(); + e0.entities = Arrays.asList(e1, e2); + template.insert(e0); + final MultiReferenceLazyTestEntity document = template.find(e0.id, MultiReferenceLazyTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(2)); + for (final BasicTestEntity e : document.entities) { + assertThat(e, instanceOf(BasicTestEntity.class)); + assertThat(e.getId(), is(notNullValue())); + assertThat(e.getId(), is(isOneOf(e1.getId(), e2.getId()))); + } + } + + public static class NestedReferenceTestEntity extends BasicTestEntity { + private NestedReferenceSubTestEntity sub; + } + + public static class NestedReferenceSubTestEntity { + @Ref + private Collection entities; + } + + @Test + public void testNestedRef() { + final NestedReferenceTestEntity o = new NestedReferenceTestEntity(); + o.sub = new NestedReferenceSubTestEntity(); + o.sub.entities = new ArrayList<>(); + final BasicTestEntity e = new BasicTestEntity(); + o.sub.entities.add(e); + template.insert(e); + template.insert(o); + final NestedReferenceTestEntity document = template.find(o.id, NestedReferenceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.sub, is(notNullValue())); + assertThat(document.sub.entities, is(notNullValue())); + assertThat(document.sub.entities.size(), is(1)); + assertThat(document.sub.entities.iterator().next().id, is(e.id)); + } + + @Edge + public static class BasicEdgeTestEntity extends BasicTestEntity { + @From + BasicTestEntity from; + @To + BasicTestEntity to; + + public BasicEdgeTestEntity() { + super(); + } + + public BasicEdgeTestEntity(final BasicTestEntity from, final BasicTestEntity to) { + super(); + this.from = from; + this.to = to; + } + + public BasicTestEntity getFrom() { + return from; + } + + public void setFrom(final BasicTestEntity from) { + this.from = from; + } + + public BasicTestEntity getTo() { + return to; + } + + public void setTo(final BasicTestEntity to) { + this.to = to; + } + + } + + @Test + public void edgeFromTo() { + final BasicTestEntity e1 = new BasicTestEntity(); + template.insert(e1); + final BasicTestEntity e2 = new BasicTestEntity(); + template.insert(e2); + final BasicEdgeTestEntity e0 = new BasicEdgeTestEntity(e1, e2); + template.insert(e0); + final BasicEdgeTestEntity document = template.find(e0.id, BasicEdgeTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.getFrom(), is(notNullValue())); + assertThat(document.getFrom().getId(), is(e1.getId())); + assertThat(document.getTo(), is(notNullValue())); + assertThat(document.getTo().getId(), is(e2.getId())); + } + + @Edge + public static class BasicEdgeLazyTestEntity extends BasicTestEntity { + @From(lazy = true) + BasicTestEntity from; + @To(lazy = true) + BasicTestEntity to; + + public BasicEdgeLazyTestEntity() { + super(); + } + + public BasicEdgeLazyTestEntity(final BasicTestEntity from, final BasicTestEntity to) { + super(); + this.from = from; + this.to = to; + } + + public BasicTestEntity getFrom() { + return from; + } + + public void setFrom(final BasicTestEntity from) { + this.from = from; + } + + public BasicTestEntity getTo() { + return to; + } + + public void setTo(final BasicTestEntity to) { + this.to = to; + } + + } + + @Test + public void edgeFromToLazy() { + final BasicTestEntity e1 = new BasicTestEntity(); + template.insert(e1); + final BasicTestEntity e2 = new BasicTestEntity(); + template.insert(e2); + final BasicEdgeLazyTestEntity e0 = new BasicEdgeLazyTestEntity(e1, e2); + template.insert(e0); + final BasicEdgeLazyTestEntity document = template.find(e0.id, BasicEdgeLazyTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.getFrom(), is(notNullValue())); + assertThat(document.getFrom().getId(), is(e1.getId())); + assertThat(document.getTo(), is(notNullValue())); + assertThat(document.getTo().getId(), is(e2.getId())); + } + + public static class DocumentFromTestEntity extends BasicTestEntity { + @From + private Collection entities; + } + + @Test + public void documentFrom() { + final DocumentFromTestEntity e0 = new DocumentFromTestEntity(); + template.insert(e0); + final DocumentFromTestEntity e1 = new DocumentFromTestEntity(); + template.insert(e1); + final BasicEdgeLazyTestEntity edge0 = new BasicEdgeLazyTestEntity(e0, e1); + template.insert(edge0); + final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(e0, e1); + template.insert(edge1); + final DocumentFromTestEntity document = template.find(e0.id, DocumentFromTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(2)); + for (final BasicEdgeLazyTestEntity e : document.entities) { + assertThat(e, instanceOf(BasicEdgeLazyTestEntity.class)); + assertThat(e.getId(), is(notNullValue())); + assertThat(e.getId(), is(isOneOf(edge0.getId(), edge1.getId()))); + assertThat(e.getFrom(), is(notNullValue())); + assertThat(e.getFrom().getId(), is(notNullValue())); + assertThat(e.getFrom().getId(), is(e0.getId())); + } + } + + public static class DocumentFromLazyTestEntity extends BasicTestEntity { + @From(lazy = true) + private Collection entities; + } + + @Test + public void documentFromLazy() { + final DocumentFromLazyTestEntity e0 = new DocumentFromLazyTestEntity(); + template.insert(e0); + final DocumentFromLazyTestEntity e1 = new DocumentFromLazyTestEntity(); + template.insert(e1); + final BasicEdgeLazyTestEntity edge0 = new BasicEdgeLazyTestEntity(e0, e1); + template.insert(edge0); + final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(e0, e1); + template.insert(edge1); + final DocumentFromLazyTestEntity document = template.find(e0.id, DocumentFromLazyTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(2)); + for (final BasicEdgeLazyTestEntity e : document.entities) { + assertThat(e, instanceOf(BasicEdgeLazyTestEntity.class)); + assertThat(e.getId(), is(notNullValue())); + assertThat(e.getId(), is(isOneOf(edge0.getId(), edge1.getId()))); + assertThat(e.getFrom(), is(notNullValue())); + assertThat(e.getFrom().getId(), is(notNullValue())); + assertThat(e.getFrom().getId(), is(e0.getId())); + } + } + + public static class DocumentToTestEntity extends BasicTestEntity { + @To + private Collection entities; + } + + @Test + public void documentTo() { + final DocumentToTestEntity e0 = new DocumentToTestEntity(); + template.insert(e0); + final DocumentToTestEntity e1 = new DocumentToTestEntity(); + template.insert(e1); + final BasicEdgeLazyTestEntity edge0 = new BasicEdgeLazyTestEntity(e1, e0); + template.insert(edge0); + final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(e1, e0); + template.insert(edge1); + final DocumentToTestEntity document = template.find(e0.id, DocumentToTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(2)); + for (final BasicEdgeLazyTestEntity e : document.entities) { + assertThat(e, instanceOf(BasicEdgeLazyTestEntity.class)); + assertThat(e.getId(), is(notNullValue())); + assertThat(e.getId(), is(isOneOf(edge0.getId(), edge1.getId()))); + assertThat(e.getTo(), is(notNullValue())); + assertThat(e.getTo().getId(), is(notNullValue())); + assertThat(e.getTo().getId(), is(e0.getId())); + } + } + + public static class DocumentToLazyTestEntity extends BasicTestEntity { + @To(lazy = true) + private Collection entities; + } + + @Test + public void documentToLazy() { + final DocumentToLazyTestEntity e0 = new DocumentToLazyTestEntity(); + template.insert(e0); + final DocumentToLazyTestEntity e1 = new DocumentToLazyTestEntity(); + template.insert(e1); + final BasicEdgeLazyTestEntity edge0 = new BasicEdgeLazyTestEntity(e1, e0); + template.insert(edge0); + final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(e1, e0); + template.insert(edge1); + final DocumentToLazyTestEntity document = template.find(e0.id, DocumentToLazyTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(2)); + for (final BasicEdgeLazyTestEntity e : document.entities) { + assertThat(e, instanceOf(BasicEdgeLazyTestEntity.class)); + assertThat(e.getId(), is(notNullValue())); + assertThat(e.getId(), is(isOneOf(edge0.getId(), edge1.getId()))); + assertThat(e.getTo(), is(notNullValue())); + assertThat(e.getTo().getId(), is(notNullValue())); + assertThat(e.getTo().getId(), is(e0.getId())); + } + } + + public static class RelationsTestEntity extends BasicTestEntity { + @Relations(edges = BasicEdgeTestEntity.class) + private Collection entities; + } + + @Test + public void relations() { + final BasicTestEntity e1 = new BasicTestEntity(); + template.insert(e1); + final BasicTestEntity e2 = new BasicTestEntity(); + template.insert(e2); + final RelationsTestEntity e0 = new RelationsTestEntity(); + template.insert(e0); + template.insert(new BasicEdgeTestEntity(e0, e1)); + template.insert(new BasicEdgeTestEntity(e0, e2)); + + final RelationsTestEntity document = template.find(e0.id, RelationsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(2)); + for (final BasicTestEntity e : document.entities) { + assertThat(e, instanceOf(BasicTestEntity.class)); + assertThat(e.getId(), is(notNullValue())); + assertThat(e.getId(), is(isOneOf(e1.getId(), e2.getId()))); + } + } + + public static class RelationsLazyTestEntity extends BasicTestEntity { + @Relations(edges = BasicEdgeTestEntity.class, lazy = true) + private Collection entities; + } + + @Test + public void relationsLazy() { + final BasicTestEntity e1 = new BasicTestEntity(); + template.insert(e1); + final BasicTestEntity e2 = new BasicTestEntity(); + template.insert(e2); + final RelationsTestEntity e0 = new RelationsTestEntity(); + template.insert(e0); + template.insert(new BasicEdgeTestEntity(e0, e1)); + template.insert(new BasicEdgeTestEntity(e0, e2)); + + final RelationsLazyTestEntity document = template.find(e0.id, RelationsLazyTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.entities, is(notNullValue())); + assertThat(document.entities.size(), is(2)); + for (final BasicTestEntity e : document.entities) { + assertThat(e, instanceOf(BasicTestEntity.class)); + assertThat(e.getId(), is(notNullValue())); + assertThat(e.getId(), is(isOneOf(e1.getId(), e2.getId()))); + } + } + + public static class ConstructorWithParamTestEntity extends BasicTestEntity { + private final String value; + + public ConstructorWithParamTestEntity(final String value) { + super(); + this.value = value; + } + } + + @Test + public void constructorWithParam() { + final ConstructorWithParamTestEntity entity = new ConstructorWithParamTestEntity("test"); + template.insert(entity); + final ConstructorWithParamTestEntity document = template + .find(entity.getId(), ConstructorWithParamTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(entity.value)); + } + + public static class ConstructorWithMultipleParamsTestEntity extends BasicTestEntity { + private final String value1; + private final boolean value2; + private final double value3; + private final long value4; + private final int value5; + private final String[] value6; + + public ConstructorWithMultipleParamsTestEntity(final String value1, final boolean value2, final double value3, + final long value4, final int value5, final String[] value6) { + super(); + this.value1 = value1; + this.value2 = value2; + this.value3 = value3; + this.value4 = value4; + this.value5 = value5; + this.value6 = value6; + } + + } + + @Test + public void constructorWithMultipleParams() { + final ConstructorWithMultipleParamsTestEntity entity = new ConstructorWithMultipleParamsTestEntity("test", true, + 3.5, 13L, 69, new String[] { "a", "b" }); + template.insert(entity); + final ConstructorWithMultipleParamsTestEntity document = template + .find(entity.getId(), ConstructorWithMultipleParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value1, is(entity.value1)); + assertThat(document.value2, is(entity.value2)); + assertThat(document.value3, is(entity.value3)); + assertThat(document.value4, is(entity.value4)); + assertThat(document.value5, is(entity.value5)); + assertThat(document.value6, is(entity.value6)); + } + + public static class ConstructorWithRefParamsTestEntity extends BasicTestEntity { + @Ref + private final BasicTestEntity value1; + @Ref + private final Collection value2; + + public ConstructorWithRefParamsTestEntity(final BasicTestEntity value1, + final Collection value2) { + super(); + this.value1 = value1; + this.value2 = value2; + } + } + + @Test + public void constructorWithRefParams() { + final BasicTestEntity value1 = new BasicTestEntity(); + final BasicTestEntity value2 = new BasicTestEntity(); + final BasicTestEntity value3 = new BasicTestEntity(); + template.insert(value1); + template.insert(value2); + template.insert(value3); + final ConstructorWithRefParamsTestEntity entity = new ConstructorWithRefParamsTestEntity(value1, + Arrays.asList(value2, value3)); + template.insert(entity); + final ConstructorWithRefParamsTestEntity document = template + .find(entity.id, ConstructorWithRefParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value1.id, is(value1.id)); + assertThat(document.value2.size(), is(2)); + assertThat(document.value2.stream().map((e) -> e.id).collect(Collectors.toList()), + hasItems(value2.id, value3.id)); + } + + public static class ConstructorWithRefLazyParamsTestEntity extends BasicTestEntity { + @Ref(lazy = true) + private final BasicTestEntity value1; + @Ref(lazy = true) + private final Collection value2; + + public ConstructorWithRefLazyParamsTestEntity(final BasicTestEntity value1, + final Collection value2) { + super(); + this.value1 = value1; + this.value2 = value2; + } + } + + @Test + public void constructorWithRefLazyParams() { + final BasicTestEntity value1 = new BasicTestEntity(); + final BasicTestEntity value2 = new BasicTestEntity(); + final BasicTestEntity value3 = new BasicTestEntity(); + template.insert(value1); + template.insert(value2); + template.insert(value3); + final ConstructorWithRefLazyParamsTestEntity entity = new ConstructorWithRefLazyParamsTestEntity(value1, + Arrays.asList(value2, value3)); + template.insert(entity); + final ConstructorWithRefLazyParamsTestEntity document = template + .find(entity.id, ConstructorWithRefLazyParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value1.getId(), is(value1.id)); + assertThat(document.value2.size(), is(2)); + assertThat(document.value2.stream().map((e) -> e.getId()).collect(Collectors.toList()), + hasItems(value2.id, value3.id)); + } + + public static class ConstructorWithRelationsParamsTestEntity extends BasicTestEntity { + @Relations(edges = BasicEdgeTestEntity.class) + private final Collection value; + + public ConstructorWithRelationsParamsTestEntity(final Collection value) { + super(); + this.value = value; + } + } + + @Test + public void constructorWithRelationsParams() { + final BasicTestEntity vertex1 = new BasicTestEntity(); + final BasicTestEntity vertex2 = new BasicTestEntity(); + template.insert(vertex1); + template.insert(vertex2); + final ConstructorWithRelationsParamsTestEntity entity = new ConstructorWithRelationsParamsTestEntity( + Arrays.asList(vertex1, vertex2)); + template.insert(entity); + template.insert(new BasicEdgeTestEntity(entity, vertex1)); + template.insert(new BasicEdgeTestEntity(entity, vertex2)); + final ConstructorWithRelationsParamsTestEntity document = template + .find(entity.id, ConstructorWithRelationsParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value.stream().map((e) -> e.id).collect(Collectors.toList()), + hasItems(vertex1.id, vertex2.id)); + } + + public static class ConstructorWithRelationsLazyParamsTestEntity extends BasicTestEntity { + @Relations(edges = BasicEdgeTestEntity.class, lazy = true) + private final Collection value; + + public ConstructorWithRelationsLazyParamsTestEntity(final Collection value) { + super(); + this.value = value; + } + } + + @Test + public void constructorWithRelationsLazyParams() { + final BasicTestEntity vertex1 = new BasicTestEntity(); + final BasicTestEntity vertex2 = new BasicTestEntity(); + template.insert(vertex1); + template.insert(vertex2); + final ConstructorWithRelationsLazyParamsTestEntity entity = new ConstructorWithRelationsLazyParamsTestEntity( + Arrays.asList(vertex1, vertex2)); + template.insert(entity); + template.insert(new BasicEdgeTestEntity(entity, vertex1)); + template.insert(new BasicEdgeTestEntity(entity, vertex2)); + final ConstructorWithRelationsLazyParamsTestEntity document = template + .find(entity.id, ConstructorWithRelationsLazyParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value.stream().map((e) -> e.id).collect(Collectors.toList()), + hasItems(vertex1.id, vertex2.id)); + } + + public static class ConstructorWithFromParamsTestEntity extends BasicTestEntity { + @From + private final Collection value; + + public ConstructorWithFromParamsTestEntity(final Collection value) { + super(); + this.value = value; + } + } + + @Test + public void constructorWithFromParams() { + final ConstructorWithFromParamsTestEntity entity = new ConstructorWithFromParamsTestEntity(null); + template.insert(entity); + final BasicTestEntity to = new BasicTestEntity(); + template.insert(to); + final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(entity, to); + final BasicEdgeLazyTestEntity edge2 = new BasicEdgeLazyTestEntity(entity, to); + template.insert(edge1); + template.insert(edge2); + final ConstructorWithFromParamsTestEntity document = template + .find(entity.id, ConstructorWithFromParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value.stream().map((e) -> e.id).collect(Collectors.toList()), hasItems(edge1.id, edge2.id)); + } + + public static class ConstructorWithFromLazyParamsTestEntity extends BasicTestEntity { + @From(lazy = true) + private final Collection value; + + public ConstructorWithFromLazyParamsTestEntity(final Collection value) { + super(); + this.value = value; + } + } + + @Test + public void constructorWithFromLazyParams() { + final ConstructorWithFromLazyParamsTestEntity entity = new ConstructorWithFromLazyParamsTestEntity(null); + template.insert(entity); + final BasicTestEntity to = new BasicTestEntity(); + template.insert(to); + final BasicEdgeTestEntity edge1 = new BasicEdgeTestEntity(entity, to); + final BasicEdgeTestEntity edge2 = new BasicEdgeTestEntity(entity, to); + template.insert(edge1); + template.insert(edge2); + final ConstructorWithFromLazyParamsTestEntity document = template + .find(entity.id, ConstructorWithFromLazyParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value.stream().map((e) -> e.getId()).collect(Collectors.toList()), + hasItems(edge1.id, edge2.id)); + } + + public static class ConstructorWithToParamsTestEntity extends BasicTestEntity { + @To + private final Collection value; + + public ConstructorWithToParamsTestEntity(final Collection value) { + super(); + this.value = value; + } + } + + @Test + public void constructorWithToParams() { + final ConstructorWithToParamsTestEntity entity = new ConstructorWithToParamsTestEntity(null); + template.insert(entity); + final BasicTestEntity from = new BasicTestEntity(); + template.insert(from); + final BasicEdgeLazyTestEntity edge1 = new BasicEdgeLazyTestEntity(from, entity); + final BasicEdgeLazyTestEntity edge2 = new BasicEdgeLazyTestEntity(from, entity); + template.insert(edge1); + template.insert(edge2); + final ConstructorWithToParamsTestEntity document = template + .find(entity.id, ConstructorWithToParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value.stream().map((e) -> e.id).collect(Collectors.toList()), hasItems(edge1.id, edge2.id)); + } + + public static class ConstructorWithToLazyParamsTestEntity extends BasicTestEntity { + @To(lazy = true) + private final Collection value; + + public ConstructorWithToLazyParamsTestEntity(final Collection value) { + super(); + this.value = value; + } + } + + @Test + public void constructorWithToLazyParams() { + final ConstructorWithToLazyParamsTestEntity entity = new ConstructorWithToLazyParamsTestEntity(null); + template.insert(entity); + final BasicTestEntity from = new BasicTestEntity(); + template.insert(from); + final BasicEdgeTestEntity edge1 = new BasicEdgeTestEntity(from, entity); + final BasicEdgeTestEntity edge2 = new BasicEdgeTestEntity(from, entity); + template.insert(edge1); + template.insert(edge2); + final ConstructorWithToLazyParamsTestEntity document = template + .find(entity.id, ConstructorWithToLazyParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value.stream().map((e) -> e.getId()).collect(Collectors.toList()), + hasItems(edge1.id, edge2.id)); + } + + public static class EdgeConstructorWithFromToParamsTestEntity extends BasicEdgeTestEntity { + @From + private final BasicTestEntity from; + @To + private final BasicTestEntity to; + + public EdgeConstructorWithFromToParamsTestEntity(final BasicTestEntity from, final BasicTestEntity to) { + super(); + this.from = from; + this.to = to; + } + } + + @Test + public void edgeConstructorWithFromToParams() { + final BasicTestEntity from = new BasicTestEntity(); + final BasicTestEntity to = new BasicTestEntity(); + template.insert(from); + template.insert(to); + final EdgeConstructorWithFromToParamsTestEntity edge = new EdgeConstructorWithFromToParamsTestEntity(from, to); + template.insert(edge); + final EdgeConstructorWithFromToParamsTestEntity document = template + .find(edge.id, EdgeConstructorWithFromToParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.from.id, is(from.id)); + assertThat(document.to.id, is(to.id)); + } + + public static class EdgeConstructorWithFromToLazyParamsTestEntity extends BasicEdgeTestEntity { + @From + private final BasicTestEntity from; + @To + private final BasicTestEntity to; + + public EdgeConstructorWithFromToLazyParamsTestEntity(final BasicTestEntity from, final BasicTestEntity to) { + super(); + this.from = from; + this.to = to; + } + } + + @Test + public void edgeConstructorWithFromToLazyParams() { + final BasicTestEntity from = new BasicTestEntity(); + final BasicTestEntity to = new BasicTestEntity(); + template.insert(from); + template.insert(to); + final EdgeConstructorWithFromToLazyParamsTestEntity edge = new EdgeConstructorWithFromToLazyParamsTestEntity( + from, to); + template.insert(edge); + final EdgeConstructorWithFromToLazyParamsTestEntity document = template + .find(edge.id, EdgeConstructorWithFromToLazyParamsTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.from.getId(), is(from.id)); + assertThat(document.to.getId(), is(to.id)); + } + + public static class JodaTestEntity extends BasicTestEntity { + private org.joda.time.DateTime value1; + private org.joda.time.Instant value2; + private org.joda.time.LocalDate value3; + private org.joda.time.LocalDateTime value4; + } + + @Test + public void jodaMapping() { + final JodaTestEntity entity = new JodaTestEntity(); + entity.value1 = org.joda.time.DateTime.now(DateTimeZone.forOffsetHours(1)); + entity.value2 = org.joda.time.Instant.now(); + entity.value3 = org.joda.time.LocalDate.now(); + entity.value4 = org.joda.time.LocalDateTime.now(); + template.insert(entity); + final JodaTestEntity document = template.find(entity.getId(), JodaTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value1, is(entity.value1)); + assertThat(document.value2, is(entity.value2)); + assertThat(document.value3, is(entity.value3)); + assertThat(document.value4, is(entity.value4)); + } + + public static class Java8TimeTestEntity extends BasicTestEntity { + private java.time.Instant value1; + private java.time.LocalDate value2; + private java.time.LocalDateTime value3; + } + + @Test + public void timeMapping() { + final Java8TimeTestEntity entity = new Java8TimeTestEntity(); + entity.value1 = java.time.Instant.now(); + entity.value2 = java.time.LocalDate.now(); + entity.value3 = java.time.LocalDateTime.now(); + template.insert(entity); + final Java8TimeTestEntity document = template.find(entity.getId(), Java8TimeTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value1, is(entity.value1)); + assertThat(document.value2, is(entity.value2)); + assertThat(document.value3, is(entity.value3)); + } + + public static class SimpleBasicChildTestEntity extends BasicTestEntity { + private String field; + } + + public static class ComplexBasicChildTestEntity extends BasicTestEntity { + private BasicTestEntity nestedEntity; + } + + public static class PropertyInheritanceTestEntity extends BasicTestEntity { + private BasicTestEntity value; + } + + @Test + public void simplePropertyInheritanceMapping() { + final SimpleBasicChildTestEntity child = new SimpleBasicChildTestEntity(); + child.field = "value"; + final PropertyInheritanceTestEntity entity = new PropertyInheritanceTestEntity(); + entity.value = child; + template.insert(entity); + final PropertyInheritanceTestEntity document = template + .find(entity.getId(), PropertyInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(SimpleBasicChildTestEntity.class))); + assertThat(((SimpleBasicChildTestEntity) document.value).field, is(child.field)); + } + + @Test + public void complexPropertyInheritanceMapping() { + final SimpleBasicChildTestEntity innerChild = new SimpleBasicChildTestEntity(); + innerChild.field = "value"; + final ComplexBasicChildTestEntity child = new ComplexBasicChildTestEntity(); + child.nestedEntity = innerChild; + final PropertyInheritanceTestEntity entity = new PropertyInheritanceTestEntity(); + entity.value = child; + template.insert(entity); + final PropertyInheritanceTestEntity document = template + .find(entity.getId(), PropertyInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(ComplexBasicChildTestEntity.class))); + final ComplexBasicChildTestEntity complexDocument = (ComplexBasicChildTestEntity) document.value; + assertThat(complexDocument.nestedEntity, is(instanceOf(SimpleBasicChildTestEntity.class))); + final SimpleBasicChildTestEntity simpleDocument = (SimpleBasicChildTestEntity) complexDocument.nestedEntity; + assertThat(simpleDocument.field, is(innerChild.field)); + } + + public static class ListInheritanceTestEntity extends BasicTestEntity { + private List value; + } + + @Test + public void simpleListInheritanceMapping() { + final List list = new ArrayList<>(); + final String value = "value"; + for (int i = 0; i < 3; ++i) { + final SimpleBasicChildTestEntity child = new SimpleBasicChildTestEntity(); + child.field = value; + list.add(child); + } + final ListInheritanceTestEntity entity = new ListInheritanceTestEntity(); + entity.value = list; + template.insert(entity); + final ListInheritanceTestEntity document = template.find(entity.getId(), ListInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(List.class))); + for (final BasicTestEntity elem : document.value) { + assertThat(elem, is(instanceOf(SimpleBasicChildTestEntity.class))); + assertThat(((SimpleBasicChildTestEntity) elem).field, is(value)); + } + } + + @Test + public void complexListInheritanceMapping() { + final List list = new ArrayList<>(); + final String value = "value"; + for (int i = 0; i < 3; ++i) { + final SimpleBasicChildTestEntity innerChild = new SimpleBasicChildTestEntity(); + innerChild.field = value; + final ComplexBasicChildTestEntity child = new ComplexBasicChildTestEntity(); + child.nestedEntity = innerChild; + list.add(child); + } + final ListInheritanceTestEntity entity = new ListInheritanceTestEntity(); + entity.value = list; + template.insert(entity); + final ListInheritanceTestEntity document = template.find(entity.getId(), ListInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(List.class))); + for (final BasicTestEntity elem : document.value) { + assertThat(elem, is(instanceOf(ComplexBasicChildTestEntity.class))); + final ComplexBasicChildTestEntity complexElem = (ComplexBasicChildTestEntity) elem; + assertThat(complexElem.nestedEntity, is(instanceOf(SimpleBasicChildTestEntity.class))); + final SimpleBasicChildTestEntity simpleElem = (SimpleBasicChildTestEntity) complexElem.nestedEntity; + assertThat(simpleElem.field, is(value)); + } + } + + @SuppressWarnings("rawtypes") + public static class UntypedListInheritanceTestEntity extends BasicTestEntity { + private List value; + } + + @Test + public void untypedListInheritanceMapping() { + final List list = new ArrayList<>(); + final String value = "value"; + for (int i = 0; i < 3; ++i) { + final SimpleBasicChildTestEntity innerChild = new SimpleBasicChildTestEntity(); + innerChild.field = value; + final ComplexBasicChildTestEntity child = new ComplexBasicChildTestEntity(); + child.nestedEntity = innerChild; + list.add(child); + } + final UntypedListInheritanceTestEntity entity = new UntypedListInheritanceTestEntity(); + entity.value = list; + template.insert(entity); + final UntypedListInheritanceTestEntity document = template + .find(entity.getId(), UntypedListInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(List.class))); + for (final Object elem : document.value) { + assertThat(elem, is(instanceOf(ComplexBasicChildTestEntity.class))); + final ComplexBasicChildTestEntity complexElem = (ComplexBasicChildTestEntity) elem; + assertThat(complexElem.nestedEntity, is(instanceOf(SimpleBasicChildTestEntity.class))); + final SimpleBasicChildTestEntity simpleElem = (SimpleBasicChildTestEntity) complexElem.nestedEntity; + assertThat(simpleElem.field, is(value)); + } + } + + public static class MapInheritanceTestEntity extends BasicTestEntity { + private Map value; + } + + @Test + public void simpleMapInheritanceMapping() { + final Map map = new HashMap<>(); + final String value = "value"; + for (int i = 0; i < 3; ++i) { + final SimpleBasicChildTestEntity child = new SimpleBasicChildTestEntity(); + child.field = value; + map.put(String.valueOf(i), child); + } + final MapInheritanceTestEntity entity = new MapInheritanceTestEntity(); + entity.value = map; + template.insert(entity); + final MapInheritanceTestEntity document = template.find(entity.getId(), MapInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(Map.class))); + for (final Map.Entry entry : document.value.entrySet()) { + assertThat(entry.getValue(), is(instanceOf(SimpleBasicChildTestEntity.class))); + assertThat(((SimpleBasicChildTestEntity) entry.getValue()).field, is(value)); + } + } + + @Test + public void complexMapInheritanceMapping() { + final Map map = new HashMap<>(); + final String value = "value"; + for (int i = 0; i < 3; ++i) { + final SimpleBasicChildTestEntity innerChild = new SimpleBasicChildTestEntity(); + innerChild.field = value; + final ComplexBasicChildTestEntity child = new ComplexBasicChildTestEntity(); + child.nestedEntity = innerChild; + map.put(String.valueOf(i), child); + } + final MapInheritanceTestEntity entity = new MapInheritanceTestEntity(); + entity.value = map; + template.insert(entity); + final MapInheritanceTestEntity document = template.find(entity.getId(), MapInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(Map.class))); + for (final Map.Entry entry : document.value.entrySet()) { + assertThat(entry.getValue(), is(instanceOf(ComplexBasicChildTestEntity.class))); + final ComplexBasicChildTestEntity complexElem = (ComplexBasicChildTestEntity) entry.getValue(); + assertThat(complexElem.nestedEntity, is(instanceOf(SimpleBasicChildTestEntity.class))); + final SimpleBasicChildTestEntity simpleElem = (SimpleBasicChildTestEntity) complexElem.nestedEntity; + assertThat(simpleElem.field, is(value)); + } + } + + @SuppressWarnings("rawtypes") + public static class UntypedMapInheritanceTestEntity extends BasicTestEntity { + private Map value; + } + + @SuppressWarnings("rawtypes") + @Test + public void untypedMapInheritanceMapping() { + final Map map = new HashMap<>(); + final String value = "value"; + for (int i = 0; i < 3; ++i) { + final SimpleBasicChildTestEntity innerChild = new SimpleBasicChildTestEntity(); + innerChild.field = value; + final ComplexBasicChildTestEntity child = new ComplexBasicChildTestEntity(); + child.nestedEntity = innerChild; + map.put(String.valueOf(i), child); + } + final UntypedMapInheritanceTestEntity entity = new UntypedMapInheritanceTestEntity(); + entity.value = map; + template.insert(entity); + final UntypedMapInheritanceTestEntity document = template + .find(entity.getId(), UntypedMapInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(Map.class))); + for (final Object entry : document.value.entrySet()) { + final Object val = ((Map.Entry) entry).getValue(); + assertThat(val, is(instanceOf(ComplexBasicChildTestEntity.class))); + final ComplexBasicChildTestEntity complexElem = (ComplexBasicChildTestEntity) val; + assertThat(complexElem.nestedEntity, is(instanceOf(SimpleBasicChildTestEntity.class))); + final SimpleBasicChildTestEntity simpleElem = (SimpleBasicChildTestEntity) complexElem.nestedEntity; + assertThat(simpleElem.field, is(value)); + } + } + + public static class ConstructorWithPropertyInheritanceTestEntity extends BasicTestEntity { + private final BasicTestEntity value; + + public ConstructorWithPropertyInheritanceTestEntity(final BasicTestEntity value) { + this.value = value; + } + } + + @Test + public void constructorPropertyInheritanceMapping() { + final SimpleBasicChildTestEntity innerChild = new SimpleBasicChildTestEntity(); + innerChild.field = "value"; + final ComplexBasicChildTestEntity child = new ComplexBasicChildTestEntity(); + child.nestedEntity = innerChild; + final ConstructorWithPropertyInheritanceTestEntity entity = new ConstructorWithPropertyInheritanceTestEntity( + child); + template.insert(entity); + final ConstructorWithPropertyInheritanceTestEntity document = template + .find(entity.getId(), ConstructorWithPropertyInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(ComplexBasicChildTestEntity.class))); + final ComplexBasicChildTestEntity complexDocument = (ComplexBasicChildTestEntity) document.value; + assertThat(complexDocument.nestedEntity, is(instanceOf(SimpleBasicChildTestEntity.class))); + final SimpleBasicChildTestEntity simpleDocument = (SimpleBasicChildTestEntity) complexDocument.nestedEntity; + assertThat(simpleDocument.field, is(innerChild.field)); + } + + public static class ListInMapInheritanceTestEntity extends BasicTestEntity { + private Map> value; + } + + @Test + public void listInMapInheritanceMapping() { + final Map> map = new HashMap<>(); + final String value = "value"; + for (int i = 0; i < 3; ++i) { + final List list = new ArrayList<>(); + map.put(String.valueOf(i), list); + for (int j = 0; j < 3; ++j) { + final SimpleBasicChildTestEntity innerChild = new SimpleBasicChildTestEntity(); + innerChild.field = value; + final ComplexBasicChildTestEntity child = new ComplexBasicChildTestEntity(); + child.nestedEntity = innerChild; + list.add(child); + } + } + final ListInMapInheritanceTestEntity entity = new ListInMapInheritanceTestEntity(); + entity.value = map; + template.insert(entity); + final ListInMapInheritanceTestEntity document = template + .find(entity.getId(), ListInMapInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(Map.class))); + for (final Map.Entry> entry : document.value.entrySet()) { + assertThat(entry.getValue(), is(instanceOf(List.class))); + for (final BasicTestEntity elem : entry.getValue()) { + assertThat(elem, is(instanceOf(ComplexBasicChildTestEntity.class))); + final ComplexBasicChildTestEntity complexElem = (ComplexBasicChildTestEntity) elem; + assertThat(complexElem.nestedEntity, is(instanceOf(SimpleBasicChildTestEntity.class))); + final SimpleBasicChildTestEntity simpleElem = (SimpleBasicChildTestEntity) complexElem.nestedEntity; + assertThat(simpleElem.field, is(value)); + } + } + } + + public static class PropertyRefInheritanceTestEntity extends BasicTestEntity { + @Ref + private BasicTestEntity value; + } + + @Test + public void propertyRefInheritanceMapping() { + final SimpleBasicChildTestEntity innerChild = new SimpleBasicChildTestEntity(); + innerChild.field = "value"; + final ComplexBasicChildTestEntity child = new ComplexBasicChildTestEntity(); + child.nestedEntity = innerChild; + final PropertyRefInheritanceTestEntity entity = new PropertyRefInheritanceTestEntity(); + entity.value = child; + template.insert(child); + template.insert(entity); + final PropertyRefInheritanceTestEntity document = template + .find(entity.getId(), PropertyRefInheritanceTestEntity.class).get(); + assertThat(document, is(notNullValue())); + assertThat(document.value, is(instanceOf(ComplexBasicChildTestEntity.class))); + final ComplexBasicChildTestEntity complexDocument = (ComplexBasicChildTestEntity) document.value; + assertThat(complexDocument.nestedEntity, is(instanceOf(SimpleBasicChildTestEntity.class))); + final SimpleBasicChildTestEntity simpleDocument = (SimpleBasicChildTestEntity) complexDocument.nestedEntity; + assertThat(simpleDocument.field, is(innerChild.field)); + } + + public class SimpleTypesTestEntity extends BasicTestEntity { + private String stringValue; + private Boolean boolValue; + private int intValue; + private Long longValue; + private Short shortValue; + private Float floatValue; + private Double doubleValue; + private Character charValue; + private Byte byteValue; + private Date dateValue; + private java.sql.Date sqlDateValue; + private Timestamp timestampValue; + private byte[] byteArray; + } + + @Test + public void simpleTypesMapping() { + final SimpleTypesTestEntity entity = new SimpleTypesTestEntity(); + entity.stringValue = "hello world"; + entity.boolValue = true; + entity.intValue = 123456; + entity.longValue = 1234567890123456789l; + entity.shortValue = 1234; + entity.floatValue = 1.234567890f; + entity.doubleValue = 1.2345678901234567890; + entity.charValue = 'a'; + entity.byteValue = 'z'; + entity.dateValue = new Date(); + entity.sqlDateValue = new java.sql.Date(new Date().getTime()); + entity.timestampValue = new Timestamp(new Date().getTime()); + entity.byteArray = new byte[] { 'a', 'b', 'c', 'x', 'y', 'z' }; + template.insert(entity); + final SimpleTypesTestEntity document = template.find(entity.getId(), SimpleTypesTestEntity.class).get(); + assertThat(entity.stringValue, is(document.stringValue)); + assertThat(entity.boolValue, is(document.boolValue)); + assertThat(entity.intValue, is(document.intValue)); + assertThat(entity.longValue, is(document.longValue)); + assertThat(entity.shortValue, is(document.shortValue)); + assertThat(entity.floatValue, is(document.floatValue)); + assertThat(entity.doubleValue, is(document.doubleValue)); + assertThat(entity.charValue, is(document.charValue)); + assertThat(entity.byteValue, is(document.byteValue)); + assertThat(entity.dateValue, is(document.dateValue)); + assertThat(entity.sqlDateValue, is(document.sqlDateValue)); + assertThat(entity.timestampValue, is(document.timestampValue)); + assertThat(entity.byteArray, is(document.byteArray)); + } + + public enum TestEnum { + A, B; + } + + public class EnumTestEntity extends BasicTestEntity { + private TestEnum value; + } + + @Test + public void enumMapping() { + final EnumTestEntity entity = new EnumTestEntity(); + entity.value = TestEnum.A; + template.insert(entity); + final EnumTestEntity document = template.find(entity.getId(), EnumTestEntity.class).get(); + assertThat(entity.value, is(document.value)); + } +} diff --git a/src/test/java/com/arangodb/springframework/core/template/ArangoIndexTest.java b/src/test/java/com/arangodb/springframework/core/template/ArangoIndexTest.java index f45086898..325cf2bef 100644 --- a/src/test/java/com/arangodb/springframework/core/template/ArangoIndexTest.java +++ b/src/test/java/com/arangodb/springframework/core/template/ArangoIndexTest.java @@ -58,6 +58,18 @@ @ContextConfiguration(classes = { ArangoTestConfiguration.class }) public class ArangoIndexTest extends AbstractArangoTest { + private IndexType geo1() { + return geoType(IndexType.geo1); + } + + private IndexType geo2() { + return geoType(IndexType.geo2); + } + + private IndexType geoType(final IndexType type) { + return Integer.valueOf(template.getVersion().getVersion().split("\\.")[1]) >= 4 ? IndexType.geo : type; + } + public static class HashIndexedSingleFieldTestEntity { @HashIndexed private String a; @@ -340,9 +352,9 @@ public void singleFieldGeoIndexed() { assertThat(template.collection(GeoIndexedSingleFieldTestEntity.class).getIndexes().size(), is(2)); assertThat(template.collection(GeoIndexedSingleFieldTestEntity.class).getIndexes().stream() .map(i -> i.getType()).collect(Collectors.toList()), - hasItems(IndexType.primary, IndexType.geo1)); + hasItems(IndexType.primary, geo1())); assertThat(template.collection(GeoIndexedSingleFieldTestEntity.class).getIndexes().stream() - .filter(i -> i.getType() == IndexType.geo1).findFirst().get().getFields(), + .filter(i -> i.getType() == geo1()).findFirst().get().getFields(), hasItems("a")); } @@ -358,7 +370,7 @@ public void multipleSingleFieldGeoIndexed() { assertThat(template.collection(GeoIndexedMultipleSingleFieldTestEntity.class).getIndexes().size(), is(3)); assertThat(template.collection(GeoIndexedMultipleSingleFieldTestEntity.class).getIndexes().stream() .map(i -> i.getType()).collect(Collectors.toList()), - hasItems(IndexType.primary, IndexType.geo1)); + hasItems(IndexType.primary, geo1())); } @GeoIndex(fields = { "a" }) @@ -370,9 +382,9 @@ public void singleFieldGeoIndex() { assertThat(template.collection(GeoIndexWithSingleFieldTestEntity.class).getIndexes().size(), is(2)); assertThat(template.collection(GeoIndexWithSingleFieldTestEntity.class).getIndexes().stream() .map(i -> i.getType()).collect(Collectors.toList()), - hasItems(IndexType.primary, IndexType.geo1)); + hasItems(IndexType.primary, geo1())); assertThat(template.collection(GeoIndexedSingleFieldTestEntity.class).getIndexes().stream() - .filter(i -> i.getType() == IndexType.geo1).findFirst().get().getFields(), + .filter(i -> i.getType() == geo1()).findFirst().get().getFields(), hasItems("a")); } @@ -386,7 +398,7 @@ public void multipleSingleFieldGeoIndex() { assertThat(template.collection(GeoIndexedMultipleSingleFieldTestEntity.class).getIndexes().size(), is(3)); assertThat(template.collection(GeoIndexedMultipleSingleFieldTestEntity.class).getIndexes().stream() .map(i -> i.getType()).collect(Collectors.toList()), - hasItems(IndexType.primary, IndexType.geo1)); + hasItems(IndexType.primary, geo1())); } @GeoIndex(fields = { "a", "b" }) @@ -398,9 +410,9 @@ public void multiFieldGeoIndex() { assertThat(template.collection(GeoIndexWithMultiFieldTestEntity.class).getIndexes().size(), is(2)); assertThat(template.collection(GeoIndexWithMultiFieldTestEntity.class).getIndexes().stream() .map(i -> i.getType()).collect(Collectors.toList()), - hasItems(IndexType.primary, IndexType.geo2)); + hasItems(IndexType.primary, geo2())); assertThat(template.collection(GeoIndexWithMultiFieldTestEntity.class).getIndexes().stream() - .filter(i -> i.getType() == IndexType.geo2).findFirst().get().getFields(), + .filter(i -> i.getType() == geo2()).findFirst().get().getFields(), hasItems("a", "b")); } @@ -413,7 +425,7 @@ public void multipleIndexesGeoIndex() { assertThat(template.collection(GeoIndexedMultipleSingleFieldTestEntity.class).getIndexes().size(), is(3)); assertThat(template.collection(GeoIndexedMultipleSingleFieldTestEntity.class).getIndexes().stream() .map(i -> i.getType()).collect(Collectors.toList()), - hasItems(IndexType.primary, IndexType.geo1)); + hasItems(IndexType.primary, geo1())); } public static class FulltextIndexedSingleFieldTestEntity { @@ -506,7 +518,7 @@ public void differentIndexedAnnotationsSameField() { assertThat( template.collection(DifferentIndexedAnnotations.class).getIndexes().stream().map(i -> i.getType()) .collect(Collectors.toList()), - hasItems(IndexType.primary, IndexType.hash, IndexType.skiplist, IndexType.persistent, IndexType.geo1, + hasItems(IndexType.primary, IndexType.hash, IndexType.skiplist, IndexType.persistent, geo1(), IndexType.fulltext)); } @@ -525,7 +537,7 @@ public void differentIndexAnnotations() { assertThat( template.collection(DifferentIndexAnnotations.class).getIndexes().stream().map(i -> i.getType()) .collect(Collectors.toList()), - hasItems(IndexType.primary, IndexType.hash, IndexType.skiplist, IndexType.persistent, IndexType.geo1, + hasItems(IndexType.primary, IndexType.hash, IndexType.skiplist, IndexType.persistent, geo1(), IndexType.fulltext)); } @@ -549,7 +561,7 @@ public void multipleDifferentIndexAnnotations() { assertThat( template.collection(MultipleDifferentIndexAnnotations.class).getIndexes().stream().map(i -> i.getType()) .collect(Collectors.toList()), - hasItems(IndexType.primary, IndexType.hash, IndexType.skiplist, IndexType.persistent, IndexType.geo1, + hasItems(IndexType.primary, IndexType.hash, IndexType.skiplist, IndexType.persistent, geo1(), IndexType.fulltext)); } diff --git a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java index d9cd27901..397210ec7 100644 --- a/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java +++ b/src/test/java/com/arangodb/springframework/core/template/ArangoTemplateTest.java @@ -1,332 +1,410 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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 static org.hamcrest.Matchers.hasItems; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertThat; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import com.arangodb.ArangoCursor; -import com.arangodb.entity.ArangoDBVersion; -import com.arangodb.entity.BaseDocument; -import com.arangodb.entity.DocumentEntity; -import com.arangodb.entity.MultiDocumentEntity; -import com.arangodb.model.AqlQueryOptions; -import com.arangodb.springframework.AbstractArangoTest; -import com.arangodb.springframework.ArangoTestConfiguration; -import com.arangodb.springframework.core.ArangoOperations.UpsertStrategy; -import com.arangodb.springframework.testdata.Address; -import com.arangodb.springframework.testdata.Customer; -import com.arangodb.springframework.testdata.Product; -import com.arangodb.util.MapBuilder; -import com.arangodb.velocypack.VPackSlice; - -/** - * @author Mark Vollmary - * - */ -@RunWith(SpringJUnit4ClassRunner.class) -@ContextConfiguration(classes = { ArangoTestConfiguration.class }) -public class ArangoTemplateTest extends AbstractArangoTest { - - @Test - public void template() { - final ArangoDBVersion version = template.getVersion(); - assertThat(version, is(notNullValue())); - assertThat(version.getLicense(), is(notNullValue())); - assertThat(version.getServer(), is(notNullValue())); - assertThat(version.getVersion(), is(notNullValue())); - } - - @Test - public void insertDocument() { - final DocumentEntity res = template.insert(new Customer("John", "Doe", 30)); - assertThat(res, is(notNullValue())); - assertThat(res.getId(), is(notNullValue())); - } - - @Test - public void insertDocuments() { - final Customer c1 = new Customer(); - final Customer c2 = new Customer(); - final Customer c3 = new Customer(); - c3.setKey("3"); - final Customer c4 = new Customer(); - c4.setKey("3"); - final MultiDocumentEntity res = template.insert(Arrays.asList(c1, c2, c3, c4), - Customer.class); - assertThat(res, is(notNullValue())); - assertThat(res.getDocuments().size(), is(3)); - assertThat(res.getErrors().size(), is(1)); - assertThat(c1.getId(), is(notNullValue())); - assertThat(c2.getId(), is(notNullValue())); - assertThat(c3.getId(), is(notNullValue())); - } - - @Test - public void upsertReplace() { - final Customer customer = new Customer("John", "Doe", 30); - template.upsert(customer, UpsertStrategy.REPLACE); - assertThat(template.find(customer.getId(), Customer.class).get().getAge(), is(30)); - customer.setAge(35); - template.upsert(customer, UpsertStrategy.REPLACE); - assertThat(template.find(customer.getId(), Customer.class).get().getAge(), is(35)); - } - - @Test - public void upsertUpdate() { - final Customer customer = new Customer("John", "Doe", 30); - template.upsert(customer, UpsertStrategy.UPDATE); - assertThat(template.find(customer.getId(), Customer.class).get().getAge(), is(30)); - customer.setAge(35); - template.upsert(customer, UpsertStrategy.UPDATE); - assertThat(template.find(customer.getId(), Customer.class).get().getAge(), is(35)); - } - - @Test - public void upsertReplaceMultiple() { - final Customer c1 = new Customer("John", "Doe", 30); - final Customer c2 = new Customer("John2", "Doe2", 30); - template.upsert(Arrays.asList(c1, c2), UpsertStrategy.REPLACE); - assertThat(template.find(c1.getId(), Customer.class).get().getAge(), is(30)); - assertThat(template.find(c2.getId(), Customer.class).get().getAge(), is(30)); - c1.setAge(35); - c2.setAge(35); - final Customer c3 = new Customer("John3", "Doe2", 30); - template.upsert(Arrays.asList(c1, c2, c3), UpsertStrategy.REPLACE); - assertThat(template.find(c1.getId(), Customer.class).get().getAge(), is(35)); - assertThat(template.find(c2.getId(), Customer.class).get().getAge(), is(35)); - assertThat(template.find(c3.getId(), Customer.class).get().getAge(), is(30)); - } - - @Test - public void upsertUpdateMultiple() { - final Customer c1 = new Customer("John", "Doe", 30); - final Customer c2 = new Customer("John2", "Doe2", 30); - template.upsert(Arrays.asList(c1, c2), UpsertStrategy.UPDATE); - assertThat(template.find(c1.getId(), Customer.class).get().getAge(), is(30)); - assertThat(template.find(c2.getId(), Customer.class).get().getAge(), is(30)); - c1.setAge(35); - c2.setAge(35); - final Customer c3 = new Customer("John3", "Doe2", 30); - template.upsert(Arrays.asList(c1, c2, c3), UpsertStrategy.UPDATE); - assertThat(template.find(c1.getId(), Customer.class).get().getAge(), is(35)); - assertThat(template.find(c2.getId(), Customer.class).get().getAge(), is(35)); - assertThat(template.find(c3.getId(), Customer.class).get().getAge(), is(30)); - } - - @Test - public void getDocument() { - final DocumentEntity res = template.insert(new Customer("John", "Doe", 30, new Address("22162–1010"))); - final Customer customer = template.find(res.getId(), Customer.class).get(); - assertThat(customer, is(notNullValue())); - assertThat(customer.getName(), is("John")); - assertThat(customer.getSurname(), is("Doe")); - assertThat(customer.getAge(), is(30)); - assertThat(customer.getAddress(), is(notNullValue())); - assertThat(customer.getAddress().getZipCode(), is("22162–1010")); - } - - @Test - public void getDocuments() { - final Customer c1 = new Customer("John", "Doe", 30); - final Customer c2 = new Customer("John2", "Doe", 30); - template.insert(Arrays.asList(c1, c2), Customer.class); - final Iterable customers = template.find(Arrays.asList(c1.getId(), c2.getId()), Customer.class); - assertThat(customers, is(notNullValue())); - assertThat( - StreamSupport.stream(customers.spliterator(), false).map((e) -> e.getId()).collect(Collectors.toList()), - hasItems(c1.getId(), c2.getId())); - } - - @Test - public void getAllDocuments() { - final Customer c1 = new Customer("John", "Doe", 30); - final Customer c2 = new Customer("John2", "Doe", 30); - template.insert(Arrays.asList(c1, c2), Customer.class); - final Iterable customers = template.findAll(Customer.class); - assertThat(customers, is(notNullValue())); - assertThat( - StreamSupport.stream(customers.spliterator(), false).map((e) -> e.getId()).collect(Collectors.toList()), - hasItems(c1.getId(), c2.getId())); - } - - @Test - public void replaceDocument() { - final DocumentEntity res = template.insert(new Customer("John", "Doe", 30)); - final DocumentEntity replaceDocument = template.replace(res.getId(), new Customer("Jane", "Doe", 26)); - assertThat(replaceDocument, is(notNullValue())); - final Customer customer = template.find(res.getId(), Customer.class).get(); - assertThat(customer, is(notNullValue())); - assertThat(customer.getName(), is("Jane")); - assertThat(customer.getSurname(), is("Doe")); - assertThat(customer.getAge(), is(26)); - } - - @Test - public void replaceDocuments() { - final DocumentEntity a = template.insert(new Product("a")); - final DocumentEntity b = template.insert(new Product("b")); - - final Product documentA = template.find(a.getId(), Product.class).get(); - documentA.setName("aa"); - final Product documentB = template.find(b.getId(), Product.class).get(); - documentB.setName("bb"); - - final MultiDocumentEntity res = template.replace(Arrays.asList(documentA, documentB), - Product.class); - assertThat(res, is(notNullValue())); - assertThat(res.getDocuments().size(), is(2)); - - final Product newA = template.find(a.getId(), Product.class).get(); - assertThat(newA.getName(), is("aa")); - final Product newB = template.find(b.getId(), Product.class).get(); - assertThat(newB.getName(), is("bb")); - } - - @Test - public void updateDocument() { - final DocumentEntity res = template.insert(new Customer("John", "Doe", 30)); - template.update(res.getId(), new Customer("Jane", "Doe", 26)); - final Customer customer = template.find(res.getId(), Customer.class).get(); - assertThat(customer, is(notNullValue())); - assertThat(customer.getName(), is("Jane")); - assertThat(customer.getSurname(), is("Doe")); - assertThat(customer.getAge(), is(26)); - } - - @Test - public void updateDocuments() { - final DocumentEntity a = template.insert(new Product("a")); - final DocumentEntity b = template.insert(new Product("b")); - - final Product documentA = template.find(a.getId(), Product.class).get(); - documentA.setName("aa"); - final Product documentB = template.find(b.getId(), Product.class).get(); - documentB.setName("bb"); - - final MultiDocumentEntity res = template.update(Arrays.asList(documentA, documentB), - Product.class); - assertThat(res, is(notNullValue())); - assertThat(res.getDocuments().size(), is(2)); - - final Product newA = template.find(a.getId(), Product.class).get(); - assertThat(newA.getName(), is("aa")); - final Product newB = template.find(b.getId(), Product.class).get(); - assertThat(newB.getName(), is("bb")); - } - - @Test - public void deleteDocument() { - final DocumentEntity res = template.insert(new Customer("John", "Doe", 30)); - template.delete(res.getId(), Customer.class); - final Optional customer = template.find(res.getId(), Customer.class); - assertThat(customer.isPresent(), is(false)); - } - - @Test - public void deleteDocuments() { - final DocumentEntity a = template.insert(new Product("a")); - final DocumentEntity b = template.insert(new Product("b")); - - final Product documentA = template.find(a.getId(), Product.class).get(); - final Product documentB = template.find(b.getId(), Product.class).get(); - - final MultiDocumentEntity res = template.delete(Arrays.asList(documentA, documentB), - Product.class); - assertThat(res, is(notNullValue())); - assertThat(res.getDocuments().size(), is(2)); - - final Optional deletedA = template.find(a.getId(), Product.class); - assertThat(deletedA.isPresent(), is(false)); - final Optional deletedB = template.find(b.getId(), Product.class); - assertThat(deletedB.isPresent(), is(false)); - } - - @Test - public void query() { - template.insert(new Customer("John", "Doe", 30)); - final ArangoCursor cursor = template.query("FOR c IN @@coll FILTER c.name == @name RETURN c", - new MapBuilder().put("@coll", "customer").put("name", "John").get(), new AqlQueryOptions(), Customer.class); - assertThat(cursor, is(notNullValue())); - final List customers = cursor.asListRemaining(); - assertThat(customers.size(), is(1)); - assertThat(customers.get(0).getName(), is("John")); - assertThat(customers.get(0).getSurname(), is("Doe")); - assertThat(customers.get(0).getAge(), is(30)); - } - - @SuppressWarnings("rawtypes") - @Test - public void queryMap() { - template.insert(new Customer("John", "Doe", 30)); - final ArangoCursor cursor = template.query("FOR c IN @@coll FILTER c.name == @name RETURN c", - new MapBuilder().put("@coll", "customer").put("name", "John").get(), new AqlQueryOptions(), Map.class); - assertThat(cursor, is(notNullValue())); - final List customers = cursor.asListRemaining(); - assertThat(customers.size(), is(1)); - assertThat(customers.get(0).get("name"), is("John")); - assertThat(customers.get(0).get("surname"), is("Doe")); - assertThat(customers.get(0).get("age"), is(30L)); - } - - @Test - public void queryBaseDocument() { - template.insert(new Customer("John", "Doe", 30)); - final ArangoCursor cursor = template.query("FOR c IN @@coll FILTER c.name == @name RETURN c", - new MapBuilder().put("@coll", "customer").put("name", "John").get(), new AqlQueryOptions(), - BaseDocument.class); - assertThat(cursor, is(notNullValue())); - final List customers = cursor.asListRemaining(); - assertThat(customers.size(), is(1)); - assertThat(customers.get(0).getAttribute("name"), is("John")); - assertThat(customers.get(0).getAttribute("surname"), is("Doe")); - assertThat(customers.get(0).getAttribute("age"), is(30L)); - } - - @Test - public void queryVPackSlice() { - template.insert(new Customer("John", "Doe", 30)); - final ArangoCursor cursor = template.query("FOR c IN @@coll FILTER c.name == @name RETURN c", - new MapBuilder().put("@coll", "customer").put("name", "John").get(), new AqlQueryOptions(), - VPackSlice.class); - assertThat(cursor, is(notNullValue())); - final List customers = cursor.asListRemaining(); - assertThat(customers.size(), is(1)); - assertThat(customers.get(0).get("name").getAsString(), is("John")); - assertThat(customers.get(0).get("surname").getAsString(), is("Doe")); - assertThat(customers.get(0).get("age").getAsInt(), is(30)); - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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 static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThat; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.domain.Persistable; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.arangodb.ArangoCursor; +import com.arangodb.entity.ArangoDBVersion; +import com.arangodb.entity.BaseDocument; +import com.arangodb.entity.DocumentEntity; +import com.arangodb.entity.MultiDocumentEntity; +import com.arangodb.model.AqlQueryOptions; +import com.arangodb.springframework.AbstractArangoTest; +import com.arangodb.springframework.ArangoTestConfiguration; +import com.arangodb.springframework.annotation.Key; +import com.arangodb.springframework.core.ArangoOperations.UpsertStrategy; +import com.arangodb.springframework.testdata.Address; +import com.arangodb.springframework.testdata.Customer; +import com.arangodb.springframework.testdata.Product; +import com.arangodb.util.MapBuilder; +import com.arangodb.velocypack.VPackSlice; + +/** + * @author Mark Vollmary + * + */ +@SuppressWarnings("deprecation") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { ArangoTestConfiguration.class }) +public class ArangoTemplateTest extends AbstractArangoTest { + + @Test + public void template() { + final ArangoDBVersion version = template.getVersion(); + assertThat(version, is(notNullValue())); + assertThat(version.getLicense(), is(notNullValue())); + assertThat(version.getServer(), is(notNullValue())); + assertThat(version.getVersion(), is(notNullValue())); + } + + @Test + public void insertDocument() { + final DocumentEntity res = template.insert(new Customer("John", "Doe", 30)); + assertThat(res, is(notNullValue())); + assertThat(res.getId(), is(notNullValue())); + } + + @Test + public void insertDocuments() { + final Customer c1 = new Customer(); + final Customer c2 = new Customer(); + final Customer c3 = new Customer(); + c3.setKey("3"); + final Customer c4 = new Customer(); + c4.setKey("3"); + final MultiDocumentEntity res = template.insert(Arrays.asList(c1, c2, c3, c4), + Customer.class); + assertThat(res, is(notNullValue())); + assertThat(res.getDocuments().size(), is(3)); + assertThat(res.getErrors().size(), is(1)); + assertThat(c1.getId(), is(notNullValue())); + assertThat(c2.getId(), is(notNullValue())); + assertThat(c3.getId(), is(notNullValue())); + } + + @Test + public void insertDocumentWithCollName() { + final DocumentEntity res = template.insert("customer", new Customer("John", "Doe", 30)); + assertThat(res, is(notNullValue())); + assertThat(res.getId(), is(notNullValue())); + } + + @Test + public void upsertReplace() { + final Customer customer = new Customer("John", "Doe", 30); + template.upsert(customer, UpsertStrategy.REPLACE); + assertThat(template.find(customer.getId(), Customer.class).get().getAge(), is(30)); + customer.setAge(35); + template.upsert(customer, UpsertStrategy.REPLACE); + assertThat(template.find(customer.getId(), Customer.class).get().getAge(), is(35)); + } + + @Test + public void upsertUpdate() { + final Customer customer = new Customer("John", "Doe", 30); + template.upsert(customer, UpsertStrategy.UPDATE); + assertThat(template.find(customer.getId(), Customer.class).get().getAge(), is(30)); + customer.setAge(35); + template.upsert(customer, UpsertStrategy.UPDATE); + assertThat(template.find(customer.getId(), Customer.class).get().getAge(), is(35)); + } + + @Test + public void upsertReplaceMultiple() { + final Customer c1 = new Customer("John", "Doe", 30); + final Customer c2 = new Customer("John2", "Doe2", 30); + template.upsert(Arrays.asList(c1, c2), UpsertStrategy.REPLACE); + assertThat(template.find(c1.getId(), Customer.class).get().getAge(), is(30)); + assertThat(template.find(c2.getId(), Customer.class).get().getAge(), is(30)); + c1.setAge(35); + c2.setAge(35); + final Customer c3 = new Customer("John3", "Doe2", 30); + template.upsert(Arrays.asList(c1, c2, c3), UpsertStrategy.REPLACE); + assertThat(template.find(c1.getId(), Customer.class).get().getAge(), is(35)); + assertThat(template.find(c2.getId(), Customer.class).get().getAge(), is(35)); + assertThat(template.find(c3.getId(), Customer.class).get().getAge(), is(30)); + } + + @Test + public void upsertUpdateMultiple() { + final Customer c1 = new Customer("John", "Doe", 30); + final Customer c2 = new Customer("John2", "Doe2", 30); + template.upsert(Arrays.asList(c1, c2), UpsertStrategy.UPDATE); + assertThat(template.find(c1.getId(), Customer.class).get().getAge(), is(30)); + assertThat(template.find(c2.getId(), Customer.class).get().getAge(), is(30)); + c1.setAge(35); + c2.setAge(35); + final Customer c3 = new Customer("John3", "Doe2", 30); + template.upsert(Arrays.asList(c1, c2, c3), UpsertStrategy.UPDATE); + assertThat(template.find(c1.getId(), Customer.class).get().getAge(), is(35)); + assertThat(template.find(c2.getId(), Customer.class).get().getAge(), is(35)); + assertThat(template.find(c3.getId(), Customer.class).get().getAge(), is(30)); + } + + @Test + public void getDocument() { + final DocumentEntity res = template.insert(new Customer("John", "Doe", 30, new Address("22162–1010"))); + final Customer customer = template.find(res.getId(), Customer.class).get(); + assertThat(customer, is(notNullValue())); + assertThat(customer.getName(), is("John")); + assertThat(customer.getSurname(), is("Doe")); + assertThat(customer.getAge(), is(30)); + assertThat(customer.getAddress(), is(notNullValue())); + assertThat(customer.getAddress().getZipCode(), is("22162–1010")); + } + + @Test + public void getDocuments() { + final Customer c1 = new Customer("John", "Doe", 30); + final Customer c2 = new Customer("John2", "Doe", 30); + template.insert(Arrays.asList(c1, c2), Customer.class); + final Iterable customers = template.find(Arrays.asList(c1.getId(), c2.getId()), Customer.class); + assertThat(customers, is(notNullValue())); + assertThat( + StreamSupport.stream(customers.spliterator(), false).map((e) -> e.getId()).collect(Collectors.toList()), + hasItems(c1.getId(), c2.getId())); + } + + @Test + public void getAllDocuments() { + final Customer c1 = new Customer("John", "Doe", 30); + final Customer c2 = new Customer("John2", "Doe", 30); + template.insert(Arrays.asList(c1, c2), Customer.class); + final Iterable customers = template.findAll(Customer.class); + assertThat(customers, is(notNullValue())); + assertThat( + StreamSupport.stream(customers.spliterator(), false).map((e) -> e.getId()).collect(Collectors.toList()), + hasItems(c1.getId(), c2.getId())); + } + + @Test + public void replaceDocument() { + final DocumentEntity res = template.insert(new Customer("John", "Doe", 30)); + final DocumentEntity replaceDocument = template.replace(res.getId(), new Customer("Jane", "Doe", 26)); + assertThat(replaceDocument, is(notNullValue())); + final Customer customer = template.find(res.getId(), Customer.class).get(); + assertThat(customer, is(notNullValue())); + assertThat(customer.getName(), is("Jane")); + assertThat(customer.getSurname(), is("Doe")); + assertThat(customer.getAge(), is(26)); + } + + @Test + public void replaceDocuments() { + final DocumentEntity a = template.insert(new Product("a")); + final DocumentEntity b = template.insert(new Product("b")); + + final Product documentA = template.find(a.getId(), Product.class).get(); + documentA.setName("aa"); + final Product documentB = template.find(b.getId(), Product.class).get(); + documentB.setName("bb"); + + final MultiDocumentEntity res = template.replace(Arrays.asList(documentA, documentB), + Product.class); + assertThat(res, is(notNullValue())); + assertThat(res.getDocuments().size(), is(2)); + + final Product newA = template.find(a.getId(), Product.class).get(); + assertThat(newA.getName(), is("aa")); + final Product newB = template.find(b.getId(), Product.class).get(); + assertThat(newB.getName(), is("bb")); + } + + @Test + public void updateDocument() { + final DocumentEntity res = template.insert(new Customer("John", "Doe", 30)); + template.update(res.getId(), new Customer("Jane", "Doe", 26)); + final Customer customer = template.find(res.getId(), Customer.class).get(); + assertThat(customer, is(notNullValue())); + assertThat(customer.getName(), is("Jane")); + assertThat(customer.getSurname(), is("Doe")); + assertThat(customer.getAge(), is(26)); + } + + @Test + public void updateDocuments() { + final DocumentEntity a = template.insert(new Product("a")); + final DocumentEntity b = template.insert(new Product("b")); + + final Product documentA = template.find(a.getId(), Product.class).get(); + documentA.setName("aa"); + final Product documentB = template.find(b.getId(), Product.class).get(); + documentB.setName("bb"); + + final MultiDocumentEntity res = template.update(Arrays.asList(documentA, documentB), + Product.class); + assertThat(res, is(notNullValue())); + assertThat(res.getDocuments().size(), is(2)); + + final Product newA = template.find(a.getId(), Product.class).get(); + assertThat(newA.getName(), is("aa")); + final Product newB = template.find(b.getId(), Product.class).get(); + assertThat(newB.getName(), is("bb")); + } + + @Test + public void deleteDocument() { + final DocumentEntity res = template.insert(new Customer("John", "Doe", 30)); + template.delete(res.getId(), Customer.class); + final Optional customer = template.find(res.getId(), Customer.class); + assertThat(customer.isPresent(), is(false)); + } + + @Test + public void deleteDocuments() { + final DocumentEntity a = template.insert(new Product("a")); + final DocumentEntity b = template.insert(new Product("b")); + + final Product documentA = template.find(a.getId(), Product.class).get(); + final Product documentB = template.find(b.getId(), Product.class).get(); + + final MultiDocumentEntity res = template.delete(Arrays.asList(documentA, documentB), + Product.class); + assertThat(res, is(notNullValue())); + assertThat(res.getDocuments().size(), is(2)); + + final Optional deletedA = template.find(a.getId(), Product.class); + assertThat(deletedA.isPresent(), is(false)); + final Optional deletedB = template.find(b.getId(), Product.class); + assertThat(deletedB.isPresent(), is(false)); + } + + @Test + public void query() { + template.insert(new Customer("John", "Doe", 30)); + final ArangoCursor cursor = template.query("FOR c IN @@coll FILTER c.name == @name RETURN c", + new MapBuilder().put("@coll", "customer").put("name", "John").get(), new AqlQueryOptions(), Customer.class); + assertThat(cursor, is(notNullValue())); + final List customers = cursor.asListRemaining(); + assertThat(customers.size(), is(1)); + assertThat(customers.get(0).getName(), is("John")); + assertThat(customers.get(0).getSurname(), is("Doe")); + assertThat(customers.get(0).getAge(), is(30)); + } + + @Test + public void queryWithoutBindParams() { + template.insert(new Customer("John", "Doe", 30)); + final ArangoCursor cursor = template.query("FOR c IN customer FILTER c.name == 'John' RETURN c", null, + new AqlQueryOptions(), Customer.class); + assertThat(cursor, is(notNullValue())); + final List customers = cursor.asListRemaining(); + assertThat(customers.size(), is(1)); + assertThat(customers.get(0).getName(), is("John")); + assertThat(customers.get(0).getSurname(), is("Doe")); + assertThat(customers.get(0).getAge(), is(30)); + } + + @SuppressWarnings("rawtypes") + @Test + public void queryMap() { + template.insert(new Customer("John", "Doe", 30)); + final ArangoCursor cursor = template.query("FOR c IN @@coll FILTER c.name == @name RETURN c", + new MapBuilder().put("@coll", "customer").put("name", "John").get(), new AqlQueryOptions(), Map.class); + assertThat(cursor, is(notNullValue())); + final List customers = cursor.asListRemaining(); + assertThat(customers.size(), is(1)); + assertThat(customers.get(0).get("name"), is("John")); + assertThat(customers.get(0).get("surname"), is("Doe")); + assertThat(customers.get(0).get("age"), is(30L)); + } + + @Test + public void queryBaseDocument() { + template.insert(new Customer("John", "Doe", 30)); + final ArangoCursor cursor = template.query("FOR c IN @@coll FILTER c.name == @name RETURN c", + new MapBuilder().put("@coll", "customer").put("name", "John").get(), new AqlQueryOptions(), + BaseDocument.class); + assertThat(cursor, is(notNullValue())); + final List customers = cursor.asListRemaining(); + assertThat(customers.size(), is(1)); + assertThat(customers.get(0).getAttribute("name"), is("John")); + assertThat(customers.get(0).getAttribute("surname"), is("Doe")); + assertThat(customers.get(0).getAttribute("age"), is(30L)); + } + + @Test + public void queryVPackSlice() { + template.insert(new Customer("John", "Doe", 30)); + final ArangoCursor cursor = template.query("FOR c IN @@coll FILTER c.name == @name RETURN c", + new MapBuilder().put("@coll", "customer").put("name", "John").get(), new AqlQueryOptions(), + VPackSlice.class); + assertThat(cursor, is(notNullValue())); + final List customers = cursor.asListRemaining(); + assertThat(customers.size(), is(1)); + assertThat(customers.get(0).get("name").getAsString(), is("John")); + assertThat(customers.get(0).get("surname").getAsString(), is("Doe")); + assertThat(customers.get(0).get("age").getAsInt(), is(30)); + } + + public static class NewEntityTest implements Persistable { + + private static final long serialVersionUID = 1L; + + @Id + private String id; + @Key + private final String key; + @Transient + private transient boolean persisted; + + public NewEntityTest(final String key) { + super(); + this.key = key; + } + + public void setPersisted(final boolean persisted) { + this.persisted = persisted; + } + + @Override + public boolean isNew() { + return !persisted; + } + + @Override + public String getId() { + return key; + } + } + + @Test + public void upsertWithUserGeneratedKey() { + final NewEntityTest entity = new NewEntityTest("test"); + template.upsert(entity, UpsertStrategy.REPLACE); + assertThat(template.collection(NewEntityTest.class).count(), is(1L)); + entity.setPersisted(true); + template.upsert(entity, UpsertStrategy.REPLACE); + assertThat(template.collection(NewEntityTest.class).count(), is(1L)); + } + + @Test + public void mutliUpsertWithUserGeneratedKey() { + final NewEntityTest entity1 = new NewEntityTest("test1"); + final NewEntityTest entity2 = new NewEntityTest("test2"); + template.upsert(Arrays.asList(entity1, entity2), UpsertStrategy.REPLACE); + assertThat(template.collection(NewEntityTest.class).count(), is(2L)); + entity1.setPersisted(true); + entity2.setPersisted(true); + template.upsert(Arrays.asList(entity1, entity2), UpsertStrategy.REPLACE); + assertThat(template.collection(NewEntityTest.class).count(), is(2L)); + } + +} diff --git a/src/test/java/com/arangodb/springframework/core/util/AqlUtilsTest.java b/src/test/java/com/arangodb/springframework/core/util/AqlUtilsTest.java new file mode 100644 index 000000000..d35848e3c --- /dev/null +++ b/src/test/java/com/arangodb/springframework/core/util/AqlUtilsTest.java @@ -0,0 +1,195 @@ +/* + * 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.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; + +/** + * + * @author Christian Lechner + */ +public class AqlUtilsTest { + + @Test + public void buildLimitClauseTest() { + assertThat(AqlUtils.buildLimitClause(null), is("")); + assertThat(AqlUtils.buildLimitClause(new PageRequest(0, 1)), is("LIMIT 0, 1")); + assertThat(AqlUtils.buildLimitClause(new PageRequest(10, 20)), is("LIMIT 200, 20")); + } + + @Test + public void buildPageableClauseTest() { + // Special cases + assertThat(AqlUtils.buildPageableClause(null), is("")); + + // Paging without sort + assertThat(AqlUtils.buildPageableClause(new PageRequest(0, 1)), is("LIMIT 0, 1")); + assertThat(AqlUtils.buildPageableClause(new PageRequest(5, 10)), is("LIMIT 50, 10")); + + // Paging with sort + assertThat(AqlUtils.buildPageableClause(new PageRequest(2, 10, Direction.ASC, "property")), + is("SORT `property` ASC LIMIT 20, 10")); + assertThat(AqlUtils.buildPageableClause(new PageRequest(2, 10, Direction.ASC, "property"), "var"), + is("SORT `var`.`property` ASC LIMIT 20, 10")); + + assertThat(AqlUtils.buildPageableClause(new PageRequest(2, 10, Direction.DESC, "property", "property2")), + is("SORT `property` DESC, `property2` DESC LIMIT 20, 10")); + assertThat(AqlUtils.buildPageableClause(new PageRequest(2, 10, Direction.DESC, "property", "property2"), "var"), + is("SORT `var`.`property` DESC, `var`.`property2` DESC LIMIT 20, 10")); + + assertThat( + AqlUtils.buildPageableClause( + new PageRequest(2, 10, new Sort("ascProp").and(new Sort(Direction.DESC, "descProp")))), + is("SORT `ascProp` ASC, `descProp` DESC LIMIT 20, 10")); + assertThat( + AqlUtils.buildPageableClause( + new PageRequest(2, 10, new Sort("ascProp").and(new Sort(Direction.DESC, "descProp"))), "var"), + is("SORT `var`.`ascProp` ASC, `var`.`descProp` DESC LIMIT 20, 10")); + } + + @Test + public void buildSortClauseTest() { + // Special cases + assertThat(AqlUtils.buildSortClause(null), is("")); + + // Others + assertThat(AqlUtils.buildSortClause(new Sort("property")), is("SORT `property` ASC")); + assertThat(AqlUtils.buildSortClause(new Sort("property"), "var"), is("SORT `var`.`property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort(Direction.DESC, "property")), is("SORT `property` DESC")); + assertThat(AqlUtils.buildSortClause(new Sort(Direction.DESC, "property"), "var"), + is("SORT `var`.`property` DESC")); + + assertThat(AqlUtils.buildSortClause(new Sort(Direction.DESC, "property", "property2")), + is("SORT `property` DESC, `property2` DESC")); + assertThat(AqlUtils.buildSortClause(new Sort(Direction.DESC, "property", "property2"), "var"), + is("SORT `var`.`property` DESC, `var`.`property2` DESC")); + + assertThat(AqlUtils.buildSortClause(new Sort(Direction.DESC, "property").and(new Sort("property2"))), + is("SORT `property` DESC, `property2` ASC")); + assertThat(AqlUtils.buildSortClause(new Sort(Direction.DESC, "property").and(new Sort("property2")), "var"), + is("SORT `var`.`property` DESC, `var`.`property2` ASC")); + } + + @Test + public void sortClauseEscapingTest() { + assertThat(AqlUtils.buildSortClause(new Sort("property")), is("SORT `property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("`property`")), is("SORT `property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("`pro\\`perty\\``")), is("SORT `pro\\`perty\\`` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("`dont.split.property`")), is("SORT `dont.split.property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("property.`property`")), is("SORT `property`.`property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("property.`.`.`property`")), + is("SORT `property`.`.`.`property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("property.\\.property")), + is("SORT `property`.`\\\\`.`property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("property.\\\\`.property")), + is("SORT `property`.`\\\\\\``.`property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("`property.\\`.property`")), + is("SORT `property.\\`.property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("`property.\\``.property")), + is("SORT `property.\\``.`property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("`property..property`")), is("SORT `property..property` ASC")); + + assertThat(AqlUtils.buildSortClause(new Sort("property\\. REMOVE doc IN collection //")), + is("SORT `property\\\\`.` REMOVE doc IN collection //` ASC")); + + // Illegal sort properties + + try { + AqlUtils.buildSortClause(new Sort(".property")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + try { + AqlUtils.buildSortClause(new Sort("property.")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + try { + AqlUtils.buildSortClause(new Sort("property..property")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + try { + AqlUtils.buildSortClause(new Sort("property.`property")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + try { + AqlUtils.buildSortClause(new Sort("pro`perty.property")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + try { + AqlUtils.buildSortClause(new Sort("`property``.property")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + try { + AqlUtils.buildSortClause(new Sort("`property```.property")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + try { + AqlUtils.buildSortClause(new Sort("`property.`\\`.property`")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + try { + AqlUtils.buildSortClause(new Sort("`property.`\\``.property`")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + try { + AqlUtils.buildSortClause(new Sort("`property`.\\``.property`")); + Assert.fail(); + } catch (final IllegalArgumentException e) { + } + + } + +} diff --git a/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java b/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java index c663f66e7..d1881a54a 100644 --- a/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java +++ b/src/test/java/com/arangodb/springframework/repository/ArangoRepositoryTest.java @@ -1,7 +1,10 @@ package com.arangodb.springframework.repository; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.ArrayList; @@ -12,6 +15,7 @@ import org.junit.runner.RunWith; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.domain.ExampleMatcher.StringMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -324,4 +328,19 @@ public void endingWithByExampleNestedIncludeNullTest() { final Customer retrieved = repository.findOne(example); assertEquals(check, retrieved); } + + @Test + public void containingExampleTest() { + final Customer entity = new Customer("name", "surname", 10); + repository.save(entity); + + final Customer probe = new Customer(); + probe.setName("am"); + final Example example = Example.of(probe, + ExampleMatcher.matching().withStringMatcher(StringMatcher.CONTAINING).withIgnorePaths("arangoId", "id", + "key", "rev", "surname", "age")); + final Customer retrieved = repository.findOne(example); + assertThat(retrieved, is(notNullValue())); + assertThat(retrieved.getName(), is("name")); + } } diff --git a/src/test/java/com/arangodb/springframework/repository/CustomerRepository.java b/src/test/java/com/arangodb/springframework/repository/CustomerRepository.java index 3db5a17d0..69d1257ab 100644 --- a/src/test/java/com/arangodb/springframework/repository/CustomerRepository.java +++ b/src/test/java/com/arangodb/springframework/repository/CustomerRepository.java @@ -1,9 +1,9 @@ package com.arangodb.springframework.repository; +import java.time.Instant; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import org.springframework.data.domain.Page; @@ -18,20 +18,24 @@ import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; +import org.springframework.data.repository.query.Param; import com.arangodb.ArangoCursor; import com.arangodb.entity.BaseDocument; import com.arangodb.model.AqlQueryOptions; import com.arangodb.springframework.annotation.BindVars; -import com.arangodb.springframework.annotation.Param; import com.arangodb.springframework.annotation.Query; import com.arangodb.springframework.annotation.QueryOptions; -import com.arangodb.springframework.repository.ArangoRepository; import com.arangodb.springframework.repository.query.derived.geo.Ring; import com.arangodb.springframework.testdata.Customer; +import com.arangodb.springframework.testdata.CustomerNameProjection; /** - * Created by F625633 on 07/07/2017. + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner */ public interface CustomerRepository extends ArangoRepository { @@ -42,48 +46,33 @@ public interface CustomerRepository extends ArangoRepository { @QueryOptions(cache = true, ttl = 128) BaseDocument findOneByIdAndNameAql(String id, String name); - @Query("FOR c IN customer FILTER c._id == @0 RETURN c") - Optional findOneByIdAqlPotentialNameClash(@Param("0") String id); - - @Query("FOR c IN customer FILTER c._id == @0 AND c.name == @0 RETURN c") - ArangoCursor findOneByIdAqlParamNameClash(String id, @Param("0") String name); - @QueryOptions(maxPlans = 1000, ttl = 128) @Query("FOR c IN customer FILTER c._id == @id AND c.name == @name RETURN c") - ArangoCursor findOneByBindVarsAql( - AqlQueryOptions options, - @SuppressWarnings("rawtypes") @BindVars Map bindVars); + ArangoCursor findOneByBindVarsAql(AqlQueryOptions options, @BindVars Map bindVars); @Query("FOR c IN customer FILTER c._id == @id AND c.name == @name RETURN c") Customer findOneByNameAndBindVarsAql(@Param("name") String name, @BindVars Map bindVars); - @Query("FOR c IN customer FILTER c._id == @id AND c.name == @name RETURN c") - Customer findOneByBindVarsAndClashingParametersAql( - @BindVars Map bindVars, - @Param("name") String name, - AqlQueryOptions options, - @Param("name") String name2); - - @Query("FOR c IN customer FILTER c.name == @name RETURN c") - Customer findOneByNameWithDuplicateOptionsAql( - @Param("name") String name, - AqlQueryOptions options, - AqlQueryOptions options2); - @Query("FOR c IN customer FILTER c._id == @id AND c.name == @0 RETURN c") Customer findOneByIdAndNameWithBindVarsAql(String name, @BindVars Map bindVars); @Query("FOR c IN @@0 FILTER \"@1\" != '@2' AND c._id == @1 RETURN c") - Customer findOneByIdInCollectionAql(String collection, String id, String id2); + Customer findOneByIdInCollectionAqlWithUnusedParam(String collection, String id, String id2); - @Query("FOR c IN @@collection FILTER \"\\\"@1\\\"\" != '\"@2\"' AND c._id == @1 RETURN c") - Customer findOneByIdInNamedCollectionAql(@Param("@collection") String collection, String id, String id2); + @Query("FOR c IN @@collection FILTER \"\\\"@id\\\"\" != '\"@id2\"' AND c._id == @id RETURN c") + Customer findOneByIdInNamedCollectionAqlWithUnusedParam( + @Param("@collection") String collection, + @Param("id") String id, + @Param("id2") String id2); - @Query("FOR c IN @@collection FILTER \"'@1'\" != '\\'@2\\'' AND c._id == @1 RETURN c") - Customer findOneByIdInIncorrectNamedCollectionAql(@Param("collection") String collection, String id, String id2); + @Query("FOR c IN @@collection FILTER \"'@id'\" != '\\'@id2\\'' AND c._id == @id RETURN c") + Customer findOneByIdInIncorrectNamedCollectionAql( + @Param("collection") String collection, + @Param("id") String id, + @Param("id2") String id2); - @Query("FOR c IN @collection FILTER c._id == @1 RETURN c") - Customer findOneByIdInNamedCollectionAqlRejected(@Param("collection") String collection, String id); + @Query("FOR c IN @collection FILTER c._id == @id RETURN c") + Customer findOneByIdInNamedCollectionAqlRejected(@Param("collection") String collection, @Param("id") String id); @Query("FOR c in customer FILTER c.surname == @0 RETURN c") List findManyBySurname(String surname); @@ -109,6 +98,10 @@ Collection findTop2DistinctByStringArrayContainingIgnoreCaseOrIntegerL Collection findByStringArrayNotContainingIgnoreCase(String string); + Collection findByNameContaining(String string); + + Collection findByNameContainingIgnoreCase(String string); + int countByAgeGreaterThanOrStringArrayNullAndIntegerList(int age, List integerList); Integer countDistinctByAliveTrueOrNameLikeOrAgeLessThanEqual(String pattern, int age); @@ -161,10 +154,16 @@ List findByNameOrLocationWithinOrNameAndSurnameOrNameAndLocationNearAn Customer[] findByNameOrderBySurnameAsc(Sort sort, String name); + @Query("FOR c IN customer FILTER c.name == @1 #sort RETURN c") + List findByNameWithSort(Sort sort, String name); + // PAGEABLE Page readByNameAndSurname(Pageable pageable, String name, AqlQueryOptions options, String surname); + @Query("FOR c IN customer FILTER c.name == @1 AND c.surname == @2 #pageable RETURN c") + Page findByNameAndSurnameWithPageable(Pageable pageable, String name, String surname); + // GEO_RESULT, GEO_RESULTS, GEO_PAGE GeoResult queryByLocationWithin(Point location, double distance); @@ -197,5 +196,38 @@ List findByNestedCustomersNestedCustomerShoppingCartProductsLocationWi List getByOwnsName(String name); + List getByOwnsNameAndOwns2Name(String name, String name2); + List getByOwnsContainsName(String name); + + // Count query + + @Query("RETURN COUNT(@@collection)") + long queryCount(@Param("@collection") Class collection); + + // Date query + + @Query("RETURN DATE_ISO8601(1474988621)") + Instant queryDate(); + + // Named query + + Customer findOneByIdNamedQuery(@Param("id") String id); + + // Static projection + + @Query("FOR c IN customer FILTER c._id == @id RETURN c") + CustomerNameProjection findOneByIdWithStaticProjection(@Param("id") String id); + + @Query("FOR c IN customer FILTER c.age >= 18 RETURN c") + List findManyLegalAgeWithStaticProjection(); + + // Dynamic projection + + @Query("FOR c IN customer FILTER c._id == @id RETURN c") + T findOneByIdWithDynamicProjection(@Param("id") String id, Class projection); + + @Query("FOR c IN customer FILTER c.age >= 18 RETURN c") + List findManyLegalAgeWithDynamicProjection(Class projection); + } diff --git a/src/test/java/com/arangodb/springframework/repository/OverriddenCrudMethodsRepository.java b/src/test/java/com/arangodb/springframework/repository/OverriddenCrudMethodsRepository.java new file mode 100644 index 000000000..42ef3eeb7 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/repository/OverriddenCrudMethodsRepository.java @@ -0,0 +1,37 @@ +/* + * 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 com.arangodb.springframework.annotation.Query; +import com.arangodb.springframework.testdata.Customer; + +/** + * + * @author Christian Lechner + * + */ +public interface OverriddenCrudMethodsRepository extends ArangoRepository { + + @Override + @Query("RETURN NULL") + Iterable findAll(); + +} diff --git a/src/test/java/com/arangodb/springframework/repository/query/ArangoAqlQueryTest.java b/src/test/java/com/arangodb/springframework/repository/query/ArangoAqlQueryTest.java index 483098fc0..1c1367d6e 100644 --- a/src/test/java/com/arangodb/springframework/repository/query/ArangoAqlQueryTest.java +++ b/src/test/java/com/arangodb/springframework/repository/query/ArangoAqlQueryTest.java @@ -1,16 +1,27 @@ package com.arangodb.springframework.repository.query; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.collection.IsIn.isOneOf; +import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import java.time.Instant; import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.arangodb.ArangoCursor; @@ -19,22 +30,29 @@ import com.arangodb.model.AqlQueryOptions; import com.arangodb.springframework.core.convert.DBDocumentEntity; import com.arangodb.springframework.repository.AbstractArangoRepositoryTest; +import com.arangodb.springframework.repository.OverriddenCrudMethodsRepository; import com.arangodb.springframework.testdata.Customer; +import com.arangodb.springframework.testdata.CustomerNameProjection; /** - * Created by F625633 on 12/07/2017. + * + * @author Audrius Malele + * @author Mark McCormick + * @author Mark Vollmary + * @author Christian Lechner */ @RunWith(SpringJUnit4ClassRunner.class) public class ArangoAqlQueryTest extends AbstractArangoRepositoryTest { - private final AqlQueryOptions OPTIONS = new AqlQueryOptions(); + @Autowired + protected OverriddenCrudMethodsRepository overriddenRepository; @Test public void findOneByIdAqlWithNamedParameterTest() { repository.save(customers); - final Map retrieved = repository.findOneByIdAqlWithNamedParameter(john.getId(), OPTIONS); - final Customer retrievedCustomer = template.getConverter().read(Customer.class, - new DBDocumentEntity(retrieved)); + final Map retrieved = repository.findOneByIdAqlWithNamedParameter(john.getId(), + new AqlQueryOptions()); + final Customer retrievedCustomer = template.getConverter().read(Customer.class, new DBDocumentEntity(retrieved)); assertEquals(john, retrievedCustomer); } @@ -48,42 +66,18 @@ public void findOneByIdAndNameAqlTest() { allProperties.put("_rev", retrieved.getRevision()); retrieved.getProperties().forEach((k, v) -> allProperties.put(k, v)); final Customer retrievedCustomer = template.getConverter().read(Customer.class, - new DBDocumentEntity(allProperties)); + new DBDocumentEntity(allProperties)); assertEquals(john, retrievedCustomer); } - @Test - public void findOneByIdAqlPotentialNameClashTest() { - repository.save(customers); - final Optional retrieved = repository.findOneByIdAqlPotentialNameClash(john.getId()); - assertEquals(john, retrieved.get()); - } - - @Test(expected = IllegalArgumentException.class) - public void findOneByIdAqlParamNameClashTest() { - repository.save(customers); - final ArangoCursor retrieved = repository.findOneByIdAqlParamNameClash(john.getId(), john.getName()); - assertEquals(john, retrieved.next()); - } - @Test public void findOneByBindVarsAqlTest() { repository.save(customers); final Map bindVars = new HashMap<>(); bindVars.put("id", john.getId()); bindVars.put("name", john.getName()); - final ArangoCursor retrieved = repository.findOneByBindVarsAql(OPTIONS.ttl(127).cache(true), - bindVars); - assertEquals(john, retrieved.next()); - } - - @Test(expected = ClassCastException.class) - public void findOneByBindVarsOfIllegalTypeAqlTest() { - repository.save(customers); - final Map bindVars = new HashMap<>(); - bindVars.put(1, john.getId()); - bindVars.put(2, john.getName()); - final ArangoCursor retrieved = repository.findOneByBindVarsAql(OPTIONS, bindVars); + final ArangoCursor retrieved = repository.findOneByBindVarsAql(new AqlQueryOptions().ttl(127).cache(true), + bindVars); assertEquals(john, retrieved.next()); } @@ -106,24 +100,6 @@ public void findOneByOverridingNameAndBindVarsAqlTest() { assertEquals(john, retrieved); } - @Test(expected = IllegalArgumentException.class) - public void findOneByBindVarsAndClashingParametersAqlTest() { - repository.save(customers); - final Map bindVars = new HashMap<>(); - bindVars.put("id", john.getId()); - bindVars.put("name", john.getName()); - final Customer retrieved = repository.findOneByBindVarsAndClashingParametersAql(bindVars, john.getName(), - OPTIONS, john.getName()); - assertEquals(john, retrieved); - } - - @Test(expected = IllegalArgumentException.class) - public void findOneByNameWithDuplicateOptionsAqlTest() { - repository.save(customers); - final Customer retrieved = repository.findOneByNameWithDuplicateOptionsAql(john.getName(), OPTIONS, OPTIONS); - assertEquals(john, retrieved); - } - @Test public void findOneByIdAndNameWithBindVarsAqlTest() { repository.save(customers); @@ -134,19 +110,19 @@ public void findOneByIdAndNameWithBindVarsAqlTest() { assertEquals(john, retrieved); } - @Test - public void findOneByIdInCollectionAqlTest() { + @Test(expected = ArangoDBException.class) + public void findOneByIdInCollectionAqlWithUnusedParamTest() { repository.save(customers); - final Customer retrieved = repository.findOneByIdInCollectionAql(john.getId().split("/")[0], john.getId(), - john.getId()); + final Customer retrieved = repository.findOneByIdInCollectionAqlWithUnusedParam(john.getId().split("/")[0], + john.getId(), john.getId()); assertEquals(john, retrieved); } - @Test - public void findOneByIdInNamedCollectionAqlTest() { + @Test(expected = ArangoDBException.class) + public void findOneByIdInNamedCollectionAqlWithUnusedParamTest() { repository.save(customers); - final Customer retrieved = repository.findOneByIdInNamedCollectionAql(john.getId().split("/")[0], john.getId(), - john.getId()); + final Customer retrieved = repository.findOneByIdInNamedCollectionAqlWithUnusedParam(john.getId().split("/")[0], + john.getId(), john.getId()); assertEquals(john, retrieved); } @@ -154,7 +130,7 @@ public void findOneByIdInNamedCollectionAqlTest() { public void findOneByIdInIncorrectNamedCollectionAqlTest() { repository.save(customers); final Customer retrieved = repository.findOneByIdInIncorrectNamedCollectionAql(john.getId().split("/")[0], - john.getId(), john.getId()); + john.getId(), john.getId()); assertEquals(john, retrieved); } @@ -162,7 +138,7 @@ public void findOneByIdInIncorrectNamedCollectionAqlTest() { public void findOneByIdInNamedCollectionAqlRejectedTest() { repository.save(customers); final Customer retrieved = repository.findOneByIdInNamedCollectionAqlRejected(john.getId().split("/")[0], - john.getId()); + john.getId()); assertEquals(john, retrieved); } @@ -176,4 +152,96 @@ public void findManyBySurnameTest() { assertTrue(equals(retrieved, toBeRetrieved, cmp, eq, false)); } + + @Test + public void queryCount() { + assertEquals(repository.queryCount(Customer.class), 0L); + } + + @Test + public void queryDate() { + assertEquals(repository.queryDate(), Instant.ofEpochMilli(1474988621)); + } + + @Test + public void findOneByIdNamedQueryTest() { + repository.save(customers); + final Customer retrieved = repository.findOneByIdNamedQuery(john.getId()); + assertEquals(john, retrieved); + } + + @Test + public void findOneByIdWithStaticProjectionTest() { + repository.save(customers); + final CustomerNameProjection retrieved = repository.findOneByIdWithStaticProjection(john.getId()); + assertEquals(retrieved.getName(), john.getName()); + } + + @Test + public void findManyLegalAgeWithStaticProjectionTest() { + repository.save(customers); + final List retrieved = repository.findManyLegalAgeWithStaticProjection(); + for (final CustomerNameProjection proj : retrieved) { + assertThat(proj.getName(), isOneOf(john.getName(), bob.getName())); + } + } + + @Test + public void findOneByIdWithDynamicProjectionTest() { + repository.save(customers); + final CustomerNameProjection retrieved = repository.findOneByIdWithDynamicProjection(john.getId(), + CustomerNameProjection.class); + assertEquals(retrieved.getName(), john.getName()); + } + + @Test + public void findManyLegalAgeWithDynamicProjectionTest() { + repository.save(customers); + final List retrieved = repository + .findManyLegalAgeWithDynamicProjection(CustomerNameProjection.class); + for (final CustomerNameProjection proj : retrieved) { + assertThat(proj.getName(), isOneOf(john.getName(), bob.getName())); + } + } + + @Test + public void pageableTest() { + final List toBeRetrieved = new LinkedList<>(); + repository.save(new Customer("A", "A", 0)); + repository.save(new Customer("A", "A", 1)); + toBeRetrieved.add(new Customer("A", "A", 2)); + repository.save(new Customer("B", "B", 3)); + toBeRetrieved.add(new Customer("A", "A", 4)); + repository.save(new Customer("A", "A", 5)); + repository.save(toBeRetrieved); + final Pageable pageable = new PageRequest(1, 2, new Sort("c.age")); + final Page retrieved = repository.findByNameAndSurnameWithPageable(pageable, "A", "A"); + assertThat(retrieved.getTotalElements(), is(5L)); + assertThat(retrieved.getTotalPages(), is(3)); + assertThat(retrieved.getContent(), is(toBeRetrieved)); + } + + @Test + public void sortTest() { + final List toBeRetrieved = new LinkedList<>(); + toBeRetrieved.add(new Customer("A", "B", 2)); + toBeRetrieved.add(new Customer("A", "B", 3)); + toBeRetrieved.add(new Customer("A", "A", 1)); + repository.save(toBeRetrieved.get(1)); + repository.save(toBeRetrieved.get(0)); + repository.save(toBeRetrieved.get(2)); + repository.save(new Customer("C", "C", 0)); + final List retrieved = repository + .findByNameWithSort(new Sort(Direction.DESC, "c.surname").and(new Sort("c.age")), "A"); + assertThat(retrieved, is(toBeRetrieved)); + } + + @Test + public void overriddenCrudMethodsTest() { + overriddenRepository.save(customers); + Iterator customers = overriddenRepository.findAll().iterator(); + assertThat(customers.hasNext(), is(true)); + assertThat(customers.next(), is(nullValue())); + } + } diff --git a/src/test/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreatorTest.java b/src/test/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreatorTest.java index b97d5d329..94e3883f7 100644 --- a/src/test/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreatorTest.java +++ b/src/test/java/com/arangodb/springframework/repository/query/derived/DerivedQueryCreatorTest.java @@ -8,8 +8,10 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Set; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.data.domain.Page; @@ -160,6 +162,41 @@ public void NotContainingTest() { assertTrue(equals(retrieved, toBeRetrieved, cmp, eq, false)); } + @Test + public void findByStringContaining() { + final Customer c1 = new Customer("abc", "", 0); + final Customer c2 = new Customer("Abc", "", 0); + final Customer c3 = new Customer("abcd", "", 0); + final Customer c4 = new Customer("ab", "", 0); + repository.save(c1); + repository.save(c2); + repository.save(c3); + repository.save(c4); + final Collection toBeRetrieved = new LinkedList<>(); + toBeRetrieved.add(c1); + toBeRetrieved.add(c3); + final Collection retrieved = repository.findByNameContaining("abc"); + assertTrue(equals(retrieved, toBeRetrieved, cmp, eq, false)); + } + + @Test + public void findByStringContainingIgnoreCase() { + final Customer c1 = new Customer("abc", "", 0); + final Customer c2 = new Customer("Abc", "", 0); + final Customer c3 = new Customer("abcd", "", 0); + final Customer c4 = new Customer("ab", "", 0); + repository.save(c1); + repository.save(c2); + repository.save(c3); + repository.save(c4); + final Collection toBeRetrieved = new LinkedList<>(); + toBeRetrieved.add(c1); + toBeRetrieved.add(c2); + toBeRetrieved.add(c3); + final Collection retrieved = repository.findByNameContainingIgnoreCase("abc"); + assertTrue(equals(retrieved, toBeRetrieved, cmp, eq, false)); + } + @Test public void findTest() { final Customer customer1 = new Customer("%_\\name", "%surname%", 20); @@ -619,6 +656,7 @@ public void referenceTest() { } @Test + @Ignore // https://github.com/arangodb/arangodb/issues/5303 public void referenceGeospatialTest() { final List toBeRetrieved = new LinkedList<>(); final Customer customer1 = new Customer("", "", 0); @@ -698,6 +736,36 @@ public void relationsSingleLevelTest() { assertTrue(equals(toBeRetrieved, retrieved, cmp, eq, false)); } + @Test + public void twoRelationsSingleLevelTest() { + final List toBeRetrieved = new LinkedList<>(); + final List customers = new LinkedList<>(); + List retrieved; + final Customer john = new Customer("John", "Smith", 52); + final Customer adam = new Customer("Adam", "Smith", 294); + final Customer matt = new Customer("Matt", "Smith", 34); + final Product phone = new Product("phone"); + final Product car = new Product("car"); + final Product chair = new Product("chair"); + template.insert(phone); + template.insert(car); + template.insert(chair); + customers.add(john); + customers.add(matt); + customers.add(adam); + repository.save(customers); + template.insert(new Owns(john, phone)); + template.insert(new Owns(john, car)); + template.insert(new Owns(adam, chair)); + template.insert(new Owns(matt, phone)); + template.insert(new Owns(matt, car)); + template.insert(new Owns(matt, chair)); + toBeRetrieved.add(john); + toBeRetrieved.add(matt); + retrieved = repository.getByOwnsNameAndOwns2Name(phone.getName(), phone.getName()); + assertTrue(equals(toBeRetrieved, retrieved, cmp, eq, false)); + } + @Test public void relationsMultiLevelTest() { final List toBeRetrieved = new LinkedList<>(); @@ -744,8 +812,8 @@ private double convertAngleToDistance(final int angle) { } private double getDistanceBetweenPoints(final Point point1, final Point point2) { - final String query = String.format("RETURN DISTANCE(%f, %f, %f, %f)", point1.getY(), point1.getX(), - point2.getY(), point2.getX()); + final String query = String.format(Locale.ENGLISH, "RETURN DISTANCE(%f, %f, %f, %f)", point1.getY(), + point1.getX(), point2.getY(), point2.getX()); return template.query(query, new HashMap<>(), null, Double.class).next(); } } diff --git a/src/test/java/com/arangodb/springframework/testdata/Customer.java b/src/test/java/com/arangodb/springframework/testdata/Customer.java index 28c12cf94..162b9261a 100644 --- a/src/test/java/com/arangodb/springframework/testdata/Customer.java +++ b/src/test/java/com/arangodb/springframework/testdata/Customer.java @@ -1,247 +1,259 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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 java.util.Collection; - -import org.springframework.data.annotation.Id; - -import com.arangodb.springframework.annotation.Document; -import com.arangodb.springframework.annotation.GeoIndexed; -import com.arangodb.springframework.annotation.Key; -import com.arangodb.springframework.annotation.Ref; -import com.arangodb.springframework.annotation.Relations; -import com.arangodb.springframework.annotation.Rev; - -/** - * @author Mark Vollmary - * - */ -@Document -public class Customer { - - @Id - private String id; - @Key - private String key; - @Rev - private String rev; - private String name; - private String surname; - private int age; - private Address address; - - private boolean alive; - @GeoIndexed - private int[] location; - private Iterable integerList; - private String[] stringArray; - private Iterable stringList; - private Customer nestedCustomer; - private Iterable nestedCustomers; - - @Ref - private ShoppingCart shoppingCart; - @Relations(edges = { Owns.class }) - private Collection owns; - - public Customer() { - super(); - } - - public Customer(final String name, final String surname, final int age) { - super(); - this.name = name; - this.surname = surname; - this.age = age; - } - - public Customer(final String name, final String surname, final int age, final boolean alive) { - super(); - this.name = name; - this.surname = surname; - this.age = age; - this.alive = alive; - } - - public Customer(final String name, final String surname, final int age, final Address address) { - super(); - this.name = name; - this.surname = surname; - this.age = age; - this.address = address; - } - - public String getId() { - return id; - } - - public void setId(final String id) { - this.id = id; - } - - public String getKey() { - return key; - } - - public void setKey(final String key) { - this.key = key; - } - - public String getRev() { - return rev; - } - - public void setRev(final String rev) { - this.rev = rev; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public String getSurname() { - return surname; - } - - public void setSurname(final String surname) { - this.surname = surname; - } - - public int getAge() { - return age; - } - - public void setAge(final int age) { - this.age = age; - } - - public Address getAddress() { - return address; - } - - public void setAddress(final Address address) { - this.address = address; - } - - public ShoppingCart getShoppingCart() { - return shoppingCart; - } - - public void setShoppingCart(final ShoppingCart shoppingCart) { - this.shoppingCart = shoppingCart; - } - - public Collection getOwns() { - return owns; - } - - public void setOwns(final Collection owns) { - this.owns = owns; - } - - public boolean isAlive() { - return alive; - } - - public void setAlive(final boolean alive) { - this.alive = alive; - } - - public int[] getLocation() { - return location; - } - - public void setLocation(final int[] location) { - this.location = location; - } - - public Iterable getIntegerList() { - return integerList; - } - - public void setIntegerList(final Iterable integerList) { - this.integerList = integerList; - } - - public String[] getStringArray() { - return stringArray; - } - - public void setStringArray(final String[] stringArray) { - this.stringArray = stringArray; - } - - public void setStringList(final Iterable stringList) { - this.stringList = stringList; - } - - public Customer getNestedCustomer() { - return nestedCustomer; - } - - public void setNestedCustomer(final Customer nestedCustomer) { - this.nestedCustomer = nestedCustomer; - } - - public Iterable getNestedCustomers() { - return nestedCustomers; - } - - public void setNestedCustomers(final Iterable nestedCustomers) { - this.nestedCustomers = nestedCustomers; - } - - public Iterable getStringList() { - return stringList; - } - - @Override - public boolean equals(final Object o) { - if (!(o instanceof Customer)) { - return false; - } - final Customer customer = (Customer) o; - if (!customer.getId().equals(this.getId())) { - return false; - } - if (!customer.getName().equals(this.getName())) { - return false; - } - if (!customer.getSurname().equals(this.getSurname())) { - return false; - } - if (customer.getAge() != this.getAge()) { - return false; - } - return true; - } - - @Override - public String toString() { - return "Customer {id: " + id + ", name: " + name + ", surname: " + surname + ", age: " + age + "}"; - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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 java.util.Collection; + +import org.springframework.data.annotation.Id; + +import com.arangodb.springframework.annotation.Document; +import com.arangodb.springframework.annotation.GeoIndexed; +import com.arangodb.springframework.annotation.Key; +import com.arangodb.springframework.annotation.Ref; +import com.arangodb.springframework.annotation.Relations; +import com.arangodb.springframework.annotation.Rev; + +/** + * @author Mark Vollmary + * + */ +@SuppressWarnings("deprecation") +@Document +public class Customer { + + @Id + private String id; + @Key + private String key; + @Rev + private String rev; + private String name; + private String surname; + private int age; + private Address address; + + private boolean alive; + @GeoIndexed + private int[] location; + private Iterable integerList; + private String[] stringArray; + private Iterable stringList; + private Customer nestedCustomer; + private Iterable nestedCustomers; + + @Ref + private ShoppingCart shoppingCart; + @Relations(edges = { Owns.class }) + private Collection owns; + + @Relations(edges = { Owns.class }) + private Collection owns2; + + public Customer() { + super(); + } + + public Customer(final String name, final String surname, final int age) { + super(); + this.name = name; + this.surname = surname; + this.age = age; + } + + public Customer(final String name, final String surname, final int age, final boolean alive) { + super(); + this.name = name; + this.surname = surname; + this.age = age; + this.alive = alive; + } + + public Customer(final String name, final String surname, final int age, final Address address) { + super(); + this.name = name; + this.surname = surname; + this.age = age; + this.address = address; + } + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(final String key) { + this.key = key; + } + + public String getRev() { + return rev; + } + + public void setRev(final String rev) { + this.rev = rev; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public String getSurname() { + return surname; + } + + public void setSurname(final String surname) { + this.surname = surname; + } + + public int getAge() { + return age; + } + + public void setAge(final int age) { + this.age = age; + } + + public Address getAddress() { + return address; + } + + public void setAddress(final Address address) { + this.address = address; + } + + public ShoppingCart getShoppingCart() { + return shoppingCart; + } + + public void setShoppingCart(final ShoppingCart shoppingCart) { + this.shoppingCart = shoppingCart; + } + + public Collection getOwns() { + return owns; + } + + public void setOwns(final Collection owns) { + this.owns = owns; + } + + public Collection getOwns2() { + return owns2; + } + + public void setOwns2(final Collection owns2) { + this.owns2 = owns2; + } + + public boolean isAlive() { + return alive; + } + + public void setAlive(final boolean alive) { + this.alive = alive; + } + + public int[] getLocation() { + return location; + } + + public void setLocation(final int[] location) { + this.location = location; + } + + public Iterable getIntegerList() { + return integerList; + } + + public void setIntegerList(final Iterable integerList) { + this.integerList = integerList; + } + + public String[] getStringArray() { + return stringArray; + } + + public void setStringArray(final String[] stringArray) { + this.stringArray = stringArray; + } + + public void setStringList(final Iterable stringList) { + this.stringList = stringList; + } + + public Customer getNestedCustomer() { + return nestedCustomer; + } + + public void setNestedCustomer(final Customer nestedCustomer) { + this.nestedCustomer = nestedCustomer; + } + + public Iterable getNestedCustomers() { + return nestedCustomers; + } + + public void setNestedCustomers(final Iterable nestedCustomers) { + this.nestedCustomers = nestedCustomers; + } + + public Iterable getStringList() { + return stringList; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof Customer)) { + return false; + } + final Customer customer = (Customer) o; + if (!customer.getId().equals(this.getId())) { + return false; + } + if (!customer.getName().equals(this.getName())) { + return false; + } + if (!customer.getSurname().equals(this.getSurname())) { + return false; + } + if (customer.getAge() != this.getAge()) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Customer {id: " + id + ", name: " + name + ", surname: " + surname + ", age: " + age + "}"; + } + +} diff --git a/src/test/java/com/arangodb/springframework/testdata/CustomerNameProjection.java b/src/test/java/com/arangodb/springframework/testdata/CustomerNameProjection.java new file mode 100644 index 000000000..425e98f99 --- /dev/null +++ b/src/test/java/com/arangodb/springframework/testdata/CustomerNameProjection.java @@ -0,0 +1,7 @@ +package com.arangodb.springframework.testdata; + +public interface CustomerNameProjection { + + String getName(); + +} diff --git a/src/test/java/com/arangodb/springframework/testdata/IncompleteCustomer.java b/src/test/java/com/arangodb/springframework/testdata/IncompleteCustomer.java index 9a208ff4e..ae515a2f8 100644 --- a/src/test/java/com/arangodb/springframework/testdata/IncompleteCustomer.java +++ b/src/test/java/com/arangodb/springframework/testdata/IncompleteCustomer.java @@ -8,6 +8,7 @@ import com.arangodb.springframework.annotation.Key; import com.arangodb.springframework.annotation.Rev; +@SuppressWarnings("deprecation") @Document("customer") public class IncompleteCustomer { @Id diff --git a/src/test/java/com/arangodb/springframework/testdata/Material.java b/src/test/java/com/arangodb/springframework/testdata/Material.java index d7f0dbed7..2f78229a9 100644 --- a/src/test/java/com/arangodb/springframework/testdata/Material.java +++ b/src/test/java/com/arangodb/springframework/testdata/Material.java @@ -1,31 +1,33 @@ package com.arangodb.springframework.testdata; +import org.springframework.data.annotation.Id; + import com.arangodb.springframework.annotation.Key; import com.arangodb.springframework.annotation.Rev; -import org.springframework.data.annotation.Id; /** * Created by markmccormick on 24/08/2017. */ +@SuppressWarnings("deprecation") public class Material { - @Id - private String id; - @Key - private String key; - @Rev - private String rev; - private String name; + @Id + private String id; + @Key + private String key; + @Rev + private String rev; + private String name; - public Material(String name) { - this.name = name; - } + public Material(final String name) { + this.name = name; + } - public String getName() { - return name; - } + public String getName() { + return name; + } - public void setName(String name) { - this.name = name; - } + public void setName(final String name) { + this.name = name; + } } diff --git a/src/test/java/com/arangodb/springframework/testdata/Product.java b/src/test/java/com/arangodb/springframework/testdata/Product.java index a81bd9daa..d1aaff4e8 100644 --- a/src/test/java/com/arangodb/springframework/testdata/Product.java +++ b/src/test/java/com/arangodb/springframework/testdata/Product.java @@ -1,132 +1,133 @@ -/* - * DISCLAIMER - * - * Copyright 2017 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 org.springframework.data.annotation.Id; - -import com.arangodb.springframework.annotation.Field; -import com.arangodb.springframework.annotation.GeoIndex; -import com.arangodb.springframework.annotation.Key; -import com.arangodb.springframework.annotation.Relations; -import com.arangodb.springframework.annotation.Rev; - -/** - * @author Mark Vollmary - * - */ -@GeoIndex(fields = { "location" }) -public class Product { - - @Id - private String id; - @Key - private String key; - @Rev - private String rev; - private String name; - @Field("description") - private String desc; - private double[] location; - - private Product nested; - - @Relations(edges = { Contains.class }) - private Material contains; - - public Product() { - super(); - } - - public Product(final String name) { - super(); - this.name = name; - } - - public Product(final String name, final double[] location) { - this(name); - this.location = location; - } - - public String getId() { - return id; - } - - public void setId(final String id) { - this.id = id; - } - - public String getKey() { - return key; - } - - public void setKey(final String key) { - this.key = key; - } - - public String getRev() { - return rev; - } - - public void setRev(final String rev) { - this.rev = rev; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public String getDesc() { - return desc; - } - - public void setDesc(final String desc) { - this.desc = desc; - } - - public Material getContains() { - return contains; - } - - public void setContains(final Material contains) { - this.contains = contains; - } - - public double[] getLocation() { - return location; - } - - public void setLocation(final double[] location) { - this.location = location; - } - - public Product getNested() { - return nested; - } - - public void setNested(final Product nested) { - this.nested = nested; - } - -} +/* + * DISCLAIMER + * + * Copyright 2017 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 org.springframework.data.annotation.Id; + +import com.arangodb.springframework.annotation.Field; +import com.arangodb.springframework.annotation.GeoIndex; +import com.arangodb.springframework.annotation.Key; +import com.arangodb.springframework.annotation.Relations; +import com.arangodb.springframework.annotation.Rev; + +/** + * @author Mark Vollmary + * + */ +@SuppressWarnings("deprecation") +@GeoIndex(fields = { "location" }) +public class Product { + + @Id + private String id; + @Key + private String key; + @Rev + private String rev; + private String name; + @Field("description") + private String desc; + private double[] location; + + private Product nested; + + @Relations(edges = { Contains.class }) + private Material contains; + + public Product() { + super(); + } + + public Product(final String name) { + super(); + this.name = name; + } + + public Product(final String name, final double[] location) { + this(name); + this.location = location; + } + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(final String key) { + this.key = key; + } + + public String getRev() { + return rev; + } + + public void setRev(final String rev) { + this.rev = rev; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public String getDesc() { + return desc; + } + + public void setDesc(final String desc) { + this.desc = desc; + } + + public Material getContains() { + return contains; + } + + public void setContains(final Material contains) { + this.contains = contains; + } + + public double[] getLocation() { + return location; + } + + public void setLocation(final double[] location) { + this.location = location; + } + + public Product getNested() { + return nested; + } + + public void setNested(final Product nested) { + this.nested = nested; + } + +} diff --git a/src/test/resources/arango-named-queries-test.properties b/src/test/resources/arango-named-queries-test.properties new file mode 100644 index 000000000..8ab67b3c1 --- /dev/null +++ b/src/test/resources/arango-named-queries-test.properties @@ -0,0 +1 @@ +Customer.findOneByIdNamedQuery = FOR c IN customer FILTER c._id == @id RETURN c \ No newline at end of file diff --git a/src/test/resources/arangodb.properties b/src/test/resources/arangodb.properties index cf509a8a5..5682618e6 100644 --- a/src/test/resources/arangodb.properties +++ b/src/test/resources/arangodb.properties @@ -1 +1 @@ -arangodb.hosts=127.0.0.1:8529 \ No newline at end of file +arangodb.hosts=127.0.0.1:8529