diff --git a/.version b/.version index a8fdfda..f8e233b 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -1.8.1 +1.9.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e645e..024c247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,19 @@ # Changelog +#### Version 1.9.0 (TBD) +* Added support for multi-field `Unique` value constraints. When applying the `Unique` constraint to a `Record` field, you can now provide a `name` parameter (e.g. `@Unique(name = "identity"))`. If multiple `Unique` annotated fields have the same `name`, Runway will enforce uniqueness among the combination of values for all those fields across all `Records` in the same class. If a `Unique` annotated field is a `Sequence`, Runway will consider uniqueness to be violated if and only if any items in the sequence are shared and all the other fields in the same uniqueness group are also considered shared. +* Added `Realms` to virtually segregate records within the same environment into distinct groups. A `Record` can be dynamically added to or removed from a `realm` (use `Record#addRealm` and `Record#removeRealm` to manage). Runway provides overloaded read methods that accept a `Realms` parameter to specify the realms from which data can be read. If a Record exists in at least one of the specified `Realms`, it will be read. + * By default, all Records exist in ALL realms, so this feature is backwards compatible. + * By default, read methods consider data from ANY realm, so this feature is backwards compatible. +* Fixed a bug where the `Required` annotation was not enforced when loading data from the database. If a record was modified outside of Runway such that a required field was nullified, Runway would previously load the record without enforcing the constraint. This caused applications to encounter some unexpected `NullPointerException`s. + #### Version 1.8.1 (April 20, 2020) * Fixed a bug that allowed for dynamically `set`ing an intrinsic attribute of a `Record` with a value of an invalid type. In this scenario, Runway should have thrown an error, but it didn't. While the value with the invalid type was not persisted when saving the Record, it was return on intermediate reads of the Record. #### Version 1.8.0 (February 12, 2020) * Improved validation exception messages by including the class name of the Record that fails to validate. * Added a `onLoadFailure` hook to the `Runway.builder` that can be used to get insight and perform processing on errors that occur when loading records from the database. Depending on the error, load failures can be fatal (e.g. the entire load operation fails). The `onLoadFailure` hook does not change this, but it does ensure that fatal errors can be caught and inspected. By default, Runway uses a non-operational `onLoadFailure` hook. The hook can be customized by providing a `TriConsumer` accepting three inputs: the record's `Class` and `id` and the `Throwable` that represents the error. +* Fixed an issue that occurred when setting a value to `null` and that value not being removed from the database. #### Version 1.7.0 (January 1, 2020) * Fixed a bug that caused `Runway` to exhibit poor performance when using the `withCache` option. diff --git a/src/main/java/com/cinchapi/runway/DatabaseInterface.java b/src/main/java/com/cinchapi/runway/DatabaseInterface.java index adb9cd7..53a1efb 100644 --- a/src/main/java/com/cinchapi/runway/DatabaseInterface.java +++ b/src/main/java/com/cinchapi/runway/DatabaseInterface.java @@ -72,7 +72,19 @@ public static Set sort(Set records, String order) { * @return the number of {@link Records} in {@code clazz}. */ public default int count(Class clazz) { - return load(clazz).size(); + return count(clazz, Realms.any()); + } + + /** + * Return the number of {@link Records} in the {@code clazz} among the + * provided {@code realms}. + * + * @param clazz + * @param realms + * @return the number of {@link Records} in {@code clazz}. + */ + public default int count(Class clazz, Realms realms) { + return load(clazz, realms).size(); } /** @@ -80,12 +92,28 @@ public default int count(Class clazz) { * {@code criteria}. * * @param clazz + * @param criteria * @return the number of {@link Records} in {@code clazz} that match the * {@code criteria}. */ public default int count(Class clazz, Criteria criteria) { - return find(clazz, criteria).size(); + return count(clazz, criteria, Realms.any()); + } + + /** + * Return the number of {@link Records} in the {@code clazz} that match the + * {@code criteria} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param realms + * @return the number of {@link Records} in {@code clazz} that match the + * {@code criteria}. + */ + public default int count(Class clazz, + Criteria criteria, Realms realms) { + return find(clazz, criteria, realms).size(); } /** @@ -99,7 +127,23 @@ public default int count(Class clazz, */ public default int count(Class clazz, Criteria criteria, Predicate filter) { - return find(clazz, criteria, filter).size(); + return count(clazz, criteria, filter, Realms.any()); + } + + /** + * Return the number of {@link Records} in the {@code clazz} that match the + * {@code criteria} and pass the {@code filter} among the provided + * {@code realms}. + * + * @param clazz + * @param filter + * @param realms + * @return the number of {@link Records} in {@code clazz} that match the + * {@code criteria}. + */ + public default int count(Class clazz, + Criteria criteria, Predicate filter, Realms realms) { + return find(clazz, criteria, filter, realms).size(); } /** @@ -112,7 +156,21 @@ public default int count(Class clazz, */ public default int count(Class clazz, Predicate filter) { - return load(clazz, filter).size(); + return count(clazz, filter, Realms.any()); + } + + /** + * Return the number of {@link Records} in the {@code clazz} that pass the + * {@code filter} among the provided {@code realms}. + * + * @param clazz + * @param filter + * @param realms + * @return the number of {@link Records} in {@code clazz}. + */ + public default int count(Class clazz, + Predicate filter, Realms realms) { + return load(clazz, filter, realms).size(); } /** @@ -123,7 +181,20 @@ public default int count(Class clazz, * @return the number of {@link Records} in {@code clazz}. */ public default int countAny(Class clazz) { - return loadAny(clazz).size(); + return countAny(clazz, Realms.any()); + } + + /** + * Return the number of {@link Records} across the hierarchy of + * {@code clazz} among the provided {@code realms}. + * + * @param clazz + * @param realms + * @return the number of {@link Records} in {@code clazz}. + */ + public default int countAny(Class clazz, + Realms realms) { + return loadAny(clazz, realms).size(); } /** @@ -137,7 +208,23 @@ public default int countAny(Class clazz) { */ public default int countAny(Class clazz, Criteria criteria) { - return findAny(clazz, criteria).size(); + return countAny(clazz, criteria, Realms.any()); + } + + /** + * Return the number of {@link Records} across the hierarchy of + * {@code clazz} that match the {@code criteria} among the provided + * {@code realms}. + * + * @param clazz + * @param criteria + * @param realms + * @return the number of {@link Records} in {@code clazz} that match the + * {@code criteria}. + */ + public default int countAny(Class clazz, + Criteria criteria, Realms realms) { + return findAny(clazz, criteria, realms).size(); } /** @@ -152,7 +239,24 @@ public default int countAny(Class clazz, */ public default int countAny(Class clazz, Criteria criteria, Predicate filter) { - return findAny(clazz, criteria, filter).size(); + return countAny(clazz, criteria, filter, Realms.any()); + } + + /** + * Return the number of {@link Records} across the hierarchy of + * {@code clazz} that match the {@code criteria} among the provided + * {@code realms}. + * + * @param clazz + * @param criteria + * @param filter + * @param realms + * @return the number of {@link Records} in {@code clazz} that match the + * {@code criteria}. + */ + public default int countAny(Class clazz, + Criteria criteria, Predicate filter, Realms realms) { + return findAny(clazz, criteria, filter, realms).size(); } /** @@ -160,11 +264,27 @@ public default int countAny(Class clazz, * {@code clazz} that pass the {@code filter} * * @param clazz + * @param filter * @return the number of {@link Records} in {@code clazz}. */ public default int countAny(Class clazz, Predicate filter) { - return loadAny(clazz, filter).size(); + return countAny(clazz, filter, Realms.any()); + } + + /** + * Return the number of {@link Records} across the hierarchy of + * {@code clazz} that pass the {@code filter} among the provided + * {@code realms}. + * + * @param clazz + * @param filter + * @param realms + * @return the number of {@link Records} in {@code clazz}. + */ + public default int countAny(Class clazz, + Predicate filter, Realms realms) { + return loadAny(clazz, filter, realms).size(); } /** @@ -175,7 +295,22 @@ public default int countAny(Class clazz, * @param criteria * @return the matching records */ - public Set find(Class clazz, Criteria criteria); + public default Set find(Class clazz, + Criteria criteria) { + return find(clazz, criteria, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param realms + * @return the matching records + */ + public Set find(Class clazz, Criteria criteria, + Realms realms); /** * Find and return all the records of type {@code clazz} that match the @@ -203,8 +338,24 @@ public default Set find(Class clazz, * @param order * @return the matching records */ + public default Set find(Class clazz, + Criteria criteria, Order order) { + return find(clazz, criteria, order, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} sorted by the specified {@code order} among the provided + * {@code realms}. + * + * @param clazz + * @param criteria + * @param order + * @param realms + * @return the matching records + */ public Set find(Class clazz, Criteria criteria, - Order order); + Order order, Realms realms); /** * Find and return all the records of type {@code clazz} that match the @@ -217,8 +368,25 @@ public Set find(Class clazz, Criteria criteria, * @param page * @return the matching records */ + public default Set find(Class clazz, + Criteria criteria, Order order, Page page) { + return find(clazz, criteria, order, page, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} sorted by the specified {@code order} and limited to the + * specified {@code page} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param order + * @param page + * @param realms + * @return the matching records + */ public Set find(Class clazz, Criteria criteria, - Order order, Page page); + Order order, Page page, Realms realms); /** * Find and return all the records of type {@code clazz} that match the @@ -235,7 +403,27 @@ public Set find(Class clazz, Criteria criteria, */ public default Set find(Class clazz, Criteria criteria, Order order, Page page, Predicate filter) { - Set unfiltered = find(clazz, criteria, order); + return find(clazz, criteria, order, page, filter, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} and pass the {@code filter}, sorted by the specified + * {@code order} and limited to the + * specified {@code page} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param order + * @param page + * @param filter + * @param realms + * @return the matching records + */ + public default Set find(Class clazz, + Criteria criteria, Order order, Page page, Predicate filter, + Realms realms) { + Set unfiltered = find(clazz, criteria, order, realms); Set filtered = Sets.filter(unfiltered, filter::test); return Paging.paginate(filtered, page); } @@ -253,7 +441,25 @@ public default Set find(Class clazz, */ public default Set find(Class clazz, Criteria criteria, Order order, Predicate filter) { - Set unfiltered = find(clazz, criteria, order); + return find(clazz, criteria, order, filter, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} and pass the {@code filter}, sorted by the specified + * {@code order} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param order + * @param filter + * @param realms + * @return the matching records + */ + public default Set find(Class clazz, + Criteria criteria, Order order, Predicate filter, + Realms realms) { + Set unfiltered = find(clazz, criteria, order, realms); return Sets.filter(unfiltered, filter::test); } @@ -266,8 +472,24 @@ public default Set find(Class clazz, * @param page * @return the matching records */ + public default Set find(Class clazz, + Criteria criteria, Page page) { + return find(clazz, criteria, page, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} limited to the specified {@code page} among the provided + * {@code realms}. + * + * @param clazz + * @param criteria + * @param page + * @param realms + * @return the matching records + */ public Set find(Class clazz, Criteria criteria, - Page page); + Page page, Realms realms); /** * Find and return all the records of type {@code clazz} that match the @@ -282,7 +504,24 @@ public Set find(Class clazz, Criteria criteria, */ public default Set find(Class clazz, Criteria criteria, Page page, Order order) { - return find(clazz, criteria, order, page); + return find(clazz, criteria, page, order, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} sorted by the specified {@code order} and limited to the + * specified {@code page} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param order + * @param page + * @param realms + * @return the matching records + */ + public default Set find(Class clazz, + Criteria criteria, Page page, Order order, Realms realms) { + return find(clazz, criteria, order, page, realms); } /** @@ -299,7 +538,27 @@ public default Set find(Class clazz, */ public default Set find(Class clazz, Criteria criteria, Page page, Order order, Predicate filter) { - return find(clazz, criteria, order, page, filter); + return find(clazz, criteria, page, order, filter, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} and pass the {@code filter}, sorted by the specified + * {@code order} and limited to the specified {@code page} among the + * provided {@code realms}. + * + * @param clazz + * @param criteria + * @param order + * @param page + * @param filter + * @param realms + * @return the matching records + */ + public default Set find(Class clazz, + Criteria criteria, Page page, Order order, Predicate filter, + Realms realms) { + return find(clazz, criteria, order, page, filter, realms); } /** @@ -315,7 +574,24 @@ public default Set find(Class clazz, */ public default Set find(Class clazz, Criteria criteria, Page page, Predicate filter) { - Set unfiltered = find(clazz, criteria); + return find(clazz, criteria, page, filter, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} and pass the {@code filter} limited to the specified + * {@code page} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param page + * @param filter + * @param realms + * @return the matching records + */ + public default Set find(Class clazz, + Criteria criteria, Page page, Predicate filter, Realms realms) { + Set unfiltered = find(clazz, criteria, realms); Set filtered = Sets.filter(unfiltered, filter::test); return Paging.paginate(filtered, page); } @@ -331,7 +607,23 @@ public default Set find(Class clazz, */ public default Set find(Class clazz, Criteria criteria, Predicate filter) { - Set unfiltered = find(clazz, criteria); + return find(clazz, criteria, filter, Realms.any()); + } + + /** + * Find and return all the records of type {@code clazz} that match the + * {@code criteria} and pass the {@code filter} among the provided + * {@code realms}. + * + * @param clazz + * @param criteria + * @param filter + * @param realms + * @return the matching records + */ + public default Set find(Class clazz, + Criteria criteria, Predicate filter, Realms realms) { + Set unfiltered = find(clazz, criteria, realms); return Sets.filter(unfiltered, filter::test); } @@ -360,7 +652,22 @@ public default Set find(Class clazz, * @param criteria * @return the matching records */ - public Set findAny(Class clazz, Criteria criteria); + public default Set findAny(Class clazz, + Criteria criteria) { + return findAny(clazz, criteria, Realms.any()); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} and + * all of its descendants among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param realms + * @return the matching records + */ + public Set findAny(Class clazz, Criteria criteria, + Realms realms); /** * Execute the {@link #find(Class, Criteria)} query for {@code clazz} @@ -388,8 +695,24 @@ public default Set findAny(Class clazz, * @param order * @return the matching records */ + public default Set findAny(Class clazz, + Criteria criteria, Order order) { + return findAny(clazz, criteria, Realms.any()); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} + * and all of its descendants sorted by the specified {@code order} among + * the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param order + * @param realms + * @return the matching records + */ public Set findAny(Class clazz, Criteria criteria, - Order order); + Order order, Realms realms); /** * Execute the {@link #find(Class, Criteria)} query for {@code clazz} @@ -402,8 +725,25 @@ public Set findAny(Class clazz, Criteria criteria, * @param page * @return the matching records */ + public default Set findAny(Class clazz, + Criteria criteria, Order order, Page page) { + return findAny(clazz, criteria, order, page, Realms.any()); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} + * and all of its descendants sorted by the specified {@code order} and + * limited to the specified {@code page} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param order + * @param page + * @param realms + * @return the matching records + */ public Set findAny(Class clazz, Criteria criteria, - Order order, Page page); + Order order, Page page, Realms realms); /** * Execute the {@link #find(Class, Criteria)} query for {@code clazz} @@ -419,39 +759,93 @@ public Set findAny(Class clazz, Criteria criteria, */ public default Set findAny(Class clazz, Criteria criteria, Order order, Page page, Predicate filter) { - Set unfiltered = findAny(clazz, criteria, order); - Set filtered = Sets.filter(unfiltered, filter::test); - return Paging.paginate(filtered, page); + return findAny(clazz, criteria, order, page, filter, Realms.any()); } /** * Execute the {@link #find(Class, Criteria)} query for {@code clazz} * and all of its descendants that pass the {@code filter}, sorted by the - * specified {@code order}. + * specified {@code order} and limited to the specified {@code page} among + * the provided {@code realms}. * * @param clazz * @param criteria * @param order + * @param page * @param filter + * @param realms * @return the matching records */ public default Set findAny(Class clazz, - Criteria criteria, Order order, Predicate filter) { - Set unfiltered = findAny(clazz, criteria, order); - return Sets.filter(unfiltered, filter::test); + Criteria criteria, Order order, Page page, Predicate filter, + Realms realms) { + Set unfiltered = findAny(clazz, criteria, order, realms); + Set filtered = Sets.filter(unfiltered, filter::test); + return Paging.paginate(filtered, page); } /** * Execute the {@link #find(Class, Criteria)} query for {@code clazz} - * and all of its descendants limited to the specified {@code page}. + * and all of its descendants that pass the {@code filter}, sorted by the + * specified {@code order}. * * @param clazz * @param criteria - * @param page + * @param order + * @param filter + * @return the matching records + */ + public default Set findAny(Class clazz, + Criteria criteria, Order order, Predicate filter) { + return findAny(clazz, criteria, order, filter, Realms.any()); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} + * and all of its descendants that pass the {@code filter}, sorted by the + * specified {@code order} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param order + * @param filter + * @param realms + * @return the matching records + */ + public default Set findAny(Class clazz, + Criteria criteria, Order order, Predicate filter, + Realms realms) { + Set unfiltered = findAny(clazz, criteria, order, realms); + return Sets.filter(unfiltered, filter::test); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} + * and all of its descendants limited to the specified {@code page}. + * + * @param clazz + * @param criteria + * @param page + * @return the matching records + */ + public default Set findAny(Class clazz, + Criteria criteria, Page page) { + return findAny(clazz, criteria, page, Realms.any()); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} + * and all of its descendants limited to the specified {@code page} among + * the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param page + * @param realms * @return the matching records */ public Set findAny(Class clazz, Criteria criteria, - Page page); + Page page, Realms realms); /** * Execute the {@link #find(Class, Criteria)} query for {@code clazz} @@ -466,7 +860,24 @@ public Set findAny(Class clazz, Criteria criteria, */ public default Set findAny(Class clazz, Criteria criteria, Page page, Order order) { - return findAny(clazz, criteria, order, page); + return findAny(clazz, criteria, page, order, Realms.any()); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} + * and all of its descendants sorted by the specified {@code order} and + * limited to the specified {@code page} among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param page + * @param order + * @param realms + * @return the matching records + */ + public default Set findAny(Class clazz, + Criteria criteria, Page page, Order order, Realms realms) { + return findAny(clazz, criteria, order, page, realms); } /** @@ -483,7 +894,27 @@ public default Set findAny(Class clazz, */ public default Set findAny(Class clazz, Criteria criteria, Page page, Order order, Predicate filter) { - return findAny(clazz, criteria, order, page, filter); + return findAny(clazz, criteria, page, order, filter, Realms.any()); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} + * and all of its descendants that pass the {@code filter}, sorted by the + * specified {@code order} and limited to the specified {@code page} among + * the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param page + * @param order + * @param filter + * @param realms + * @return the matching records + */ + public default Set findAny(Class clazz, + Criteria criteria, Page page, Order order, Predicate filter, + Realms realms) { + return findAny(clazz, criteria, order, page, filter, realms); } /** @@ -499,7 +930,24 @@ public default Set findAny(Class clazz, */ public default Set findAny(Class clazz, Criteria criteria, Page page, Predicate filter) { - Set unfiltered = findAny(clazz, criteria); + return findAny(clazz, criteria, page, filter, Realms.any()); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} + * and all of its descendants that pass the {@code filter}, limited to the + * specified {@code page} among the provided {@code realms} + * + * @param clazz + * @param criteria + * @param page + * @param filter + * @param realms + * @return the matching records + */ + public default Set findAny(Class clazz, + Criteria criteria, Page page, Predicate filter, Realms realms) { + Set unfiltered = findAny(clazz, criteria, realms); Set filtered = Sets.filter(unfiltered, filter::test); return Paging.paginate(filtered, page); } @@ -515,7 +963,23 @@ public default Set findAny(Class clazz, */ public default Set findAny(Class clazz, Criteria criteria, Predicate filter) { - Set unfiltered = findAny(clazz, criteria); + return findAny(clazz, criteria, filter, Realms.any()); + } + + /** + * Execute the {@link #find(Class, Criteria)} query for {@code clazz} and + * all of its descendants and return those that pass the {@code filter} + * among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param filter + * @param realms + * @return the matching records + */ + public default Set findAny(Class clazz, + Criteria criteria, Predicate filter, Realms realms) { + Set unfiltered = findAny(clazz, criteria, realms); return Sets.filter(unfiltered, filter::test); } @@ -544,8 +1008,22 @@ public default Set findAny(Class clazz, * @param criteria * @return the one matching record */ - public T findAnyUnique(Class clazz, - Criteria criteria); + public default T findAnyUnique(Class clazz, + Criteria criteria) { + return findAnyUnique(clazz, criteria, Realms.any()); + } + + /** + * Execute the {@link #findUnique(Class, Criteria)} query for {@code clazz} + * and all of its descendants among the provided {@code realms}. + * + * @param clazz + * @param criteria + * @param realms + * @return the one matching record + */ + public T findAnyUnique(Class clazz, Criteria criteria, + Realms realms); /** * Find the one record of type {@code clazz} that matches the @@ -557,7 +1035,24 @@ public T findAnyUnique(Class clazz, * @return the one matching record * @throws DuplicateEntryException */ - public T findUnique(Class clazz, Criteria criteria); + public default T findUnique(Class clazz, + Criteria criteria) { + return findUnique(clazz, criteria, Realms.any()); + } + + /** + * Find the one record of type {@code clazz} that matches the + * {@code criteria} among the provided {@code realms}. If more than one + * record matches, throw a {@link DuplicateEntryException}. + * + * @param clazz + * @param criteria + * @param realms + * @return the one matching record + * @throws DuplicateEntryException + */ + public T findUnique(Class clazz, Criteria criteria, + Realms realms); /** * Load all the Records that are contained within the specified @@ -573,7 +1068,26 @@ public T findAnyUnique(Class clazz, * @param clazz * @return a {@link Set set} of {@link Record} objects */ - public Set load(Class clazz); + public default Set load(Class clazz) { + return load(clazz, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} among the {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public Set load(Class clazz, Realms realms); /** * Load all the Records that are contained within the specified @@ -611,11 +1125,207 @@ public default Set load(Class clazz, * @param id * @return the existing Record */ - public T load(Class clazz, long id); + public default T load(Class clazz, long id) { + return load(clazz, id, Realms.any()); + } + + /** + * Load the Record that is contained within the specified {@code clazz} and + * has the specified {@code id} if it exist in any of the {@code realms}. + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param id + * @return the existing Record + */ + public T load(Class clazz, long id, Realms realms); + + /** + * Load all the Records that are contained within the specified + * {@code clazz} and sorted using the specified {@code order}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @return a {@link Set set} of {@link Record} objects + */ + public default Set load(Class clazz, Order order) { + return load(clazz, order, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} and sorted using the specified {@code order} among the + * {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public Set load(Class clazz, Order order, + Realms realms); + + /** + * Load all the Records that are contained within the specified + * {@code clazz} and sorted using the specified {@code order} and limited to + * the specified {@code page}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param page + * @return a {@link Set set} of {@link Record} objects + */ + public default Set load(Class clazz, Order order, + Page page) { + return load(clazz, order, page, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} and sorted using the specified {@code order} and limited to + * the specified {@code page} among the {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param page + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public Set load(Class clazz, Order order, + Page page, Realms realms); + + /** + * Load all the Records that are contained within the specified + * {@code clazz} that pass the {@code filter}, sorted using the specified + * {@code order} and limited to the specified {@code page}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param page + * @param filter + * @return a {@link Set set} of {@link Record} objects + */ + public default Set load(Class clazz, Order order, + Page page, Predicate filter) { + return load(clazz, order, page, filter, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} that pass the {@code filter}, sorted using the specified + * {@code order} and limited to the specified {@code page} among the + * {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param page + * @param filter + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public default Set load(Class clazz, Order order, + Page page, Predicate filter, Realms realms) { + Set unfiltered = load(clazz, order, realms); + Set filtered = Sets.filter(unfiltered, filter::test); + return Paging.paginate(filtered, page); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} that pass the {@code filter}, sorted using the specified + * {@code order}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param filter + * @return a {@link Set set} of {@link Record} objects + */ + public default Set load(Class clazz, Order order, + Predicate filter) { + return load(clazz, order, filter, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} that pass the {@code filter}, sorted using the specified + * {@code order} among the {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param filter + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public default Set load(Class clazz, Order order, + Predicate filter, Realms realms) { + Set unfiltered = load(clazz, order, realms); + return Sets.filter(unfiltered, filter::test); + } /** * Load all the Records that are contained within the specified - * {@code clazz} and sorted using the specified {@code order}. + * {@code clazz} and limited to the specified {@code page}. * *

* Multiple calls to this method with the same parameters will return @@ -625,15 +1335,17 @@ public default Set load(Class clazz, *

* * @param clazz - * @param order + * @param page * @return a {@link Set set} of {@link Record} objects */ - public Set load(Class clazz, Order order); + public default Set load(Class clazz, Page page) { + return load(clazz, page, Realms.any()); + } /** * Load all the Records that are contained within the specified - * {@code clazz} and sorted using the specified {@code order} and limited to - * the specified {@code page}. + * {@code clazz} and limited to the specified {@code page} among the + * {@code realms}. * *

* Multiple calls to this method with the same parameters will return @@ -643,17 +1355,17 @@ public default Set load(Class clazz, *

* * @param clazz - * @param order * @param page + * @param realms * @return a {@link Set set} of {@link Record} objects */ - public Set load(Class clazz, Order order, - Page page); + public Set load(Class clazz, Page page, + Realms realms); /** * Load all the Records that are contained within the specified - * {@code clazz} that pass the {@code filter}, sorted using the specified - * {@code order} and limited to the specified {@code page}. + * {@code clazz} and sorted using the specified {@code order} and limited to + * the specified {@code page}. * *

* Multiple calls to this method with the same parameters will return @@ -663,22 +1375,19 @@ public Set load(Class clazz, Order order, *

* * @param clazz - * @param order * @param page - * @param filter + * @param order * @return a {@link Set set} of {@link Record} objects */ - public default Set load(Class clazz, Order order, - Page page, Predicate filter) { - Set unfiltered = load(clazz, order); - Set filtered = Sets.filter(unfiltered, filter::test); - return Paging.paginate(filtered, page); + public default Set load(Class clazz, Page page, + Order order) { + return load(clazz, order, page); } /** * Load all the Records that are contained within the specified * {@code clazz} that pass the {@code filter}, sorted using the specified - * {@code order}. + * {@code order} and limited to the specified {@code page}. * *

* Multiple calls to this method with the same parameters will return @@ -688,19 +1397,21 @@ public default Set load(Class clazz, Order order, *

* * @param clazz + * @param page * @param order * @param filter * @return a {@link Set set} of {@link Record} objects */ - public default Set load(Class clazz, Order order, - Predicate filter) { - Set unfiltered = load(clazz, order); - return Sets.filter(unfiltered, filter::test); + public default Set load(Class clazz, Page page, + Order order, Predicate filter) { + return load(clazz, page, order, filter, Realms.any()); } /** * Load all the Records that are contained within the specified - * {@code clazz} and limited to the specified {@code page}. + * {@code clazz} that pass the {@code filter}, sorted using the specified + * {@code order} and limited to the specified {@code page} among the + * {@code realms}. * *

* Multiple calls to this method with the same parameters will return @@ -711,14 +1422,20 @@ public default Set load(Class clazz, Order order, * * @param clazz * @param page + * @param order + * @param filter + * @param realms * @return a {@link Set set} of {@link Record} objects */ - public Set load(Class clazz, Page page); + public default Set load(Class clazz, Page page, + Order order, Predicate filter, Realms realms) { + return load(clazz, order, page, filter, realms); + } /** * Load all the Records that are contained within the specified - * {@code clazz} and sorted using the specified {@code order} and limited to - * the specified {@code page}. + * {@code clazz} that pass the {@code filter}, limited to the specified + * {@code page}. * *

* Multiple calls to this method with the same parameters will return @@ -729,18 +1446,18 @@ public default Set load(Class clazz, Order order, * * @param clazz * @param page - * @param order + * @param filter * @return a {@link Set set} of {@link Record} objects */ public default Set load(Class clazz, Page page, - Order order) { - return load(clazz, order, page); + Predicate filter) { + return load(clazz, page, filter, Realms.any()); } /** * Load all the Records that are contained within the specified - * {@code clazz} that pass the {@code filter}, sorted using the specified - * {@code order} and limited to the specified {@code page}. + * {@code clazz} that pass the {@code filter}, limited to the specified + * {@code page} among the {@code realms}. * *

* Multiple calls to this method with the same parameters will return @@ -751,19 +1468,20 @@ public default Set load(Class clazz, Page page, * * @param clazz * @param page - * @param order * @param filter + * @param realms * @return a {@link Set set} of {@link Record} objects */ public default Set load(Class clazz, Page page, - Order order, Predicate filter) { - return load(clazz, order, page, filter); + Predicate filter, Realms realms) { + Set unfiltered = load(clazz, realms); + Set filtered = Sets.filter(unfiltered, filter::test); + return Paging.paginate(filtered, page); } /** * Load all the Records that are contained within the specified - * {@code clazz} that pass the {@code filter}, limited to the specified - * {@code page}. + * {@code clazz} and pass the {@code filter}. * *

* Multiple calls to this method with the same parameters will return @@ -773,20 +1491,16 @@ public default Set load(Class clazz, Page page, *

* * @param clazz - * @param page - * @param filter * @return a {@link Set set} of {@link Record} objects */ - public default Set load(Class clazz, Page page, + public default Set load(Class clazz, Predicate filter) { - Set unfiltered = load(clazz); - Set filtered = Sets.filter(unfiltered, filter::test); - return Paging.paginate(filtered, page); + return load(clazz, filter, Realms.any()); } /** * Load all the Records that are contained within the specified - * {@code clazz} and pass the {@code filter}. + * {@code clazz} and pass the {@code filter} among the {@code realms}. * *

* Multiple calls to this method with the same parameters will return @@ -796,11 +1510,12 @@ public default Set load(Class clazz, Page page, *

* * @param clazz + * @param realms * @return a {@link Set set} of {@link Record} objects */ public default Set load(Class clazz, - Predicate filter) { - Set unfiltered = load(clazz); + Predicate filter, Realms realms) { + Set unfiltered = load(clazz, realms); return Sets.filter(unfiltered, filter::test); } @@ -840,7 +1555,26 @@ public default Set load(Class clazz, * @param clazz * @return a {@link Set set} of {@link Record} objects */ - public Set loadAny(Class clazz); + public default Set loadAny(Class clazz) { + return loadAny(clazz, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} or any of its descendants among the {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public Set loadAny(Class clazz, Realms realms); /** * Load all the Records that are contained within the specified @@ -881,7 +1615,30 @@ public default Set loadAny(Class clazz, * @param order * @return a {@link Set set} of {@link Record} objects */ - public Set loadAny(Class clazz, Order order); + public default Set loadAny(Class clazz, + Order order) { + return loadAny(clazz, order, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} or any of its descendants and sorted using the specified + * {@code order} among {@code realms} + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public Set loadAny(Class clazz, Order order, + Realms realms); /** * Load all the Records that are contained within the specified @@ -900,8 +1657,32 @@ public default Set loadAny(Class clazz, * @param page * @return a {@link Set set} of {@link Record} objects */ + public default Set loadAny(Class clazz, + Order order, Page page) { + return loadAny(clazz, order, page, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} or any of its descendants and sorted using the specified + * {@code order} and limited to the specified {@code page} among the + * {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param page + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ public Set loadAny(Class clazz, Order order, - Page page); + Page page, Realms realms); /** * Load all the Records that are contained within the specified @@ -924,7 +1705,32 @@ public Set loadAny(Class clazz, Order order, */ public default Set loadAny(Class clazz, Order order, Page page, Predicate filter) { - Set unfiltered = loadAny(clazz, order); + return loadAny(clazz, order, page, filter, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} or any of its descendants that pass the {@code filter}, + * sorted using the specified {@code order} and limited to the specified + * {@code page} among the {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param page + * @param filter + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public default Set loadAny(Class clazz, + Order order, Page page, Predicate filter, Realms realms) { + Set unfiltered = loadAny(clazz, order, realms); Set filtered = Sets.filter(unfiltered, filter::test); return Paging.paginate(filtered, page); } @@ -948,7 +1754,30 @@ public default Set loadAny(Class clazz, */ public default Set loadAny(Class clazz, Order order, Predicate filter) { - Set unfiltered = loadAny(clazz, order); + return loadAny(clazz, order, filter, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} or any of its descendants that pass the {@code filter}, + * sorted using the specified {@code order} among the {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param order + * @param filter + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public default Set loadAny(Class clazz, + Order order, Predicate filter, Realms realms) { + Set unfiltered = loadAny(clazz, order, realms); return Sets.filter(unfiltered, filter::test); } @@ -967,7 +1796,30 @@ public default Set loadAny(Class clazz, * @param page * @return a {@link Set set} of {@link Record} objects */ - public Set loadAny(Class clazz, Page page); + public default Set loadAny(Class clazz, + Page page) { + return loadAny(clazz, page, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} and limited to the specified {@code page} among the + * {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param page + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public Set loadAny(Class clazz, Page page, + Realms realms); /** * Load all the Records that are contained within the specified @@ -988,7 +1840,31 @@ public default Set loadAny(Class clazz, */ public default Set loadAny(Class clazz, Page page, Order order) { - return loadAny(clazz, order, page); + return loadAny(clazz, order, page, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} or any of its descendants and sorted using the specified + * {@code order} and limited to the specified {@code page} among the + * {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param page + * @param order + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public default Set loadAny(Class clazz, Page page, + Order order, Realms realms) { + return loadAny(clazz, page, order, realms); } /** @@ -1012,7 +1888,32 @@ public default Set loadAny(Class clazz, Page page, */ public default Set loadAny(Class clazz, Page page, Order order, Predicate filter) { - return loadAny(clazz, order, page, filter); + return loadAny(clazz, page, order, filter, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} or any of its descendants that pass the {@code filter}, + * sorted using the specified {@code order} and limited to the specified + * {@code page}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param page + * @param order + * @param filter + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public default Set loadAny(Class clazz, Page page, + Order order, Predicate filter, Realms realms) { + return loadAny(clazz, order, page, filter, realms); } /** @@ -1034,7 +1935,30 @@ public default Set loadAny(Class clazz, Page page, */ public default Set loadAny(Class clazz, Page page, Predicate filter) { - Set unfiltered = loadAny(clazz); + return loadAny(clazz, page, filter, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} that pass the {@code filter}, limited to the specified + * {@code page} among the {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param page + * @param filter + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public default Set loadAny(Class clazz, Page page, + Predicate filter, Realms realms) { + Set unfiltered = loadAny(clazz, realms); Set filtered = Sets.filter(unfiltered, filter::test); return Paging.paginate(filtered, page); } @@ -1056,7 +1980,29 @@ public default Set loadAny(Class clazz, Page page, */ public default Set loadAny(Class clazz, Predicate filter) { - Set unfiltered = loadAny(clazz); + return loadAny(clazz, filter, Realms.any()); + } + + /** + * Load all the Records that are contained within the specified + * {@code clazz} or any of its descendants and pass the {@code filter} amomg + * the {@code realms}. + * + *

+ * Multiple calls to this method with the same parameters will return + * different instances (e.g. the instances are not cached). + * This is done deliberately so different threads/clients can make changes + * to a Record in isolation. + *

+ * + * @param clazz + * @param filter + * @param realms + * @return a {@link Set set} of {@link Record} objects + */ + public default Set loadAny(Class clazz, + Predicate filter, Realms realms) { + Set unfiltered = loadAny(clazz, realms); return Sets.filter(unfiltered, filter::test); } diff --git a/src/main/java/com/cinchapi/runway/Realms.java b/src/main/java/com/cinchapi/runway/Realms.java new file mode 100644 index 0000000..5d5c5cf --- /dev/null +++ b/src/main/java/com/cinchapi/runway/Realms.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2013-2021 Cinchapi Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cinchapi.runway; + +import java.util.Collection; +import java.util.Set; + +import javax.annotation.concurrent.Immutable; + +import com.cinchapi.common.collect.Collections; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; + +/** + * A matcher for one or more realms in which {@link Record Records} may exist. + *

+ * Realms simulate housing {@link Record Records} in logically distinct groups, + * even though the data is physically located in the same environment. Realms + * are flexible such that a {@link Record} may exist in multiple realms at the + * same time. + *

+ *

+ * Realms make it easy to logically segregate data for different purposes + * without incurring the overhead of connecting to multiple environments. The + * nature of Realms also makes it easy to have data that is considered common + * across realms without creating multiple copies. + *

+ * + * @author Jeff Nelson + */ +@Immutable +public final class Realms { + + /** + * Match any {@link Realms}. + */ + private static final Realms ANY = anyOf(ImmutableSet.of()); + + /** + * Match any and all realms. + * + * @return a matcher + */ + public static Realms any() { + return ANY; + } + + /** + * Semantic alias for {@link #any()}. + * + * @return a matcher + */ + public static Realms all() { + return any(); + } + + /** + * Match any of the specified {@code realms} such that a {@link Record} + * would be valid if any of its realms overlaps with any of the provided + * {@code realms}. + * + * @param realms + * @return a matcher + */ + public static Realms anyOf(Collection realms) { + return new Realms(realms); + } + + /** + * Match any of the specified {@code realms} such that a {@link Record} + * would be valid if any of its realms overlaps with any of the provided + * {@code realms}. + * + * @param realms + * @return a matcher + */ + public static Realms anyOf(String... realms) { + return anyOf(Sets.newHashSet(realms)); + } + + /** + * Match only one {@code realm}. + * + * @param realm + * @return a matcher + */ + public static Realms only(String realm) { + return anyOf(ImmutableSet.of(realm)); + } + + /** + * The matched realms. + */ + private final Set realms; + + /** + * Construct a new instance. + * + * @param realms + */ + private Realms(Collection realms) { + this.realms = Collections.ensureSet(realms); + } + + @Override + public boolean equals(Object object) { + if(object instanceof Realms) { + return realms.equals(((Realms) object).realms); + } + else { + return false; + } + } + + @Override + public int hashCode() { + return realms.hashCode(); + } + + @Override + public String toString() { + return realms.toString(); + } + + /** + * Return the names of the matched realms. + * + * @return the matched realms + */ + /* package */ Set names() { + return java.util.Collections.unmodifiableSet(realms); + } + +} diff --git a/src/main/java/com/cinchapi/runway/Record.java b/src/main/java/com/cinchapi/runway/Record.java index cfd7343..cee378d 100644 --- a/src/main/java/com/cinchapi/runway/Record.java +++ b/src/main/java/com/cinchapi/runway/Record.java @@ -36,6 +36,7 @@ import java.util.ArrayList; import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -48,10 +49,12 @@ import org.apache.commons.lang.StringUtils; import com.cinchapi.ccl.Parser; +import com.cinchapi.common.base.AnyStrings; import com.cinchapi.common.base.Array; import com.cinchapi.common.base.ArrayBuilder; import com.cinchapi.common.base.CheckedExceptions; import com.cinchapi.common.base.Verify; +import com.cinchapi.common.collect.AnyMaps; import com.cinchapi.common.collect.Association; import com.cinchapi.common.collect.MergeStrategies; import com.cinchapi.common.collect.Sequences; @@ -63,6 +66,7 @@ import com.cinchapi.concourse.Link; import com.cinchapi.concourse.Tag; import com.cinchapi.concourse.Timestamp; +import com.cinchapi.concourse.lang.BuildableState; import com.cinchapi.concourse.lang.Criteria; import com.cinchapi.concourse.lang.paginate.Page; import com.cinchapi.concourse.lang.sort.Order; @@ -377,8 +381,58 @@ private static T newDefaultInstance(Class clazz, } } + /** + * Serialize {@code value} by converting it to an object that can be stored + * within the database. This method assumes that {@code value} is a scalar + * (e.g. not a {@link Sequences#isSequence(Object)}). + * + * @param value + * @return the serialized value + */ + @SuppressWarnings("rawtypes") + private static Object serialize(Object value) { + // NOTE: This logic mirrors storage logic in the #store method. But, + // since the #store method has some optimizations, it doesn't call into + // this method directly. So, if modifications are made to this method, + // please make similar and appropriate modifications to #store. + Preconditions.checkArgument(!Sequences.isSequence(value)); + Preconditions.checkNotNull(value); + if(value instanceof Record) { + return Link.to(((Record) value).id()); + } + else if(value instanceof DeferredReference) { + return serialize(((DeferredReference) value).get()); + } + else if(value.getClass().isPrimitive() || value instanceof String + || value instanceof Tag || value instanceof Link + || value instanceof Integer || value instanceof Long + || value instanceof Float || value instanceof Double + || value instanceof Boolean || value instanceof Timestamp) { + return value; + } + else if(value instanceof Enum) { + return Tag.create(((Enum) value).name()); + } + else if(value instanceof Serializable) { + ByteBuffer bytes = Serializables.getBytes((Serializable) value); + Tag base64 = Tag.create(BaseEncoding.base64Url() + .encode(ByteBuffers.toByteArray(bytes))); + return base64; + } + else { + Gson gson = new Gson(); + Tag json = Tag.create(gson.toJson(value)); + return json; + } + } + /* package */ static Runway PINNED_RUNWAY_INSTANCE = null; + /** + * The key used to hold the {@link #__realms} metadata. + */ + /* package */ static final String REALMS_KEY = "_realms"; + /** * The key used to hold the section metadata. */ @@ -465,6 +519,34 @@ private static T newDefaultInstance(Class clazz, */ private transient String __ = getClass().getName(); + /** + * An internal flag that tracks whether {@link #_realms} have been + * {@link #addRealm(String) added} or {@link #removeRealm(String) removed}. + * This flag is necessary so that this Record's data cache isn't + * unnecessarily invalidated when reconciling the realms on + * {@link #saveWithinTransaction(Concourse, Set)}. + */ + private transient boolean _hasModifiedRealms = false; + + /** + * The list of realms to which this Record belongs. + *

+ * Realms allow records to be placed in logically distinct groups while + * existing in the same physical database and environment. + *

+ *

+ * By default, a Record is not explicitly assigned to any realm and is + * therefore a member of all realms. If this field contains one or more + * explicit realms, then a Record is considered to only exist in those + * realms. + *

+ *

+ * Runway supports loading data that exists in any realm or within one or + * more explicit realms. + *

+ */ + private transient Set _realms = ImmutableSet.of(); + /** * The {@link Concourse} database in which this {@link Record} is stored. */ @@ -510,6 +592,20 @@ public Record() { } } + /** + * Add this {@link Record} to {@code realm}. + * + * @param realm + * @return {@code true} if this {@link Record} was added to {@link realm}; + * otherwise {@code false} + */ + public boolean addRealm(String realm) { + if(_realms.isEmpty()) { + _realms = Sets.newLinkedHashSet(); + } + return _realms.add(realm) && (_hasModifiedRealms = true); + } + /** * Assign this {@link Record} to a the specified {@code runway} instance. *

@@ -707,6 +803,18 @@ public final long id() { return id; } + /** + * Return {@code true} if this {@link Record} and the other {@code record} + * exist in at least one overlapping realm. + * + * @param record + * @return {@code true} if this and {@code record} share any realms + */ + public boolean inSameRealm(Record record) { + return _realms.isEmpty() && record._realms.isEmpty() + || !Sets.intersection(_realms, record._realms).isEmpty(); + } + /** * Return the "readable" intrinsic (e.g. not {@link #derived() or * {@link #computed()}) data from this {@link Record} as a {@link Map}. @@ -851,7 +959,7 @@ public String json(SerializationOptions options, String... keys) { */ public String json(String... keys) { return json(SerializationOptions.defaults(), keys); - }; + } /** * Return a map that contains "readable" data from this {@link Record}. @@ -965,7 +1073,7 @@ else if(Sequences.isSequence(destination)) { Map data = pool.filter(filter).collect(Association::of, accumulator, MergeStrategies::upsert); return data; - } + }; /** * Return a map that contains "readable" data from this {@link Record}. @@ -986,6 +1094,33 @@ public Map map(String... keys) { return map(SerializationOptions.defaults(), keys); } + /** + * Return the names of all the {@link Realms} where this {@link Record} exists. + * @return this {@link Record Record's} realms + */ + public Set realms() { + return Collections.unmodifiableSet(_realms); + } + + /** + * Remove this {@link Record} from {@code realm}. + * + * @param realm + * @return {@code true} if this {@link Record} was removed from + * {@code realm}; otherwise {@code false} (e.g. this {@link Record} + * never existed in {@code realm}) + */ + public boolean removeRealm(String realm) { + try { + return _realms.remove(realm) && (_hasModifiedRealms = true); + } + finally { + if(_realms.isEmpty()) { + _realms = ImmutableSet.of(); + } + } + } + /** * Save any changes made to this {@link Record}. *

@@ -1176,6 +1311,11 @@ final void load(Concourse concourse, TLongObjectMap existing, throw new ZombieException(); } data = data == null ? concourse.select(id) : data; + Set realms = data.getOrDefault(REALMS_KEY, ImmutableSet.of()); + _realms = realms.size() > 0 + ? realms.stream().map(Object::toString) + .collect(Collectors.toCollection(LinkedHashSet::new)) + : ImmutableSet.of(); for (Field field : fields()) { try { if(!Modifier.isTransient(field.getModifiers())) { @@ -1241,6 +1381,12 @@ else if(type == Set.class) { if(value != null) { field.set(this, value); } + else if(value == null + && field.isAnnotationPresent(Required.class)) { + throw new IllegalStateException("Record " + id + + " cannot be loaded because '" + key + + "' is a required field, but no value is present in the database."); + } else { // no-op; NOTE: Java doesn't allow primitive types to // hold null values @@ -1335,15 +1481,21 @@ else if(type == Set.class) { /** * Save the data in this record using the specified {@code concourse} - * connection. This method assumes that the caller has already started an + * connection. This method assumes that the caller has already started a * transaction, if necessary and will commit the transaction after this * method completes. * * @param concourse + * @param seen */ /* package */ void saveWithinTransaction(final Concourse concourse, Set seen) { concourse.verifyOrSet(SECTION_KEY, __, id); + Set alreadyVerifiedUniqueConstraints = Sets.newHashSet(); + if(_hasModifiedRealms) { + concourse.reconcile(REALMS_KEY, id, _realms); + _hasModifiedRealms = false; + } fields().forEach(field -> { try { if(!Modifier.isTransient(field.getModifiers())) { @@ -1358,9 +1510,40 @@ else if(type == Set.class) { validator.getErrorMessage()); } if(field.isAnnotationPresent(Unique.class)) { - Preconditions.checkState( - isUnique(concourse, key, value), - field.getName() + " must be unique in " + __); + Unique constraint = field.getAnnotation(Unique.class); + String name = constraint.name(); + if(name.length() == 0) { + Preconditions + .checkState(isUnique(concourse, key, value), + field.getName() + + " must be unique in " + + __); + } + else if(!alreadyVerifiedUniqueConstraints + .contains(name)) { + // Find all the fields that have this constraint and + // check for uniqueness. + Map values = Maps.newHashMap(); + values.put(key, value); + fields().stream().filter($field -> $field != field) + .filter($field -> $field + .isAnnotationPresent(Unique.class)) + .filter($field -> $field + .getAnnotation(Unique.class).name() + .equals(name)) + .forEach($field -> { + values.put($field.getName(), Reflection + .get($field.getName(), this)); + }); + if(isUnique(concourse, values)) { + alreadyVerifiedUniqueConstraints.add(name); + } + else { + throw new IllegalStateException(AnyStrings + .format("{} must be unique in {}", name, + __)); + } + } } if(field.isAnnotationPresent(Required.class)) { Preconditions.checkState(!Empty.ness().describes(value), @@ -1628,6 +1811,60 @@ private final boolean inZombieState(Concourse concourse) { return inZombieState(id, concourse, null); } + /** + * Return {@code true} if all the key/value pairs in {@code data} are + * collectively unique for this class. This means that there is no other + * record in the database for this class with all the mappings. + *

+ * If any of the values in {@code data} are a + * {@link Sequences#isSequence(Object) sequence}, this method will return + * {@code true} if and only if every element in every + * {@link Sequences#isSequence(Object) sequence} is unique. + *

+ * + * @param concourse + * @param data + * @return + */ + private boolean isUnique(Concourse concourse, Map data) { + AtomicReference $criteria = new AtomicReference<>( + Criteria.where().key(SECTION_KEY).operator(Operator.EQUALS) + .value(getClass().getName())); + for (Entry entry : data.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if(value == null) { + continue; // A null value does not affect uniqueness. + } + else if(Sequences.isSequence(value)) { + AtomicReference $sub = new AtomicReference<>( + null); + Sequences.forEach(value, item -> { + item = serialize(item); + if($sub.get() == null) { + $sub.set(Criteria.where().key(key) + .operator(Operator.EQUALS).value(item)); + } + else { + $sub.set($sub.get().or().key(key) + .operator(Operator.EQUALS).value(item)); + } + }); + $criteria.set(Criteria.where() + .group($criteria.get().and().group($sub.get()))); + } + else { + value = serialize(value); + $criteria.set($criteria.get().and().key(key) + .operator(Operator.EQUALS).value(value)); + } + } + Criteria criteria = $criteria.get(); + Set records = concourse.find(criteria); + return records.isEmpty() + || (records.contains(id) && records.size() == 1); + } + /** * Return {@code true} if {@code key} as {@code value} for this class is * unique, meaning there is no other record in the database in this class @@ -1642,22 +1879,8 @@ private final boolean inZombieState(Concourse concourse) { * for this class */ private boolean isUnique(Concourse concourse, String key, Object value) { - if(value instanceof Iterable || value.getClass().isArray()) { - for (Object obj : (Iterable) value) { - if(!isUnique(concourse, key, obj)) { - return false; - } - } - return true; - } - else { - Criteria criteria = Criteria.where().key(SECTION_KEY) - .operator(Operator.EQUALS).value(getClass().getName()).and() - .key(key).operator(Operator.EQUALS).value(value).build(); - Set records = concourse.find(criteria); - return records.isEmpty() - || (records.contains(id) && records.size() == 1); - } + return value != null ? isUnique(concourse, AnyMaps.create(key, value)) + : true; } /** @@ -1737,6 +1960,11 @@ public TypeAdapter create(Gson gson, TypeToken type) { @SuppressWarnings("rawtypes") private void store(String key, Object value, Concourse concourse, boolean append, Set seen) { + // NOTE: This logic mirrors serialization logic in the #serialize + // method. Since this method has some optimizations, it doesn't call + // into #serialize directly. So, if modifications are made to this + // method, please make similar and appropriate modifications to + // #serialize. // TODO: dirty field detection! if(value instanceof Record) { Record record = (Record) value; @@ -1860,6 +2088,31 @@ public Set find(Class clazz, Criteria criteria, } } + @Override + public Set find(Class clazz, Criteria criteria, + Order order, Page page, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.find(clazz, criteria, order, page, + realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + + @Override + public Set find(Class clazz, Criteria criteria, + Order order, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.find(clazz, criteria, order, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public Set find(Class clazz, Criteria criteria, Page page) { @@ -1872,6 +2125,30 @@ public Set find(Class clazz, Criteria criteria, } } + @Override + public Set find(Class clazz, Criteria criteria, + Page page, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.find(clazz, criteria, page, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + + @Override + public Set find(Class clazz, Criteria criteria, + Realms realms) { + if(tracked.runway != null) { + return tracked.runway.find(clazz, criteria, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public Set findAny(Class clazz, Criteria criteria) { @@ -1908,6 +2185,31 @@ public Set findAny(Class clazz, } } + @Override + public Set findAny(Class clazz, + Criteria criteria, Order order, Page page, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.findAny(clazz, criteria, order, page, + realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + + @Override + public Set findAny(Class clazz, + Criteria criteria, Order order, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.findAny(clazz, criteria, order, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public Set findAny(Class clazz, Criteria criteria, Page page) { @@ -1920,6 +2222,30 @@ public Set findAny(Class clazz, } } + @Override + public Set findAny(Class clazz, + Criteria criteria, Page page, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.findAny(clazz, criteria, page, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + + @Override + public Set findAny(Class clazz, + Criteria criteria, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.findAny(clazz, criteria, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public T findAnyUnique(Class clazz, Criteria criteria) { @@ -1932,6 +2258,18 @@ public T findAnyUnique(Class clazz, } } + @Override + public T findAnyUnique(Class clazz, + Criteria criteria, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.findAnyUnique(clazz, criteria, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public T findUnique(Class clazz, Criteria criteria) { @@ -1944,6 +2282,18 @@ public T findUnique(Class clazz, } } + @Override + public T findUnique(Class clazz, + Criteria criteria, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.findUnique(clazz, criteria, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public Set load(Class clazz) { if(tracked.runway != null) { @@ -1966,6 +2316,18 @@ public T load(Class clazz, long id) { } } + @Override + public T load(Class clazz, long id, + Realms realms) { + if(tracked.runway != null) { + return tracked.runway.load(clazz, id, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public Set load(Class clazz, Order order) { if(tracked.runway != null) { @@ -1989,6 +2351,30 @@ public Set load(Class clazz, Order order, } } + @Override + public Set load(Class clazz, Order order, + Page page, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.load(clazz, order, page, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + + @Override + public Set load(Class clazz, Order order, + Realms realms) { + if(tracked.runway != null) { + return tracked.runway.load(clazz, order, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public Set load(Class clazz, Page page) { if(tracked.runway != null) { @@ -2000,6 +2386,29 @@ public Set load(Class clazz, Page page) { } } + @Override + public Set load(Class clazz, Page page, + Realms realms) { + if(tracked.runway != null) { + return tracked.runway.load(clazz, page, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + + @Override + public Set load(Class clazz, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.load(clazz, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public Set loadAny(Class clazz) { if(tracked.runway != null) { @@ -2034,6 +2443,30 @@ public Set loadAny(Class clazz, Order order, } } + @Override + public Set loadAny(Class clazz, Order order, + Page page, Realms realms) { + if(tracked.runway != null) { + return tracked.runway.loadAny(clazz, order, page, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + + @Override + public Set loadAny(Class clazz, Order order, + Realms realms) { + if(tracked.runway != null) { + return tracked.runway.loadAny(clazz, order, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + @Override public Set loadAny(Class clazz, Page page) { if(tracked.runway != null) { @@ -2045,6 +2478,30 @@ public Set loadAny(Class clazz, Page page) { } } + @Override + public Set loadAny(Class clazz, Page page, + Realms realms) { + if(tracked.runway != null) { + return tracked.runway.loadAny(clazz, page, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + + @Override + public Set loadAny(Class clazz, + Realms realms) { + if(tracked.runway != null) { + return tracked.runway.loadAny(clazz, realms); + } + else { + throw new UnsupportedOperationException( + "No database interface has been assigned to this Record"); + } + } + } } diff --git a/src/main/java/com/cinchapi/runway/Runway.java b/src/main/java/com/cinchapi/runway/Runway.java index 27d8cff..d45d389 100644 --- a/src/main/java/com/cinchapi/runway/Runway.java +++ b/src/main/java/com/cinchapi/runway/Runway.java @@ -18,6 +18,7 @@ import java.util.AbstractMap; import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -33,6 +34,8 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; + +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.reflections.Reflections; @@ -51,6 +54,7 @@ import com.cinchapi.concourse.DuplicateEntryException; import com.cinchapi.concourse.lang.BuildableState; import com.cinchapi.concourse.lang.Criteria; +import com.cinchapi.concourse.lang.ValueState; import com.cinchapi.concourse.lang.paginate.Page; import com.cinchapi.concourse.lang.sort.Direction; import com.cinchapi.concourse.lang.sort.Order; @@ -67,6 +71,7 @@ import com.google.common.base.MoreObjects; import com.google.common.cache.Cache; import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -346,47 +351,54 @@ public void close() throws Exception { } @Override - public int count(Class clazz) { - return count($Criteria.forClass(clazz)); + public int count(Class clazz, Realms realms) { + return count($Criteria.amongRealms(realms, $Criteria.forClass(clazz))); } @Override - public int count(Class clazz, Criteria criteria) { + public int count(Class clazz, Criteria criteria, + Realms realms) { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { - return count($Criteria.withinClass(clazz, criteria)); + return count($Criteria.amongRealms(realms, + $Criteria.withinClass(clazz, criteria))); } else { - return filter(clazz, criteria, NO_ORDER, NO_PAGINATION).size(); + return filter(clazz, criteria, NO_ORDER, NO_PAGINATION, realms) + .size(); } - } @Override - public int countAny(Class clazz) { - return count($Criteria.forClassHierarchy(clazz)); + public int countAny(Class clazz, Realms realms) { + return count($Criteria.amongRealms(realms, + $Criteria.forClassHierarchy(clazz))); } @Override - public int countAny(Class clazz, Criteria criteria) { + public int countAny(Class clazz, Criteria criteria, + Realms realms) { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { - return count($Criteria.withinClass(clazz, criteria)); + return count($Criteria.amongRealms(realms, + $Criteria.withinClass(clazz, criteria))); } else { - return filterAny(clazz, criteria, NO_ORDER, NO_PAGINATION).size(); + return filterAny(clazz, criteria, NO_ORDER, NO_PAGINATION, realms) + .size(); } } @Override - public Set find(Class clazz, Criteria criteria) { + public Set find(Class clazz, Criteria criteria, + Realms realms) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $find(concourse, - clazz, criteria, NO_ORDER, NO_PAGINATION); + clazz, criteria, NO_ORDER, NO_PAGINATION, realms); return instantiateAll(clazz, data); } else { - return filter(clazz, criteria, NO_ORDER, NO_PAGINATION); + return filter(clazz, criteria, NO_ORDER, NO_PAGINATION, realms); } } finally { @@ -397,17 +409,18 @@ public Set find(Class clazz, Criteria criteria) { @SuppressWarnings("deprecation") @Override public Set find(Class clazz, Criteria criteria, - Order order) { + Order order, Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $find(concourse, - clazz, criteria, order, NO_PAGINATION); + clazz, criteria, order, NO_PAGINATION, realms); return instantiateAll(clazz, data); } else { - return filter(clazz, criteria, order, NO_PAGINATION); + return filter(clazz, criteria, order, NO_PAGINATION, + realms); } } finally { @@ -415,24 +428,28 @@ public Set find(Class clazz, Criteria criteria, } } else { - return findAny(clazz, criteria, backwardsCompatible(order)); + return find(clazz, criteria, backwardsCompatible(order)).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) + .collect(Collectors.toCollection(LinkedHashSet::new)); } } @SuppressWarnings("deprecation") @Override public Set find(Class clazz, Criteria criteria, - Order order, Page page) { + Order order, Page page, Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $find(concourse, - clazz, criteria, order, page); + clazz, criteria, order, page, realms); return instantiateAll(clazz, data); } else { - return filter(clazz, criteria, order, page); + return filter(clazz, criteria, order, page, realms); } } finally { @@ -440,25 +457,27 @@ public Set find(Class clazz, Criteria criteria, } } else { - return findAny(clazz, criteria, backwardsCompatible(order)).stream() - .skip(page.skip()).limit(page.limit()) + return find(clazz, criteria, backwardsCompatible(order)).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) .collect(Collectors.toCollection(LinkedHashSet::new)); } } @Override public Set find(Class clazz, Criteria criteria, - Page page) { + Page page, Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $find(concourse, - clazz, criteria, NO_ORDER, page); + clazz, criteria, NO_ORDER, page, realms); return instantiateAll(clazz, data); } else { - return filter(clazz, criteria, NO_ORDER, page); + return filter(clazz, criteria, NO_ORDER, page, realms); } } finally { @@ -466,24 +485,28 @@ public Set find(Class clazz, Criteria criteria, } } else { - return findAny(clazz, criteria).stream().skip(page.skip()) + return find(clazz, criteria).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) .limit(page.limit()) .collect(Collectors.toCollection(LinkedHashSet::new)); } } @Override - public Set findAny(Class clazz, - Criteria criteria) { + public Set findAny(Class clazz, Criteria criteria, + Realms realms) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $findAny(concourse, - clazz, criteria, NO_ORDER, NO_PAGINATION); + clazz, criteria, NO_ORDER, NO_PAGINATION, realms); return instantiateAll(data); } else { - return filterAny(clazz, criteria, NO_ORDER, NO_PAGINATION); + return filterAny(clazz, criteria, NO_ORDER, NO_PAGINATION, + realms); } } finally { @@ -494,17 +517,19 @@ public Set findAny(Class clazz, @SuppressWarnings("deprecation") @Override public Set findAny(Class clazz, Criteria criteria, - Order order) { + Order order, Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $findAny( - concourse, clazz, criteria, order, NO_PAGINATION); + concourse, clazz, criteria, order, NO_PAGINATION, + realms); return instantiateAll(data); } else { - return filterAny(clazz, criteria, order, NO_PAGINATION); + return filterAny(clazz, criteria, order, NO_PAGINATION, + realms); } } finally { @@ -512,24 +537,28 @@ public Set findAny(Class clazz, Criteria criteria, } } else { - return findAny(clazz, criteria, backwardsCompatible(order)); + return findAny(clazz, criteria, backwardsCompatible(order)).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) + .collect(Collectors.toCollection(LinkedHashSet::new)); } } @SuppressWarnings("deprecation") @Override public Set findAny(Class clazz, Criteria criteria, - Order order, Page page) { + Order order, Page page, Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $findAny( - concourse, clazz, criteria, order, page); + concourse, clazz, criteria, order, page, realms); return instantiateAll(data); } else { - return filterAny(clazz, criteria, order, page); + return filterAny(clazz, criteria, order, page, realms); } } finally { @@ -538,6 +567,9 @@ public Set findAny(Class clazz, Criteria criteria, } else { return findAny(clazz, criteria, backwardsCompatible(order)).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) .skip(page.skip()).limit(page.limit()) .collect(Collectors.toCollection(LinkedHashSet::new)); } @@ -545,17 +577,17 @@ public Set findAny(Class clazz, Criteria criteria, @Override public Set findAny(Class clazz, Criteria criteria, - Page page) { + Page page, Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $findAny( - concourse, clazz, criteria, NO_ORDER, page); + concourse, clazz, criteria, NO_ORDER, page, realms); return instantiateAll(data); } else { - return filterAny(clazz, criteria, NO_ORDER, page); + return filterAny(clazz, criteria, NO_ORDER, page, realms); } } finally { @@ -563,21 +595,24 @@ public Set findAny(Class clazz, Criteria criteria, } } else { - return findAny(clazz, criteria).stream().skip(page.skip()) - .limit(page.limit()) + return findAny(clazz, criteria).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) + .skip(page.skip()).limit(page.limit()) .collect(Collectors.toCollection(LinkedHashSet::new)); } } @SuppressWarnings("unchecked") @Override - public T findAnyUnique(Class clazz, - Criteria criteria) { + public T findAnyUnique(Class clazz, Criteria criteria, + Realms realms) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $findAny(concourse, - clazz, criteria, NO_ORDER, NO_PAGINATION); + clazz, criteria, NO_ORDER, NO_PAGINATION, realms); if(data.isEmpty()) { return null; } @@ -595,7 +630,7 @@ else if(data.size() == 1) { } else { Set records = filterAny(clazz, criteria, NO_ORDER, - NO_PAGINATION); + NO_PAGINATION, realms); if(records.isEmpty()) { return null; } @@ -649,12 +684,13 @@ public T findOne(Class clazz, Criteria criteria) { } @Override - public T findUnique(Class clazz, Criteria criteria) { + public T findUnique(Class clazz, Criteria criteria, + Realms realms) { Concourse concourse = connections.request(); try { if(Record.isDatabaseResolvableCondition(clazz, criteria)) { Map>> data = $find(concourse, - clazz, criteria, NO_ORDER, NO_PAGINATION); + clazz, criteria, NO_ORDER, NO_PAGINATION, realms); if(data.isEmpty()) { return null; } @@ -673,7 +709,7 @@ else if(data.size() == 1) { } else { Set records = filterAny(clazz, criteria, NO_ORDER, - NO_PAGINATION); + NO_PAGINATION, realms); if(records.isEmpty()) { return null; } @@ -695,11 +731,11 @@ else if(records.size() == 1) { } @Override - public Set load(Class clazz) { + public Set load(Class clazz, Realms realms) { Concourse concourse = connections.request(); try { Map>> data = $load(concourse, clazz, - NO_ORDER, NO_PAGINATION); + NO_ORDER, NO_PAGINATION, realms); return instantiateAll(clazz, data); } finally { @@ -708,7 +744,7 @@ public Set load(Class clazz) { } @Override - public T load(Class clazz, long id) { + public T load(Class clazz, long id, Realms realms) { if(hierarchies.get(clazz).size() > 1) { // The provided clazz has descendants, so it is possible that the // Record with the #id is actually a member of a subclass @@ -723,17 +759,32 @@ public T load(Class clazz, long id) { connections.release(connection); } } + if(!realms.names().isEmpty()) { + Concourse connection = connections.request(); + try { + Set $realms = MoreObjects.firstNonNull( + connection.select(Record.REALMS_KEY, id), + ImmutableSet.of()); + if(Sets.intersection($realms, realms.names()).isEmpty()) { + return null; // TODO: what to do here? + } + } + finally { + connections.release(connection); + } + } return instantiate(clazz, id, null); } @SuppressWarnings("deprecation") @Override - public Set load(Class clazz, Order order) { + public Set load(Class clazz, Order order, + Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { Map>> data = $load(concourse, - clazz, order, NO_PAGINATION); + clazz, order, NO_PAGINATION, realms); return instantiateAll(clazz, data); } finally { @@ -748,12 +799,12 @@ public Set load(Class clazz, Order order) { @SuppressWarnings("deprecation") @Override public Set load(Class clazz, Order order, - Page page) { + Page page, Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { Map>> data = $load(concourse, - clazz, order, page); + clazz, order, page, realms); return instantiateAll(clazz, data); } finally { @@ -762,18 +813,22 @@ public Set load(Class clazz, Order order, } else { return load(clazz, backwardsCompatible(order)).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) .skip(page.skip()).limit(page.limit()) .collect(Collectors.toCollection(LinkedHashSet::new)); } } @Override - public Set load(Class clazz, Page page) { + public Set load(Class clazz, Page page, + Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { Map>> data = $load(concourse, - clazz, NO_ORDER, page); + clazz, NO_ORDER, page, realms); return instantiateAll(clazz, data); } finally { @@ -781,17 +836,21 @@ public Set load(Class clazz, Page page) { } } else { - return load(clazz).stream().skip(page.skip()).limit(page.limit()) + return load(clazz).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) + .skip(page.skip()).limit(page.limit()) .collect(Collectors.toCollection(LinkedHashSet::new)); } } @Override - public Set loadAny(Class clazz) { + public Set loadAny(Class clazz, Realms realms) { Concourse concourse = connections.request(); try { Map>> data = $loadAny(concourse, - clazz, NO_ORDER, NO_PAGINATION); + clazz, NO_ORDER, NO_PAGINATION, realms); return instantiateAll(data); } finally { @@ -801,12 +860,13 @@ public Set loadAny(Class clazz) { @SuppressWarnings("deprecation") @Override - public Set loadAny(Class clazz, Order order) { + public Set loadAny(Class clazz, Order order, + Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { Map>> data = $loadAny(concourse, - clazz, order, NO_PAGINATION); + clazz, order, NO_PAGINATION, realms); return instantiateAll(data); } finally { @@ -814,19 +874,23 @@ public Set loadAny(Class clazz, Order order) { } } else { - return load(clazz, backwardsCompatible(order)); + return load(clazz, backwardsCompatible(order)).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) + .collect(Collectors.toCollection(LinkedHashSet::new)); } } @SuppressWarnings("deprecation") @Override public Set loadAny(Class clazz, Order order, - Page page) { + Page page, Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { Map>> data = $loadAny(concourse, - clazz, order, page); + clazz, order, page, realms); return instantiateAll(data); } finally { @@ -835,18 +899,22 @@ public Set loadAny(Class clazz, Order order, } else { return load(clazz, backwardsCompatible(order)).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) .skip(page.skip()).limit(page.limit()) .collect(Collectors.toCollection(LinkedHashSet::new)); } } @Override - public Set loadAny(Class clazz, Page page) { + public Set loadAny(Class clazz, Page page, + Realms realms) { if(hasNativeSortingAndPagination) { Concourse concourse = connections.request(); try { Map>> data = $loadAny(concourse, - clazz, NO_ORDER, page); + clazz, NO_ORDER, page, realms); return instantiateAll(data); } finally { @@ -854,7 +922,11 @@ public Set loadAny(Class clazz, Page page) { } } else { - return load(clazz).stream().skip(page.skip()).limit(page.limit()) + return load(clazz).stream() + .filter(record -> realms.names().isEmpty() || !Sets + .intersection(record.realms(), realms.names()) + .isEmpty()) + .skip(page.skip()).limit(page.limit()) .collect(Collectors.toCollection(LinkedHashSet::new)); } } @@ -974,8 +1046,10 @@ T load(long id) { */ private Map>> $find( Concourse concourse, Class clazz, Criteria criteria, - @Nullable Order order, @Nullable Page page) { - criteria = $Criteria.withinClass(clazz, criteria); + @Nullable Order order, @Nullable Page page, + @Nonnull Realms realms) { + criteria = $Criteria.amongRealms(realms, + $Criteria.withinClass(clazz, criteria)); return read(concourse, criteria, order, page); } @@ -987,12 +1061,15 @@ T load(long id) { * @param criteria * @param order * @param page + * @param realms * @return the result set */ private Map>> $findAny( Concourse concourse, Class clazz, Criteria criteria, - @Nullable Order order, @Nullable Page page) { - criteria = $Criteria.accrossClassHierachy(clazz, criteria); + @Nullable Order order, @Nullable Page page, + @Nonnull Realms realms) { + criteria = $Criteria.amongRealms(realms, + $Criteria.accrossClassHierachy(clazz, criteria)); return read(concourse, criteria, order, page); } @@ -1002,12 +1079,16 @@ T load(long id) { * * @param concourse * @param clazz + * @param order + * @param page + * @param realms * @return the records in the class */ private Map>> $load( Concourse concourse, Class clazz, @Nullable Order order, - @Nullable Page page) { - Criteria criteria = $Criteria.forClass(clazz); + @Nullable Page page, @Nonnull Realms realms) { + Criteria criteria = $Criteria.amongRealms(realms, + $Criteria.forClass(clazz)); return read(concourse, criteria, order, page); } @@ -1017,12 +1098,16 @@ T load(long id) { * * @param concourse * @param clazz + * @param order + * @param page + * @param realms * @return the records in the class hierarchy */ private Map>> $loadAny( Concourse concourse, Class clazz, @Nullable Order order, - @Nullable Page page) { - Criteria criteria = $Criteria.forClassHierarchy(clazz); + @Nullable Page page, Realms realms) { + Criteria criteria = $Criteria.amongRealms(realms, + $Criteria.forClassHierarchy(clazz)); return read(concourse, criteria, order, page); } @@ -1100,12 +1185,14 @@ private int count(Criteria criteria) { * @param criteria * @param order * @param page + * @param realms * @return the matching records in {@code clazz} */ private Set filter(Class clazz, Criteria criteria, - @Nullable Order order, @Nullable Page page) { + @Nullable Order order, @Nullable Page page, + @Nonnull Realms realms) { Set records = order == null ? load(clazz) : load(clazz, order); - Parser parser = Parsers.create(criteria); + Parser parser = Parsers.create($Criteria.amongRealms(realms, criteria)); String[] keys = parser.analyze().keys().toArray(Array.containing()); records = Sets.filter(records, record -> parser.evaluate(record.mmap(keys))); @@ -1124,12 +1211,14 @@ record -> parser.evaluate(record.mmap(keys))); * @param criteria * @param order * @param page + * @param realms * @return the matching records in the {@code clazz} hierarchy */ private Set filterAny(Class clazz, - Criteria criteria, @Nullable Order order, @Nullable Page page) { + Criteria criteria, @Nullable Order order, @Nullable Page page, + @Nonnull Realms realms) { Set records = order == null ? loadAny(clazz) : loadAny(clazz, order); - Parser parser = Parsers.create(criteria); + Parser parser = Parsers.create($Criteria.amongRealms(realms, criteria)); String[] keys = parser.analyze().keys().toArray(Array.containing()); records = Sets.filter(records, record -> parser.evaluate(record.mmap(keys))); @@ -1689,6 +1778,30 @@ public static Criteria accrossClassHierachy(Class clazz, .group(criteria).build(); } + /** + * Utility method to ensure that the {@code criteria} is limited to + * records that exist in the {@code realms}. + * + * @param realms + * @param criteria + * @return limiting {@link Criteria} + */ + public static Criteria amongRealms(Realms realms, Criteria criteria) { + if(realms.names().isEmpty()) { + return criteria; + } + else { + Iterator it = realms.names().iterator(); + ValueState vs = Criteria.where().key(Record.REALMS_KEY) + .operator(Operator.EQUALS).value(it.next()); + while (it.hasNext()) { + vs.or().key(Record.REALMS_KEY).operator(Operator.EQUALS) + .value(it.next()); + } + return Criteria.where().group(criteria).and().group(vs); + } + } + /** * Return a {@link Criteria} to find records within {@code clazz}. * @@ -1725,12 +1838,12 @@ public static Criteria forClassHierarchy(Class clazz) { } /** - * Utility method do ensure that the {@code criteria} is limited to - * querying - * objects that belong to a specific {@code clazz}. + * Utility method to ensure that the {@code criteria} is limited to + * querying objects that belong to a specific {@code clazz}. * * @param clazz * @param criteria + * @return limiting {@link Criteria} */ public static Criteria withinClass(Class clazz, Criteria criteria) { diff --git a/src/main/java/com/cinchapi/runway/Unique.java b/src/main/java/com/cinchapi/runway/Unique.java index 7ebc65a..24e5743 100644 --- a/src/main/java/com/cinchapi/runway/Unique.java +++ b/src/main/java/com/cinchapi/runway/Unique.java @@ -10,6 +10,12 @@ * A marker to indicate that the value for a field in a {@link Record} should be * unique. The ORM framework will ensure that any field with this annotation is * unique before saving the data to the database. + *

+ * By default a {@link Unique} constraint is applied to a single element. You + * can simultaneously apply the same {@link Unique} constraint to multiple + * fields to simulate a compound index by providing the same {@link #name()} to + * the {@link Unique} annotation on all the desired fields. + *

* * @author jnelson */ @@ -18,4 +24,16 @@ @Retention(RetentionPolicy.RUNTIME) public @interface Unique { + /** + * The name of {@link Unique} constraint. Use the same name for + * {@link Unique} constraints on multiple fields to enforce combined + * uniqueness. + * + * @return the name of the {@link Unique} constraint + */ + String name() default ""; + + // TODO: In future, add a field String[] names to allow a field to be a part + // of multiple unique constraints + } diff --git a/src/test/java/com/cinchapi/runway/RecordAnnotationTest.java b/src/test/java/com/cinchapi/runway/RecordAnnotationTest.java new file mode 100644 index 0000000..2665f9b --- /dev/null +++ b/src/test/java/com/cinchapi/runway/RecordAnnotationTest.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2013-2020 Cinchapi Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cinchapi.runway; + +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import com.cinchapi.common.base.CheckedExceptions; +import com.cinchapi.concourse.Tag; +import com.cinchapi.concourse.Timestamp; +import com.cinchapi.concourse.test.ClientServerTest; +import com.cinchapi.concourse.time.Time; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; + +/** + * Unit tests for {@link Record} annotations. + * + * @author Jeff Nelson + */ +public class RecordAnnotationTest extends ClientServerTest { + + @Override + protected String getServerVersion() { + return Testing.CONCOURSE_VERSION; + } + + Runway db; + + @Override + public void beforeEachTest() { + db = Runway.builder().port(server.getClientPort()).build(); + } + + @Override + public void afterEachTest() { + try { + db.close(); + } + catch (Exception e) { + CheckedExceptions.wrapAsRuntimeException(e); + } + } + + @Test + public void testMultiUniqueConstraint() { + Student student = new Student(); + student.name = "Jeff Nelson"; + + Job job = new Job(); + job.title = "Software Engineer"; + + Invitation invitation = new Invitation(); + invitation.student = student; + invitation.job = job; + invitation.timestamp = Timestamp.now(); + invitation.save(); + + invitation = new Invitation(); + invitation.student = student; + invitation.job = job; + invitation.timestamp = Timestamp.now(); + Assert.assertFalse(invitation.save()); + try { + invitation.throwSupressedExceptions(); + Assert.fail(); + } + catch (Exception e) { + Assert.assertTrue( e.getMessage().startsWith("foo must be unique")); + } + } + + @Test + public void testUniqueConstraintPrimitiveLong() { + Model a = new Model(); + a.age = 10L; + Model b = new Model(); + b.age = 10L; + Model c = new Model(); + c.age = 11L; + ImmutableMap.of(a, true, b, false, c, true) + .forEach((model, expected) -> { + boolean actual = model.save(); + if(expected.equals(actual)) { + System.out.println("PASS: " + model.id()); + } + else { + System.out.println("FAIL: " + model.id()); + model.throwSupressedExceptions(); + Assert.fail(); + } + }); + } + + @Test + public void testUniqueConstraintPrimitiveString() { + Model a = new Model(); + a.name = "Jeff"; + Model b = new Model(); + b.name = "Jeff"; + Model c = new Model(); + c.name = "Jeffery"; + ImmutableMap.of(a, true, b, false, c, true) + .forEach((model, expected) -> { + boolean actual = model.save(); + if(expected.equals(actual)) { + System.out.println("PASS: " + model.id()); + } + else { + System.out.println("FAIL: " + model.id()); + model.throwSupressedExceptions(); + Assert.fail(); + } + }); + } + + @Test + public void testUniqueConstraintPrimitiveTag() { + Model a = new Model(); + a.description = Tag.create("Jeff"); + Model b = new Model(); + b.description = Tag.create("Jeff"); + Model c = new Model(); + c.description = Tag.create("Jeffery"); + ImmutableMap.of(a, true, b, false, c, true) + .forEach((model, expected) -> { + boolean actual = model.save(); + if(expected.equals(actual)) { + System.out.println("PASS: " + model.id()); + } + else { + System.out.println("FAIL: " + model.id()); + model.throwSupressedExceptions(); + Assert.fail(); + } + }); + } + + @Test + public void testUniqueConstraintPrimitiveSerializable() { + Model a = new Model(); + a.dict = ImmutableMap.of("foo", "foo"); + Model b = new Model(); + b.dict = ImmutableMap.of("foo", "foo"); + Model c = new Model(); + c.dict = ImmutableMap.of("foo", "bar"); + ImmutableMap.of(a, true, b, false, c, true) + .forEach((model, expected) -> { + boolean actual = model.save(); + if(expected.equals(actual)) { + System.out.println("PASS: " + model.id()); + } + else { + System.out.println("FAIL: " + model.id()); + model.throwSupressedExceptions(); + Assert.fail(); + } + }); + } + + @Test + public void testMultiUniqueConstraintWithSequence() { + Waddle a = new Waddle(); + a.name = "Jeff"; + a.nicknames.add("A"); + a.nicknames.add("C"); + Waddle b = new Waddle(); + b.name = "Ashleah"; + b.nicknames.add("B"); + a.save(); + b.save(); + Waddle c = new Waddle(); + c.name = "Jeff"; + c.nicknames.add("C"); + Assert.assertFalse(c.save()); + try { + c.throwSupressedExceptions(); + Assert.fail(); + } + catch (Exception e) { + Assert.assertTrue( e.getMessage().startsWith("foo must be unique")); + } + } + + @Test + public void testUniqueConstraintWithSequence() { + Computer a = new Computer(); + a.names.add("A"); + a.names.add("B"); + a.names.add("C"); + Assert.assertTrue(a.save()); + Computer b = new Computer(); + b.names.add("B"); + Assert.assertFalse(b.save()); + try { + b.throwSupressedExceptions(); + Assert.fail(); + } + catch (Exception e) { + Assert.assertTrue( e.getMessage().startsWith("names must be unique")); + } + } + + class Invitation extends Record { + + @Unique(name = "foo") + Student student; + + @Unique(name = "foo") + Job job; + + Timestamp timestamp; + } + + class Job extends Record { + + String title; + } + + class Student extends Record { + + String name; + } + + class Model extends Record { + + @Unique + long age = Time.now(); + + @Unique + String name; + + @Unique + Tag description; + + @Unique + Map dict; + + @Unique + Job job; + + } + + class Waddle extends Record { + @Unique(name = "foo") + String name; + + @Unique(name = "foo") + List nicknames = Lists.newArrayList(); + } + + class Computer extends Record { + + @Unique + List names = Lists.newArrayList(); + } + +} diff --git a/src/test/java/com/cinchapi/runway/RecordTest.java b/src/test/java/com/cinchapi/runway/RecordTest.java index 5532ece..d4ed92b 100644 --- a/src/test/java/com/cinchapi/runway/RecordTest.java +++ b/src/test/java/com/cinchapi/runway/RecordTest.java @@ -41,9 +41,6 @@ import com.cinchapi.concourse.test.ClientServerTest; import com.cinchapi.concourse.thrift.Operator; import com.cinchapi.concourse.util.Random; -import com.cinchapi.runway.Record; -import com.cinchapi.runway.Required; -import com.cinchapi.runway.Unique; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -62,7 +59,7 @@ public class RecordTest extends ClientServerTest { @Override protected String getServerVersion() { - return "0.10.4"; + return Testing.CONCOURSE_VERSION; } @Override @@ -811,6 +808,117 @@ public void testCannotDynamicallySetIntrinsicAttributeWithInvalidType() { } Assert.assertNotEquals("10", mock.age); } + + @Test(expected = IllegalStateException.class) + public void testRequiredConstraintEnforcedOnExplicitLoad() { + Mock mock = new Mock(); + mock.name = "Jeff Nelson"; + mock.age = 32; + mock.save(); + Concourse concourse = Concourse.at().port(server.getClientPort()).connect(); + try { + concourse.clear("name", mock.id()); + } + finally { + concourse.close(); + } + mock = runway.load(Mock.class, mock.id()); + } + + @Test(expected = IllegalStateException.class) + public void testRequiredConstraintEnforcedOnImplicitLoad() { + for(int i = 0; i < Random.getScaleCount(); ++i) { + Mock m = new Mock(); + m.name = Random.getSimpleString(); + m.age = i; + m.save(); + } + Mock mock = new Mock(); + mock.name = "Jeff Nelson"; + mock.age = 32; + mock.save(); + Concourse concourse = Concourse.at().port(server.getClientPort()).connect(); + try { + concourse.clear("name", mock.id()); + } + finally { + concourse.close(); + } + Set mocks = runway.find(Mock.class, Criteria.where().key("age").operator(Operator.LESS_THAN_OR_EQUALS).value(32)); + for(Mock m : mocks) { + System.out.println(m.name); + } + } + + @Test + public void testDefaultRealms() { + Mock mock = new Mock(); + mock.name = "Jeff Nelson"; + mock.age = 32; + mock.save(); + mock = runway.load(Mock.class, mock.id()); + Assert.assertEquals(ImmutableSet.of(), mock.realms()); + } + + @Test + public void testAddRealm() { + Mock mock = new Mock(); + mock.name = "Jeff Nelson"; + mock.age = 32; + mock.addRealm("test"); + mock.save(); + mock = runway.load(Mock.class, mock.id()); + Assert.assertEquals(ImmutableSet.of("test"), mock.realms()); + } + + @Test + public void testAddMultiRealms() { + Mock mock = new Mock(); + mock.name = "Jeff Nelson"; + mock.age = 32; + mock.addRealm("test"); + mock.save(); + mock = runway.load(Mock.class, mock.id()); + mock.addRealm("prod"); + mock.save(); + mock = runway.load(Mock.class, mock.id()); + Assert.assertEquals(ImmutableSet.of("test", "prod"), mock.realms()); + } + + @Test + public void testRemoveRealm() { + Mock mock = new Mock(); + mock.name = "Jeff Nelson"; + mock.age = 32; + mock.addRealm("test"); + mock.save(); + mock = runway.load(Mock.class, mock.id()); + mock.addRealm("prod"); + mock.save(); + mock = runway.load(Mock.class, mock.id()); + mock.removeRealm("test"); + mock.save(); + mock = runway.load(Mock.class, mock.id()); + Assert.assertEquals(ImmutableSet.of("prod"), mock.realms()); + } + + @Test + public void testRemoveAllRealms() { + Mock mock = new Mock(); + mock.name = "Jeff Nelson"; + mock.age = 32; + mock.addRealm("test"); + mock.save(); + mock = runway.load(Mock.class, mock.id()); + mock.addRealm("prod"); + mock.save(); + mock = runway.load(Mock.class, mock.id()); + mock.removeRealm("test"); + mock.removeRealm("prod"); + mock.save(); + mock = runway.load(Mock.class, mock.id()); + Assert.assertEquals(ImmutableSet.of(), mock.realms()); + } class Node extends Record { diff --git a/src/test/java/com/cinchapi/runway/RunwayBaseClientServerTest.java b/src/test/java/com/cinchapi/runway/RunwayBaseClientServerTest.java new file mode 100644 index 0000000..3243ff9 --- /dev/null +++ b/src/test/java/com/cinchapi/runway/RunwayBaseClientServerTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2013-2021 Cinchapi Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cinchapi.runway; + +import java.util.Map; +import java.util.function.Supplier; + +import com.cinchapi.common.base.CheckedExceptions; +import com.cinchapi.concourse.test.ClientServerTest; +import com.google.common.collect.ImmutableMap; + +/** + * Base test class for {@link Runway} tests that use the + * {@ClientServerTest} framework. + * + * @author Jeff Nelson + */ +abstract class RunwayBaseClientServerTest extends ClientServerTest { + + @Override + protected String getServerVersion() { + return Testing.CONCOURSE_VERSION; + } + + protected Runway runway; + + @Override + public void beforeEachTest() { + runway = Runway.builder().port(server.getClientPort()).build(); + } + + @Override + public void afterEachTest() { + try { + runway.close(); + } + catch (Exception e) { + throw CheckedExceptions.throwAsRuntimeException(e); + } + } + + class Player extends Record { + String name; + int score; + + public Player(String name, int score) { + this.name = name; + this.score = score; + } + + @Override + protected Map derived() { + return ImmutableMap.of("isAllstar", score > 20); + } + + @Override + protected Map> computed() { + return ImmutableMap.of("isAboveAverage", () -> { + double average = db.load(Player.class).stream() + .mapToInt(player -> player.score).summaryStatistics() + .getAverage(); + return score > average; + }, "isBelowAverage", () -> { + double average = db.load(Player.class).stream() + .mapToInt(player -> player.score).summaryStatistics() + .getAverage(); + return score < average; + }); + } + + } + + class PointGuard extends Player { + + int assists; + + /** + * Construct a new instance. + * @param name + * @param score + */ + public PointGuard(String name, int score, int assists) { + super(name, score); + this.assists = assists; + } + + } + +} diff --git a/src/test/java/com/cinchapi/runway/RunwayCrossVersionTest.java b/src/test/java/com/cinchapi/runway/RunwayCrossVersionTest.java index 131fc48..b69affc 100644 --- a/src/test/java/com/cinchapi/runway/RunwayCrossVersionTest.java +++ b/src/test/java/com/cinchapi/runway/RunwayCrossVersionTest.java @@ -39,7 +39,7 @@ * * @author Jeff Nelson */ -@Versions({ "0.9.6", "0.10.4" }) +@Versions({ "0.9.6", Testing.CONCOURSE_VERSION }) public class RunwayCrossVersionTest extends CrossVersionTest { private Runway runway; diff --git a/src/test/java/com/cinchapi/runway/RunwayFilterTest.java b/src/test/java/com/cinchapi/runway/RunwayFilterTest.java index 07b67c0..3d48616 100644 --- a/src/test/java/com/cinchapi/runway/RunwayFilterTest.java +++ b/src/test/java/com/cinchapi/runway/RunwayFilterTest.java @@ -35,7 +35,7 @@ public class RunwayFilterTest extends ClientServerTest { @Override protected String getServerVersion() { - return ClientServerTest.LATEST_SNAPSHOT_VERSION; + return Testing.CONCOURSE_VERSION; } Runway db; diff --git a/src/test/java/com/cinchapi/runway/RunwayRealmsTest.java b/src/test/java/com/cinchapi/runway/RunwayRealmsTest.java new file mode 100644 index 0000000..7907594 --- /dev/null +++ b/src/test/java/com/cinchapi/runway/RunwayRealmsTest.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2013-2021 Cinchapi Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cinchapi.runway; + +import java.util.Set; + +import org.junit.Assert; +import org.junit.Test; + +import com.cinchapi.concourse.DuplicateEntryException; +import com.cinchapi.concourse.lang.Criteria; +import com.cinchapi.concourse.thrift.Operator; +import com.google.common.collect.ImmutableSet; + +/** + * Unit test for the {@link Realms} feature support in {@link Runway}. + * + * @author Jeff Nelson + */ +public class RunwayRealmsTest extends RunwayBaseClientServerTest { + + @Test + public void testDefaultLoadIsRealmAgnostic() { + Player a = new Player("a", 1); + Player b = new Player("b", 2); + Player c = new Player("c", 3); + a.addRealm("test"); + c.addRealm("test"); + runway.save(a, b, c); + Set records = runway.load(Player.class); + Assert.assertEquals(ImmutableSet.of(a, b, c), records); + } + + @Test + public void testLoadRecordsInSingleRealm() { + Player a = new Player("a", 1); + Player b = new Player("b", 2); + Player c = new Player("c", 3); + a.addRealm("test"); + c.addRealm("test"); + runway.save(a, b, c); + Set records = runway.load(Player.class, Realms.only("test")); + Assert.assertEquals(ImmutableSet.of(a, c), records); + } + + @Test + public void testLoadRecordsInMultipleRealms() { + Player a = new Player("a", 1); + Player b = new Player("b", 2); + Player c = new Player("c", 3); + Player d = new Player("d", 4); + a.addRealm("test"); + c.addRealm("test"); + b.addRealm("prod"); + runway.save(a, b, c, d); + Set records = runway.load(Player.class, + Realms.anyOf("test", "prod")); + Assert.assertEquals(ImmutableSet.of(a, b, c), records); + } + + @Test + public void testLoadRecordsFromEmptyRealm() { + Player a = new Player("a", 1); + Player b = new Player("b", 2); + Player c = new Player("c", 3); + Player d = new Player("d", 4); + a.addRealm("test"); + c.addRealm("test"); + b.addRealm("test"); + runway.save(a, b, c, d); + Set records = runway.load(Player.class, Realms.only("prod")); + Assert.assertEquals(ImmutableSet.of(), records); + } + + @Test + public void testLoadRecordFromWrongRealm() { + Player a = new Player("a", 1); + a.addRealm("test"); + a.save(); + a = runway.load(Player.class, a.id(), Realms.only("prod")); + Assert.assertNull(a); + } + + @Test + public void testLoadRecordFromCorrectRealm() { + Player a = new Player("a", 1); + a.addRealm("test"); + a.save(); + a = runway.load(Player.class, a.id(), Realms.only("test")); + Assert.assertNotNull(a); + } + + @Test + public void testFindUniqueFromRealm() { + Player a = new Player("a", 1); + a.addRealm("test"); + a.save(); + a = runway.findUnique( + Player.class, Criteria.where().key("name") + .operator(Operator.EQUALS).value("a"), + Realms.only("test")); + Assert.assertNotNull(a); + } + + @Test + public void testFindUniqueFromRealmImplicit() { + Player a = new Player("a", 1); + a.addRealm("test"); + a.save(); + a = runway.findUnique(Player.class, Criteria.where().key("name") + .operator(Operator.EQUALS).value("a")); + Assert.assertNotNull(a); + } + + @Test(expected = DuplicateEntryException.class) + public void testFindUniqueFromRealmImplicitConflict() { + Player a1 = new Player("a", 1); + Player a2 = new Player("a", 1); + a1.addRealm("test"); + a2.addRealm("fest"); + a1.save(); + a2.save(); + runway.findUnique(Player.class, Criteria.where().key("name") + .operator(Operator.EQUALS).value("a")); + } + + @Test + public void testFindUniqueFromRealmNotExists() { + Player a = new Player("a", 1); + Player b = new Player("b", 1); + a.addRealm("test"); + b.addRealm("fest"); + runway.save(a, a); + a = runway.findUnique( + Player.class, Criteria.where().key("name") + .operator(Operator.EQUALS).value("a"), + Realms.only("fest")); + Assert.assertNull(a); + } + + @Test + public void testFindUniqueFromRealmDuplicateInDifferentRealm() { + Player a1 = new Player("a", 1); + Player a2 = new Player("a", 1); + a1.addRealm("test"); + a2.addRealm("fest"); + runway.save(a1, a2); + a1 = runway.findUnique( + Player.class, Criteria.where().key("name") + .operator(Operator.EQUALS).value("a"), + Realms.only("test")); + Assert.assertNotNull(a1); + } + + @Test(expected = DuplicateEntryException.class) + public void testFindUniqueFromRealmDuplicateInDifferentRealmConflict() { + Player a1 = new Player("a", 1); + Player a2 = new Player("a", 1); + a1.addRealm("test"); + a2.addRealm("fest"); + runway.save(a1, a2); + a1 = runway.findUnique(Player.class, Criteria.where().key("name") + .operator(Operator.EQUALS).value("a"), Realms.all()); + Assert.assertNotNull(a1); + } + + @Test + public void testFindAnyUniqueFromRealm() { + PointGuard pg = new PointGuard("a", 1, 1); + pg.addRealm("test"); + Player a = new Player("a", 1); + a.addRealm("fest"); + pg.save(); + a.save(); + a = runway.findAnyUnique( + Player.class, Criteria.where().key("name") + .operator(Operator.EQUALS).value("a"), + Realms.anyOf("test")); + Assert.assertNotNull(a); + a = runway.findAnyUnique( + Player.class, Criteria.where().key("name") + .operator(Operator.EQUALS).value("a"), + Realms.anyOf("fest")); + Assert.assertNotNull(a); + } + + @Test(expected = DuplicateEntryException.class) + public void testFindAnyUniqueFromRealmConflict() { + PointGuard pg = new PointGuard("a", 1, 1); + pg.addRealm("test"); + Player a = new Player("a", 1); + a.addRealm("fest"); + pg.save(); + a.save(); + a = runway.findAnyUnique( + Player.class, Criteria.where().key("name") + .operator(Operator.EQUALS).value("a"), + Realms.all()); + } + + // findAny* tests + + // find* tests + + // count tests + + // TODO: need to test legacy paths... + +} diff --git a/src/test/java/com/cinchapi/runway/RunwayTest.java b/src/test/java/com/cinchapi/runway/RunwayTest.java index 474758c..9aea3f7 100644 --- a/src/test/java/com/cinchapi/runway/RunwayTest.java +++ b/src/test/java/com/cinchapi/runway/RunwayTest.java @@ -53,7 +53,7 @@ public class RunwayTest extends ClientServerTest { @Override protected String getServerVersion() { - return "0.10.4"; + return Testing.CONCOURSE_VERSION; } private Runway runway; diff --git a/src/test/java/com/cinchapi/runway/Testing.java b/src/test/java/com/cinchapi/runway/Testing.java new file mode 100644 index 0000000..75f0002 --- /dev/null +++ b/src/test/java/com/cinchapi/runway/Testing.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2013-2020 Cinchapi Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cinchapi.runway; + +/** + * Utilities for testing. + * + * @author Jeff Nelson + */ +public final class Testing { + + /** + * The version of Concourse to use in all tests. + */ + public static final String CONCOURSE_VERSION = "0.10.5"; + + private Testing() {/* no-init */} + +} diff --git a/src/test/java/com/cinchapi/runway/cache/CachingConcourseTest.java b/src/test/java/com/cinchapi/runway/cache/CachingConcourseTest.java index 24a23fd..9ad5e00 100644 --- a/src/test/java/com/cinchapi/runway/cache/CachingConcourseTest.java +++ b/src/test/java/com/cinchapi/runway/cache/CachingConcourseTest.java @@ -30,6 +30,7 @@ import com.cinchapi.concourse.test.ClientServerTest; import com.cinchapi.concourse.thrift.Operator; import com.cinchapi.concourse.util.Random; +import com.cinchapi.runway.Testing; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableMap; @@ -47,7 +48,7 @@ public class CachingConcourseTest extends ClientServerTest { @Override protected String getServerVersion() { - return ClientServerTest.LATEST_SNAPSHOT_VERSION; + return Testing.CONCOURSE_VERSION; } @Override @@ -80,12 +81,14 @@ long record = client.insert(ImmutableMap.of("name", "Jeff Nelson", } @Test - public void testCachevsNonCachePerformanceQuery() throws InterruptedException { + public void testCachevsNonCachePerformanceQuery() + throws InterruptedException { List records = Lists.newArrayList(); for (int i = 0; i < 10000; ++i) { - records.add(client.insert(ImmutableMap.of("name", - Random.getString(), "count", i, "foo", Random.getString(), - "bar", Random.getBoolean(), "baz", Random.getNumber()))); + records.add(client + .insert(ImmutableMap.of("name", Random.getSimpleString(), + "count", i, "foo", Random.getSimpleString(), "bar", + Random.getBoolean(), "baz", Random.getNumber()))); } client.select("count >= 0"); Concourse client2 = Concourse.connect("localhost", @@ -132,19 +135,21 @@ public void action() { client2.close(); } } - + @Test - public void testCachevsNonCachePerformanceBulkSelect() throws InterruptedException { + public void testCachevsNonCachePerformanceBulkSelect() + throws InterruptedException { List records = Lists.newArrayList(); for (int i = 0; i < 10000; ++i) { - records.add(client.insert(ImmutableMap.of("name", - Random.getString(), "count", i, "foo", Random.getString(), - "bar", Random.getBoolean(), "baz", Random.getNumber()))); + records.add(client + .insert(ImmutableMap.of("name", Random.getSimpleString(), + "count", i, "foo", Random.getSimpleString(), "bar", + Random.getBoolean(), "baz", Random.getNumber()))); } client.select("count >= 0"); Concourse client2 = Concourse.connect("localhost", server.getClientPort(), "admin", "admin"); - for(long record : records) { // Warm up the cache... + for (long record : records) { // Warm up the cache... db.select(record); } try { @@ -185,7 +190,7 @@ public void action() { client2.close(); } } - + @Test public void testCacheNotPopulatedWhileStaged() { long record = db.insert(ImmutableMap.of("foo", "bar")); @@ -194,7 +199,7 @@ long record = db.insert(ImmutableMap.of("foo", "bar")); Assert.assertNull(cache.getIfPresent(record)); db.abort(); } - + @Test public void testCacheDoesNotInvalidateWhileStaged() { long record = db.insert(ImmutableMap.of("foo", "bar")); @@ -205,7 +210,7 @@ long record = db.insert(ImmutableMap.of("foo", "bar")); Assert.assertNotNull(cache.getIfPresent(record)); db.abort(); } - + @Test public void testCacheInvalidatedAfterStageIsCommitted() { long record = db.insert(ImmutableMap.of("foo", "bar")); diff --git a/src/test/java/com/cinchapi/runway/cache/CachingConnectionPoolTest.java b/src/test/java/com/cinchapi/runway/cache/CachingConnectionPoolTest.java index 6b1574f..a9096a2 100644 --- a/src/test/java/com/cinchapi/runway/cache/CachingConnectionPoolTest.java +++ b/src/test/java/com/cinchapi/runway/cache/CachingConnectionPoolTest.java @@ -31,6 +31,7 @@ import com.cinchapi.concourse.Concourse; import com.cinchapi.concourse.test.ClientServerTest; import com.cinchapi.concourse.util.Random; +import com.cinchapi.runway.Testing; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -152,7 +153,7 @@ public void testConcurrentBulkSelectAccuracy() throws Exception { @Override protected String getServerVersion() { - return ClientServerTest.LATEST_SNAPSHOT_VERSION; + return Testing.CONCOURSE_VERSION; } } diff --git a/src/test/java/com/cinchapi/runway/util/BackupReadSourcesHashMapTest.java b/src/test/java/com/cinchapi/runway/util/BackupReadSourcesHashMapTest.java index 69c1dd4..6c222ae 100644 --- a/src/test/java/com/cinchapi/runway/util/BackupReadSourcesHashMapTest.java +++ b/src/test/java/com/cinchapi/runway/util/BackupReadSourcesHashMapTest.java @@ -26,7 +26,6 @@ import org.junit.Test; import com.cinchapi.common.collect.Continuation; -import com.cinchapi.runway.util.BackupReadSourcesHashMap; import com.google.common.collect.ImmutableMap; /**