From 38b1e4a6cd9c8bf873e696eb28ac14c83faba9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jack=20=28=EC=A0=95=ED=99=98=29?= Date: Tue, 4 Jun 2019 13:09:48 -0700 Subject: [PATCH 01/14] =?UTF-8?q?parent=20a14ccfbb7b558d27799b4e7b19168505?= =?UTF-8?q?19639708=20author=20Jack=20(=EC=A0=95=ED=99=98)=20=201559678988=20-0700=20committer=20Aaron?= =?UTF-8?q?=20Klish=20=201589583449=20-0500?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create AggregationDataStore module (#845) * Create AggregationDataStore module * Address Aaron's comments * Fix build failure AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron Define QueryEngine Contract (#867) Fixed rebase on master SQL Query Engine (#878) * AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron Added calcite as a dependency. Merged in changes for QueryEngine interface Fixed checkstyle issues Added basic H2 DB test harness Started breaking out projections Moved getValue and setValue from PersistentResource to EntityDictionary Added basic logic to hydrate entities Added FromTable and FromSubquery annotations. Add explicit exclusion of entity relationship hydration Minor cleanup Refactored HQLFilterOperation to take an alias generator Added test support for RSQL filter generation. Some cleanup Added basic support for WHERE clause filtering on the fact table Added working test for subquery SQL Added basic join logic for filters Added a test with a subquery and a filter join Refactored Schema classes and Query to support metric aggregation SQL expansion Added group by support Added logic for ID generation Added sorting logic and test Added pagination support and testing All column references use proper name now for SQL Removed calcite as a query engine Refactored HQLFilterOperation so it can be used for Having and Where clause generaiton Added HAVING clause support Changed Query to take schema instead of entityClass First pass at cleanup Fixed checkstyles Cleanup Cleanup Added a complex SQL expression test and fixed bugs Fixed merge issues. Added another test. Added better logging Fixed bug in pagination SQL generation * Build is working * Inspection rework Add EntityProjection plumbing (#949) * AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron * Initial sketch PersistentResourceTest now passes LifeCycleTest tests now pass More API changes for data store transaction. Also fixed createObject in persistent resource to take the correct projection Started to refactor tests IncludedProcessorTest refactored Refactored LifeCycleTest Started refactor on PersistentResourceTest More refactoring. Fixed a bug in Resource.toPersistentResource Only one test failing in PersistentResourceTest PersistentResourceTests now pass UpdateOnCreateTests now pass Added skeleton for translating JSON-API URL path into an EntityProjection Basic EntityProjectionMaker almost complete Added ability to merge entity projections by relationship Added first test for EntityProjectionMaker Non-working veresion (but clean) Tests now pass All EntityProjectioNMaker tests pass Elide-Core now builds Added handling of sparse attributes and relationships Expanding attributes for included entities Fixed a number of bugs found in IT tests Fixed some of the EntityProjectionMaker tests Fixed unit tests Made temporary modifications to exclude GraphQL (Build now passes) Added sparse field unit tests for EntityProjectionMaker * Fixed build issues after rebase * Removed duplicated Schema class from rebase * Entity projection with aliases (#963) * Hacked up PersistentResource with new design * Core now compiles (and tests can run * EntityProjectionMaker tests pass * Build now passes (major cleanup still needed * Wire in entity projection4 json api (#964) * Fixed DataStore API. Fixed a lot of the core unit tests * Checkstyles and more fixes * Hibernate 5 Tests Pass * Full build passes * Wire in entity projection4 json api (#965) * Initial concept. No testing changed. * Core compiles and EntityProjectionMaker tests (original ones) now pass * Minor edits to TestRequestScope * Full build passes now * removed entity dictionary from entity projection * Pre-inspection cleanup * minor inspection fixup Hydrate Relationship (#987) * AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron * Added basic H2 DB test harness * Started breaking out projections * Moved getValue and setValue from PersistentResource to EntityDictionary * Added basic logic to hydrate entities * Added FromTable and FromSubquery annotations. Add explicit exclusion of entity relationship hydration * Refactored HQLFilterOperation to take an alias generator * Added test support for RSQL filter generation. Some cleanup * Added basic support for WHERE clause filtering on the fact table * Added working test for subquery SQL * Added basic join logic for filters * Added a test with a subquery and a filter join * Refactored Schema classes and Query to support metric aggregation SQL expansion * Added group by support * Added logic for ID generation * Added sorting logic and test * Added pagination support and testing * All column references use proper name now for SQL * Removed calcite as a query engine * Refactored HQLFilterOperation so it can be used for Having and Where clause generaiton * Added HAVING clause support * Changed Query to take schema instead of entityClass * First pass at cleanup * Fixed checkstyles * Cleanup * Hydrate Relationship * Cleanup * Added a complex SQL expression test and fixed bugs * Fixed merge issues. Added another test. Added better logging * Self-review * Self-review * Self-review * Self-review * Self-review * Address comments from @aklish * Refactor EntityHydrator (#893) * rebase * keep Jiaqi's changes * fix id * fix maven verify * Remove HQLFilterOperation * fix dictionary * fix SqlEngineTest * remove unused part * make codacy happy * should use getParametrizedType * address comments Implement GraphQLEntityProjectionMaker (#986) * AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron * Initial sketch PersistentResourceTest now passes LifeCycleTest tests now pass More API changes for data store transaction. Also fixed createObject in persistent resource to take the correct projection Started to refactor tests IncludedProcessorTest refactored Refactored LifeCycleTest Started refactor on PersistentResourceTest More refactoring. Fixed a bug in Resource.toPersistentResource Only one test failing in PersistentResourceTest PersistentResourceTests now pass UpdateOnCreateTests now pass Added skeleton for translating JSON-API URL path into an EntityProjection Basic EntityProjectionMaker almost complete Added ability to merge entity projections by relationship Added first test for EntityProjectionMaker Non-working veresion (but clean) Tests now pass All EntityProjectioNMaker tests pass Elide-Core now builds Added handling of sparse attributes and relationships Expanding attributes for included entities Fixed a number of bugs found in IT tests Fixed some of the EntityProjectionMaker tests Fixed unit tests Made temporary modifications to exclude GraphQL (Build now passes) Added sparse field unit tests for EntityProjectionMaker * Fixed build issues after rebase * GraphQL projection maker using document * Argument handling and fragment check * Add comments * Add fragment resolver * fix typo * break code into more methods * remove pagination and sorting * Removed duplicated Schema class from rebase * re-arrange keywords * Address comment * Add arguments for attribute fields * Handle arguments * support partial query, update edges/node logic * Entity projection with aliases * Entity projection with aliases (#963) * Hacked up PersistentResource with new design * Core now compiles (and tests can run * EntityProjectionMaker tests pass * Build now passes (major cleanup still needed * fix create relationship object using entity * Add tests passed * code clean up * refactor fatcher, fix test cases * rename keywords * rebase branch (#12) * rebased * Graphql projection refactor (#13) * fix fragment resolver * Fix variable resolver * Wire in entity projection4 json api (#964) * Fixed DataStore API. Fixed a lot of the core unit tests * Checkstyles and more fixes * Hibernate 5 Tests Pass * Full build passes * Wire in entity projection4 json api (#965) * Initial concept. No testing changed. * Core compiles and EntityProjectionMaker tests (original ones) now pass * Minor edits to TestRequestScope * Full build passes now * removed entity dictionary from entity projection * Pre-inspection cleanup * minor inspection fixup * rebase * Rebased on AggregationDataStore * clean up extra new lines * address comments * Builder pattern * update comments * remove projection in entity * fix jackson * Hydrate Relationship (#987) (#15) * Address some codecy comments * Add comment for partial query * Reenable tests * Address comments, refactor alias * Add test for alias * swapped test case * fix get type Added AggregationDataStore Code (#991) * Adding testing for aggregation data store * Debugging integration tests * Continuing testing work * AggregationDataStore * AggregationDataStore testing * Added more tests * Aggregation Data Store * Cleaned up testing code * Cleaned up code, fixed helper for AggregationDataStore * end * Fixed checkstyle, other minor fixes * fixed comment * Minor fixes * Fixed id type issue, added exception for queries with no metrics Fixed build (#993) Making TimeDimension an interface (#992) [maven-release-plugin] prepare release 5.0.0-pr1 [maven-release-plugin] prepare for next development iteration Renamed graphQL file to match test (#1002) [maven-release-plugin] prepare release 5.0.0-pr2 [maven-release-plugin] prepare for next development iteration Add JoinTo annotation (#1006) * Added JoinTo Annotation * Added working test * Added TODO comment for next PR * Added TODO comment for next PR * Added Sorting and Filtering support for JoinTo Columns * Fixed IT tests for Aggregation Data Store * Moved entityManager creation to happen separately for each query (#1008) * Moved entityManager creation to happen separately for each query * Closing EntityManager after each query * Inspection rework Column annotation (#1017) * Solved column issue and added QueryEngineFactory * Caching query engine in AggregationDataStore * Fixed column description * Update SQLQueryEngine.java (#1019) * Add SQLMetrix, rearrange packages (#1020) * Add SQLMetrix, rearrange packages * address comment Manager transacton manually (#1021) * Manager transacton manually * Add readonly Hydrate GraphQL Schema with parameterized attributes (#1018) * GraphQL schema expose expected argument name and its type for each attribute * Change empty arguments to unmodifable set AggregationStore: Add multiple time grain definitions to schema (#1022) * Fixed checkstyle warnings and errors. Separated the Query dimension interface from the Schema dimension interface * Added skeleton code to convert entity projection arguments into time grains * Cleanup * Class renames per inspection comments * Inspection comments Refactor time dimension logic (#1028) * Manager transacton manually * Add readonly * some rework * use getTimeDimension() * change exception ISSUE-1026 Add support for @Subselect (#1038) * Manager transacton manually * Add readonly * some rework * use getTimeDimension() * change exception * ISSUE-1026 Add support for @Subselect * Address comments ISSUE-1027 Support join for having clause (#1039) * Manager transacton manually * Add readonly * some rework * use getTimeDimension() * change exception * ISSUE-1027 Support join for having clause function name fixed to enableISO8601Dates (#1052) Support for multiple queries at root is added (#1044) * Support for multiple queries at root is added * Added test with alias * comments resolved Add time grain to GraphQL schema (#1042) * Added basic plumbing to push attributes from the entity projection down to the QueryEngine * Added logic to expand SQL time expression in SQLQueryEngine * Added SQLQueryEngine tests * Added IT tests * The AggregationStore now adds graphql parameters for parameterized columns * Minor refactor * Inspection rework * Minor fix Support multiple query of same entity with different alias (#1055) * Support multiple query of same entity with different alias * add static method to generate keyname for GraphQLProjectionInfo projections * Remove aliasPartialQuerySameAttribute MetadataStore Models (#1068) * Manager transacton manually * Add readonly * some rework * use getTimeDimension() * change exception * Metadatastore models * Address comments * address comments * move root * fix style check SQLQueryTemplate Model (#1073) * SQLQueryTemplate * SQLTables * refactor * update sql dimension projection * update sql dimension projection * clean up dimension projection * refactor sql components * aggregatable field rework * add comments * rearrange packages * Add dimension projection back * fix checkstyle * Add dictionary * Simplify MetricFunction and SQLQueryTemplate * Address comments Integrate Metadata Model and SQLQueryTemplate Model (#1083) * Integrate Metadata Model and SQLQueryTemplate Model * remove AggregationDictionary and AggregationManager * Add timezone * Can only query analyticView Fixed issues with rebase Add auto configuration for aggregation store (#1087) * Added autoconfiguration for QueryEngineFactory * Unified class scanning. Started cleaning up datastores so they only register the entities they manage * Full build passes * Minor cleanup * Minor refactoring * Added EntityManagerFactory bean configuratino * Refactored class scanning for Elide standalone * Updated spring boot starter pom * Removed @Entity from all metadata models. Started cleaning up entity dictionary entity registration * Broken implementation. Just checking in so I can revert if needed. * All tests pass * Added unit tests * Minor cleanup * One more fix * Fixed broken tests * Added package include support back * Class scanning for annotations ignores inherited * Added a test based on inspection comments * Inspection comment fix * Changed initalization of MetadataStore * More inspection rework * Turned back on OWASP scanning * More rework remove @Inherited (#1092) Support Non JPA Entity in AggregationDataStore (#1051) * Create AggregationDataStore module (#845) * Create AggregationDataStore module * Address Aaron's comments * Fix build failure AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron Define QueryEngine Contract (#867) Fixed rebase on master SQL Query Engine (#878) * AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron Added calcite as a dependency. Merged in changes for QueryEngine interface Fixed checkstyle issues Added basic H2 DB test harness Started breaking out projections Moved getValue and setValue from PersistentResource to EntityDictionary Added basic logic to hydrate entities Added FromTable and FromSubquery annotations. Add explicit exclusion of entity relationship hydration Minor cleanup Refactored HQLFilterOperation to take an alias generator Added test support for RSQL filter generation. Some cleanup Added basic support for WHERE clause filtering on the fact table Added working test for subquery SQL Added basic join logic for filters Added a test with a subquery and a filter join Refactored Schema classes and Query to support metric aggregation SQL expansion Added group by support Added logic for ID generation Added sorting logic and test Added pagination support and testing All column references use proper name now for SQL Removed calcite as a query engine Refactored HQLFilterOperation so it can be used for Having and Where clause generaiton Added HAVING clause support Changed Query to take schema instead of entityClass First pass at cleanup Fixed checkstyles Cleanup Cleanup Added a complex SQL expression test and fixed bugs Fixed merge issues. Added another test. Added better logging Fixed bug in pagination SQL generation * Build is working * Inspection rework Add EntityProjection plumbing (#949) * AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron * Initial sketch PersistentResourceTest now passes LifeCycleTest tests now pass More API changes for data store transaction. Also fixed createObject in persistent resource to take the correct projection Started to refactor tests IncludedProcessorTest refactored Refactored LifeCycleTest Started refactor on PersistentResourceTest More refactoring. Fixed a bug in Resource.toPersistentResource Only one test failing in PersistentResourceTest PersistentResourceTests now pass UpdateOnCreateTests now pass Added skeleton for translating JSON-API URL path into an EntityProjection Basic EntityProjectionMaker almost complete Added ability to merge entity projections by relationship Added first test for EntityProjectionMaker Non-working veresion (but clean) Tests now pass All EntityProjectioNMaker tests pass Elide-Core now builds Added handling of sparse attributes and relationships Expanding attributes for included entities Fixed a number of bugs found in IT tests Fixed some of the EntityProjectionMaker tests Fixed unit tests Made temporary modifications to exclude GraphQL (Build now passes) Added sparse field unit tests for EntityProjectionMaker * Fixed build issues after rebase * Removed duplicated Schema class from rebase * Entity projection with aliases (#963) * Hacked up PersistentResource with new design * Core now compiles (and tests can run * EntityProjectionMaker tests pass * Build now passes (major cleanup still needed * Wire in entity projection4 json api (#964) * Fixed DataStore API. Fixed a lot of the core unit tests * Checkstyles and more fixes * Hibernate 5 Tests Pass * Full build passes * Wire in entity projection4 json api (#965) * Initial concept. No testing changed. * Core compiles and EntityProjectionMaker tests (original ones) now pass * Minor edits to TestRequestScope * Full build passes now * removed entity dictionary from entity projection * Pre-inspection cleanup * minor inspection fixup Hydrate Relationship (#987) * AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron * Added basic H2 DB test harness * Started breaking out projections * Moved getValue and setValue from PersistentResource to EntityDictionary * Added basic logic to hydrate entities * Added FromTable and FromSubquery annotations. Add explicit exclusion of entity relationship hydration * Refactored HQLFilterOperation to take an alias generator * Added test support for RSQL filter generation. Some cleanup * Added basic support for WHERE clause filtering on the fact table * Added working test for subquery SQL * Added basic join logic for filters * Added a test with a subquery and a filter join * Refactored Schema classes and Query to support metric aggregation SQL expansion * Added group by support * Added logic for ID generation * Added sorting logic and test * Added pagination support and testing * All column references use proper name now for SQL * Removed calcite as a query engine * Refactored HQLFilterOperation so it can be used for Having and Where clause generaiton * Added HAVING clause support * Changed Query to take schema instead of entityClass * First pass at cleanup * Fixed checkstyles * Cleanup * Hydrate Relationship * Cleanup * Added a complex SQL expression test and fixed bugs * Fixed merge issues. Added another test. Added better logging * Self-review * Self-review * Self-review * Self-review * Self-review * Address comments from @aklish * Refactor EntityHydrator (#893) * rebase * keep Jiaqi's changes * fix id * fix maven verify * Remove HQLFilterOperation * fix dictionary * fix SqlEngineTest * remove unused part * make codacy happy * should use getParametrizedType * address comments Implement GraphQLEntityProjectionMaker (#986) * AggregationDataStore: Schema (#846) * AggregationDataStore: Static Attribute Aggregation * Address comments * Implement TimeDimension and all its supporting components * refactor * Address comments from @aklish * Address comments from @aklish && Tests & Javadoc * Address comments from @aklish * Address comments from @aklish and @hellohanchen * Address comments from Aaron * ToMany is not supported * Address comments from Aaron * Initial sketch PersistentResourceTest now passes LifeCycleTest tests now pass More API changes for data store transaction. Also fixed createObject in persistent resource to take the correct projection Started to refactor tests IncludedProcessorTest refactored Refactored LifeCycleTest Started refactor on PersistentResourceTest More refactoring. Fixed a bug in Resource.toPersistentResource Only one test failing in PersistentResourceTest PersistentResourceTests now pass UpdateOnCreateTests now pass Added skeleton for translating JSON-API URL path into an EntityProjection Basic EntityProjectionMaker almost complete Added ability to merge entity projections by relationship Added first test for EntityProjectionMaker Non-working veresion (but clean) Tests now pass All EntityProjectioNMaker tests pass Elide-Core now builds Added handling of sparse attributes and relationships Expanding attributes for included entities Fixed a number of bugs found in IT tests Fixed some of the EntityProjectionMaker tests Fixed unit tests Made temporary modifications to exclude GraphQL (Build now passes) Added sparse field unit tests for EntityProjectionMaker * Fixed build issues after rebase * GraphQL projection maker using document * Argument handling and fragment check * Add comments * Add fragment resolver * fix typo * break code into more methods * remove pagination and sorting * Removed duplicated Schema class from rebase * re-arrange keywords * Address comment * Add arguments for attribute fields * Handle arguments * support partial query, update edges/node logic * Entity projection with aliases * Entity projection with aliases (#963) * Hacked up PersistentResource with new design * Core now compiles (and tests can run * EntityProjectionMaker tests pass * Build now passes (major cleanup still needed * fix create relationship object using entity * Add tests passed * code clean up * refactor fatcher, fix test cases * rename keywords * rebase branch (#12) * rebased * Graphql projection refactor (#13) * fix fragment resolver * Fix variable resolver * Wire in entity projection4 json api (#964) * Fixed DataStore API. Fixed a lot of the core unit tests * Checkstyles and more fixes * Hibernate 5 Tests Pass * Full build passes * Wire in entity projection4 json api (#965) * Initial concept. No testing changed. * Core compiles and EntityProjectionMaker tests (original ones) now pass * Minor edits to TestRequestScope * Full build passes now * removed entity dictionary from entity projection * Pre-inspection cleanup * minor inspection fixup * rebase * Rebased on AggregationDataStore * clean up extra new lines * address comments * Builder pattern * update comments * remove projection in entity * fix jackson * Hydrate Relationship (#987) (#15) * Address some codecy comments * Add comment for partial query * Reenable tests * Address comments, refactor alias * Add test for alias * swapped test case * fix get type Added AggregationDataStore Code (#991) * Adding testing for aggregation data store * Debugging integration tests * Continuing testing work * AggregationDataStore * AggregationDataStore testing * Added more tests * Aggregation Data Store * Cleaned up testing code * Cleaned up code, fixed helper for AggregationDataStore * end * Fixed checkstyle, other minor fixes * fixed comment * Minor fixes * Fixed id type issue, added exception for queries with no metrics Fixed build (#993) Making TimeDimension an interface (#992) * [maven-release-plugin] prepare release 5.0.0-pr1 * [maven-release-plugin] prepare for next development iteration * Renamed graphQL file to match test (#1002) * [maven-release-plugin] prepare release 5.0.0-pr2 * [maven-release-plugin] prepare for next development iteration * Add JoinTo annotation (#1006) * Added JoinTo Annotation * Added working test * Added TODO comment for next PR * Added TODO comment for next PR * Added Sorting and Filtering support for JoinTo Columns * Fixed IT tests for Aggregation Data Store * Moved entityManager creation to happen separately for each query (#1008) * Moved entityManager creation to happen separately for each query * Closing EntityManager after each query * Inspection rework * Column annotation (#1017) * Solved column issue and added QueryEngineFactory * Caching query engine in AggregationDataStore * Fixed column description * Update SQLQueryEngine.java (#1019) * Add SQLMetrix, rearrange packages (#1020) * Add SQLMetrix, rearrange packages * address comment * Manager transacton manually * Add readonly * Manager transacton manually (#1021) * Manager transacton manually * Add readonly * Hydrate GraphQL Schema with parameterized attributes (#1018) * GraphQL schema expose expected argument name and its type for each attribute * Change empty arguments to unmodifable set * AggregationStore: Add multiple time grain definitions to schema (#1022) * Fixed checkstyle warnings and errors. Separated the Query dimension interface from the Schema dimension interface * Added skeleton code to convert entity projection arguments into time grains * Cleanup * Class renames per inspection comments * Inspection comments * some rework * use getTimeDimension() * change exception * Refactor time dimension logic (#1028) * Manager transacton manually * Add readonly * some rework * use getTimeDimension() * change exception * ISSUE-1026 Add support for @Subselect (#1038) * Manager transacton manually * Add readonly * some rework * use getTimeDimension() * change exception * ISSUE-1026 Add support for @Subselect * Address comments * ISSUE-1027 Support join for having clause (#1039) * Manager transacton manually * Add readonly * some rework * use getTimeDimension() * change exception * ISSUE-1027 Support join for having clause * View Design * Add tests and cleanup * rename annotation * function name fixed to enableISO8601Dates (#1052) * fix bugs * Support for multiple queries at root is added (#1044) * Support for multiple queries at root is added * Added test with alias * comments resolved * merge annotations * don't group by view relationship * Add time grain to GraphQL schema (#1042) * Added basic plumbing to push attributes from the entity projection down to the QueryEngine * Added logic to expand SQL time expression in SQLQueryEngine * Added SQLQueryEngine tests * Added IT tests * The AggregationStore now adds graphql parameters for parameterized columns * Minor refactor * Inspection rework * Minor fix * Support multiple query of same entity with different alias (#1055) * Support multiple query of same entity with different alias * add static method to generate keyname for GraphQLProjectionInfo projections * Remove aliasPartialQuerySameAttribute * MetadataStore Models (#1068) * Manager transacton manually * Add readonly * some rework * use getTimeDimension() * change exception * Metadatastore models * Address comments * address comments * move root * fix style check * SQLQueryTemplate Model (#1073) * SQLQueryTemplate * SQLTables * refactor * update sql dimension projection * update sql dimension projection * clean up dimension projection * refactor sql components * aggregatable field rework * add comments * rearrange packages * Add dimension projection back * fix checkstyle * Add dictionary * Simplify MetricFunction and SQLQueryTemplate * Address comments * Integrate Metadata Model and SQLQueryTemplate Model (#1083) * Integrate Metadata Model and SQLQueryTemplate Model * remove AggregationDictionary and AggregationManager * Add timezone * Can only query analyticView * integrate view with aggregation and metadata * remove includeField * remove @view * Use NonEntityDictionary * remove id * revert access changes * fix JPA entity check * remove @Entity from analyticViews * use table name as relationship type id * revert NonEntitydictinoary * tiny rework * Integration tests * Add jsonapi ittest * aggregation data store doesn't manage jpa entities * address comments fix integration dependencies (#1093) [maven-release-plugin] prepare release 5.0.0-pr3 [maven-release-plugin] prepare for next development iteration Fixed elide standalone pom from rebase Fixed minor bug in rebase Fixed rebase Improving class scanning performance for MetadataStore (#1117) Enable elide5 travis builds (#1129) * Move repeated @Sql annotations to class level (#1119) * Turning on travis builds with code coverage for Elide 5.x * Fixing security issue in spring-boot-web Co-authored-by: Brutus5000 Fix sorting and ambiguous join issue (#1127) * Added sorting on aggregated metric based on latest elide-5.x * Fix ambiguity problem * update comments * fix codacy * refactor generateColumnReference * update comment * address comments * test cleanup * update unittest * fix elide core alias * QueryValidatorTest * EntityProjectionTranslatorTest * go joinFragment approach * delete jointrienode Support no metric query (#1137) [maven-release-plugin] prepare release 5.0.0-pr4 [maven-release-plugin] prepare for next development iteration Check dependency injection (#1138) * Move repeated @Sql annotations to class level (#1119) * Fixing OWASP security warning for Tomcat dependency in Spring Web (#1132) * Adding support for dependency injection of Checks. Added test injection classes * Unit tests pass * Tests pass * Removed Initializer Concept Co-authored-by: Brutus5000 Fix travis log length (#1140) * Move repeated @Sql annotations to class level (#1119) * Fixing OWASP security warning for Tomcat dependency in Spring Web (#1132) * Removed unnecessary request/response logging (to shorten travis logs) * Address inspection comments * Address inspection comments * Address inspection comments * Removed logging of graphQL model building to shorten length * Fixed compilation error Co-authored-by: Brutus5000 [maven-release-plugin] prepare release 5.0.0-pr5 [maven-release-plugin] prepare for next development iteration Refactoring Elide Security Checks (#1144) Removed UpdateOnCreate. Refactored AuditLogger, Pagination, & Sorting (#1146) * Removed UpdateOnCreate. Refactored AuditLogger * Refactored Sorting * Refactored Pagination * Refactored Pagination * Pagination refactor builds and tests pass * Codacy fixes * Inspection rework * Fixes build * More inspection rework * Fix build Refactor share permission (#1154) * Refactored Sorting * Refactored Pagination * Refactored Pagination * Pagination refactor builds and tests pass * Codacy fixes * Refactored SharePermission to NonTransferable * Fixed build * Fixed startup bug * Fixed codacy and inspection comments * Update elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java Co-Authored-By: Jon Kilroy * Inspection rework Co-authored-by: Jon Kilroy metadata refactor (#1179) * metadata refactor * merge table and analyticView * fix reflection package * Make Table constrcut its own columns * table json type alias * add comment @Join and JoinPath add comment hide non-jpd entities in grpahql hide joins refactor hidden remove ant remove relationshp, update model (#1186) Rebased on master [maven-release-plugin] prepare release 5.0.0-pr6 [maven-release-plugin] prepare for next development iteration sourceColumn (#1196) * sourceColumn * address comment * change to sourcePath Cleaning up ElideSettings Added more test fixes Fixed a number of broken tests Build completes Fixed JSON-API Patch Response Fixed GraphQL errors. Added better errors for Forbidden Access Exception. Minor fix Added setId to EntityDictionary Initial version All test pass except for spring Build passes Adding checkstyle comments for classes Inspection comments Fixed checkstyle issues Change the way types are named in the GraphQL schema (#1215) * name utils * ready for review * fix: AggregationDataStoreIntegrationTest#testGraphQLSchema Label Resolver for Dimension Formula (#1208) * sql expression to dimension formula * Metric formula * Add unit tests * refacot formula references * fix comment * fix comment * resolve physical column * refactor dimensionFormula * cleanup * label resolver * cleanup * refactor * cleanup * move code * labelStore * labelStore 2 * remove generator * refactor metric formula * address comments * move symbol table into sql query engine * remove sourceColumn * update reference expression * visitor design * add comments * add unit join path test * fix timeDimensionProjection * fix null value number * address comments * fix codacy Co-authored-by: hchen04 Fixing broken javadoc ColumnProjection Refactor (#1239) * unify projections * remove getFunction() * add table into query template Co-authored-by: hchen04 Refactored metric SQL expansion to occur dynamically at query time ra… (#1270) * Refactored metric SQL expansion to occur dynamically at query time rather than statically during service initialization * Update elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLMetricProjection.java Co-Authored-By: Jon Kilroy Co-authored-by: Aaron Klish Co-authored-by: Jon Kilroy Remove path logic from aggregation store (#1271) * Extended query validation to ensure where clause and sorting clauses don't traverse relationships * Added error check for relationship traversal for Having clauses * Hacked up logic to remove reference Table resolve references that take a path * Minor refactoring * Removed logic to extend join path * Refactored column projections to use generics * Removed reference functions from MetricProjection base class * Refactored so that all SQL generation is done inside the ColumnProjection * Refactored so all projection happens through projections * Refactored column projection creation * Removed unnecessary code * Added templateQuery to arguments required to generate SQL in column projections * Fixed codacy issues Co-authored-by: Aaron Klish Added InMemoryStore to list of datastores that run IT tests (#1225) Co-authored-by: Aaron Klish Adding TimeDimensions to Table (#1273) Elide 5.x async (#1203) * Adding Async Entity Models Co-authored-by: moizarafat * Adding async module and fixing parent pom version Co-authored-by: moizarafat * Adding async service classes, security and cleanup services Co-authored-by: moizarafat * Adding Copyright License Header to Async module classes Co-authored-by: moizarafat * Using new request scope for datastore transactions Co-authored-by: moizarafat * Remove thread sleep used in testing Co-authored-by: moizarafat * Fixing based on code quality review from codacy Co-authored-by: moizarafat * Fixing additional issues based on code quality review from codacy Co-authored-by: moizarafat * Adding getter for AsyncQueryResult Co-authored-by: moizarafat * Reformatting. Co-authored-by: Abhino * Adding mappedBy. Co-authored-by: Abhino * Fixing several review comments - pom, lombok, thread exec Co-authored-by: moizarafat * Fixing codacity errors Co-authored-by: moizarafat * Moving all the DB ORM logic to a utility class for Async Co-authored-by: moizarafat * Remove dependency on number of hosts, delete permission, separate method for interrupttime calculation Co-authored-by: Abhino * Variable Naming convention. Co-authored-by: Abhino * Variable Naming convention. Co-authored-by: Abhino * Removing unwanted params. Co-authored-by: Abhino * Making cleaner a separate service. Co-authored-by: Abhino * Remove unwanted params. Co-authored-by: Abhino * Remove unwanted tabs. Co-authored-by: Abhino * Updating order of modules. Co-authored-by: Abhino * Adding cleanup sql. Co-authored-by: Abhino * Adding delete logic. Co-authored-by: Abhino * Adding query result retention. Co-authored-by: Abhino * Resolving build error. Co-authored-by: Abhino * Changing AsyncDbUtil to use Functional Interface Co-authored-by: moizarafat * Removing sleep Co-authored-by: moizarafat * Adding functional interface logic for executeInTransaction Co-authored-by: moizarafat * Adding debug statements for AsyncDbUtil Co-authored-by: moizarafat * Codacity errors, exception handling Co-authored-by: moizarafat * Adding Async Entity Models Co-authored-by: moizarafat * Adding async module and fixing parent pom version Co-authored-by: moizarafat * Adding async service classes, security and cleanup services Co-authored-by: moizarafat * Adding Copyright License Header to Async module classes Co-authored-by: moizarafat * Using new request scope for datastore transactions Co-authored-by: moizarafat * Remove thread sleep used in testing Co-authored-by: moizarafat * Fixing based on code quality review from codacy Co-authored-by: moizarafat * Fixing additional issues based on code quality review from codacy Co-authored-by: moizarafat * Adding getter for AsyncQueryResult Co-authored-by: moizarafat * Reformatting. Co-authored-by: Abhino * Adding mappedBy. Co-authored-by: Abhino * Fixing several review comments - pom, lombok, thread exec Co-authored-by: moizarafat * Fixing codacity errors Co-authored-by: moizarafat * Moving all the DB ORM logic to a utility class for Async Co-authored-by: moizarafat * Remove dependency on number of hosts, delete permission, separate method for interrupttime calculation Co-authored-by: Abhino * Variable Naming convention. Co-authored-by: Abhino * Variable Naming convention. Co-authored-by: Abhino * Removing unwanted params. Co-authored-by: Abhino * Making cleaner a separate service. Co-authored-by: Abhino * Remove unwanted params. Co-authored-by: Abhino * Remove unwanted tabs. Co-authored-by: Abhino * Updating order of modules. Co-authored-by: Abhino * Adding cleanup sql. Co-authored-by: Abhino * Adding delete logic. Co-authored-by: Abhino * Adding query result retention. Co-authored-by: Abhino * Resolving build error. Co-authored-by: Abhino * Changing AsyncDbUtil to use Functional Interface Co-authored-by: moizarafat * Removing sleep Co-authored-by: moizarafat * Adding functional interface logic for executeInTransaction Co-authored-by: moizarafat * Adding debug statements for AsyncDbUtil Co-authored-by: moizarafat * Codacity errors, exception handling Co-authored-by: moizarafat * Rebased with elide-5.x and changed User principal logic Co-authored-by: moizarafat * Rebase Co-authored-by: Abhino * Changing CleanerThread to use ExecuteInTransaction Co-authored-by: moizarafat * Removing unused imports Co-authored-by: moizarafat * Delete Method changes Co-authored-by: Abhino * Overriding hashCode and equals Co-authored-by: Abhino * Overriding hashCode and equals Co-authored-by: Abhino * Removing unused method Co-authored-by: Abhino * Changing response logic for AsyncQuery Co-authored-by: moizarafat * Adding return to delete method * Resolving Review Comments Co-authored-by: Abhino * Resolve some review comments Co-authored-by: moizarafat * Changing the DAO contract and updating references to DAO Co-authored-by: moizarafat * Removing singletons and extra constructors, simplyfing logic Co-authored-by: moizarafat * Adding default constructor and setters for DefaultDAO Co-authored-by: moizarafat * Fixing codacy error Co-authored-by: moizarafat * Changing Base to use correct obj Co-authored-by: moizarafat * Fixing review comments Co-authored-by: moizarafat * Resolving review comments Co-authored-by: moizarafat * Review comments Co-authored-by: Abhino * Elide 5.x elide-async integration in elide-standalone (#1205) * Integrating elide-async. Co-authored-by: Abhino * Default enableAsync to False. Co-authored-by: Abhino * Default enableAsync to False. Co-authored-by: Abhino * Integrating elide-async. Co-authored-by: Abhino * Default enableAsync to False. Co-authored-by: Abhino * Default enableAsync to False. Co-authored-by: Abhino * Resolving review comments. Co-authored-by: Abhino * Resolving review comments. Co-authored-by: Abhino * Removing unwanted params. Co-authored-by: Abhino * Binding Cleaner service. Co-authored-by: Abhino Binding Cleaner service. Co-authored-by: Abhino * Adding query result retention. Co-authored-by: Abhino * Integrating elide-async. Co-authored-by: Abhino * Default enableAsync to False. Co-authored-by: Abhino * Default enableAsync to False. Co-authored-by: Abhino * Integrating elide-async. Co-authored-by: Abhino * Resolving review comments. Co-authored-by: Abhino * Resolving review comments. Co-authored-by: Abhino * Removing unwanted params. Co-authored-by: Abhino * Binding Cleaner service. Co-authored-by: Abhino Binding Cleaner service. Co-authored-by: Abhino * Adding query result retention. Co-authored-by: Abhino * Making AsyncQueryDAO configurable Co-authored-by: Abhino * Making AsyncQueryDAO configurable Co-authored-by: Abhino * Review comments Co-authored-by: Abhino * Review comments Co-authored-by: abhino * Changing call to DefaultAsyncQueryDAO * Review comments Co-authored-by: Abhino Co-authored-by: Abhino * Elide 5.x elide-async integration in elide-spring (#1204) * Integrating elide-async. Co-authored-by: Abhino Integrating elide-async. Co-authored-by: Abhino * Removing unwanted params. Co-authored-by: Abhino * Binding Cleaner service. Co-authored-by: Abhino * Adding query result retention. Co-authored-by: Abhino * Adding DAO configuration Co-authored-by: Abhino * Checkstyle error Co-authored-by: Abhino * Review comments Co-authored-by: Abhino * Review comments Co-authored-by: Abhino * Review comments Co-authored-by: Abhino * Codacy Error Co-authored-by: Abhino * Changing include type to asyncQuery to avoid conflict Co-authored-by: moizarafat * Sync Co-authored-by: Abhino * Singleton for Cleaner Service Co-authored-by: Abhino * Singleton for Cleaner Service Co-authored-by: Abhino * Singleton for Cleaner Service Co-authored-by: Abhino * Singleton for Executor Service Co-authored-by: Abhino * Sync with Standalone Value * Changes to change UUID columns type as varchar(36) * Review Comments * Review Comments * Fix review comments Co-authored-by: Abhino * Updating per review comments * Remove unused import * Removing status change to Queued * removing unused import * prepersist for status Co-authored-by: moizarafat Co-authored-by: avijay Co-authored-by: moiz arafat Co-authored-by: Moiz Arafat <4307621+moizarafat@users.noreply.github.com> Co-authored-by: moizarafat Simplify life cycle hooks (#1224) * Initial version * Finished code. Starting to flesh out tests * Elide core compiles * Removed CRUDEvent.CrudAction * Added new test structure for LifeCycleTest * Fixed bug in Persistent Resource where relationship reads did not tigger a lifecycle event * Added several LifeCycle Tests * Added create test for persistent resource * Added Elide Persistent Resource Update Test * Added update with change spec test * Added Relationship Edit test * Added relationship test * Fixed checkstyle and compilation errors * Added remove from collection test * Added exception tests * Added delete test * Added read test * Fixed checkstyles * Elide core builds and tests pass * Fixed graphQL tests * Full build now passes * Removed old life cycle annotations * Minor cleanup * Fixed codacy issues * Update README.md Co-Authored-By: Jon Kilroy Co-authored-by: Aaron Klish Co-authored-by: Jon Kilroy Fixed elide-5.x build Fixed rebase issues Turning off retireJS for dependency check Better errors for missing IDs in Patch Extension Request. (#1278) (#1281) * Better errors for missing IDs in Patch Extension Request. (#1278) * Return a better error when handling invalid patch extension requests that are missing IDs * Added tests Co-authored-by: Aaron Klish * Expect encoded response as array Co-authored-by: Aaron Klish Co-authored-by: Aaron Klish Co-authored-by: wcekan [maven-release-plugin] prepare release 5.0.0-pr8 [maven-release-plugin] prepare for next development iteration Patch Extension Lifecycle tests (#1280) * Patch Extension Lifecycle tests * Missing check * Update error body Co-authored-by: wcekan Add version support (#1295) * Initial non working version * More changes * Build passes * Expanded version param outside entity dictionary * Plumbed API version up through RequestScope * Plumbed API version up to controllers and endpoints * All code minus async written and working * Code complete * Build passes * All tests are passing * Changed aggregation store Table id to include version. Metadata will now surface table versions * Added a swagger IT test with API versions * Replace empty version string with constant. Fixed a few bugs in JSON-API parser * Added graphql type introspection test * Added graphql type introspection test * Added spring controller test. Fixed issue with version header parsing * Added spring controller test for graphql * Added swagger controller tests * Added more happy path versioned tests * Fixed bug in GraphQLIT * Added graphQL test for invalid API version * ADded invalid API version graphql controller test * Fixed checkstyles * Added standalone and patch extension tests * Fixed build errors and codacy errors Co-authored-by: Aaron Klish Migrated spring controllers to use async (#1296) Co-authored-by: Aaron Klish fixes for Async Models lifecycle hooks failing (#1294) * elide standalone fixes for lifecycle hooks failing * Manual Binding of lifecycle hooks * Manual binding of lifecycle in elide spring * Refactor * Review Comments * elide standalone fixes for lifecycle hooks failing * Manual Binding of lifecycle hooks * Manual binding of lifecycle in elide spring * Refactor * Review Comments * Rebase with 5.x Co-authored-by: moiz arafat Implement equals/hashCode and immutability where needed (#1297) * ColumnProjection must implement equals/hashCode * Use @Value for SQLTimeDimensionProjection * Use @Value for Argument * Use @Value for TimeDimensionGrain * Use @Value for TimeDimension * Make Column/Metric immutable * Table can be immutable * Use @Value to simplify Query Changes elide to only inject models annotated with @Inject (#1299) * Changes elide to only inject models annotated with @Inject * Codacy fix Co-authored-by: Aaron Klish Enhancemnents to Aggregation Store (#1300) Co-authored-by: moiz arafat QueryEngine result cache (#1279) * Add QueryEngine cache API * Remove outdated javadoc text Elide Async Feature Unit and Integration Tests (#1311) * Added integration,unit test setup for Async Module with few tests Co-authored-by: moizarafat * Adding Licence headers, Sample entity and working Async test for POST,GET Co-authored-by: moizarafat * Adding additional integration tests Co-authored-by: moizarafat * Adding remaining integration tests and updating javadocs Co-authored-by: moizarafat * Adding all unit tests Co-authored-by: moizarafat * Updating tests to work with Singleton changes Co-authored-by: moizarafat * Modifying tests to work with singleton logic Co-authored-by: moizarafat * Resolving some codacy errors Co-authored-by: moizarafat * Adding DSL for GraphQL and addressing review comments Co-authored-by: moizarafat * Fixing unit tests and checkstyle errors Co-authored-by: moizarafat * Moving tests to new format Co-authored-by: moizarafat * Fixing session not closed error and checkstyle errors * Fixing imports Co-authored-by: moizarafat * Removing unused method and updating harness Co-authored-by: moizarafat * Updating unit tests Co-authored-by: moizarafat * Updating ResourceConfig for AsyncTest Co-authored-by: moizarafat * Moving logic for resource config to new test binder Co-authored-by: moizarafat * Adding bindFactory logic for async services Co-authored-by: moizarafat * Fixing IT * Fixing IT Co-authored-by: Abhino * Adding Remaining integration tests Co-authored-by: moizarafat * Consolidating filter logic for integration tests Co-authored-by: moizarafat * Fixing legacy Hibernate entity manager store (so it doesn't recycle the entity manager * Adding additional integration tests for Standalone and Spring boot Co-authored-by: moizarafat * Fixing codacy errors Co-authored-by: moizarafat * Review Comments Co-authored-by: abhino Co-authored-by: avijay Co-authored-by: moizarafat Co-authored-by: moiz arafat Co-authored-by: Aaron Klish in memory compilation integrated with dynamic config helpers (#1255) * Rebase against 5.x Co-authored-by: Ayeswarya Co-authored-by: AvaniMakwana * pom fix Co-authored-by: Ayeswarya Co-authored-by: AvaniMakwana * pom fix Co-authored-by: Ayeswarya Co-authored-by: AvaniMakwana * Merging 1220 Co-authored-by: rishi-aga Co-authored-by: AvaniMakwana * Spring Boot Changes Co-authored-by: Ayeswarya * Spring Boot Changes Co-authored-by: Ayeswarya * Spring Boot Changes Co-authored-by: Ayeswarya * Review Comments Co-authored-by: Ayeswarya * Update DynamicConfigTest.java * Review Comments Co-authored-by: Ayeswarya Co-authored-by: moiz arafat Co-authored-by: AvaniMakwana Co-authored-by: rishi-aga --- .gitignore | 2 + .travis.yml | 1 + README.md | 13 +- checkstyle-suppressions.xml | 4 +- elide-annotations/pom.xml | 3 +- .../yahoo/elide/annotation/ApiVersion.java | 26 + .../com/yahoo/elide/annotation/Exclude.java | 2 - .../com/yahoo/elide/annotation/Include.java | 2 - .../annotation/LifeCycleHookBinding.java | 63 + ...curity.java => LifeCycleHookBindings.java} | 14 +- .../elide/annotation/NonTransferable.java | 30 + .../elide/annotation/OnCreatePostCommit.java | 33 - .../elide/annotation/OnCreatePreCommit.java | 33 - .../elide/annotation/OnCreatePreSecurity.java | 31 - .../elide/annotation/OnDeletePostCommit.java | 25 - .../elide/annotation/OnDeletePreCommit.java | 25 - .../elide/annotation/OnReadPostCommit.java | 33 - .../elide/annotation/OnReadPreCommit.java | 33 - .../elide/annotation/OnReadPreSecurity.java | 31 - .../elide/annotation/OnUpdatePostCommit.java | 33 - .../elide/annotation/OnUpdatePreCommit.java | 33 - .../elide/annotation/OnUpdatePreSecurity.java | 31 - .../elide/annotation/SharePermission.java | 33 - .../yahoo/elide/functions/LifeCycleHook.java | 5 +- .../elide/security/PersistentResource.java | 8 - .../yahoo/elide/security/RequestScope.java | 6 +- .../java/com/yahoo/elide/security/User.java | 17 +- .../yahoo/elide/security/checks/Check.java | 32 +- .../elide/security/checks/CommitCheck.java | 25 - .../elide/security/checks/InlineCheck.java | 16 - .../elide/security/checks/OperationCheck.java | 21 +- .../elide/security/checks/UserCheck.java | 19 +- .../security/checks/prefab/Collections.java | 6 +- .../elide/security/checks/prefab/Common.java | 22 +- .../elide/security/checks/prefab/Role.java | 15 + elide-async/pom.xml | 136 ++ .../elide/async/hooks/ExecuteQueryHook.java | 30 + .../async/hooks/UpdatePrincipalNameHook.java | 27 + .../yahoo/elide/async/models/AsyncBase.java | 52 + .../yahoo/elide/async/models/AsyncQuery.java | 62 + .../elide/async/models/AsyncQueryResult.java | 52 + .../elide/async/models/PrincipalOwned.java | 13 + .../yahoo/elide/async/models/QueryStatus.java | 18 + .../yahoo/elide/async/models/QueryType.java | 14 + .../security/AsyncQueryOperationChecks.java | 52 + .../async/service/AsyncCleanerService.java | 81 + .../async/service/AsyncExecutorService.java | 101 + .../service/AsyncQueryCleanerThread.java | 85 + .../elide/async/service/AsyncQueryDAO.java | 57 + .../service/AsyncQueryInterruptThread.java | 80 + .../elide/async/service/AsyncQueryThread.java | 129 ++ .../async/service/DefaultAsyncQueryDAO.java | 210 ++ .../elide/async/service/Transactional.java | 17 + .../elide/async/service/UpdateQuery.java | 16 + .../service/AsyncCleanerServiceTest.java | 35 + .../service/AsyncExecutorServiceTest.java | 74 + .../service/AsyncQueryCleanerThreadTest.java | 55 + .../async/service/AsyncQueryThreadTest.java | 105 + .../service/DefaultAsyncQueryDAOTest.java | 125 ++ .../elide-dynamic-config-helpers/pom.xml | 165 ++ .../DynamicConfigHelpers.java | 169 ++ .../compile/ElideDynamicEntityCompiler.java | 150 ++ .../ElideDynamicInMemoryClassLoader.java | 55 + .../dynamicconfighelpers/model/Dimension.java | 82 + .../model/ElideSecurityConfig.java | 42 + .../model/ElideTableConfig.java | 37 + .../dynamicconfighelpers/model/Grains.java | 79 + .../dynamicconfighelpers/model/Join.java | 85 + .../dynamicconfighelpers/model/Measure.java | 65 + .../dynamicconfighelpers/model/Rule.java | 74 + .../dynamicconfighelpers/model/Table.java | 137 ++ .../dynamicconfighelpers/model/Type.java | 31 + .../parser/ElideConfigParser.java | 52 + .../parser/handlebars/HandlebarsHelper.java | 125 ++ .../parser/handlebars/HandlebarsHydrator.java | 107 + .../main/resources/elideSecuritySchema.json | 54 + .../src/main/resources/elideTableSchema.json | 406 ++++ .../main/resources/elideVariableSchema.json | 18 + .../src/main/resources/templates/security.hbs | 18 + .../src/main/resources/templates/table.hbs | 90 + .../DynamicConfigHelpersTest.java | 59 + .../dynamicconfighelpers/SchemaTest.java | 42 + .../SecuritySchemaValidationTest.java | 63 + .../TableSchemaValidationTest.java | 77 + .../VariableSchemaValidationTest.java | 63 + .../parser/ElideConfigParserTest.java | 68 + .../handlebars/HandlebarsHydratorTest.java | 286 +++ .../src/test/resources/models/security.hjson | 18 + .../test/resources/models/tables/table1.hjson | 62 + .../test/resources/models/tables/table2.hjson | 48 + .../src/test/resources/models/variables.hjson | 8 + .../models_missing/tables/table1.hjson | 48 + .../resources/security/invalid/security.hjson | 11 + .../resources/security/invalid/security.json | 8 + .../resources/security/valid/security.hjson | 14 + .../resources/security/valid/security.json | 14 + .../test/resources/tables/invalid/table.hjson | 11 + .../test/resources/tables/invalid/table.json | 7 + .../test/resources/tables/valid/table.hjson | 48 + .../test/resources/tables/valid/table.json | 63 + .../variables/invalid/variables.hjson | 11 + .../variables/invalid/variables.json | 8 + .../resources/variables/valid/variables.hjson | 9 + .../resources/variables/valid/variables.json | 11 + elide-contrib/elide-swagger/pom.xml | 6 +- .../contrib/swagger/JsonApiModelResolver.java | 2 +- .../elide/contrib/swagger/SwaggerBuilder.java | 11 +- .../swagger/resources/DocEndpoint.java | 47 +- .../swagger/JsonApiModelResolverTest.java | 6 +- .../contrib/swagger/SwaggerBuilderTest.java | 13 +- .../elide/contrib/swagger/SwaggerIT.java | 14 + .../swagger/SwaggerResourceConfig.java | 38 +- .../swagger => example}/models/Author.java | 2 +- .../models/AuthorType.java | 2 +- .../swagger => example}/models/Book.java | 2 +- .../swagger => example}/models/Publisher.java | 2 +- elide-contrib/elide-test-helpers/pom.xml | 2 +- .../testhelpers/graphql/GraphQLDSL.java | 4 + .../testhelpers/graphql/elements/Field.java | 2 +- elide-contrib/pom.xml | 5 +- elide-core/pom.xml | 10 +- .../src/main/java/com/yahoo/elide/Elide.java | 63 +- .../java/com/yahoo/elide/ElideSettings.java | 3 - .../com/yahoo/elide/ElideSettingsBuilder.java | 58 +- .../com/yahoo/elide/audit/AuditLogger.java | 4 +- .../com/yahoo/elide/audit/LogMessage.java | 195 +- .../com/yahoo/elide/audit/LogMessageImpl.java | 176 ++ .../com/yahoo/elide/audit/Slf4jLogger.java | 5 +- .../com/yahoo/elide/core/ArgumentType.java | 23 + .../java/com/yahoo/elide/core/CRUDEvent.java | 23 +- .../yahoo/elide/core/CheckInstantiator.java | 9 +- .../elide/core/DataStoreTransaction.java | 94 +- .../com/yahoo/elide/core/EntityBinding.java | 339 ++-- .../yahoo/elide/core/EntityDictionary.java | 497 +++-- .../yahoo/elide/core/EntityPermissions.java | 6 +- .../java/com/yahoo/elide/core/HttpStatus.java | 1 + .../com/yahoo/elide/core/Initializer.java | 22 - .../elide/core/LifecycleHookInvoker.java | 19 +- .../main/java/com/yahoo/elide/core/Path.java | 57 +- .../yahoo/elide/core/PersistentResource.java | 421 ++-- .../com/yahoo/elide/core/RequestScope.java | 153 +- .../com/yahoo/elide/core/TimedFunction.java | 40 + ...ifyFieldAccessFilterExpressionVisitor.java | 16 +- .../datastore/inmemory/HashMapDataStore.java | 4 +- .../inmemory/HashMapStoreTransaction.java | 27 +- .../inmemory/InMemoryStoreTransaction.java | 188 +- .../datastore/wrapped/TransactionWrapper.java | 45 +- .../core/exceptions/CustomErrorException.java | 4 +- .../exceptions/ForbiddenAccessException.java | 16 +- .../core/exceptions/HttpStatusException.java | 45 +- .../exceptions/InvalidAttributeException.java | 2 +- .../InvalidCollectionException.java | 2 +- .../InvalidObjectIdentifierException.java | 2 +- .../exceptions/InvalidOperationException.java | 2 +- .../JsonPatchExtensionException.java | 57 +- .../core/exceptions/TimeoutException.java | 21 + .../exceptions/UnknownEntityException.java | 2 +- .../elide/core/filter/FilterPredicate.java | 30 +- .../filter/dialect/DefaultFilterDialect.java | 21 +- .../filter/dialect/JoinFilterDialect.java | 4 +- .../filter/dialect/MultipleFilterDialect.java | 11 +- .../filter/dialect/RSQLFilterDialect.java | 11 +- .../filter/dialect/SubqueryFilterDialect.java | 4 +- .../expression/AndFilterExpression.java | 26 + .../filter/expression/OrFilterExpression.java | 26 + .../elide/core/pagination/Pagination.java | 345 ---- .../elide/core/pagination/PaginationImpl.java | 269 +++ .../sort/{Sorting.java => SortingImpl.java} | 50 +- .../yahoo/elide/extensions/JsonApiPatch.java | 26 +- .../elide/extensions/PatchRequestScope.java | 7 +- .../elide/jsonapi/EntityProjectionMaker.java | 396 ++++ .../processors/IncludedProcessor.java | 20 +- .../yahoo/elide/jsonapi/models/Resource.java | 14 +- .../jsonapi/models/ResourceIdentifier.java | 8 +- .../expression/CanPaginateVisitor.java | 4 +- .../PermissionToFilterExpressionVisitor.java | 2 +- .../state/CollectionTerminalState.java | 46 +- .../elide/parsers/state/RecordState.java | 117 +- .../state/RelationshipTerminalState.java | 16 +- .../yahoo/elide/parsers/state/StartState.java | 39 +- .../com/yahoo/elide/request/Argument.java | 32 + .../com/yahoo/elide/request/Attribute.java | 43 + .../yahoo/elide/request/EntityProjection.java | 258 +++ .../com/yahoo/elide/request/Pagination.java | 64 + .../com/yahoo/elide/request/Relationship.java | 48 + .../java/com/yahoo/elide/request/Sorting.java | 41 + .../resources/DefaultOpaqueUserFunction.java | 18 - .../elide/resources/JsonApiEndpoint.java | 35 +- .../elide/resources/SecurityContextUser.java | 28 + .../elide/security/FilterExpressionCheck.java | 38 +- .../elide/security/PermissionExecutor.java | 1 + .../executors/ActivePermissionExecutor.java | 19 +- .../executors/BypassPermissionExecutor.java | 2 +- .../permissions/PermissionCondition.java | 4 +- .../PermissionExpressionBuilder.java | 2 +- .../expressions/CheckExpression.java | 13 +- .../com/yahoo/elide/utils/ClassScanner.java | 25 +- .../com/yahoo/elide/utils/TypeHelper.java | 108 +- .../yahoo/elide/utils/coerce/CoerceUtil.java | 10 +- .../ElideCustomSerdeRegistrationTest.java | 4 +- ...ssageTest.java => LogMessageImplTest.java} | 33 +- .../yahoo/elide/audit/TestAuditLogger.java | 5 +- .../elide/core/DataStoreTransactionTest.java | 41 +- .../elide/core/EntityDictionaryTest.java | 288 ++- .../com/yahoo/elide/core/LifeCycleTest.java | 1733 +++++++++-------- ...LogicTest.java => PaginationImplTest.java} | 155 +- .../elide/core/PermissionAnnotationTest.java | 13 +- .../core/PersistenceResourceTestSetup.java | 44 +- .../PersistentResourceNoopUpdateTest.java | 24 +- .../elide/core/PersistentResourceTest.java | 614 +++--- .../yahoo/elide/core/RequestScopeTest.java | 7 +- .../com/yahoo/elide/core/TestDictionary.java | 70 + .../com/yahoo/elide/core/TestInjector.java | 33 + .../yahoo/elide/core/TestRequestScope.java | 54 + .../yahoo/elide/core/UpdateOnCreateTest.java | 366 +++- ...ieldAccessFilterExpressionVisitorTest.java | 4 + .../InMemoryStoreTransactionTest.java | 337 ++-- .../wrapped/TransactionWrapperTest.java | 57 +- .../exceptions/HttpStatusExceptionTest.java | 34 +- .../core/filter/FilterPredicateTest.java | 7 +- .../dialect/DefaultFilterDialectTest.java | 13 +- .../dialect/MultipleFilterDialectTest.java | 37 +- .../filter/dialect/RSQLFilterDialectTest.java | 43 +- ...ialectWithColumnCollationStrategyTest.java | 27 +- .../FilterPredicatePushdownExtractorTest.java | 2 +- .../elide/core/utils/ClassScannerTest.java | 11 + .../jsonapi/EntityProjectionMakerTest.java | 760 ++++++++ .../com/yahoo/elide/jsonapi/JsonApiTest.java | 67 +- .../processors/IncludedProcessorTest.java | 61 +- .../expression/CanPaginateVisitorTest.java | 27 +- .../PermissionExpressionVisitorTest.java | 5 +- ...rmissionToFilterExpressionVisitorTest.java | 9 +- .../security/PermissionExecutorTest.java | 269 ++- .../com/yahoo/elide/security/TestUser.java | 17 + .../PermissionExpressionBuilderTest.java | 6 +- elide-core/src/test/java/example/Author.java | 2 - elide-core/src/test/java/example/Book.java | 99 - elide-core/src/test/java/example/Child.java | 27 +- elide-core/src/test/java/example/Editor.java | 2 - elide-core/src/test/java/example/Left.java | 2 + .../src/test/java/example/MapColorShape.java | 2 - .../java/example/NegativeChildIdCheck.java | 5 - .../example/NegativeIntegerUserCheck.java | 7 +- .../src/test/java/example/NoShareEntity.java | 2 + elide-core/src/test/java/example/Parent.java | 32 +- .../src/test/java/example/Publisher.java | 19 +- .../java/example/PublisherUpdateHook.java | 26 + elide-core/src/test/java/example/Right.java | 2 - .../test/java/example/TestCheckMappings.java | 9 +- .../test/java/example/UpdateAndCreate.java | 2 - .../src/test/java/example/UserIdChecks.java | 28 +- .../ContainerWithPackageShare.java | 16 +- .../ShareableWithPackageShare.java | 4 +- .../Untransferable.java} | 6 +- .../package-info.java | 6 +- .../elide-datastore-aggregation/.gitignore | 1 + .../elide-datastore-aggregation/pom.xml | 210 ++ .../aggregation/AggregationDataStore.java | 80 + .../AggregationDataStoreTransaction.java | 74 + .../EntityProjectionTranslator.java | 195 ++ .../datastores/aggregation/QueryEngine.java | 192 ++ .../aggregation/QueryValidator.java | 175 ++ .../aggregation/annotation/Cardinality.java | 44 + .../annotation/CardinalitySize.java | 34 + .../annotation/DimensionFormula.java | 72 + .../aggregation/annotation/FriendlyName.java | 23 + .../aggregation/annotation/Join.java | 25 + .../aggregation/annotation/JoinTo.java | 29 + .../aggregation/annotation/Meta.java | 23 + .../annotation/MetricAggregation.java | 24 + .../aggregation/annotation/MetricFormula.java | 69 + .../aggregation/annotation/Temporal.java | 47 + .../annotation/TimeGrainDefinition.java | 29 + .../datastores/aggregation/core/JoinPath.java | 64 + .../filter/visitor/FilterConstraints.java | 111 ++ .../visitor/SplitFilterExpressionVisitor.java | 239 +++ .../aggregation/metadata/ColumnVisitor.java | 113 ++ .../metadata/FormulaValidator.java | 134 ++ .../aggregation/metadata/MetaDataStore.java | 244 +++ .../metadata/enums/ColumnType.java | 15 + .../aggregation/metadata/enums/Format.java | 14 + .../metadata/enums/FunctionArgumentType.java | 14 + .../aggregation/metadata/enums/TimeGrain.java | 26 + .../aggregation/metadata/enums/ValueType.java | 47 + .../aggregation/metadata/models/Column.java | 134 ++ .../metadata/models/Dimension.java | 22 + .../metadata/models/FunctionArgument.java | 47 + .../aggregation/metadata/models/Metric.java | 115 ++ .../metadata/models/MetricFunction.java | 47 + .../aggregation/metadata/models/Table.java | 226 +++ .../metadata/models/TimeDimension.java | 50 + .../metadata/models/TimeDimensionGrain.java | 32 + .../datastores/aggregation/query/Cache.java | 27 + .../aggregation/query/ColumnProjection.java | 43 + .../aggregation/query/MetricProjection.java | 21 + .../datastores/aggregation/query/Query.java | 55 + .../query/TimeDimensionProjection.java | 84 + .../queryengines/AbstractEntityHydrator.java | 206 ++ .../aggregation/queryengines/StitchList.java | 162 ++ .../queryengines/sql/SQLEntityHydrator.java | 83 + .../queryengines/sql/SQLQueryEngine.java | 289 +++ .../sql/annotation/FromSubquery.java | 28 + .../sql/annotation/FromTable.java | 28 + .../sql/metadata/SQLJoinVisitor.java | 117 ++ .../queryengines/sql/metadata/SQLMetric.java | 58 + .../sql/metadata/SQLReferenceTable.java | 94 + .../sql/metadata/SQLReferenceVisitor.java | 152 ++ .../queryengines/sql/metadata/SQLTable.java | 26 + .../sql/metric/SQLMetricFunction.java | 21 + .../sql/metric/functions/SqlAvg.java | 19 + .../sql/metric/functions/SqlMax.java | 19 + .../sql/metric/functions/SqlMin.java | 19 + .../sql/metric/functions/SqlSum.java | 19 + .../sql/query/SQLColumnProjection.java | 29 + .../sql/query/SQLMetricProjection.java | 33 + .../queryengines/sql/query/SQLQuery.java | 53 + .../sql/query/SQLQueryConstructor.java | 533 +++++ .../sql/query/SQLQueryTemplate.java | 96 + .../sql/query/SQLTimeDimensionProjection.java | 108 + .../EntityProjectionTranslatorTest.java | 141 ++ .../aggregation/QueryValidatorTest.java | 169 ++ .../DimensionFormulaTest.java | 60 + .../dimensionformula/JoinToLoop.java | 34 + .../annotation/dimensionformula/Loop.java | 34 + .../dimensionformula/LoopCountryA.java | 53 + .../dimensionformula/LoopCountryB.java | 53 + .../annotation/metricformula/Loop.java | 34 + .../metricformula/MetricFormulaTest.java | 29 + .../aggregation/core/JoinPathTest.java | 48 + .../aggregation/example/Continent.java | 28 + .../aggregation/example/Country.java | 111 ++ .../aggregation/example/CountryView.java | 67 + .../example/CountryViewNested.java | 56 + .../aggregation/example/Player.java | 34 + .../aggregation/example/PlayerStats.java | 268 +++ .../aggregation/example/PlayerStatsView.java | 45 + .../example/PlayerStatsWithView.java | 209 ++ .../aggregation/example/SubCountry.java | 63 + .../aggregation/example/VideoGame.java | 83 + .../filter/visitor/FilterConstraintsTest.java | 70 + .../SplitFilterExpressionVisitorTest.java | 176 ++ .../AggregationDataStoreTestHarness.java | 46 + .../aggregation/framework/SQLUnitTest.java | 104 + .../AggregationDataStoreIntegrationTest.java | 876 +++++++++ .../metadata/MetaDataStoreTest.java | 49 + .../queryengines/sql/QueryEngineTest.java | 660 +++++++ .../queryengines/sql/SubselectTest.java | 155 ++ .../queryengines/sql/ViewTest.java | 244 +++ .../test/resources/META-INF/persistence.xml | 28 + .../src/test/resources/continent.csv | 3 + .../src/test/resources/country.csv | 3 + .../src/test/resources/create_tables.sql | 40 + .../graphql/responses/testGraphQLSchema.json | 80 + .../src/test/resources/player.csv | 4 + .../src/test/resources/player_stats.csv | 4 + .../src/test/resources/video_games.csv | 5 + .../elide-datastore-hibernate/pom.xml | 4 +- .../elide/core/filter/FilterTranslator.java | 59 +- .../hql/AbstractHQLQueryBuilder.java | 43 +- .../hql/RootCollectionFetchQueryBuilder.java | 13 +- .../RootCollectionPageTotalsQueryBuilder.java | 12 +- .../hql/SubCollectionFetchQueryBuilder.java | 8 +- .../SubCollectionPageTotalsQueryBuilder.java | 13 +- .../core/filter/FilterTranslatorTest.java | 3 +- .../hql/AbstractHQLQueryBuilderTest.java | 21 +- .../RootCollectionFetchQueryBuilderTest.java | 66 +- ...tCollectionPageTotalsQueryBuilderTest.java | 25 +- .../SubCollectionFetchQueryBuilderTest.java | 22 +- ...bCollectionPageTotalsQueryBuilderTest.java | 24 +- .../elide-datastore-hibernate3/pom.xml | 8 +- .../hibernate3/HibernateTransaction.java | 86 +- .../hibernate3/HibernateDataStoreHarness.java | 10 +- .../com/yahoo/elide/tests/DataStoreIT.java | 23 +- .../elide-datastore-hibernate5/pom.xml | 8 +- .../hibernate5/HibernateTransaction.java | 88 +- .../hibernate5/HibernateDataStoreHarness.java | 10 +- ...ibernateEntityManagerDataStoreHarness.java | 10 +- .../com/yahoo/elide/tests/DataStoreIT.java | 23 +- .../elide-datastore-inmemorydb/pom.xml | 84 +- .../inmemory/HashMapDataStoreTest.java | 22 +- elide-datastore/elide-datastore-jpa/pom.xml | 9 +- .../jpa/PersistenceUnitInfoImpl.java | 71 + .../transaction/AbstractJpaTransaction.java | 86 +- .../datastores/jpa/JpaDataStoreHarness.java | 10 +- .../elide-datastore-multiplex/pom.xml | 8 +- .../multiplex/BridgeableTransaction.java | 4 +- .../multiplex/MultiplexManager.java | 16 +- .../multiplex/MultiplexTransaction.java | 80 +- .../multiplex/MultiplexWriteTransaction.java | 30 +- .../multiplex/MultiplexManagerTest.java | 33 +- .../datastores/multiplex/TestDataStore.java | 17 +- .../bridgeable/BridgeableRedisStore.java | 81 +- elide-datastore/elide-datastore-noop/pom.xml | 2 +- .../datastores/noop/NoopTransaction.java | 28 +- .../java/com/yahoo/elide/beans/NoopBean.java | 2 +- .../datastores/noop/NoopDataStoreTest.java | 2 +- .../datastores/noop/NoopTransactionTest.java | 21 +- .../elide-datastore-search/pom.xml | 6 +- .../datastores/search/SearchDataStore.java | 2 +- .../search/SearchDataTransaction.java | 42 +- .../datastores/search/DataStoreLoadTest.java | 140 +- .../datastores/search/DependencyBinder.java | 14 +- elide-datastore/pom.xml | 6 +- elide-example-models/pom.xml | 2 +- .../yahoo/elide/models/triggers/Invoice.java | 54 - .../elide => example}/models/BaseId.java | 4 +- .../models/generics/Employee.java | 2 +- .../models/generics/Manager.java | 2 +- .../models/generics/Overlord.java | 4 +- .../models/generics/Peon.java | 4 +- .../models/generics/package-info.java | 5 +- .../java/example/models/triggers/Invoice.java | 34 + .../triggers/InvoiceCompletionHook.java | 44 + .../triggers/services/BillingService.java | 4 +- .../java/example/models/versioned/BookV2.java | 27 + .../models/versioned/package-info.java | 10 + .../elide-blog-example-resteasy/pom.xml | 6 +- .../elide/example/ElideResourceConfig.java | 2 +- .../yahoo/elide/example/models/Comment.java | 2 - .../com/yahoo/elide/example/models/User.java | 2 - elide-example/elide-blog-example/pom.xml | 8 +- .../elide/example/CommonElideSettings.java | 10 +- .../com/yahoo/elide/example/models/User.java | 2 - .../elide-hibernate3-mysql-example/pom.xml | 6 +- .../hibernate3/ElideResourceConfig.java | 4 - elide-example/pom.xml | 4 +- elide-graphql/pom.xml | 6 +- .../java/com/yahoo/elide/graphql/Entity.java | 67 +- .../com/yahoo/elide/graphql/Environment.java | 2 +- .../elide/graphql/GraphQLConversionUtils.java | 68 +- .../yahoo/elide/graphql/GraphQLEndpoint.java | 44 +- .../elide/graphql/GraphQLErrorSerializer.java | 15 +- .../yahoo/elide/graphql/GraphQLNameUtils.java | 63 + .../elide/graphql/GraphQLRequestScope.java | 22 +- .../java/com/yahoo/elide/graphql/KeyWord.java | 47 + .../com/yahoo/elide/graphql/ModelBuilder.java | 52 +- .../elide/graphql/NonEntityDictionary.java | 3 +- .../graphql/PersistentResourceFetcher.java | 225 +-- .../com/yahoo/elide/graphql/QueryRunner.java | 80 +- .../containers/ConnectionContainer.java | 12 +- .../graphql/containers/EdgesContainer.java | 6 +- .../graphql/containers/NodeContainer.java | 24 +- .../graphql/containers/PageInfoContainer.java | 20 +- .../graphql/containers/RootContainer.java | 22 +- .../graphql/parser/FragmentResolver.java | 139 ++ .../parser/GraphQLEntityProjectionMaker.java | 584 ++++++ .../graphql/parser/GraphQLProjectionInfo.java | 37 + .../graphql/parser/VariableResolver.java | 122 ++ .../elide/graphql/FetcherDeleteTest.java | 11 + .../yahoo/elide/graphql/FetcherFetchTest.java | 108 +- .../elide/graphql/FetcherRemoveTest.java | 12 + .../elide/graphql/FetcherReplaceTest.java | 2 +- .../elide/graphql/FetcherUpdateTest.java | 2 +- .../elide/graphql/GraphQLEndpointTest.java | 290 ++- .../com/yahoo/elide/graphql/GraphQLTest.java | 2 +- .../yahoo/elide/graphql/ModelBuilderTest.java | 118 +- .../PersistentResourceFetcherTest.java | 35 +- .../src/test/java/example/Author.java | 2 - elide-graphql/src/test/java/example/Book.java | 80 - .../graphqlEndpointTestModels/Author.java | 18 +- .../java/graphqlEndpointTestModels/Book.java | 35 +- ...sallowShare.java => DisallowTransfer.java} | 4 +- .../security/CommitChecks.java | 6 +- .../security/UserChecks.java | 4 +- .../java/hooks/BookUpdatePostCommitHook.java | 28 + .../java/hooks/BookUpdatePreCommitHook.java | 28 + .../java/hooks/BookUpdatePreSecurityHook.java | 28 + .../requests/fetch/aliasAmbiguous.graphql | 10 + .../requests/fetch/aliasAttribute.graphql | 9 + .../fetch/aliasPartialQueryAmbiguous.graphql | 23 + .../requests/fetch/aliasRelationship.graphql | 18 + .../fetch/aliasSameRelationship.graphql | 26 + ...agment.graphql => fragmentCorrect.graphql} | 4 +- .../requests/fetch/fragmentInline.graphql | 26 + .../requests/fetch/fragmentLoop.graphql | 31 + .../requests/fetch/fragmentUnknown.graphql | 31 + .../fetch/nestedCollectionFilter.graphql | 4 +- .../fetch/rootCollectionInvalidSort.graphql | 10 + .../requests/fetch/rootMultiple.graphql | 16 + .../requests/fetch/rootUnknownField.graphql | 18 + .../requests/fetch/variableDefinition.graphql | 18 + .../fetch/variableInvalidNonNull.graphql | 18 + .../fetch/variableUnknownReference.graphql | 16 + ...ls.graphql => replaceWithIdsFails.graphql} | 0 .../responses/fetch/aliasAttribute.json | 11 + .../responses/fetch/aliasRelationship.json | 22 + .../fetch/aliasSameRelationship.json | 32 + ...WithFragment.json => fragmentCorrect.json} | 12 +- .../responses/fetch/variableDefinition.json | 22 + elide-integration-tests/pom.xml | 9 +- .../async/integration/tests/AsyncIT.java | 556 ++++++ ...egrationTestApplicationResourceConfig.java | 112 ++ .../com/yahoo/elide/audit/InMemoryLogger.java | 3 +- .../yahoo/elide/audit/TestAuditLogger.java | 4 +- .../EncodedErrorObjectsIT.java | 10 +- .../EncodedErrorResponsesIT.java | 162 -- .../VerboseEncodedErrorResponsesIT.java | 14 - .../errorObjectsTests/ErrorObjectsIT.java | 6 - .../AbstractApiResourceInitializer.java | 7 +- ...rObjectsTestApplicationResourceConfig.java | 21 - ...esponsesTestApplicationResourceConfig.java | 24 - .../EncodedErrorResponsesTestBinder.java | 98 - ...egrationTestApplicationResourceConfig.java | 19 - .../InMemoryDataStoreHarness.java | 10 +- .../elide/initialization/IntegrationTest.java | 2 +- ...egrationTestApplicationResourceConfig.java | 1 + ...egrationTestApplicationResourceConfig.java | 79 + .../initialization/StandardTestBinder.java | 24 +- .../elide/initialization/TestAuthFilter.java | 44 + ...esponsesTestApplicationResourceConfig.java | 2 +- ...a => VerboseErrorResponsesTestBinder.java} | 45 +- .../yahoo/elide/tests/AnyPolymorphismIT.java | 2 +- .../java/com/yahoo/elide/tests/FilterIT.java | 51 +- .../com/yahoo/elide/tests/GenerateIT.java | 2 + .../java/com/yahoo/elide/tests/GraphQLIT.java | 301 +-- .../com/yahoo/elide/tests/PaginateIT.java | 14 +- .../com/yahoo/elide/tests/ResourceIT.java | 233 ++- .../java/com/yahoo/elide/tests/SortingIT.java | 3 + .../{ShareableIT.java => TransferableIT.java} | 144 +- .../com/yahoo/elide/tests/UserTypeIT.java | 5 + .../yahoo/elide/triggers/LifeCycleHookIT.java | 16 +- .../AnotherFilterExpressionCheckObj.java | 2 - .../src/test/java/example/AuditEntity.java | 2 - .../test/java/example/AuditEntityInverse.java | 2 - .../src/test/java/example/Author.java | 2 - .../src/test/java/example/Book.java | 2 - .../src/test/java/example/Chapter.java | 2 - .../src/test/java/example/Child.java | 20 +- .../src/test/java/example/Container.java | 22 +- .../src/test/java/example/Editor.java | 2 - .../example/EntityWithPaginateMaxLimit.java | 2 +- .../example/FilterExpressionCheckObj.java | 4 +- .../src/test/java/example/MapColorShape.java | 2 - .../example/NegativeIntegerUserCheck.java | 2 +- .../src/test/java/example/NoCommitEntity.java | 4 +- ...onal.java => NoTransferBiDirectional.java} | 12 +- .../src/test/java/example/Parent.java | 21 +- .../src/test/java/example/Property.java | 2 - .../src/test/java/example/Smartphone.java | 2 - .../src/test/java/example/SpecialRead.java | 12 +- .../test/java/example/TestCheckMappings.java | 4 - .../src/test/java/example/Tractor.java | 2 - .../{Unshareable.java => Transferable.java} | 8 +- .../{Shareable.java => Untransferable.java} | 12 +- .../src/test/java/example/User.java | 7 +- .../graphQLFetchError.json | 10 - .../graphQLFetchErrorObjectEncoded.json | 7 + .../graphQLFetchErrorResponseEncoded.json | 5 + .../invalidAttributeException.json | 14 +- .../invalidCollection.json | 8 +- .../invalidCollectionErrorObject.json | 2 +- .../invalidEntityBodyException.json | 4 +- ...invalidEntityBodyExceptionErrorObject.json | 2 +- .../invalidObjectIdentifierException.json | 4 +- ...dObjectIdentifierExceptionErrorObject.json | 2 +- .../invalidValueExceptionErrorObject.json | 2 +- .../invalidValueExceptionVerbose.json | 4 +- .../jsonPatchExtensionException.json | 14 +- ...sonPatchExtensionExceptionErrorObject.json | 7 +- .../transactionException.json | 4 +- .../transactionExceptionErrorObject.json | 2 +- .../checkJsonApiPatchWithError.json | 28 +- .../ResourceIT/patchExtBadDelete.json | 2 +- .../resources/ResourceIT/patchExtBadId.json | 5 +- .../ResourceIT/testPatchDeferredOnCreate.json | 4 +- .../versionedPatchExtension.req.json | 14 + .../versionedPatchExtension.resp.json | 12 + .../elide-spring-boot-autoconfigure/pom.xml | 50 +- .../elide/spring/config/AsyncProperties.java | 42 + .../config/DynamicConfigProperties.java | 26 + .../config/ElideAsyncConfiguration.java | 93 + .../spring/config/ElideAutoConfiguration.java | 99 +- .../spring/config/ElideConfigProperties.java | 10 + .../config/ElideDynamicConfiguration.java | 158 ++ .../config/SwaggerControllerProperties.java | 2 +- .../spring/controllers/GraphqlController.java | 43 +- .../spring/controllers/JsonApiController.java | 114 +- .../spring/controllers/SwaggerController.java | 86 +- .../yahoo/elide/spring/controllers/Utils.java | 17 + .../spring/security/AuthenticationUser.java | 30 + .../main/resources/META-INF/spring.factories | 2 + .../yahoo/elide/spring => example}/App.java | 4 +- .../java/example/SecurityConfiguration.java | 25 + .../spring => example}/checks/AdminCheck.java | 2 +- .../example/models/aggregation/Stats.java | 42 + .../models/jpa}/ArtifactGroup.java | 4 +- .../models/jpa}/ArtifactProduct.java | 2 +- .../models/jpa}/ArtifactVersion.java | 2 +- .../models/jpa/v2/ArtifactGroupV2.java | 27 + .../example/models/jpa/v2/package-info.java | 9 + .../example/tests/AggregationStoreTest.java | 54 + .../test/java/example/tests/AsyncTest.java | 144 ++ .../tests/ControllerTest.java | 112 +- .../java/example/tests/DynamicConfigTest.java | 87 + .../tests/IntegrationTest.java | 2 +- .../src/test/resources/application.yaml | 15 +- .../models/tables/playerCountry.hjson | 19 + .../resources/models/tables/playerStats.hjson | 52 + .../elide-spring-boot-starter/pom.xml | 46 +- elide-spring/pom.xml | 4 +- elide-standalone/pom.xml | 17 +- .../elide/standalone/ElideStandalone.java | 23 +- .../java/com/yahoo/elide/standalone/Util.java | 23 +- .../config/ElideResourceConfig.java | 76 +- .../config/ElideStandaloneSettings.java | 96 +- .../UserExtractionFunctionProvider.java | 21 - .../elide/standalone/ElideStandaloneTest.java | 167 -- .../java/example/ElideStandaloneTest.java | 344 ++++ .../checks/AdminCheck.java | 2 +- .../standalone => example}/models/Post.java | 4 +- .../test/java/example/models/v2/PostV2.java | 40 + .../java/example/models/v2/package-info.java | 10 + pom.xml | 18 +- translations/zh/README.md | 9 +- 614 files changed, 27813 insertions(+), 7022 deletions(-) create mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/ApiVersion.java create mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/LifeCycleHookBinding.java rename elide-annotations/src/main/java/com/yahoo/elide/annotation/{OnDeletePreSecurity.java => LifeCycleHookBindings.java} (59%) create mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/NonTransferable.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePostCommit.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePreCommit.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePreSecurity.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePostCommit.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePreCommit.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPostCommit.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPreCommit.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPreSecurity.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePostCommit.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePreCommit.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePreSecurity.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/annotation/SharePermission.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/security/checks/CommitCheck.java delete mode 100644 elide-annotations/src/main/java/com/yahoo/elide/security/checks/InlineCheck.java create mode 100644 elide-async/pom.xml create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/hooks/ExecuteQueryHook.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/hooks/UpdatePrincipalNameHook.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/models/AsyncBase.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/models/AsyncQuery.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/models/AsyncQueryResult.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/models/PrincipalOwned.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/models/QueryStatus.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/models/QueryType.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/models/security/AsyncQueryOperationChecks.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/service/AsyncCleanerService.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/service/AsyncExecutorService.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryCleanerThread.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryDAO.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryInterruptThread.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryThread.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/service/DefaultAsyncQueryDAO.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/service/Transactional.java create mode 100644 elide-async/src/main/java/com/yahoo/elide/async/service/UpdateQuery.java create mode 100644 elide-async/src/test/java/com/yahoo/elide/async/service/AsyncCleanerServiceTest.java create mode 100644 elide-async/src/test/java/com/yahoo/elide/async/service/AsyncExecutorServiceTest.java create mode 100644 elide-async/src/test/java/com/yahoo/elide/async/service/AsyncQueryCleanerThreadTest.java create mode 100644 elide-async/src/test/java/com/yahoo/elide/async/service/AsyncQueryThreadTest.java create mode 100644 elide-async/src/test/java/com/yahoo/elide/async/service/DefaultAsyncQueryDAOTest.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/pom.xml create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigHelpers.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/compile/ElideDynamicEntityCompiler.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/compile/ElideDynamicInMemoryClassLoader.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Dimension.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/ElideSecurityConfig.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/ElideTableConfig.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Grains.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Join.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Measure.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Rule.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Table.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Type.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/ElideConfigParser.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHelper.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHydrator.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideSecuritySchema.json create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideTableSchema.json create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideVariableSchema.json create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/resources/templates/security.hbs create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/main/resources/templates/table.hbs create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigHelpersTest.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/SchemaTest.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/SecuritySchemaValidationTest.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/TableSchemaValidationTest.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/VariableSchemaValidationTest.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/ElideConfigParserTest.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHydratorTest.java create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/security.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/tables/table1.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/tables/table2.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/variables.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/models_missing/tables/table1.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/invalid/security.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/invalid/security.json create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/valid/security.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/valid/security.json create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/invalid/table.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/invalid/table.json create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/valid/table.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/valid/table.json create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/invalid/variables.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/invalid/variables.json create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/valid/variables.hjson create mode 100644 elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/valid/variables.json rename elide-contrib/elide-swagger/src/test/java/{com/yahoo/elide/contrib/swagger => example}/models/Author.java (93%) rename elide-contrib/elide-swagger/src/test/java/{com/yahoo/elide/contrib/swagger => example}/models/AuthorType.java (80%) rename elide-contrib/elide-swagger/src/test/java/{com/yahoo/elide/contrib/swagger => example}/models/Book.java (96%) rename elide-contrib/elide-swagger/src/test/java/{com/yahoo/elide/contrib/swagger => example}/models/Publisher.java (96%) create mode 100644 elide-core/src/main/java/com/yahoo/elide/audit/LogMessageImpl.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/core/ArgumentType.java delete mode 100644 elide-core/src/main/java/com/yahoo/elide/core/Initializer.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/core/TimedFunction.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/core/exceptions/TimeoutException.java delete mode 100644 elide-core/src/main/java/com/yahoo/elide/core/pagination/Pagination.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/core/pagination/PaginationImpl.java rename elide-core/src/main/java/com/yahoo/elide/core/sort/{Sorting.java => SortingImpl.java} (80%) create mode 100644 elide-core/src/main/java/com/yahoo/elide/jsonapi/EntityProjectionMaker.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/request/Argument.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/request/Attribute.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/request/EntityProjection.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/request/Pagination.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/request/Relationship.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/request/Sorting.java delete mode 100644 elide-core/src/main/java/com/yahoo/elide/resources/DefaultOpaqueUserFunction.java create mode 100644 elide-core/src/main/java/com/yahoo/elide/resources/SecurityContextUser.java rename elide-core/src/test/java/com/yahoo/elide/audit/{LogMessageTest.java => LogMessageImplTest.java} (82%) rename elide-core/src/test/java/com/yahoo/elide/core/{PaginationLogicTest.java => PaginationImplTest.java} (52%) create mode 100644 elide-core/src/test/java/com/yahoo/elide/core/TestDictionary.java create mode 100644 elide-core/src/test/java/com/yahoo/elide/core/TestInjector.java create mode 100644 elide-core/src/test/java/com/yahoo/elide/core/TestRequestScope.java create mode 100644 elide-core/src/test/java/com/yahoo/elide/jsonapi/EntityProjectionMakerTest.java create mode 100644 elide-core/src/test/java/com/yahoo/elide/security/TestUser.java create mode 100644 elide-core/src/test/java/example/PublisherUpdateHook.java rename elide-core/src/test/java/example/{packageshareable => nontransferable}/ContainerWithPackageShare.java (70%) rename elide-core/src/test/java/example/{packageshareable => nontransferable}/ShareableWithPackageShare.java (89%) rename elide-core/src/test/java/example/{packageshareable/UnshareableWithEntityUnshare.java => nontransferable/Untransferable.java} (85%) rename elide-core/src/test/java/example/{packageshareable => nontransferable}/package-info.java (56%) create mode 100644 elide-datastore/elide-datastore-aggregation/.gitignore create mode 100644 elide-datastore/elide-datastore-aggregation/pom.xml create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStore.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/AggregationDataStoreTransaction.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/EntityProjectionTranslator.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/QueryEngine.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/QueryValidator.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/Cardinality.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/CardinalitySize.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/DimensionFormula.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/FriendlyName.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/Join.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/JoinTo.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/Meta.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/MetricAggregation.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/MetricFormula.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/Temporal.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/annotation/TimeGrainDefinition.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/core/JoinPath.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/filter/visitor/FilterConstraints.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/filter/visitor/SplitFilterExpressionVisitor.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/ColumnVisitor.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/FormulaValidator.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStore.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/enums/ColumnType.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/enums/Format.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/enums/FunctionArgumentType.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/enums/TimeGrain.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/enums/ValueType.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/Column.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/Dimension.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/FunctionArgument.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/Metric.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/MetricFunction.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/Table.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimension.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/metadata/models/TimeDimensionGrain.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/query/Cache.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/query/ColumnProjection.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/query/MetricProjection.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/query/Query.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/query/TimeDimensionProjection.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/AbstractEntityHydrator.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/StitchList.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLEntityHydrator.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SQLQueryEngine.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/annotation/FromSubquery.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/annotation/FromTable.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLJoinVisitor.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLMetric.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLReferenceTable.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLReferenceVisitor.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metadata/SQLTable.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metric/SQLMetricFunction.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metric/functions/SqlAvg.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metric/functions/SqlMax.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metric/functions/SqlMin.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/metric/functions/SqlSum.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLColumnProjection.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLMetricProjection.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLQuery.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLQueryConstructor.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLQueryTemplate.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/main/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/query/SQLTimeDimensionProjection.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/EntityProjectionTranslatorTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/QueryValidatorTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/annotation/dimensionformula/DimensionFormulaTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/annotation/dimensionformula/JoinToLoop.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/annotation/dimensionformula/Loop.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/annotation/dimensionformula/LoopCountryA.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/annotation/dimensionformula/LoopCountryB.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/annotation/metricformula/Loop.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/annotation/metricformula/MetricFormulaTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/core/JoinPathTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Continent.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Country.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/CountryView.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/CountryViewNested.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/Player.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStats.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStatsView.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/PlayerStatsWithView.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/SubCountry.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/example/VideoGame.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/FilterConstraintsTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/filter/visitor/SplitFilterExpressionVisitorTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/framework/AggregationDataStoreTestHarness.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/framework/SQLUnitTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/integration/AggregationDataStoreIntegrationTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/metadata/MetaDataStoreTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/QueryEngineTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/SubselectTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/java/com/yahoo/elide/datastores/aggregation/queryengines/sql/ViewTest.java create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/resources/META-INF/persistence.xml create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/resources/continent.csv create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/resources/country.csv create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/resources/create_tables.sql create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/resources/graphql/responses/testGraphQLSchema.json create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/resources/player.csv create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/resources/player_stats.csv create mode 100644 elide-datastore/elide-datastore-aggregation/src/test/resources/video_games.csv create mode 100644 elide-datastore/elide-datastore-jpa/src/main/java/com/yahoo/elide/datastores/jpa/PersistenceUnitInfoImpl.java delete mode 100644 elide-example-models/src/main/java/com/yahoo/elide/models/triggers/Invoice.java rename elide-example-models/src/main/java/{com/yahoo/elide => example}/models/BaseId.java (96%) rename elide-example-models/src/main/java/{com/yahoo/elide => example}/models/generics/Employee.java (90%) rename elide-example-models/src/main/java/{com/yahoo/elide => example}/models/generics/Manager.java (89%) rename elide-example-models/src/main/java/{com/yahoo/elide => example}/models/generics/Overlord.java (84%) rename elide-example-models/src/main/java/{com/yahoo/elide => example}/models/generics/Peon.java (82%) rename elide-example-models/src/main/java/{com/yahoo/elide => example}/models/generics/package-info.java (55%) create mode 100644 elide-example-models/src/main/java/example/models/triggers/Invoice.java create mode 100644 elide-example-models/src/main/java/example/models/triggers/InvoiceCompletionHook.java rename elide-example-models/src/main/java/{com/yahoo/elide => example}/models/triggers/services/BillingService.java (82%) create mode 100644 elide-example-models/src/main/java/example/models/versioned/BookV2.java create mode 100644 elide-example-models/src/main/java/example/models/versioned/package-info.java create mode 100644 elide-graphql/src/main/java/com/yahoo/elide/graphql/GraphQLNameUtils.java create mode 100644 elide-graphql/src/main/java/com/yahoo/elide/graphql/KeyWord.java create mode 100644 elide-graphql/src/main/java/com/yahoo/elide/graphql/parser/FragmentResolver.java create mode 100644 elide-graphql/src/main/java/com/yahoo/elide/graphql/parser/GraphQLEntityProjectionMaker.java create mode 100644 elide-graphql/src/main/java/com/yahoo/elide/graphql/parser/GraphQLProjectionInfo.java create mode 100644 elide-graphql/src/main/java/com/yahoo/elide/graphql/parser/VariableResolver.java rename elide-graphql/src/test/java/graphqlEndpointTestModels/{DisallowShare.java => DisallowTransfer.java} (85%) create mode 100644 elide-graphql/src/test/java/hooks/BookUpdatePostCommitHook.java create mode 100644 elide-graphql/src/test/java/hooks/BookUpdatePreCommitHook.java create mode 100644 elide-graphql/src/test/java/hooks/BookUpdatePreSecurityHook.java create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/aliasAmbiguous.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/aliasAttribute.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/aliasPartialQueryAmbiguous.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/aliasRelationship.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/aliasSameRelationship.graphql rename elide-graphql/src/test/resources/graphql/requests/fetch/{fetchWithFragment.graphql => fragmentCorrect.graphql} (74%) create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/fragmentInline.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/fragmentLoop.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/fragmentUnknown.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/rootCollectionInvalidSort.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/rootMultiple.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/rootUnknownField.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/variableDefinition.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/variableInvalidNonNull.graphql create mode 100644 elide-graphql/src/test/resources/graphql/requests/fetch/variableUnknownReference.graphql rename elide-graphql/src/test/resources/graphql/requests/replace/{replaceWithidsFails.graphql => replaceWithIdsFails.graphql} (100%) create mode 100644 elide-graphql/src/test/resources/graphql/responses/fetch/aliasAttribute.json create mode 100644 elide-graphql/src/test/resources/graphql/responses/fetch/aliasRelationship.json create mode 100644 elide-graphql/src/test/resources/graphql/responses/fetch/aliasSameRelationship.json rename elide-graphql/src/test/resources/graphql/responses/fetch/{fetchWithFragment.json => fragmentCorrect.json} (77%) create mode 100644 elide-graphql/src/test/resources/graphql/responses/fetch/variableDefinition.json create mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/async/integration/tests/AsyncIT.java create mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/async/integration/tests/framework/AsyncIntegrationTestApplicationResourceConfig.java delete mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/errorEncodingTests/EncodedErrorResponsesIT.java delete mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/initialization/EncodedErrorObjectsTestApplicationResourceConfig.java delete mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/initialization/EncodedErrorResponsesTestApplicationResourceConfig.java delete mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/initialization/EncodedErrorResponsesTestBinder.java delete mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/initialization/ErrorObjectsIntegrationTestApplicationResourceConfig.java create mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/initialization/LifeCycleIntegrationTestApplicationResourceConfig.java create mode 100644 elide-integration-tests/src/test/java/com/yahoo/elide/initialization/TestAuthFilter.java rename elide-integration-tests/src/test/java/com/yahoo/elide/initialization/{ErrorObjectsTestBinder.java => VerboseErrorResponsesTestBinder.java} (67%) rename elide-integration-tests/src/test/java/com/yahoo/elide/tests/{ShareableIT.java => TransferableIT.java} (77%) rename elide-integration-tests/src/test/java/example/{NoShareBiDirectional.java => NoTransferBiDirectional.java} (62%) rename elide-integration-tests/src/test/java/example/{Unshareable.java => Transferable.java} (80%) rename elide-integration-tests/src/test/java/example/{Shareable.java => Untransferable.java} (72%) delete mode 100644 elide-integration-tests/src/test/resources/EncodedErrorResponsesIT/graphQLFetchError.json create mode 100644 elide-integration-tests/src/test/resources/EncodedErrorResponsesIT/graphQLFetchErrorObjectEncoded.json create mode 100644 elide-integration-tests/src/test/resources/EncodedErrorResponsesIT/graphQLFetchErrorResponseEncoded.json create mode 100644 elide-integration-tests/src/test/resources/ResourceIT/versionedPatchExtension.req.json create mode 100644 elide-integration-tests/src/test/resources/ResourceIT/versionedPatchExtension.resp.json create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/AsyncProperties.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/DynamicConfigProperties.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideAsyncConfiguration.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/config/ElideDynamicConfiguration.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/controllers/Utils.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/main/java/com/yahoo/elide/spring/security/AuthenticationUser.java rename elide-spring/elide-spring-boot-autoconfigure/src/test/java/{com/yahoo/elide/spring => example}/App.java (84%) create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/SecurityConfiguration.java rename elide-spring/elide-spring-boot-autoconfigure/src/test/java/{com/yahoo/elide/spring => example}/checks/AdminCheck.java (94%) create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/aggregation/Stats.java rename elide-spring/elide-spring-boot-autoconfigure/src/test/java/{com/yahoo/elide/spring/models => example/models/jpa}/ArtifactGroup.java (91%) rename elide-spring/elide-spring-boot-autoconfigure/src/test/java/{com/yahoo/elide/spring/models => example/models/jpa}/ArtifactProduct.java (94%) rename elide-spring/elide-spring-boot-autoconfigure/src/test/java/{com/yahoo/elide/spring/models => example/models/jpa}/ArtifactVersion.java (92%) create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v2/ArtifactGroupV2.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/models/jpa/v2/package-info.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/AggregationStoreTest.java create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/AsyncTest.java rename elide-spring/elide-spring-boot-autoconfigure/src/test/java/{com/yahoo/elide/spring => example}/tests/ControllerTest.java (75%) create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/java/example/tests/DynamicConfigTest.java rename elide-spring/elide-spring-boot-autoconfigure/src/test/java/{com/yahoo/elide/spring => example}/tests/IntegrationTest.java (95%) create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/resources/models/tables/playerCountry.hjson create mode 100644 elide-spring/elide-spring-boot-autoconfigure/src/test/resources/models/tables/playerStats.hjson delete mode 100644 elide-standalone/src/main/java/com/yahoo/elide/standalone/interfaces/UserExtractionFunctionProvider.java delete mode 100644 elide-standalone/src/test/java/com/yahoo/elide/standalone/ElideStandaloneTest.java create mode 100644 elide-standalone/src/test/java/example/ElideStandaloneTest.java rename elide-standalone/src/test/java/{com/yahoo/elide/standalone => example}/checks/AdminCheck.java (93%) rename elide-standalone/src/test/java/{com/yahoo/elide/standalone => example}/models/Post.java (90%) create mode 100644 elide-standalone/src/test/java/example/models/v2/PostV2.java create mode 100644 elide-standalone/src/test/java/example/models/v2/package-info.java diff --git a/.gitignore b/.gitignore index 40c012cba2..b35bcfc796 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ dependency-reduced-pom.xml .project */.project */*/.project +*.factorypath +*.vscode diff --git a/.travis.yml b/.travis.yml index 2daa1906d7..deb9b6fbfb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ branches: only: - master + - elide-5.x - "/^[0-9]+\\.[0-9]+(\\.[0-9]+|-(alpha|beta)-[0-9]+)/" install: true diff --git a/README.md b/README.md index 80d92e73f8..99523cd47d 100644 --- a/README.md +++ b/README.md @@ -134,8 +134,9 @@ Add Lifecycle hooks to your models to embed custom business logic that execute i @Include(rootLevel = true) @ReadPermission("Everyone") @CreatePermission("Admin OR Publisher") -@DeletePermission("Noone") -@UpdatePermission("Noone") +@DeletePermission("None") +@UpdatePermission("None") +@LifeCycleHookBinding(operation = UPDATE, hook = BookCreationHook.class, phase = PRECOMMIT) public class Book { @Id @@ -146,9 +147,13 @@ public class Book { @ManyToMany(mappedBy = "books") private Set authors; +} + +public class BookCreationHook implements LifeCycleHook { - @OnCreatePreCommit - public void onCreate(RequestScope scope) { + @Override + public void execute(LifeCycleHookBinding.Operation operation, Book book, + RequestScope requestScope, Optional changes) { //Do something } } diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index b79828e727..7be2a7532a 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -10,8 +10,10 @@ "http://www.puppycrawl.com/dtds/suppressions_1_0.dtd"> - + + + diff --git a/elide-annotations/pom.xml b/elide-annotations/pom.xml index 26bf308125..2f1e48bd31 100644 --- a/elide-annotations/pom.xml +++ b/elide-annotations/pom.xml @@ -9,7 +9,7 @@ com.yahoo.elide elide-parent-pom - 4.6.3-SNAPSHOT + 5.0.0-pr9-SNAPSHOT @@ -53,5 +53,4 @@ - diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/ApiVersion.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/ApiVersion.java new file mode 100644 index 0000000000..99f04a7f9a --- /dev/null +++ b/elide-annotations/src/main/java/com/yahoo/elide/annotation/ApiVersion.java @@ -0,0 +1,26 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.annotation; + +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Versions API Models. + */ +@Target({PACKAGE}) +@Retention(RUNTIME) +public @interface ApiVersion { + + /** + * Models in this package are tied to this API version. + * @return the string (default = "") + */ + String version() default ""; +} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/Exclude.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/Exclude.java index 043fa15b70..60dcce6e1e 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/Exclude.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/annotation/Exclude.java @@ -11,7 +11,6 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -20,6 +19,5 @@ */ @Target({METHOD, FIELD, TYPE, PACKAGE}) @Retention(RUNTIME) -@Inherited public @interface Exclude { } diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/Include.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/Include.java index af1faadf13..4b90e31683 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/Include.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/annotation/Include.java @@ -9,7 +9,6 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; -import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.Target; @@ -18,7 +17,6 @@ */ @Target({TYPE, PACKAGE}) @Retention(RUNTIME) -@Inherited public @interface Include { /** diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/LifeCycleHookBinding.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/LifeCycleHookBinding.java new file mode 100644 index 0000000000..e4915e6d5d --- /dev/null +++ b/elide-annotations/src/main/java/com/yahoo/elide/annotation/LifeCycleHookBinding.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.annotation; + +import com.yahoo.elide.functions.LifeCycleHook; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Executes arbitrary logic (a lifecycle hook) when an Elide model is read or written. + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(LifeCycleHookBindings.class) +public @interface LifeCycleHookBinding { + + enum Operation { + CREATE, + READ, + UPDATE, + DELETE + }; + + enum TransactionPhase { + PRESECURITY, + PRECOMMIT, + POSTCOMMIT + } + + /** + * The function to invoke when this life cycle triggers. + * @return the function class. + */ + Class hook(); + + /** + * Which CRUD operation to trigger on. + * @return CREATE, READ, UPDATE, or DELETE + */ + Operation operation(); + + /** + * Which transaction phase to trigger on. + * @return PRESECURITY, PRECOMMIT, or POSTCOMMIT + */ + TransactionPhase phase() default TransactionPhase.PRECOMMIT; + + /** + * Controls how often the hook is invoked: + * A hook is invoked once per class per request (when bound to the model). + * A hook is invoked once per field per request (when bound to a model field or method). + * A hook is invoked one or more times per class per request (when bound to a model and oncePerRequest is false). + * @return true or false. + */ + boolean oncePerRequest() default true; +} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePreSecurity.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/LifeCycleHookBindings.java similarity index 59% rename from elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePreSecurity.java rename to elide-annotations/src/main/java/com/yahoo/elide/annotation/LifeCycleHookBindings.java index f826635535..5a030a0b0b 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePreSecurity.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/annotation/LifeCycleHookBindings.java @@ -1,23 +1,21 @@ /* - * Copyright 2016, Yahoo Inc. + * Copyright 2020, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ package com.yahoo.elide.annotation; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * On Delete trigger annotation. - * - * The invoked function takes a RequestScope as parameter. - * @see com.yahoo.elide.security.RequestScope + * A group of repeatable LifeCycleHookBinding annotations. */ -@Target({ElementType.METHOD}) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) -public @interface OnDeletePreSecurity { - +public @interface LifeCycleHookBindings { + LifeCycleHookBinding[] value(); } diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/NonTransferable.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/NonTransferable.java new file mode 100644 index 0000000000..38c428f265 --- /dev/null +++ b/elide-annotations/src/main/java/com/yahoo/elide/annotation/NonTransferable.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.annotation; + +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Marks that the given entity cannot be added to another collection after creation of the entity. + */ +@Target({TYPE, PACKAGE}) +@Retention(RUNTIME) +@Inherited +public @interface NonTransferable { + + /** + * If NonTransferable is used at the package level, it can be disabled for individual entities by setting + * this flag to false. + * @return true if enabled. + */ + boolean enabled() default true; +} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePostCommit.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePostCommit.java deleted file mode 100644 index 390ca96e4c..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePostCommit.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Post-create hook. This annotation marks a callback that is triggered when a user performs a "create" action. - * This hook will be triggered after all security checks have been run and after the datastore - * has been committed. - * - * The invoked function takes a RequestScope as parameter. - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnCreatePostCommit { - /** - * Field name on which the annotated method is only triggered if that field is modified. - * If value is empty string, then trigger once when the object is created. - * If value is "*", then this method will be triggered once for each field that - * the user sent in the creation request. - * - * @return the field name that triggers the method - */ - String value() default ""; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePreCommit.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePreCommit.java deleted file mode 100644 index 1624b940f4..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePreCommit.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Pre-create hook. This annotation marks a callback that is triggered when a user performs a "create" action. - * This hook will be triggered after all security checks have been run, but before the datastore - * has been committed. - * - * The invoked function takes a RequestScope as parameter. - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnCreatePreCommit { - /** - * Field name on which the annotated method is only triggered if that field is modified. - * If value is empty string, then trigger once when the object is created. - * If value is "*", then this method will be triggered once for each field that - * the user sent in the creation request. - * - * @return the field name that triggers the method - */ - String value() default ""; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePreSecurity.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePreSecurity.java deleted file mode 100644 index 7bb0d7539e..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnCreatePreSecurity.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * On Create trigger annotation. - * - * The invoked function takes a RequestScope as parameter. - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnCreatePreSecurity { - /** - * Field name on which the annotated method is only triggered if that field is modified. - * If value is empty string, then trigger once when the object is created. - * If value is "*", then this method will be triggered once for each field that - * the user sent in the creation request. - * - * @return the field name that triggers the method - */ - String value() default ""; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePostCommit.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePostCommit.java deleted file mode 100644 index 32183cf29b..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePostCommit.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Post-delete hook. This annotation marks a callback that is triggered when a user performs a "delete" action. - * This hook will be triggered after all security checks have been run and after the datastore - * has been committed. - * - * The invoked function takes a RequestScope as parameter. - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnDeletePostCommit { - -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePreCommit.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePreCommit.java deleted file mode 100644 index 78abed034a..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnDeletePreCommit.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Pre-delete hook. This annotation marks a callback that is triggered when a user performs a "delete" action. - * This hook will be triggered after all security checks have been run, but before the datastore - * has been committed. - * - * The invoked function takes a RequestScope as parameter. - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnDeletePreCommit { - -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPostCommit.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPostCommit.java deleted file mode 100644 index 2d2dad821a..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPostCommit.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Post-read hook. This annotation marks a callback that is triggered when a user performs a "read" action. - * This hook will be triggered after all security checks have been run and after the datastore - * has been committed. - *

- * The invoked function takes a RequestScope as parameter. - * - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnReadPostCommit { - /** - * Field name on which the annotated method is only triggered if that field is read. - * If value is empty string, then trigger once when the object is read. - * If value is "*", then trigger for all field reads. - * - * @return the field name that triggers the method - */ - String value() default ""; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPreCommit.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPreCommit.java deleted file mode 100644 index 1607ad300a..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPreCommit.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Pre-read hook. This annotation marks a callback that is triggered when a user performs a "read" action. - * This hook will be triggered after all security checks have been run, but before the datastore - * has been committed. - *

- * The invoked function takes a RequestScope as parameter. - * - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnReadPreCommit { - /** - * Field name on which the annotated method is only triggered if that field is read. - * If value is empty string, then trigger once when the object is read. - * If value is "*", then trigger for all field reads. - * - * @return the field name that triggers the method - */ - String value() default ""; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPreSecurity.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPreSecurity.java deleted file mode 100644 index 26508c3d92..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnReadPreSecurity.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * On read trigger annotation. - *

- * The invoked function takes a RequestScope as parameter. - * - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnReadPreSecurity { - /** - * Field name on which the annotated method is only triggered if that field is read. - * If value is empty string, then trigger once when the object is read. - * If value is "*", then trigger for all field reads. - * - * @return the field name that triggers this method - */ - String value() default ""; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePostCommit.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePostCommit.java deleted file mode 100644 index 9ae6b91022..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePostCommit.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Post-update hook. This annotation marks a callback that is triggered when a user performs a "update" action. - * This hook will be triggered after all security checks have been run and after the datastore - * has been committed. - *

- * The invoked function takes a RequestScope as parameter. - * - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnUpdatePostCommit { - /** - * Field name on which the annotated method is only triggered if that field is modified. - * If value is empty string, then trigger once when the object is updated. - * If value is "*", then trigger for all field modifications. - * - * @return the field name that triggers this method - */ - String value() default ""; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePreCommit.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePreCommit.java deleted file mode 100644 index 624b1344d2..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePreCommit.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Pre-update hook. This annotation marks a callback that is triggered when a user performs a "update" action. - * This hook will be triggered after all security checks have been run, but before the datastore - * has been committed. - *

- * The invoked function takes a RequestScope as parameter. - * - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnUpdatePreCommit { - /** - * Field name on which the annotated method is only triggered if that field is modified. - * If value is empty string, then trigger once when the object is updated. - * If value is "*", then trigger for all field modifications. - * - * @return the field name that triggers the method - */ - String value() default ""; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePreSecurity.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePreSecurity.java deleted file mode 100644 index b6efe8c72c..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/OnUpdatePreSecurity.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * On Update trigger annotation. - *

- * The invoked function takes a RequestScope as parameter. - * - * @see com.yahoo.elide.security.RequestScope - */ -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface OnUpdatePreSecurity { - /** - * Field name on which the annotated method is only triggered if that field is modified. - * If value is empty string, then trigger once when the object is updated. - * If value is "*", then trigger for all field modifications. - * - * @return the field name that triggers the method - */ - String value() default ""; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/annotation/SharePermission.java b/elide-annotations/src/main/java/com/yahoo/elide/annotation/SharePermission.java deleted file mode 100644 index f53f697a6a..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/annotation/SharePermission.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.annotation; - -import static java.lang.annotation.ElementType.PACKAGE; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * A permission that is checked whenever an object is loaded without the context of a lineage and assigned - * to a relationship or collection. If SharePermission is specified, checking SharePermission falls back to checking - * ReadPermission. Otherwise, the entity is not shareable. - */ -@Target({TYPE, PACKAGE}) -@Retention(RUNTIME) -@Inherited -public @interface SharePermission { - - /** - * A boolean value indicating if the entity is shareable. If not specifying, shareable is true. Setting shareable to - * false provide a way to override package level SharePermission. - * - * @return the boolean if entity is shareable - */ - boolean sharable() default true; -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/functions/LifeCycleHook.java b/elide-annotations/src/main/java/com/yahoo/elide/functions/LifeCycleHook.java index c0a6339df5..0cf569d4f0 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/functions/LifeCycleHook.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/functions/LifeCycleHook.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.functions; +import com.yahoo.elide.annotation.LifeCycleHookBinding; import com.yahoo.elide.security.ChangeSpec; import com.yahoo.elide.security.RequestScope; @@ -18,11 +19,13 @@ public interface LifeCycleHook { /** * Run for a lifecycle event. + * @param operation CREATE, READ, UPDATE, or DELETE * @param elideEntity The entity that triggered the event * @param requestScope The request scope * @param changes Optionally, the changes that were made to the entity */ - public abstract void execute(T elideEntity, + public abstract void execute(LifeCycleHookBinding.Operation operation, + T elideEntity, RequestScope requestScope, Optional changes); } diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/PersistentResource.java b/elide-annotations/src/main/java/com/yahoo/elide/security/PersistentResource.java index 557e24c673..b05663f07f 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/PersistentResource.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/security/PersistentResource.java @@ -22,12 +22,4 @@ public interface PersistentResource { T getObject(); Class getResourceClass(); RequestScope getRequestScope(); - - /** - * Returns whether or not this resource was created in this transaction. - * @return True if this resource is newly created. - */ - default boolean isNewlyCreated() { - return getRequestScope().getNewResources().contains(this); - } } diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/RequestScope.java b/elide-annotations/src/main/java/com/yahoo/elide/security/RequestScope.java index 19e925694f..08c6356c84 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/RequestScope.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/security/RequestScope.java @@ -1,16 +1,14 @@ /* - * Copyright 2016, Yahoo Inc. + * Copyright 2020, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ package com.yahoo.elide.security; -import java.util.Set; - /** * The request scope interface passed to checks. */ public interface RequestScope { User getUser(); - Set getNewResources(); + String getApiVersion(); } diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/User.java b/elide-annotations/src/main/java/com/yahoo/elide/security/User.java index a1e95414fc..3328225b11 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/User.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/security/User.java @@ -1,19 +1,28 @@ /* - * Copyright 2016, Yahoo Inc. + * Copyright 2020, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ package com.yahoo.elide.security; import lombok.Getter; +import java.security.Principal; /** * Wrapper for opaque user passed in every request. */ public class User { - @Getter private final Object opaqueUser; + @Getter private final Principal principal; - public User(Object opaqueUser) { - this.opaqueUser = opaqueUser; + public User(Principal principal) { + this.principal = principal; + } + + public String getName() { + return principal.getName(); + } + + public boolean isInRole(String role) { + return false; } } diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/Check.java b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/Check.java index 6a18e17cb4..d2fd64d6c7 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/Check.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/Check.java @@ -4,39 +4,9 @@ * See LICENSE file in project root for terms. */ package com.yahoo.elide.security.checks; - -import com.yahoo.elide.security.ChangeSpec; -import com.yahoo.elide.security.RequestScope; -import com.yahoo.elide.security.User; - -import java.util.Optional; - /** * Custom security access that verifies whether a user belongs to a role. * Permissions are assigned as a set of checks that grant access to the permission. - * @param Type of record for Check */ -public interface Check { - - /** - * Determines whether the user can access the resource. - * - * @param object Fully modified object - * @param requestScope Request scope object - * @param changeSpec Summary of modifications - * @return true if security check passed - */ - boolean ok(T object, RequestScope requestScope, Optional changeSpec); - - /** - * Method reserved for user checks. - * - * @param user User to check - * @return True if user check passes, false otherwise - */ - boolean ok(User user); - - default String checkIdentifier() { - return this.getClass().getName(); - } +public interface Check { } diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/CommitCheck.java b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/CommitCheck.java deleted file mode 100644 index e62d3909aa..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/CommitCheck.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.security.checks; - -import com.yahoo.elide.security.User; - -/** - * Commit check interface. - * @see Check - * - * Commit checks are run immediately before a transaction is about to commit but after all changes have been made. - * Objects passed to this check are guaranteed to be in their final state. - * - * @param Type parameter - */ -public abstract class CommitCheck implements Check { - /* NOTE: Operation checks and user checks are intended to be _distinct_ */ - @Override - public final boolean ok(User user) { - throw new UnsupportedOperationException(); - } -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/InlineCheck.java b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/InlineCheck.java deleted file mode 100644 index 8f9c38cb3a..0000000000 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/InlineCheck.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2016, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.security.checks; - -/** - * Intermediate check representing the hierarchical structure of checks. - * For instance, Read/Delete permissions can take any type of InlineCheck - * while Create/Update permissions can be of any Check type. - * - * @param type parameter - */ -public abstract class InlineCheck implements Check { -} diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/OperationCheck.java b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/OperationCheck.java index d02472239b..8b72185f13 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/OperationCheck.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/OperationCheck.java @@ -5,7 +5,10 @@ */ package com.yahoo.elide.security.checks; -import com.yahoo.elide.security.User; +import com.yahoo.elide.security.ChangeSpec; +import com.yahoo.elide.security.RequestScope; + +import java.util.Optional; /** * Operation check interface. @@ -18,10 +21,14 @@ * * @param Type parameter */ -public abstract class OperationCheck extends InlineCheck { - /* NOTE: Operation checks and user checks are intended to be _distinct_ */ - @Override - public final boolean ok(User user) { - throw new UnsupportedOperationException(); - } +public abstract class OperationCheck implements Check { + /** + * Determines whether the user can access the resource. + * + * @param object Fully modified object + * @param requestScope Request scope object + * @param changeSpec Summary of modifications + * @return true if security check passed + */ + public abstract boolean ok(T object, RequestScope requestScope, Optional changeSpec); } diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/UserCheck.java b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/UserCheck.java index f44a953060..41e62c68bc 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/UserCheck.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/UserCheck.java @@ -5,19 +5,18 @@ */ package com.yahoo.elide.security.checks; -import com.yahoo.elide.security.ChangeSpec; -import com.yahoo.elide.security.RequestScope; - -import java.util.Optional; +import com.yahoo.elide.security.User; /** * Custom security access that verifies whether a user belongs to a role. * Permissions are assigned as a set of checks that grant access to the permission. */ -public abstract class UserCheck extends InlineCheck { - /* NOTE: Operation checks and user checks are intended to be _distinct_ */ - @Override - public final boolean ok(Object object, RequestScope requestScope, Optional changeSpec) { - return ok(requestScope.getUser()); - } +public abstract class UserCheck implements Check { + /** + * Method reserved for user checks. + * + * @param user User to check + * @return True if user check passes, false otherwise + */ + public abstract boolean ok(User user); } diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Collections.java b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Collections.java index e91bfc77a9..7a21a3668b 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Collections.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Collections.java @@ -8,7 +8,7 @@ import com.yahoo.elide.security.ChangeSpec; import com.yahoo.elide.security.RequestScope; -import com.yahoo.elide.security.checks.CommitCheck; +import com.yahoo.elide.security.checks.OperationCheck; import java.util.Collection; import java.util.Optional; @@ -27,7 +27,7 @@ private Collections() { * * @param type collection to be validated */ - public static class AppendOnly extends CommitCheck { + public static class AppendOnly extends OperationCheck { @Override public boolean ok(T record, RequestScope requestScope, Optional changeSpec) { @@ -47,7 +47,7 @@ public boolean ok(T record, RequestScope requestScope, Optional chan * * @param type parameter */ - public static class RemoveOnly extends CommitCheck { + public static class RemoveOnly extends OperationCheck { @Override public boolean ok(T record, RequestScope requestScope, Optional changeSpec) { diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Common.java b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Common.java index cd6600983c..7b863fecec 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Common.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Common.java @@ -7,9 +7,7 @@ package com.yahoo.elide.security.checks.prefab; import com.yahoo.elide.security.ChangeSpec; -import com.yahoo.elide.security.PersistentResource; import com.yahoo.elide.security.RequestScope; -import com.yahoo.elide.security.checks.CommitCheck; import com.yahoo.elide.security.checks.OperationCheck; import java.util.Optional; @@ -18,31 +16,13 @@ * Checks that are generally applicable. */ public class Common { - /** - * A check that enables users to update objects or fields during a create operation. This check allows - * users to be able to set values during object creation which are normally unmodifiable. - * - * @param the type of object that this check guards - */ - public static class UpdateOnCreate extends OperationCheck { - @Override - public boolean ok(T record, RequestScope requestScope, Optional changeSpec) { - for (PersistentResource resource : requestScope.getNewResources()) { - if (record == resource.getObject()) { - return true; - } - } - return false; - } - } - /** * A generic check which denies any mutation that sets a field value to anything other than null. * The check is handy in case where we want to prevent the sharing of the child entity with a different parent * but at the same time allows the removal of the child from the relationship with the existing parent * @param the type of object that this check guards */ - public static class FieldSetToNull extends CommitCheck { + public static class FieldSetToNull extends OperationCheck { @Override public boolean ok(T record, RequestScope requestScope, Optional changeSpec) { return changeSpec.map((c) -> { return c.getModified() == null; }) diff --git a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Role.java b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Role.java index b9387dd671..5ba7aaff77 100644 --- a/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Role.java +++ b/elide-annotations/src/main/java/com/yahoo/elide/security/checks/prefab/Role.java @@ -31,4 +31,19 @@ public boolean ok(User user) { return false; } } + + /** + * Check which verifies if the user is a member of a particular role. + */ + public static class RoleMemberCheck extends UserCheck { + private String role; + + public RoleMemberCheck(String role) { + this.role = role; + } + @Override + public boolean ok(User user) { + return user.isInRole(role); + } + } } diff --git a/elide-async/pom.xml b/elide-async/pom.xml new file mode 100644 index 0000000000..e80e4a7eda --- /dev/null +++ b/elide-async/pom.xml @@ -0,0 +1,136 @@ + + + + 4.0.0 + elide-async + jar + Elide Async + Elide Async + https://github.com/yahoo/elide + + com.yahoo.elide + elide-parent-pom + 5.0.0-pr9-SNAPSHOT + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + Yahoo! Inc. + http://www.yahoo.com + + + + + Yahoo Inc. + https://github.com/yahoo + + + + + scm:git:ssh://git@github.com/yahoo/elide.git + https://github.com/yahoo/elide.git + HEAD + + + + 5.5.2 + + + + + com.yahoo.elide + elide-graphql + 5.0.0-pr9-SNAPSHOT + + + + javax.persistence + javax.persistence-api + 2.2 + provided + + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + com.h2database + h2 + test + + + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + 1 + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.codehaus.gmaven + gmaven-plugin + + + + generateStubs + compile + generateTestStubs + testCompile + + + + + + + diff --git a/elide-async/src/main/java/com/yahoo/elide/async/hooks/ExecuteQueryHook.java b/elide-async/src/main/java/com/yahoo/elide/async/hooks/ExecuteQueryHook.java new file mode 100644 index 0000000000..e191e5ef9a --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/hooks/ExecuteQueryHook.java @@ -0,0 +1,30 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.hooks; + +import com.yahoo.elide.annotation.LifeCycleHookBinding; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.async.service.AsyncExecutorService; +import com.yahoo.elide.functions.LifeCycleHook; +import com.yahoo.elide.security.ChangeSpec; +import com.yahoo.elide.security.RequestScope; + +import java.util.Optional; + +public class ExecuteQueryHook implements LifeCycleHook { + + private AsyncExecutorService asyncExecutorService; + + public ExecuteQueryHook (AsyncExecutorService asyncExecutorService) { + this.asyncExecutorService = asyncExecutorService; + } + + @Override + public void execute(LifeCycleHookBinding.Operation operation, AsyncQuery query, + RequestScope requestScope, Optional changes) { + asyncExecutorService.executeQuery(query, requestScope.getUser(), requestScope.getApiVersion()); + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/hooks/UpdatePrincipalNameHook.java b/elide-async/src/main/java/com/yahoo/elide/async/hooks/UpdatePrincipalNameHook.java new file mode 100644 index 0000000000..efdcb4b632 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/hooks/UpdatePrincipalNameHook.java @@ -0,0 +1,27 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.hooks; + +import com.yahoo.elide.annotation.LifeCycleHookBinding; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.functions.LifeCycleHook; +import com.yahoo.elide.security.ChangeSpec; +import com.yahoo.elide.security.RequestScope; + +import java.security.Principal; +import java.util.Optional; + +public class UpdatePrincipalNameHook implements LifeCycleHook { + + @Override + public void execute(LifeCycleHookBinding.Operation operation, AsyncQuery query, + RequestScope requestScope, Optional changes) { + Principal principal = requestScope.getUser().getPrincipal(); + if (principal != null) { + query.setPrincipalName(principal.getName()); + } + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/models/AsyncBase.java b/elide-async/src/main/java/com/yahoo/elide/async/models/AsyncBase.java new file mode 100644 index 0000000000..b58e05fdc3 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/models/AsyncBase.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.models; + +import com.yahoo.elide.annotation.Exclude; + +import lombok.Getter; + +import java.util.Date; +import java.util.UUID; + +import javax.persistence.MappedSuperclass; +import javax.persistence.PrePersist; +import javax.persistence.PreUpdate; + +@MappedSuperclass +public abstract class AsyncBase { + + @Getter private Date createdOn; + + @Getter private Date updatedOn; + + @Exclude + protected String naturalKey = UUID.randomUUID().toString(); + + @PrePersist + public void prePersist() { + createdOn = updatedOn = new Date(); + } + + @PreUpdate + public void preUpdate() { + this.updatedOn = new Date(); + } + + @Override + public int hashCode() { + return naturalKey.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof AsyncBase) || this.getClass() != obj.getClass()) { + return false; + } + + return ((AsyncBase) obj).naturalKey.equals(naturalKey); + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/models/AsyncQuery.java b/elide-async/src/main/java/com/yahoo/elide/async/models/AsyncQuery.java new file mode 100644 index 0000000000..3f112f9410 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/models/AsyncQuery.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.models; + +import com.yahoo.elide.annotation.DeletePermission; +import com.yahoo.elide.annotation.Exclude; +import com.yahoo.elide.annotation.Include; +import com.yahoo.elide.annotation.ReadPermission; +import com.yahoo.elide.annotation.UpdatePermission; + +import lombok.Data; + +import java.util.UUID; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToOne; +import javax.persistence.PrePersist; + +/** + * Model for Async Query. + * ExecuteQueryHook and UpdatePrincipalNameHook is binded manually during the elide startup, + * after asyncexecutorservice is initialized. + */ +@Entity +@Include(type = "asyncQuery", rootLevel = true) +@ReadPermission(expression = "Principal is Owner") +@UpdatePermission(expression = "Prefab.Role.None") +@DeletePermission(expression = "Prefab.Role.None") +@Data +public class AsyncQuery extends AsyncBase implements PrincipalOwned { + @Id + @Column(columnDefinition = "varchar(36)") + private UUID id; //Provided. + + private String query; //JSON-API PATH or GraphQL payload. + + private QueryType queryType; //GRAPHQL, JSONAPI + + @UpdatePermission(expression = "Principal is Owner AND value is Cancelled") + private QueryStatus status; + + @OneToOne(mappedBy = "query", cascade = CascadeType.REMOVE) + private AsyncQueryResult result; + + @Exclude + private String principalName; + + @PrePersist + public void prePersistStatus() { + status = QueryStatus.QUEUED; + } + + @Override + public String getPrincipalName() { + return principalName; + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/models/AsyncQueryResult.java b/elide-async/src/main/java/com/yahoo/elide/async/models/AsyncQueryResult.java new file mode 100644 index 0000000000..0b1c7252cc --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/models/AsyncQueryResult.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.models; + +import com.yahoo.elide.annotation.CreatePermission; +import com.yahoo.elide.annotation.DeletePermission; +import com.yahoo.elide.annotation.Exclude; +import com.yahoo.elide.annotation.Include; +import com.yahoo.elide.annotation.ReadPermission; +import com.yahoo.elide.annotation.UpdatePermission; + +import lombok.Data; + +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToOne; + +/** + * Model for Async Query Result. + */ +@Entity +@Include(type = "asyncQueryResult") +@ReadPermission(expression = "Principal is Owner") +@UpdatePermission(expression = "Prefab.Role.None") +@CreatePermission(expression = "Prefab.Role.None") +@DeletePermission(expression = "Prefab.Role.None") +@Data +public class AsyncQueryResult extends AsyncBase implements PrincipalOwned { + @Id + @Column(columnDefinition = "varchar(36)") + private UUID id; //Matches UUID in query. + + private Integer contentLength; + + private String responseBody; //success or errors + + private Integer status; // HTTP Status + + @OneToOne + private AsyncQuery query; + + @Exclude + public String getPrincipalName() { + return query.getPrincipalName(); + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/models/PrincipalOwned.java b/elide-async/src/main/java/com/yahoo/elide/async/models/PrincipalOwned.java new file mode 100644 index 0000000000..792c56efa6 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/models/PrincipalOwned.java @@ -0,0 +1,13 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.models; + +/** + * Get principal owner name interface. + */ +public interface PrincipalOwned { + public String getPrincipalName(); +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/models/QueryStatus.java b/elide-async/src/main/java/com/yahoo/elide/async/models/QueryStatus.java new file mode 100644 index 0000000000..39e4cf64f8 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/models/QueryStatus.java @@ -0,0 +1,18 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.models; + +/** + * ENUM of possible query statuses. + */ +public enum QueryStatus { + COMPLETE, + QUEUED, + PROCESSING, + CANCELLED, + TIMEDOUT, + FAILURE +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/models/QueryType.java b/elide-async/src/main/java/com/yahoo/elide/async/models/QueryType.java new file mode 100644 index 0000000000..d1b5dcd598 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/models/QueryType.java @@ -0,0 +1,14 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.models; + +/** + * ENUM of supported query types. + */ +public enum QueryType { + GRAPHQL_V1_0, + JSONAPI_V1_0 +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/models/security/AsyncQueryOperationChecks.java b/elide-async/src/main/java/com/yahoo/elide/async/models/security/AsyncQueryOperationChecks.java new file mode 100644 index 0000000000..24477afaf3 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/models/security/AsyncQueryOperationChecks.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.models.security; + +import com.yahoo.elide.annotation.SecurityCheck; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.async.models.PrincipalOwned; +import com.yahoo.elide.async.models.QueryStatus; +import com.yahoo.elide.security.ChangeSpec; +import com.yahoo.elide.security.RequestScope; +import com.yahoo.elide.security.checks.OperationCheck; + +import java.security.Principal; +import java.util.Optional; + +/** + * Operation Checks on the Async Query and Result objects. + */ +public class AsyncQueryOperationChecks { + @SecurityCheck(AsyncQueryOwner.PRINCIPAL_IS_OWNER) + public static class AsyncQueryOwner extends OperationCheck { + + public static final String PRINCIPAL_IS_OWNER = "Principal is Owner"; + + @Override + public boolean ok(Object object, RequestScope requestScope, Optional changeSpec) { + Principal principal = requestScope.getUser().getPrincipal(); + boolean status = false; + String principalName = ((PrincipalOwned) object).getPrincipalName(); + if (principalName == null && (principal == null || principal.getName() == null)) { + status = true; + } else { + status = principalName.equals(principal.getName()); + } + return status; + } + } + + @SecurityCheck(AsyncQueryStatusValue.VALUE_IS_CANCELLED) + public static class AsyncQueryStatusValue extends OperationCheck { + + public static final String VALUE_IS_CANCELLED = "value is Cancelled"; + + @Override + public boolean ok(AsyncQuery object, RequestScope requestScope, Optional changeSpec) { + return changeSpec.get().getModified().toString().equals(QueryStatus.CANCELLED.name()); + } + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncCleanerService.java b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncCleanerService.java new file mode 100644 index 0000000000..f29efa9df8 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncCleanerService.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import com.yahoo.elide.Elide; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Random; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +/** + * Service to execute Async queries. + * It will schedule task to track long running queries and kills them. + * It will also schedule task to update orphan query statuses + * after host/app crash or restart. + */ +@Slf4j +public class AsyncCleanerService { + + private final int defaultCleanupDelayMinutes = 360; + private final int maxCleanupInitialDelayMinutes = 100; + + private static AsyncCleanerService asyncCleanerService = null; + + @Inject + private AsyncCleanerService(Elide elide, Integer maxRunTimeMinutes, Integer queryCleanupDays, + AsyncQueryDAO asyncQueryDao) { + + //If query is still running for twice than maxRunTime, then interrupt did not work due to host/app crash. + int queryRunTimeThresholdMinutes = maxRunTimeMinutes * 2; + + // Setting up query cleaner that marks long running query as TIMEDOUT. + ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor(); + AsyncQueryCleanerThread cleanUpTask = new AsyncQueryCleanerThread(queryRunTimeThresholdMinutes, elide, + queryCleanupDays, asyncQueryDao); + + // Since there will be multiple hosts running the elide service, + // setting up random delays to avoid all of them trying to cleanup at the same time. + Random random = new Random(); + int initialDelayMinutes = random.ints(0, maxCleanupInitialDelayMinutes).limit(1).findFirst().getAsInt(); + log.debug("Initial Delay for cleaner service is {}", initialDelayMinutes); + + //Having a delay of at least DEFAULT_CLEANUP_DELAY between two cleanup attempts. + //Or maxRunTimeMinutes * 2 so that this process does not coincides with query + //interrupt process. + cleaner.scheduleWithFixedDelay(cleanUpTask, initialDelayMinutes, Math.max(defaultCleanupDelayMinutes, + queryRunTimeThresholdMinutes), TimeUnit.MINUTES); + } + + /** + * Initialize the singleton AsyncCleanerService object. + * If already initialized earlier, no new object is created. + * @param elide Elide Instance + * @param maxRunTimeMinutes max run times in minutes + * @param queryCleanupDays Async Query Clean up days + * @param asyncQueryDao DAO Object + */ + public static void init(Elide elide, Integer maxRunTimeMinutes, Integer queryCleanupDays, + AsyncQueryDAO asyncQueryDao) { + if (asyncCleanerService == null) { + asyncCleanerService = new AsyncCleanerService(elide, maxRunTimeMinutes, queryCleanupDays, asyncQueryDao); + } else { + log.debug("asyncCleanerService is already initialized."); + } + } + + /** + * Get instance of AsyncCleanerService. + * @return AsyncCleanerService Object + */ + public synchronized static AsyncCleanerService getInstance() { + return asyncCleanerService; + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncExecutorService.java b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncExecutorService.java new file mode 100644 index 0000000000..a4a0f56f53 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncExecutorService.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import com.yahoo.elide.Elide; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.core.exceptions.InvalidOperationException; +import com.yahoo.elide.graphql.QueryRunner; +import com.yahoo.elide.security.User; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.inject.Inject; + +/** + * Service to execute Async queries. + * It will schedule task to track long running queries and kills them. + * It will also schedule task to update orphan query statuses after + * host/app crash or restart. + */ +@Getter +@Slf4j +public class AsyncExecutorService { + + private final int defaultThreadpoolSize = 6; + + private Elide elide; + private Map runners; + private ExecutorService executor; + private ExecutorService interruptor; + private int maxRunTime; + private AsyncQueryDAO asyncQueryDao; + private static AsyncExecutorService asyncExecutorService = null; + + @Inject + private AsyncExecutorService(Elide elide, Integer threadPoolSize, Integer maxRunTime, AsyncQueryDAO asyncQueryDao) { + this.elide = elide; + runners = new HashMap(); + + for (String apiVersion : elide.getElideSettings().getDictionary().getApiVersions()) { + runners.put(apiVersion, new QueryRunner(elide, apiVersion)); + } + + this.maxRunTime = maxRunTime; + executor = Executors.newFixedThreadPool(threadPoolSize == null ? defaultThreadpoolSize : threadPoolSize); + interruptor = Executors.newFixedThreadPool(threadPoolSize == null ? defaultThreadpoolSize : threadPoolSize); + this.asyncQueryDao = asyncQueryDao; + } + + /** + * Initialize the singleton AsyncExecutorService object. + * If already initialized earlier, no new object is created. + * @param elide Elide Instance + * @param threadPoolSize thred pool size + * @param maxRunTime max run times in minutes + * @param asyncQueryDao DAO Object + */ + public static void init(Elide elide, Integer threadPoolSize, Integer maxRunTime, AsyncQueryDAO asyncQueryDao) { + if (asyncExecutorService == null) { + asyncExecutorService = new AsyncExecutorService(elide, threadPoolSize, maxRunTime, asyncQueryDao); + } else { + log.debug("asyncExecutorService is already initialized."); + } + } + + /** + * Get instance of AsyncExecutorService. + * @return AsyncExecutorService Object + */ + public synchronized static AsyncExecutorService getInstance() { + return asyncExecutorService; + } + + /** + * Execute Query asynchronously. + * @param queryObj Query Object + * @param user User + */ + public void executeQuery(AsyncQuery queryObj, User user, String apiVersion) { + QueryRunner runner = runners.get(apiVersion); + if (runner == null) { + throw new InvalidOperationException("Invalid API Version"); + } + + AsyncQueryThread queryWorker = new AsyncQueryThread(queryObj, user, elide, runner, asyncQueryDao, apiVersion); + + AsyncQueryInterruptThread queryInterruptWorker = new AsyncQueryInterruptThread(elide, + executor.submit(queryWorker), queryObj, new Date(), maxRunTime, asyncQueryDao); + interruptor.execute(queryInterruptWorker); + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryCleanerThread.java b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryCleanerThread.java new file mode 100644 index 0000000000..fb24bb4872 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryCleanerThread.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import com.yahoo.elide.Elide; +import com.yahoo.elide.async.models.QueryStatus; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +/** + * Runnable thread for updating AsyncQueryThread status. + * beyond the max run time and if not terminated by interrupt process + * due to app/host crash or restart. + */ +@Slf4j +@Data +@AllArgsConstructor +public class AsyncQueryCleanerThread implements Runnable { + + private int maxRunTimeMinutes; + private Elide elide; + private int queryCleanupDays; + private AsyncQueryDAO asyncQueryDao; + + @Override + public void run() { + deleteAsyncQuery(); + timeoutAsyncQuery(); + } + + /** + * This method deletes the historical queries based on threshold. + * */ + @SuppressWarnings("unchecked") + protected void deleteAsyncQuery() { + + String cleanupDateFormatted = evaluateFormattedFilterDate(Calendar.DATE, queryCleanupDays); + + String filterExpression = "createdOn=le='" + cleanupDateFormatted + "'"; + + asyncQueryDao.deleteAsyncQueryAndResultCollection(filterExpression); + + } + + /** + * This method updates the status of long running async query which + * were interrupted due to host crash/app shutdown to TIMEDOUT. + * */ + @SuppressWarnings("unchecked") + protected void timeoutAsyncQuery() { + + String filterDateFormatted = evaluateFormattedFilterDate(Calendar.MINUTE, maxRunTimeMinutes); + String filterExpression = "status=in=(" + QueryStatus.PROCESSING.toString() + "," + + QueryStatus.QUEUED.toString() + ");createdOn=le='" + filterDateFormatted + "'"; + + asyncQueryDao.updateStatusAsyncQueryCollection(filterExpression, QueryStatus.TIMEDOUT); + } + + /** + * Evaluates and subtracts the amount based on the calendar unit and amount from current date. + * @param calendarUnit Enum such as Calendar.DATE or Calendar.MINUTE + * @param amount Amount of days to be subtracted from current time + * @return formatted filter date + */ + private String evaluateFormattedFilterDate(int calendarUnit, int amount) { + Calendar cal = Calendar.getInstance(); + cal.setTime(new Date()); + cal.add(calendarUnit, -(amount)); + Date filterDate = cal.getTime(); + Format dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); + String filterDateFormatted = dateFormat.format(filterDate); + log.debug("FilterDateFormatted = {}", filterDateFormatted); + return filterDateFormatted; + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryDAO.java b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryDAO.java new file mode 100644 index 0000000000..3dd4c0cc66 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryDAO.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.async.models.AsyncQueryResult; +import com.yahoo.elide.async.models.QueryStatus; + +import java.util.Collection; +import java.util.UUID; + +/** + * Utility interface which uses the elide datastore to modify and create AsyncQuery and AsyncQueryResult Objects. + */ +public interface AsyncQueryDAO { + + /** + * This method updates the QueryStatus for AsyncQuery for given QueryStatus. + * @param asyncQuery The AsyncQuery Object to be updated + * @param status Status from Enum QueryStatus + * @return AsyncQuery Updated AsyncQuery Object + */ + public AsyncQuery updateStatus(AsyncQuery asyncQuery, QueryStatus status); + + /** + * This method persists the model for AsyncQueryResult, AsyncQuery object and establishes the relationship. + * @param status ElideResponse status from AsyncQuery + * @param responseBody ElideResponse responseBody from AsyncQuery + * @param asyncQuery AsyncQuery object to be associated with the AsyncQueryResult object + * @param asyncQueryId UUID of the AsyncQuery to be associated with the AsyncQueryResult object + * @return AsyncQueryResult Object + */ + public AsyncQueryResult createAsyncQueryResult(Integer status, String responseBody, AsyncQuery asyncQuery, + UUID asyncQueryId); + + /** + * This method deletes a collection of AsyncQuery and its associated AsyncQueryResult objects from database and + * returns the objects deleted. + * @param filterExpression filter expression to delete AsyncQuery Objects based on + * @return query object list deleted + */ + public Collection deleteAsyncQueryAndResultCollection(String filterExpression); + + /** + * This method updates the status for a collection of AsyncQuery objects from database and + * returns the objects updated. + * @param filterExpression filter expression to update AsyncQuery Objects based on + * @param status status to be updated + * @return query object list updated + */ + public Collection updateStatusAsyncQueryCollection(String filterExpression, + QueryStatus status); + +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryInterruptThread.java b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryInterruptThread.java new file mode 100644 index 0000000000..59b7e3571b --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryInterruptThread.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import com.yahoo.elide.Elide; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.async.models.QueryStatus; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Runnable thread for terminating AsyncQueryThread executing + * beyond the max run time and update status. + */ +@Slf4j +@Data +@AllArgsConstructor +public class AsyncQueryInterruptThread implements Runnable { + + private Elide elide; + private Future task; + private AsyncQuery asyncQuery; + private Date submittedOn; + private int maxRunTimeMinutes; + private AsyncQueryDAO asyncQueryDao; + + @Override + public void run() { + interruptQuery(); + } + + /** + * This is the main method which interrupts the Async Query request, if it has executed beyond + * the maximum run time. + */ + protected void interruptQuery() { + try { + long interruptTimeMillies = calculateTimeOut(maxRunTimeMinutes, submittedOn); + + if (interruptTimeMillies > 0) { + log.debug("Waiting on the future with the given timeout for {}", interruptTimeMillies); + task.get(interruptTimeMillies, TimeUnit.MILLISECONDS); + } + } catch (InterruptedException e) { + // Incase the future.get is interrupted , the underlying query may still have succeeded + log.error("InterruptedException: {}", e); + } catch (ExecutionException e) { + // Query Status set to failure will be handled by the processQuery method + log.error("ExecutionException: {}", e); + } catch (TimeoutException e) { + log.error("TimeoutException: {}", e); + task.cancel(true); + asyncQueryDao.updateStatus(asyncQuery, QueryStatus.TIMEDOUT); + } + } + + /** + * Method to calculate the time left to interrupt since submission of thread in Milliseconds. + * @param interruptTimeMinutes max duration to run the query + * @param submittedOn time when query was submitted + * @return Interrupt time left + */ + private long calculateTimeOut(long maxRunTimeMinutes, Date submittedOn) { + long maxRunTimeMinutesMillies = maxRunTimeMinutes * 60 * 1000; + long interruptTimeMillies = maxRunTimeMinutesMillies - ((new Date()).getTime() - submittedOn.getTime()); + + return interruptTimeMillies; + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryThread.java b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryThread.java new file mode 100644 index 0000000000..c06cce503c --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/service/AsyncQueryThread.java @@ -0,0 +1,129 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import com.yahoo.elide.Elide; +import com.yahoo.elide.ElideResponse; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.async.models.QueryStatus; +import com.yahoo.elide.async.models.QueryType; +import com.yahoo.elide.graphql.QueryRunner; +import com.yahoo.elide.security.User; + +import org.apache.http.NameValuePair; +import org.apache.http.NoHttpResponseException; +import org.apache.http.client.utils.URIBuilder; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.net.URISyntaxException; + +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; + +/** + * Runnable thread for executing the query provided in Async Query. + * It will also update the query status and result object at different + * stages of execution. + */ +@Slf4j +@Data +@AllArgsConstructor +public class AsyncQueryThread implements Runnable { + + private AsyncQuery queryObj; + private User user; + private Elide elide; + private final QueryRunner runner; + private AsyncQueryDAO asyncQueryDao; + private String apiVersion; + + @Override + public void run() { + processQuery(); + } + + /** + * This is the main method which processes the Async Query request, executes the query and updates + * values for AsyncQuery and AsyncQueryResult models accordingly. + */ + protected void processQuery() { + try { + // Change async query to processing + asyncQueryDao.updateStatus(queryObj, QueryStatus.PROCESSING); + ElideResponse response = null; + log.debug("AsyncQuery Object from request: {}", queryObj); + if (queryObj.getQueryType().equals(QueryType.JSONAPI_V1_0)) { + MultivaluedMap queryParams = getQueryParams(queryObj.getQuery()); + log.debug("Extracted QueryParams from AsyncQuery Object: {}", queryParams); + response = elide.get(getPath(queryObj.getQuery()), queryParams, user, apiVersion); + log.debug("JSONAPI_V1_0 getResponseCode: {}, JSONAPI_V1_0 getBody: {}", + response.getResponseCode(), response.getBody()); + } + else if (queryObj.getQueryType().equals(QueryType.GRAPHQL_V1_0)) { + response = runner.run(queryObj.getQuery(), user); + log.debug("GRAPHQL_V1_0 getResponseCode: {}, GRAPHQL_V1_0 getBody: {}", + response.getResponseCode(), response.getBody()); + } + if (response == null) { + throw new NoHttpResponseException("Response for request returned as null"); + } + + // Create AsyncQueryResult entry for AsyncQuery and + // add queryResult object to query object + asyncQueryDao.createAsyncQueryResult(response.getResponseCode(), response.getBody(), + queryObj, queryObj.getId()); + + // If we receive a response update Query Status to complete + asyncQueryDao.updateStatus(queryObj, QueryStatus.COMPLETE); + + } catch (Exception e) { + log.error("Exception: {}", e); + if (e.getClass().equals(InterruptedException.class)) { + // An InterruptedException is encountered when we interrupt the query when it goes beyond max run time + // We set the QueryStatus to TIMEDOUT + // No AsyncQueryResult will be set for this case + asyncQueryDao.updateStatus(queryObj, QueryStatus.TIMEDOUT); + } else { + // If an Exception is encountered we set the QueryStatus to FAILURE + // No AsyncQueryResult will be set for this case + asyncQueryDao.updateStatus(queryObj, QueryStatus.FAILURE); + } + } + } + + /** + * This method parses the url and gets the query params. + * And adds them into a MultivaluedMap to be used by underlying Elide.get method + * @param query query from the Async request + * @throws URISyntaxException URISyntaxException from malformed or incorrect URI + * @return MultivaluedMap with query parameters + */ + protected MultivaluedMap getQueryParams(String query) throws URISyntaxException { + URIBuilder uri; + uri = new URIBuilder(query); + MultivaluedMap queryParams = new MultivaluedHashMap(); + for (NameValuePair queryParam : uri.getQueryParams()) { + queryParams.add(queryParam.getName(), queryParam.getValue()); + } + return queryParams; + } + + /** + * This method parses the url and gets the query params. + * And retrieves path to be used by underlying Elide.get method + * @param query query from the Async request + * @throws URISyntaxException URISyntaxException from malformed or incorrect URI + * @return Path extracted from URI + */ + protected String getPath(String query) throws URISyntaxException { + URIBuilder uri; + uri = new URIBuilder(query); + return uri.getPath(); + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/service/DefaultAsyncQueryDAO.java b/elide-async/src/main/java/com/yahoo/elide/async/service/DefaultAsyncQueryDAO.java new file mode 100644 index 0000000000..1802b87dcb --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/service/DefaultAsyncQueryDAO.java @@ -0,0 +1,210 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; + +import com.yahoo.elide.Elide; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.async.models.AsyncQueryResult; +import com.yahoo.elide.async.models.QueryStatus; +import com.yahoo.elide.core.DataStore; +import com.yahoo.elide.core.DataStoreTransaction; +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.filter.dialect.ParseException; +import com.yahoo.elide.core.filter.dialect.RSQLFilterDialect; +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.jsonapi.models.JsonApiDocument; +import com.yahoo.elide.request.EntityProjection; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.Collection; +import java.util.Iterator; +import java.util.UUID; + +import javax.inject.Singleton; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; + +/** + * Utility class which implements AsyncQueryDAO. + */ +@Singleton +@Slf4j +@Getter +public class DefaultAsyncQueryDAO implements AsyncQueryDAO { + + @Setter private Elide elide; + @Setter private DataStore dataStore; + private EntityDictionary dictionary; + private RSQLFilterDialect filterParser; + + // Default constructor is needed for standalone implementation for override in getAsyncQueryDao + public DefaultAsyncQueryDAO() { + } + + public DefaultAsyncQueryDAO(Elide elide, DataStore dataStore) { + this.elide = elide; + this.dataStore = dataStore; + dictionary = elide.getElideSettings().getDictionary(); + filterParser = new RSQLFilterDialect(dictionary); + } + + @Override + public AsyncQuery updateStatus(AsyncQuery asyncQuery, QueryStatus status) { + return updateAsyncQuery(asyncQuery, (asyncQueryObj) -> { + asyncQueryObj.setStatus(status); + }); + } + + /** + * This method updates the model for AsyncQuery with passed value. + * @param asyncQuery The AsyncQuery Object which will be updated + * @param updateFunction Functional interface for updating AsyncQuery Object + * @return AsyncQuery Object + */ + private AsyncQuery updateAsyncQuery(AsyncQuery asyncQuery, UpdateQuery updateFunction) { + log.debug("updateAsyncQuery"); + AsyncQuery queryObj = (AsyncQuery) executeInTransaction(dataStore, (tx, scope) -> { + updateFunction.update(asyncQuery); + tx.save(asyncQuery, scope); + return asyncQuery; + }); + return queryObj; + } + + @Override + public Collection updateStatusAsyncQueryCollection(String filterExpression, + QueryStatus status) { + return updateAsyncQueryCollection(filterExpression, (asyncQuery) -> { + asyncQuery.setStatus(status); + }); + } + + /** + * This method updates a collection of AsyncQuery objects from database and + * returns the objects updated. + * @param filterExpression Filter expression to update AsyncQuery Objects based on + * @param updateFunction Functional interface for updating AsyncQuery Object + * @return query object list updated + */ + @SuppressWarnings("unchecked") + private Collection updateAsyncQueryCollection(String filterExpression, + UpdateQuery updateFunction) { + log.debug("updateAsyncQueryCollection"); + + Collection asyncQueryList = null; + + try { + FilterExpression filter = filterParser.parseFilterExpression(filterExpression, + AsyncQuery.class, false); + asyncQueryList = (Collection) executeInTransaction(dataStore, + (tx, scope) -> { + EntityProjection asyncQueryCollection = EntityProjection.builder() + .type(AsyncQuery.class) + .filterExpression(filter) + .build(); + + Iterable loaded = tx.loadObjects(asyncQueryCollection, scope); + Iterator itr = loaded.iterator(); + + while (itr.hasNext()) { + AsyncQuery query = (AsyncQuery) itr.next(); + updateFunction.update(query); + tx.save(query, scope); + } + return loaded; + }); + } catch (ParseException e) { + log.error("Exception: {}", e); + } + return asyncQueryList; + } + + @Override + @SuppressWarnings("unchecked") + public Collection deleteAsyncQueryAndResultCollection(String filterExpression) { + log.debug("deleteAsyncQueryAndResultCollection"); + + Collection asyncQueryList = null; + + try { + FilterExpression filter = filterParser.parseFilterExpression(filterExpression, + AsyncQuery.class, false); + asyncQueryList = (Collection) executeInTransaction(dataStore, (tx, scope) -> { + + EntityProjection asyncQueryCollection = EntityProjection.builder() + .type(AsyncQuery.class) + .filterExpression(filter) + .build(); + + Iterable loaded = tx.loadObjects(asyncQueryCollection, scope); + Iterator itr = loaded.iterator(); + + while (itr.hasNext()) { + AsyncQuery query = (AsyncQuery) itr.next(); + if (query != null) { + tx.delete(query, scope); + } + } + return loaded; + }); + } catch (ParseException e) { + log.error("Exception: {}", e); + } + return asyncQueryList; + } + + @Override + public AsyncQueryResult createAsyncQueryResult(Integer status, String responseBody, + AsyncQuery asyncQuery, UUID asyncQueryId) { + log.debug("createAsyncQueryResult"); + AsyncQueryResult queryResultObj = (AsyncQueryResult) executeInTransaction(dataStore, (tx, scope) -> { + AsyncQueryResult asyncQueryResult = new AsyncQueryResult(); + asyncQueryResult.setStatus(status); + asyncQueryResult.setResponseBody(responseBody); + asyncQueryResult.setContentLength(responseBody.length()); + asyncQueryResult.setQuery(asyncQuery); + asyncQueryResult.setId(asyncQueryId); + asyncQuery.setResult(asyncQueryResult); + tx.createObject(asyncQueryResult, scope); + tx.save(asyncQuery, scope); + return asyncQueryResult; + }); + return queryResultObj; + } + + /** + * This method creates a transaction from the datastore, performs the DB action using + * a generic functional interface and closes the transaction. + * @param dataStore Elide datastore retrieved from Elide object + * @param action Functional interface to perform DB action + * @return Object Returns Entity Object (AsyncQueryResult or AsyncResult) + */ + protected Object executeInTransaction(DataStore dataStore, Transactional action) { + log.debug("executeInTransaction"); + Object result = null; + try (DataStoreTransaction tx = dataStore.beginTransaction()) { + JsonApiDocument jsonApiDoc = new JsonApiDocument(); + MultivaluedMap queryParams = new MultivaluedHashMap(); + RequestScope scope = new RequestScope("query", NO_VERSION, jsonApiDoc, + tx, null, queryParams, elide.getElideSettings()); + result = action.execute(tx, scope); + tx.flush(scope); + tx.commit(scope); + } catch (IOException e) { + log.error("IOException: {}", e); + } catch (Exception e) { + log.error("Exception: {}", e); + } + return result; + } +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/service/Transactional.java b/elide-async/src/main/java/com/yahoo/elide/async/service/Transactional.java new file mode 100644 index 0000000000..8360316a70 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/service/Transactional.java @@ -0,0 +1,17 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import com.yahoo.elide.core.DataStoreTransaction; +import com.yahoo.elide.core.RequestScope; + +/** + * Function which will be invoked for executing elide async transactions. + */ +@FunctionalInterface +public interface Transactional { + public Object execute(DataStoreTransaction tx, RequestScope scope); +} diff --git a/elide-async/src/main/java/com/yahoo/elide/async/service/UpdateQuery.java b/elide-async/src/main/java/com/yahoo/elide/async/service/UpdateQuery.java new file mode 100644 index 0000000000..9e9b68e421 --- /dev/null +++ b/elide-async/src/main/java/com/yahoo/elide/async/service/UpdateQuery.java @@ -0,0 +1,16 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import com.yahoo.elide.async.models.AsyncQuery; + +/** + * Function which will be invoked for updating elide async query. + */ +@FunctionalInterface +public interface UpdateQuery { + public void update(AsyncQuery query); +} diff --git a/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncCleanerServiceTest.java b/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncCleanerServiceTest.java new file mode 100644 index 0000000000..d10b3f07fd --- /dev/null +++ b/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncCleanerServiceTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; + +import com.yahoo.elide.Elide; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AsyncCleanerServiceTest { + + private AsyncCleanerService service; + + @BeforeAll + public void setupMocks() { + Elide elide = mock(Elide.class); + + AsyncQueryDAO dao = mock(DefaultAsyncQueryDAO.class); + AsyncCleanerService.init(elide, 5, 60, dao); + service = AsyncCleanerService.getInstance(); + } + + @Test + public void testCleanerSet() { + assertNotNull(service); + } +} diff --git a/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncExecutorServiceTest.java b/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncExecutorServiceTest.java new file mode 100644 index 0000000000..38d690e83a --- /dev/null +++ b/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncExecutorServiceTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.yahoo.elide.Elide; +import com.yahoo.elide.ElideSettingsBuilder; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.async.models.QueryStatus; +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore; +import com.yahoo.elide.security.User; +import com.yahoo.elide.security.checks.Check; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.util.HashMap; +import java.util.Map; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class AsyncExecutorServiceTest { + + private AsyncExecutorService service; + private Elide elide; + private AsyncQueryDAO asyncQueryDao; + + @BeforeAll + public void setupMocks() { + HashMapDataStore inMemoryStore = new HashMapDataStore(AsyncQuery.class.getPackage()); + Map> checkMappings = new HashMap<>(); + + elide = new Elide( + new ElideSettingsBuilder(inMemoryStore) + .withEntityDictionary(new EntityDictionary(checkMappings)) + .build()); + + asyncQueryDao = mock(DefaultAsyncQueryDAO.class); + + AsyncExecutorService.init(elide, 5, 60, asyncQueryDao); + + service = AsyncExecutorService.getInstance(); + } + + @Test + public void testAsyncExecutorServiceSet() { + assertEquals(elide, service.getElide()); + assertNotNull(service.getRunners()); + assertEquals(60, service.getMaxRunTime()); + assertNotNull(service.getExecutor()); + assertNotNull(service.getInterruptor()); + assertEquals(asyncQueryDao, service.getAsyncQueryDao()); + } + + @Test + public void testExecuteQuery() { + AsyncQuery queryObj = mock(AsyncQuery.class); + User testUser = mock(User.class); + + service.executeQuery(queryObj, testUser, NO_VERSION); + + verify(asyncQueryDao, times(0)).updateStatus(queryObj, QueryStatus.QUEUED); + } +} diff --git a/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncQueryCleanerThreadTest.java b/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncQueryCleanerThreadTest.java new file mode 100644 index 0000000000..a9e1b987c4 --- /dev/null +++ b/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncQueryCleanerThreadTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.yahoo.elide.Elide; +import com.yahoo.elide.async.models.QueryStatus; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class AsyncQueryCleanerThreadTest { + + private AsyncQueryCleanerThread cleanerThread; + private Elide elide; + private AsyncQueryDAO asyncQueryDao; + + @BeforeEach + public void setupMocks() { + elide = mock(Elide.class); + asyncQueryDao = mock(DefaultAsyncQueryDAO.class); + cleanerThread = new AsyncQueryCleanerThread(7, elide, 7, asyncQueryDao); + } + + @Test + public void testAsyncQueryCleanerThreadSet() { + assertEquals(elide, cleanerThread.getElide()); + assertEquals(asyncQueryDao, cleanerThread.getAsyncQueryDao()); + assertEquals(7, cleanerThread.getMaxRunTimeMinutes()); + assertEquals(7, cleanerThread.getQueryCleanupDays()); + } + + @Test + public void testDeleteAsyncQuery() { + cleanerThread.deleteAsyncQuery(); + + verify(asyncQueryDao, times(1)).deleteAsyncQueryAndResultCollection(anyString()); + } + + @Test + public void timeoutAsyncQuery() { + cleanerThread.timeoutAsyncQuery(); + + verify(asyncQueryDao, times(1)).updateStatusAsyncQueryCollection(anyString(), any(QueryStatus.class)); + } +} diff --git a/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncQueryThreadTest.java b/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncQueryThreadTest.java new file mode 100644 index 0000000000..609009b07c --- /dev/null +++ b/elide-async/src/test/java/com/yahoo/elide/async/service/AsyncQueryThreadTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.yahoo.elide.Elide; +import com.yahoo.elide.ElideResponse; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.async.models.QueryStatus; +import com.yahoo.elide.async.models.QueryType; +import com.yahoo.elide.graphql.QueryRunner; +import com.yahoo.elide.security.User; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class AsyncQueryThreadTest { + + private AsyncQueryThread queryThread; + private User user; + private Elide elide; + private QueryRunner runner; + private AsyncQuery queryObj; + private AsyncQueryDAO asyncQueryDao; + + @BeforeEach + public void setupMocks() { + user = mock(User.class); + elide = mock(Elide.class); + runner = mock(QueryRunner.class); + queryObj = mock(AsyncQuery.class); + asyncQueryDao = mock(DefaultAsyncQueryDAO.class); + queryThread = new AsyncQueryThread(queryObj, user, elide, runner, asyncQueryDao, "v1"); + } + + @Test + public void testAsyncQueryCleanerThreadSet() { + assertEquals(queryObj, queryThread.getQueryObj()); + assertEquals(user, queryThread.getUser()); + assertEquals(elide, queryThread.getElide()); + assertEquals(runner, queryThread.getRunner()); + assertEquals(asyncQueryDao, queryThread.getAsyncQueryDao()); + } + + @Test + public void testProcessQueryJsonApi() { + String query = "/group?sort=commonName&fields%5Bgroup%5D=commonName,description"; + ElideResponse response = mock(ElideResponse.class); + + when(queryObj.getQuery()).thenReturn(query); + when(queryObj.getQueryType()).thenReturn(QueryType.JSONAPI_V1_0); + when(elide.get(anyString(), any(), any(), anyString())).thenReturn(response); + when(response.getResponseCode()).thenReturn(200); + when(response.getBody()).thenReturn("ResponseBody"); + + queryThread.processQuery(); + + verify(asyncQueryDao, times(1)).updateStatus(queryObj, QueryStatus.PROCESSING); + verify(asyncQueryDao, times(1)).updateStatus(queryObj, QueryStatus.COMPLETE); + verify(asyncQueryDao, times(1)).createAsyncQueryResult(anyInt(), anyString(), any(), any()); + } + + @Test + public void testProcessQueryGraphQl() { + String query = "{\"query\":\"{ group { edges { node { name commonName description } } } }\",\"variables\":null}"; + ElideResponse response = mock(ElideResponse.class); + + when(queryObj.getQuery()).thenReturn(query); + when(queryObj.getQueryType()).thenReturn(QueryType.GRAPHQL_V1_0); + when(runner.run(query, user)).thenReturn(response); + when(response.getResponseCode()).thenReturn(200); + when(response.getBody()).thenReturn("ResponseBody"); + + queryThread.processQuery(); + + verify(asyncQueryDao, times(1)).updateStatus(queryObj, QueryStatus.PROCESSING); + verify(asyncQueryDao, times(1)).updateStatus(queryObj, QueryStatus.COMPLETE); + verify(asyncQueryDao, times(1)).createAsyncQueryResult(anyInt(), anyString(), any(), any()); + } + + @Test + public void testProcessQueryException() { + String query = "{\"query\":\"{ group { edges { node { name commonName description } } } }\",\"variables\":null}"; + + when(queryObj.getQuery()).thenReturn(query); + when(queryObj.getQueryType()).thenReturn(QueryType.GRAPHQL_V1_0); + when(runner.run(query, user)).thenThrow(RuntimeException.class); + + queryThread.processQuery(); + + verify(asyncQueryDao, times(1)).updateStatus(queryObj, QueryStatus.PROCESSING); + verify(asyncQueryDao, times(1)).updateStatus(queryObj, QueryStatus.FAILURE); + } +} diff --git a/elide-async/src/test/java/com/yahoo/elide/async/service/DefaultAsyncQueryDAOTest.java b/elide-async/src/test/java/com/yahoo/elide/async/service/DefaultAsyncQueryDAOTest.java new file mode 100644 index 0000000000..137f16af3d --- /dev/null +++ b/elide-async/src/test/java/com/yahoo/elide/async/service/DefaultAsyncQueryDAOTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.async.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.yahoo.elide.Elide; +import com.yahoo.elide.ElideSettings; +import com.yahoo.elide.ElideSettingsBuilder; +import com.yahoo.elide.async.models.AsyncQuery; +import com.yahoo.elide.async.models.AsyncQueryResult; +import com.yahoo.elide.async.models.QueryStatus; +import com.yahoo.elide.core.DataStore; +import com.yahoo.elide.core.DataStoreTransaction; +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.filter.dialect.RSQLFilterDialect; +import com.yahoo.elide.security.checks.Check; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; + +public class DefaultAsyncQueryDAOTest { + + private DefaultAsyncQueryDAO asyncQueryDAO; + private Elide elide; + private DataStore dataStore; + private AsyncQuery asyncQuery; + private DataStoreTransaction tx; + private EntityDictionary dictionary; + + @BeforeEach + public void setupMocks() { + dataStore = mock(DataStore.class); + asyncQuery = mock(AsyncQuery.class); + tx = mock(DataStoreTransaction.class); + + Map> checkMappings = new HashMap<>(); + + dictionary = new EntityDictionary(checkMappings); + dictionary.bindEntity(AsyncQuery.class); + dictionary.bindEntity(AsyncQueryResult.class); + + ElideSettings elideSettings = new ElideSettingsBuilder(dataStore) + .withEntityDictionary(dictionary) + .withJoinFilterDialect(new RSQLFilterDialect(dictionary)) + .withSubqueryFilterDialect(new RSQLFilterDialect(dictionary)) + .withISO8601Dates("yyyy-MM-dd'T'HH:mm'Z'", TimeZone.getTimeZone("UTC")) + .build(); + + elide = new Elide(elideSettings); + + when(dataStore.beginTransaction()).thenReturn(tx); + + asyncQueryDAO = new DefaultAsyncQueryDAO(elide, dataStore); + } + + @Test + public void testAsyncQueryCleanerThreadSet() { + assertEquals(elide, asyncQueryDAO.getElide()); + assertEquals(dictionary, asyncQueryDAO.getDictionary()); + } + + @Test + public void testUpdateStatus() { + AsyncQuery result = asyncQueryDAO.updateStatus(asyncQuery, QueryStatus.PROCESSING); + + assertEquals(result, asyncQuery); + verify(tx, times(1)).save(any(AsyncQuery.class), any(RequestScope.class)); + verify(asyncQuery, times(1)).setStatus(QueryStatus.PROCESSING); + } + + @Test + public void testUpdateStatusAsyncQueryCollection() { + Iterable loaded = Arrays.asList(asyncQuery, asyncQuery); + when(tx.loadObjects(any(), any())).thenReturn(loaded); + + asyncQueryDAO.updateStatusAsyncQueryCollection("status=in=(PROCESSING,QUEUED);createdOn=le='2020-04-22T13:28Z'", QueryStatus.TIMEDOUT); + + verify(tx, times(2)).save(any(AsyncQuery.class), any(RequestScope.class)); + verify(asyncQuery, times(2)).setStatus(QueryStatus.TIMEDOUT); + } + + @Test + public void testDeleteAsyncQueryAndResultCollection() { + Iterable loaded = Arrays.asList(asyncQuery, asyncQuery, asyncQuery); + when(tx.loadObjects(any(), any())).thenReturn(loaded); + + asyncQueryDAO.deleteAsyncQueryAndResultCollection("createdOn=le='2020-03-23T02:02Z'"); + + verify(dataStore, times(1)).beginTransaction(); + verify(tx, times(1)).loadObjects(any(), any()); + verify(tx, times(3)).delete(any(AsyncQuery.class), any(RequestScope.class)); + } + + @Test + public void testCreateAsyncQueryResult() { + Integer status = 200; + String responseBody = "responseBody"; + UUID uuid = UUID.fromString("ba31ca4e-ed8f-4be0-a0f3-12088fa9263e"); + AsyncQueryResult result = asyncQueryDAO.createAsyncQueryResult(status, "responseBody", asyncQuery, uuid); + + assertEquals(status, result.getStatus()); + assertEquals(responseBody, result.getResponseBody()); + assertEquals(asyncQuery, result.getQuery()); + assertEquals(uuid, result.getId()); + verify(tx, times(1)).createObject(any(), any(RequestScope.class)); + verify(tx, times(1)).save(any(), any(RequestScope.class)); + + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/pom.xml b/elide-contrib/elide-dynamic-config-helpers/pom.xml new file mode 100644 index 0000000000..dc720e496b --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/pom.xml @@ -0,0 +1,165 @@ + + + + 4.0.0 + elide-dynamic-config-helpers + jar + Elide Dynamic Config Helpers + Dynamic config helpers + https://github.com/yahoo/elide + + elide-contrib-parent-pom + com.yahoo.elide + 5.0.0-pr9-SNAPSHOT + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Yahoo Inc. + https://github.com/yahoo + + + + + scm:git:ssh://git@github.com/yahoo/elide.git + https://github.com/yahoo/elide.git + HEAD + + + + 3.0.0 + 2.10.2 + 4.2.0 + 2.2.12 + 1.3.0 + 1.0 + 2.6 + + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + + + org.apache.commons + commons-lang3 + + + commons-io + commons-io + ${commons-io.version} + + + org.slf4j + slf4j-api + + + org.projectlombok + lombok + + + com.github.java-json-tools + json-schema-validator + ${json-schema-validator.version} + test + + + org.hjson + hjson + ${hjson.version} + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + com.github.jknack + handlebars + ${handlebars.version} + + + org.junit.platform + junit-platform-launcher + test + + + org.mdkt.compiler + InMemoryJavaCompiler + ${mdkt.compiler.version} + + + com.google.collections + google-collections + ${google.collections.version} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + maven-assembly-plugin + + + package + + single + + + + + + jar-with-dependencies + + + + + + diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigHelpers.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigHelpers.java new file mode 100644 index 0000000000..33bb35fe28 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigHelpers.java @@ -0,0 +1,169 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers; + +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideSecurityConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideTableConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.Table; +import com.yahoo.elide.contrib.dynamicconfighelpers.parser.handlebars.HandlebarsHydrator; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.commons.io.FileUtils; +import org.hjson.JsonValue; + +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@Slf4j +/** + * Util class for Dynamic config helper module. + */ +public class DynamicConfigHelpers { + + private static final String TABLE_CONFIG_PATH = "tables" + File.separator; + private static final String SECURITY_CONFIG_PATH = "security.hjson"; + private static final String VARIABLE_CONFIG_PATH = "variables.hjson"; + private static final String NEW_LINE = "\n"; + + /** + * Checks whether input is null or empty. + * @param input : input string + * @return true or false + */ + public static boolean isNullOrEmpty(String input) { + return (input == null || input.trim().length() == 0); + } + + /** + * format config file path. + * @param basePath : path to hjson config. + * @return formatted file path. + */ + public static String formatFilePath(String basePath) { + String path = basePath; + if (!path.endsWith(File.separator)) { + path += File.separator; + } + return path; + } + + /** + * converts variable.hjson to map of variables. + * @param basePath : root path to model dir + * @return Map of variables + * @throws JsonProcessingException + */ + @SuppressWarnings("unchecked") + public static Map getVariablesPojo(String basePath) throws JsonProcessingException { + String filePath = basePath + VARIABLE_CONFIG_PATH; + File variableFile = new File(filePath); + if (variableFile.exists()) { + String jsonConfig = hjsonToJson(readConfigFile(variableFile)); + return getModelPojo(jsonConfig, Map.class); + } else { + log.info("Variables config file not found at " + filePath); + return null; + } + } + + /** + * converts all available table config to ElideTableConfig Pojo. + * @param basePath : root path to model dir + * @param variables : variables to resolve. + * @return ElideTableConfig pojo + * @throws IOException + */ + public static ElideTableConfig getElideTablePojo(String basePath, Map variables) + throws IOException { + return getElideTablePojo(basePath, variables, TABLE_CONFIG_PATH); + } + + /** + * converts all available table config to ElideTableConfig Pojo. + * @param basePath : root path to model dir + * @param variables : variables to resolve. + * @param tableDirName : dir name for table configs + * @return ElideTableConfig pojo + * @throws IOException + */ + public static ElideTableConfig getElideTablePojo(String basePath, Map variables, + String tableDirName) throws IOException { + Collection tableConfigs = FileUtils.listFiles(new File(basePath + tableDirName), + new String[] {"hjson"}, false); + Set tables = new HashSet<>(); + for (File tableConfig : tableConfigs) { + String jsonConfig = hjsonToJson(resolveVariables(readConfigFile(tableConfig), variables)); + ElideTableConfig table = getModelPojo(jsonConfig, ElideTableConfig.class); + tables.addAll(table.getTables()); + } + ElideTableConfig elideTableConfig = new ElideTableConfig(); + elideTableConfig.setTables(tables); + return elideTableConfig; + } + + /** + * converts security.hjson to ElideSecurityConfig Pojo. + * @param basePath : root path to model dir. + * @param variables : variables to resolve. + * @return ElideSecurityConfig Pojo + * @throws IOException + */ + public static ElideSecurityConfig getElideSecurityPojo(String basePath, Map variables) + throws IOException { + String filePath = basePath + SECURITY_CONFIG_PATH; + File securityFile = new File(filePath); + if (securityFile.exists()) { + String jsonConfig = hjsonToJson(resolveVariables(readConfigFile(securityFile), variables)); + return getModelPojo(jsonConfig, ElideSecurityConfig.class); + } else { + log.info("Security config file not found at " + filePath); + return null; + } + } + + /** + * resolves variables in table and security config. + * @param jsonConfig of table or security + * @param variables map from config + * @return json string with resolved variables + * @throws IOException + */ + public static String resolveVariables(String jsonConfig, Map variables) throws IOException { + HandlebarsHydrator hydrator = new HandlebarsHydrator(); + return hydrator.hydrateConfigTemplate(jsonConfig, variables); + } + + private static String hjsonToJson(String hjson) { + return JsonValue.readHjson(hjson).toString(); + } + + private static T getModelPojo(String jsonConfig, final Class configPojo) throws JsonProcessingException { + return new ObjectMapper().readValue(jsonConfig, configPojo); + } + + private static String readConfigFile(File configFile) { + StringBuffer sb = new StringBuffer(); + try { + for (String line : FileUtils.readLines(configFile, StandardCharsets.UTF_8)) { + sb.append(line); + sb.append(NEW_LINE); + } + } catch (IOException e) { + log.error("error while reading config file " + configFile.getName()); + log.error(e.getMessage()); + } + return sb.toString(); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/compile/ElideDynamicEntityCompiler.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/compile/ElideDynamicEntityCompiler.java new file mode 100644 index 0000000000..5973b3baea --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/compile/ElideDynamicEntityCompiler.java @@ -0,0 +1,150 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.compile; + +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideSecurityConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideTableConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.parser.ElideConfigParser; +import com.yahoo.elide.contrib.dynamicconfighelpers.parser.handlebars.HandlebarsHydrator; + +import com.google.common.collect.Sets; + +import org.mdkt.compiler.InMemoryJavaCompiler; + +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Compiles dynamic model pojos generated from hjson files. + * + */ +@Slf4j +public class ElideDynamicEntityCompiler { + + public static ArrayList classNames = new ArrayList(); + + public static final String PACKAGE_NAME = "dynamicconfig.models."; + private Map> compiledObjects; + + private InMemoryJavaCompiler compiler = InMemoryJavaCompiler.newInstance().ignoreWarnings(); + + private Map tableClasses = new HashMap(); + private Map securityClasses = new HashMap(); + + /** + * Parse dynamic config path. + * @param path : Dynamic config hjsons root location + * @throws Exception Exception thrown + */ + public ElideDynamicEntityCompiler(String path) throws Exception { + + ElideTableConfig tableConfig = new ElideTableConfig(); + ElideSecurityConfig securityConfig = new ElideSecurityConfig(); + ElideConfigParser elideConfigParser = new ElideConfigParser(path); + HandlebarsHydrator hydrator = new HandlebarsHydrator(); + + tableConfig = elideConfigParser.getElideTableConfig(); + securityConfig = elideConfigParser.getElideSecurityConfig(); + tableClasses = hydrator.hydrateTableTemplate(tableConfig); + securityClasses = hydrator.hydrateSecurityTemplate(securityConfig); + + for (Entry entry : tableClasses.entrySet()) { + classNames.add(PACKAGE_NAME + entry.getKey()); + } + + for (Entry entry : securityClasses.entrySet()) { + classNames.add(PACKAGE_NAME + entry.getKey()); + } + + compiler.useParentClassLoader( + new ElideDynamicInMemoryClassLoader(ClassLoader.getSystemClassLoader(), + Sets.newHashSet(classNames))); + compile(); + } + + /** + * Compile table and security model pojos. + * @throws Exception + */ + private void compile() throws Exception { + + for (Map.Entry tablePojo : tableClasses.entrySet()) { + log.debug("key: " + tablePojo.getKey() + ", value: " + tablePojo.getValue()); + compiler.addSource(PACKAGE_NAME + tablePojo.getKey(), tablePojo.getValue()); + } + + for (Map.Entry secPojo : securityClasses.entrySet()) { + log.debug("key: " + secPojo.getKey() + ", value: " + secPojo.getValue()); + compiler.addSource(PACKAGE_NAME + secPojo.getKey(), secPojo.getValue()); + } + + compiledObjects = compiler.compileAll(); + } + + /** + * Get Inmemorycompiler's classloader. + * @return ClassLoader + */ + public ClassLoader getClassLoader() { + return compiler.getClassloader(); + } + + /** + * Get the class from compiled class lists. + * @param name name of the class + * @return Class + */ + public Class getCompiled(String name) { + return compiledObjects.get(name); + } + + /** + * Find classes with a particular annotation from dynamic compiler. + * @param annotationClass Annotation to search for. + * @return Set of Classes matching the annotation. + * @throws ClassNotFoundException + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public Set> findAnnotatedClasses(Class annotationClass) + throws ClassNotFoundException { + + Set> annotatedClasses = new HashSet>(); + ArrayList dynamicClasses = classNames; + + for (String dynamicClass : dynamicClasses) { + Class classz = compiledObjects.get(dynamicClass); + if (classz.getAnnotation(annotationClass) != null) { + annotatedClasses.add(classz); + } + } + + return annotatedClasses; + } + + /** + * Find classes with a particular annotation from dynamic compiler. + * @param annotationClass Annotation to search for. + * @return Set of Classes matching the annotation. + * @throws ClassNotFoundException + */ + @SuppressWarnings({ "rawtypes" }) + public List findAnnotatedClassNames(Class annotationClass) + throws ClassNotFoundException { + + return findAnnotatedClasses(annotationClass) + .stream() + .map(Class::getName) + .collect(Collectors.toList()); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/compile/ElideDynamicInMemoryClassLoader.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/compile/ElideDynamicInMemoryClassLoader.java new file mode 100644 index 0000000000..1165f6a6ca --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/compile/ElideDynamicInMemoryClassLoader.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.compile; + +import com.google.common.collect.Sets; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Set; + +/** + * ClassLoader for dynamic configuration. + */ +@Slf4j +@Data +@AllArgsConstructor +public class ElideDynamicInMemoryClassLoader extends ClassLoader { + + private Set classNames = Sets.newHashSet(); + + public ElideDynamicInMemoryClassLoader(ClassLoader parent, Set classNames) { + super(parent); + setClassNames(classNames); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + return super.findClass(name); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + return super.loadClass(name); + } + + @Override + protected URL findResource(String name) { + log.debug("Finding Resource " + name + " in " + classNames); + if (classNames.contains(name.replace("/", ".").replace(".class", ""))) { + try { + log.debug("Returning Resource " + "file://" + name); + return new URL("file://" + name); + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + } + return super.findResource(name); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Dimension.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Dimension.java new file mode 100644 index 0000000000..320f24d8cf --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Dimension.java @@ -0,0 +1,82 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Dimensions represent labels for measures. + * Dimensions are used to filter and group measures. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "name", + "description", + "category", + "hidden", + "readAccess", + "definition", + "type", + "grains", + "tags" +}) +@Data +@EqualsAndHashCode() +@AllArgsConstructor +@NoArgsConstructor +public class Dimension { + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + @JsonProperty("category") + private String category; + + + @JsonProperty("hidden") + private Boolean hidden = false; + + @JsonProperty("readAccess") + private String readAccess = "Prefab.Role.All"; + + @JsonProperty("definition") + private String definition = ""; + + @JsonProperty("type") + private Type type = Type.TEXT; + + @JsonProperty("grains") + private List grains = new ArrayList(); + + @JsonProperty("tags") + @JsonDeserialize(as = LinkedHashSet.class) + private Set tags = new LinkedHashSet(); + + /** + * Returns description of the dimension. + * If null, returns the name. + * @return description + */ + public String getDescription() { + return (this.description == null ? getName() : this.description); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/ElideSecurityConfig.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/ElideSecurityConfig.java new file mode 100644 index 0000000000..9ef99e6797 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/ElideSecurityConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Elide Security POJO. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "roles", + "rules" +}) +@Data +@EqualsAndHashCode() +@AllArgsConstructor +@NoArgsConstructor +public class ElideSecurityConfig { + + @JsonProperty("roles") + @JsonDeserialize(as = LinkedHashSet.class) + private Set roles = new LinkedHashSet(); + + @JsonProperty("rules") + @JsonDeserialize(as = LinkedHashSet.class) + private Set rules = new LinkedHashSet(); +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/ElideTableConfig.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/ElideTableConfig.java new file mode 100644 index 0000000000..ed85357d2e --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/ElideTableConfig.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Elide Table POJO. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "tables" +}) +@Data +@EqualsAndHashCode() +@AllArgsConstructor +@NoArgsConstructor +public class ElideTableConfig { + + @JsonProperty("tables") + @JsonDeserialize(as = LinkedHashSet.class) + private Set
tables = new LinkedHashSet
(); +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Grains.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Grains.java new file mode 100644 index 0000000000..dc2e9b801e --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Grains.java @@ -0,0 +1,79 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +/** + * Grains can have SQL expressions that can substitute column + * with the dimension definition expression. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "grain", + "sql" +}) +@Data +@EqualsAndHashCode() +@AllArgsConstructor +@NoArgsConstructor +public class Grains { + + + @JsonProperty("grain") + private Grains.Grain grain; + + @JsonProperty("sql") + private String sql; + + public enum Grain { + + DAY("DAY"), + WEEK("WEEK"), + MONTH("MONTH"), + YEAR("YEAR"); + private final String value; + private final static Map CONSTANTS = new HashMap(); + + static { + for (Grains.Grain c: values()) { + CONSTANTS.put(c.value, c); + } + } + + private Grain(String value) { + this.value = value; + } + + @JsonValue + @Override + public String toString() { + return this.value; + } + + @JsonCreator + public static Grains.Grain fromValue(String value) { + Grains.Grain constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Join.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Join.java new file mode 100644 index 0000000000..f5e2d2d7b5 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Join.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +/** + * Joins describe the SQL expression necessary to join two physical tables. + * Joins can be used when defining dimension columns that reference other tables. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "name", + "to", + "type", + "definition" +}) +@Data +@EqualsAndHashCode() +@AllArgsConstructor +@NoArgsConstructor +public class Join { + + + @JsonProperty("name") + private String name; + + @JsonProperty("to") + private String to; + + @JsonProperty("type") + private Join.Type type; + + @JsonProperty("definition") + private String definition; + + public enum Type { + + TO_ONE("toOne"), + TO_MANY("toMany"); + private final String value; + private final static Map CONSTANTS = new HashMap(); + + static { + for (Join.Type c: values()) { + CONSTANTS.put(c.value, c); + } + } + + private Type(String value) { + this.value = value; + } + + @JsonValue + @Override + public String toString() { + return this.value; + } + + @JsonCreator + public static Join.Type fromValue(String value) { + Join.Type constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Measure.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Measure.java new file mode 100644 index 0000000000..b95174a171 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Measure.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Measures represent metrics that can be aggregated at query time. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "name", + "description", + "category", + "hidden", + "readAccess", + "definition", + "type" +}) +@Data +@EqualsAndHashCode() +@AllArgsConstructor +@NoArgsConstructor +public class Measure { + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + @JsonProperty("category") + private String category; + + @JsonProperty("hidden") + private Boolean hidden = false; + + @JsonProperty("readAccess") + private String readAccess = "Prefab.Role.All"; + + @JsonProperty("definition") + private String definition; + + @JsonProperty("type") + private Type type = Type.INTEGER; + + /** + * Returns description of the measure. + * If null, returns the name. + * @return description + */ + public String getDescription() { + return (this.description == null ? getName() : this.description); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Rule.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Rule.java new file mode 100644 index 0000000000..7e97311532 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Rule.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * Rules are a list of RSQL filter expression templates that + * support property expansion on the principal object. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "type", + "filter", + "name" +}) +@Data +@EqualsAndHashCode() +@AllArgsConstructor +@NoArgsConstructor +public class Rule { + + @JsonProperty("type") + private Rule.Type type; + + @JsonProperty("filter") + private String filter; + + @JsonProperty("name") + private String name; + + public enum Type { + + FILTER("filter"); + private final String value; + + private Type(String value) { + this.value = value; + } + + @JsonValue + @Override + public String toString() { + return this.value; + } + } + + public enum Filter { + + FILTER("filter"); + private final String value; + + private Filter(String value) { + this.value = value; + } + + @JsonValue + @Override + public String toString() { + return this.value; + } + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Table.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Table.java new file mode 100644 index 0000000000..f610cc4ce6 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Table.java @@ -0,0 +1,137 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Table Model JSON. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "name", + "schema", + "hidden", + "description", + "cardinality", + "readAccess", + "joins", + "measures", + "dimensions", + "tags", + "extends", + "sql", + "table" +}) +@Data +@EqualsAndHashCode() +@AllArgsConstructor +@NoArgsConstructor +public class Table { + + @JsonProperty("name") + private String name; + + @JsonProperty("schema") + private String schema = ""; + + @JsonProperty("hidden") + private Boolean hidden = false; + + @JsonProperty("description") + private String description; + + @JsonProperty("cardinality") + private Table.Cardinality cardinality = Table.Cardinality.fromValue("tiny"); + + @JsonProperty("readAccess") + private String readAccess = "Prefab.Role.All"; + + @JsonProperty("joins") + private List joins = new ArrayList(); + + @JsonProperty("measures") + private List measures = new ArrayList(); + + @JsonProperty("dimensions") + private List dimensions = new ArrayList(); + + @JsonProperty("tags") + @JsonDeserialize(as = LinkedHashSet.class) + private Set tags = new LinkedHashSet(); + + @JsonProperty("extends") + private String extend = ""; + + @JsonProperty("sql") + private String sql = ""; + + @JsonProperty("table") + private String table = ""; + + /** + * Returns description of the table object. + * If null, returns the name. + * @return description + */ + public String getDescription() { + return (this.description == null ? getName() : this.description); + } + + public enum Cardinality { + + TINY("tiny"), + SMALL("small"), + MEDIUM("medium"), + LARGE("large"), + HUGE("huge"); + private final String value; + private final static Map CONSTANTS = new HashMap(); + + static { + for (Table.Cardinality c: values()) { + CONSTANTS.put(c.value, c); + } + } + + private Cardinality(String value) { + this.value = value; + } + + @JsonValue + @Override + public String toString() { + return this.value; + } + + @JsonCreator + public static Table.Cardinality fromValue(String value) { + Table.Cardinality constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } else { + return constant; + } + } + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Type.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Type.java new file mode 100644 index 0000000000..703fa50c23 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/model/Type.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.model; + +/** + * Data Type of the field. + */ +public enum Type { + + TIME("TIME"), + INTEGER("INTEGER"), + DECIMAL("DECIMAL"), + MONEY("MONEY"), + TEXT("TEXT"), + COORDINATE("COORDINATE"), + BOOLEAN("BOOLEAN"); + + private final String value; + + private Type(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/ElideConfigParser.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/ElideConfigParser.java new file mode 100644 index 0000000000..9117383518 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/ElideConfigParser.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.parser; + +import com.yahoo.elide.contrib.dynamicconfighelpers.DynamicConfigHelpers; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideSecurityConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideTableConfig; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.Map; + +@Slf4j +/** + * Parses Hjson configuration from local file path and initializes Dynamic Model POJOs + */ +@Data +public class ElideConfigParser { + + private ElideTableConfig elideTableConfig; + private ElideSecurityConfig elideSecurityConfig; + private Map variables; + + /** + * Initialize Dynamic config objects. + * @param localFilePath : Path to dynamic model config dir. + * @throws IllegalArgumentException + */ + public ElideConfigParser(String localFilePath) { + + if (DynamicConfigHelpers.isNullOrEmpty(localFilePath)) { + throw new IllegalArgumentException("Config path is null"); + } + try { + String basePath = DynamicConfigHelpers.formatFilePath(localFilePath); + + this.variables = DynamicConfigHelpers.getVariablesPojo(basePath); + this.elideTableConfig = DynamicConfigHelpers.getElideTablePojo(basePath, this.variables); + this.elideSecurityConfig = DynamicConfigHelpers.getElideSecurityPojo(basePath, this.variables); + + } catch (IOException e) { + log.error("Error while parsing dynamic config at location " + localFilePath); + log.error(e.getMessage()); + throw new IllegalStateException(e); + } + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHelper.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHelper.java new file mode 100644 index 0000000000..285ec7dabb --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHelper.java @@ -0,0 +1,125 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.parser.handlebars; + +import com.yahoo.elide.contrib.dynamicconfighelpers.model.Type; +import com.github.jknack.handlebars.Options; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Locale; +import java.util.stream.Collectors; + +/** + * Helper class for handlebar template hydration. + */ +public class HandlebarsHelper { + + private static final String EMPTY_STRING = ""; + private static final String STRING = "String"; + private static final String DATE = "Date"; + private static final String BIGDECIMAL = "BigDecimal"; + private static final String LONG = "Long"; + private static final String BOOLEAN = "Boolean"; + private static final String WHITESPACE_REGEX = "\\s+"; + + /** + * Capitalize first letter of the string. + * @param str string to capitalize first letter + * @return string with first letter capitalized + */ + public String capitalizeFirstLetter(String str) { + + return (str == null || str.length() == 0) ? str : str.substring(0, 1).toUpperCase(Locale.ENGLISH) + + str.substring(1); + } + + /** + * LowerCase first letter of the string. + * @param str string to lower case first letter + * @return string with first letter lower cased + */ + public String lowerCaseFirstLetter(String str) { + + return (str == null || str.length() == 0) ? str : str.substring(0, 1).toLowerCase(Locale.ENGLISH) + + str.substring(1); + } + + /** + * Transform string to capitalize first character of each word, change other + * characters to lower case and remove spaces. + * @param str String to be transformed + * @return Capitalize First Letter of Each Word and remove spaces + */ + public String titleCaseRemoveSpaces(String str) { + + return (str == null || str.length() == 0) ? str + : String.join(EMPTY_STRING, Arrays.asList(str.trim().split(WHITESPACE_REGEX)).stream().map( + s -> toUpperCase(s.substring(0, 1)) + toLowerCase(s.substring(1))) + .collect(Collectors.toList())); + } + + /** + * Transform string to upper case. + * @param obj Object representation of the string + * @return string converted to upper case + */ + public String toUpperCase(Object obj) { + + return (obj == null) ? EMPTY_STRING : obj.toString().toUpperCase(Locale.ENGLISH); + } + + /** + * Transform string to lower case. + * @param obj Object representation of the string + * @return string converted to lower case + */ + public String toLowerCase(Object obj) { + + return (obj == null) ? EMPTY_STRING : obj.toString().toLowerCase(Locale.ENGLISH); + } + + /** + * If type matches passed value. + * @param type Elide model type object + * @param options options object with type/string to match + * @return template if matched + * @throws IOException IOException + */ + public CharSequence ifTypeMatches(Object type, Options options) throws IOException { + + String inputType = type.toString(); + String typeToMatch = options.param(0, null); + return inputType.equals(typeToMatch) ? options.fn() : options.inverse(); + } + + /** + * Get java type name corresponding to the Elide model type. + * @param type Elide model type object + * @return The corresponding java type name + */ + public String getJavaType(Type type) { + + switch (type) { + case BOOLEAN: + return BOOLEAN; + case COORDINATE: + return STRING; + case INTEGER: + return LONG; + case TEXT: + return STRING; + case TIME: + return DATE; + case DECIMAL: + return BIGDECIMAL; + case MONEY: + return BIGDECIMAL; + default: + return STRING; + } + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHydrator.java b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHydrator.java new file mode 100644 index 0000000000..30fd5a0b23 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHydrator.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.parser.handlebars; + +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideSecurityConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideTableConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.Table; +import com.github.jknack.handlebars.Context; +import com.github.jknack.handlebars.EscapingStrategy; +import com.github.jknack.handlebars.EscapingStrategy.Hbs; +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Template; +import com.github.jknack.handlebars.helper.ConditionalHelpers; +import com.github.jknack.handlebars.io.ClassPathTemplateLoader; +import com.github.jknack.handlebars.io.TemplateLoader; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Class for handlebars hydration. + */ +public class HandlebarsHydrator { + + public static final String SECURITY_CLASS_PREFIX = "DynamicConfigOperationChecksPrincipalIs"; + public static final String HANDLEBAR_START_DELIMITER = "<%"; + public static final String HANDLEBAR_END_DELIMITER = "%>"; + public static final EscapingStrategy MY_ESCAPING_STRATEGY = new Hbs(new String[][]{ + {"<", "<" }, + {">", ">" }, + {"\"", """ }, + {"`", "`" }, + {"&", "&" } + }); + + /** + * Method to hydrate the Table template. + * @param table ElideTable object + * @return map with key as table java class name and value as table java class definition + * @throws IOException IOException + */ + public Map hydrateTableTemplate(ElideTableConfig table) throws IOException { + + Map tableClasses = new HashMap<>(); + + TemplateLoader loader = new ClassPathTemplateLoader("/templates"); + Handlebars handlebars = new Handlebars(loader).with(MY_ESCAPING_STRATEGY); + HandlebarsHelper helper = new HandlebarsHelper(); + handlebars.registerHelpers(ConditionalHelpers.class); + handlebars.registerHelpers(helper); + Template template = handlebars.compile("table", HANDLEBAR_START_DELIMITER, HANDLEBAR_END_DELIMITER); + + for (Table t : table.getTables()) { + tableClasses.put(helper.capitalizeFirstLetter(t.getName()), template.apply(t)); + } + + return tableClasses; + } + + /** + * Method to replace variables in hjson config. + * @param config hjson config string + * @param replacements Map of variable key value pairs + * @return hjson config string with variables replaced + * @throws IOException IOException + */ + public String hydrateConfigTemplate(String config, Map replacements) throws IOException { + + Context context = Context.newBuilder(replacements).build(); + Handlebars handlebars = new Handlebars(); + Template template = handlebars.compileInline(config, HANDLEBAR_START_DELIMITER, HANDLEBAR_END_DELIMITER); + + return template.apply(context); + } + + /** + * Method to hydrate the Security template. + * @param security ElideSecurity Object + * @return security java class string + * @throws IOException IOException + */ + public Map hydrateSecurityTemplate(ElideSecurityConfig security) throws IOException { + + Map securityClasses = new HashMap<>(); + + if (security == null) { + return securityClasses; + } + + TemplateLoader loader = new ClassPathTemplateLoader("/templates"); + Handlebars handlebars = new Handlebars(loader).with(MY_ESCAPING_STRATEGY); + HandlebarsHelper helper = new HandlebarsHelper(); + handlebars.registerHelpers(ConditionalHelpers.class); + handlebars.registerHelpers(helper); + Template template = handlebars.compile("security", HANDLEBAR_START_DELIMITER, HANDLEBAR_END_DELIMITER); + + for (String role : security.getRoles()) { + securityClasses.put(SECURITY_CLASS_PREFIX + helper.titleCaseRemoveSpaces(role), template.apply(role)); + } + + return securityClasses; + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideSecuritySchema.json b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideSecuritySchema.json new file mode 100644 index 0000000000..1f89dff286 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideSecuritySchema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft-06/schema#", + "$id": "https://elide.io/schemas/security_schema_v1#", + "description": "Elide Security config json/hjson schema", + "type": "object", + "properties": { + "roles": { + "title": "Security Roles", + "description": "List of Roles that will map to security checks", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "rules": { + "title": "Security Rules", + "description": "List of RSQL filter expression templates", + "type": "array", + "uniqueItems": true, + "items": { + "properties": { + "type": { + "title": "Rule Type", + "description": "Type of security rule", + "type": "string", + "enum": [ + "filter" + ] + }, + "filter": { + "title": "Rule Filter", + "description": "Rule filter expression", + "type": "string", + "enum": [ + "filter" + ] + }, + "name": { + "title": "Rule Name", + "description": "Name of the security rule", + "type": "string" + } + }, + "required": [ + "filter", + "name" + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideTableSchema.json b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideTableSchema.json new file mode 100644 index 0000000000..3342f4edc3 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideTableSchema.json @@ -0,0 +1,406 @@ +{ + "$schema": "https://json-schema.org/draft-06/schema#", + "$id": "https://elide.io/schemas/table_schema_v1#", + "description": "Elide Table config json/hjson schema", + "definitions": { + "grain": { + "title": "Grains", + "description": "Grains can have SQL expressions that can substitute column with the dimension definition expression", + "type": "object", + "properties": { + "grain": { + "title": "Time granularity", + "description": "Indicates grain time granularity", + "type": "string", + "enum": [ + "DAY", + "WEEK", + "MONTH", + "YEAR" + ] + }, + "sql": { + "title": "Grain SQL", + "description": "Grain SQL query", + "type": "string" + } + }, + "required": [ + "grain", + "sql" + ], + "additionalProperties": false + }, + "join": { + "title": "Join", + "description": "Joins describe the SQL expression necessary to join two physical tables. Joins can be used when defining dimension columns that reference other tables.", + "type": "object", + "properties": { + "name": { + "title": "Join name", + "description": "The name of the join relationship.", + "type": "string" + }, + "to": { + "title": "Join table name", + "description": "The name of the table that is being joined to", + "type": "string", + "pattern": "^[A-Za-z]([0-9A-Za-z]*_?[0-9A-Za-z]*)*$" + }, + "type": { + "title": "Type of Join", + "description": "Type of the join - toOne or toMany", + "type": "string", + "enum": [ + "toOne", + "toMany" + ] + }, + "definition": { + "title": "Join definition SQL", + "description": "Templated SQL expression that represents the ON clause of the join", + "type": "string" + } + }, + "required": [ + "name", + "to", + "type", + "definition" + ], + "additionalProperties": false + }, + "enumtype": { + "title": "Dimension field type", + "description": "The data type of the dimension field", + "type": "string", + "enum": [ + "INTEGER", + "DECIMAL", + "MONEY", + "TEXT", + "COORDINATE", + "BOOLEAN" + ] + }, + "measure": { + "title": "Measure", + "description": "Metric definitions are extensible objects that contain a type field and one or more additional attributes. Each type is tied to logic in Elide that generates a metric function.", + "type": "object", + "properties": { + "name": { + "title": "Metric name", + "description": "The name of the metric. This will be the same as the POJO field name.", + "type": "string", + "pattern": "^[A-Za-z]([0-9A-Za-z]*_?[0-9A-Za-z]*)*$" + }, + "description": { + "title": "Metric description", + "description": "A long description of the metric.", + "type": "string" + }, + "category": { + "title": "Measure group category", + "description": "Category for grouping", + "type": "string" + }, + "hidden": { + "title": "Hide/Show measure", + "description": "Whether this metric is exposed via API metadata", + "type": "boolean", + "default": false + }, + "readAccess": { + "title": "Measure read access", + "description": "Read permission for the metric.", + "type": "string", + "default": "Prefab.Role.All" + }, + "definition": { + "title": "Metric definition", + "description": "The definition of the metric", + "type": "string" + }, + "type": { + "oneOf":[ + {"$ref": "#/definitions/enumtype"}, + {"enum": [ + "TIME" + ]} + ], + "default": "INTEGER" + } + }, + "required": [ + "name", + "definition" + ], + "additionalProperties": false + }, + "dimensionRef": { + "title": "Dimension", + "description": "Dimensions represent labels for measures. Dimensions are used to filter and group measures.", + "type": "object", + "properties": { + "name": { + "title": "Dimension name", + "description": "The name of the dimension. This will be the same as the POJO field name.", + "type": "string", + "pattern": "^[A-Za-z]([0-9A-Za-z]*_?[0-9A-Za-z]*)*$" + }, + "description": { + "title": "Dimension description", + "description": "A long description of the dimension.", + "type": "string" + }, + "category": { + "title": "Dimension group category", + "description": "Category for grouping dimension", + "type": "string" + }, + "hidden": { + "title": "Hide/Show dimension", + "description": "Whether this dimension is exposed via API metadata", + "type": "boolean", + "default": false + }, + "readAccess": { + "title": "Dimension read access", + "description": "Read permission for the dimension.", + "type": "string", + "default": "Prefab.Role.All" + }, + "definition": { + "title": "Dimension definition", + "description": "The definition of the dimension", + "type": "string", + "default": "" + }, + "tags": { + "title": "Dimension tags", + "description": "An array of string based tags for dimensions", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + }, + "default": [] + } + } + }, + "dimension": { + "title": "Dimension", + "description": "Dimensions represent labels for measures. Dimensions are used to filter and group measures.", + "type": "object", + "allOf": [ + { "$ref": "#/definitions/dimensionRef" }, + { + "properties": { + "type": { + "oneOf":[ + {"$ref": "#/definitions/enumtype"}, + {"enum": [ + "RELATIONSHIP","ID" + ]} + ], + "default": "TEXT" + } + } + } + ], + "required": [ + "name", + "type", + "definition" + ] + }, + "timeDimension": { + "title": "Time Dimension", + "description": "Time Dimensions represent labels for measures. Dimensions are used to filter and group measures.", + "type": "object", + "allOf": [ + { "$ref": "#/definitions/dimensionRef" }, + { + "properties": { + "type": { + "title": "Dimension field type", + "description": "The data type of the dimension field", + "type": "string", + "enum": [ + "TIME" + ], + "default": "TIME" + }, + "grains": { + "title": "Time Dimension grains", + "description": "Time Dimension granularity and Sqls", + "type": "array", + "items": { + "$ref": "#/definitions/grain" + }, + "default": [] + } + } + } + ], + "required": [ + "name", + "type", + "definition", + "grains" + ] + } + }, + "type": "object", + "properties": { + "tables": { + "title": "Elide Table Models", + "description": "Array of elide Table Models", + "type": "array", + "uniqueItems": true, + "items": { + "title": "Elide Table Models", + "description": "Array of elide Table Models", + "type": "object", + "properties": { + "name": { + "title": "Table Model Name", + "description": "The name of the model. This will be the same as the POJO class name.", + "type": "string", + "pattern": "^[A-Z][0-9A-Za-z]*$" + }, + "schema": { + "title": "Table Schema", + "description": "The database or schema where the model lives.", + "type": "string", + "pattern": "^[A-Za-z]([0-9A-Za-z]*_?[0-9A-Za-z]*)*$", + "default": "" + }, + "hidden": { + "title": "Hide/Show Table", + "description": "Whether this table is exposed via API metadata", + "type": "boolean", + "default": false + }, + "description": { + "title": "Table Model description", + "description": "A long description of the model.", + "type": "string" + }, + "cardinality": { + "title": "Model cardinality", + "description": "The number of rows in the table: (tiny, small, medium, large, huge). The relative sizes are decided by the table designer(s).", + "type": "string", + "enum": [ + "tiny", + "small", + "medium", + "large", + "huge" + ], + "default": "tiny" + }, + "readAccess": { + "title": "Table read access", + "description": "Read permission for the table.", + "type": "string", + "default": "Prefab.Role.All" + }, + "joins": { + "title": "Table joins", + "description": "Describes SQL joins to other tables for column references.", + "type": "array", + "items": { + "$ref": "#/definitions/join" + }, + "default": [] + }, + "measures": { + "title": "Table measures", + "description": "Zero or more metric definitions.", + "type": "array", + "items": { + "$ref": "#/definitions/measure" + }, + "default": [] + }, + "dimensions": { + "title": "Table dimensions", + "description": "One or more dimension definitions.", + "type": "array", + "items": { + "anyOf": [ + {"$ref": "#/definitions/dimension"} , + {"$ref": "#/definitions/timeDimension"} + ] + }, + "default": [] + }, + "tags": { + "title": "Table tags", + "description": "An array of string based tags", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + }, + "default": [] + } + }, + "oneOf": [ + { + "properties": { + "sql": { + "title": "Table SQL", + "description": "SQL query which is used to populate the table.", + "type": "string", + "default": "" + } + }, + "required": [ + "name", + "sql", + "dimensions" + ] + } , + { + "properties": { + "table": { + "title": "Table name", + "description": "The physical table name where the model lives.", + "type": "string", + "pattern": "^[A-Za-z]([0-9A-Za-z]*_?[0-9A-Za-z]*)*$", + "default": "" + } + }, + "required": [ + "name", + "table", + "dimensions" + ] + }, + { + "properties": { + "extends": { + "title": "Table Extends", + "description": "Extends another logical table.", + "type": "string", + "default": "" + } + }, + "required": [ + "name", + "extends", + "dimensions" + ] + } + ] + } + } + }, + "minProperties": 1, + "required": [ + "tables" + ], + "additionalProperties": false +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideVariableSchema.json b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideVariableSchema.json new file mode 100644 index 0000000000..1e8c38bb1a --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/elideVariableSchema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft-06/schema#", + "$id": "https://elide.io/schemas/variable_schema_v1#", + "description": "Elide Variable config json/hjson schema", + "type": "object", + "patternProperties": { + "^([A-Za-z]*_?[A-Za-z]*)*$": { + "anyOf": [ + {"type": "string"}, + {"type": "array"}, + {"type": "object"}, + {"type": "null"} + ] + } + }, + "additionalProperties": false, + "minProperties": 1 +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/resources/templates/security.hbs b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/templates/security.hbs new file mode 100644 index 0000000000..d9f3ee8afd --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/templates/security.hbs @@ -0,0 +1,18 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package dynamicconfig.models; + +import com.yahoo.elide.annotation.SecurityCheck; +import com.yahoo.elide.security.checks.prefab.Role.RoleMemberCheck; + +@SecurityCheck(DynamicConfigOperationChecksPrincipalIs<%#titleCaseRemoveSpaces this%><%/titleCaseRemoveSpaces%>.PRINCIPAL_IS_<%#toUpperCase this%><%/toUpperCase%>) +public class DynamicConfigOperationChecksPrincipalIs<%#titleCaseRemoveSpaces this%><%/titleCaseRemoveSpaces%> extends RoleMemberCheck { + + public static final String PRINCIPAL_IS_<%#toUpperCase this%><%/toUpperCase%> = "Principal is <%this%>"; + public DynamicConfigOperationChecksPrincipalIs<%#titleCaseRemoveSpaces this%><%/titleCaseRemoveSpaces%>() { + super("<%this%>"); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/main/resources/templates/table.hbs b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/templates/table.hbs new file mode 100644 index 0000000000..5a2f6f7f75 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/main/resources/templates/table.hbs @@ -0,0 +1,90 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package dynamicconfig.models; + +import com.yahoo.elide.annotation.DeletePermission; +import com.yahoo.elide.annotation.Include; +import com.yahoo.elide.annotation.Exclude; +import com.yahoo.elide.annotation.ReadPermission; +import com.yahoo.elide.annotation.UpdatePermission; +import com.yahoo.elide.datastores.aggregation.annotation.Cardinality; +import com.yahoo.elide.datastores.aggregation.annotation.CardinalitySize; +import com.yahoo.elide.datastores.aggregation.annotation.DimensionFormula; +import com.yahoo.elide.datastores.aggregation.annotation.MetricFormula; +import com.yahoo.elide.datastores.aggregation.annotation.Join; +import com.yahoo.elide.datastores.aggregation.annotation.Meta; +import com.yahoo.elide.datastores.aggregation.annotation.Temporal; +import com.yahoo.elide.datastores.aggregation.annotation.TimeGrainDefinition; +import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain; +import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.FromSubquery; +import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.FromTable; + +import lombok.EqualsAndHashCode; +import lombok.ToString; +import lombok.Data; + +import java.util.Date; +import javax.persistence.Column; +import javax.persistence.Id; + +/** + * A root level entity for testing AggregationDataStore. + */ +@Cardinality(size = CardinalitySize.<%#toUpperCase cardinality%><%/toUpperCase%>) +@EqualsAndHashCode +@ToString +@Data +<%#if table%>@FromTable(name = "<%#if schema%><%schema%>.<%/if%><%table%>") +<%else if sql%>@FromSubquery(sql = "<%sql%>") +<%/if%> +<%#if readAccess%>@ReadPermission(expression = "<%readAccess%>")<%/if%> +<%#if description%>@Meta(description = "<%description%>")<%/if%> +<%#if hidden%>@Exclude<%else%>@Include(rootLevel = true, type = "<%#lowerCaseFirstLetter name%><%/lowerCaseFirstLetter%>")<%/if%> +public class <%#capitalizeFirstLetter name%><%/capitalizeFirstLetter%> <%#if extend%>extends <%#capitalizeFirstLetter extend%><%/capitalizeFirstLetter%><%/if%>{ + + @Id + private String id; + +<%#each dimensions%> + +<%#ifTypeMatches type "TIME"%> + @Temporal(grains = { + <%#each grains%> + @TimeGrainDefinition(grain = TimeGrain.<%grain%>, expression = "<%sql%>")<%#if @last%><%else%>, <%/if%> + <%/each%> + }, timeZone = "UTC") +<%/ifTypeMatches%> + + <%#if readAccess%>@ReadPermission(expression = "<%readAccess%>")<%/if%> + <%#if description%>@Meta(description = "<%description%>")<%/if%> + <%#if hidden%>@Exclude<%/if%> + @DimensionFormula("<%definition%>") + private <%#getJavaType type%><%/getJavaType%> <%name%>; + +<%/each%> + + +<%#each joins%> + + @Join("<% definition %>") +<%#ifTypeMatches type "toMany"%> + private Set<<%#capitalizeFirstLetter to%><%/capitalizeFirstLetter%>> <%name%>; +<%else%> + private <%#capitalizeFirstLetter to%><%/capitalizeFirstLetter%> <%name%>; +<%/ifTypeMatches%> + +<%/each%> + +<%#each measures%> + + @MetricFormula("<%definition%>") + <%#if readAccess%>@ReadPermission(expression = "<%readAccess%>")<%/if%> + <%#if description%>@Meta(description = "<%description%>")<%/if%> + <%#if hidden%>@Exclude<%/if%> + private <%#getJavaType type%><%/getJavaType%> <%name%>; + +<%/each%> +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigHelpersTest.java b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigHelpersTest.java new file mode 100644 index 0000000000..0ab9b555a2 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/DynamicConfigHelpersTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideSecurityConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideTableConfig; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import org.junit.jupiter.api.Test; + +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +@Slf4j +public class DynamicConfigHelpersTest { + + @Test + public void testValidSecuritySchema() throws IOException { + String path = "src/test/resources/security/valid"; + File file = new File(path); + String absolutePath = file.getAbsolutePath(); + Map vars = DynamicConfigHelpers.getVariablesPojo( + DynamicConfigHelpers.formatFilePath(absolutePath)); + ElideSecurityConfig config = DynamicConfigHelpers.getElideSecurityPojo( + DynamicConfigHelpers.formatFilePath(absolutePath), vars); + assertNotNull(config); + } + + @Test + public void testValidVariableSchema() throws JsonProcessingException { + String path = "src/test/resources/variables/valid"; + File file = new File(path); + String absolutePath = file.getAbsolutePath(); + Map config = DynamicConfigHelpers.getVariablesPojo( + DynamicConfigHelpers.formatFilePath(absolutePath)); + assertNotNull(config); + } + + @Test + public void testValidTableSchema() throws IOException { + String path = "src/test/resources/tables"; + File file = new File(path); + String absolutePath = file.getAbsolutePath(); + Map vars = DynamicConfigHelpers.getVariablesPojo( + DynamicConfigHelpers.formatFilePath(absolutePath)); + ElideTableConfig config = DynamicConfigHelpers.getElideTablePojo( + DynamicConfigHelpers.formatFilePath(absolutePath), vars, "valid/"); + assertNotNull(config); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/SchemaTest.java b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/SchemaTest.java new file mode 100644 index 0000000000..0b92b173bb --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/SchemaTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hjson.JsonValue; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; + +public class SchemaTest { + + private InputStream loadStreamFromClasspath(String resource) throws Exception { + return TableSchemaValidationTest.class.getResourceAsStream(resource); + } + + private Reader loadReaderFromClasspath(String resource) throws Exception { + return new InputStreamReader(loadStreamFromClasspath(resource)); + } + + protected JsonNode loadJsonFromClasspath(String resource, boolean translate) throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + + Reader reader = loadReaderFromClasspath(resource); + + if (translate) { + String jsonText = JsonValue.readHjson(reader).toString(); + return objectMapper.readTree(jsonText); + } + + return objectMapper.readTree(reader); + } + + protected JsonNode loadJsonFromClasspath(String resource) throws Exception { + return loadJsonFromClasspath(resource, false); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/SecuritySchemaValidationTest.java b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/SecuritySchemaValidationTest.java new file mode 100644 index 0000000000..8455aa5bd3 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/SecuritySchemaValidationTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jsonschema.core.report.ProcessingReport; +import com.github.fge.jsonschema.main.JsonSchema; +import com.github.fge.jsonschema.main.JsonSchemaFactory; +import org.junit.jupiter.api.Test; + +/** + * Security Schema functional test. + */ +public class SecuritySchemaValidationTest extends SchemaTest { + + private final JsonSchema schema; + + public SecuritySchemaValidationTest() throws Exception { + JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); + schema = factory.getJsonSchema("resource:/elideSecuritySchema.json"); + } + + @Test + public void testValidSecuritySchema() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/security/valid/security.json"); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } + + @Test + public void testInValidSecuritySchema() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/security/invalid/security.json"); + ProcessingReport results = schema.validate(testNode); + assertFalse(results.isSuccess()); + } + + @Test + public void testValidSecurityHJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/security/valid/security.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } + + @Test + public void testInvalidSecurityHJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/security/invalid/security.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertFalse(results.isSuccess()); + } + + @Test + public void testModelecurityHJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/models/security.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/TableSchemaValidationTest.java b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/TableSchemaValidationTest.java new file mode 100644 index 0000000000..a2bdc9f77b --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/TableSchemaValidationTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jsonschema.core.report.ProcessingReport; +import com.github.fge.jsonschema.main.JsonSchema; +import com.github.fge.jsonschema.main.JsonSchemaFactory; +import org.junit.jupiter.api.Test; + +/** + * Security Schema functional test. + */ +public class TableSchemaValidationTest extends SchemaTest { + + private final JsonSchema schema; + + public TableSchemaValidationTest() throws Exception { + JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); + schema = factory.getJsonSchema("resource:/elideTableSchema.json"); + } + + @Test + public void testValidTableSchema() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/tables/valid/table.json"); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } + + @Test + public void testInvalidTableSchema() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/tables/invalid/table.json"); + ProcessingReport results = schema.validate(testNode); + assertFalse(results.isSuccess()); + } + + @Test + public void testValidTableHJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/tables/valid/table.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } + + @Test + public void testInvalidTableHJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/tables/invalid/table.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertFalse(results.isSuccess()); + } + + @Test + public void testModelsTable1HJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/models/tables/table1.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } + + @Test + public void testModelsTable2HJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/models/tables/table2.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } + + @Test + public void testModelsTable3HJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/models_missing/tables/table1.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/VariableSchemaValidationTest.java b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/VariableSchemaValidationTest.java new file mode 100644 index 0000000000..3cc47d8c37 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/VariableSchemaValidationTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jsonschema.core.report.ProcessingReport; +import com.github.fge.jsonschema.main.JsonSchema; +import com.github.fge.jsonschema.main.JsonSchemaFactory; +import org.junit.jupiter.api.Test; + +/** + * Security Schema functional test. + */ +public class VariableSchemaValidationTest extends SchemaTest { + + private final JsonSchema schema; + + public VariableSchemaValidationTest() throws Exception { + JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); + schema = factory.getJsonSchema("resource:/elideVariableSchema.json"); + } + + @Test + public void testValidVariableSchema() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/variables/valid/variables.json"); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } + + @Test + public void testInValidVariableSchema() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/variables/invalid/variables.json"); + ProcessingReport results = schema.validate(testNode); + assertFalse(results.isSuccess()); + } + + @Test + public void testValidVariableHJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/variables/valid/variables.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } + + @Test + public void testInvalidVariableHJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/variables/invalid/variables.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertFalse(results.isSuccess()); + } + + @Test + public void testModelsVariableHJson() throws Exception { + JsonNode testNode = loadJsonFromClasspath("/models/variables.hjson", true); + ProcessingReport results = schema.validate(testNode); + assertTrue(results.isSuccess()); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/ElideConfigParserTest.java b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/ElideConfigParserTest.java new file mode 100644 index 0000000000..1a267f8bd3 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/ElideConfigParserTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideSecurityConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.ElideTableConfig; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.Table; +import com.yahoo.elide.contrib.dynamicconfighelpers.model.Type; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Map; + +public class ElideConfigParserTest { + + @Test + public void testValidateVariablePath() throws Exception { + + String path = "src/test/resources/models"; + File file = new File(path); + String absolutePath = file.getAbsolutePath(); + ElideConfigParser testClass = new ElideConfigParser(absolutePath); + + Map variable = testClass.getVariables(); + assertEquals(6, variable.size()); + assertEquals("blah", variable.get("bar")); + + ElideSecurityConfig security = testClass.getElideSecurityConfig(); + assertEquals(3, security.getRoles().size()); + + ElideTableConfig tables = testClass.getElideTableConfig(); + assertEquals(2, tables.getTables().size()); + for (Table t : tables.getTables()) { + assertEquals(t.getMeasures().get(0).getName() , t.getMeasures().get(0).getDescription()); + assertEquals("MAX(score)", t.getMeasures().get(0).getDefinition()); + assertEquals(Table.Cardinality.LARGE, t.getCardinality()); + // test hydration, variable substitution + assertEquals(Type.INTEGER, t.getMeasures().get(0).getType()); + } + } + + @Test + public void testNullConfig() { + try { + new ElideConfigParser(null); + } catch (IllegalArgumentException e) { + assertEquals("Config path is null", e.getMessage()); + } + } + + @Test + public void testMissingConfig() { + String path = "src/test/resources/models_missing"; + File file = new File(path); + String absolutePath = file.getAbsolutePath(); + ElideConfigParser testClass = new ElideConfigParser(absolutePath); + + assertNull(testClass.getVariables()); + assertNull(testClass.getElideSecurityConfig()); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHydratorTest.java b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHydratorTest.java new file mode 100644 index 0000000000..d7baefa88d --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/java/com/yahoo/elide/contrib/dynamicconfighelpers/parser/handlebars/HandlebarsHydratorTest.java @@ -0,0 +1,286 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.contrib.dynamicconfighelpers.parser.handlebars; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.yahoo.elide.contrib.dynamicconfighelpers.parser.ElideConfigParser; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; + +public class HandlebarsHydratorTest { + + private static final String VALID_TABLE_WITH_VARIABLES = "{\n" + + " tables: [{\n" + + " name: <% name %>\n" + + " table: <% table %>\n" + + " schema: gamedb\n" + + " description:\n" + + " '''\n" + + " A long description\n" + + " '''\n" + + " cardinality : large\n" + + " hidden : false\n" + + " readAccess : A user is admin or is a player in the game\n" + + " joins: [\n" + + " {\n" + + " name: playerCountry\n" + + " to: country\n" + + " type: toOne\n" + + " definition: '${to}.id = ${from}.country_id'\n" + + " },\n" + + " {\n" + + " name: playerTeam\n" + + " to: team\n" + + " type: toMany\n" + + " definition: '${to}.id = ${from}.team_id'\n" + + " }\n" + + " ]\n" + + "\n" + + " measures : [\n" + + " {\n" + + " name : highScore\n" + + " type : INTEGER\n" + + " definition: 'MAX(score)'\n" + + " }\n" + + " ]\n" + + " dimensions : [\n" + + " {\n" + + " name : countryIsoCode\n" + + " type : TEXT\n" + + " definition : '{{playerCountry.isoCode}}'\n" + + " },\n" + + " {\n" + + " name : createdOn\n" + + " type : TIME\n" + + " definition : create_on\n" + + " grains: [\n" + + " {\n" + + " grain : DAY\n" + + " sql : '''\n" + + " PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-dd'), 'yyyy-MM-dd')\n" + + " '''\n" + + " },\n" + + " {\n" + + " grain : MONTH\n" + + " sql : '''\n" + + " PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-01'), 'yyyy-MM-dd')\n" + + " '''\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }]\n" + + "}\n"; + + private static final String VALID_TABLE_JAVA_NAME = "PlayerStats"; + + private static final String VALID_TABLE_JAVA = "/*\n" + + " * Copyright 2020, Yahoo Inc.\n" + + " * Licensed under the Apache License, Version 2.0\n" + + " * See LICENSE file in project root for terms.\n" + + " */\n" + + "package dynamicconfig.models;\n" + + "\n" + + "import com.yahoo.elide.annotation.DeletePermission;\n" + + "import com.yahoo.elide.annotation.Include;\n" + + "import com.yahoo.elide.annotation.Exclude;\n" + + "import com.yahoo.elide.annotation.ReadPermission;\n" + + "import com.yahoo.elide.annotation.UpdatePermission;\n" + + "import com.yahoo.elide.datastores.aggregation.annotation.Cardinality;\n" + + "import com.yahoo.elide.datastores.aggregation.annotation.CardinalitySize;\n" + + "import com.yahoo.elide.datastores.aggregation.annotation.DimensionFormula;\n" + + "import com.yahoo.elide.datastores.aggregation.annotation.MetricFormula;\n" + + "import com.yahoo.elide.datastores.aggregation.annotation.Join;\n" + + "import com.yahoo.elide.datastores.aggregation.annotation.Meta;\n" + + "import com.yahoo.elide.datastores.aggregation.annotation.Temporal;\n" + + "import com.yahoo.elide.datastores.aggregation.annotation.TimeGrainDefinition;\n" + + "import com.yahoo.elide.datastores.aggregation.metadata.enums.TimeGrain;\n" + + "import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.FromSubquery;\n" + + "import com.yahoo.elide.datastores.aggregation.queryengines.sql.annotation.FromTable;\n" + + "\n" + + "import lombok.EqualsAndHashCode;\n" + + "import lombok.ToString;\n" + + "import lombok.Data;\n" + + "\n" + + "import java.util.Date;\n" + + "import javax.persistence.Column;\n" + + "import javax.persistence.Id;\n" + + "\n" + + "/**\n" + + " * A root level entity for testing AggregationDataStore.\n" + + " */\n" + + "@Cardinality(size = CardinalitySize.LARGE)\n" + + "@EqualsAndHashCode\n" + + "@ToString\n" + + "@Data\n" + + "@FromTable(name = \"gamedb.player_stats\")\n" + + "\n" + + "@ReadPermission(expression = \"A user is admin or is a player in the game\")\n" + + "@Meta(description = \"A long description\")\n" + + "@Include(rootLevel = true, type = \"playerStats\")\n" + + "public class PlayerStats {\n" + + "\n" + + " @Id\n" + + " private String id;\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " @ReadPermission(expression = \"Prefab.Role.All\")\n" + + " @Meta(description = \"countryIsoCode\")\n" + + " \n" + + " @DimensionFormula(\"{{playerCountry.isoCode}}\")\n" + + " private String countryIsoCode;\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " @Temporal(grains = {\n" + + " \n" + + " @TimeGrainDefinition(grain = TimeGrain.DAY, expression = \"PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-dd'), 'yyyy-MM-dd')\"), \n" + + " \n" + + " @TimeGrainDefinition(grain = TimeGrain.MONTH, expression = \"PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-01'), 'yyyy-MM-dd')\")\n" + + " \n" + + " }, timeZone = \"UTC\")\n" + + "\n" + + "\n" + + " @ReadPermission(expression = \"Prefab.Role.All\")\n" + + " @Meta(description = \"createdOn\")\n" + + " \n" + + " @DimensionFormula(\"create_on\")\n" + + " private Date createdOn;\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " @Join(\"${to}.id = ${from}.country_id\")\n" + + "\n" + + " private Country playerCountry;\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " @Join(\"${to}.id = ${from}.team_id\")\n" + + "\n" + + " private Set playerTeam;\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + " @MetricFormula(\"MAX(score)\")\n" + + " @ReadPermission(expression = \"Prefab.Role.All\")\n" + + " @Meta(description = \"highScore\")\n" + + " \n" + + " private Long highScore;\n" + + "\n" + + "\n" + + "}\n"; + + + private static final String VALID_SECURITY_ADMIN_JAVA_NAME = "DynamicConfigOperationChecksPrincipalIsAdmin"; + private static final String VALID_SECURITY_GUEST_JAVA_NAME = "DynamicConfigOperationChecksPrincipalIsGuest"; + + private static final String VALID_SECURITY_ADMIN_JAVA = "/*\n" + + " * Copyright 2020, Yahoo Inc.\n" + + " * Licensed under the Apache License, Version 2.0\n" + + " * See LICENSE file in project root for terms.\n" + + " */\n" + + "package dynamicconfig.models;\n" + + "\n" + + "import com.yahoo.elide.annotation.SecurityCheck;\n" + + "import com.yahoo.elide.security.checks.prefab.Role.RoleMemberCheck;\n" + + "\n" + + "@SecurityCheck(DynamicConfigOperationChecksPrincipalIsAdmin.PRINCIPAL_IS_ADMIN)\n" + + "public class DynamicConfigOperationChecksPrincipalIsAdmin extends RoleMemberCheck {\n" + + "\n" + + " public static final String PRINCIPAL_IS_ADMIN = \"Principal is admin\";\n" + + " public DynamicConfigOperationChecksPrincipalIsAdmin() {\n" + + " super(\"admin\");\n" + + " }\n" + + "}\n"; + + private static final String VALID_SECURITY_GUEST_JAVA = "/*\n" + + " * Copyright 2020, Yahoo Inc.\n" + + " * Licensed under the Apache License, Version 2.0\n" + + " * See LICENSE file in project root for terms.\n" + + " */\n" + + "package dynamicconfig.models;\n" + + "\n" + + "import com.yahoo.elide.annotation.SecurityCheck;\n" + + "import com.yahoo.elide.security.checks.prefab.Role.RoleMemberCheck;\n" + + "\n" + + "@SecurityCheck(DynamicConfigOperationChecksPrincipalIsGuest.PRINCIPAL_IS_GUEST)\n" + + "public class DynamicConfigOperationChecksPrincipalIsGuest extends RoleMemberCheck {\n" + + "\n" + + " public static final String PRINCIPAL_IS_GUEST = \"Principal is guest\";\n" + + " public DynamicConfigOperationChecksPrincipalIsGuest() {\n" + + " super(\"guest\");\n" + + " }\n" + + "}\n"; + + @Test + public void testConfigHydration() throws IOException { + + HandlebarsHydrator obj = new HandlebarsHydrator(); + String path = "src/test/resources/models"; + File file = new File(path); + String absolutePath = file.getAbsolutePath(); + String hjsonPath = absolutePath + "/tables/table1.hjson"; + + ElideConfigParser testClass = new ElideConfigParser(absolutePath); + + Map map = testClass.getVariables(); + + String content = new String (Files.readAllBytes(Paths.get(hjsonPath))); + + assertEquals(content, obj.hydrateConfigTemplate(VALID_TABLE_WITH_VARIABLES, map)); + } + + @Test + public void testTableHydration() throws IOException { + + HandlebarsHydrator obj = new HandlebarsHydrator(); + String path = "src/test/resources/models"; + File file = new File(path); + String absolutePath = file.getAbsolutePath(); + + ElideConfigParser testClass = new ElideConfigParser(absolutePath); + + Map tableClasses = obj.hydrateTableTemplate(testClass.getElideTableConfig()); + + assertEquals(true, tableClasses.keySet().contains(VALID_TABLE_JAVA_NAME)); + assertEquals(VALID_TABLE_JAVA, tableClasses.get(VALID_TABLE_JAVA_NAME)); + } + + @Test + public void testSecurityHydration() throws IOException { + HandlebarsHydrator obj = new HandlebarsHydrator(); + String path = "src/test/resources/models"; + File file = new File(path); + String absolutePath = file.getAbsolutePath(); + + ElideConfigParser testClass = new ElideConfigParser(absolutePath); + + Map securityClasses = obj.hydrateSecurityTemplate(testClass.getElideSecurityConfig()); + + assertEquals(true, securityClasses.keySet().contains(VALID_SECURITY_ADMIN_JAVA_NAME)); + assertEquals(true, securityClasses.keySet().contains(VALID_SECURITY_GUEST_JAVA_NAME)); + assertEquals(VALID_SECURITY_ADMIN_JAVA, securityClasses.get(VALID_SECURITY_ADMIN_JAVA_NAME)); + assertEquals(VALID_SECURITY_GUEST_JAVA, securityClasses.get(VALID_SECURITY_GUEST_JAVA_NAME)); + } +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/security.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/security.hjson new file mode 100644 index 0000000000..effa2da850 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/security.hjson @@ -0,0 +1,18 @@ +{ + roles : [ + admin + guest + member + ] + rules: [ + { + type: filter + filter: filter + name: User belongs to company + }, + { + filter: filter + name: Principal is owner + }, + ] +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/tables/table1.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/tables/table1.hjson new file mode 100644 index 0000000000..90cdaf69d7 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/tables/table1.hjson @@ -0,0 +1,62 @@ +{ + tables: [{ + name: PlayerStats + table: player_stats + schema: gamedb + description: + ''' + A long description + ''' + cardinality : large + hidden : false + readAccess : A user is admin or is a player in the game + joins: [ + { + name: playerCountry + to: country + type: toOne + definition: '${to}.id = ${from}.country_id' + }, + { + name: playerTeam + to: team + type: toMany + definition: '${to}.id = ${from}.team_id' + } + ] + + measures : [ + { + name : highScore + type : INTEGER + definition: 'MAX(score)' + } + ] + dimensions : [ + { + name : countryIsoCode + type : TEXT + definition : '{{playerCountry.isoCode}}' + }, + { + name : createdOn + type : TIME + definition : create_on + grains: [ + { + grain : DAY + sql : ''' + PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-dd'), 'yyyy-MM-dd') + ''' + }, + { + grain : MONTH + sql : ''' + PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-01'), 'yyyy-MM-dd') + ''' + } + ] + } + ] + }] +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/tables/table2.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/tables/table2.hjson new file mode 100644 index 0000000000..848bab22f5 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/tables/table2.hjson @@ -0,0 +1,48 @@ +{ + tables: [{ + name: Player + table: player + schema: playerdb + description: + ''' + A long description + ''' + cardinality : large + readAccess : A user is admin or is a player in the game + joins: [ + { + name: playerCountry + to: country + type: toOne + definition: '${to}.id = ${from}.country_id' + } + ] + measures : [ + { + name : highScore + type : "INTEGER" + definition: '<%measure_type%>(score)' + } + ] + dimensions : [ + { + name : countryCode + type : TEXT + definition : '{{playerCountry.isoCode}}' + }, + { + name : createdOn + type : TIME + definition : create_on + grains: [ + { + grain : MONTH + sql : ''' + PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-01'), 'yyyy-MM-dd') + ''' + } + ] + } + ] + }] +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/variables.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/variables.hjson new file mode 100644 index 0000000000..6d534ed7b7 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models/variables.hjson @@ -0,0 +1,8 @@ +{ + foo: [1, 2, 3] + bar: blah + hour: hour_replace + measure_type: MAX + name: PlayerStats + table: player_stats +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models_missing/tables/table1.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models_missing/tables/table1.hjson new file mode 100644 index 0000000000..8f0b8014d1 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/models_missing/tables/table1.hjson @@ -0,0 +1,48 @@ +{ + tables: [{ + name: PlayerStats + table: player_stats + schema: gamedb + description: + ''' + A long description + ''' + cardinality : large + readAccess : A user is admin or is a player in the game + joins: [ + { + name: playerCountry + to: country + type: toOne + definition: '${to}.id = ${from}.country_id' + } + ] + measures : [ + { + name : highScore + type : INTEGER + definition: 'MAX(score)' + } + ] + dimensions : [ + { + name : countryCode + type : TEXT + definition : playerCountry.isoCode + }, + { + name : createdOn + type : TIME + definition : create_on + grains: [ + { + grain : MONTH + sql : ''' + PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-01'), 'yyyy-MM-dd') + ''' + } + ] + } + ] + }] +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/invalid/security.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/invalid/security.hjson new file mode 100644 index 0000000000..d166716f97 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/invalid/security.hjson @@ -0,0 +1,11 @@ +{ + //Comment + name : book + table : book + schema$ : [123] + description : + ''' + valid schema for a book + ''' + cardinality : small +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/invalid/security.json b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/invalid/security.json new file mode 100644 index 0000000000..c1e1422253 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/invalid/security.json @@ -0,0 +1,8 @@ + +{ + "name!" : "book", + "table" : "book", + "schema$" : "testdb", + "description" : "valid schema for a book", + "cardinality" : "invalid" +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/valid/security.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/valid/security.hjson new file mode 100644 index 0000000000..ad33e38836 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/valid/security.hjson @@ -0,0 +1,14 @@ +{ + roles : ["admin", "guest", "member"] + rules: [ + { + type: filter + filter: filter + name: User belongs to company + }, + { + filter: filter + name: Principal is owner + } + ] +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/valid/security.json b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/valid/security.json new file mode 100644 index 0000000000..e3a52f6e1c --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/security/valid/security.json @@ -0,0 +1,14 @@ +{ + "roles" : ["admin", "guest", "member"], + "rules": [ + { + "type": "filter", + "filter": "filter", + "name": "User belongs to company" + }, + { + "filter": "filter", + "name": "Principal is owner" + } + ] +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/invalid/table.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/invalid/table.hjson new file mode 100644 index 0000000000..d166716f97 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/invalid/table.hjson @@ -0,0 +1,11 @@ +{ + //Comment + name : book + table : book + schema$ : [123] + description : + ''' + valid schema for a book + ''' + cardinality : small +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/invalid/table.json b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/invalid/table.json new file mode 100644 index 0000000000..0c3f97c1bd --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/invalid/table.json @@ -0,0 +1,7 @@ +{ + "fname" : "book", + "ftable" : "book", + "fschema$" : "testdb", + "description" : "valid schema for a book", + "cardinality" : "invalid" +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/valid/table.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/valid/table.hjson new file mode 100644 index 0000000000..8f0b8014d1 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/valid/table.hjson @@ -0,0 +1,48 @@ +{ + tables: [{ + name: PlayerStats + table: player_stats + schema: gamedb + description: + ''' + A long description + ''' + cardinality : large + readAccess : A user is admin or is a player in the game + joins: [ + { + name: playerCountry + to: country + type: toOne + definition: '${to}.id = ${from}.country_id' + } + ] + measures : [ + { + name : highScore + type : INTEGER + definition: 'MAX(score)' + } + ] + dimensions : [ + { + name : countryCode + type : TEXT + definition : playerCountry.isoCode + }, + { + name : createdOn + type : TIME + definition : create_on + grains: [ + { + grain : MONTH + sql : ''' + PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-01'), 'yyyy-MM-dd') + ''' + } + ] + } + ] + }] +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/valid/table.json b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/valid/table.json new file mode 100644 index 0000000000..309f59ae45 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/tables/valid/table.json @@ -0,0 +1,63 @@ +{ + "tables": [ + { + "name": "PlayerStats", + "table": "player_stats", + "schema": "gamedb", + "cardinality" : "large", + "readAccess" : "A user is admin or is a player in the game", + "joins": [ + { + "name": "playerCountry", + "to": "country", + "type": "toOne", + "definition": "${to}.id = ${from}.country_id" + } + ], + "measures" : [ + { + "name" : "highScore", + "type" : "INTEGER", + "definition": "MAX(score)" + }, + { + "name" : "highScoreCoord", + "type" : "COORDINATE", + "definition": "MAX(score)" + } + ], + "dimensions" : [ + { + "name" : "countryCode", + "type" : "RELATIONSHIP", + "definition" : "playerCountry.isoCode" + }, + { + "name" : "countryCode", + "type" : "TEXT", + "definition" : "playerCountry.isoCode" + }, + { + "name" : "createdOn", + "type" : "TIME", + "definition" : "create_on", + "grains":[{ + + "grain": "MONTH", + "sql": "PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-01'), 'yyyy-MM-dd')" + }] + }, + { + "name" : "createdOn", + "type" : "TIME", + "definition" : "create_on", + "grains":[{ + + "grain": "MONTH", + "sql": "PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-01'), 'yyyy-MM-dd')" + }] + } + ] + } + ] +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/invalid/variables.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/invalid/variables.hjson new file mode 100644 index 0000000000..d166716f97 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/invalid/variables.hjson @@ -0,0 +1,11 @@ +{ + //Comment + name : book + table : book + schema$ : [123] + description : + ''' + valid schema for a book + ''' + cardinality : small +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/invalid/variables.json b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/invalid/variables.json new file mode 100644 index 0000000000..c1e1422253 --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/invalid/variables.json @@ -0,0 +1,8 @@ + +{ + "name!" : "book", + "table" : "book", + "schema$" : "testdb", + "description" : "valid schema for a book", + "cardinality" : "invalid" +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/valid/variables.hjson b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/valid/variables.hjson new file mode 100644 index 0000000000..e0358bfc8b --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/valid/variables.hjson @@ -0,0 +1,9 @@ +{ + desc_blah: blah blah blah + def_on: create_on + grain_dmy: ["{{day}}", "{{month}}", "{{year}}"] + grain_hd: ["{{hour}}", "{{day}}"] + foo: [1, 2, 3] + foobar: "[1, 2, 3]" + nullCheck: null +} diff --git a/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/valid/variables.json b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/valid/variables.json new file mode 100644 index 0000000000..0d0b85e3fe --- /dev/null +++ b/elide-contrib/elide-dynamic-config-helpers/src/test/resources/variables/valid/variables.json @@ -0,0 +1,11 @@ +{ + "desc_blah": "blah blah blah", + "def_on_test": "create_on", + "grain_dmy": "[{{day}}, {{month}}, {{year}}]", + "grain_hd": ["{{hour}}", "{{day}}"], + "nullCheck": null, + "grainVariable":[{ + "grain": "MONTH", + "sql": "PARSEDATETIME(FORMATDATETIME(${column}, 'yyyy-MM-01'), 'yyyy-MM-dd')" + }] +} diff --git a/elide-contrib/elide-swagger/pom.xml b/elide-contrib/elide-swagger/pom.xml index d0c8016c18..01cf38768b 100644 --- a/elide-contrib/elide-swagger/pom.xml +++ b/elide-contrib/elide-swagger/pom.xml @@ -14,7 +14,7 @@ elide-contrib-parent-pom com.yahoo.elide - 4.6.3-SNAPSHOT + 5.0.0-pr9-SNAPSHOT @@ -42,7 +42,7 @@ com.yahoo.elide elide-core - 4.6.3-SNAPSHOT + 5.0.0-pr9-SNAPSHOT @@ -54,7 +54,7 @@ com.yahoo.elide elide-integration-tests - 4.6.3-SNAPSHOT + 5.0.0-pr9-SNAPSHOT test-jar test diff --git a/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/JsonApiModelResolver.java b/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/JsonApiModelResolver.java index 2bad09a5bd..aaf6f7e846 100644 --- a/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/JsonApiModelResolver.java +++ b/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/JsonApiModelResolver.java @@ -31,7 +31,7 @@ import java.util.stream.Collectors; /** - * Swagger ModelResolvers map POJO classes to Swagger com.yahoo.elide.contrib.swagger.models. + * Swagger ModelResolvers map POJO classes to Swagger example.models. * This resolver maps the POJO to a JSON-API Resource. */ public class JsonApiModelResolver extends ModelResolver { diff --git a/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/SwaggerBuilder.java b/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/SwaggerBuilder.java index ca8f1798ed..9ec92cd7c5 100644 --- a/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/SwaggerBuilder.java +++ b/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/SwaggerBuilder.java @@ -5,6 +5,8 @@ */ package com.yahoo.elide.contrib.swagger; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; + import com.yahoo.elide.contrib.swagger.model.Data; import com.yahoo.elide.contrib.swagger.model.Datum; import com.yahoo.elide.contrib.swagger.property.Relationship; @@ -676,10 +678,15 @@ public Swagger build() { ModelConverters converters = ModelConverters.getInstance(); converters.addConverter(new JsonApiModelResolver(dictionary)); + String apiVersion = swagger.getInfo().getVersion(); + if (apiVersion == null) { + apiVersion = NO_VERSION; + } + if (allClasses.isEmpty()) { - allClasses = dictionary.getBindings(); + allClasses = dictionary.getBoundClassesByVersion(apiVersion); } else { - allClasses = Sets.intersection(dictionary.getBindings(), allClasses); + allClasses = Sets.intersection(dictionary.getBoundClassesByVersion(apiVersion), allClasses); if (allClasses.isEmpty()) { throw new IllegalArgumentException("None of the provided classes are exported by Elide"); } diff --git a/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/resources/DocEndpoint.java b/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/resources/DocEndpoint.java index bfc06c5341..a941f206c7 100644 --- a/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/resources/DocEndpoint.java +++ b/elide-contrib/elide-swagger/src/main/java/com/yahoo/elide/contrib/swagger/resources/DocEndpoint.java @@ -5,17 +5,25 @@ */ package com.yahoo.elide.contrib.swagger.resources; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; + import com.yahoo.elide.contrib.swagger.SwaggerBuilder; +import org.apache.commons.lang3.tuple.Pair; + import io.swagger.models.Swagger; +import lombok.AllArgsConstructor; +import lombok.Data; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; @@ -28,7 +36,15 @@ @Path("/doc") @Produces("application/json") public class DocEndpoint { - protected Map documents; + //Maps api version & path to a swagger document. + protected Map, String> documents; + + @Data + @AllArgsConstructor + public static class SwaggerRegistration { + private String path; + private Swagger document; + } /** * Constructs the resource. @@ -36,18 +52,29 @@ public class DocEndpoint { * @param docs Map of path parameter name to swagger document. */ @Inject - public DocEndpoint(@Named("swagger") Map docs) { + public DocEndpoint(@Named("swagger") List docs) { documents = new HashMap<>(); - docs.forEach((key, value) -> { - documents.put(key, SwaggerBuilder.getDocument(value)); + docs.forEach((doc) -> { + String apiVersion = doc.document.getInfo().getVersion(); + apiVersion = apiVersion == null ? NO_VERSION : apiVersion; + String apiPath = doc.path; + + documents.put(Pair.of(apiVersion, apiPath), SwaggerBuilder.getDocument(doc.document)); }); } @GET @Path("/") - public Response list() { - String body = "[" + documents.keySet().stream() + public Response list(@HeaderParam("ApiVersion") String apiVersion) { + String safeApiVersion = apiVersion == null ? NO_VERSION : apiVersion; + + List documentPaths = documents.keySet().stream() + .filter(key -> key.getLeft().equals(safeApiVersion)) + .map(key -> key.getRight()) + .collect(Collectors.toList()); + + String body = "[" + documentPaths.stream() .map(key -> '"' + key + '"') .collect(Collectors.joining(",")) + "]"; @@ -62,9 +89,11 @@ public Response list() { */ @GET @Path("/{name}") - public Response get(@PathParam("name") String name) { - if (documents.containsKey(name)) { - return Response.ok(documents.get(name)).build(); + public Response get(@HeaderParam("ApiVersion") String apiVersion, @PathParam("name") String name) { + String safeApiVersion = apiVersion == null ? NO_VERSION : apiVersion; + Pair lookupKey = Pair.of(safeApiVersion, name); + if (documents.containsKey(lookupKey)) { + return Response.ok(documents.get(lookupKey)).build(); } return Response.status(404).entity("Unknown document: " + name).build(); } diff --git a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/JsonApiModelResolverTest.java b/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/JsonApiModelResolverTest.java index ed617dd9b6..010159ac6a 100644 --- a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/JsonApiModelResolverTest.java +++ b/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/JsonApiModelResolverTest.java @@ -11,13 +11,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.yahoo.elide.contrib.swagger.model.Resource; -import com.yahoo.elide.contrib.swagger.models.Author; -import com.yahoo.elide.contrib.swagger.models.Book; -import com.yahoo.elide.contrib.swagger.models.Publisher; import com.yahoo.elide.core.EntityDictionary; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; +import example.models.Author; +import example.models.Book; +import example.models.Publisher; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; diff --git a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerBuilderTest.java b/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerBuilderTest.java index 40ade616fa..e2b92430b7 100644 --- a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerBuilderTest.java +++ b/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerBuilderTest.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.contrib.swagger; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -13,15 +14,15 @@ import com.yahoo.elide.annotation.Include; import com.yahoo.elide.contrib.swagger.model.Resource; -import com.yahoo.elide.contrib.swagger.models.Author; -import com.yahoo.elide.contrib.swagger.models.Book; -import com.yahoo.elide.contrib.swagger.models.Publisher; import com.yahoo.elide.contrib.swagger.property.Data; import com.yahoo.elide.contrib.swagger.property.Datum; import com.yahoo.elide.contrib.swagger.property.Relationship; import com.yahoo.elide.core.EntityDictionary; import com.google.common.collect.Maps; +import example.models.Author; +import example.models.Book; +import example.models.Publisher; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -66,7 +67,7 @@ public void setup() { dictionary.bindEntity(Book.class); dictionary.bindEntity(Author.class); dictionary.bindEntity(Publisher.class); - Info info = new Info().title("Test Service").version("1.0"); + Info info = new Info().title("Test Service").version(NO_VERSION); SwaggerBuilder builder = new SwaggerBuilder(dictionary, info); swagger = builder.build(); @@ -533,7 +534,7 @@ public void testTagGeneration() throws Exception { public void testGlobalErrorResponses() throws Exception { Info info = new Info() .title("Test Service") - .version("1.0"); + .version(NO_VERSION); SwaggerBuilder builder = new SwaggerBuilder(dictionary, info); @@ -582,7 +583,7 @@ class NothingToSort { EntityDictionary entityDictionary = new EntityDictionary(Maps.newHashMap()); entityDictionary.bindEntity(NothingToSort.class); - Info info = new Info().title("Test Service").version("1.0"); + Info info = new Info().title("Test Service").version(NO_VERSION); SwaggerBuilder builder = new SwaggerBuilder(entityDictionary, info); Swagger testSwagger = builder.build(); diff --git a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerIT.java b/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerIT.java index 5ebce42e84..1964562b2e 100644 --- a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerIT.java +++ b/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerIT.java @@ -6,7 +6,10 @@ package com.yahoo.elide.contrib.swagger; import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.yahoo.elide.contrib.swagger.resources.DocEndpoint; import com.yahoo.elide.initialization.AbstractApiResourceInitializer; @@ -25,6 +28,17 @@ public SwaggerIT() { void testDocumentFetch() throws Exception { ObjectMapper mapper = new ObjectMapper(); JsonNode node = mapper.readTree(get("/doc/test").asString()); + assertTrue(node.get("paths").size() > 1); assertNotNull(node.get("paths").get("/book")); + assertNotNull(node.get("paths").get("/publisher")); + } + + @Test + void testVersion2DocumentFetch() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readTree(given().header("ApiVersion", "1.0").get("/doc/test").asString()); + assertEquals(2, node.get("paths").size()); + assertNotNull(node.get("paths").get("/book")); + assertNotNull(node.get("paths").get("/book/{bookId}")); } } diff --git a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerResourceConfig.java b/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerResourceConfig.java index 52220ae32d..280ba98b84 100644 --- a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerResourceConfig.java +++ b/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/SwaggerResourceConfig.java @@ -5,13 +5,17 @@ */ package com.yahoo.elide.contrib.swagger; -import com.yahoo.elide.contrib.swagger.models.Author; -import com.yahoo.elide.contrib.swagger.models.Book; -import com.yahoo.elide.contrib.swagger.models.Publisher; + +import com.yahoo.elide.contrib.swagger.resources.DocEndpoint; import com.yahoo.elide.core.EntityDictionary; import com.google.common.collect.Maps; +import example.models.Author; +import example.models.Book; +import example.models.Publisher; +import example.models.versioned.BookV2; + import org.glassfish.hk2.api.Factory; import org.glassfish.hk2.api.TypeLiteral; import org.glassfish.hk2.utilities.binding.AbstractBinder; @@ -20,8 +24,8 @@ import io.swagger.models.Info; import io.swagger.models.Swagger; -import java.util.HashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; public class SwaggerResourceConfig extends ResourceConfig { @@ -29,30 +33,36 @@ public SwaggerResourceConfig() { register(new AbstractBinder() { @Override protected void configure() { - bindFactory(new Factory>() { + bindFactory(new Factory>() { @Override - public Map provide() { + public List provide() { EntityDictionary dictionary = new EntityDictionary(Maps.newHashMap()); dictionary.bindEntity(Book.class); + dictionary.bindEntity(BookV2.class); dictionary.bindEntity(Author.class); dictionary.bindEntity(Publisher.class); - Info info = new Info().title("Test Service").version("1.0"); + Info info1 = new Info().title("Test Service"); + + SwaggerBuilder builder1 = new SwaggerBuilder(dictionary, info1); + Swagger swagger1 = builder1.build(); - SwaggerBuilder builder = new SwaggerBuilder(dictionary, info); - Swagger swagger = builder.build(); + Info info2 = new Info().title("Test Service").version("1.0"); + SwaggerBuilder builder2 = new SwaggerBuilder(dictionary, info2); + Swagger swagger2 = builder2.build(); - Map docs = new HashMap<>(); - docs.put("test", swagger); + List docs = new ArrayList<>(); + docs.add(new DocEndpoint.SwaggerRegistration("test", swagger1)); + docs.add(new DocEndpoint.SwaggerRegistration("test", swagger2)); return docs; } @Override - public void dispose(Map instance) { + public void dispose(List instance) { //NOP } - }).to(new TypeLiteral>() { + }).to(new TypeLiteral>() { }).named("swagger"); } }); diff --git a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/Author.java b/elide-contrib/elide-swagger/src/test/java/example/models/Author.java similarity index 93% rename from elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/Author.java rename to elide-contrib/elide-swagger/src/test/java/example/models/Author.java index 5fe81694a3..6c4f0583c2 100644 --- a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/Author.java +++ b/elide-contrib/elide-swagger/src/test/java/example/models/Author.java @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ -package com.yahoo.elide.contrib.swagger.models; +package example.models; import com.yahoo.elide.annotation.Include; diff --git a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/AuthorType.java b/elide-contrib/elide-swagger/src/test/java/example/models/AuthorType.java similarity index 80% rename from elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/AuthorType.java rename to elide-contrib/elide-swagger/src/test/java/example/models/AuthorType.java index a4a282cb65..0d33c043ef 100644 --- a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/AuthorType.java +++ b/elide-contrib/elide-swagger/src/test/java/example/models/AuthorType.java @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ -package com.yahoo.elide.contrib.swagger.models; +package example.models; public enum AuthorType { SIGNED, diff --git a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/Book.java b/elide-contrib/elide-swagger/src/test/java/example/models/Book.java similarity index 96% rename from elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/Book.java rename to elide-contrib/elide-swagger/src/test/java/example/models/Book.java index 793aab19cb..e82dc8365c 100644 --- a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/Book.java +++ b/elide-contrib/elide-swagger/src/test/java/example/models/Book.java @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ -package com.yahoo.elide.contrib.swagger.models; +package example.models; import com.yahoo.elide.annotation.CreatePermission; import com.yahoo.elide.annotation.DeletePermission; diff --git a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/Publisher.java b/elide-contrib/elide-swagger/src/test/java/example/models/Publisher.java similarity index 96% rename from elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/Publisher.java rename to elide-contrib/elide-swagger/src/test/java/example/models/Publisher.java index 43b4375ea3..71f4695ec1 100644 --- a/elide-contrib/elide-swagger/src/test/java/com/yahoo/elide/contrib/swagger/models/Publisher.java +++ b/elide-contrib/elide-swagger/src/test/java/example/models/Publisher.java @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ -package com.yahoo.elide.contrib.swagger.models; +package example.models; import com.yahoo.elide.annotation.Include; import io.swagger.annotations.ApiModelProperty; diff --git a/elide-contrib/elide-test-helpers/pom.xml b/elide-contrib/elide-test-helpers/pom.xml index 6ea8d1653f..f6ef368c93 100644 --- a/elide-contrib/elide-test-helpers/pom.xml +++ b/elide-contrib/elide-test-helpers/pom.xml @@ -14,7 +14,7 @@ elide-contrib-parent-pom com.yahoo.elide - 4.6.3-SNAPSHOT + 5.0.0-pr9-SNAPSHOT diff --git a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/GraphQLDSL.java b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/GraphQLDSL.java index 4b53d99e1d..ea101ddf7f 100644 --- a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/GraphQLDSL.java +++ b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/GraphQLDSL.java @@ -416,6 +416,10 @@ public static Selection field(String name, Arguments arguments, SelectionSet... return new Field(null, name, arguments, relayWrap(Arrays.asList(selectionSet))); } + public static Selection field(String alias, String name, Arguments arguments, SelectionSet... selectionSet) { + return new Field(alias, name, arguments, relayWrap(Arrays.asList(selectionSet))); + } + /** * Creates an attribute(scalar field) selection. * diff --git a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Field.java b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Field.java index 902d575af3..09e18b329c 100644 --- a/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Field.java +++ b/elide-contrib/elide-test-helpers/src/main/java/com/yahoo/elide/contrib/testhelpers/graphql/elements/Field.java @@ -83,7 +83,7 @@ public String toGraphQLSpec() { @Override public String toResponse() { - if (selectionSet instanceof String) { + if (selectionSet instanceof String || selectionSet instanceof Number) { // scalar response field return String.format( "\"%s\":%s", diff --git a/elide-contrib/pom.xml b/elide-contrib/pom.xml index ed81d56ee2..a4da51c46d 100644 --- a/elide-contrib/pom.xml +++ b/elide-contrib/pom.xml @@ -14,7 +14,7 @@ elide-parent-pom com.yahoo.elide - 4.6.3-SNAPSHOT + 5.0.0-pr9-SNAPSHOT @@ -46,6 +46,7 @@ elide-swagger elide-test-helpers + elide-dynamic-config-helpers @@ -53,7 +54,7 @@ com.yahoo.elide elide-core - 4.6.3-SNAPSHOT + 5.0.0-pr9-SNAPSHOT diff --git a/elide-core/pom.xml b/elide-core/pom.xml index 0ac31db56b..d102c7ec16 100644 --- a/elide-core/pom.xml +++ b/elide-core/pom.xml @@ -14,7 +14,7 @@ com.yahoo.elide elide-parent-pom - 4.6.3-SNAPSHOT + 5.0.0-pr9-SNAPSHOT @@ -183,6 +183,13 @@ test + + com.google.inject + guice + 4.2.2 + test + + ch.qos.logback logback-classic @@ -201,6 +208,7 @@ org.eclipse.jetty jetty-webapp + ${version.jetty} test diff --git a/elide-core/src/main/java/com/yahoo/elide/Elide.java b/elide-core/src/main/java/com/yahoo/elide/Elide.java index 7509d7bdad..07c1394afa 100644 --- a/elide-core/src/main/java/com/yahoo/elide/Elide.java +++ b/elide-core/src/main/java/com/yahoo/elide/Elide.java @@ -8,21 +8,21 @@ import com.yahoo.elide.audit.AuditLogger; import com.yahoo.elide.core.DataStore; import com.yahoo.elide.core.DataStoreTransaction; -import com.yahoo.elide.core.ErrorObjects; import com.yahoo.elide.core.HttpStatus; import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore; -import com.yahoo.elide.core.exceptions.CustomErrorException; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; import com.yahoo.elide.core.exceptions.HttpStatusException; import com.yahoo.elide.core.exceptions.InternalServerErrorException; import com.yahoo.elide.core.exceptions.InvalidConstraintException; import com.yahoo.elide.core.exceptions.InvalidURLException; import com.yahoo.elide.core.exceptions.JsonPatchExtensionException; +import com.yahoo.elide.core.exceptions.TimeoutException; import com.yahoo.elide.core.exceptions.TransactionException; import com.yahoo.elide.core.exceptions.UnableToAddSerdeException; import com.yahoo.elide.extensions.JsonApiPatch; import com.yahoo.elide.extensions.PatchRequestScope; +import com.yahoo.elide.jsonapi.EntityProjectionMaker; import com.yahoo.elide.jsonapi.JsonApiMapper; import com.yahoo.elide.jsonapi.models.JsonApiDocument; import com.yahoo.elide.parsers.BaseVisitor; @@ -50,7 +50,6 @@ import org.apache.commons.collections4.IterableUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; -import org.owasp.encoder.Encode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -161,10 +160,14 @@ protected Set> registerCustomSerdeScan() { * @param opaqueUser the opaque user * @return Elide response object */ - public ElideResponse get(String path, MultivaluedMap queryParams, Object opaqueUser) { + public ElideResponse get(String path, MultivaluedMap queryParams, + User opaqueUser, String apiVersion) { return handleRequest(true, opaqueUser, dataStore::beginReadTransaction, (tx, user) -> { JsonApiDocument jsonApiDoc = new JsonApiDocument(); - RequestScope requestScope = new RequestScope(path, jsonApiDoc, tx, user, queryParams, elideSettings); + RequestScope requestScope = new RequestScope(path, apiVersion, jsonApiDoc, + tx, user, queryParams, elideSettings); + requestScope.setEntityProjection(new EntityProjectionMaker(elideSettings.getDictionary(), + requestScope).parsePath(path)); BaseVisitor visitor = new GetVisitor(requestScope); return visit(path, requestScope, visitor); }); @@ -179,10 +182,13 @@ public ElideResponse get(String path, MultivaluedMap queryParams * @param opaqueUser the opaque user * @return Elide response object */ - public ElideResponse post(String path, String jsonApiDocument, Object opaqueUser) { + public ElideResponse post(String path, String jsonApiDocument, User opaqueUser, String apiVersion) { return handleRequest(false, opaqueUser, dataStore::beginTransaction, (tx, user) -> { JsonApiDocument jsonApiDoc = mapper.readJsonApiDocument(jsonApiDocument); - RequestScope requestScope = new RequestScope(path, jsonApiDoc, tx, user, null, elideSettings); + RequestScope requestScope = new RequestScope(path, apiVersion, + jsonApiDoc, tx, user, null, elideSettings); + requestScope.setEntityProjection(new EntityProjectionMaker(elideSettings.getDictionary(), + requestScope).parsePath(path)); BaseVisitor visitor = new PostVisitor(requestScope); return visit(path, requestScope, visitor); }); @@ -199,12 +205,12 @@ public ElideResponse post(String path, String jsonApiDocument, Object opaqueUser * @return Elide response object */ public ElideResponse patch(String contentType, String accept, - String path, String jsonApiDocument, Object opaqueUser) { + String path, String jsonApiDocument, User opaqueUser, String apiVersion) { Handler handler; if (JsonApiPatch.isPatchExtension(contentType) && JsonApiPatch.isPatchExtension(accept)) { handler = (tx, user) -> { - PatchRequestScope requestScope = new PatchRequestScope(path, tx, user, elideSettings); + PatchRequestScope requestScope = new PatchRequestScope(path, apiVersion, tx, user, elideSettings); try { Supplier> responder = JsonApiPatch.processJsonPatch(dataStore, path, jsonApiDocument, requestScope); @@ -216,7 +222,10 @@ public ElideResponse patch(String contentType, String accept, } else { handler = (tx, user) -> { JsonApiDocument jsonApiDoc = mapper.readJsonApiDocument(jsonApiDocument); - RequestScope requestScope = new RequestScope(path, jsonApiDoc, tx, user, null, elideSettings); + RequestScope requestScope = new RequestScope(path, apiVersion, jsonApiDoc, + tx, user, null, elideSettings); + requestScope.setEntityProjection(new EntityProjectionMaker(elideSettings.getDictionary(), + requestScope).parsePath(path)); BaseVisitor visitor = new PatchVisitor(requestScope); return visit(path, requestScope, visitor); }; @@ -233,12 +242,15 @@ public ElideResponse patch(String contentType, String accept, * @param opaqueUser the opaque user * @return Elide response object */ - public ElideResponse delete(String path, String jsonApiDocument, Object opaqueUser) { + public ElideResponse delete(String path, String jsonApiDocument, User opaqueUser, String apiVersion) { return handleRequest(false, opaqueUser, dataStore::beginTransaction, (tx, user) -> { JsonApiDocument jsonApiDoc = StringUtils.isEmpty(jsonApiDocument) ? new JsonApiDocument() : mapper.readJsonApiDocument(jsonApiDocument); - RequestScope requestScope = new RequestScope(path, jsonApiDoc, tx, user, null, elideSettings); + RequestScope requestScope = new RequestScope(path, apiVersion, jsonApiDoc, + tx, user, null, elideSettings); + requestScope.setEntityProjection(new EntityProjectionMaker(elideSettings.getDictionary(), + requestScope).parsePath(path)); BaseVisitor visitor = new DeleteVisitor(requestScope); return visit(path, requestScope, visitor); }); @@ -257,17 +269,16 @@ public HandlerResult visit(String path, RequestScope requestScope, BaseVisitor v * Handle JSON API requests. * * @param isReadOnly if the transaction is read only - * @param opaqueUser the user object from the container + * @param user the user object from the container * @param transaction a transaction supplier * @param handler a function that creates the request scope and request handler * @return the response */ - protected ElideResponse handleRequest(boolean isReadOnly, Object opaqueUser, + protected ElideResponse handleRequest(boolean isReadOnly, User user, Supplier transaction, Handler handler) { boolean isVerbose = false; try (DataStoreTransaction tx = transaction.get()) { - final User user = tx.accessUser(opaqueUser); HandlerResult result = handler.handle(tx, user); RequestScope requestScope = result.getRequestScope(); isVerbose = requestScope.getPermissionExecutor().isVerbose(); @@ -284,7 +295,7 @@ protected ElideResponse handleRequest(boolean isReadOnly, Object opaqueUser, ElideResponse response = buildResponse(responder.get()); - auditLogger.commit(requestScope); + auditLogger.commit(); tx.commit(requestScope); requestScope.runQueuedPostCommitTriggers(); @@ -327,8 +338,11 @@ protected ElideResponse handleRequest(boolean isReadOnly, Object opaqueUser, message = IterableUtils.first(e.getConstraintViolations()).getMessage(); } return buildErrorResponse(new InvalidConstraintException(message), isVerbose); - } catch (Exception | Error e) { + if (e instanceof InterruptedException) { + log.debug("Request Thread interrupted.", e); + return buildErrorResponse(new TimeoutException(e), isVerbose); + } log.error("Error or exception uncaught by Elide", e); throw e; @@ -342,19 +356,8 @@ protected ElideResponse buildErrorResponse(HttpStatusException error, boolean is log.error("Internal Server Error", error); } - boolean encodeErrorResponse = elideSettings.isEncodeErrorResponses(); - if (!(error instanceof CustomErrorException) && elideSettings.isReturnErrorObjects()) { - String errorDetail = isVerbose ? error.getVerboseMessage() : error.toString(); - if (encodeErrorResponse) { - errorDetail = Encode.forHtml(errorDetail); - } - ErrorObjects errors = ErrorObjects.builder().addError().withDetail(errorDetail).build(); - JsonNode responseBody = mapper.getObjectMapper().convertValue(errors, JsonNode.class); - return buildResponse(Pair.of(error.getStatus(), responseBody)); - } - - return buildResponse(isVerbose ? error.getVerboseErrorResponse(encodeErrorResponse) - : error.getErrorResponse(encodeErrorResponse)); + return buildResponse(isVerbose ? error.getVerboseErrorResponse() + : error.getErrorResponse()); } protected ElideResponse buildResponse(Pair response) { diff --git a/elide-core/src/main/java/com/yahoo/elide/ElideSettings.java b/elide-core/src/main/java/com/yahoo/elide/ElideSettings.java index 6b2dadeb6e..bd64e8417e 100644 --- a/elide-core/src/main/java/com/yahoo/elide/ElideSettings.java +++ b/elide-core/src/main/java/com/yahoo/elide/ElideSettings.java @@ -36,9 +36,6 @@ public class ElideSettings { @Getter private final List subqueryFilterDialects; @Getter private final int defaultMaxPageSize; @Getter private final int defaultPageSize; - @Getter private final boolean useFilterExpressions; @Getter private final int updateStatusCode; - @Getter private final boolean returnErrorObjects; @Getter private final Map serdes; - @Getter private final boolean encodeErrorResponses; } diff --git a/elide-core/src/main/java/com/yahoo/elide/ElideSettingsBuilder.java b/elide-core/src/main/java/com/yahoo/elide/ElideSettingsBuilder.java index e898f6e85c..f824d28a8b 100644 --- a/elide-core/src/main/java/com/yahoo/elide/ElideSettingsBuilder.java +++ b/elide-core/src/main/java/com/yahoo/elide/ElideSettingsBuilder.java @@ -15,16 +15,15 @@ import com.yahoo.elide.core.filter.dialect.JoinFilterDialect; import com.yahoo.elide.core.filter.dialect.RSQLFilterDialect; import com.yahoo.elide.core.filter.dialect.SubqueryFilterDialect; -import com.yahoo.elide.core.pagination.Pagination; +import com.yahoo.elide.core.pagination.PaginationImpl; import com.yahoo.elide.jsonapi.JsonApiMapper; import com.yahoo.elide.security.PermissionExecutor; import com.yahoo.elide.security.executors.ActivePermissionExecutor; +import com.yahoo.elide.security.executors.VerbosePermissionExecutor; import com.yahoo.elide.utils.coerce.converters.EpochToDateConverter; import com.yahoo.elide.utils.coerce.converters.ISO8601DateSerde; import com.yahoo.elide.utils.coerce.converters.Serde; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -45,12 +44,9 @@ public class ElideSettingsBuilder { private List joinFilterDialects; private List subqueryFilterDialects; private Map serdes; - private int defaultMaxPageSize = Pagination.MAX_PAGE_LIMIT; - private int defaultPageSize = Pagination.DEFAULT_PAGE_LIMIT; - private boolean useFilterExpressions; + private int defaultMaxPageSize = PaginationImpl.MAX_PAGE_LIMIT; + private int defaultPageSize = PaginationImpl.DEFAULT_PAGE_LIMIT; private int updateStatusCode; - private boolean returnErrorObjects; - private boolean encodeErrorResponses; /** * A new builder used to generate Elide instances. Instantiates an {@link EntityDictionary} without @@ -92,11 +88,8 @@ public ElideSettings build() { subqueryFilterDialects, defaultMaxPageSize, defaultPageSize, - useFilterExpressions, updateStatusCode, - returnErrorObjects, - serdes, - encodeErrorResponses); + serdes); } public ElideSettingsBuilder withAuditLogger(AuditLogger auditLogger) { @@ -114,33 +107,6 @@ public ElideSettingsBuilder withJsonApiMapper(JsonApiMapper jsonApiMapper) { return this; } - public ElideSettingsBuilder withPermissionExecutor( - Function permissionExecutorFunction) { - this.permissionExecutorFunction = permissionExecutorFunction; - return this; - } - - public ElideSettingsBuilder withPermissionExecutor(Class permissionExecutorClass) { - permissionExecutorFunction = (requestScope) -> { - try { - try { - // Try to find a constructor with request scope - Constructor ctor = - permissionExecutorClass.getDeclaredConstructor(RequestScope.class); - return ctor.newInstance(requestScope); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException - | InstantiationException e) { - // If that fails, try blank constructor - return permissionExecutorClass.newInstance(); - } - } catch (IllegalAccessException | InstantiationException e) { - // Everything failed. Throw hands up, not sure how to proceed. - throw new RuntimeException(e); - } - }; - return this; - } - public ElideSettingsBuilder withJoinFilterDialect(JoinFilterDialect dialect) { joinFilterDialects.add(dialect); return this; @@ -171,8 +137,8 @@ public ElideSettingsBuilder withUpdate204Status() { return this; } - public ElideSettingsBuilder withUseFilterExpressions(boolean useFilterExpressions) { - this.useFilterExpressions = useFilterExpressions; + public ElideSettingsBuilder withVerboseErrors() { + permissionExecutorFunction = VerbosePermissionExecutor::new; return this; } @@ -191,14 +157,4 @@ public ElideSettingsBuilder withEpochDates() { serdes.put(java.sql.Timestamp.class, new EpochToDateConverter(java.sql.Timestamp.class)); return this; } - - public ElideSettingsBuilder withReturnErrorObjects(boolean returnErrorObjects) { - this.returnErrorObjects = returnErrorObjects; - return this; - } - - public ElideSettingsBuilder withEncodeErrorResponses(boolean encodeErrorResponses) { - this.encodeErrorResponses = encodeErrorResponses; - return this; - } } diff --git a/elide-core/src/main/java/com/yahoo/elide/audit/AuditLogger.java b/elide-core/src/main/java/com/yahoo/elide/audit/AuditLogger.java index be5c78721d..a3e5683c11 100644 --- a/elide-core/src/main/java/com/yahoo/elide/audit/AuditLogger.java +++ b/elide-core/src/main/java/com/yahoo/elide/audit/AuditLogger.java @@ -5,8 +5,6 @@ */ package com.yahoo.elide.audit; -import com.yahoo.elide.core.RequestScope; - import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -27,7 +25,7 @@ public void log(LogMessage message) { messages.get().add(message); } - public abstract void commit(RequestScope requestScope) throws IOException; + public abstract void commit() throws IOException; public void clear() { List remainingMessages = messages.get(); diff --git a/elide-core/src/main/java/com/yahoo/elide/audit/LogMessage.java b/elide-core/src/main/java/com/yahoo/elide/audit/LogMessage.java index 6b821b0ecd..18e383de07 100644 --- a/elide-core/src/main/java/com/yahoo/elide/audit/LogMessage.java +++ b/elide-core/src/main/java/com/yahoo/elide/audit/LogMessage.java @@ -1,202 +1,55 @@ /* - * Copyright 2015, Yahoo Inc. + * Copyright 2019, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ + package com.yahoo.elide.audit; -import com.yahoo.elide.annotation.Audit; -import com.yahoo.elide.core.PersistentResource; -import com.yahoo.elide.core.RequestScope; -import com.yahoo.elide.core.ResourceLineage; import com.yahoo.elide.security.ChangeSpec; - +import com.yahoo.elide.security.PersistentResource; import com.yahoo.elide.security.User; -import de.odysseus.el.ExpressionFactoryImpl; -import de.odysseus.el.util.SimpleContext; -import java.text.MessageFormat; -import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; - -import javax.el.ELException; -import javax.el.ExpressionFactory; -import javax.el.PropertyNotFoundException; -import javax.el.ValueExpression; /** - * An audit log message that can be logged to a logger. + * Elide audit entity for a CRUD action. */ -public class LogMessage { - //Supposedly this is thread safe. - private static final ExpressionFactory EXPRESSION_FACTORY = new ExpressionFactoryImpl(); - private static final String[] EMPTY_STRING_ARRAY = new String[0]; - - private final String template; - private final PersistentResource record; - private final String[] expressions; - private final int operationCode; - private final Optional changeSpec; +public interface LogMessage { /** - * Construct a log message that does not involve any templating. - * @param template - The unsubstituted text that will be logged. - * @param code - The operation code of the auditable action. + * Gets message. + * + * @return the message */ - public LogMessage(String template, int code) { - this(template, null, EMPTY_STRING_ARRAY, code, Optional.empty()); - } + public String getMessage(); /** - * Construct a log message from an Audit annotation and the record that was updated in some way. - * @param audit - The annotation containing the type of operation (UPDATE, DELETE, CREATE) - * @param record - The modified record - * @param changeSpec - Change spec of modified elements (if logging object change). empty otherwise - * @throws InvalidSyntaxException if the Audit annotation has invalid syntax. + * Gets operation code. The operation code is assigned by the developer to uniquely identify + * the type of change that is being audited. Operation code definitions are outside the scope of Elide. + * + * @return the operation code */ - public LogMessage(Audit audit, PersistentResource record, Optional changeSpec) - throws InvalidSyntaxException { - this(audit.logStatement(), record, audit.logExpressions(), audit.operation(), changeSpec); - } + public int getOperationCode(); /** - * Construct a log message. - * @param template - The log message template that requires variable substitution. - * @param record - The record which will serve as the data to substitute. - * @param expressions - A set of UEL expressions that reference record. - * @param code - The operation code of the auditable action. - * @param changeSpec - the change spec that we want to log - * @throws InvalidSyntaxException the invalid syntax exception + * Get the user principal associated with the request. + * + * @return the user principal. */ - public LogMessage(String template, - PersistentResource record, - String[] expressions, - int code, - Optional changeSpec) throws InvalidSyntaxException { - this.template = template; - this.record = record; - this.expressions = expressions; - this.operationCode = code; - this.changeSpec = changeSpec; - } + public User getUser(); /** - * Gets operation code. + * Get the change specification * - * @return the operation code + * @return the change specification. */ - public int getOperationCode() { - return operationCode; - } + public Optional getChangeSpec(); /** - * Gets message. + * Get the resource that was manipulated. * - * @return the message + * @return the resource. */ - public String getMessage() { - final SimpleContext ctx = new SimpleContext(); - final SimpleContext singleElementContext = new SimpleContext(); - - if (record != null) { - /* Create a new lineage which includes the passed in record */ - ResourceLineage lineage = new ResourceLineage(record.getLineage(), record); - - for (String name : lineage.getKeys()) { - List values = lineage.getRecord(name); - - final ValueExpression expression; - final ValueExpression singleElementExpression; - if (values.size() == 1) { - expression = EXPRESSION_FACTORY.createValueExpression(values.get(0).getObject(), Object.class); - singleElementExpression = expression; - } else { - List objects = values.stream().map(PersistentResource::getObject) - .collect(Collectors.toList()); - expression = EXPRESSION_FACTORY.createValueExpression(objects, List.class); - singleElementExpression = EXPRESSION_FACTORY.createValueExpression(values.get(values.size() - 1) - .getObject(), Object.class); - } - ctx.setVariable(name, expression); - singleElementContext.setVariable(name, singleElementExpression); - } - - final Object user = getUser(); - if (user != null) { - final ValueExpression opaqueUserValueExpression = EXPRESSION_FACTORY - .createValueExpression( - user, Object.class - ); - ctx.setVariable("opaqueUser", opaqueUserValueExpression); - singleElementContext.setVariable("opaqueUser", opaqueUserValueExpression); - } - } - - Object[] results = new Object[expressions.length]; - for (int idx = 0; idx < results.length; idx++) { - String expressionText = expressions[idx]; - - final ValueExpression expression; - final ValueExpression singleElementExpression; - try { - expression = EXPRESSION_FACTORY.createValueExpression(ctx, expressionText, Object.class); - singleElementExpression = - EXPRESSION_FACTORY.createValueExpression(singleElementContext, expressionText, Object.class); - } catch (ELException e) { - throw new InvalidSyntaxException(e); - } - - Object result; - try { - // Single element expressions are intended to allow for access to ${entityType.field} when there are - // multiple "entityType" types listed in the lineage. Without this, any access to an entityType - // without an explicit list index would otherwise result in a 500. Similarly, since we already - // supported lists (i.e. the ${entityType[idx].field} syntax), this also continues to support that. - // It should be noted, however, that list indexing is somewhat brittle unless properly accounted for - // from all possible paths. - result = singleElementExpression.getValue(singleElementContext); - } catch (PropertyNotFoundException e) { - // Try list syntax if not single element - result = expression.getValue(ctx); - } - results[idx] = result; - } - - try { - return MessageFormat.format(template, results); - } catch (IllegalArgumentException e) { - throw new InvalidSyntaxException(e); - } - } - - public RequestScope getRequestScope() { - if (record != null) { - return record.getRequestScope(); - } - return null; - } - - public Object getUser() { - RequestScope requestScope = getRequestScope(); - if (requestScope != null) { - User user = requestScope.getUser(); - if (user != null) { - return user.getOpaqueUser(); - } - } - return null; - } - - public Optional getChangeSpec() { - return changeSpec; - } - - @Override - public String toString() { - return "LogMessage{" - + "message='" + getMessage() + '\'' - + ", operationCode=" + getOperationCode() - + '}'; - } + public PersistentResource getPersistentResource(); } diff --git a/elide-core/src/main/java/com/yahoo/elide/audit/LogMessageImpl.java b/elide-core/src/main/java/com/yahoo/elide/audit/LogMessageImpl.java new file mode 100644 index 0000000000..637797e168 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/audit/LogMessageImpl.java @@ -0,0 +1,176 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.audit; + +import com.yahoo.elide.annotation.Audit; +import com.yahoo.elide.core.ResourceLineage; +import com.yahoo.elide.security.ChangeSpec; +import com.yahoo.elide.security.PersistentResource; +import com.yahoo.elide.security.User; + +import de.odysseus.el.ExpressionFactoryImpl; +import de.odysseus.el.util.SimpleContext; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +import java.security.Principal; +import java.text.MessageFormat; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.el.ELException; +import javax.el.ExpressionFactory; +import javax.el.PropertyNotFoundException; +import javax.el.ValueExpression; + +/** + * An audit log message that can be logged to a logger. + */ +@ToString +@EqualsAndHashCode +public class LogMessageImpl implements LogMessage { + //Supposedly this is thread safe. + private static final ExpressionFactory EXPRESSION_FACTORY = new ExpressionFactoryImpl(); + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private final String template; + private final String[] expressions; + + @Getter + private final int operationCode; + + @Getter + private final Optional changeSpec; + + @Getter + private final User user; + + @Getter + private final PersistentResource persistentResource; + + /** + * Construct a log message that does not involve any templating. + * @param template - The unsubstituted text that will be logged. + * @param code - The operation code of the auditable action. + */ + public LogMessageImpl(String template, int code) { + this(template, null, EMPTY_STRING_ARRAY, code, Optional.empty()); + } + + /** + * Construct a log message from an Audit annotation and the record that was updated in some way. + * @param audit - The annotation containing the type of operation (UPDATE, DELETE, CREATE) + * @param record - The modified record + * @param changeSpec - Change spec of modified elements (if logging object change). empty otherwise + * @throws InvalidSyntaxException if the Audit annotation has invalid syntax. + */ + public LogMessageImpl(Audit audit, PersistentResource record, Optional changeSpec) + throws InvalidSyntaxException { + this(audit.logStatement(), record, audit.logExpressions(), audit.operation(), changeSpec); + } + + /** + * Construct a log message. + * @param template - The log message template that requires variable substitution. + * @param record - The record which will serve as the data to substitute. + * @param expressions - A set of UEL expressions that reference record. + * @param code - The operation code of the auditable action. + * @param changeSpec - the change spec that we want to log + * @throws InvalidSyntaxException the invalid syntax exception + */ + public LogMessageImpl(String template, + PersistentResource record, + String[] expressions, + int code, + Optional changeSpec) throws InvalidSyntaxException { + this.template = template; + this.persistentResource = record; + this.expressions = expressions; + this.operationCode = code; + this.changeSpec = changeSpec; + this.user = (record == null ? null : record.getRequestScope().getUser()); + } + + @Override + public String getMessage() { + final SimpleContext ctx = new SimpleContext(); + final SimpleContext singleElementContext = new SimpleContext(); + + if (persistentResource != null) { + /* Create a new lineage which includes the passed in record */ + com.yahoo.elide.core.PersistentResource internalResource = ( + com.yahoo.elide.core.PersistentResource) persistentResource; + ResourceLineage lineage = new ResourceLineage(internalResource.getLineage(), internalResource); + + for (String name : lineage.getKeys()) { + List values = lineage.getRecord(name); + + final ValueExpression expression; + final ValueExpression singleElementExpression; + if (values.size() == 1) { + expression = EXPRESSION_FACTORY.createValueExpression(values.get(0).getObject(), Object.class); + singleElementExpression = expression; + } else { + List objects = values.stream().map(PersistentResource::getObject) + .collect(Collectors.toList()); + expression = EXPRESSION_FACTORY.createValueExpression(objects, List.class); + singleElementExpression = EXPRESSION_FACTORY.createValueExpression(values.get(values.size() - 1) + .getObject(), Object.class); + } + ctx.setVariable(name, expression); + singleElementContext.setVariable(name, singleElementExpression); + } + + final Principal user = getUser().getPrincipal(); + if (user != null) { + final ValueExpression opaqueUserValueExpression = EXPRESSION_FACTORY + .createValueExpression( + user, Object.class + ); + ctx.setVariable("opaqueUser", opaqueUserValueExpression); + singleElementContext.setVariable("opaqueUser", opaqueUserValueExpression); + } + } + + Object[] results = new Object[expressions.length]; + for (int idx = 0; idx < results.length; idx++) { + String expressionText = expressions[idx]; + + final ValueExpression expression; + final ValueExpression singleElementExpression; + try { + expression = EXPRESSION_FACTORY.createValueExpression(ctx, expressionText, Object.class); + singleElementExpression = + EXPRESSION_FACTORY.createValueExpression(singleElementContext, expressionText, Object.class); + } catch (ELException e) { + throw new InvalidSyntaxException(e); + } + + Object result; + try { + // Single element expressions are intended to allow for access to ${entityType.field} when there are + // multiple "entityType" types listed in the lineage. Without this, any access to an entityType + // without an explicit list index would otherwise result in a 500. Similarly, since we already + // supported lists (i.e. the ${entityType[idx].field} syntax), this also continues to support that. + // It should be noted, however, that list indexing is somewhat brittle unless properly accounted for + // from all possible paths. + result = singleElementExpression.getValue(singleElementContext); + } catch (PropertyNotFoundException e) { + // Try list syntax if not single element + result = expression.getValue(ctx); + } + results[idx] = result; + } + + try { + return MessageFormat.format(template, results); + } catch (IllegalArgumentException e) { + throw new InvalidSyntaxException(e); + } + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/audit/Slf4jLogger.java b/elide-core/src/main/java/com/yahoo/elide/audit/Slf4jLogger.java index f649c369f5..181e1b2f73 100644 --- a/elide-core/src/main/java/com/yahoo/elide/audit/Slf4jLogger.java +++ b/elide-core/src/main/java/com/yahoo/elide/audit/Slf4jLogger.java @@ -5,10 +5,7 @@ */ package com.yahoo.elide.audit; -import com.yahoo.elide.core.RequestScope; - import lombok.extern.slf4j.Slf4j; - import java.io.IOException; /** @@ -18,7 +15,7 @@ public class Slf4jLogger extends AuditLogger { @Override - public void commit(RequestScope requestScope) throws IOException { + public void commit() throws IOException { try { for (LogMessage message : messages.get()) { log.info("{} {} {}", System.currentTimeMillis(), message.getOperationCode(), message.getMessage()); diff --git a/elide-core/src/main/java/com/yahoo/elide/core/ArgumentType.java b/elide-core/src/main/java/com/yahoo/elide/core/ArgumentType.java new file mode 100644 index 0000000000..ffe0773280 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/ArgumentType.java @@ -0,0 +1,23 @@ +/* + * Copyright 2015, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core; + +import lombok.Getter; + +/** + * Argument Type wraps an argument to the type of value it accepts. + */ +public class ArgumentType { + @Getter + private String name; + @Getter + private Class type; + + public ArgumentType(String name, Class type) { + this.name = name; + this.type = type; + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/CRUDEvent.java b/elide-core/src/main/java/com/yahoo/elide/core/CRUDEvent.java index e275d7b1aa..dbf5e4c880 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/CRUDEvent.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/CRUDEvent.java @@ -6,6 +6,12 @@ package com.yahoo.elide.core; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.CREATE; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.DELETE; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.READ; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.UPDATE; + +import com.yahoo.elide.annotation.LifeCycleHookBinding; import com.yahoo.elide.security.ChangeSpec; import lombok.AllArgsConstructor; @@ -19,31 +25,24 @@ @Data @AllArgsConstructor public class CRUDEvent { - private CRUDAction eventType; + private LifeCycleHookBinding.Operation eventType; private PersistentResource resource; private String fieldName; private Optional changes; public boolean isCreateEvent() { - return eventType == CRUDAction.CREATE; + return eventType == CREATE; } public boolean isUpdateEvent() { - return eventType == CRUDAction.UPDATE; + return eventType == UPDATE; } public boolean isDeleteEvent() { - return eventType == CRUDAction.DELETE; + return eventType == DELETE; } public boolean isReadEvent() { - return eventType == CRUDAction.READ; - } - - /** - * Enum describing possible CRUD actions. - */ - public static enum CRUDAction { - CREATE, READ, UPDATE, DELETE + return eventType == READ; } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/CheckInstantiator.java b/elide-core/src/main/java/com/yahoo/elide/core/CheckInstantiator.java index 43773fe074..c1b25701ea 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/CheckInstantiator.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/CheckInstantiator.java @@ -6,6 +6,7 @@ package com.yahoo.elide.core; +import com.yahoo.elide.Injector; import com.yahoo.elide.security.checks.Check; import java.util.Objects; @@ -25,7 +26,7 @@ public interface CheckInstantiator { */ default Check getCheck(EntityDictionary dictionary, String checkName) { Class checkCls = dictionary.getCheck(checkName); - return instantiateCheck(checkCls); + return instantiateCheck(checkCls, dictionary.getInjector()); } /** @@ -34,9 +35,11 @@ default Check getCheck(EntityDictionary dictionary, String checkName) { * @return the instance of the check * @throws IllegalArgumentException if the check class cannot be instantiated with a zero argument constructor */ - default Check instantiateCheck(Class checkCls) { + default Check instantiateCheck(Class checkCls, Injector injector) { try { - return Objects.requireNonNull(checkCls).newInstance(); + Check check = Objects.requireNonNull(checkCls).newInstance(); + injector.inject(check); + return check; } catch (InstantiationException | IllegalAccessException | NullPointerException e) { String checkName = (checkCls != null) ? checkCls.getName() : "null"; throw new IllegalArgumentException("Could not instantiate specified check '" + checkName + "'.", e); diff --git a/elide-core/src/main/java/com/yahoo/elide/core/DataStoreTransaction.java b/elide-core/src/main/java/com/yahoo/elide/core/DataStoreTransaction.java index 9286603661..8225c74adc 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/DataStoreTransaction.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/DataStoreTransaction.java @@ -8,14 +8,14 @@ import com.yahoo.elide.core.filter.InPredicate; import com.yahoo.elide.core.filter.expression.AndFilterExpression; import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; -import com.yahoo.elide.security.User; +import com.yahoo.elide.request.Attribute; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Relationship; +import com.yahoo.elide.request.Sorting; import java.io.Closeable; import java.io.Serializable; import java.util.Iterator; -import java.util.Optional; import java.util.Set; /** @@ -32,16 +32,6 @@ public enum FeatureSupport { NONE } - /** - * Wrap the opaque user. - * - * @param opaqueUser the opaque user - * @return wrapped user context - */ - default User accessUser(Object opaqueUser) { - return new User(opaqueUser); - } - /** * Save the updated object. * @@ -112,19 +102,21 @@ default T createNewObject(Class entityClass) { } /** - * Loads an object by ID. + * Loads an object by ID. The reason we support both load by ID and load by filter is that + * some legacy stores are optimized to load by ID. * - * @param entityClass the type of class to load + * @param entityProjection the collection to load. * @param id - the ID of the object to load. - * @param filterExpression - security filters that can be evaluated in the data store. * @param scope - the current request scope * It is optional for the data store to attempt evaluation. * @return the loaded object if it exists AND any provided security filters pass. */ - default Object loadObject(Class entityClass, - Serializable id, - Optional filterExpression, - RequestScope scope) { + default Object loadObject(EntityProjection entityProjection, + Serializable id, + RequestScope scope) { + Class entityClass = entityProjection.getType(); + FilterExpression filterExpression = entityProjection.getFilterExpression(); + EntityDictionary dictionary = scope.getDictionary(); Class idType = dictionary.getIdType(entityClass); String idField = dictionary.getIdFieldName(entityClass); @@ -132,14 +124,15 @@ default Object loadObject(Class entityClass, new Path.PathElement(entityClass, idType, idField), id ); - FilterExpression joinedFilterExpression = filterExpression - .map(fe -> (FilterExpression) new AndFilterExpression(idFilter, fe)) - .orElse(idFilter); - Iterable results = loadObjects(entityClass, - Optional.of(joinedFilterExpression), - Optional.empty(), - Optional.empty(), + FilterExpression joinedFilterExpression = (filterExpression != null) + ? new AndFilterExpression(idFilter, filterExpression) + : idFilter; + + Iterable results = loadObjects(entityProjection.copyOf() + .filterExpression(joinedFilterExpression) + .build(), scope); + Iterator it = results == null ? null : results.iterator(); if (it != null && it.hasNext()) { return it.next(); @@ -150,19 +143,12 @@ default Object loadObject(Class entityClass, /** * Loads a collection of objects. * - * @param entityClass - the class to load - * @param filterExpression - filters that can be evaluated in the data store. - * It is optional for the data store to attempt evaluation. - * @param sorting - sorting which can be pushed down to the data store. - * @param pagination - pagination which can be pushed down to the data store. + * @param entityProjection - the class to load * @param scope - contains request level metadata. * @return a collection of the loaded objects */ Iterable loadObjects( - Class entityClass, - Optional filterExpression, - Optional sorting, - Optional pagination, + EntityProjection entityProjection, RequestScope scope); /** @@ -170,25 +156,18 @@ Iterable loadObjects( * * @param relationTx - The datastore that governs objects of the relationhip's type. * @param entity - The object which owns the relationship. - * @param relationName - name of the relationship. - * @param filterExpression - filtering which can be pushed down to the data store. - * It is optional for the data store to attempt evaluation. - * @param sorting - sorting which can be pushed down to the data store. - * @param pagination - pagination which can be pushed down to the data store. + * @param relationship - the relationship to fetch. * @param scope - contains request level metadata. * @return the object in the relation */ default Object getRelation( DataStoreTransaction relationTx, Object entity, - String relationName, - Optional filterExpression, - Optional sorting, - Optional pagination, + Relationship relationship, RequestScope scope) { - return PersistentResource.getValue(entity, relationName, scope); - } + return PersistentResource.getValue(entity, relationship.getName(), scope); + } /** * Elide core will update the in memory representation of the objects to the requested state. @@ -230,14 +209,14 @@ default void updateToOneRelation(DataStoreTransaction relationTx, * Get an attribute from an object. * * @param entity - The object which owns the attribute. - * @param attributeName - name of the attribute. + * @param attribute - The attribute to fetch * @param scope - contains request level metadata. * @return the value of the attribute */ default Object getAttribute(Object entity, - String attributeName, + Attribute attribute, RequestScope scope) { - return PersistentResource.getValue(entity, attributeName, scope); + return PersistentResource.getValue(entity, attribute.getName(), scope); } @@ -248,13 +227,11 @@ default Object getAttribute(Object entity, * This function allow a data store to optionally persist the attribute if needed. * * @param entity - The object which owns the attribute. - * @param attributeName - name of the attribute. - * @param attributeValue - the desired attribute value. + * @param attribute - the attribute to set. * @param scope - contains request level metadata. */ default void setAttribute(Object entity, - String attributeName, - Object attributeValue, + Attribute attribute, RequestScope scope) { } @@ -270,7 +247,7 @@ default FeatureSupport supportsFiltering(Class entityClass, FilterExpression /** * Whether or not the transaction can sort the provided class. - * @param entityClass + * @param entityClass The entity class that is being sorted. * @return true if sorting is possible */ default boolean supportsSorting(Class entityClass, Sorting sorting) { @@ -279,10 +256,11 @@ default boolean supportsSorting(Class entityClass, Sorting sorting) { /** * Whether or not the transaction can paginate the provided class. - * @param entityClass + * @param entityClass The entity class that is being paged. + * @param expression The filter expression * @return true if pagination is possible */ - default boolean supportsPagination(Class entityClass) { + default boolean supportsPagination(Class entityClass, FilterExpression expression) { return true; } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/EntityBinding.java b/elide-core/src/main/java/com/yahoo/elide/core/EntityBinding.java index ab35dac888..3ae883bace 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/EntityBinding.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/EntityBinding.java @@ -5,38 +5,28 @@ */ package com.yahoo.elide.core; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; import static com.yahoo.elide.core.EntityDictionary.REGULAR_ID_NAME; +import com.yahoo.elide.Injector; import com.yahoo.elide.annotation.ComputedAttribute; import com.yahoo.elide.annotation.ComputedRelationship; import com.yahoo.elide.annotation.Exclude; -import com.yahoo.elide.annotation.OnCreatePostCommit; -import com.yahoo.elide.annotation.OnCreatePreCommit; -import com.yahoo.elide.annotation.OnCreatePreSecurity; -import com.yahoo.elide.annotation.OnDeletePostCommit; -import com.yahoo.elide.annotation.OnDeletePreCommit; -import com.yahoo.elide.annotation.OnDeletePreSecurity; -import com.yahoo.elide.annotation.OnReadPostCommit; -import com.yahoo.elide.annotation.OnReadPreCommit; -import com.yahoo.elide.annotation.OnReadPreSecurity; -import com.yahoo.elide.annotation.OnUpdatePostCommit; -import com.yahoo.elide.annotation.OnUpdatePreCommit; -import com.yahoo.elide.annotation.OnUpdatePreSecurity; +import com.yahoo.elide.annotation.LifeCycleHookBinding; import com.yahoo.elide.annotation.ToMany; import com.yahoo.elide.annotation.ToOne; import com.yahoo.elide.core.exceptions.DuplicateMappingException; import com.yahoo.elide.functions.LifeCycleHook; -import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.reflect.TypeUtils; import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; import lombok.Getter; -import lombok.Setter; import java.lang.annotation.Annotation; import java.lang.reflect.AccessibleObject; @@ -51,14 +41,16 @@ import java.util.Collection; import java.util.Collections; import java.util.Deque; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; - +import javax.inject.Inject; import javax.persistence.AccessType; import javax.persistence.CascadeType; import javax.persistence.Column; @@ -83,6 +75,7 @@ public class EntityBinding { Arrays.asList(ManyToMany.class, ManyToOne.class, OneToMany.class, OneToOne.class, ToOne.class, ToMany.class); + @Getter public final Class entityClass; public final String jsonApiType; public final String entityName; @@ -95,14 +88,19 @@ public class EntityBinding { @Getter private Class idType; @Getter - @Setter - private Initializer initializer; - @Getter private AccessType accessType; + @Getter + private final boolean injected; + + private EntityDictionary dictionary; + + @Getter + private String apiVersion; + public final EntityPermissions entityPermissions; - public final List attributes; - public final List relationships; + public final List apiAttributes; + public final List apiRelationships; public final List> inheritedTypes; public final ConcurrentLinkedDeque attributesDeque = new ConcurrentLinkedDeque<>(); public final ConcurrentLinkedDeque relationshipsDeque = new ConcurrentLinkedDeque<>(); @@ -111,39 +109,78 @@ public class EntityBinding { public final ConcurrentHashMap relationshipToInverse = new ConcurrentHashMap<>(); public final ConcurrentHashMap relationshipToCascadeTypes = new ConcurrentHashMap<>(); public final ConcurrentHashMap fieldsToValues = new ConcurrentHashMap<>(); - public final MultiValuedMap, LifeCycleHook> fieldsToTriggers = new HashSetValuedHashMap<>(); - public final MultiValuedMap classToTriggers = new HashSetValuedHashMap<>(); + public final MultiValuedMap, + LifeCycleHook> fieldTriggers = new HashSetValuedHashMap<>(); + public final MultiValuedMap, + LifeCycleHook> classTriggers = new HashSetValuedHashMap<>(); public final ConcurrentHashMap> fieldsToTypes = new ConcurrentHashMap<>(); public final ConcurrentHashMap aliasesToFields = new ConcurrentHashMap<>(); public final ConcurrentHashMap requestScopeableMethods = new ConcurrentHashMap<>(); + public final ConcurrentHashMap> attributeArguments = new ConcurrentHashMap<>(); public final ConcurrentHashMap annotations = new ConcurrentHashMap<>(); public static final EntityBinding EMPTY_BINDING = new EntityBinding(); + public static final Set EMPTY_ATTRIBUTES_ARGS = Collections.unmodifiableSet(new HashSet<>()); private static final String ALL_FIELDS = "*"; /* empty binding constructor */ private EntityBinding() { + injected = false; jsonApiType = null; entityName = null; - attributes = new ArrayList<>(); - relationships = new ArrayList<>(); + apiVersion = NO_VERSION; + apiAttributes = new ArrayList<>(); + apiRelationships = new ArrayList<>(); inheritedTypes = new ArrayList<>(); idField = null; idType = null; entityClass = null; entityPermissions = EntityPermissions.EMPTY_PERMISSIONS; idGenerated = false; + dictionary = null; + } + + /** + * Constructor + * + * @param dictionary Dictionary to use + * @param cls Entity class + * @param type Declared Elide type name + * @param name Declared Entity name + */ + public EntityBinding(EntityDictionary dictionary, + Class cls, + String type, + String name) { + this(dictionary, cls, type, name, NO_VERSION, new HashSet<>()); } - public EntityBinding(EntityDictionary dictionary, Class cls, String type, String name) { + /** + * Constructor + * + * @param dictionary Dictionary to use + * @param cls Entity class + * @param type Declared Elide type name + * @param name Declared Entity name + * @param hiddenAnnotations Annotations for hiding a field in API + */ + public EntityBinding(EntityDictionary dictionary, + Class cls, + String type, + String name, + String apiVersion, + Set> hiddenAnnotations) { + this.dictionary = dictionary; entityClass = cls; jsonApiType = type; + this.apiVersion = apiVersion; entityName = name; inheritedTypes = getInheritedTypes(cls); // Map id's, attributes, and relationships List fieldOrMethodList = getAllFields(); + injected = shouldInject(); if (fieldOrMethodList.stream().anyMatch(field -> field.isAnnotationPresent(Id.class))) { accessType = AccessType.FIELD; @@ -151,20 +188,9 @@ public EntityBinding(EntityDictionary dictionary, Class cls, String type, Str /* Add all public methods that are computed OR life cycle hooks */ fieldOrMethodList.addAll( getInstanceMembers(cls.getMethods(), - (method) -> method.isAnnotationPresent(ComputedAttribute.class) - || method.isAnnotationPresent(ComputedRelationship.class) - || method.isAnnotationPresent(OnReadPreSecurity.class) - || method.isAnnotationPresent(OnReadPreCommit.class) - || method.isAnnotationPresent(OnReadPostCommit.class) - || method.isAnnotationPresent(OnUpdatePreSecurity.class) - || method.isAnnotationPresent(OnUpdatePreCommit.class) - || method.isAnnotationPresent(OnUpdatePostCommit.class) - || method.isAnnotationPresent(OnCreatePreSecurity.class) - || method.isAnnotationPresent(OnCreatePreCommit.class) - || method.isAnnotationPresent(OnCreatePostCommit.class) - || method.isAnnotationPresent(OnDeletePreSecurity.class) - || method.isAnnotationPresent(OnDeletePreCommit.class) - || method.isAnnotationPresent(OnDeletePostCommit.class) + (method) -> method.isAnnotationPresent(LifeCycleHookBinding.class) + || method.isAnnotationPresent(ComputedAttribute.class) + || method.isAnnotationPresent(ComputedRelationship.class) ) ); @@ -183,10 +209,11 @@ public EntityBinding(EntityDictionary dictionary, Class cls, String type, Str fieldOrMethodList.addAll(getInstanceMembers(cls.getMethods())); } - bindEntityFields(cls, type, fieldOrMethodList); + bindEntityFields(cls, type, fieldOrMethodList, hiddenAnnotations); + bindTriggerIfPresent(); - attributes = dequeToList(attributesDeque); - relationships = dequeToList(relationshipsDeque); + apiAttributes = dequeToList(attributesDeque); + apiRelationships = dequeToList(relationshipsDeque); entityPermissions = new EntityPermissions(dictionary, cls, fieldOrMethodList); } @@ -231,27 +258,30 @@ public List getAllFields() { return fields; } + private List getAllMethods() { + List methods = new ArrayList<>(); + + methods.addAll(getInstanceMembers(entityClass.getDeclaredMethods(), (method) -> !method.isSynthetic())); + for (Class type : inheritedTypes) { + methods.addAll(getInstanceMembers(type.getDeclaredMethods(), (method) -> !method.isSynthetic())); + } + + return methods; + } + /** * Bind fields of an entity including the Id field, attributes, and relationships. * * @param cls Class type to bind fields * @param type JSON API type identifier * @param fieldOrMethodList List of fields and methods on entity + * @param hiddenAnnotations Annotations for hiding a field in API */ - private void bindEntityFields(Class cls, String type, Collection fieldOrMethodList) { + private void bindEntityFields(Class cls, String type, + Collection fieldOrMethodList, + Set> hiddenAnnotations) { for (AccessibleObject fieldOrMethod : fieldOrMethodList) { - bindTriggerIfPresent(OnCreatePreSecurity.class, fieldOrMethod); - bindTriggerIfPresent(OnDeletePreSecurity.class, fieldOrMethod); - bindTriggerIfPresent(OnUpdatePreSecurity.class, fieldOrMethod); - bindTriggerIfPresent(OnReadPreSecurity.class, fieldOrMethod); - bindTriggerIfPresent(OnCreatePreCommit.class, fieldOrMethod); - bindTriggerIfPresent(OnDeletePreCommit.class, fieldOrMethod); - bindTriggerIfPresent(OnUpdatePreCommit.class, fieldOrMethod); - bindTriggerIfPresent(OnReadPreCommit.class, fieldOrMethod); - bindTriggerIfPresent(OnCreatePostCommit.class, fieldOrMethod); - bindTriggerIfPresent(OnDeletePostCommit.class, fieldOrMethod); - bindTriggerIfPresent(OnUpdatePostCommit.class, fieldOrMethod); - bindTriggerIfPresent(OnReadPostCommit.class, fieldOrMethod); + bindTriggerIfPresent(fieldOrMethod); if (fieldOrMethod.isAnnotationPresent(Id.class)) { bindEntityId(cls, type, fieldOrMethod); @@ -271,7 +301,9 @@ private void bindEntityFields(Class cls, String type, Collection dequeToList(final Deque deque) { * Bind an attribute or relationship. * * @param fieldOrMethod Field or method to bind + * @param isHidden Whether this field is hidden from API */ - private void bindAttrOrRelation(AccessibleObject fieldOrMethod) { + private void bindAttrOrRelation(AccessibleObject fieldOrMethod, boolean isHidden) { boolean isRelation = RELATIONSHIP_TYPES.stream().anyMatch(fieldOrMethod::isAnnotationPresent); String fieldName = getFieldName(fieldOrMethod); @@ -340,13 +373,21 @@ private void bindAttrOrRelation(AccessibleObject fieldOrMethod) { } if (isRelation) { - bindRelation(fieldOrMethod, fieldName, fieldType); + bindRelation(fieldOrMethod, fieldName, fieldType, isHidden); } else { - bindAttr(fieldOrMethod, fieldName, fieldType); + bindAttr(fieldOrMethod, fieldName, fieldType, isHidden); } } - private void bindRelation(AccessibleObject fieldOrMethod, String fieldName, Class fieldType) { + /** + * Bind a relationship to current class + * + * @param fieldOrMethod Field or method to bind + * @param fieldName Field name + * @param fieldType Field type + * @param isHidden Whether this field is hidden from API + */ + private void bindRelation(AccessibleObject fieldOrMethod, String fieldName, Class fieldType, boolean isHidden) { boolean manyToMany = fieldOrMethod.isAnnotationPresent(ManyToMany.class); boolean manyToOne = fieldOrMethod.isAnnotationPresent(ManyToOne.class); boolean oneToMany = fieldOrMethod.isAnnotationPresent(OneToMany.class); @@ -388,13 +429,25 @@ private void bindRelation(AccessibleObject fieldOrMethod, String fieldName, Clas relationshipToInverse.put(fieldName, mappedBy); relationshipToCascadeTypes.put(fieldName, cascadeTypes); - relationshipsDeque.push(fieldName); + if (!isHidden) { + relationshipsDeque.push(fieldName); + } fieldsToValues.put(fieldName, fieldOrMethod); fieldsToTypes.put(fieldName, fieldType); } - private void bindAttr(AccessibleObject fieldOrMethod, String fieldName, Class fieldType) { - attributesDeque.push(fieldName); + /** + * Bind an attribute to current class + * + * @param fieldOrMethod Field or method to bind + * @param fieldName Field name + * @param fieldType Field type + * @param isHidden Whether this field is hidden from API + */ + private void bindAttr(AccessibleObject fieldOrMethod, String fieldName, Class fieldType, boolean isHidden) { + if (!isHidden) { + attributesDeque.push(fieldName); + } fieldsToValues.put(fieldName, fieldOrMethod); fieldsToTypes.put(fieldName, fieldType); } @@ -484,68 +537,74 @@ public static Class getFieldType(Class parentClass, return TypeUtils.getRawType(type, parentClass); } - private void bindTriggerIfPresent(Class annotationClass, AccessibleObject fieldOrMethod) { - if (fieldOrMethod instanceof Method && fieldOrMethod.isAnnotationPresent(annotationClass)) { - Annotation trigger = fieldOrMethod.getAnnotation(annotationClass); - String value; - try { - value = (String) annotationClass.getMethod("value").invoke(trigger); - } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) { - value = ""; - } - - Method method = (Method) fieldOrMethod; - - int paramCount = method.getParameterCount(); - Class[] paramTypes = method.getParameterTypes(); - - LifeCycleHook callback = (entity, scope, changes) -> { - try { - if (changes.isPresent() && paramCount == 2 - && paramTypes[0].isInstance(scope) - && paramTypes[1].isInstance(changes.get())) { - method.invoke(entity, scope, changes.get()); - } else if (paramCount == 1 && paramTypes[0].isInstance(scope)) { - method.invoke(entity, scope); - } else if (paramCount == 0) { - method.invoke(entity); - } else { - throw new IllegalArgumentException(); - } - } catch (ReflectiveOperationException e) { - Throwables.propagateIfPossible(e.getCause()); - throw new IllegalArgumentException(e); - } - }; - - if (value.equals(ALL_FIELDS)) { - bindTrigger(annotationClass, callback); - } else { - bindTrigger(annotationClass, value, callback); - } + private void bindTriggerIfPresent(AccessibleObject fieldOrMethod) { + LifeCycleHookBinding[] triggers = fieldOrMethod.getAnnotationsByType(LifeCycleHookBinding.class); + for (LifeCycleHookBinding trigger : triggers) { + bindTrigger(trigger, getFieldName(fieldOrMethod)); + } + } + private void bindTriggerIfPresent() { + LifeCycleHookBinding[] triggers = entityClass.getAnnotationsByType(LifeCycleHookBinding.class); + for (LifeCycleHookBinding trigger : triggers) { + bindTrigger(trigger); } } - public void bindTrigger(Class annotationClass, + public void bindTrigger(LifeCycleHookBinding.Operation operation, + LifeCycleHookBinding.TransactionPhase phase, String fieldOrMethodName, - LifeCycleHook callback) { - fieldsToTriggers.put(Pair.of(annotationClass, fieldOrMethodName), callback); + LifeCycleHook hook) { + Triple key = + Triple.of(fieldOrMethodName, operation, phase); + + fieldTriggers.put(key, hook); + } + + private void bindTrigger(LifeCycleHookBinding binding, + String fieldOrMethodName) { + Injector injector = dictionary.getInjector(); + LifeCycleHook hook = injector.instantiate(binding.hook()); + injector.inject(hook); + bindTrigger(binding.operation(), binding.phase(), fieldOrMethodName, hook); } - public void bindTrigger(Class annotationClass, - LifeCycleHook callback) { - classToTriggers.put(annotationClass, callback); + public void bindTrigger(LifeCycleHookBinding.Operation operation, + LifeCycleHookBinding.TransactionPhase phase, + LifeCycleHook hook) { + Pair key = + Pair.of(operation, phase); + + classTriggers.put(key, hook); } + private void bindTrigger(LifeCycleHookBinding binding) { + if (binding.oncePerRequest()) { + bindTrigger(binding, PersistentResource.CLASS_NO_FIELD); + return; + } - public Collection getTriggers(Class annotationClass, String fieldName) { - Collection methods = fieldsToTriggers.get(Pair.of(annotationClass, fieldName)); - return methods == null ? Collections.emptyList() : methods; + Injector injector = dictionary.getInjector(); + LifeCycleHook hook = dictionary.getInjector().instantiate(binding.hook()); + injector.inject(hook); + bindTrigger(binding.operation(), binding.phase(), hook); } - public Collection getTriggers(Class annotationClass) { - Collection methods = classToTriggers.get(annotationClass); - return methods == null ? Collections.emptyList() : methods; + public Collection getTriggers(LifeCycleHookBinding.Operation op, + LifeCycleHookBinding.TransactionPhase phase, + String fieldName) { + Triple key = + Triple.of(fieldName, op, phase); + Collection bindings = fieldTriggers.get(key); + return (bindings == null ? Collections.emptyList() : bindings); + } + + public Collection getTriggers(LifeCycleHookBinding.Operation op, + LifeCycleHookBinding.TransactionPhase phase) { + + Pair key = + Pair.of(op, phase); + Collection bindings = classTriggers.get(key); + return (bindings == null ? Collections.emptyList() : bindings); } /** @@ -592,6 +651,27 @@ public A getMethodAnnotation(Class annotationClass, St return annotation == NO_ANNOTATION ? null : annotationClass.cast(annotation); } + private boolean shouldInject() { + boolean hasField = getAllFields().stream() + .anyMatch(accessibleObject -> accessibleObject.isAnnotationPresent(Inject.class)); + + if (hasField) { + return true; + } + + boolean hasMethod = getAllMethods().stream() + .anyMatch(accessibleObject -> accessibleObject.isAnnotationPresent(Inject.class)); + + if (hasMethod) { + return true; + } + + boolean hasConstructor = Arrays.stream(entityClass.getConstructors()) + .anyMatch(ctor -> ctor.getAnnotation(Inject.class) != null); + + return hasConstructor; + } + private List> getInheritedTypes(Class entityClass) { ArrayList> results = new ArrayList<>(); @@ -601,4 +681,35 @@ private List> getInheritedTypes(Class entityClass) { return results; } + + + /** + * Add a collection of arguments to the attributes of this Entity. + * @param attribute attribute name to which argument has to be added + * @param arguments Set of Argument Type for the attribute + */ + public void addArgumentsToAttribute(String attribute, Set arguments) { + AccessibleObject fieldObject = fieldsToValues.get(attribute); + if (fieldObject != null && arguments != null) { + Set existingArgs = attributeArguments.get(fieldObject); + if (existingArgs != null) { + //Replace any argument names with new value + existingArgs.addAll(arguments); + } else { + attributeArguments.put(fieldObject, new HashSet<>(arguments)); + } + } + } + + /** + * Returns the Collection of all attributes of an argument. + * @param attribute Name of the argument for ehich arguments are to be retrieved. + * @return A Set of ArgumentType for the given attribute. + */ + public Set getAttributeArguments(String attribute) { + AccessibleObject fieldObject = fieldsToValues.get(attribute); + return (fieldObject != null) + ? attributeArguments.getOrDefault(fieldObject, EMPTY_ATTRIBUTES_ARGS) + : EMPTY_ATTRIBUTES_ARGS; + } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java b/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java index bca8faf241..5c4274c016 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java @@ -9,21 +9,23 @@ import static com.yahoo.elide.core.EntityBinding.EMPTY_BINDING; import com.yahoo.elide.Injector; +import com.yahoo.elide.annotation.ApiVersion; import com.yahoo.elide.annotation.ComputedAttribute; import com.yahoo.elide.annotation.ComputedRelationship; import com.yahoo.elide.annotation.Exclude; import com.yahoo.elide.annotation.Include; +import com.yahoo.elide.annotation.LifeCycleHookBinding; import com.yahoo.elide.annotation.MappedInterface; +import com.yahoo.elide.annotation.NonTransferable; import com.yahoo.elide.annotation.SecurityCheck; -import com.yahoo.elide.annotation.SharePermission; import com.yahoo.elide.core.exceptions.HttpStatusException; import com.yahoo.elide.core.exceptions.InternalServerErrorException; import com.yahoo.elide.core.exceptions.InvalidAttributeException; import com.yahoo.elide.functions.LifeCycleHook; +import com.yahoo.elide.security.FilterExpressionCheck; import com.yahoo.elide.security.checks.Check; import com.yahoo.elide.security.checks.prefab.Collections.AppendOnly; import com.yahoo.elide.security.checks.prefab.Collections.RemoveOnly; -import com.yahoo.elide.security.checks.prefab.Common; import com.yahoo.elide.security.checks.prefab.Role; import com.yahoo.elide.utils.ClassScanner; import com.yahoo.elide.utils.coerce.CoerceUtil; @@ -31,10 +33,12 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.Maps; - +import com.google.common.collect.Sets; import org.antlr.v4.runtime.tree.ParseTree; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.lang.annotation.Annotation; @@ -60,10 +64,11 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Function; import java.util.stream.Collectors; - import javax.persistence.AccessType; import javax.persistence.CascadeType; +import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.JoinColumn; import javax.persistence.Transient; import javax.ws.rs.WebApplicationException; @@ -76,11 +81,19 @@ @SuppressWarnings("static-method") public class EntityDictionary { - protected final ConcurrentHashMap> bindJsonApiToEntity = new ConcurrentHashMap<>(); + public static final String ELIDE_PACKAGE_PREFIX = "com.yahoo.elide"; + public static final String NO_VERSION = ""; + + protected final ConcurrentHashMap, Class> bindJsonApiToEntity = new ConcurrentHashMap<>(); protected final ConcurrentHashMap, EntityBinding> entityBindings = new ConcurrentHashMap<>(); protected final CopyOnWriteArrayList> bindEntityRoots = new CopyOnWriteArrayList<>(); protected final ConcurrentHashMap, List>> subclassingEntities = new ConcurrentHashMap<>(); protected final BiMap> checkNames; + + @Getter + protected final Set apiVersions; + + @Getter protected final Injector injector; public final static String REGULAR_ID_NAME = "id"; @@ -95,7 +108,25 @@ public class EntityDictionary { * to their implementing classes */ public EntityDictionary(Map> checks) { - this(checks, null); + this.checkNames = Maps.synchronizedBiMap(HashBiMap.create(checks)); + this.apiVersions = new HashSet<>(); + initializeChecks(); + + //Default injector only injects Elide internals. + this.injector = new Injector() { + @Override + public void inject(Object entity) { + if (entity instanceof FilterExpressionCheck) { + try { + Field field = FilterExpressionCheck.class.getDeclaredField("dictionary"); + field.setAccessible(true); + field.set(entity, EntityDictionary.this); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } + }; } /** @@ -109,17 +140,20 @@ public EntityDictionary(Map> checks) { * initialize Elide models. */ public EntityDictionary(Map> checks, Injector injector) { - checkNames = Maps.synchronizedBiMap(HashBiMap.create(checks)); + this.checkNames = Maps.synchronizedBiMap(HashBiMap.create(checks)); + this.apiVersions = new HashSet<>(); + initializeChecks(); + this.injector = injector; + } + private void initializeChecks() { addPrefabCheck("Prefab.Role.All", Role.ALL.class); addPrefabCheck("Prefab.Role.None", Role.NONE.class); addPrefabCheck("Prefab.Collections.AppendOnly", AppendOnly.class); addPrefabCheck("Prefab.Collections.RemoveOnly", RemoveOnly.class); - addPrefabCheck("Prefab.Common.UpdateOnCreate", Common.UpdateOnCreate.class); - - this.injector = injector; } + private void addPrefabCheck(String alias, Class checkClass) { if (checkNames.containsKey(alias) || checkNames.inverse().containsKey(checkClass)) { return; @@ -206,8 +240,19 @@ public boolean isMappedInterface(Class interfaceClass) { * @param entityName entity name * @return binding class */ - public Class getEntityClass(String entityName) { - return bindJsonApiToEntity.get(entityName); + public Class getEntityClass(String entityName, String version) { + Class lookup = bindJsonApiToEntity.getOrDefault(Pair.of(entityName, version), null); + + if (lookup == null) { + //Elide standard models transcend API versions. + return entityBindings.values().stream() + .filter(binding -> binding.entityClass.getName().startsWith(ELIDE_PACKAGE_PREFIX)) + .filter(binding -> binding.entityName.equals(entityName)) + .map(EntityBinding::getEntityClass) + .findFirst() + .orElse(null); + } + return lookup; } /** @@ -267,8 +312,8 @@ public ParseTree getPermissionsForClass(Class resourceClass, Class resourceClass, - String field, - Class annotationClass) { + String field, + Class annotationClass) { EntityBinding binding = getEntityBinding(resourceClass); return binding.entityPermissions.getFieldChecksForPermission(field, annotationClass); } @@ -291,35 +336,16 @@ public Class getCheck(String checkIdentifier) { } /** - * Get inherited entity names for a particular entity. - * - * @param entityName Json alias name for entity - * @return List of all inherited entity type names - */ - public List getSubclassingEntityNames(String entityName) { - return getSubclassingEntityNames(getEntityClass(entityName)); - } - - /** - * Get inherited entity names for a particular entity. + * Fetch all entity classes that provided entity inherits from (i.e. all superclass entities down to, + * but excluding Object). * * @param entityClass Entity class - * @return List of all inherited entity type names - */ - public List getSubclassingEntityNames(Class entityClass) { - List> entities = getSubclassingEntities(entityClass); - return entities.stream().map(this::getJsonAliasFor).collect(Collectors.toList()); - } - - /** - * Get a list of inherited entities from a particular entity. - * Namely, the list of entities inheriting from the provided class. - * - * @param entityName Json alias name for entity - * @return List of all inherited entity types + * @return List of all super class entity classes */ - public List> getSubclassingEntities(String entityName) { - return getSubclassingEntities(getEntityClass(entityName)); + public List> getSuperClassEntities(Class entityClass) { + return getEntityBinding(entityClass).inheritedTypes.stream() + .filter(entityBindings::containsKey) + .collect(Collectors.toList()); } /** @@ -329,60 +355,12 @@ public List> getSubclassingEntities(String entityName) { * @param entityClass Entity class * @return List of all inherited entity types */ - public List> getSubclassingEntities(Class entityClass) { - return subclassingEntities.computeIfAbsent(entityClass, unused -> entityBindings - .keySet().stream() - .filter(c -> c != entityClass && entityClass.isAssignableFrom(c)) - .collect(Collectors.toList())); - } - - /** - * Fetch all entity names that the provided entity inherits from (i.e. all superclass entities down to, - * but excluding Object). - * - * @param entityName Json alias name for entity - * @return List of all super class entity json names - */ - public List getSuperClassEntityNames(String entityName) { - return getSuperClassEntityNames(getEntityClass(entityName)); - } - - /** - * Fetch all entity names that the provided entity inherits from (i.e. all superclass entities down to, - * but excluding Object). - * - * @param entityClass Entity class - * @return List of all super class entity json names - */ - public List getSuperClassEntityNames(Class entityClass) { - return getSuperClassEntities(entityClass).stream() - .map(this::getJsonAliasFor) - .collect(Collectors.toList()); - } - - /** - * Fetch all entity classes that the provided entity inherits from (i.e. all superclass entities down to, - * but excluding Object). - * - * @param entityName Json alias name for entity - * @return List of all super class entity classes - */ - public List> getSuperClassEntities(String entityName) { - return getSuperClassEntities(getEntityClass(entityName)); - } - - /** - * Fetch all entity classes that provided entity inherits from (i.e. all superclass entities down to, - * but excluding Object). - * - * @param entityClass Entity class - * @return List of all super class entity classes - */ - public List> getSuperClassEntities(Class entityClass) { - return getEntityBinding(entityClass).inheritedTypes.stream() - .filter(entityBindings::containsKey) - .collect(Collectors.toList()); - } + public List> getSubclassingEntities(Class entityClass) { + return subclassingEntities.computeIfAbsent(entityClass, unused -> entityBindings + .keySet().stream() + .filter(c -> c != entityClass && entityClass.isAssignableFrom(c)) + .collect(Collectors.toList())); + } /** * Returns the friendly named mapped to this given check. @@ -417,13 +395,37 @@ public AccessType getAccessType(Class entityClass) { return getEntityBinding(entityClass).getAccessType(); } + /** + * Get all bound classes. + * + * @return the bound classes + */ + public Set> getBoundClasses() { + return entityBindings.keySet(); + } + + /** + * Get all bound classes for a particular API version. + * + * @return the bound classes + */ + public Set> getBoundClassesByVersion(String apiVersion) { + return entityBindings.values().stream() + .filter(binding -> + binding.getApiVersion().equals(apiVersion) + || binding.entityClass.getName().startsWith(ELIDE_PACKAGE_PREFIX) + ) + .map(EntityBinding::getEntityClass) + .collect(Collectors.toSet()); + } + /** * Get all bindings. * * @return the bindings */ - public Set> getBindings() { - return entityBindings.keySet(); + public Set getBindings() { + return new HashSet<>(entityBindings.values()); } /** @@ -441,7 +443,7 @@ public Map> getCheckMappings() { * @return List of attribute names for entity */ public List getAttributes(Class entityClass) { - return getEntityBinding(entityClass).attributes; + return getEntityBinding(entityClass).apiAttributes; } /** @@ -461,7 +463,7 @@ public List getAttributes(Object entity) { * @return List of relationship names for entity */ public List getRelationships(Class entityClass) { - return getEntityBinding(entityClass).relationships; + return getEntityBinding(entityClass).apiRelationships; } /** @@ -482,7 +484,7 @@ public List getRelationships(Object entity) { */ public List getElideBoundRelationships(Class entityClass) { return getRelationships(entityClass).stream() - .filter(relationName -> getBindings().contains(getParameterizedType(entityClass, relationName))) + .filter(relationName -> getBoundClasses().contains(getParameterizedType(entityClass, relationName))) .collect(Collectors.toList()); } @@ -779,45 +781,42 @@ public String getNameFromAlias(Object entity, String alias) { */ public void initializeEntity(T entity) { if (entity != null) { - @SuppressWarnings("unchecked") - Initializer initializer = getEntityBinding(entity.getClass()).getInitializer(); - if (initializer != null) { - initializer.initialize(entity); - } else if (injector != null) { + EntityBinding binding = getEntityBinding(entity.getClass()); + + if (binding.isInjected()) { injector.inject(entity); } } } /** - * Bind a particular initializer to a class. + * Returns whether or not an entity is shareable. * - * @param the type parameter - * @param initializer Initializer to use for class - * @param cls Class to bind initialization + * @param entityClass the entity type to check for the shareable permissions + * @return true if entityClass is shareable. False otherwise. */ - public void bindInitializer(Initializer initializer, Class cls) { - bindIfUnbound(cls); - getEntityBinding(cls).setInitializer(initializer); + public boolean isTransferable(Class entityClass) { + NonTransferable nonTransferable = getAnnotation(entityClass, NonTransferable.class); + + return (nonTransferable == null || !nonTransferable.enabled()); } /** - * Returns whether or not an entity is shareable. + * Add given Entity bean to dictionary. * - * @param entityClass the entity type to check for the shareable permissions - * @return true if entityClass is shareable. False otherwise. + * @param cls Entity bean class */ - public boolean isShareable(Class entityClass) { - return getAnnotation(entityClass, SharePermission.class) != null - && getAnnotation(entityClass, SharePermission.class).sharable(); + public void bindEntity(Class cls) { + bindEntity(cls, new HashSet<>()); } /** * Add given Entity bean to dictionary. * * @param cls Entity bean class + * @param hiddenAnnotations Annotations for hiding a field in API */ - public void bindEntity(Class cls) { + public void bindEntity(Class cls, Set> hiddenAnnotations) { Class declaredClass = lookupIncludeClass(cls); if (declaredClass == null) { @@ -847,8 +846,35 @@ public void bindEntity(Class cls) { type = include.type(); } - bindJsonApiToEntity.put(type, declaredClass); - entityBindings.put(declaredClass, new EntityBinding(this, declaredClass, type, name)); + String version = getModelVersion(cls); + bindJsonApiToEntity.put(Pair.of(type, version), declaredClass); + apiVersions.add(version); + entityBindings.put(declaredClass, new EntityBinding(this, declaredClass, + type, name, version, hiddenAnnotations)); + if (include.rootLevel()) { + bindEntityRoots.add(declaredClass); + } + } + + /** + * Add an EntityBinding instance to dictionary. + * + * @param entityBinding EntityBinding instance + */ + public void bindEntity(EntityBinding entityBinding) { + Class declaredClass = entityBinding.entityClass; + + if (isClassBound(declaredClass)) { + //Ignore duplicate bindings. + return; + } + + Include include = (Include) getFirstAnnotation(declaredClass, Collections.singletonList(Include.class)); + + String version = getModelVersion(declaredClass); + bindJsonApiToEntity.put(Pair.of(entityBinding.jsonApiType, version), declaredClass); + entityBindings.put(declaredClass, entityBinding); + apiVersions.add(version); if (include.rootLevel()) { bindEntityRoots.add(declaredClass); } @@ -891,14 +917,16 @@ public A getMethodAnnotation(Class recordClass, String } public Collection getTriggers(Class cls, - Class annotationClass, + LifeCycleHookBinding.Operation op, + LifeCycleHookBinding.TransactionPhase phase, String fieldName) { - return getEntityBinding(cls).getTriggers(annotationClass, fieldName); + return getEntityBinding(cls).getTriggers(op, phase, fieldName); } public Collection getTriggers(Class cls, - Class annotationClass) { - return getEntityBinding(cls).getTriggers(annotationClass); + LifeCycleHookBinding.Operation op, + LifeCycleHookBinding.TransactionPhase phase) { + return getEntityBinding(cls).getTriggers(op, phase); } /** @@ -953,10 +981,24 @@ public static Annotation getFirstAnnotation(Class entityClass, for (Class annotationClass : annotationClassList) { annotation = cls.getDeclaredAnnotation(annotationClass); if (annotation != null) { - break; + return annotation; } } } + + return getFirstPackageAnnotation(entityClass, annotationClassList); + } + + /** + * Return first matching annotation from a package or parent package. + * + * @param entityClass Entity class type + * @param annotationClassList List of sought annotations + * @return annotation found + */ + public static Annotation getFirstPackageAnnotation(Class entityClass, + List> annotationClassList) { + Annotation annotation = null; // no class annotation, try packages for (Package pkg = entityClass.getPackage(); annotation == null && pkg != null; pkg = getParentPackage(pkg)) { for (Class annotationClass : annotationClassList) { @@ -1040,7 +1082,7 @@ public Collection getIdAnnotations(Object value) { } /** - * Follow for this class or super-class for Entity annotation. + * Follow for this class or super-class for JPA {@link Entity} annotation. * * @param objClass provided class * @return class with Entity annotation @@ -1062,6 +1104,12 @@ public Class lookupEntityClass(Class objClass) { public Class lookupIncludeClass(Class objClass) { Annotation first = getFirstAnnotation(objClass, Arrays.asList(Exclude.class, Include.class)); if (first instanceof Include) { + Class declaringClass = lookupAnnotationDeclarationClass(objClass, Include.class); + if (declaringClass != null) { + return declaringClass; + } + + //If we didn't find Include declared on a class, it must be declared at the package level. return objClass; } return null; @@ -1082,6 +1130,7 @@ public Class lookupAnnotationDeclarationClass(Class objClass, Class objClass) { return (entityBindings.getOrDefault(objClass, EMPTY_BINDING) != EMPTY_BINDING); } + /** + * Check whether a class is a JPA entity. + * + * @param objClass class + * @return True if it is a JPA entity + */ + public final boolean isJPAEntity(Class objClass) { + try { + lookupEntityClass(objClass); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } /** * Retrieve the accessible object for a field from a target object. @@ -1177,11 +1240,11 @@ public Set getFieldsOfType(Class targetClass, Class targetType) { } public boolean isRelation(Class entityClass, String relationName) { - return getEntityBinding(entityClass).relationships.contains(relationName); + return getEntityBinding(entityClass).apiRelationships.contains(relationName); } public boolean isAttribute(Class entityClass, String attributeName) { - return getEntityBinding(entityClass).attributes.contains(attributeName); + return getEntityBinding(entityClass).apiAttributes.contains(attributeName); } /** @@ -1193,7 +1256,22 @@ public void scanForSecurityChecks() { // /elide-spring-boot-autoconfigure/src/main/java/org/illyasviel/elide // /spring/boot/autoconfigure/ElideAutoConfiguration.java - for (Class cls : ClassScanner.getAnnotatedClasses(SecurityCheck.class)) { + Set> classes = ClassScanner.getAnnotatedClasses(SecurityCheck.class); + + addSecurityChecks(classes); + } + + /** + * Add security checks and bind them to the dictionary. + * @param classes Security check classes. + */ + public void addSecurityChecks(Set> classes) { + + if (classes == null && classes.size() == 0) { + return; + } + + for (Class cls : classes) { if (Check.class.isAssignableFrom(cls)) { SecurityCheck securityCheckMeta = cls.getAnnotation(SecurityCheck.class); log.debug("Register Elide Check [{}] with expression [{}]", @@ -1209,17 +1287,19 @@ public void scanForSecurityChecks() { * Binds a lifecycle hook to a particular field or method in an entity. The hook will be called a * single time per request per field READ, CREATE, or UPDATE. * @param entityClass The entity that triggers the lifecycle hook. - * @param annotationClass (OnReadPostCommit, OnUpdatePreSecurity, etc) - * @param fieldOrMethodName The name of the field or method - * @param callback The callback function to invoke. + * @param fieldOrMethodName The name of the field or method. + * @param operation CREATE, READ, or UPDATE + * @param phase PRESECURITY, PRECOMMIT, or POSTCOMMIT + * @param hook The callback to invoke. */ public void bindTrigger(Class entityClass, - Class annotationClass, String fieldOrMethodName, - LifeCycleHook callback) { - + LifeCycleHookBinding.Operation operation, + LifeCycleHookBinding.TransactionPhase phase, + LifeCycleHook hook) { bindIfUnbound(entityClass); - getEntityBinding(entityClass).bindTrigger(annotationClass, fieldOrMethodName, callback); + + getEntityBinding(entityClass).bindTrigger(operation, phase, fieldOrMethodName, hook); } /** @@ -1229,37 +1309,26 @@ public void bindTrigger(Class entityClass, * * The behavior is determined by the value of the {@code allowMultipleInvocations} flag. * @param entityClass The entity that triggers the lifecycle hook. - * @param annotationClass (OnReadPostCommit, OnUpdatePreSecurity, etc) - * @param callback The callback function to invoke. + * @param operation CREATE, READ, or UPDATE + * @param phase PRESECURITY, PRECOMMIT, or POSTCOMMIT + * @param hook The callback to invoke. * @param allowMultipleInvocations Should the same life cycle hook be invoked multiple times for multiple - * CRUD actions on the same model. + * CRUD actions on the same model. */ public void bindTrigger(Class entityClass, - Class annotationClass, - LifeCycleHook callback, + LifeCycleHookBinding.Operation operation, + LifeCycleHookBinding.TransactionPhase phase, + LifeCycleHook hook, boolean allowMultipleInvocations) { bindIfUnbound(entityClass); + if (allowMultipleInvocations) { - getEntityBinding(entityClass).bindTrigger(annotationClass, callback); + getEntityBinding(entityClass).bindTrigger(operation, phase, hook); } else { - getEntityBinding(entityClass).bindTrigger(annotationClass, PersistentResource.CLASS_NO_FIELD, callback); + getEntityBinding(entityClass).bindTrigger(operation, phase, PersistentResource.CLASS_NO_FIELD, hook); } } - /** - * Binds a lifecycle hook to a particular entity class. The hook will be called a single time per request - * per class READ, CREATE, UPDATE, or DELETE. - * @param entityClass The entity that triggers the lifecycle hook. - * @param annotationClass (OnReadPostCommit, OnUpdatePreSecurity, etc) - * @param callback The callback function to invoke. - */ - public void bindTrigger(Class entityClass, - Class annotationClass, - LifeCycleHook callback) { - bindTrigger(entityClass, annotationClass, callback, false); - } - - /** * Returns true if the relationship cascades deletes and false otherwise. * @param targetClass The class which owns the relationship. @@ -1313,11 +1382,13 @@ public List walkEntityGraph(Set> entities, Function, T /** * Returns whether or not a class is already bound. - * @param cls + * @param cls The class to verify. * @return true if the class is bound. False otherwise. */ public boolean hasBinding(Class cls) { - return bindJsonApiToEntity.contains(cls); + return entityBindings.values().stream() + .filter(binding -> binding.entityClass.equals(cls)) + .findFirst().orElse(null) != null; } /** @@ -1347,8 +1418,18 @@ public Object getValue(Object target, String fieldName, RequestScope scope) { throw new InvalidAttributeException(fieldName, getJsonAliasFor(target.getClass())); } + /** + * Sets the ID field of a target object. + * @param target the object which owns the ID to set. + * @param id the value to set + */ + public void setId(Object target, String id) { + setValue(target, getIdFieldName(lookupBoundClass(target.getClass())), id); + } + /** * Invoke the set[fieldName] method on the target object OR set the field with the corresponding name. + * @param target The object which owns the field to set * @param fieldName the field name to set or invoke equivalent set method * @param value the value to set */ @@ -1464,6 +1545,36 @@ private Map coerceMap(Object target, Map values, String fieldName) { return result; } + /** + * Returns whether or not a specified annotation is present on an entity field or its corresponding method. + * + * @param fieldName The entity field + * @param annotationClass The provided annotation class + * + * @param The type of the {@code annotationClass} + * + * @return {@code true} if the field is annotated by the {@code annotationClass} + */ + public boolean attributeOrRelationAnnotationExists( + Class cls, + String fieldName, + Class annotationClass + ) { + return getAttributeOrRelationAnnotation(cls, annotationClass, fieldName) != null; + } + + /** + * Returns whether or not a specified field exists in an entity. + * + * @param cls The entity + * @param fieldName The provided field to check + * + * @return {@code true} if the field exists in the entity + */ + public boolean isValidField(Class cls, String fieldName) { + return getAllFields(cls).contains(fieldName); + } + private boolean isValidParameterizedMap(Map values, Class keyType, Class valueType) { for (Map.Entry entry : values.entrySet()) { Object key = entry.getKey(); @@ -1481,8 +1592,76 @@ private boolean isValidParameterizedMap(Map values, Class keyType, Clas * @param entityClass the class to bind. */ private void bindIfUnbound(Class entityClass) { + /* This is safe to call with non-proxy objects. Not safe to call with ORM proxy objects. */ + if (! isClassBound(entityClass)) { bindEntity(entityClass); } } + + /** + * Add a collection of argument to the attributes + * @param cls The entity + * @param attributeName attribute name to which argument has to be added + * @param arguments Set of Argument type containing name and type of each argument. + */ + public void addArgumentsToAttribute(Class cls, String attributeName, Set arguments) { + getEntityBinding(cls).addArgumentsToAttribute(attributeName, arguments); + } + + /** + * Add a single argument to the attribute + * @param cls The entity + * @param attributeName attribute name to which argument has to be added + * @param argument A single argument + */ + public void addArgumentToAttribute(Class cls, String attributeName, ArgumentType argument) { + this.addArgumentsToAttribute(cls, attributeName, Sets.newHashSet(argument)); + } + + /** + * Returns the Collection of all attributes of an argument. + * @param cls The entity + * @param attributeName Name of the argument for ehich arguments are to be retrieved. + * @return A Set of ArgumentType for the given attribute. + */ + public Set getAttributeArguments(Class cls, String attributeName) { + return entityBindings.getOrDefault(cls, EMPTY_BINDING).getAttributeArguments(attributeName); + } + + /** + * Get column name using JPA. + * + * @param cls The entity class. + * @param fieldName The entity attribute. + * @return The jpa column name. + */ + public String getAnnotatedColumnName(Class cls, String fieldName) { + Column[] column = getAttributeOrRelationAnnotations(cls, Column.class, fieldName); + + // this would only be valid for dimension columns + JoinColumn[] joinColumn = getAttributeOrRelationAnnotations(cls, JoinColumn.class, fieldName); + + if (column == null || column.length == 0) { + if (joinColumn == null || joinColumn.length == 0) { + return fieldName; + } else { + return joinColumn[0].name(); + } + } else { + return column[0].name(); + } + } + + /** + * Returns the api version bound to a particular model class. + * @param modelClass The model class to lookup. + * @return The api version associated with the model or empty string if there is no association. + */ + public static String getModelVersion(Class modelClass) { + ApiVersion apiVersionAnnotation = + (ApiVersion) getFirstPackageAnnotation(modelClass, Arrays.asList(ApiVersion.class)); + + return (apiVersionAnnotation == null) ? NO_VERSION : apiVersionAnnotation.version(); + } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/EntityPermissions.java b/elide-core/src/main/java/com/yahoo/elide/core/EntityPermissions.java index 181fab7c03..b9aafcf256 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/EntityPermissions.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/EntityPermissions.java @@ -7,8 +7,8 @@ import com.yahoo.elide.annotation.CreatePermission; import com.yahoo.elide.annotation.DeletePermission; +import com.yahoo.elide.annotation.NonTransferable; import com.yahoo.elide.annotation.ReadPermission; -import com.yahoo.elide.annotation.SharePermission; import com.yahoo.elide.annotation.UpdatePermission; import com.yahoo.elide.generated.parsers.ExpressionLexer; import com.yahoo.elide.generated.parsers.ExpressionParser; @@ -43,7 +43,7 @@ public class EntityPermissions implements CheckInstantiator { ReadPermission.class, CreatePermission.class, DeletePermission.class, - SharePermission.class, + NonTransferable.class, UpdatePermission.class ); @@ -79,7 +79,7 @@ public EntityPermissions(EntityDictionary dictionary, final Map fieldPermissions = new HashMap<>(); fieldOrMethodList.stream() .forEach(member -> bindMemberPermissions(fieldPermissions, member, annotationClass)); - if (annotationClass != SharePermission.class) { + if (annotationClass != NonTransferable.class) { ParseTree classPermission = bindClassPermissions(cls, annotationClass); if (classPermission != null || !fieldPermissions.isEmpty()) { bindings.put(annotationClass, new AnnotationBinding(classPermission, fieldPermissions)); diff --git a/elide-core/src/main/java/com/yahoo/elide/core/HttpStatus.java b/elide-core/src/main/java/com/yahoo/elide/core/HttpStatus.java index 208cc22ab6..44a6bfb312 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/HttpStatus.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/HttpStatus.java @@ -16,6 +16,7 @@ public class HttpStatus { public static final int SC_BAD_REQUEST = 400; public static final int SC_FORBIDDEN = 403; public static final int SC_NOT_FOUND = 404; + public static final int SC_TIMEOUT = 408; public static final int SC_LOCKED = 423; public static final int SC_INTERNAL_SERVER_ERROR = 500; } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/Initializer.java b/elide-core/src/main/java/com/yahoo/elide/core/Initializer.java deleted file mode 100644 index 79aa554139..0000000000 --- a/elide-core/src/main/java/com/yahoo/elide/core/Initializer.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2017, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.core; - -/** - * Used to perform any additional initialization required on entity beans which is not - * possible at time of construction. - * @param bean type - */ -@FunctionalInterface -public interface Initializer { - - /** - * Initialize an entity bean. - * - * @param entity Entity bean to initialize - */ - void initialize(T entity); -} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/LifecycleHookInvoker.java b/elide-core/src/main/java/com/yahoo/elide/core/LifecycleHookInvoker.java index e75ce6c3ef..64e35d2a26 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/LifecycleHookInvoker.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/LifecycleHookInvoker.java @@ -5,12 +5,12 @@ */ package com.yahoo.elide.core; +import com.yahoo.elide.annotation.LifeCycleHookBinding; import com.yahoo.elide.functions.LifeCycleHook; import io.reactivex.Observer; import io.reactivex.disposables.Disposable; -import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Optional; @@ -20,15 +20,18 @@ public class LifecycleHookInvoker implements Observer { private EntityDictionary dictionary; - private Class annotation; + private LifeCycleHookBinding.Operation op; + private LifeCycleHookBinding.TransactionPhase phase; private Optional exception; private boolean throwsExceptions; public LifecycleHookInvoker(EntityDictionary dictionary, - Class annotation, + LifeCycleHookBinding.Operation op, + LifeCycleHookBinding.TransactionPhase phase, boolean throwExceptions) { this.dictionary = dictionary; - this.annotation = annotation; + this.op = op; + this.phase = phase; this.exception = Optional.empty(); this.throwsExceptions = throwExceptions; } @@ -43,20 +46,18 @@ public void onNext(CRUDEvent event) { ArrayList hooks = new ArrayList<>(); //Collect all the hooks that are keyed on a specific field. - hooks.addAll(dictionary.getTriggers( - event.getResource().getResourceClass(), - this.annotation, - event.getFieldName())); + hooks.addAll(dictionary.getTriggers(event.getResource().getResourceClass(), op, phase, event.getFieldName())); //Collect all the hooks that are keyed on any field. if (!event.getFieldName().isEmpty()) { - hooks.addAll(dictionary.getTriggers(event.getResource().getResourceClass(), this.annotation)); + hooks.addAll(dictionary.getTriggers(event.getResource().getResourceClass(), op, phase)); } try { //Invoke all the hooks hooks.forEach((hook) -> { hook.execute( + this.op, event.getResource().getObject(), event.getResource().getRequestScope(), event.getChanges()); diff --git a/elide-core/src/main/java/com/yahoo/elide/core/Path.java b/elide-core/src/main/java/com/yahoo/elide/core/Path.java index bea0a157f1..75c9d40519 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/Path.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/Path.java @@ -5,6 +5,10 @@ */ package com.yahoo.elide.core; +import static com.yahoo.elide.core.EntityDictionary.getSimpleName; +import static com.yahoo.elide.utils.TypeHelper.appendAlias; +import static com.yahoo.elide.utils.TypeHelper.getTypeAlias; + import com.yahoo.elide.core.exceptions.InvalidValueException; import com.google.common.collect.ImmutableList; @@ -25,7 +29,6 @@ @EqualsAndHashCode public class Path { private static final String PERIOD = "."; - private static final String UNDERSCORE = "_"; @Getter private List pathElements; /** @@ -50,15 +53,29 @@ public Path(List pathElements) { } public Path(Class entityClass, EntityDictionary dictionary, String dotSeparatedPath) { + pathElements = resolvePathElements(entityClass, dictionary, dotSeparatedPath); + } + + /** + * Resolve a dot separated path into list of path elements. + * + * @param entityClass root class e.g. "foo" + * @param dictionary dictionary + * @param dotSeparatedPath path e.g. "bar.baz" + * @return list of path elements e.g. ["foo.bar", "bar.baz"] + */ + private List resolvePathElements(Class entityClass, + EntityDictionary dictionary, + String dotSeparatedPath) { List elements = new ArrayList<>(); String[] fieldNames = dotSeparatedPath.split("\\."); Class currentClass = entityClass; for (String fieldName : fieldNames) { - if (dictionary.isRelation(currentClass, fieldName)) { - Class relationClass = dictionary.getParameterizedType(currentClass, fieldName); - elements.add(new PathElement(currentClass, relationClass, fieldName)); - currentClass = relationClass; + if (needNavigation(currentClass, fieldName, dictionary)) { + Class joinClass = dictionary.getParameterizedType(currentClass, fieldName); + elements.add(new PathElement(currentClass, joinClass, fieldName)); + currentClass = joinClass; } else if (dictionary.isAttribute(currentClass, fieldName) || fieldName.equals(dictionary.getIdFieldName(entityClass))) { Class attributeClass = dictionary.getType(currentClass, fieldName); @@ -67,10 +84,23 @@ public Path(Class entityClass, EntityDictionary dictionary, String dotSeparat elements.add(new PathElement(currentClass, null, fieldName)); } else { String alias = dictionary.getJsonAliasFor(currentClass); - throw new InvalidValueException(alias + " doesn't contain the field " + fieldName); + throw new InvalidValueException(alias + " does not contain the field " + fieldName); } } - pathElements = ImmutableList.copyOf(elements); + + return ImmutableList.copyOf(elements); + } + + /** + * Check whether a field need navigation to another entity. + * + * @param entityClass entity class + * @param fieldName field name + * @param dictionary dictionary + * @return True if the field requires navigation. + */ + protected boolean needNavigation(Class entityClass, String fieldName, EntityDictionary dictionary) { + return dictionary.isRelation(entityClass, fieldName); } public Optional lastElement() { @@ -95,23 +125,14 @@ public String getAlias() { } PathElement previous = pathElements.get(pathElements.size() - 2); - return getTypeAlias(previous.getType()) + UNDERSCORE + previous.getFieldName(); + return appendAlias(getTypeAlias(previous.getType()), previous.getFieldName()); } @Override public String toString() { return pathElements.size() == 0 ? "EMPTY" : pathElements.stream() - .map(e -> '[' + EntityDictionary.getSimpleName(e.getType()) + ']' + PERIOD + e.getFieldName()) + .map(e -> '[' + getSimpleName(e.getType()) + ']' + PERIOD + e.getFieldName()) .collect(Collectors.joining("/")); } - - /** - * Convert a class name into a hibernate friendly name. - * @param type The type to alias - * @return type name alias that will likely not conflict with other types or with reserved keywords. - */ - public static String getTypeAlias(Class type) { - return type.getCanonicalName().replace(PERIOD, UNDERSCORE); - } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/PersistentResource.java b/elide-core/src/main/java/com/yahoo/elide/core/PersistentResource.java index 888fb4cc4e..6e6ad26c7e 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/PersistentResource.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/PersistentResource.java @@ -5,14 +5,21 @@ */ package com.yahoo.elide.core; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.CREATE; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.DELETE; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.READ; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.UPDATE; + import com.yahoo.elide.annotation.Audit; import com.yahoo.elide.annotation.CreatePermission; import com.yahoo.elide.annotation.DeletePermission; +import com.yahoo.elide.annotation.LifeCycleHookBinding; +import com.yahoo.elide.annotation.NonTransferable; import com.yahoo.elide.annotation.ReadPermission; -import com.yahoo.elide.annotation.SharePermission; import com.yahoo.elide.annotation.UpdatePermission; import com.yahoo.elide.audit.InvalidSyntaxException; import com.yahoo.elide.audit.LogMessage; +import com.yahoo.elide.audit.LogMessageImpl; import com.yahoo.elide.core.exceptions.BadRequestException; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; import com.yahoo.elide.core.exceptions.InternalServerErrorException; @@ -23,14 +30,17 @@ import com.yahoo.elide.core.filter.InPredicate; import com.yahoo.elide.core.filter.expression.AndFilterExpression; import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; import com.yahoo.elide.jsonapi.models.Data; import com.yahoo.elide.jsonapi.models.Relationship; import com.yahoo.elide.jsonapi.models.Resource; import com.yahoo.elide.jsonapi.models.ResourceIdentifier; import com.yahoo.elide.jsonapi.models.SingleElementSet; import com.yahoo.elide.parsers.expression.CanPaginateVisitor; +import com.yahoo.elide.request.Argument; +import com.yahoo.elide.request.Attribute; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Pagination; +import com.yahoo.elide.request.Sorting; import com.yahoo.elide.security.ChangeSpec; import com.yahoo.elide.security.permissions.ExpressionResult; import com.yahoo.elide.utils.coerce.CoerceUtil; @@ -38,13 +48,11 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; - import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.IterableUtils; import org.apache.commons.lang3.StringUtils; import lombok.NonNull; - import java.io.Serializable; import java.lang.annotation.Annotation; import java.util.ArrayList; @@ -77,6 +85,7 @@ public class PersistentResource implements com.yahoo.elide.security.Persisten private final DataStoreTransaction transaction; private final RequestScope requestScope; private int hashCode = 0; + static final String CLASS_NO_FIELD = ""; /** @@ -95,6 +104,21 @@ public String toString() { return String.format("PersistentResource{type=%s, id=%s}", type, uuid.orElse(getId())); } + /** + * Create a resource in the database. + * @param entityClass the entity class + * @param requestScope the request scope + * @param uuid the (optional) uuid + * @param object type + * @return persistent resource + */ + public static PersistentResource createObject( + Class entityClass, + RequestScope requestScope, + Optional uuid) { + return createObject(null, entityClass, requestScope, uuid); + } + /** * Create a resource in the database. * @param parent - The immediate ancestor in the lineage or null if this is a root. @@ -110,7 +134,7 @@ public static PersistentResource createObject( RequestScope requestScope, Optional uuid) { - //instead of calling transcation.createObject, create the new object here. + //instead of calling transaction.createObject, create the new object here. T obj = requestScope.getTransaction().createNewObject(entityClass); String id = uuid.orElse(null); @@ -121,16 +145,15 @@ public static PersistentResource createObject( //hashcode and equals are only based on the ID/UUID & type. assignId(newResource, id); - // Keep track of new resources for non shareable resources + // Keep track of new resources for non-transferable resources requestScope.getNewPersistentResources().add(newResource); checkPermission(CreatePermission.class, newResource); newResource.auditClass(Audit.Action.CREATE, new ChangeSpec(newResource, null, null, newResource.getObject())); - requestScope.publishLifecycleEvent(newResource, CRUDEvent.CRUDAction.CREATE); + requestScope.publishLifecycleEvent(newResource, CREATE); - String type = newResource.getType(); - requestScope.setUUIDForObject(type, id, newResource.getObject()); + requestScope.setUUIDForObject(newResource.getResourceClass(), id, newResource.getObject()); // Initialize null ToMany collections requestScope.getDictionary().getRelationships(entityClass).stream() @@ -150,7 +173,12 @@ public static PersistentResource createObject( * @param id the id * @param scope the request scope */ - public PersistentResource(@NonNull T obj, PersistentResource parent, String id, @NonNull RequestScope scope) { + public PersistentResource( + @NonNull T obj, + PersistentResource parent, + String id, + @NonNull RequestScope scope + ) { this.obj = obj; this.uuid = Optional.ofNullable(id); this.lineage = parent != null ? new ResourceLineage(parent.lineage, parent) : new ResourceLineage(); @@ -161,7 +189,7 @@ public PersistentResource(@NonNull T obj, PersistentResource parent, String id, dictionary.initializeEntity(obj); } - /** + /** * Check whether an id matches for this persistent resource. * * @param checkId the check id @@ -183,7 +211,7 @@ public boolean matchesId(String checkId) { /** * Load an single entity from the DB. * - * @param loadClass resource type + * @param projection What to load from the DB. * @param id the id * @param requestScope the request scope * @param type of resource @@ -192,31 +220,43 @@ public boolean matchesId(String checkId) { */ @SuppressWarnings("resource") @NonNull public static PersistentResource loadRecord( - Class loadClass, String id, RequestScope requestScope) - throws InvalidObjectIdentifierException { - Preconditions.checkNotNull(loadClass); + EntityProjection projection, + String id, + RequestScope requestScope + ) throws InvalidObjectIdentifierException { + Preconditions.checkNotNull(projection); Preconditions.checkNotNull(id); Preconditions.checkNotNull(requestScope); DataStoreTransaction tx = requestScope.getTransaction(); EntityDictionary dictionary = requestScope.getDictionary(); + Class loadClass = projection.getType(); // Check the resource cache if exists - Object obj = requestScope.getObjectById(dictionary.getJsonAliasFor(loadClass), id); + Object obj = requestScope.getObjectById(loadClass, id); if (obj == null) { // try to load object Optional permissionFilter = getPermissionFilterExpression(loadClass, requestScope); Class idType = dictionary.getIdType(loadClass); - obj = tx.loadObject(loadClass, (Serializable) CoerceUtil.coerce(id, idType), - permissionFilter, requestScope); + + projection = projection + .copyOf() + .filterExpression(permissionFilter.orElse(null)) + .build(); + + obj = tx.loadObject(projection, (Serializable) CoerceUtil.coerce(id, idType), requestScope); if (obj == null) { throw new InvalidObjectIdentifierException(id, dictionary.getJsonAliasFor(loadClass)); } } PersistentResource resource = new PersistentResource<>( - loadClass.cast(obj), null, requestScope.getUUIDFor(obj), requestScope); + (T) obj, + null, + requestScope.getUUIDFor(obj), + requestScope); + // No need to have read access for a newly created object if (!requestScope.getNewResources().contains(resource)) { resource.checkFieldAwarePermissions(ReadPermission.class); @@ -245,25 +285,26 @@ private static Optional getPermissionFilterExpression(Clas /** * Load a collection from the datastore. * - * @param loadClass the load class + * @param projection the projection to load * @param requestScope the request scope * @param ids a list of object identifiers to optionally load. Can be empty. * @return a filtered collection of resources loaded from the datastore. */ public static Set loadRecords( - Class loadClass, + EntityProjection projection, List ids, - Optional filter, - Optional sorting, - Optional pagination, RequestScope requestScope) { + Class loadClass = projection.getType(); + Pagination pagination = projection.getPagination(); + Sorting sorting = projection.getSorting(); + + FilterExpression filterExpression = projection.getFilterExpression(); + EntityDictionary dictionary = requestScope.getDictionary(); - FilterExpression filterExpression; DataStoreTransaction tx = requestScope.getTransaction(); - if (shouldSkipCollection(loadClass, ReadPermission.class, requestScope)) { if (ids.isEmpty()) { return Collections.emptySet(); @@ -271,8 +312,7 @@ public static Set loadRecords( throw new InvalidObjectIdentifierException(ids.toString(), dictionary.getJsonAliasFor(loadClass)); } - - if (pagination.isPresent() && !pagination.get().isDefaultInstance() + if (pagination != null && !pagination.isDefaultInstance() && !CanPaginateVisitor.canPaginate(loadClass, dictionary, requestScope)) { throw new BadRequestException(String.format("Cannot paginate %s", dictionary.getJsonAliasFor(loadClass))); @@ -289,11 +329,9 @@ public static Set loadRecords( FilterExpression idExpression = buildIdFilterExpression(ids, loadClass, dictionary, requestScope); // Combine filters if necessary - filterExpression = filter + filterExpression = Optional.ofNullable(filterExpression) .map(fe -> (FilterExpression) new AndFilterExpression(idExpression, fe)) .orElse(idExpression); - } else { - filterExpression = filter.orElse(null); } Optional permissionFilter = getPermissionFilterExpression(loadClass, requestScope); @@ -305,9 +343,16 @@ public static Set loadRecords( } } - Set existingResources = filter(ReadPermission.class, filter, - new PersistentResourceSet(tx.loadObjects(loadClass, Optional.ofNullable(filterExpression), sorting, - pagination.map(p -> p.evaluate(loadClass)), requestScope), requestScope)); + EntityProjection modifiedProjection = projection + .copyOf() + .filterExpression(filterExpression) + .sorting(sorting) + .pagination(pagination) + .build(); + + Set existingResources = filter(ReadPermission.class, + Optional.ofNullable(modifiedProjection.getFilterExpression()), + new PersistentResourceSet(tx.loadObjects(modifiedProjection, requestScope), requestScope)); Set allResources = Sets.union(newResources, existingResources); @@ -340,7 +385,13 @@ public boolean updateAttribute(String fieldName, Object newVal) { this.markDirty(); //Hooks for customize logic for setAttribute/Relation if (dictionary.isAttribute(obj.getClass(), fieldName)) { - transaction.setAttribute(obj, fieldName, newVal, requestScope); + transaction.setAttribute(obj, Attribute.builder() + .name(fieldName) + .type(fieldClass) + .argument(Argument.builder() + .name("_UNUSED_") + .value(newVal).build()) + .build(), requestScope); } return true; } @@ -432,7 +483,7 @@ protected boolean updateToManyRelation(String fieldName, added = Sets.difference(updated, deleted); - checkSharePermission(added); + checkTransferablePermission(added); Collection collection = (Collection) this.getValueUnchecked(fieldName); @@ -494,11 +545,11 @@ protected boolean updateToOneRelation(String fieldName, if (newValue == null) { return false; } - checkSharePermission(resourceIdentifiers); + checkTransferablePermission(resourceIdentifiers); } else if (oldResource.getObject().equals(newValue)) { return false; } else { - checkSharePermission(resourceIdentifiers); + checkTransferablePermission(resourceIdentifiers); if (hasInverseRelation(fieldName)) { deleteInverseRelation(fieldName, oldResource.getObject()); oldResource.markDirty(); @@ -668,7 +719,7 @@ public void addRelation(String fieldName, PersistentResource newRelation) { if (!newRelation.isNewlyCreated() && relationshipAlreadyExists(fieldName, newRelation)) { return; } - checkSharePermission(Collections.singleton(newRelation)); + checkTransferablePermission(Collections.singleton(newRelation)); Object relation = this.getValueUnchecked(fieldName); if (relation instanceof Collection) { @@ -692,7 +743,7 @@ public void addRelation(String fieldName, PersistentResource newRelation) { * * @param resourceIdentifiers The persistent resources that are being added */ - protected void checkSharePermission(Set resourceIdentifiers) { + protected void checkTransferablePermission(Set resourceIdentifiers) { if (resourceIdentifiers == null) { return; } @@ -702,7 +753,7 @@ protected void checkSharePermission(Set resourceIdentifiers) for (PersistentResource persistentResource : resourceIdentifiers) { if (!newResources.contains(persistentResource) && !lineage.getRecord(persistentResource.getType()).contains(persistentResource)) { - checkPermission(SharePermission.class, persistentResource); + checkPermission(NonTransferable.class, persistentResource); } } } @@ -740,7 +791,7 @@ public void deleteResource() throws ForbiddenAccessException { transaction.delete(getObject(), requestScope); auditClass(Audit.Action.DELETE, new ChangeSpec(this, null, getObject(), null)); - requestScope.publishLifecycleEvent(this, CRUDEvent.CRUDAction.DELETE); + requestScope.publishLifecycleEvent(this, DELETE); requestScope.getDeletedResources().add(this); } @@ -760,7 +811,7 @@ public String getId() { * @param id resource id */ public void setId(String id) { - this.setValue(dictionary.getIdFieldName(getResourceClass()), id); + dictionary.setId(obj, id); } /** @@ -789,49 +840,44 @@ public Optional getUUID() { * * NOTE: Filter expressions for this type are _not_ applied at this level. * - * @param relation relation name + * @param relationship The relationship * @param id single id to lookup * @return The PersistentResource of the sought id or null if does not exist. */ - public PersistentResource getRelation(String relation, String id) { - Set resources = getRelation(relation, Collections.singletonList(id), - Optional.empty(), Optional.empty(), Optional.empty()); - if (resources.isEmpty()) { + public PersistentResource getRelation(com.yahoo.elide.request.Relationship relationship, String id) { + Set resources = getRelation(Collections.singletonList(id), relationship); + + if (resources.isEmpty()) { return null; - } - // If this is an in-memory object (i.e. UUID being created within tx), datastore may not be able to filter. - // If we get multiple results back, make sure we find the right id first. - for (PersistentResource resource : resources) { - if (resource.matchesId(id)) { - return resource; - } - } - return null; + } + // If this is an in-memory object (i.e. UUID being created within tx), datastore may not be able to filter. + // If we get multiple results back, make sure we find the right id first. + for (PersistentResource resource : resources) { + if (resource.matchesId(id)) { + return resource; + } + } + return null; } /** - * Load a single entity relation from the PersistentResource. Example: GET /book/2 + * Load a relation from the PersistentResource. * - * @param relation the relation + * @param relationship the relation * @param ids a list of object identifiers to optionally load. Can be empty. * @return PersistentResource relation */ - public Set getRelation(String relation, - List ids, - Optional filter, - Optional sorting, - Optional pagination) { + public Set getRelation(List ids, com.yahoo.elide.request.Relationship relationship) { - FilterExpression filterExpression; + FilterExpression filterExpression = Optional.ofNullable(relationship.getProjection().getFilterExpression()) + .orElse(null); - Class entityType = dictionary.getParameterizedType(getResourceClass(), relation); + assertRelationshipExists(relationship.getName()); + Class entityType = dictionary.getParameterizedType(getResourceClass(), relationship.getName()); Set newResources = new LinkedHashSet<>(); /* If this is a bulk edit request and the ID we are fetching for is newly created... */ - if (entityType == null) { - throw new InvalidAttributeException(relation, type); - } if (!ids.isEmpty()) { // Fetch our set of new resources that we know about since we can't find them in the datastore newResources = requestScope.getNewPersistentResources().stream() @@ -842,18 +888,22 @@ public Set getRelation(String relation, FilterExpression idExpression = buildIdFilterExpression(ids, entityType, dictionary, requestScope); // Combine filters if necessary - filterExpression = filter + filterExpression = Optional.ofNullable(relationship.getProjection().getFilterExpression()) .map(fe -> (FilterExpression) new AndFilterExpression(idExpression, fe)) .orElse(idExpression); - } else { - filterExpression = filter.orElse(null); } // TODO: Filter on new resources? // TODO: Update pagination to subtract the number of new resources created? - Set existingResources = filter(ReadPermission.class, filter, - getRelation(relation, Optional.ofNullable(filterExpression), sorting, pagination, true)); + Set existingResources = filter( + ReadPermission.class, + Optional.ofNullable(filterExpression), + getRelation(relationship.copyOf() + .projection(relationship.getProjection().copyOf() + .filterExpression(filterExpression) + .build()) + .build(), true)); // TODO: Sort again in memory now that two sets are glommed together? @@ -864,7 +914,7 @@ public Set getRelation(String relation, Set missedIds = Sets.difference(new HashSet<>(ids), allExpectedIds); if (!missedIds.isEmpty()) { - throw new InvalidObjectIdentifierException(missedIds.toString(), relation); + throw new InvalidObjectIdentifierException(missedIds.toString(), relationship.getName()); } return allResources; @@ -883,10 +933,9 @@ private static FilterExpression buildIdFilterExpression(List ids, RequestScope scope) { Class idType = dictionary.getIdType(entityType); String idField = dictionary.getIdFieldName(entityType); - String typeAlias = dictionary.getJsonAliasFor(entityType); List coercedIds = ids.stream() - .filter(id -> scope.getObjectById(typeAlias, id) == null) // these don't exist yet + .filter(id -> scope.getObjectById(entityType, id) == null) // these don't exist yet .map(id -> CoerceUtil.coerce(id, idType)) .collect(Collectors.toList()); @@ -904,60 +953,81 @@ private static FilterExpression buildIdFilterExpression(List ids, /** * Get collection of resources from relation field. * - * @param relationName field + * @param relationship relationship * @return collection relation */ - public Set getRelationCheckedFiltered(String relationName, - Optional filterExpression, - Optional sorting, - Optional pagination) { - - return filter(ReadPermission.class, filterExpression, - getRelation(relationName, filterExpression, sorting, pagination, true)); + public Set getRelationCheckedFiltered(com.yahoo.elide.request.Relationship relationship) { + return filter(ReadPermission.class, + Optional.ofNullable(relationship.getProjection().getFilterExpression()), + getRelation(relationship, true)); } private Set getRelationUncheckedUnfiltered(String relationName) { - return getRelation(relationName, Optional.empty(), Optional.empty(), Optional.empty(), false); + assertRelationshipExists(relationName); + return getRelation(com.yahoo.elide.request.Relationship.builder() + .name(relationName) + .alias(relationName) + .projection(EntityProjection.builder() + .type(dictionary.getParameterizedType(getResourceClass(), relationName)) + .build()) + .build(), false); } private Set getRelationCheckedUnfiltered(String relationName) { - return getRelation(relationName, Optional.empty(), Optional.empty(), Optional.empty(), true); + assertRelationshipExists(relationName); + return getRelation(com.yahoo.elide.request.Relationship.builder() + .name(relationName) + .alias(relationName) + .projection(EntityProjection.builder() + .type(dictionary.getParameterizedType(getResourceClass(), relationName)) + .build()) + .build(), true); + } + + private void assertRelationshipExists(String relationName) { + if (relationName == null || dictionary.getParameterizedType(obj, relationName) == null) { + throw new InvalidAttributeException(relationName, this.getType()); + } } - private Set getRelation(String relationName, - Optional filterExpression, - Optional sorting, - Optional pagination, - boolean checked) { + private Set getRelation(com.yahoo.elide.request.Relationship relationship, + boolean checked) { + if (checked) { + //All getRelation calls funnel to here. We only publish events for actions triggered directly + //by the API client. + requestScope.publishLifecycleEvent(this, READ); + requestScope.publishLifecycleEvent(this, relationship.getName(), READ, Optional.empty()); + } - if (checked && !checkRelation(relationName)) { + if (checked && !checkRelation(relationship)) { return Collections.emptySet(); } - final Class relationClass = dictionary.getParameterizedType(obj, relationName); + final Class relationClass = dictionary.getParameterizedType(obj, relationship.getName()); + + Optional pagination = Optional.ofNullable(relationship.getProjection().getPagination()); + if (pagination.isPresent() && !pagination.get().isDefaultInstance() && !CanPaginateVisitor.canPaginate(relationClass, dictionary, requestScope)) { throw new BadRequestException(String.format("Cannot paginate %s", dictionary.getJsonAliasFor(relationClass))); } - return getRelationUnchecked(relationName, filterExpression, sorting, pagination); + return getRelationUnchecked(relationship); } /** * Check the permissions of the relationship, and return true or false. - * @param relationName The relationship to the entity + * @param relationship The relationship to the entity * @return True if the relationship to the entity has valid permissions for the user */ - protected boolean checkRelation(String relationName) { - List relations = dictionary.getRelationships(obj); + protected boolean checkRelation(com.yahoo.elide.request.Relationship relationship) { + String relationName = relationship.getName(); String realName = dictionary.getNameFromAlias(obj, relationName); relationName = (realName == null) ? relationName : realName; - if (relationName == null || relations == null || !relations.contains(relationName)) { - throw new InvalidAttributeException(relationName, type); - } + assertRelationshipExists(relationName); checkFieldAwareDeferPermissions(ReadPermission.class, relationName, null, null); @@ -970,70 +1040,70 @@ protected boolean checkRelation(String relationName) { /** * Get collection of resources from relation field. * - * @param relationName field - * @param filterExpression An optional filter expression + * @param relationship the relationship to fetch * @return collection relation */ - protected Set getRelationChecked(String relationName, - Optional filterExpression, - Optional sorting, - Optional pagination) { - if (!checkRelation(relationName)) { + protected Set getRelationChecked(com.yahoo.elide.request.Relationship relationship) { + if (!checkRelation(relationship)) { return Collections.emptySet(); } - return getRelationUnchecked(relationName, filterExpression, sorting, pagination); + return getRelationUnchecked(relationship); } /** - * Retrieve an uncheck set of relations. - * - * @param relationName field - * @param filterExpression An optional filter expression - * @param sorting the sorting clause - * @param pagination the pagination params - * @return the resources in the relationship - */ - private Set getRelationUnchecked(String relationName, - Optional filterExpression, - Optional sorting, - Optional pagination) { + * Retrieve an unchecked set of relations. + */ + private Set getRelationUnchecked(com.yahoo.elide.request.Relationship relationship) { + String relationName = relationship.getName(); + FilterExpression filterExpression = relationship.getProjection().getFilterExpression(); + Pagination pagination = relationship.getProjection().getPagination(); + Sorting sorting = relationship.getProjection().getSorting(); + RelationshipType type = getRelationshipType(relationName); final Class relationClass = dictionary.getParameterizedType(obj, relationName); if (relationClass == null) { throw new InvalidAttributeException(relationName, this.getType()); } - Optional computedPagination = pagination.map(p -> p.evaluate(relationClass)); - //Invoke filterExpressionCheck and then merge with filterExpression. Optional permissionFilter = getPermissionFilterExpression(relationClass, requestScope); - Optional computedFilters = filterExpression; + Optional computedFilters = Optional.ofNullable(filterExpression); - if (permissionFilter.isPresent() && filterExpression.isPresent()) { + if (permissionFilter.isPresent() && filterExpression != null) { FilterExpression mergedExpression = - new AndFilterExpression(filterExpression.get(), permissionFilter.get()); + new AndFilterExpression(filterExpression, permissionFilter.get()); computedFilters = Optional.of(mergedExpression); } else if (permissionFilter.isPresent()) { computedFilters = permissionFilter; } - Object val = transaction.getRelation(transaction, obj, relationName, - computedFilters, sorting, computedPagination, requestScope); + com.yahoo.elide.request.Relationship modifiedRelationship = relationship.copyOf() + .projection(relationship.getProjection().copyOf() + .filterExpression(computedFilters.orElse(null)) + .sorting(sorting) + .pagination(pagination) + .build() + ).build(); + + Object val = transaction.getRelation(transaction, obj, modifiedRelationship, requestScope); if (val == null) { return Collections.emptySet(); } Set resources = Sets.newLinkedHashSet(); + if (val instanceof Iterable) { Iterable filteredVal = (Iterable) val; resources = new PersistentResourceSet(this, filteredVal, requestScope); } else if (type.isToOne()) { - resources = new SingleElementSet<>( - new PersistentResource<>(val, this, requestScope.getUUIDFor(val), requestScope)); + resources = new SingleElementSet( + new PersistentResource(val, this, + requestScope.getUUIDFor(val), requestScope)); } else { - resources.add(new PersistentResource<>(val, this, requestScope.getUUIDFor(val), requestScope)); + resources.add(new PersistentResource(val, this, + requestScope.getUUIDFor(val), requestScope)); } return resources; @@ -1072,10 +1142,20 @@ public RelationshipType getRelationshipType(String relation) { * @param attr Attribute name * @return Object value for attribute */ + @Deprecated public Object getAttribute(String attr) { return this.getValueChecked(attr); } + /** + * Get the value for a particular attribute (i.e. non-relational field) + * @param attr the Attribute + * @return Object value for attribute + */ + public Object getAttribute(Attribute attr) { + return this.getValueChecked(attr); + } + /** * Wrapped Entity bean. * @@ -1151,6 +1231,14 @@ public boolean equals(Object obj) { return false; } + /** + * Returns whether or not this resource was created in this transaction. + * @return True if this resource is newly created. + */ + public boolean isNewlyCreated() { + return requestScope.getNewResources().contains(this); + } + /** * Gets lineage. * @return the lineage @@ -1189,8 +1277,8 @@ public Resource toResource() { * Fetch a resource with support for lambda function for getting relationships and attributes. * @return The Resource */ - public Resource toResourceWithSortingAndPagination() { - return toResource(this::getRelationshipsWithSortingAndPagination, this::getAttributes); + public Resource toResource(EntityProjection projection) { + return toResource(() -> { return getRelationships(projection); }, this::getAttributes); } /** @@ -1199,7 +1287,7 @@ public Resource toResourceWithSortingAndPagination() { * @param attributeSupplier The attribute supplier * @return The Resource */ - public Resource toResource(final Supplier> relationshipSupplier, + private Resource toResource(final Supplier> relationshipSupplier, final Supplier> attributeSupplier) { final Resource resource = new Resource(type, (obj == null) ? uuid.orElseThrow( @@ -1217,8 +1305,17 @@ public Resource toResource(final Supplier> relationshi */ protected Map getRelationships() { return getRelationshipsWithRelationshipFunction((relationName) -> { - Optional filterExpression = requestScope.getExpressionForRelation(this, relationName); - return getRelationCheckedFiltered(relationName, filterExpression, Optional.empty(), Optional.empty()); + Optional filterExpression = requestScope.getExpressionForRelation(getResourceClass(), + relationName); + + return getRelationCheckedFiltered(com.yahoo.elide.request.Relationship.builder() + .alias(relationName) + .name(relationName) + .projection(EntityProjection.builder() + .type(dictionary.getParameterizedType(getResourceClass(), relationName)) + .filterExpression(filterExpression.orElse(null)) + .build()) + .build()); }); } @@ -1227,14 +1324,11 @@ protected Map getRelationships() { * * @return Relationship mapping */ - protected Map getRelationshipsWithSortingAndPagination() { - return getRelationshipsWithRelationshipFunction((relationName) -> { - Optional filterExpression = requestScope.getExpressionForRelation(this, relationName); - Optional sorting = Optional.ofNullable(requestScope.getSorting()); - Optional pagination = Optional.ofNullable(requestScope.getPagination()); - return getRelationCheckedFiltered(relationName, - filterExpression, sorting, pagination); - }); + private Map getRelationships(EntityProjection projection) { + return getRelationshipsWithRelationshipFunction( + (relationName) -> getRelationCheckedFiltered(projection.getRelationship(relationName) + .orElseThrow(IllegalStateException::new) + )); } /** @@ -1325,13 +1419,26 @@ protected void nullValue(String fieldName, PersistentResource oldValue) { * @param fieldName the field name * @return value value */ + @Deprecated protected Object getValueChecked(String fieldName) { - requestScope.publishLifecycleEvent(this, CRUDEvent.CRUDAction.READ); - requestScope.publishLifecycleEvent(this, fieldName, CRUDEvent.CRUDAction.READ, Optional.empty()); + requestScope.publishLifecycleEvent(this, READ); + requestScope.publishLifecycleEvent(this, fieldName, READ, Optional.empty()); checkFieldAwareDeferPermissions(ReadPermission.class, fieldName, (Object) null, (Object) null); return getValue(getObject(), fieldName, requestScope); } + /** + * Gets a value from an entity and checks read permissions. + * @param attribute the attribute to fetch. + * @return value value + */ + protected Object getValueChecked(Attribute attribute) { + requestScope.publishLifecycleEvent(this, READ); + requestScope.publishLifecycleEvent(this, attribute.getName(), READ, Optional.empty()); + checkFieldAwareDeferPermissions(ReadPermission.class, attribute.getName(), (Object) null, (Object) null); + return transaction.getAttribute(getObject(), attribute, requestScope); + } + /** * Retrieve an object without checking read permissions (i.e. value is used internally and not sent to others) * @@ -1339,8 +1446,6 @@ protected Object getValueChecked(String fieldName) { * @return Value */ protected Object getValueUnchecked(String fieldName) { - requestScope.publishLifecycleEvent(this, CRUDEvent.CRUDAction.READ); - requestScope.publishLifecycleEvent(this, fieldName, CRUDEvent.CRUDAction.READ, Optional.empty()); return getValue(getObject(), fieldName, requestScope); } @@ -1439,7 +1544,9 @@ protected void delFromCollection( */ protected void setValue(String fieldName, Object value) { final Object original = getValueUnchecked(fieldName); + dictionary.setValue(obj, fieldName, value); + triggerUpdate(fieldName, original, value); } @@ -1451,8 +1558,7 @@ protected void setValue(String fieldName, Object value) { * @return the value */ public static Object getValue(Object target, String fieldName, RequestScope requestScope) { - EntityDictionary dictionary = requestScope.getDictionary(); - return dictionary.getValue(target, fieldName, requestScope); + return requestScope.getDictionary().getValue(target, fieldName, requestScope); } /** @@ -1468,7 +1574,8 @@ protected void deleteInverseRelation(String relationName, Object inverseEntity) Class inverseType = dictionary.getType(inverseEntity.getClass(), inverseField); String uuid = requestScope.getUUIDFor(inverseEntity); - PersistentResource inverseResource = new PersistentResource(inverseEntity, this, uuid, requestScope); + PersistentResource inverseResource = new PersistentResource(inverseEntity, + this, uuid, requestScope); Object inverseRelation = inverseResource.getValueUnchecked(inverseField); if (inverseRelation == null) { @@ -1616,9 +1723,9 @@ protected Set filterFields(Collection fields) { */ private void triggerUpdate(String fieldName, Object original, Object value) { ChangeSpec changeSpec = new ChangeSpec(this, fieldName, original, value); - CRUDEvent.CRUDAction action = isNewlyCreated() - ? CRUDEvent.CRUDAction.CREATE - : CRUDEvent.CRUDAction.UPDATE; + LifeCycleHookBinding.Operation action = isNewlyCreated() + ? CREATE + : UPDATE; requestScope.publishLifecycleEvent(this, fieldName, action, Optional.of(changeSpec)); requestScope.publishLifecycleEvent(this, action); @@ -1686,7 +1793,7 @@ protected void auditField(final ChangeSpec changeSpec) { } for (Audit annotation : annotations) { if (annotation.action().length == 1 && annotation.action()[0] == Audit.Action.UPDATE) { - LogMessage message = new LogMessage(annotation, this, Optional.of(changeSpec)); + LogMessage message = new LogMessageImpl(annotation, this, Optional.of(changeSpec)); getRequestScope().getAuditLogger().log(message); } else { throw new InvalidSyntaxException("Only Audit.Action.UPDATE is allowed on fields."); @@ -1709,7 +1816,7 @@ protected void auditClass(Audit.Action action, ChangeSpec changeSpec) { for (Audit annotation : annotations) { for (Audit.Action auditAction : annotation.action()) { if (auditAction == action) { // compare object reference - LogMessage message = new LogMessage(annotation, this, Optional.ofNullable(changeSpec)); + LogMessage message = new LogMessageImpl(annotation, this, Optional.ofNullable(changeSpec)); getRequestScope().getAuditLogger().log(message); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java b/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java index b2917930e0..4ef788cbfe 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java @@ -6,18 +6,7 @@ package com.yahoo.elide.core; import com.yahoo.elide.ElideSettings; -import com.yahoo.elide.annotation.OnCreatePostCommit; -import com.yahoo.elide.annotation.OnCreatePreCommit; -import com.yahoo.elide.annotation.OnCreatePreSecurity; -import com.yahoo.elide.annotation.OnDeletePostCommit; -import com.yahoo.elide.annotation.OnDeletePreCommit; -import com.yahoo.elide.annotation.OnDeletePreSecurity; -import com.yahoo.elide.annotation.OnReadPostCommit; -import com.yahoo.elide.annotation.OnReadPreCommit; -import com.yahoo.elide.annotation.OnReadPreSecurity; -import com.yahoo.elide.annotation.OnUpdatePostCommit; -import com.yahoo.elide.annotation.OnUpdatePreCommit; -import com.yahoo.elide.annotation.OnUpdatePreSecurity; +import com.yahoo.elide.annotation.LifeCycleHookBinding; import com.yahoo.elide.audit.AuditLogger; import com.yahoo.elide.core.exceptions.BadRequestException; import com.yahoo.elide.core.exceptions.InvalidAttributeException; @@ -25,10 +14,9 @@ import com.yahoo.elide.core.filter.dialect.MultipleFilterDialect; import com.yahoo.elide.core.filter.dialect.ParseException; import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; import com.yahoo.elide.jsonapi.JsonApiMapper; import com.yahoo.elide.jsonapi.models.JsonApiDocument; +import com.yahoo.elide.request.EntityProjection; import com.yahoo.elide.security.ChangeSpec; import com.yahoo.elide.security.PermissionExecutor; import com.yahoo.elide.security.User; @@ -38,6 +26,7 @@ import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.ReplaySubject; import lombok.Getter; +import lombok.Setter; import java.util.Collections; import java.util.HashMap; @@ -59,13 +48,11 @@ public class RequestScope implements com.yahoo.elide.security.RequestScope { @Getter private final JsonApiDocument jsonApiDocument; @Getter private final DataStoreTransaction transaction; @Getter private final User user; - @Getter private final EntityDictionary dictionary; + @Getter protected final EntityDictionary dictionary; @Getter private final JsonApiMapper mapper; @Getter private final AuditLogger auditLogger; @Getter private final Optional> queryParams; @Getter private final Map> sparseFields; - @Getter private final Pagination pagination; - @Getter private final Sorting sorting; @Getter private final PermissionExecutor permissionExecutor; @Getter private final ObjectEntityCache objectEntityCache; @Getter private final Set newPersistentResources; @@ -73,10 +60,13 @@ public class RequestScope implements com.yahoo.elide.security.RequestScope { @Getter private final LinkedHashSet deletedResources; @Getter private final String path; @Getter private final ElideSettings elideSettings; - @Getter private final boolean useFilterExpressions; @Getter private final int updateStatusCode; - @Getter private final MultipleFilterDialect filterDialect; + @Getter private final String apiVersion; + + //TODO - this ought to be read only and set in the constructor. + @Getter @Setter private EntityProjection entityProjection; + private final Map expressionsByType; private PublishSubject lifecycleEvents; @@ -90,6 +80,7 @@ public class RequestScope implements com.yahoo.elide.security.RequestScope { * Create a new RequestScope with specified update status code. * * @param path the URL path + * @param apiVersion the API version. * @param jsonApiDocument the document for this request * @param transaction the transaction for this request * @param user the user making this request @@ -97,11 +88,13 @@ public class RequestScope implements com.yahoo.elide.security.RequestScope { * @param elideSettings Elide settings object */ public RequestScope(String path, + String apiVersion, JsonApiDocument jsonApiDocument, DataStoreTransaction transaction, User user, MultivaluedMap queryParams, ElideSettings elideSettings) { + this.apiVersion = apiVersion; this.lifecycleEvents = PublishSubject.create(); this.distinctLifecycleEvents = lifecycleEvents.distinct(); this.queuedLifecycleEvents = ReplaySubject.create(); @@ -117,7 +110,6 @@ public RequestScope(String path, this.filterDialect = new MultipleFilterDialect(elideSettings.getJoinFilterDialects(), elideSettings.getSubqueryFilterDialects()); this.elideSettings = elideSettings; - this.useFilterExpressions = elideSettings.isUseFilterExpressions(); this.updateStatusCode = elideSettings.getUpdateStatusCode(); this.globalFilterExpression = null; @@ -148,14 +140,14 @@ public RequestScope(String path, /* First check to see if there is a global, cross-type filter */ try { - globalFilterExpression = filterDialect.parseGlobalExpression(path, filterParams); + globalFilterExpression = filterDialect.parseGlobalExpression(path, filterParams, apiVersion); } catch (ParseException e) { errorMessage = e.getMessage(); } /* Next check to see if there is are type specific filters */ try { - expressionsByType.putAll(filterDialect.parseTypedExpression(path, filterParams)); + expressionsByType.putAll(filterDialect.parseTypedExpression(path, filterParams, apiVersion)); } catch (ParseException e) { /* If neither dialect parsed, report the last error found */ @@ -175,12 +167,8 @@ public RequestScope(String path, } this.sparseFields = parseSparseFields(queryParams); - this.sorting = Sorting.parseQueryParams(queryParams); - this.pagination = Pagination.parseQueryParams(queryParams, this.getElideSettings()); } else { this.sparseFields = Collections.emptyMap(); - this.sorting = Sorting.getDefaultEmptyInstance(); - this.pagination = Pagination.getDefaultPagination(this.getElideSettings()); } } @@ -188,11 +176,14 @@ public RequestScope(String path, * Special copy constructor for use by PatchRequestScope. * * @param path the URL path + * @param apiVersion the API version * @param jsonApiDocument the json api document * @param outerRequestScope the outer request scope */ - protected RequestScope(String path, JsonApiDocument jsonApiDocument, RequestScope outerRequestScope) { + protected RequestScope(String path, String apiVersion, + JsonApiDocument jsonApiDocument, RequestScope outerRequestScope) { this.jsonApiDocument = jsonApiDocument; + this.apiVersion = apiVersion; this.path = path; this.transaction = outerRequestScope.transaction; this.user = outerRequestScope.user; @@ -201,8 +192,6 @@ protected RequestScope(String path, JsonApiDocument jsonApiDocument, RequestScop this.auditLogger = outerRequestScope.auditLogger; this.queryParams = Optional.empty(); this.sparseFields = Collections.emptyMap(); - this.sorting = Sorting.getDefaultEmptyInstance(); - this.pagination = Pagination.getDefaultPagination(outerRequestScope.getElideSettings()); this.objectEntityCache = outerRequestScope.objectEntityCache; this.newPersistentResources = outerRequestScope.newPersistentResources; this.permissionExecutor = outerRequestScope.getPermissionExecutor(); @@ -211,14 +200,12 @@ protected RequestScope(String path, JsonApiDocument jsonApiDocument, RequestScop this.filterDialect = outerRequestScope.filterDialect; this.expressionsByType = outerRequestScope.expressionsByType; this.elideSettings = outerRequestScope.elideSettings; - this.useFilterExpressions = outerRequestScope.useFilterExpressions; - this.updateStatusCode = outerRequestScope.updateStatusCode; this.lifecycleEvents = outerRequestScope.lifecycleEvents; this.distinctLifecycleEvents = outerRequestScope.distinctLifecycleEvents; + this.updateStatusCode = outerRequestScope.updateStatusCode; this.queuedLifecycleEvents = outerRequestScope.queuedLifecycleEvents; } - @Override public Set getNewResources() { return (Set) newPersistentResources; } @@ -232,7 +219,7 @@ public boolean isNewResource(Object entity) { * @param queryParams The request query parameters * @return Parsed sparseFields map */ - private static Map> parseSparseFields(MultivaluedMap queryParams) { + public static Map> parseSparseFields(MultivaluedMap queryParams) { Map> result = new HashMap<>(); for (Map.Entry> kv : queryParams.entrySet()) { @@ -263,6 +250,15 @@ public Optional getFilterExpressionByType(String type) { return Optional.ofNullable(expressionsByType.get(type)); } + /** + * Get filter expression for a specific collection type. + * @param entityClass The class to lookup + * @return The filter expression for the given type + */ + public Optional getFilterExpressionByType(Class entityClass) { + return Optional.ofNullable(expressionsByType.get(dictionary.getJsonAliasFor(entityClass))); + } + /** * Get the global/cross-type filter expression. * @param loadClass Entity class @@ -280,15 +276,15 @@ public Optional getLoadFilterExpression(Class loadClass) { } /** - * Get the filter expression for a particular relationship - * @param parent The object which has the relationship + * Get the filter expression for a particular relationship. + * @param parentType The parent type which has the relationship * @param relationName The relationship name * @return A type specific filter expression for the given relationship */ - public Optional getExpressionForRelation(PersistentResource parent, String relationName) { - final Class entityClass = dictionary.getParameterizedType(parent.getObject(), relationName); + public Optional getExpressionForRelation(Class parentType, String relationName) { + final Class entityClass = dictionary.getParameterizedType(parentType, relationName); if (entityClass == null) { - throw new InvalidAttributeException(relationName, parent.getType()); + throw new InvalidAttributeException(relationName, dictionary.getJsonAliasFor(parentType)); } if (dictionary.isMappedInterface(entityClass) && interfaceHasFilterExpression(entityClass)) { throw new InvalidOperationException( @@ -305,7 +301,8 @@ public Optional getExpressionForRelation(PersistentResource pa */ private boolean interfaceHasFilterExpression(Class entityInterface) { for (String filterType : expressionsByType.keySet()) { - Class polyMorphicClass = dictionary.getEntityClass(filterType); + String version = EntityDictionary.getModelVersion(entityInterface); + Class polyMorphicClass = dictionary.getEntityClass(filterType, version); if (entityInterface.isAssignableFrom(polyMorphicClass)) { return true; } @@ -332,62 +329,80 @@ private static MultivaluedMap getFilterParams(MultivaluedMap resource, CRUDEvent.CRUDAction crudAction) { + protected void publishLifecycleEvent(PersistentResource resource, LifeCycleHookBinding.Operation crudAction) { lifecycleEvents.onNext( new CRUDEvent(crudAction, resource, PersistentResource.CLASS_NO_FIELD, Optional.empty()) ); @@ -413,7 +428,7 @@ protected void publishLifecycleEvent(PersistentResource resource, CRUDEvent.C */ protected void publishLifecycleEvent(PersistentResource resource, String fieldName, - CRUDEvent.CRUDAction crudAction, + LifeCycleHookBinding.Operation crudAction, Optional changeSpec) { lifecycleEvents.onNext( new CRUDEvent(crudAction, resource, fieldName, changeSpec) @@ -435,25 +450,29 @@ public String getUUIDFor(Object o) { return objectEntityCache.getUUID(o); } - public Object getObjectById(String type, String id) { - Object result = objectEntityCache.get(type, id); + public Object getObjectById(Class type, String id) { + Class boundType = dictionary.lookupBoundClass(type); + + Object result = objectEntityCache.get(boundType.getName(), id); // Check inheritance too - Iterator it = dictionary.getSubclassingEntityNames(type).iterator(); + Iterator> it = dictionary.getSubclassingEntities(boundType).iterator(); while (result == null && it.hasNext()) { - String newType = getInheritanceKey(it.next(), type); + String newType = getInheritanceKey(it.next().getName(), boundType.getName()); result = objectEntityCache.get(newType, id); } return result; } - public void setUUIDForObject(String type, String id, Object object) { - objectEntityCache.put(type, id, object); + public void setUUIDForObject(Class type, String id, Object object) { + Class boundType = dictionary.lookupBoundClass(type); + + objectEntityCache.put(boundType.getName(), id, object); // Insert for all inherited entities as well - dictionary.getSuperClassEntityNames(type).stream() - .map(i -> getInheritanceKey(type, i)) + dictionary.getSuperClassEntities(type).stream() + .map(i -> getInheritanceKey(boundType.getName(), i.getName())) .forEach((newType) -> objectEntityCache.put(newType, id, object)); } @@ -465,14 +484,20 @@ private void registerPreSecurityObservers() { this.distinctLifecycleEvents .filter(CRUDEvent::isReadEvent) - .subscribeWith(new LifecycleHookInvoker(dictionary, OnReadPreSecurity.class, true)); + .subscribeWith(new LifecycleHookInvoker(dictionary, + LifeCycleHookBinding.Operation.READ, + LifeCycleHookBinding.TransactionPhase.PRESECURITY, true)); this.distinctLifecycleEvents .filter(CRUDEvent::isUpdateEvent) - .subscribeWith(new LifecycleHookInvoker(dictionary, OnUpdatePreSecurity.class, true)); + .subscribeWith(new LifecycleHookInvoker(dictionary, + LifeCycleHookBinding.Operation.UPDATE, + LifeCycleHookBinding.TransactionPhase.PRESECURITY, true)); this.distinctLifecycleEvents .filter(CRUDEvent::isDeleteEvent) - .subscribeWith(new LifecycleHookInvoker(dictionary, OnDeletePreSecurity.class, true)); + .subscribeWith(new LifecycleHookInvoker(dictionary, + LifeCycleHookBinding.Operation.DELETE, + LifeCycleHookBinding.TransactionPhase.PRESECURITY, true)); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/TimedFunction.java b/elide-core/src/main/java/com/yahoo/elide/core/TimedFunction.java new file mode 100644 index 0000000000..df5b187b31 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/TimedFunction.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Supplier; + +/** + * Wraps a function and logs how long it took to run (in millis). + * @param The function return type. + */ +@Slf4j +@Data +public class TimedFunction implements Supplier { + + public TimedFunction(Supplier toRun, String logMessage) { + this.toRun = toRun; + this.logMessage = logMessage; + } + + private Supplier toRun; + private String logMessage; + + @Override + public R get() { + long start = System.currentTimeMillis(); + R ret = toRun.get(); + long end = System.currentTimeMillis(); + + log.debug(logMessage + "\tTime spent: {}", end - start); + + return ret; + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/VerifyFieldAccessFilterExpressionVisitor.java b/elide-core/src/main/java/com/yahoo/elide/core/VerifyFieldAccessFilterExpressionVisitor.java index 2f23fed858..f10628b4fb 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/VerifyFieldAccessFilterExpressionVisitor.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/VerifyFieldAccessFilterExpressionVisitor.java @@ -14,12 +14,13 @@ import com.yahoo.elide.core.filter.expression.FilterExpressionVisitor; import com.yahoo.elide.core.filter.expression.NotFilterExpression; import com.yahoo.elide.core.filter.expression.OrFilterExpression; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Relationship; import com.yahoo.elide.security.PermissionExecutor; import com.yahoo.elide.security.permissions.ExpressionResult; import java.util.Collections; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -85,6 +86,9 @@ public Boolean visitPredicate(FilterPredicate filterPredicate) { private Stream getValueChecked(PersistentResource resource, String fieldName, RequestScope requestScope) { + + EntityDictionary dictionary = resource.getDictionary(); + // checkFieldAwareReadPermissions requestScope.getPermissionExecutor().checkSpecificFieldPermissions(resource, null, ReadPermission.class, fieldName); @@ -93,8 +97,16 @@ private Stream getValueChecked(PersistentResource resourc .getRelationshipType(entity.getClass(), fieldName) == RelationshipType.NONE) { return Stream.empty(); } + + Relationship relationship = Relationship.builder() + .name(fieldName) + .alias(fieldName) + .projection(EntityProjection.builder() + .type(dictionary.getParameterizedType(resource.getResourceClass(), fieldName)) + .build()) + .build(); // use no filter to allow the read directly from loaded resource - return resource.getRelationChecked(fieldName, Optional.empty(), Optional.empty(), Optional.empty()).stream(); + return resource.getRelationChecked(relationship).stream(); } /** diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java index b20183c4d6..8d2f72fd82 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java @@ -27,8 +27,8 @@ * Simple in-memory only database. */ public class HashMapDataStore implements DataStore, DataStoreTestHarness { - private final Map, Map> dataStore = Collections.synchronizedMap(new HashMap<>()); - @Getter private EntityDictionary dictionary; + protected final Map, Map> dataStore = Collections.synchronizedMap(new HashMap<>()); + @Getter protected EntityDictionary dictionary; @Getter private final Set beanPackages; @Getter private final ConcurrentHashMap, AtomicLong> typeIds = new ConcurrentHashMap<>(); diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java index 943ca2d8bd..d4ce2ef85d 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java @@ -10,15 +10,16 @@ import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.exceptions.TransactionException; import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; + +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Relationship; +import com.yahoo.elide.request.Sorting; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; import javax.persistence.GeneratedValue; @@ -131,31 +132,25 @@ public void setId(Object value, String id) { @Override public Object getRelation(DataStoreTransaction relationTx, Object entity, - String relationName, - Optional filterExpression, - Optional sorting, - Optional pagination, + Relationship relationship, RequestScope scope) { - return dictionary.getValue(entity, relationName, scope); + return dictionary.getValue(entity, relationship.getName(), scope); } @Override - public Iterable loadObjects(Class entityClass, Optional filterExpression, - Optional sorting, Optional pagination, + public Iterable loadObjects(EntityProjection projection, RequestScope scope) { synchronized (dataStore) { - Map data = dataStore.get(entityClass); + Map data = dataStore.get(projection.getType()); return data.values(); } } @Override - public Object loadObject(Class entityClass, Serializable id, - Optional filterExpression, - RequestScope scope) { + public Object loadObject(EntityProjection projection, Serializable id, RequestScope scope) { synchronized (dataStore) { - Map data = dataStore.get(entityClass); + Map data = dataStore.get(projection.getType()); if (data == null) { return null; } @@ -179,7 +174,7 @@ public boolean supportsSorting(Class entityClass, Sorting sorting) { } @Override - public boolean supportsPagination(Class entityClass) { + public boolean supportsPagination(Class entityClass, FilterExpression expression) { return false; } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransaction.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransaction.java index 0c4475a3c2..aea6969483 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransaction.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransaction.java @@ -7,7 +7,6 @@ package com.yahoo.elide.core.datastore.inmemory; import com.yahoo.elide.core.DataStoreTransaction; -import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.core.Path; import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.RequestScope; @@ -15,9 +14,12 @@ import com.yahoo.elide.core.filter.expression.FilterPredicatePushdownExtractor; import com.yahoo.elide.core.filter.expression.InMemoryExecutionVerifier; import com.yahoo.elide.core.filter.expression.InMemoryFilterExecutor; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; -import com.yahoo.elide.security.User; +import com.yahoo.elide.request.Attribute; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Pagination; +import com.yahoo.elide.request.Relationship; +import com.yahoo.elide.request.Sorting; + import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; @@ -33,12 +35,15 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; + /** * Data Store Transaction that wraps another transaction and provides in-memory filtering, soring, and pagination * when the underlying transaction cannot perform the equivalent function. */ public class InMemoryStoreTransaction implements DataStoreTransaction { + private final DataStoreTransaction tx; + private static final Comparator NULL_SAFE_COMPARE = (a, b) -> { if (a == null && b == null) { return 0; @@ -53,8 +58,6 @@ public class InMemoryStoreTransaction implements DataStoreTransaction { } }; - private DataStoreTransaction tx; - /** * Fetches data from the store. */ @@ -71,6 +74,80 @@ public InMemoryStoreTransaction(DataStoreTransaction tx) { this.tx = tx; } + @Override + public Object getRelation(DataStoreTransaction relationTx, + Object entity, + Relationship relationship, + RequestScope scope) { + DataFetcher fetcher = new DataFetcher() { + @Override + public Object fetch(Optional filterExpression, + Optional sorting, + Optional pagination, + RequestScope scope) { + + return tx.getRelation(relationTx, entity, relationship.copyOf() + .projection(relationship.getProjection().copyOf() + .filterExpression(filterExpression.orElse(null)) + .sorting(sorting.orElse(null)) + .pagination(pagination.orElse(null)) + .build() + ).build(), scope); + } + }; + + + /* + * If we are mutating multiple entities, the data store transaction cannot perform filter & pagination directly. + * It must be done in memory by Elide as some newly created entities have not yet been persisted. + */ + boolean filterInMemory = scope.getNewPersistentResources().size() > 0; + return fetchData(fetcher, relationship.getProjection().getType(), + Optional.ofNullable(relationship.getProjection().getFilterExpression()), + Optional.ofNullable(relationship.getProjection().getSorting()), + Optional.ofNullable(relationship.getProjection().getPagination()), + filterInMemory, scope); + } + + @Override + public Object loadObject(EntityProjection projection, + Serializable id, + RequestScope scope) { + + if (projection.getFilterExpression() == null + || tx.supportsFiltering(projection.getType(), + projection.getFilterExpression()) == FeatureSupport.FULL) { + return tx.loadObject(projection, id, scope); + } else { + return DataStoreTransaction.super.loadObject(projection, id, scope); + } + } + + @Override + public Iterable loadObjects(EntityProjection projection, + RequestScope scope) { + + DataFetcher fetcher = new DataFetcher() { + @Override + public Iterable fetch(Optional filterExpression, + Optional sorting, + Optional pagination, + RequestScope scope) { + + return tx.loadObjects(projection.copyOf() + .filterExpression(filterExpression.orElse(null)) + .pagination(pagination.orElse(null)) + .sorting(sorting.orElse(null)) + .build(), scope); + } + }; + + return (Iterable) fetchData(fetcher, projection.getType(), + Optional.ofNullable(projection.getFilterExpression()), + Optional.ofNullable(projection.getSorting()), + Optional.ofNullable(projection.getPagination()), + false, scope); + } @Override public void save(Object entity, RequestScope scope) { @@ -82,11 +159,6 @@ public void delete(Object entity, RequestScope scope) { tx.delete(entity, scope); } - @Override - public User accessUser(Object opaqueUser) { - return tx.accessUser(opaqueUser); - } - @Override public void preCommit() { tx.preCommit(); @@ -98,34 +170,8 @@ public T createNewObject(Class entityClass) { } @Override - public Object getRelation(DataStoreTransaction relationTx, - Object entity, - String relationName, - Optional filterExpression, - Optional sorting, - Optional pagination, - RequestScope scope) { - - Class relationClass = scope.getDictionary().getParameterizedType(entity, relationName); - - DataFetcher fetcher = new DataFetcher() { - @Override - public Object fetch(Optional filterExpression, - Optional sorting, - Optional pagination, - RequestScope scope) { - - return tx.getRelation(relationTx, entity, relationName, filterExpression, sorting, pagination, scope); - } - }; - - - /* - * If we are mutating multiple entities, the data store transaction cannot perform filter & pagination directly. - * It must be done in memory by Elide as some newly created entities have not yet been persisted. - */ - boolean filterInMemory = scope.getNewPersistentResources().size() > 0; - return fetchData(fetcher, relationClass, filterExpression, sorting, pagination, filterInMemory, scope); + public void close() throws IOException { + tx.close(); } @Override @@ -148,14 +194,13 @@ public void updateToOneRelation(DataStoreTransaction relationTx, } @Override - public Object getAttribute(Object entity, String attributeName, RequestScope scope) { - return tx.getAttribute(entity, attributeName, scope); + public Object getAttribute(Object entity, Attribute attribute, RequestScope scope) { + return tx.getAttribute(entity, attribute, scope); } @Override - public void setAttribute(Object entity, String attributeName, Object attributeValue, RequestScope scope) { - tx.setAttribute(entity, attributeName, attributeValue, scope); - + public void setAttribute(Object entity, Attribute attribute, RequestScope scope) { + tx.setAttribute(entity, attribute, scope); } @Override @@ -173,45 +218,6 @@ public void createObject(Object entity, RequestScope scope) { tx.createObject(entity, scope); } - @Override - public Object loadObject(Class entityClass, - Serializable id, - Optional filterExpression, - RequestScope scope) { - - if (! filterExpression.isPresent() - || tx.supportsFiltering(entityClass, filterExpression.get()) == FeatureSupport.FULL) { - return tx.loadObject(entityClass, id, filterExpression, scope); - } - return DataStoreTransaction.super.loadObject(entityClass, id, filterExpression, scope); - } - - @Override - public Iterable loadObjects(Class entityClass, - Optional filterExpression, - Optional sorting, - Optional pagination, - RequestScope scope) { - - DataFetcher fetcher = new DataFetcher() { - @Override - public Iterable fetch(Optional filterExpression, - Optional sorting, - Optional pagination, - RequestScope scope) { - return tx.loadObjects(entityClass, filterExpression, sorting, pagination, scope); - } - }; - - return (Iterable) fetchData(fetcher, entityClass, - filterExpression, sorting, pagination, false, scope); - } - - @Override - public void close() throws IOException { - tx.close(); - } - private Iterable filterLoadedData(Iterable loadedRecords, Optional filterExpression, RequestScope scope) { @@ -247,8 +253,7 @@ private Object fetchData(DataFetcher fetcher, Optional inMemorySort = sortSplit.getRight(); Pair, Optional> paginationSplit = splitPagination(entityClass, - pagination, inMemoryFilter.isPresent(), inMemorySort.isPresent()); - + filterExpression.orElse(null), pagination, inMemoryFilter.isPresent(), inMemorySort.isPresent()); Optional dataStorePagination = paginationSplit.getLeft(); Optional inMemoryPagination = paginationSplit.getRight(); @@ -268,7 +273,6 @@ private Object fetchData(DataFetcher fetcher, return sortAndPaginateLoadedData( loadedRecords, - entityClass, inMemorySort, inMemoryPagination, scope); @@ -276,7 +280,6 @@ private Object fetchData(DataFetcher fetcher, private Iterable sortAndPaginateLoadedData(Iterable loadedRecords, - Class entityClass, Optional sorting, Optional pagination, RequestScope scope) { @@ -286,10 +289,8 @@ private Iterable sortAndPaginateLoadedData(Iterable loadedRecord return loadedRecords; } - EntityDictionary dictionary = scope.getDictionary(); - Map sortRules = sorting - .map((s) -> s.getValidSortingRules(entityClass, dictionary)) + .map((s) -> s.getSortingPaths()) .orElse(new HashMap<>()); // No sorting required for this type & no pagination. @@ -322,8 +323,8 @@ private List paginateInMemory(List records, Pagination paginatio endIdx = records.size(); } - if (pagination.isGenerateTotals()) { - pagination.setPageTotals(records.size()); + if (pagination.returnPageTotals()) { + pagination.setPageTotals((long) records.size()); } return records.subList(offset, endIdx); } @@ -446,11 +447,12 @@ private Pair, Optional> splitSorting( */ private Pair, Optional> splitPagination( Class entityClass, + FilterExpression expression, Optional pagination, boolean filteredInMemory, boolean sortedInMemory ) { - if (!tx.supportsPagination(entityClass) + if (!tx.supportsPagination(entityClass, expression) || filteredInMemory || sortedInMemory) { return Pair.of(Optional.empty(), pagination); diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/wrapped/TransactionWrapper.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/wrapped/TransactionWrapper.java index f27e4e0219..9ee3631d61 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/datastore/wrapped/TransactionWrapper.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/wrapped/TransactionWrapper.java @@ -9,15 +9,15 @@ import com.yahoo.elide.core.DataStoreTransaction; import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; -import com.yahoo.elide.security.User; +import com.yahoo.elide.request.Attribute; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Relationship; +import com.yahoo.elide.request.Sorting; import lombok.AllArgsConstructor; import lombok.Data; import java.io.IOException; import java.io.Serializable; -import java.util.Optional; import java.util.Set; /** @@ -28,11 +28,6 @@ public abstract class TransactionWrapper implements DataStoreTransaction { protected DataStoreTransaction tx; - @Override - public User accessUser(Object opaqueUser) { - return tx.accessUser(opaqueUser); - } - @Override public void preCommit() { tx.preCommit(); @@ -44,16 +39,15 @@ public T createNewObject(Class entityClass) { } @Override - public Object loadObject(Class entityClass, Serializable id, Optional filterExpression, + public Object loadObject(EntityProjection projection, Serializable id, RequestScope scope) { - return tx.loadObject(entityClass, id, filterExpression, scope); + return tx.loadObject(projection, id, scope); } @Override - public Object getRelation(DataStoreTransaction relationTx, Object entity, String relationName, - Optional filterExpression, Optional sorting, - Optional pagination, RequestScope scope) { - return tx.getRelation(relationTx, entity, relationName, filterExpression, sorting, pagination, scope); + public Object getRelation(DataStoreTransaction relationTx, Object entity, + Relationship relationship, RequestScope scope) { + return tx.getRelation(relationTx, entity, relationship, scope); } @Override @@ -71,13 +65,13 @@ public void updateToOneRelation(DataStoreTransaction relationTx, Object entity, } @Override - public Object getAttribute(Object entity, String attributeName, RequestScope scope) { - return tx.getAttribute(entity, attributeName, scope); + public Object getAttribute(Object entity, Attribute attribute, RequestScope scope) { + return tx.getAttribute(entity, attribute, scope); } @Override - public void setAttribute(Object entity, String attributeName, Object attributeValue, RequestScope scope) { - tx.setAttribute(entity, attributeName, attributeValue, scope); + public void setAttribute(Object entity, Attribute attribute, RequestScope scope) { + tx.setAttribute(entity, attribute, scope); } @Override @@ -91,8 +85,8 @@ public boolean supportsSorting(Class entityClass, Sorting sorting) { } @Override - public boolean supportsPagination(Class entityClass) { - return tx.supportsPagination(entityClass); + public boolean supportsPagination(Class entityClass, FilterExpression expression) { + return tx.supportsPagination(entityClass, expression); } @Override @@ -119,16 +113,11 @@ public void commit(RequestScope requestScope) { @Override public void createObject(Object o, RequestScope requestScope) { tx.createObject(o, requestScope); - } @Override - public Iterable loadObjects(Class entityClass, - Optional filterExpression, - Optional sorting, - Optional pagination, - RequestScope requestScope) { - return tx.loadObjects(entityClass, filterExpression, sorting, pagination, requestScope); + public Iterable loadObjects(EntityProjection projection, RequestScope scope) { + return tx.loadObjects(projection, scope); } @Override diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/CustomErrorException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/CustomErrorException.java index 9ddf36c375..d44fe4bec2 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/CustomErrorException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/CustomErrorException.java @@ -44,12 +44,12 @@ public CustomErrorException(int status, String message, Throwable cause, ErrorOb } @Override - public Pair getErrorResponse(boolean encodeResponse) { + public Pair getErrorResponse() { return buildCustomResponse(); } @Override - public Pair getVerboseErrorResponse(boolean encodeResponse) { + public Pair getVerboseErrorResponse() { return buildCustomResponse(); } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/ForbiddenAccessException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/ForbiddenAccessException.java index afa7ea325f..51db5de06f 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/ForbiddenAccessException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/ForbiddenAccessException.java @@ -5,11 +5,13 @@ */ package com.yahoo.elide.core.exceptions; +import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.core.HttpStatus; import com.yahoo.elide.security.permissions.expressions.Expression; import lombok.Getter; +import java.lang.annotation.Annotation; import java.util.Optional; /** @@ -23,12 +25,14 @@ public class ForbiddenAccessException extends HttpStatusException { @Getter private final Optional expression; @Getter private final Optional evaluationMode; - public ForbiddenAccessException(String message) { - this(message, null, null); + public ForbiddenAccessException(Class permission) { + this(permission, null, null); } - public ForbiddenAccessException(String message, Expression expression, Expression.EvaluationMode mode) { - super(HttpStatus.SC_FORBIDDEN, null, null, () -> message + ": " + expression); + public ForbiddenAccessException(Class permission, + Expression expression, Expression.EvaluationMode mode) { + super(HttpStatus.SC_FORBIDDEN, getMessage(permission), null, () -> getMessage(permission) + ": " + expression); + this.expression = Optional.ofNullable(expression); this.evaluationMode = Optional.ofNullable(mode); } @@ -37,4 +41,8 @@ public String getLoggedMessage() { return String.format("ForbiddenAccessException: Message=%s\tMode=%s\tExpression=[%s]", getVerboseMessage(), getEvaluationMode(), getExpression()); } + + private static String getMessage(Class permission) { + return EntityDictionary.getSimpleName(permission) + " Denied"; + } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/HttpStatusException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/HttpStatusException.java index 057d7cf663..a2f0e476cd 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/HttpStatusException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/HttpStatusException.java @@ -6,6 +6,7 @@ package com.yahoo.elide.core.exceptions; import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.ErrorObjects; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -16,9 +17,6 @@ import lombok.extern.slf4j.Slf4j; -import java.util.Collections; -import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.function.Supplier; @@ -54,56 +52,35 @@ protected static String formatExceptionCause(Throwable e) { /** * Get a response detailing the error that occurred. + * Encode the error message to be safe for HTML. * @return Pair containing status code and a JsonNode containing error details */ public Pair getErrorResponse() { - return getErrorResponse(false); - } - - /** - * Get a response detailing the error that occurred. - * Optionally, encode the error message to be safe for HTML. - * @param encodeResponse true if the message should be encoded for html - * @return Pair containing status code and a JsonNode containing error details - */ - public Pair getErrorResponse(boolean encodeResponse) { - String message = encodeResponse ? Encode.forHtml(toString()) : toString(); - Map> errors = Collections.singletonMap( - "errors", Collections.singletonList(message) - ); - return buildResponse(errors); + return buildResponse(getMessage()); } /** * Get a verbose response detailing the error that occurred. + * Encode the error message to be safe for HTML. * @return Pair containing status code and a JsonNode containing error details */ public Pair getVerboseErrorResponse() { - return getVerboseErrorResponse(false); + return buildResponse(getVerboseMessage()); } - /** - * Get a verbose response detailing the error that occurred. - * Optionally, encode the error message to be safe for HTML. - * @param encodeResponse true if the message should be encoded for html - * @return Pair containing status code and a JsonNode containing error details - */ - public Pair getVerboseErrorResponse(boolean encodeResponse) { - String message = encodeResponse ? Encode.forHtml(getVerboseMessage()) : getVerboseMessage(); - Map> errors = Collections.singletonMap( - "errors", Collections.singletonList(message) - ); - return buildResponse(errors); - } + private Pair buildResponse(String message) { + String errorDetail = message; + errorDetail = Encode.forHtml(errorDetail); - private Pair buildResponse(Map> errors) { + ErrorObjects errors = ErrorObjects.builder().addError().withDetail(errorDetail).build(); JsonNode responseBody = OBJECT_MAPPER.convertValue(errors, JsonNode.class); + return Pair.of(getStatus(), responseBody); } public String getVerboseMessage() { return verboseMessageSupplier.map(Supplier::get) - .orElse(toString()); + .orElse(getMessage()); } public int getStatus() { diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidAttributeException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidAttributeException.java index 51420a5019..2fe38d4918 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidAttributeException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidAttributeException.java @@ -13,7 +13,7 @@ */ public class InvalidAttributeException extends HttpStatusException { public InvalidAttributeException(String attributeName, String type, Throwable cause) { - super(HttpStatus.SC_NOT_FOUND, "Unknown attribute '" + attributeName + "' in '" + type + "'", cause, null); + super(HttpStatus.SC_NOT_FOUND, "Unknown attribute " + attributeName + " in " + type + "", cause, null); } public InvalidAttributeException(String attributeName, String type) { diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidCollectionException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidCollectionException.java index 1c4ebb3100..9264e524fd 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidCollectionException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidCollectionException.java @@ -13,7 +13,7 @@ */ public class InvalidCollectionException extends HttpStatusException { public InvalidCollectionException(String collection) { - this("Unknown collection '%s'", collection); + this("Unknown collection %s", collection); } public InvalidCollectionException(String format, String collection) { diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidObjectIdentifierException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidObjectIdentifierException.java index a9fed4d1ee..0f8bba017f 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidObjectIdentifierException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidObjectIdentifierException.java @@ -15,6 +15,6 @@ public class InvalidObjectIdentifierException extends HttpStatusException { private static final long serialVersionUID = 1L; public InvalidObjectIdentifierException(String id, String objectOrFieldName) { - super(HttpStatus.SC_NOT_FOUND, "Unknown identifier '" + id + "' for " + objectOrFieldName); + super(HttpStatus.SC_NOT_FOUND, "Unknown identifier " + id + " for " + objectOrFieldName); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidOperationException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidOperationException.java index 854d38315e..55db006f12 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidOperationException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/InvalidOperationException.java @@ -14,6 +14,6 @@ public class InvalidOperationException extends HttpStatusException { private static final long serialVersionUID = 1L; public InvalidOperationException(String body) { - super(HttpStatus.SC_BAD_REQUEST, "Invalid operation: '" + body + "'"); + super(HttpStatus.SC_BAD_REQUEST, "Invalid operation: " + body); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/JsonPatchExtensionException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/JsonPatchExtensionException.java index 456004c539..385a3df96b 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/JsonPatchExtensionException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/JsonPatchExtensionException.java @@ -6,14 +6,8 @@ package com.yahoo.elide.core.exceptions; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.IntNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import org.apache.commons.lang3.tuple.Pair; -import org.owasp.encoder.Encode; /** * Exception describing error caused from Json Patch Extension request. @@ -22,62 +16,17 @@ public class JsonPatchExtensionException extends HttpStatusException { private final Pair response; public JsonPatchExtensionException(int status, final JsonNode errorNode) { - super(status, null); + super(status, ""); response = Pair.of(status, errorNode); } - /** - * @deprecated use {@link #getErrorResponse(boolean encodeResponse)} - */ - @Deprecated - public Pair getResponse() { - return response; - } - @Override public Pair getErrorResponse() { - return getErrorResponse(false); - } - - @Override - public Pair getErrorResponse(boolean encodeResponse) { - if (!encodeResponse) { - return response; - } - - return encodeResponse(); + return response; } @Override public Pair getVerboseErrorResponse() { - return getVerboseErrorResponse(false); - } - - @Override - public Pair getVerboseErrorResponse(boolean encodeResponse) { - if (!encodeResponse) { - return response; - } - - return encodeResponse(); - } - - private Pair encodeResponse() { - // response is final, so construct a new response with encoded values - ArrayNode encodedArray = JsonNodeFactory.instance.arrayNode(); - ArrayNode errors = (ArrayNode) response.getRight().get("errors"); - for (JsonNode node : errors) { - ObjectNode objectNode = (ObjectNode) node; - - TextNode text = (TextNode) objectNode.get("detail"); - IntNode status = (IntNode) objectNode.get("status"); - - ObjectNode encodedObjectNode = JsonNodeFactory.instance.objectNode(); - TextNode encodedTextNode = JsonNodeFactory.instance.textNode(Encode.forHtml(text.asText())); - encodedObjectNode.set("detail", encodedTextNode); - encodedObjectNode.set("status", status); - encodedArray.add(encodedObjectNode); - } - return Pair.of(response.getLeft(), encodedArray); + return response; } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/TimeoutException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/TimeoutException.java new file mode 100644 index 0000000000..e5fab2f747 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/TimeoutException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.exceptions; + +import com.yahoo.elide.core.HttpStatus; + +/** + * Thrown for request timeouts. + * + * {@link HttpStatus#SC_TIMEOUT} + */ +public class TimeoutException extends HttpStatusException { + private static final long serialVersionUID = 1L; + + public TimeoutException(Throwable e) { + super(HttpStatus.SC_TIMEOUT, "Request Timeout", e, null); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/UnknownEntityException.java b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/UnknownEntityException.java index 8d6e9680b3..0ababf2145 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/exceptions/UnknownEntityException.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/exceptions/UnknownEntityException.java @@ -12,6 +12,6 @@ */ public class UnknownEntityException extends HttpStatusException { public UnknownEntityException(String entityType) { - super(HttpStatus.SC_BAD_REQUEST, "Unknown entity type: '" + entityType + "'"); + super(HttpStatus.SC_BAD_REQUEST, "Unknown entity type: " + entityType); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/FilterPredicate.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/FilterPredicate.java index 26c5bdfc7e..ae35c69561 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/FilterPredicate.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/FilterPredicate.java @@ -113,34 +113,6 @@ public FilterPredicate scopedBy(PathElement scope) { return new FilterPredicate(new Path(pathElements), operator, values); } - /** - * Returns an alias that uniquely identifies the last collection of entities in the path. - * @return An alias for the path. - */ - public String getAlias() { - List elements = path.getPathElements(); - - PathElement last = elements.get(elements.size() - 1); - - if (elements.size() == 1) { - return getTypeAlias(last.getType()); - } - - PathElement previous = elements.get(elements.size() - 2); - - return getTypeAlias(previous.getType()) + UNDERSCORE + previous.getFieldName(); - } - - /** - * Build an HQL friendly alias for a class. - * - * @param type The type to alias - * @return type name alias that will likely not conflict with other types or with reserved keywords. - */ - public static String getTypeAlias(Class type) { - return type.getCanonicalName().replace(PERIOD, UNDERSCORE); - } - public Class getEntityType() { List elements = path.getPathElements(); PathElement first = elements.get(0); @@ -176,8 +148,8 @@ public String toString() { for (PathElement element : elements) { formattedPath.append(PERIOD).append(element.getFieldName()); - } + return formattedPath.append(' ').append(operator).append(' ').append(values).toString(); } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/DefaultFilterDialect.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/DefaultFilterDialect.java index 667d82f83b..3fd96f6606 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/DefaultFilterDialect.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/DefaultFilterDialect.java @@ -41,7 +41,8 @@ public DefaultFilterDialect(EntityDictionary dictionary) { * @return a list of the predicates from the query params * @throws ParseException when a filter parameter cannot be parsed */ - private List extractPredicates(MultivaluedMap queryParams) throws ParseException { + private List extractPredicates(MultivaluedMap queryParams, + String apiVersion) throws ParseException { List filterPredicates = new ArrayList<>(); Pattern pattern = Pattern.compile("filter\\[([^\\]]+)\\](\\[([^\\]]+)\\])?"); @@ -65,7 +66,7 @@ private List extractPredicates(MultivaluedMap q final Operator operator = (matcher.group(3) == null) ? Operator.IN : Operator.fromString(matcher.group(3)); - Path path = getPath(keyParts); + Path path = getPath(keyParts, apiVersion); List elements = path.getPathElements(); Path.PathElement last = elements.get(elements.size() - 1); @@ -87,10 +88,11 @@ private List extractPredicates(MultivaluedMap q } @Override - public FilterExpression parseGlobalExpression(String path, MultivaluedMap filterParams) + public FilterExpression parseGlobalExpression(String path, MultivaluedMap filterParams, + String apiVersion) throws ParseException { List filterPredicates; - filterPredicates = extractPredicates(filterParams); + filterPredicates = extractPredicates(filterParams, apiVersion); /* Extract the first collection in the URL */ String normalizedPath = JsonApiParser.normalizePath(path); @@ -130,10 +132,11 @@ public FilterExpression parseGlobalExpression(String path, MultivaluedMap parseTypedExpression(String path, MultivaluedMap filterParams) + public Map parseTypedExpression(String path, MultivaluedMap filterParams, + String apiVersion) throws ParseException { Map expressionMap = new HashMap<>(); - List filterPredicates = extractPredicates(filterParams); + List filterPredicates = extractPredicates(filterParams, apiVersion); for (FilterPredicate filterPredicate : filterPredicates) { validateFilterPredicate(filterPredicate); @@ -153,10 +156,11 @@ public Map parseTypedExpression(String path, Multivalu * Parses [ author, books, publisher, name ] into [(author, books), (book, publisher), (publisher, name)]. * * @param keyParts [ author, books, publisher, name ] + * @param apiVersion The client requested version. * @return [(author, books), (book, publisher), (publisher, name)] * @throws ParseException if the filter cannot be parsed */ - private Path getPath(final String[] keyParts) throws ParseException { + private Path getPath(final String[] keyParts, String apiVersion) throws ParseException { if (keyParts == null || keyParts.length <= 0) { throw new ParseException("Invalid filter expression"); } @@ -165,7 +169,8 @@ private Path getPath(final String[] keyParts) throws ParseException { Class[] types = new Class[keyParts.length]; String type = keyParts[0]; - types[0] = dictionary.getEntityClass(type); + + types[0] = dictionary.getEntityClass(type, apiVersion); if (types[0] == null) { throw new ParseException("Unknown entity in filter: " + type); diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/JoinFilterDialect.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/JoinFilterDialect.java index 3bd4dcd95a..6cbad8d590 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/JoinFilterDialect.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/JoinFilterDialect.java @@ -24,10 +24,12 @@ public interface JoinFilterDialect { * * @param path the URL path * @param filterParams the subset of query parameters that start with 'filter' + * @param apiVersion the version of the API requested. * @return The root of an expression abstract syntax tree parsed from both the path and the query parameters. * @throws ParseException if the expression cannot be parsed. */ public FilterExpression parseGlobalExpression( String path, - MultivaluedMap filterParams) throws ParseException; + MultivaluedMap filterParams, + String apiVersion) throws ParseException; } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/MultipleFilterDialect.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/MultipleFilterDialect.java index 0fe013d81a..46d5d3724a 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/MultipleFilterDialect.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/MultipleFilterDialect.java @@ -38,24 +38,27 @@ public MultipleFilterDialect(EntityDictionary dictionary) { @Override public FilterExpression parseGlobalExpression(String path, - MultivaluedMap queryParams) throws ParseException { + MultivaluedMap queryParams, + String apiVersion) throws ParseException { if (joinDialects.isEmpty()) { throw new ParseException("Heterogeneous type filtering not supported"); } - return parseExpression(joinDialects, (dialect) -> dialect.parseGlobalExpression(path, queryParams)); + return parseExpression(joinDialects, (dialect) -> dialect.parseGlobalExpression(path, queryParams, apiVersion)); } @Override public Map parseTypedExpression(String path, - MultivaluedMap queryParams) + MultivaluedMap queryParams, + String apiVersion) throws ParseException { if (subqueryDialects.isEmpty()) { throw new ParseException("Type filtering not supported"); } - return parseExpression(subqueryDialects, (dialect) -> dialect.parseTypedExpression(path, queryParams)); + return parseExpression(subqueryDialects, (dialect) -> dialect.parseTypedExpression(path, + queryParams, apiVersion)); } private static R parseExpression(List dialects, ParseFunction parseFunction) throws ParseException { diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/RSQLFilterDialect.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/RSQLFilterDialect.java index fa1926b476..d7f2c6f5ad 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/RSQLFilterDialect.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/RSQLFilterDialect.java @@ -101,7 +101,8 @@ private static Set getDefaultOperatorsWithIsnull() { } @Override - public FilterExpression parseGlobalExpression(String path, MultivaluedMap filterParams) + public FilterExpression parseGlobalExpression(String path, MultivaluedMap filterParams, + String apiVersion) throws ParseException { if (filterParams.size() != 1) { throw new ParseException(SINGLE_PARAMETER_ONLY); @@ -133,7 +134,7 @@ public FilterExpression parseGlobalExpression(String path, MultivaluedMap parseTypedExpression(String path, MultivaluedMap filterParams) + public Map parseTypedExpression(String path, MultivaluedMap filterParams, + String apiVersion) throws ParseException { Map expressionByType = new HashMap<>(); @@ -159,7 +161,7 @@ public Map parseTypedExpression(String path, Multivalu throw new ParseException("Exactly one RSQL expression must be defined for type : " + typeName); } - Class entityType = dictionary.getEntityClass(typeName); + Class entityType = dictionary.getEntityClass(typeName, apiVersion); if (entityType == null) { throw new ParseException(INVALID_QUERY_PARAMETER + paramName); } @@ -175,7 +177,6 @@ public Map parseTypedExpression(String path, Multivalu return expressionByType; } - /** * Parses a RSQL string into an Elide FilterExpression. * @param expressionText the RSQL string diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/SubqueryFilterDialect.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/SubqueryFilterDialect.java index 6b70404047..8371b1c550 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/SubqueryFilterDialect.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/dialect/SubqueryFilterDialect.java @@ -33,9 +33,11 @@ public interface SubqueryFilterDialect { * * @param path The URL path * @param filterParams The subset of queryParams that start with 'filter' + * @param apiVersion The version of the API requested. * @return The root of an expression abstract syntax tree parsed from both the path and the query parameters. * @throws ParseException if unable to parse */ - public Map parseTypedExpression(String path, MultivaluedMap filterParams) + public Map parseTypedExpression(String path, MultivaluedMap filterParams, + String apiVersion) throws ParseException; } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/AndFilterExpression.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/AndFilterExpression.java index 9571351ba3..4c87ef7a42 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/AndFilterExpression.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/AndFilterExpression.java @@ -17,6 +17,32 @@ public class AndFilterExpression implements FilterExpression { @Getter private FilterExpression left; @Getter private FilterExpression right; + /** + * Returns a new {@link AndFilterExpression} instance with the specified null-able left and right operands. + *

+ * The publication rules are + *

    + *
  1. If both left and right are not {@code null}, this method produces the same instance as + * {@link #AndFilterExpression(FilterExpression, FilterExpression)} does, + *
  2. If one of them is {@code null}, the other non-null is returned with no modification, + *
  3. If both left and right are {@code null}, this method returns + * {@code null}. + *
+ * + * @param left The provided left {@link FilterExpression} + * @param right The provided right {@link FilterExpression} + * + * @return a new {@link AndFilterExpression} instance or {@code null} + */ + public static FilterExpression fromPair(FilterExpression left, FilterExpression right) { + if (left != null && right != null) { + return new AndFilterExpression(left, right); + } else if (left == null) { + return right; + } + return left; + } + public AndFilterExpression(FilterExpression left, FilterExpression right) { this.left = left; this.right = right; diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/OrFilterExpression.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/OrFilterExpression.java index 02cfb62ed8..90f5e628ef 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/OrFilterExpression.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/OrFilterExpression.java @@ -18,6 +18,32 @@ public class OrFilterExpression implements FilterExpression { @Getter private FilterExpression left; @Getter private FilterExpression right; + /** + * Returns a new {@link OrFilterExpression} instance with the specified null-able left and right operands. + *

+ * The publication rules are + *

    + *
  1. If both left and right are not {@code null}, this method produces the same instance as + * {@link #OrFilterExpression(FilterExpression, FilterExpression)} does, + *
  2. If one of them is {@code null}, the other non-null is returned with no modification, + *
  3. If both left and right are {@code null}, this method returns + * {@code null}. + *
+ * + * @param left The provided left {@link FilterExpression} + * @param right The provided right {@link FilterExpression} + * + * @return a new {@link OrFilterExpression} instance or {@code null} + */ + public static FilterExpression fromPair(FilterExpression left, FilterExpression right) { + if (left != null && right != null) { + return new OrFilterExpression(left, right); + } else if (left == null) { + return right; + } + return left; + } + public OrFilterExpression(FilterExpression left, FilterExpression right) { this.left = left; this.right = right; diff --git a/elide-core/src/main/java/com/yahoo/elide/core/pagination/Pagination.java b/elide-core/src/main/java/com/yahoo/elide/core/pagination/Pagination.java deleted file mode 100644 index 61ae0b6241..0000000000 --- a/elide-core/src/main/java/com/yahoo/elide/core/pagination/Pagination.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright 2015, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.core.pagination; - -import com.yahoo.elide.ElideSettings; -import com.yahoo.elide.annotation.Paginate; -import com.yahoo.elide.core.exceptions.InvalidValueException; - -import com.google.common.collect.ImmutableMap; - -import lombok.Getter; -import lombok.ToString; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -import javax.ws.rs.core.MultivaluedMap; - -/** - * Encapsulates the pagination strategy. - */ - -@ToString -public class Pagination { - /** - * Denotes the internal field names for paging. - */ - public enum PaginationKey { offset, number, size, limit, totals } - - public static final int DEFAULT_OFFSET = 0; - public static final int DEFAULT_PAGE_LIMIT = 500; - public static final int MAX_PAGE_LIMIT = 10000; - - // For specifying which page of records is to be returned in the response - public static final String PAGE_NUMBER_KEY = "page[number]"; - - // For specifying the page size - essentially an alias for page[limit] - public static final String PAGE_SIZE_KEY = "page[size]"; - - // For specifying the first row to be returned in the response - public static final String PAGE_OFFSET_KEY = "page[offset]"; - - // For limiting the number of records returned - public static final String PAGE_LIMIT_KEY = "page[limit]"; - - // For requesting total pages/records be included in the response page meta data - public static final String PAGE_TOTALS_KEY = "page[totals]"; - - public static final Map PAGE_KEYS = new HashMap<>(); - static { - PAGE_KEYS.put(PAGE_NUMBER_KEY, PaginationKey.number); - PAGE_KEYS.put(PAGE_SIZE_KEY, PaginationKey.size); - PAGE_KEYS.put(PAGE_OFFSET_KEY, PaginationKey.offset); - PAGE_KEYS.put(PAGE_LIMIT_KEY, PaginationKey.limit); - PAGE_KEYS.put(PAGE_TOTALS_KEY, PaginationKey.totals); - } - - private long pageTotals = 0; - - private static final String PAGE_KEYS_CSV = PAGE_KEYS.keySet().stream().collect(Collectors.joining(", ")); - - // For holding the page query parameters until they can be evaluated - private Map pageData; - - @Getter - private int offset; - - @Getter - private int limit; - - @Getter - private boolean generateTotals; - - private final int defaultMaxPageSize; - private final int defaultPageSize; - - private Pagination(Map pageData, int defaultMaxPageSize, int defaultPageSize) { - this.pageData = pageData; - this.defaultMaxPageSize = defaultMaxPageSize; - this.defaultPageSize = defaultPageSize; - } - - /** - * TODO - Refactor Pagination. - * IMPORTANT - This method should only be used for testing until Pagination is refactored. The - * member field values of this class change depending on evaluation later from the Pagination annotation. - * The existing implementation is too complex because logic resides in the wrong places. - * - * @param limit The page size - * @param offset The page offset - * @param generatePageTotals Whether or not to return page totals - * @return A new pagination object. - */ - public static Pagination fromOffsetAndLimit(int limit, int offset, boolean generatePageTotals) { - - ImmutableMap.Builder pageData = ImmutableMap.builder() - .put(PAGE_KEYS.get(PAGE_OFFSET_KEY), offset) - .put(PAGE_KEYS.get(PAGE_LIMIT_KEY), limit); - - if (generatePageTotals) { - pageData.put(PAGE_KEYS.get(PAGE_TOTALS_KEY), 1); - } - - Pagination result = new Pagination(pageData.build(), MAX_PAGE_LIMIT, DEFAULT_PAGE_LIMIT); - result.offset = offset; - result.limit = limit; - result.generateTotals = generatePageTotals; - return result; - } - - /** - * Given an offset and first parameter from GraphQL, generate page and pageSize values. - * - * @param firstOpt Provided first string - * @param offsetOpt Provided offset string - * @param generatePageTotals True if page totals should be generated, false otherwise - * @param elideSettings Elide settings object containing default pagination values - * @return The new Pagination object. - */ - public static Optional fromOffsetAndFirst(Optional firstOpt, - Optional offsetOpt, - boolean generatePageTotals, - ElideSettings elideSettings) { - return firstOpt.map(firstString -> { - int offset; - int first; - - try { - offset = offsetOpt.map(Integer::parseInt).orElse(0); - first = Integer.parseInt(firstString); - } catch (NumberFormatException e) { - throw new InvalidValueException("Offset and first must be numeric values."); - } - - if (offset < 0) { - throw new InvalidValueException("Offset values must be non-negative."); - } else if (first < 1) { - throw new InvalidValueException("Limit values must be positive."); - } - - ImmutableMap.Builder pageData = ImmutableMap.builder() - .put(PAGE_KEYS.get(PAGE_OFFSET_KEY), offset) - .put(PAGE_KEYS.get(PAGE_LIMIT_KEY), first); - if (generatePageTotals) { - pageData.put(PAGE_KEYS.get(PAGE_TOTALS_KEY), 1); - } - - return Optional.of(getPagination(pageData.build(), elideSettings)); - }).orElseGet(() -> { - if (generatePageTotals) { - Pagination pagination = getDefaultPagination(elideSettings); - pagination.pageData.put(PAGE_KEYS.get(PAGE_TOTALS_KEY), 1); - return Optional.of(pagination); - } - return Optional.empty(); - }); - } - - /** - * Given json-api paging params, generate page and pageSize values from query params. - * - * @param queryParams The page queryParams (ImmuatableMultiValueMap). - * @param elideSettings Elide settings containing pagination default limits - * @return The new Pagination object. - * @throws InvalidValueException invalid query parameter - */ - public static Pagination parseQueryParams(final MultivaluedMap queryParams, - ElideSettings elideSettings) - throws InvalidValueException { - final Map pageData = new HashMap<>(); - queryParams.entrySet() - .forEach(paramEntry -> { - final String queryParamKey = paramEntry.getKey(); - if (PAGE_KEYS.containsKey(queryParamKey)) { - PaginationKey paginationKey = PAGE_KEYS.get(queryParamKey); - if (paginationKey.equals(PaginationKey.totals)) { - // page[totals] is a valueless parameter, use value of 0 just so that its presence can - // be recorded in the map - pageData.put(paginationKey, 0); - } else { - final String value = paramEntry.getValue().get(0); - try { - int intValue = Integer.parseInt(value, 10); - pageData.put(paginationKey, intValue); - } catch (NumberFormatException e) { - throw new InvalidValueException("page values must be integers"); - } - } - } else if (queryParamKey.startsWith("page[")) { - throw new InvalidValueException("Invalid Pagination Parameter. Accepted values are " - + PAGE_KEYS_CSV); - } - }); - return getPagination(pageData, elideSettings); - } - - /** - * Sets the total number of records for the paginated query. - * @param total the total number of records found - */ - public void setPageTotals(long total) { - this.pageTotals = total; - } - - /** - * Fetches the total number of records of the paginated query. - * @return page totals - */ - public long getPageTotals() { - return pageTotals; - } - - /** - * Construct a pagination object from page data and elide settings. - * - * @param pageData Map containing pagination information - * @param elideSettings Settings containing pagination defaults - * @return Pagination object - */ - private static Pagination getPagination(Map pageData, ElideSettings elideSettings) { - // Decidedly default settings until evaluate is called (a call to evaluate from the datastore will update this): - Pagination result = new Pagination(pageData, - elideSettings.getDefaultMaxPageSize(), elideSettings.getDefaultPageSize()); - result.offset = 0; - result.limit = elideSettings.getDefaultPageSize(); - return result; - } - - /** - * Evaluates the pagination variables for default limits. - * - * @param defaultLimit the default page size - * @param maxLimit a hard upper limit on page size - * @return the calculated {@link Pagination} - */ - private Pagination evaluate(int defaultLimit, int maxLimit) { - if (hasInvalidCombination(pageData)) { - throw new InvalidValueException("Invalid usage of pagination parameters."); - } - if (pageData.containsKey(PaginationKey.size) || pageData.containsKey(PaginationKey.number)) { - pageByPages(defaultLimit, maxLimit); - } else if (pageData.containsKey(PaginationKey.limit) || pageData.containsKey(PaginationKey.offset)) { - pageByOffset(defaultLimit, maxLimit); - } else { - limit = defaultLimit; - offset = 0; - } - - generateTotals = pageData.containsKey(PaginationKey.totals); - - return this; - } - - private boolean hasInvalidCombination(Map pageData) { - return (pageData.containsKey(PaginationKey.size) || pageData.containsKey(PaginationKey.number)) - && (pageData.containsKey(PaginationKey.limit) || pageData.containsKey(PaginationKey.offset)); - } - - private void pageByOffset(int defaultLimit, int maxLimit) { - limit = pageData.containsKey(PaginationKey.limit) ? pageData.get(PaginationKey.limit) : defaultLimit; - if (limit > maxLimit) { - throw new InvalidValueException("page[limit] value must be less than or equal to " + maxLimit); - } - if (limit < 0) { - throw new InvalidValueException("page[limit] value must contain a positive value"); - } - - offset = pageData.containsKey(PaginationKey.offset) ? pageData.get(PaginationKey.offset) : 0; - if (offset < 0) { - throw new InvalidValueException("page[offset] must contain a positive values."); - } - } - - private void pageByPages(int defaultLimit, int maxLimit) { - limit = pageData.containsKey(PaginationKey.size) ? pageData.get(PaginationKey.size) : defaultLimit; - if (limit > maxLimit) { - throw new InvalidValueException("page[size] value must be less than or equal to " + maxLimit); - } - if (limit < 0) { - throw new InvalidValueException("page[size] must contain a positive value."); - } - - int pageNumber = pageData.containsKey(PaginationKey.number) ? pageData.get(PaginationKey.number) : 1; - if (pageNumber < 1) { - throw new InvalidValueException("page[number] must contain a positive value."); - } - - offset = (pageNumber - 1) * limit; - } - - /** - * Evaluates the pagination variables. Uses the Paginate annotation if it has been set for the entity to be - * queried. - * - * @param entityClass Entity class to paginate - * @return the calculated {@link Pagination} - */ - public Pagination evaluate(final Class entityClass) { - Paginate paginate = - entityClass != null ? (Paginate) entityClass.getAnnotation(Paginate.class) : null; - - int defaultLimit = paginate != null ? paginate.defaultLimit() : defaultPageSize; - int maxLimit = paginate != null ? paginate.maxLimit() : defaultMaxPageSize; - - evaluate(defaultLimit, maxLimit); - - generateTotals = generateTotals && (paginate == null || paginate.countable()); - - return this; - } - - /** - * Know if this is the default instance. - * @return The default pagination values. - */ - public boolean isDefaultInstance() { - return pageData.isEmpty(); - } - - /** - * Alias for isDefault. - * @return true if there are no pagination rules - */ - public boolean isEmpty() { - return isDefaultInstance(); - } - - /** - * Default Instance. - * @param elideSettings general Elide settings - * @return The default instance. - */ - public static Pagination getDefaultPagination(ElideSettings elideSettings) { - Pagination defaultPagination = new Pagination(new HashMap<>(), - elideSettings.getDefaultMaxPageSize(), elideSettings.getDefaultPageSize()); - defaultPagination.offset = DEFAULT_OFFSET; - defaultPagination.limit = DEFAULT_PAGE_LIMIT; - return defaultPagination; - } -} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/pagination/PaginationImpl.java b/elide-core/src/main/java/com/yahoo/elide/core/pagination/PaginationImpl.java new file mode 100644 index 0000000000..d419a8786b --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/pagination/PaginationImpl.java @@ -0,0 +1,269 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.pagination; + +import com.yahoo.elide.ElideSettings; +import com.yahoo.elide.annotation.Paginate; +import com.yahoo.elide.core.exceptions.InvalidValueException; +import com.yahoo.elide.request.Pagination; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.ws.rs.core.MultivaluedMap; + +/** + * Holds state associated with pagination. + */ +@ToString +@EqualsAndHashCode +public class PaginationImpl implements Pagination { + /** + * Denotes the internal field names for paging. + */ + public enum PaginationKey { offset, number, size, limit, totals } + + // For specifying which page of records is to be returned in the response + public static final String PAGE_NUMBER_KEY = "page[number]"; + + // For specifying the page size - essentially an alias for page[limit] + public static final String PAGE_SIZE_KEY = "page[size]"; + + // For specifying the first row to be returned in the response + public static final String PAGE_OFFSET_KEY = "page[offset]"; + + // For limiting the number of records returned + public static final String PAGE_LIMIT_KEY = "page[limit]"; + + // For requesting total pages/records be included in the response page meta data + public static final String PAGE_TOTALS_KEY = "page[totals]"; + + public static final Map PAGE_KEYS = new HashMap<>(); + static { + PAGE_KEYS.put(PAGE_NUMBER_KEY, PaginationKey.number); + PAGE_KEYS.put(PAGE_SIZE_KEY, PaginationKey.size); + PAGE_KEYS.put(PAGE_OFFSET_KEY, PaginationKey.offset); + PAGE_KEYS.put(PAGE_LIMIT_KEY, PaginationKey.limit); + PAGE_KEYS.put(PAGE_TOTALS_KEY, PaginationKey.totals); + } + + @Getter + @Setter + private Long pageTotals = 0L; + + private static final String PAGE_KEYS_CSV = PAGE_KEYS.keySet().stream().collect(Collectors.joining(", ")); + + @Getter + private Integer offset; + + @Getter + private Integer limit; + + private Boolean generateTotals; + + private Boolean isDefault; + + @Getter + private Class entityClass; + + /** + * Constructor. + * @param entityClass The type of collection we are paginating. + * @param clientOffset The client requested offset or null if not provided. + * @param clientLimit The client requested limit or null if not provided. + * @param systemDefaultLimit The system default limit (in terms of records). + * @param systemMaxLimit The system max limit (in terms of records). + * @param generateTotals Whether to return the total number of records. + * @param pageByPages Whether to page by pages or records. + */ + public PaginationImpl(Class entityClass, + Integer clientOffset, + Integer clientLimit, + int systemDefaultLimit, + int systemMaxLimit, + Boolean generateTotals, + Boolean pageByPages) { + + this.entityClass = entityClass; + this.isDefault = (clientOffset == null && clientLimit == null && generateTotals == null); + + Paginate paginate = entityClass != null ? (Paginate) entityClass.getAnnotation(Paginate.class) : null; + + this.limit = clientLimit != null + ? clientLimit + : (paginate != null ? paginate.defaultLimit() : systemDefaultLimit); + + int maxLimit = paginate != null ? paginate.maxLimit() : systemMaxLimit; + + String pageSizeLabel = pageByPages ? "size" : "limit"; + + if (limit > maxLimit && !isDefault) { + throw new InvalidValueException("Pagination " + + pageSizeLabel + " must be less than or equal to " + maxLimit); + } + if (limit < 1) { + throw new InvalidValueException("Pagination " + + pageSizeLabel + " must contain a positive, non-zero value."); + } + + this.generateTotals = generateTotals != null && generateTotals && (paginate == null || paginate.countable()); + + if (pageByPages) { + int pageNumber = clientOffset != null ? clientOffset : 1; + if (pageNumber < 1) { + throw new InvalidValueException("Pagination number must be a positive, non-zero value."); + } + this.offset = (pageNumber - 1) * limit; + } else { + this.offset = clientOffset != null ? clientOffset : 0; + + if (offset < 0) { + throw new InvalidValueException("Pagination offset must contain a positive value."); + } + } + } + + /** + * Whether or not the client requested to return page totals. + * @return true if page totals should be returned. + */ + @Override + public Boolean returnPageTotals() { + return generateTotals; + } + + /** + * Whether or not the client requested pagination or the system defaults are in effect. + * @return True if the system defaults are in effect. + */ + @Override + public Boolean isDefaultInstance() { + return isDefault; + } + + /** + * Given json-api paging params, generate page and pageSize values from query params. + * + * @param entityClass The collection type. + * @param queryParams The page queryParams. + * @param elideSettings Elide settings containing pagination default limits + * @return The new Pagination object. + * @throws InvalidValueException invalid query parameter + */ + public static PaginationImpl parseQueryParams(Class entityClass, + final Optional> queryParams, + ElideSettings elideSettings) + throws InvalidValueException { + + if (! queryParams.isPresent()) { + return getDefaultPagination(entityClass, elideSettings); + } + + final Map pageData = new HashMap<>(); + queryParams.get().entrySet() + .forEach(paramEntry -> { + final String queryParamKey = paramEntry.getKey(); + if (PAGE_KEYS.containsKey(queryParamKey)) { + PaginationKey paginationKey = PAGE_KEYS.get(queryParamKey); + if (paginationKey.equals(PaginationKey.totals)) { + // page[totals] is a valueless parameter, use value of 0 just so that its presence can + // be recorded in the map + pageData.put(paginationKey, 0); + } else { + final String value = paramEntry.getValue().get(0); + try { + int intValue = Integer.parseInt(value, 10); + pageData.put(paginationKey, intValue); + } catch (NumberFormatException e) { + throw new InvalidValueException("page values must be integers"); + } + } + } else if (queryParamKey.startsWith("page[")) { + throw new InvalidValueException("Invalid Pagination Parameter. Accepted values are " + + PAGE_KEYS_CSV); + } + }); + return getPagination(entityClass, pageData, elideSettings); + } + + + /** + * Construct a pagination object from page data and elide settings. + * + * @param entityClass The collection type. + * @param pageData Map containing pagination information + * @param elideSettings Settings containing pagination defaults + * @return Pagination object + */ + private static PaginationImpl getPagination(Class entityClass, Map pageData, + ElideSettings elideSettings) { + if (hasInvalidCombination(pageData)) { + throw new InvalidValueException("Invalid usage of pagination parameters."); + } + + boolean pageByPages = false; + Integer offset = pageData.getOrDefault(PaginationKey.offset, null); + Integer limit = pageData.getOrDefault(PaginationKey.limit, null); + + if (pageData.containsKey(PaginationKey.size) || pageData.containsKey(PaginationKey.number)) { + pageByPages = true; + offset = pageData.getOrDefault(PaginationKey.number, null); + limit = pageData.getOrDefault(PaginationKey.size, null); + } + + return new PaginationImpl(entityClass, + offset, + limit, + elideSettings.getDefaultPageSize(), + elideSettings.getDefaultMaxPageSize(), + pageData.containsKey(PaginationKey.totals) ? true : null, + pageByPages); + } + + private static boolean hasInvalidCombination(Map pageData) { + return (pageData.containsKey(PaginationKey.size) || pageData.containsKey(PaginationKey.number)) + && (pageData.containsKey(PaginationKey.limit) || pageData.containsKey(PaginationKey.offset)); + } + + + /** + * Default Instance. + * @param elideSettings general Elide settings + * @return The default instance. + */ + public static PaginationImpl getDefaultPagination(Class entityClass, ElideSettings elideSettings) { + return new PaginationImpl( + entityClass, + null, + null, + elideSettings.getDefaultPageSize(), + elideSettings.getDefaultMaxPageSize(), + null, + false); + } + + /** + * Default Instance. + * @return The default instance. + */ + public static PaginationImpl getDefaultPagination(Class entityClass) { + return new PaginationImpl( + entityClass, + null, + null, + DEFAULT_PAGE_LIMIT, + MAX_PAGE_LIMIT, + null, + false); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/sort/Sorting.java b/elide-core/src/main/java/com/yahoo/elide/core/sort/SortingImpl.java similarity index 80% rename from elide-core/src/main/java/com/yahoo/elide/core/sort/Sorting.java rename to elide-core/src/main/java/com/yahoo/elide/core/sort/SortingImpl.java index 042f80e5d7..624d289bcc 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/sort/Sorting.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/sort/SortingImpl.java @@ -9,12 +9,16 @@ import com.yahoo.elide.core.Path; import com.yahoo.elide.core.exceptions.InvalidValueException; +import com.yahoo.elide.request.Sorting; +import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.ToString; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import javax.ws.rs.core.MultivaluedMap; @@ -23,25 +27,30 @@ * Generates a simple wrapper around the sort fields from the JSON-API GET Query. */ @ToString -public class Sorting { - - /** - * Denotes the intended sort type from json-api field. - */ - public enum SortOrder { asc, desc } +@EqualsAndHashCode +public class SortingImpl implements Sorting { private final Map sortRules = new LinkedHashMap<>(); - private static final Sorting DEFAULT_EMPTY_INSTANCE = null; + private static final SortingImpl DEFAULT_EMPTY_INSTANCE = null; private static final String JSONAPI_ID_KEYWORD = "id"; + @Getter + private Class type; + + @Getter + private Map sortingPaths; + /** * Constructs a new Sorting instance. * @param sortingRules The map of sorting rules */ - public Sorting(final Map sortingRules) { + public SortingImpl(final Map sortingRules, Class type, EntityDictionary dictionary) { if (sortingRules != null) { sortRules.putAll(sortingRules); } + + this.type = type; + sortingPaths = getValidSortingRules(type, dictionary); } /** @@ -52,8 +61,8 @@ public Sorting(final Map sortingRules) { * @return The valid sorting rules - validated through the entity dictionary, or empty dictionary * @throws InvalidValueException when sorting values are not valid for the jpa entity */ - public Map getValidSortingRules(final Class entityClass, - final EntityDictionary dictionary) + private Map getValidSortingRules(final Class entityClass, + final EntityDictionary dictionary) throws InvalidValueException { Map returnMap = new LinkedHashMap<>(); for (Map.Entry entry : replaceIdRule(dictionary.getIdFieldName(entityClass)).entrySet()) { @@ -97,6 +106,7 @@ protected static boolean isValidSortRulePath(Path path, EntityDictionary diction * Informs if the structure is default instance. * @return true if this instance is empty - no sorting rules */ + @Override public boolean isDefaultInstance() { return this.sortRules.isEmpty(); } @@ -106,12 +116,18 @@ public boolean isDefaultInstance() { * @param queryParams The query params on the request. * @return The Sorting instance (default or specific). */ - public static Sorting parseQueryParams(final MultivaluedMap queryParams) { - List sortRules = queryParams.entrySet().stream() + public static Sorting parseQueryParams(final Optional> queryParams, + Class type, EntityDictionary dictionary) { + + if (! queryParams.isPresent()) { + return DEFAULT_EMPTY_INSTANCE; + } + + List sortRules = queryParams.get().entrySet().stream() .filter(entry -> entry.getKey().equals("sort")) .map(entry -> entry.getValue().get(0)) .collect(Collectors.toList()); - return parseSortRules(sortRules); + return parseSortRules(sortRules, type, dictionary); } /** @@ -119,8 +135,8 @@ public static Sorting parseQueryParams(final MultivaluedMap quer * @param sortRule Sorting string to parse * @return Sorting object. */ - public static Sorting parseSortRule(String sortRule) { - return parseSortRules(Arrays.asList(sortRule)); + public static Sorting parseSortRule(String sortRule, Class type, EntityDictionary dictionary) { + return parseSortRules(Arrays.asList(sortRule), type, dictionary); } /** @@ -128,7 +144,7 @@ public static Sorting parseSortRule(String sortRule) { * @param sortRules Sorting rules to parse * @return Sorting object containing parsed sort rules */ - private static Sorting parseSortRules(List sortRules) { + private static SortingImpl parseSortRules(List sortRules, Class type, EntityDictionary dictionary) { final Map sortingRules = new LinkedHashMap<>(); for (String sortRule : sortRules) { if (sortRule.contains(",")) { @@ -139,7 +155,7 @@ private static Sorting parseSortRules(List sortRules) { parseSortRule(sortRule, sortingRules); } } - return sortingRules.isEmpty() ? DEFAULT_EMPTY_INSTANCE : new Sorting(sortingRules); + return sortingRules.isEmpty() ? DEFAULT_EMPTY_INSTANCE : new SortingImpl(sortingRules, type, dictionary); } /** diff --git a/elide-core/src/main/java/com/yahoo/elide/extensions/JsonApiPatch.java b/elide-core/src/main/java/com/yahoo/elide/extensions/JsonApiPatch.java index 520c1224d9..2dd05829a3 100644 --- a/elide-core/src/main/java/com/yahoo/elide/extensions/JsonApiPatch.java +++ b/elide-core/src/main/java/com/yahoo/elide/extensions/JsonApiPatch.java @@ -29,6 +29,7 @@ import org.apache.commons.collections4.IterableUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.tuple.Pair; +import org.owasp.encoder.Encode; import java.io.IOException; import java.util.Arrays; @@ -288,12 +289,11 @@ private void postProcessRelationships(PatchRequestScope requestScope) { * Turn an exception into a proper error response from patch extension. */ private void throwErrorResponse() { - ObjectNode errorContainer = getErrorContainer(); - ArrayNode errorList = (ArrayNode) errorContainer.get("errors"); + ArrayNode errorContainer = getErrorContainer(); boolean failed = false; for (PatchAction action : actions) { - failed = processAction(errorList, failed, action); + failed = processAction(errorContainer, failed, action); } JsonPatchExtensionException failure = @@ -309,23 +309,27 @@ private void throwErrorResponse() { throw failure; } - private ObjectNode getErrorContainer() { - ObjectNode container = JsonNodeFactory.instance.objectNode(); - container.set("errors", JsonNodeFactory.instance.arrayNode()); + private ArrayNode getErrorContainer() { + ArrayNode container = JsonNodeFactory.instance.arrayNode(); return container; } private boolean processAction(ArrayNode errorList, boolean failed, PatchAction action) { + ObjectNode container = JsonNodeFactory.instance.objectNode(); + ArrayNode errors = JsonNodeFactory.instance.arrayNode(); + container.set("errors", errors); + errorList.add(container); + if (action.cause != null) { // this is the failed operation - errorList.add(toErrorNode(action.cause.getMessage(), action.cause.getStatus())); + errors.add(toErrorNode(action.cause.getMessage(), action.cause.getStatus())); failed = true; } else if (!failed) { // this operation succeeded - errorList.add(ERR_NODE_ERR_IN_SUBSEQUENT_OPERATION); + errors.add(ERR_NODE_ERR_IN_SUBSEQUENT_OPERATION); } else { // this operation never ran - errorList.add(ERR_NODE_OPERATION_NOT_RUN); + errors.add(ERR_NODE_OPERATION_NOT_RUN); } return failed; } @@ -356,9 +360,9 @@ private static void clearAllExceptRelationships(Resource resource) { */ private static JsonNode toErrorNode(String detail, Integer status) { ObjectNode formattedError = JsonNodeFactory.instance.objectNode(); - formattedError.set("detail", JsonNodeFactory.instance.textNode(detail)); + formattedError.set("detail", JsonNodeFactory.instance.textNode(Encode.forHtml(detail))); if (status != null) { - formattedError.set("status", JsonNodeFactory.instance.numberNode(status)); + formattedError.set("status", JsonNodeFactory.instance.textNode(status.toString())); } return formattedError; } diff --git a/elide-core/src/main/java/com/yahoo/elide/extensions/PatchRequestScope.java b/elide-core/src/main/java/com/yahoo/elide/extensions/PatchRequestScope.java index 4e1e993cd2..53b68e596b 100644 --- a/elide-core/src/main/java/com/yahoo/elide/extensions/PatchRequestScope.java +++ b/elide-core/src/main/java/com/yahoo/elide/extensions/PatchRequestScope.java @@ -8,6 +8,7 @@ import com.yahoo.elide.ElideSettings; import com.yahoo.elide.core.DataStoreTransaction; import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.jsonapi.EntityProjectionMaker; import com.yahoo.elide.jsonapi.models.JsonApiDocument; import com.yahoo.elide.security.User; @@ -22,17 +23,20 @@ public class PatchRequestScope extends RequestScope { * Outer RequestScope constructor for use by Patch Extension. * * @param path the URL path + * @param apiVersion client requested API version * @param transaction current database transaction * @param user request user * @param elideSettings Elide settings object */ public PatchRequestScope( String path, + String apiVersion, DataStoreTransaction transaction, User user, ElideSettings elideSettings) { super( path, + apiVersion, (JsonApiDocument) null, transaction, user, @@ -49,6 +53,7 @@ public PatchRequestScope( * @param scope outer request scope */ public PatchRequestScope(String path, JsonApiDocument jsonApiDocument, PatchRequestScope scope) { - super(path, jsonApiDocument, scope); + super(path, scope.getApiVersion(), jsonApiDocument, scope); + this.setEntityProjection(new EntityProjectionMaker(dictionary, this).parsePath(path)); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/jsonapi/EntityProjectionMaker.java b/elide-core/src/main/java/com/yahoo/elide/jsonapi/EntityProjectionMaker.java new file mode 100644 index 0000000000..08fb43926c --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/jsonapi/EntityProjectionMaker.java @@ -0,0 +1,396 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.jsonapi; + +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.exceptions.InvalidCollectionException; +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.core.pagination.PaginationImpl; +import com.yahoo.elide.core.sort.SortingImpl; +import com.yahoo.elide.generated.parsers.CoreBaseVisitor; +import com.yahoo.elide.generated.parsers.CoreParser; +import com.yahoo.elide.parsers.JsonApiParser; +import com.yahoo.elide.request.Attribute; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Pagination; +import com.yahoo.elide.request.Relationship; +import com.yahoo.elide.request.Sorting; + +import com.google.common.collect.Sets; +import org.apache.commons.lang3.tuple.Pair; + +import lombok.Builder; +import lombok.Data; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; + +/** + * Converts a JSON-API request (URL and query parameters) into an EntityProjection. + */ +public class EntityProjectionMaker + extends CoreBaseVisitor, EntityProjectionMaker.NamedEntityProjection>> { + + /** + * An entity projection labeled with the class name or relationship name it is associated with. + */ + @Data + @Builder + public static class NamedEntityProjection { + private String name; + private EntityProjection projection; + } + + private static final String INCLUDE = "include"; + + private EntityDictionary dictionary; + private MultivaluedMap queryParams; + private Map> sparseFields; + private RequestScope scope; + + public EntityProjectionMaker(EntityDictionary dictionary, RequestScope scope) { + this.dictionary = dictionary; + this.queryParams = scope.getQueryParams().orElse(new MultivaluedHashMap<>()); + sparseFields = RequestScope.parseSparseFields(queryParams); + this.scope = scope; + } + + public EntityProjection parsePath(String path) { + return visit(JsonApiParser.parse(path)).apply(null).projection; + } + + public EntityProjection parseInclude(Class entityClass) { + return EntityProjection.builder() + .type(entityClass) + .relationships(toRelationshipSet(getIncludedRelationships(entityClass))) + .build(); + } + + @Override + public Function, NamedEntityProjection> visitRootCollectionLoadEntities( + CoreParser.RootCollectionLoadEntitiesContext ctx) { + return visitTerminalCollection(ctx.term()); + } + + @Override + public Function, NamedEntityProjection> visitSubCollectionReadCollection( + CoreParser.SubCollectionReadCollectionContext ctx) { + return visitTerminalCollection(ctx.term()); + } + + @Override + public Function, NamedEntityProjection> visitRootCollectionSubCollection( + CoreParser.RootCollectionSubCollectionContext ctx) { + return visitEntityWithSubCollection(ctx.entity(), ctx.subCollection()); + } + + @Override + public Function, NamedEntityProjection> visitSubCollectionSubCollection( + CoreParser.SubCollectionSubCollectionContext ctx) { + return visitEntityWithSubCollection(ctx.entity(), ctx.subCollection()); + } + + @Override + public Function, NamedEntityProjection> visitRootCollectionRelationship( + CoreParser.RootCollectionRelationshipContext ctx) { + return visitEntityWithRelationship(ctx.entity(), ctx.relationship()); + } + + @Override + public Function, NamedEntityProjection> visitSubCollectionRelationship( + CoreParser.SubCollectionRelationshipContext ctx) { + return visitEntityWithRelationship(ctx.entity(), ctx.relationship()); + } + + @Override + public Function, NamedEntityProjection> visitRootCollectionLoadEntity( + CoreParser.RootCollectionLoadEntityContext ctx) { + return (unused) -> { + return ctx.entity().accept(this).apply(null); + }; + } + + @Override + public Function, NamedEntityProjection> visitSubCollectionReadEntity( + CoreParser.SubCollectionReadEntityContext ctx) { + return (parentClass) -> { + return ctx.entity().accept(this).apply(parentClass); + }; + } + + @Override + public Function, NamedEntityProjection> visitRelationship(CoreParser.RelationshipContext ctx) { + return (parentClass) -> { + String entityName = ctx.term().getText(); + + Class entityClass = getEntityClass(parentClass, entityName); + FilterExpression filter = scope.getExpressionForRelation(parentClass, entityName).orElse(null); + + Sorting sorting = SortingImpl.parseQueryParams(scope.getQueryParams(), entityClass, dictionary); + Pagination pagination = PaginationImpl.parseQueryParams(entityClass, + scope.getQueryParams(), scope.getElideSettings()); + + return NamedEntityProjection.builder() + .name(entityName) + .projection(EntityProjection.builder() + .filterExpression(filter) + .sorting(sorting) + .pagination(pagination) + .type(entityClass) + .build() + ).build(); + }; + } + + @Override + public Function, NamedEntityProjection> visitEntity(CoreParser.EntityContext ctx) { + return (parentClass) -> { + String entityName = ctx.term().getText(); + + Class entityClass = getEntityClass(parentClass, entityName); + + return NamedEntityProjection.builder() + .name(entityName) + .projection(EntityProjection.builder() + .type(entityClass) + .attributes(getSparseAttributes(entityClass)) + .relationships(toRelationshipSet(getRequiredRelationships(entityClass))) + .build() + ).build(); + }; + } + + @Override + protected Function, NamedEntityProjection> aggregateResult( + Function, NamedEntityProjection> aggregate, + Function, NamedEntityProjection> nextResult) { + + if (aggregate == null) { + return nextResult; + } else { + return aggregate; + } + } + + public EntityProjection visitIncludePath(Path path) { + Path.PathElement pathElement = path.getPathElements().get(0); + int size = path.getPathElements().size(); + + Class entityClass = pathElement.getFieldType(); + + if (size > 1) { + Path nextPath = new Path(path.getPathElements().subList(1, size)); + EntityProjection relationshipProjection = visitIncludePath(nextPath); + + return EntityProjection.builder() + .relationships(toRelationshipSet(getSparseRelationships(entityClass))) + .relationship(nextPath.getPathElements().get(0).getFieldName(), relationshipProjection) + .attributes(getSparseAttributes(entityClass)) + .filterExpression(scope.getFilterExpressionByType(entityClass).orElse(null)) + .type(entityClass) + .build(); + } + + return EntityProjection.builder() + .relationships(toRelationshipSet(getSparseRelationships(entityClass))) + .attributes(getSparseAttributes(entityClass)) + .type(entityClass) + .filterExpression(scope.getFilterExpressionByType(entityClass).orElse(null)) + .build(); + } + + private Function, NamedEntityProjection> visitEntityWithSubCollection(CoreParser.EntityContext entity, + CoreParser.SubCollectionContext subCollection) { + return (parentClass) -> { + String entityName = entity.term().getText(); + + Class entityClass = getEntityClass(parentClass, entityName); + + NamedEntityProjection projection = subCollection.accept(this).apply(entityClass); + + return NamedEntityProjection.builder() + .name(entityName) + .projection(EntityProjection.builder() + .type(entityClass) + .relationship(projection.name, projection.projection) + .build() + ).build(); + }; + } + + private Function, NamedEntityProjection> visitEntityWithRelationship(CoreParser.EntityContext entity, + CoreParser.RelationshipContext relationship) { + return (parentClass) -> { + String entityName = entity.term().getText(); + + Class entityClass = getEntityClass(parentClass, entityName); + + String relationshipName = relationship.term().getText(); + NamedEntityProjection relationshipProjection = relationship.accept(this).apply(entityClass); + + FilterExpression filter = scope.getFilterExpressionByType(entityClass).orElse(null); + + return NamedEntityProjection.builder() + .name(entityName) + .projection(EntityProjection.builder() + .type(entityClass) + .filterExpression(filter) + .relationships(toRelationshipSet(getRequiredRelationships(entityClass))) + .relationship(relationshipName, relationshipProjection.projection) + .build() + ).build(); + }; + } + + private Function, NamedEntityProjection> visitTerminalCollection(CoreParser.TermContext collectionName) { + return (parentClass) -> { + String collectionNameText = collectionName.getText(); + + Class entityClass = getEntityClass(parentClass, collectionNameText); + + FilterExpression filter; + if (parentClass == null) { + filter = scope.getLoadFilterExpression(entityClass).orElse(null); + } else { + filter = scope.getExpressionForRelation(parentClass, collectionNameText).orElse(null); + } + + Sorting sorting = SortingImpl.parseQueryParams(scope.getQueryParams(), entityClass, dictionary); + Pagination pagination = PaginationImpl.parseQueryParams(entityClass, + scope.getQueryParams(), scope.getElideSettings()); + + return NamedEntityProjection.builder() + .name(collectionNameText) + .projection(EntityProjection.builder() + .filterExpression(filter) + .sorting(sorting) + .pagination(pagination) + .relationships(toRelationshipSet(getRequiredRelationships(entityClass))) + .attributes(getSparseAttributes(entityClass)) + .type(entityClass) + .build() + ).build(); + }; + } + + private Class getEntityClass(Class parentClass, String entityLabel) { + + //entityLabel represents a root collection. + if (parentClass == null) { + + Class entityClass = dictionary.getEntityClass(entityLabel, scope.getApiVersion()); + + if (entityClass != null) { + return entityClass; + } + + + //entityLabel represents a relationship. + } else if (dictionary.isRelation(parentClass, entityLabel)) { + return dictionary.getParameterizedType(parentClass, entityLabel); + } + + throw new InvalidCollectionException(entityLabel); + } + + private Map getIncludedRelationships(Class entityClass) { + Set includePaths = getIncludePaths(entityClass); + + Map relationships = includePaths.stream() + .map((path) -> Pair.of(path.getPathElements().get(0).getFieldName(), visitIncludePath(path))) + .collect(Collectors.toMap( + Pair::getKey, + Pair::getValue, + EntityProjection::merge + )); + + return relationships; + } + + private Set getSparseAttributes(Class entityClass) { + Set allAttributes = new LinkedHashSet<>(dictionary.getAttributes(entityClass)); + + Set sparseFieldsForEntity = sparseFields.get(dictionary.getJsonAliasFor(entityClass)); + if (sparseFieldsForEntity == null || sparseFieldsForEntity.isEmpty()) { + sparseFieldsForEntity = allAttributes; + } + + return Sets.intersection(allAttributes, sparseFieldsForEntity).stream() + .map(attributeName -> Attribute.builder() + .name(attributeName) + .type(dictionary.getParameterizedType(entityClass, attributeName)) + .build()) + .collect(Collectors.toSet()); + } + + private Map getSparseRelationships(Class entityClass) { + Set allRelationships = new LinkedHashSet<>(dictionary.getRelationships(entityClass)); + Set sparseFieldsForEntity = sparseFields.get(dictionary.getJsonAliasFor(entityClass)); + + if (sparseFieldsForEntity == null || sparseFieldsForEntity.isEmpty()) { + sparseFieldsForEntity = allRelationships; + } + + sparseFieldsForEntity = Sets.intersection(allRelationships, sparseFieldsForEntity); + + return sparseFieldsForEntity.stream() + .collect(Collectors.toMap( + Function.identity(), + (relationshipName) -> { + FilterExpression filter = scope.getExpressionForRelation(entityClass, relationshipName) + .orElse(null); + + return EntityProjection.builder() + .type(dictionary.getParameterizedType(entityClass, relationshipName)) + .filterExpression(filter) + .build(); + } + )); + } + + private Map getRequiredRelationships(Class entityClass) { + return Stream.concat( + getIncludedRelationships(entityClass).entrySet().stream(), + getSparseRelationships(entityClass).entrySet().stream() + ).collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + EntityProjection::merge + )); + } + + private Set getIncludePaths(Class entityClass) { + if (queryParams.get(INCLUDE) != null) { + return queryParams.get(INCLUDE).stream() + .flatMap(param -> Arrays.stream(param.split(","))) + .map(pathString -> new Path(entityClass, dictionary, pathString)) + .collect(Collectors.toSet()); + } + + return new LinkedHashSet<>(); + } + + private Set toRelationshipSet(Map relationships) { + return relationships.entrySet().stream() + .map(entry -> Relationship.builder() + .name(entry.getKey()) + .alias(entry.getKey()) + .projection(entry.getValue()) + .build()) + .collect(Collectors.toSet()); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/jsonapi/document/processors/IncludedProcessor.java b/elide-core/src/main/java/com/yahoo/elide/jsonapi/document/processors/IncludedProcessor.java index 40056b0c11..a7805da4bb 100644 --- a/elide-core/src/main/java/com/yahoo/elide/jsonapi/document/processors/IncludedProcessor.java +++ b/elide-core/src/main/java/com/yahoo/elide/jsonapi/document/processors/IncludedProcessor.java @@ -7,8 +7,10 @@ import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; -import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.jsonapi.EntityProjectionMaker; import com.yahoo.elide.jsonapi.models.JsonApiDocument; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Relationship; import com.google.common.collect.Lists; @@ -60,13 +62,16 @@ public void execute(JsonApiDocument jsonApiDocument, Set res */ private void addIncludedResources(JsonApiDocument jsonApiDocument, PersistentResource rec, List requestedRelationPaths) { + + EntityProjectionMaker maker = new EntityProjectionMaker(rec.getDictionary(), rec.getRequestScope()); + EntityProjection projection = maker.parseInclude(rec.getResourceClass()); // Process each include relation path requestedRelationPaths.forEach(pathParam -> { List pathList = Arrays.asList(pathParam.split(RELATION_PATH_SEPARATOR)); pathList.forEach(requestedRelationPath -> { List relationPath = Lists.newArrayList(requestedRelationPath.split(RELATION_PATH_DELIMITER)); - addResourcesForPath(jsonApiDocument, rec, relationPath); + addResourcesForPath(jsonApiDocument, rec, relationPath, projection); }); }); } @@ -76,15 +81,17 @@ private void addIncludedResources(JsonApiDocument jsonApiDocument, PersistentRes * JsonApiDocument. */ private void addResourcesForPath(JsonApiDocument jsonApiDocument, PersistentResource rec, - List relationPath) { + List relationPath, + EntityProjection projection) { //Pop off a relation of relation path String relation = relationPath.remove(0); - Optional filterExpression = rec.getRequestScope().getExpressionForRelation(rec, relation); Set collection; + Relationship relationship = projection.getRelationship(relation).orElseThrow(IllegalStateException::new); try { - collection = rec.getRelationCheckedFiltered(relation, filterExpression, Optional.empty(), Optional.empty()); + collection = rec.getRelationCheckedFiltered(relationship); + } catch (ForbiddenAccessException e) { return; } @@ -95,7 +102,8 @@ private void addResourcesForPath(JsonApiDocument jsonApiDocument, PersistentReso //If more relations left in the path, process a level deeper if (!relationPath.isEmpty()) { //Use a copy of the relationPath to preserve the path for remaining branches of the relationship tree - addResourcesForPath(jsonApiDocument, resource, new ArrayList<>(relationPath)); + addResourcesForPath(jsonApiDocument, resource, new ArrayList<>(relationPath), + relationship.getProjection()); } }); } diff --git a/elide-core/src/main/java/com/yahoo/elide/jsonapi/models/Resource.java b/elide-core/src/main/java/com/yahoo/elide/jsonapi/models/Resource.java index 9b71f59c6d..42ae14852a 100644 --- a/elide-core/src/main/java/com/yahoo/elide/jsonapi/models/Resource.java +++ b/elide-core/src/main/java/com/yahoo/elide/jsonapi/models/Resource.java @@ -5,12 +5,14 @@ */ package com.yahoo.elide.jsonapi.models; +import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; import com.yahoo.elide.core.exceptions.InvalidObjectIdentifierException; import com.yahoo.elide.core.exceptions.UnknownEntityException; import com.yahoo.elide.jsonapi.serialization.KeySerializer; +import com.yahoo.elide.request.EntityProjection; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -147,13 +149,21 @@ public boolean equals(Object obj) { public PersistentResource toPersistentResource(RequestScope requestScope) throws ForbiddenAccessException, InvalidObjectIdentifierException { - Class cls = requestScope.getDictionary().getEntityClass(type); + EntityDictionary dictionary = requestScope.getDictionary(); + + Class cls = dictionary.getEntityClass(type, requestScope.getApiVersion()); + if (cls == null) { throw new UnknownEntityException(type); } if (id == null) { throw new InvalidObjectIdentifierException(id, type); } - return PersistentResource.loadRecord(cls, id, requestScope); + + EntityProjection projection = EntityProjection.builder() + .type(cls) + .build(); + + return PersistentResource.loadRecord(projection, id, requestScope); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/jsonapi/models/ResourceIdentifier.java b/elide-core/src/main/java/com/yahoo/elide/jsonapi/models/ResourceIdentifier.java index 5cf97420b5..63757b9242 100644 --- a/elide-core/src/main/java/com/yahoo/elide/jsonapi/models/ResourceIdentifier.java +++ b/elide-core/src/main/java/com/yahoo/elide/jsonapi/models/ResourceIdentifier.java @@ -9,6 +9,7 @@ import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; import com.yahoo.elide.core.exceptions.InvalidObjectIdentifierException; +import com.yahoo.elide.request.EntityProjection; import com.fasterxml.jackson.annotation.JsonProperty; @@ -37,8 +38,11 @@ public String getId() { public PersistentResource toPersistentResource(RequestScope requestScope) throws ForbiddenAccessException, InvalidObjectIdentifierException { - Class cls = requestScope.getDictionary().getEntityClass(type); - return PersistentResource.loadRecord(cls, id, requestScope); + + Class cls = requestScope.getDictionary().getEntityClass(type, requestScope.getApiVersion()); + return PersistentResource.loadRecord(EntityProjection.builder() + .type(cls) + .build(), id, requestScope); } public Resource castToResource() { diff --git a/elide-core/src/main/java/com/yahoo/elide/parsers/expression/CanPaginateVisitor.java b/elide-core/src/main/java/com/yahoo/elide/parsers/expression/CanPaginateVisitor.java index 4cc0615419..449abf86bd 100644 --- a/elide-core/src/main/java/com/yahoo/elide/parsers/expression/CanPaginateVisitor.java +++ b/elide-core/src/main/java/com/yahoo/elide/parsers/expression/CanPaginateVisitor.java @@ -208,14 +208,14 @@ public PaginationStatus visitPAREN(ExpressionParser.PARENContext ctx) { @Override public PaginationStatus visitPermissionClass(ExpressionParser.PermissionClassContext ctx) { - Check check = getCheck(dictionary, ctx.getText()); + Check check = getCheck(dictionary, ctx.getText()); //Filter expression checks can always be pushed to the DataStore so pagination is possible if (check instanceof FilterExpressionCheck) { return PaginationStatus.CAN_PAGINATE; } if (check instanceof UserCheck) { - if (check.ok(scope.getUser())) { + if (((UserCheck) check).ok(scope.getUser())) { return PaginationStatus.USER_CHECK_TRUE; } return PaginationStatus.USER_CHECK_FALSE; diff --git a/elide-core/src/main/java/com/yahoo/elide/parsers/expression/PermissionToFilterExpressionVisitor.java b/elide-core/src/main/java/com/yahoo/elide/parsers/expression/PermissionToFilterExpressionVisitor.java index a8622f1bbc..5413b8cd35 100644 --- a/elide-core/src/main/java/com/yahoo/elide/parsers/expression/PermissionToFilterExpressionVisitor.java +++ b/elide-core/src/main/java/com/yahoo/elide/parsers/expression/PermissionToFilterExpressionVisitor.java @@ -177,7 +177,7 @@ public FilterExpression visitPermissionClass(ExpressionParser.PermissionClassCon } if (check instanceof UserCheck) { - boolean userCheckResult = check.ok(requestScope.getUser()); + boolean userCheckResult = ((UserCheck) check).ok(requestScope.getUser()); return userCheckResult ? TRUE_USER_CHECK_EXPRESSION : FALSE_USER_CHECK_EXPRESSION; } diff --git a/elide-core/src/main/java/com/yahoo/elide/parsers/state/CollectionTerminalState.java b/elide-core/src/main/java/com/yahoo/elide/parsers/state/CollectionTerminalState.java index 7673900c80..5c30c5b559 100644 --- a/elide-core/src/main/java/com/yahoo/elide/parsers/state/CollectionTerminalState.java +++ b/elide-core/src/main/java/com/yahoo/elide/parsers/state/CollectionTerminalState.java @@ -14,9 +14,6 @@ import com.yahoo.elide.core.exceptions.InvalidObjectIdentifierException; import com.yahoo.elide.core.exceptions.InvalidValueException; import com.yahoo.elide.core.exceptions.UnknownEntityException; -import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; import com.yahoo.elide.jsonapi.JsonApiMapper; import com.yahoo.elide.jsonapi.document.processors.DocumentProcessor; import com.yahoo.elide.jsonapi.document.processors.IncludedProcessor; @@ -25,11 +22,12 @@ import com.yahoo.elide.jsonapi.models.Meta; import com.yahoo.elide.jsonapi.models.Relationship; import com.yahoo.elide.jsonapi.models.Resource; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Pagination; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Preconditions; - import org.apache.commons.collections4.IterableUtils; import org.apache.commons.lang3.tuple.Pair; @@ -56,9 +54,11 @@ public class CollectionTerminalState extends BaseState { private final Optional relationName; private final Class entityClass; private PersistentResource newObject; + private final EntityProjection parentProjection; public CollectionTerminalState(Class entityClass, Optional parent, - Optional relationName) { + Optional relationName, EntityProjection projection) { + this.parentProjection = projection; this.parent = parent; this.relationName = relationName; this.entityClass = entityClass; @@ -78,16 +78,20 @@ public Supplier> handleGet(StateContext state) { DocumentProcessor includedProcessor = new IncludedProcessor(); includedProcessor.execute(jsonApiDocument, collection, queryParams); + Pagination pagination = parentProjection.getPagination(); + if (parent.isPresent()) { + pagination = parentProjection.getRelationship(relationName.get()).get().getProjection().getPagination(); + } + // Add pagination meta data - Pagination pagination = requestScope.getPagination(); - if (!pagination.isEmpty()) { + if (!pagination.isDefaultInstance()) { Map pageMetaData = new HashMap<>(); pageMetaData.put("number", (pagination.getOffset() / pagination.getLimit()) + 1); pageMetaData.put("limit", pagination.getLimit()); // Get total records if it has been requested and add to the page meta data - if (pagination.isGenerateTotals()) { + if (pagination.returnPageTotals()) { Long totalRecords = pagination.getPageTotals(); pageMetaData.put("totalPages", totalRecords / pagination.getLimit() + ((totalRecords % pagination.getLimit()) > 0 ? 1 : 0)); @@ -126,27 +130,13 @@ private Set getResourceCollection(RequestScope requestScope) // TODO: In case of join filters, apply pagination after getting records // instead of passing it to the datastore - Optional pagination = Optional.ofNullable(requestScope.getPagination()); - Optional sorting = Optional.ofNullable(requestScope.getSorting()); - if (parent.isPresent()) { - Optional filterExpression = - requestScope.getExpressionForRelation(parent.get(), relationName.get()); - collection = parent.get().getRelationCheckedFiltered( - relationName.get(), - filterExpression, - sorting, - pagination); + parentProjection.getRelationship(relationName.get()).orElseThrow(IllegalStateException::new)); } else { - Optional filterExpression = requestScope.getLoadFilterExpression(entityClass); - collection = PersistentResource.loadRecords( - entityClass, + parentProjection, new ArrayList<>(), //Empty list of IDs - filterExpression, - sorting, - pagination, requestScope); } @@ -177,7 +167,9 @@ private PersistentResource createObject(RequestScope requestScope) } String id = resource.getId(); - Class newObjectClass = requestScope.getDictionary().getEntityClass(resource.getType()); + + Class newObjectClass = requestScope.getDictionary().getEntityClass(resource.getType(), + requestScope.getApiVersion()); if (newObjectClass == null) { throw new UnknownEntityException("Entity " + resource.getType() + " not found"); @@ -187,8 +179,8 @@ private PersistentResource createObject(RequestScope requestScope) + " to type: " + entityClass); } - PersistentResource pResource = PersistentResource.createObject( - parent.orElse(null), newObjectClass, requestScope, Optional.ofNullable(id)); + PersistentResource pResource = PersistentResource.createObject(parent.orElse(null), newObjectClass, + requestScope, Optional.ofNullable(id)); Map attributes = resource.getAttributes(); if (attributes != null) { diff --git a/elide-core/src/main/java/com/yahoo/elide/parsers/state/RecordState.java b/elide-core/src/main/java/com/yahoo/elide/parsers/state/RecordState.java index f1c759fc22..3f450bb8d4 100644 --- a/elide-core/src/main/java/com/yahoo/elide/parsers/state/RecordState.java +++ b/elide-core/src/main/java/com/yahoo/elide/parsers/state/RecordState.java @@ -8,14 +8,13 @@ import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.RelationshipType; -import com.yahoo.elide.core.exceptions.InvalidAttributeException; -import com.yahoo.elide.core.exceptions.InvalidCollectionException; -import com.yahoo.elide.core.filter.expression.FilterExpression; import com.yahoo.elide.generated.parsers.CoreParser.SubCollectionReadCollectionContext; import com.yahoo.elide.generated.parsers.CoreParser.SubCollectionReadEntityContext; import com.yahoo.elide.generated.parsers.CoreParser.SubCollectionRelationshipContext; import com.yahoo.elide.generated.parsers.CoreParser.SubCollectionSubCollectionContext; import com.yahoo.elide.jsonapi.models.SingleElementSet; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Relationship; import com.google.common.base.Preconditions; @@ -28,54 +27,53 @@ public class RecordState extends BaseState { private final PersistentResource resource; - public RecordState(PersistentResource resource) { + /* The projection which loaded this record */ + private final EntityProjection projection; + + public RecordState(PersistentResource resource, EntityProjection projection) { Preconditions.checkNotNull(resource); this.resource = resource; + this.projection = projection; } @Override public void handle(StateContext state, SubCollectionReadCollectionContext ctx) { String subCollection = ctx.term().getText(); EntityDictionary dictionary = state.getRequestScope().getDictionary(); + Class entityClass; String entityName; - try { - RelationshipType type = dictionary.getRelationshipType(resource.getObject(), subCollection); - if (type == RelationshipType.NONE) { - throw new InvalidCollectionException(subCollection); - } - Class paramType = dictionary.getParameterizedType(resource.getObject(), subCollection); - if (dictionary.isMappedInterface(paramType)) { - entityName = EntityDictionary.getSimpleName(paramType); - entityClass = paramType; - } else { - entityName = dictionary.getJsonAliasFor(paramType); - entityClass = dictionary.getEntityClass(entityName); - - } - if (entityClass == null) { - throw new IllegalArgumentException("Unknown type " + entityName); - } - final BaseState nextState; - final CollectionTerminalState collectionTerminalState = - new CollectionTerminalState(entityClass, Optional.of(resource), Optional.of(subCollection)); - Set collection = null; - if (type.isToOne()) { - Optional filterExpression = - state.getRequestScope().getExpressionForRelation(resource, subCollection); - collection = resource.getRelationCheckedFiltered(subCollection, - filterExpression, Optional.empty(), Optional.empty()); - } - if (collection instanceof SingleElementSet) { - PersistentResource record = ((SingleElementSet) collection).getValue(); - nextState = new RecordTerminalState(record, collectionTerminalState); - } else { - nextState = collectionTerminalState; - } - state.setState(nextState); - } catch (InvalidAttributeException e) { - throw new InvalidCollectionException(subCollection); + + RelationshipType type = dictionary.getRelationshipType(resource.getObject(), subCollection); + + Class paramType = dictionary.getParameterizedType(resource.getObject(), subCollection); + if (dictionary.isMappedInterface(paramType)) { + entityName = EntityDictionary.getSimpleName(paramType); + entityClass = paramType; + } else { + entityName = dictionary.getJsonAliasFor(paramType); + + entityClass = dictionary.getEntityClass(entityName, state.getRequestScope().getApiVersion()); } + if (entityClass == null) { + throw new IllegalArgumentException("Unknown type " + entityName); + } + final BaseState nextState; + final CollectionTerminalState collectionTerminalState = + new CollectionTerminalState(entityClass, Optional.of(resource), + Optional.of(subCollection), projection); + Set collection = null; + if (type.isToOne()) { + collection = resource.getRelationCheckedFiltered(projection.getRelationship(subCollection) + .orElseThrow(IllegalStateException::new)); + } + if (collection instanceof SingleElementSet) { + PersistentResource record = ((SingleElementSet) collection).getValue(); + nextState = new RecordTerminalState(record, collectionTerminalState); + } else { + nextState = collectionTerminalState; + } + state.setState(nextState); } @Override @@ -83,46 +81,35 @@ public void handle(StateContext state, SubCollectionReadEntityContext ctx) { String id = ctx.entity().id().getText(); String subCollection = ctx.entity().term().getText(); - try { - PersistentResource nextRecord = resource.getRelation(subCollection, id); - state.setState(new RecordTerminalState(nextRecord)); - } catch (InvalidAttributeException e) { - throw new InvalidCollectionException(subCollection); - } + PersistentResource nextRecord = resource.getRelation( + projection.getRelationship(subCollection).orElseThrow(IllegalStateException::new), id); + state.setState(new RecordTerminalState(nextRecord)); } @Override public void handle(StateContext state, SubCollectionSubCollectionContext ctx) { String id = ctx.entity().id().getText(); String subCollection = ctx.entity().term().getText(); - try { - state.setState(new RecordState(resource.getRelation(subCollection, id))); - } catch (InvalidAttributeException e) { - throw new InvalidCollectionException(subCollection); - } + + Relationship relationship = projection.getRelationship(subCollection) + .orElseThrow(IllegalStateException::new); + + state.setState(new RecordState(resource.getRelation(relationship, id), relationship.getProjection())); } @Override public void handle(StateContext state, SubCollectionRelationshipContext ctx) { String id = ctx.entity().id().getText(); String subCollection = ctx.entity().term().getText(); + String relationName = ctx.relationship().term().getText(); PersistentResource childRecord; - try { - childRecord = resource.getRelation(subCollection, id); - } catch (InvalidAttributeException e) { - throw new InvalidCollectionException(subCollection); - } - String relationName = ctx.relationship().term().getText(); - try { - Optional filterExpression = - state.getRequestScope().getExpressionForRelation(resource, subCollection); - childRecord.getRelationCheckedFiltered(relationName, filterExpression, Optional.empty(), Optional.empty()); - } catch (InvalidAttributeException e) { - throw new InvalidCollectionException(relationName); - } + Relationship childRelationship = projection.getRelationship(subCollection) + .orElseThrow(IllegalStateException::new); + + childRecord = resource.getRelation(childRelationship , id); - state.setState(new RelationshipTerminalState(childRecord, relationName)); + state.setState(new RelationshipTerminalState(childRecord, relationName, childRelationship.getProjection())); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/parsers/state/RelationshipTerminalState.java b/elide-core/src/main/java/com/yahoo/elide/parsers/state/RelationshipTerminalState.java index 0c0ed93346..ff1f466bf3 100644 --- a/elide-core/src/main/java/com/yahoo/elide/parsers/state/RelationshipTerminalState.java +++ b/elide-core/src/main/java/com/yahoo/elide/parsers/state/RelationshipTerminalState.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.parsers.state; +import com.yahoo.elide.annotation.UpdatePermission; import com.yahoo.elide.core.HttpStatus; import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.RelationshipType; @@ -18,6 +19,7 @@ import com.yahoo.elide.jsonapi.models.JsonApiDocument; import com.yahoo.elide.jsonapi.models.Relationship; import com.yahoo.elide.jsonapi.models.Resource; +import com.yahoo.elide.request.EntityProjection; import com.fasterxml.jackson.databind.JsonNode; @@ -41,8 +43,13 @@ public class RelationshipTerminalState extends BaseState { private final RelationshipType relationshipType; private final String relationshipName; - public RelationshipTerminalState(PersistentResource record, String relationshipName) { + /* The projection which loaded the resource which owns the relationship */ + private final EntityProjection parentProjection; + + public RelationshipTerminalState(PersistentResource record, String relationshipName, + EntityProjection parentProjection) { this.record = record; + this.parentProjection = parentProjection; this.relationshipType = record.getRelationshipType(relationshipName); this.relationshipName = relationshipName; @@ -55,9 +62,10 @@ public Supplier> handleGet(StateContext state) { JsonApiMapper mapper = requestScope.getMapper(); Optional> queryParams = requestScope.getQueryParams(); - Map relationships = record.toResourceWithSortingAndPagination().getRelationships(); + Map relationships = record.toResource(parentProjection).getRelationships(); + Relationship relationship = null; if (relationships != null) { - Relationship relationship = relationships.get(relationshipName); + relationship = relationships.get(relationshipName); // Handle valid relationship @@ -168,7 +176,7 @@ private boolean delete(Data data, RequestScope requestScope) { Collection resources = data.get(); if (CollectionUtils.isEmpty(resources)) { // As per: http://jsonapi.org/format/#crud-updating-relationship-responses-403 - throw new ForbiddenAccessException("Unknown update"); + throw new ForbiddenAccessException(UpdatePermission.class); } resources.stream().forEachOrdered(resource -> record.removeRelation(relationshipName, resource.toPersistentResource(requestScope))); diff --git a/elide-core/src/main/java/com/yahoo/elide/parsers/state/StartState.java b/elide-core/src/main/java/com/yahoo/elide/parsers/state/StartState.java index dfdba1827c..cff08982b4 100644 --- a/elide-core/src/main/java/com/yahoo/elide/parsers/state/StartState.java +++ b/elide-core/src/main/java/com/yahoo/elide/parsers/state/StartState.java @@ -7,14 +7,12 @@ import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.core.PersistentResource; -import com.yahoo.elide.core.exceptions.InvalidAttributeException; -import com.yahoo.elide.core.exceptions.InvalidCollectionException; -import com.yahoo.elide.core.filter.expression.FilterExpression; import com.yahoo.elide.generated.parsers.CoreParser.EntityContext; import com.yahoo.elide.generated.parsers.CoreParser.RootCollectionLoadEntitiesContext; import com.yahoo.elide.generated.parsers.CoreParser.RootCollectionLoadEntityContext; import com.yahoo.elide.generated.parsers.CoreParser.RootCollectionRelationshipContext; import com.yahoo.elide.generated.parsers.CoreParser.RootCollectionSubCollectionContext; +import com.yahoo.elide.request.EntityProjection; import java.util.Optional; @@ -26,11 +24,11 @@ public class StartState extends BaseState { public void handle(StateContext state, RootCollectionLoadEntitiesContext ctx) { String entityName = ctx.term().getText(); EntityDictionary dictionary = state.getRequestScope().getDictionary(); - Class entityClass = dictionary.getEntityClass(entityName); - if (entityClass == null || !dictionary.isRoot(entityClass)) { - throw new InvalidCollectionException(entityName); - } - state.setState(new CollectionTerminalState(entityClass, Optional.empty(), Optional.empty())); + + Class entityClass = dictionary.getEntityClass(entityName, state.getRequestScope().getApiVersion()); + + state.setState(new CollectionTerminalState(entityClass, Optional.empty(), Optional.empty(), + state.getRequestScope().getEntityProjection())); } @Override @@ -42,23 +40,21 @@ public void handle(StateContext state, RootCollectionLoadEntityContext ctx) { @Override public void handle(StateContext state, RootCollectionSubCollectionContext ctx) { PersistentResource record = entityRecord(state, ctx.entity()); - state.setState(new RecordState(record)); + + state.setState(new RecordState(record, state.getRequestScope().getEntityProjection())); } @Override public void handle(StateContext state, RootCollectionRelationshipContext ctx) { PersistentResource record = entityRecord(state, ctx.entity()); + EntityProjection projection = state.getRequestScope().getEntityProjection(); String relationName = ctx.relationship().term().getText(); - try { - Optional filterExpression = - state.getRequestScope().getExpressionForRelation(record, relationName); - record.getRelationCheckedFiltered(relationName, filterExpression, Optional.empty(), Optional.empty()); - } catch (InvalidAttributeException e) { - throw new InvalidCollectionException(relationName); - } - state.setState(new RelationshipTerminalState(record, relationName)); + record.getRelationCheckedFiltered(projection.getRelationship(relationName) + .orElseThrow(IllegalStateException::new)); + + state.setState(new RelationshipTerminalState(record, relationName, projection)); } @Override @@ -67,14 +63,9 @@ public String toString() { } private PersistentResource entityRecord(StateContext state, EntityContext entity) { - String entityName = entity.term().getText(); String id = entity.id().getText(); - EntityDictionary dictionary = state.getRequestScope().getDictionary(); - Class entityClass = dictionary.getEntityClass(entityName); - if (entityClass == null || !dictionary.isRoot(entityClass)) { - throw new InvalidCollectionException(entityName); - } - return PersistentResource.loadRecord(entityClass, id, state.getRequestScope()); + return PersistentResource.loadRecord(state.getRequestScope().getEntityProjection(), + id, state.getRequestScope()); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/request/Argument.java b/elide-core/src/main/java/com/yahoo/elide/request/Argument.java new file mode 100644 index 0000000000..fb8ba1798a --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/request/Argument.java @@ -0,0 +1,32 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.request; + +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +/** + * Represents an argument passed to an attribute. + */ +@Value +@Builder +public class Argument { + + @NonNull + String name; + + Object value; + + /** + * Returns the argument type. + * @return the argument type. + */ + public Class getType() { + return value.getClass(); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/request/Attribute.java b/elide-core/src/main/java/com/yahoo/elide/request/Attribute.java new file mode 100644 index 0000000000..7a05e71fd4 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/request/Attribute.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.request; + +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; +import lombok.Singular; +import lombok.ToString; + +import java.util.Set; + +/** + * Represents an attribute on an Elide entity. Attributes can take arguments. + */ +@Data +@Builder +public class Attribute { + @NonNull + @ToString.Exclude + private Class type; + + @NonNull + private String name; + + @ToString.Exclude + private String alias; + + @Singular + @ToString.Exclude + private Set arguments; + + private Attribute(@NonNull Class type, @NonNull String name, String alias, Set arguments) { + this.type = type; + this.name = name; + this.alias = alias == null ? name : alias; + this.arguments = arguments; + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/request/EntityProjection.java b/elide-core/src/main/java/com/yahoo/elide/request/EntityProjection.java new file mode 100644 index 0000000000..0b2f1ea4c4 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/request/EntityProjection.java @@ -0,0 +1,258 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.request; + +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.google.common.collect.Sets; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NonNull; + +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +import javax.ws.rs.BadRequestException; + +/** + * Represents a client data request against a subgraph of the entity relationship graph. + */ +@Data +@Builder +@AllArgsConstructor +public class EntityProjection { + @NonNull + private Class type; + + private Set attributes; + + private Set relationships; + + private FilterExpression filterExpression; + + private Sorting sorting; + + private Pagination pagination; + + /** + * Creates a builder initialized as a copy of this collection + * @return The new builder + */ + public EntityProjectionBuilder copyOf() { + return EntityProjection.builder() + .type(this.type) + .attributes(new LinkedHashSet<>(attributes)) + .relationships(new LinkedHashSet<>(this.relationships)) + .filterExpression(this.filterExpression) + .sorting(this.sorting) + .pagination(this.pagination); + } + + /** + * Returns a relationship subgraph by name. + * @param name The name of the relationship. + * @return + */ + public Optional getRelationship(String name) { + return relationships.stream() + .filter((relationship) -> relationship.getName().equalsIgnoreCase(name)) + .findFirst(); + } + + /** + * Returns a relationship subgraph by name. + * @param name The name of the relationship. + * @param name The alias of the relationship. + * @return + */ + public Optional getRelationship(String name, String alias) { + return relationships.stream() + .filter((relationship) -> relationship.getName().equalsIgnoreCase(name)) + .filter((relationship) -> relationship.getAlias().equalsIgnoreCase(alias)) + .findFirst(); + } + + /** + * Recursively merges two EntityProjections. + * @param toMerge The projection to merge + * @return A newly created and merged EntityProjection. + */ + public EntityProjection merge(EntityProjection toMerge) { + EntityProjectionBuilder merged = copyOf(); + + for (Relationship relationship: toMerge.getRelationships()) { + EntityProjection theirs = relationship.getProjection(); + + Relationship ourRelationship = getRelationship(relationship.getName(), + relationship.getAlias()).orElse(null); + + if (ourRelationship != null) { + merged.relationships.remove(ourRelationship); + merged.relationships.add((Relationship.builder() + .name(relationship.getName()) + .alias(relationship.getAlias()) + .projection(ourRelationship.getProjection().merge(theirs)) + .build())); + } else { + merged.relationships.add((relationship)); + } + } + if (toMerge.getPagination() != null) { + merged.pagination = toMerge.getPagination(); + } + + if (toMerge.getSorting() != null) { + merged.sorting = toMerge.getSorting(); + } + + if (toMerge.getFilterExpression() != null) { + merged.filterExpression = toMerge.getFilterExpression(); + } + + merged.attributes.addAll(toMerge.attributes); + + return merged.build(); + } + + /** + * Customizes the lombok builder to our needs. + */ + public static class EntityProjectionBuilder { + @Getter + private Class type; + + private Set relationships = new LinkedHashSet<>(); + + private Set attributes = new LinkedHashSet<>(); + + @Getter + private FilterExpression filterExpression; + + @Getter + private Sorting sorting; + + @Getter + private Pagination pagination; + + public EntityProjectionBuilder relationships(Set relationships) { + this.relationships = relationships; + return this; + } + + public EntityProjectionBuilder attributes(Set attributes) { + this.attributes = attributes; + return this; + } + + public EntityProjectionBuilder relationship(String name, EntityProjection projection) { + return relationship(Relationship.builder() + .alias(name) + .name(name) + .projection(projection) + .build()); + } + + /** + * Add a new relationship into this project or merge an existing relationship that has same field name + * and alias as this relationship. If there exists another attribute/relationship of different field that is + * using the same alias, it would throw exception because that's ambiguous. + * + * @param relationship new relationship to add + * @return this builder after adding the relationship + */ + public EntityProjectionBuilder relationship(Relationship relationship) { + String relationshipName = relationship.getName(); + String relationshipAlias = relationship.getAlias(); + + Relationship existing = relationships.stream() + .filter(r -> r.getName().equals(relationshipName) && r.getAlias().equals(relationshipAlias)) + .findFirst().orElse(null); + + if (existing != null) { + relationships.remove(existing); + relationships.add(Relationship.builder() + .name(relationshipName) + .alias(relationshipAlias) + .projection(existing.getProjection().merge(relationship.getProjection())) + .build()); + } else { + if (isAmbiguous(relationshipName, relationshipAlias)) { + throw new BadRequestException( + String.format("Alias {%s}.{%s} is ambiguous.", type, relationshipAlias) + ); + } + relationships.add(relationship); + } + + return this; + } + + /** + * Add a new attribute into this project or merge an existing attribute that has same field name + * and alias as this attribute. If there exists another attribute/relationship of different field that is + * using the same alias, it would throw exception because that's ambiguous. + * + * @param attribute new attribute to add + * @return this builder after adding the attribute + */ + public EntityProjectionBuilder attribute(Attribute attribute) { + String attributeName = attribute.getName(); + String attributeAlias = attribute.getAlias(); + + Attribute existing = attributes.stream() + .filter(a -> a.getName().equals(attributeName) && a.getAlias().equals(attributeAlias)) + .findFirst().orElse(null); + + if (existing != null) { + attributes.remove(existing); + attributes.add(Attribute.builder() + .type(attribute.getType()) + .name(attributeName) + .alias(attributeAlias) + .arguments(Sets.union(attribute.getArguments(), existing.getArguments())) + .build()); + } else { + if (isAmbiguous(attributeName, attributeAlias)) { + throw new BadRequestException( + String.format("Alias {%s}.{%s} is ambiguous.", type, attributeAlias) + ); + } + attributes.add(attribute); + } + + return this; + } + + /** + * Get an attribute by alias. + * + * @param attributeAlias alias to refer to an attribute field + * @return found attribute or null + */ + public Attribute getAttributeByAlias(String attributeAlias) { + return attributes.stream() + .filter(attribute -> attribute.getAlias().equals(attributeAlias)) + .findAny() + .orElse(null); + } + + /** + * Check whether a field alias is ambiguous. + * + * @param fieldName field that the alias is bound to + * @param alias an field alias + * @return whether new alias would cause ambiguous + */ + private boolean isAmbiguous(String fieldName, String alias) { + return attributes.stream().anyMatch(a -> !fieldName.equals(a.getName()) && alias.equals(a.getAlias())) + || relationships.stream().anyMatch( + r -> !fieldName.equals(r.getName()) && alias.equals(r.getAlias())); + } + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/request/Pagination.java b/elide-core/src/main/java/com/yahoo/elide/request/Pagination.java new file mode 100644 index 0000000000..22c8220685 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/request/Pagination.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.request; + +/** + * Represents a client request to paginate a collection. + */ +public interface Pagination { + + /** + * Default offset (in records) it client does not provide one. + */ + public static final int DEFAULT_OFFSET = 0; + + /** + * Default page limit (in records) it client does not provide one. + */ + public static final int DEFAULT_PAGE_LIMIT = 500; + + /** + * Maximum allowable page limit (in records). + */ + public static final int MAX_PAGE_LIMIT = 10000; + + /** + * Get the page offset. + * @return record offset. + */ + Integer getOffset(); + + /** + * Get the page limit. + * @return record limit. + */ + Integer getLimit(); + + /** + * Whether or not to fetch the collection size or not. + * @return true if the client wants the total size of the collection. + */ + Boolean returnPageTotals(); + + /** + * Get the total size of the collection + * @return total record count. + */ + Long getPageTotals(); + + /** + * Set the total size of the collection. + * @param pageTotals the total size. + */ + void setPageTotals(Long pageTotals); + + /** + * Is this the default instance (not present). + * @return true if pagination wasn't requested. False otherwise. + */ + public Boolean isDefaultInstance(); +} diff --git a/elide-core/src/main/java/com/yahoo/elide/request/Relationship.java b/elide-core/src/main/java/com/yahoo/elide/request/Relationship.java new file mode 100644 index 0000000000..5cbbad0014 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/request/Relationship.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.request; + +import lombok.Builder; +import lombok.Data; +import lombok.NonNull; + +/** + * Represents a relationship on an Elide entity. + */ +@Data +@Builder +public class Relationship { + + public RelationshipBuilder copyOf() { + return Relationship.builder() + .alias(alias) + .name(name) + .projection(projection); + } + + @NonNull + private String name; + + private String alias; + + @NonNull + private EntityProjection projection; + + private Relationship(@NonNull String name, String alias, @NonNull EntityProjection projection) { + this.name = name; + this.alias = alias == null ? name : alias; + this.projection = projection; + } + + public Relationship merge(Relationship toMerge) { + return Relationship.builder() + .name(name) + .alias(alias) + .projection(projection.merge(toMerge.projection)) + .build(); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/request/Sorting.java b/elide-core/src/main/java/com/yahoo/elide/request/Sorting.java new file mode 100644 index 0000000000..67a25bcfcb --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/request/Sorting.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.request; + +import com.yahoo.elide.core.Path; + +import java.util.Map; + +/** + * Represents a client request to sort a collection. + */ +public interface Sorting { + + /** + * Denotes the intended sort direction (ascending or descending). + */ + public enum SortOrder { asc, desc } + + /** + * Return an ordered map of paths and their sort order. + * @param The type to sort. + * @return An ordered map of paths and their sort order. + */ + public Map getSortingPaths(); + + /** + * Get the type of the collection to sort. + * @return the collection type. + */ + public Class getType(); + + /** + * Is this sorting the default instance (not present). + * @return true if sorting wasn't requested. False otherwise. + */ + public boolean isDefaultInstance(); +} diff --git a/elide-core/src/main/java/com/yahoo/elide/resources/DefaultOpaqueUserFunction.java b/elide-core/src/main/java/com/yahoo/elide/resources/DefaultOpaqueUserFunction.java deleted file mode 100644 index 9cc95e4484..0000000000 --- a/elide-core/src/main/java/com/yahoo/elide/resources/DefaultOpaqueUserFunction.java +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2017, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.resources; - -import java.util.function.Function; - -import javax.ws.rs.core.SecurityContext; - -/** - * Placeholder for injection frameworks. - */ -@FunctionalInterface -public interface DefaultOpaqueUserFunction extends Function { - // Empty -} diff --git a/elide-core/src/main/java/com/yahoo/elide/resources/JsonApiEndpoint.java b/elide-core/src/main/java/com/yahoo/elide/resources/JsonApiEndpoint.java index 3e05d6ea3d..96d3a4fd98 100644 --- a/elide-core/src/main/java/com/yahoo/elide/resources/JsonApiEndpoint.java +++ b/elide-core/src/main/java/com/yahoo/elide/resources/JsonApiEndpoint.java @@ -6,12 +6,12 @@ package com.yahoo.elide.resources; import static com.yahoo.elide.Elide.JSONAPI_CONTENT_TYPE; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; import com.yahoo.elide.Elide; import com.yahoo.elide.ElideResponse; import com.yahoo.elide.annotation.PATCH; - -import java.util.function.Function; +import com.yahoo.elide.security.User; import javax.inject.Inject; import javax.inject.Named; @@ -38,22 +38,18 @@ @Path("/") public class JsonApiEndpoint { protected final Elide elide; - protected final Function getUser; - - public static final DefaultOpaqueUserFunction DEFAULT_GET_USER = securityContext -> securityContext; @Inject public JsonApiEndpoint( - @Named("elide") Elide elide, - @Named("elideUserExtractionFunction") DefaultOpaqueUserFunction getUser) { + @Named("elide") Elide elide) { this.elide = elide; - this.getUser = getUser == null ? DEFAULT_GET_USER : getUser; } /** * Create handler. * * @param path request path + * @param apiVersion The api version * @param securityContext security context * @param jsonapiDocument post data as jsonapi document * @return response @@ -63,15 +59,19 @@ public JsonApiEndpoint( @Consumes(JSONAPI_CONTENT_TYPE) public Response post( @PathParam("path") String path, + @HeaderParam("ApiVersion") String apiVersion, @Context SecurityContext securityContext, String jsonapiDocument) { - return build(elide.post(path, jsonapiDocument, getUser.apply(securityContext))); + String safeApiVersion = apiVersion == null ? NO_VERSION : apiVersion; + User user = new SecurityContextUser(securityContext); + return build(elide.post(path, jsonapiDocument, user, safeApiVersion)); } /** * Read handler. * * @param path request path + * @param apiVersion The API version * @param uriInfo URI info * @param securityContext security context * @return response @@ -80,16 +80,20 @@ public Response post( @Path("{path:.*}") public Response get( @PathParam("path") String path, + @HeaderParam("ApiVersion") String apiVersion, @Context UriInfo uriInfo, @Context SecurityContext securityContext) { + String safeApiVersion = apiVersion == null ? NO_VERSION : apiVersion; MultivaluedMap queryParams = uriInfo.getQueryParameters(); - return build(elide.get(path, queryParams, getUser.apply(securityContext))); + User user = new SecurityContextUser(securityContext); + return build(elide.get(path, queryParams, user, safeApiVersion)); } /** * Update handler. * * @param contentType document MIME type + * @param apiVersion the API version * @param accept response MIME type * @param path request path * @param securityContext security context @@ -101,17 +105,21 @@ public Response get( @Consumes(JSONAPI_CONTENT_TYPE) public Response patch( @HeaderParam("Content-Type") String contentType, + @HeaderParam("ApiVersion") String apiVersion, @HeaderParam("accept") String accept, @PathParam("path") String path, @Context SecurityContext securityContext, String jsonapiDocument) { - return build(elide.patch(contentType, accept, path, jsonapiDocument, getUser.apply(securityContext))); + String safeApiVersion = apiVersion == null ? NO_VERSION : apiVersion; + User user = new SecurityContextUser(securityContext); + return build(elide.patch(contentType, accept, path, jsonapiDocument, user, safeApiVersion)); } /** * Delete relationship handler (expects body with resource ids and types). * * @param path request path + * @param apiVersion the API version. * @param securityContext security context * @param jsonApiDocument DELETE document * @return response @@ -121,9 +129,12 @@ public Response patch( @Consumes(JSONAPI_CONTENT_TYPE) public Response delete( @PathParam("path") String path, + @HeaderParam("ApiVersion") String apiVersion, @Context SecurityContext securityContext, String jsonApiDocument) { - return build(elide.delete(path, jsonApiDocument, getUser.apply(securityContext))); + String safeApiVersion = apiVersion == null ? NO_VERSION : apiVersion; + User user = new SecurityContextUser(securityContext); + return build(elide.delete(path, jsonApiDocument, user, safeApiVersion)); } private static Response build(ElideResponse response) { diff --git a/elide-core/src/main/java/com/yahoo/elide/resources/SecurityContextUser.java b/elide-core/src/main/java/com/yahoo/elide/resources/SecurityContextUser.java new file mode 100644 index 0000000000..275a4fe241 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/resources/SecurityContextUser.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.resources; + +import com.yahoo.elide.security.User; + +import javax.ws.rs.core.SecurityContext; + +/** + * Elide User for JAXRS. + */ +public class SecurityContextUser extends User { + private SecurityContext ctx; + + public SecurityContextUser(SecurityContext ctx) { + super(ctx.getUserPrincipal()); + this.ctx = ctx; + } + + @Override + public boolean isInRole(String role) { + return ctx.isUserInRole(role); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/security/FilterExpressionCheck.java b/elide-core/src/main/java/com/yahoo/elide/security/FilterExpressionCheck.java index d6695ff1dc..9c0bfba13d 100644 --- a/elide-core/src/main/java/com/yahoo/elide/security/FilterExpressionCheck.java +++ b/elide-core/src/main/java/com/yahoo/elide/security/FilterExpressionCheck.java @@ -12,12 +12,13 @@ import com.yahoo.elide.core.filter.FilterPredicate; import com.yahoo.elide.core.filter.expression.FilterExpression; import com.yahoo.elide.parsers.expression.FilterExpressionCheckEvaluationVisitor; -import com.yahoo.elide.security.checks.InlineCheck; +import com.yahoo.elide.security.checks.OperationCheck; import lombok.extern.slf4j.Slf4j; import java.util.Optional; import java.util.function.Predicate; +import javax.inject.Inject; /** * Check for FilterExpression. This is a super class for user defined FilterExpression check. The subclass should @@ -26,7 +27,10 @@ * @param Type of class */ @Slf4j -public abstract class FilterExpressionCheck extends InlineCheck { +public abstract class FilterExpressionCheck extends OperationCheck { + + @Inject + protected EntityDictionary dictionary; /** * Returns a FilterExpression from FilterExpressionCheck. @@ -37,13 +41,6 @@ public abstract class FilterExpressionCheck extends InlineCheck { */ public abstract FilterExpression getFilterExpression(Class entityClass, RequestScope requestScope); - /* NOTE: Filter Expression checks and user checks are intended to be _distinct_ */ - @Override - public final boolean ok(User user) { - throw new UnsupportedOperationException(); - } - - /** * The filter expression is evaluated in memory if it cannot be pushed to the data store by elide for any reason. * @@ -54,7 +51,7 @@ public final boolean ok(User user) { */ @Override public final boolean ok(T object, RequestScope requestScope, Optional changeSpec) { - Class entityClass = coreScope(requestScope).getDictionary().lookupBoundClass(object.getClass()); + Class entityClass = dictionary.lookupBoundClass(object.getClass()); FilterExpression filterExpression = getFilterExpression(entityClass, requestScope); return filterExpression.accept(new FilterExpressionCheckEvaluationVisitor(object, this, requestScope)); } @@ -88,10 +85,23 @@ public boolean applyPredicateToObject(T object, FilterPredicate filterPredicate, * @param defaultPath path to use if no FieldExpressionPath defined * @return Predicates */ - protected static Path getFieldPath(Class type, RequestScope requestScope, String method, String defaultPath) { - EntityDictionary dictionary = coreScope(requestScope).getDictionary(); - FilterExpressionPath fep = dictionary.getMethodAnnotation(type, method, FilterExpressionPath.class); - return new Path(type, dictionary, fep == null ? defaultPath : fep.value()); + protected Path getFieldPath(Class type, RequestScope requestScope, String method, String defaultPath) { + try { + FilterExpressionPath fep = getFilterExpressionPath(type, method, dictionary); + return new Path(type, dictionary, fep == null ? defaultPath : fep.value()); + } catch (NoSuchMethodException | SecurityException e) { + throw new IllegalStateException(e); + } + } + + private static FilterExpressionPath getFilterExpressionPath( + Class type, + String method, + EntityDictionary dictionary) throws NoSuchMethodException { + FilterExpressionPath path = dictionary.lookupBoundClass(type) + .getMethod(method) + .getAnnotation(FilterExpressionPath.class); + return path; } protected static com.yahoo.elide.core.RequestScope coreScope(RequestScope requestScope) { diff --git a/elide-core/src/main/java/com/yahoo/elide/security/PermissionExecutor.java b/elide-core/src/main/java/com/yahoo/elide/security/PermissionExecutor.java index 5775e87937..d36ae24aa6 100644 --- a/elide-core/src/main/java/com/yahoo/elide/security/PermissionExecutor.java +++ b/elide-core/src/main/java/com/yahoo/elide/security/PermissionExecutor.java @@ -6,6 +6,7 @@ package com.yahoo.elide.security; import com.yahoo.elide.core.Path.PathElement; +import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; import com.yahoo.elide.core.filter.FilterPredicate; import com.yahoo.elide.core.filter.expression.FilterExpression; diff --git a/elide-core/src/main/java/com/yahoo/elide/security/executors/ActivePermissionExecutor.java b/elide-core/src/main/java/com/yahoo/elide/security/executors/ActivePermissionExecutor.java index 7d1f3a3916..12df709d0c 100644 --- a/elide-core/src/main/java/com/yahoo/elide/security/executors/ActivePermissionExecutor.java +++ b/elide-core/src/main/java/com/yahoo/elide/security/executors/ActivePermissionExecutor.java @@ -11,16 +11,15 @@ import com.yahoo.elide.annotation.CreatePermission; import com.yahoo.elide.annotation.DeletePermission; +import com.yahoo.elide.annotation.NonTransferable; import com.yahoo.elide.annotation.ReadPermission; -import com.yahoo.elide.annotation.SharePermission; import com.yahoo.elide.annotation.UpdatePermission; -import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; import com.yahoo.elide.core.filter.expression.FilterExpression; import com.yahoo.elide.security.ChangeSpec; import com.yahoo.elide.security.PermissionExecutor; -import com.yahoo.elide.security.PersistentResource; import com.yahoo.elide.security.permissions.ExpressionResult; import com.yahoo.elide.security.permissions.ExpressionResultCache; import com.yahoo.elide.security.permissions.PermissionExpressionBuilder; @@ -98,7 +97,7 @@ public ExpressionResult checkPermission( } /** - * Check permission on class. Checking on SharePermission falls to check ReadPermission. + * Check permission on class. Checking on Transferable falls to check ReadPermission. * * @param annotationClass annotation class * @param resource resource @@ -114,8 +113,8 @@ public ExpressionResult checkPermission(Class annotati PersistentResource resource, ChangeSpec changeSpec) { Supplier expressionSupplier = () -> { - if (SharePermission.class == annotationClass) { - if (requestScope.getDictionary().isShareable(resource.getResourceClass())) { + if (NonTransferable.class == annotationClass) { + if (requestScope.getDictionary().isTransferable(resource.getResourceClass())) { return expressionBuilder.buildAnyFieldExpressions(resource, ReadPermission.class, changeSpec); } return PermissionExpressionBuilder.FAIL_EXPRESSION; @@ -375,8 +374,7 @@ public void executeCommitChecks() { ExpressionResult result = expression.evaluate(Expression.EvaluationMode.ALL_CHECKS); if (result == FAIL) { ForbiddenAccessException e = new ForbiddenAccessException( - EntityDictionary.getSimpleName(expr.getAnnotationClass()), - expression, Expression.EvaluationMode.ALL_CHECKS); + expr.getAnnotationClass(), expression, Expression.EvaluationMode.ALL_CHECKS); if (log.isTraceEnabled()) { log.trace("{}", e.getLoggedMessage()); } @@ -423,7 +421,7 @@ private ExpressionResult executeExpressions(final Expression expression, result = expression.evaluate(Expression.EvaluationMode.ALL_CHECKS); if (result == FAIL) { ForbiddenAccessException e = new ForbiddenAccessException( - EntityDictionary.getSimpleName(annotationClass), + annotationClass, expression, Expression.EvaluationMode.ALL_CHECKS); if (log.isTraceEnabled()) { @@ -437,8 +435,7 @@ private ExpressionResult executeExpressions(final Expression expression, return DEFERRED; } if (result == FAIL) { - ForbiddenAccessException e = new ForbiddenAccessException( - EntityDictionary.getSimpleName(annotationClass), expression, mode); + ForbiddenAccessException e = new ForbiddenAccessException(annotationClass, expression, mode); if (log.isTraceEnabled()) { log.trace("{}", e.getLoggedMessage()); } diff --git a/elide-core/src/main/java/com/yahoo/elide/security/executors/BypassPermissionExecutor.java b/elide-core/src/main/java/com/yahoo/elide/security/executors/BypassPermissionExecutor.java index f66d1b76b0..42bf636e17 100644 --- a/elide-core/src/main/java/com/yahoo/elide/security/executors/BypassPermissionExecutor.java +++ b/elide-core/src/main/java/com/yahoo/elide/security/executors/BypassPermissionExecutor.java @@ -5,10 +5,10 @@ */ package com.yahoo.elide.security.executors; +import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.filter.expression.FilterExpression; import com.yahoo.elide.security.ChangeSpec; import com.yahoo.elide.security.PermissionExecutor; -import com.yahoo.elide.security.PersistentResource; import com.yahoo.elide.security.permissions.ExpressionResult; import java.lang.annotation.Annotation; diff --git a/elide-core/src/main/java/com/yahoo/elide/security/permissions/PermissionCondition.java b/elide-core/src/main/java/com/yahoo/elide/security/permissions/PermissionCondition.java index 35602b4c7e..7791ebe8bd 100644 --- a/elide-core/src/main/java/com/yahoo/elide/security/permissions/PermissionCondition.java +++ b/elide-core/src/main/java/com/yahoo/elide/security/permissions/PermissionCondition.java @@ -8,8 +8,8 @@ import com.yahoo.elide.annotation.CreatePermission; import com.yahoo.elide.annotation.DeletePermission; +import com.yahoo.elide.annotation.NonTransferable; import com.yahoo.elide.annotation.ReadPermission; -import com.yahoo.elide.annotation.SharePermission; import com.yahoo.elide.annotation.UpdatePermission; import com.yahoo.elide.security.ChangeSpec; import com.yahoo.elide.security.PersistentResource; @@ -37,7 +37,7 @@ public class PermissionCondition { .put(UpdatePermission.class, "UPDATE") .put(DeletePermission.class, "DELETE") .put(CreatePermission.class, "CREATE") - .put(SharePermission.class, "SHARE") + .put(NonTransferable.class, "NO TRANSFER") .build(); /** diff --git a/elide-core/src/main/java/com/yahoo/elide/security/permissions/PermissionExpressionBuilder.java b/elide-core/src/main/java/com/yahoo/elide/security/permissions/PermissionExpressionBuilder.java index 428298c9f6..0cd2c96e16 100644 --- a/elide-core/src/main/java/com/yahoo/elide/security/permissions/PermissionExpressionBuilder.java +++ b/elide-core/src/main/java/com/yahoo/elide/security/permissions/PermissionExpressionBuilder.java @@ -13,6 +13,7 @@ import com.yahoo.elide.annotation.ReadPermission; import com.yahoo.elide.core.CheckInstantiator; import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.RequestScope; import com.yahoo.elide.core.filter.expression.FilterExpression; import com.yahoo.elide.core.filter.expression.OrFilterExpression; @@ -20,7 +21,6 @@ import com.yahoo.elide.parsers.expression.PermissionExpressionVisitor; import com.yahoo.elide.parsers.expression.PermissionToFilterExpressionVisitor; import com.yahoo.elide.security.ChangeSpec; -import com.yahoo.elide.security.PersistentResource; import com.yahoo.elide.security.checks.Check; import com.yahoo.elide.security.permissions.expressions.AnyFieldExpression; import com.yahoo.elide.security.permissions.expressions.CheckExpression; diff --git a/elide-core/src/main/java/com/yahoo/elide/security/permissions/expressions/CheckExpression.java b/elide-core/src/main/java/com/yahoo/elide/security/permissions/expressions/CheckExpression.java index 2593a50458..2394f2dc1e 100644 --- a/elide-core/src/main/java/com/yahoo/elide/security/permissions/expressions/CheckExpression.java +++ b/elide-core/src/main/java/com/yahoo/elide/security/permissions/expressions/CheckExpression.java @@ -11,11 +11,11 @@ import static com.yahoo.elide.security.permissions.ExpressionResult.UNEVALUATED; import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.security.ChangeSpec; -import com.yahoo.elide.security.PersistentResource; import com.yahoo.elide.security.RequestScope; import com.yahoo.elide.security.checks.Check; -import com.yahoo.elide.security.checks.InlineCheck; +import com.yahoo.elide.security.checks.OperationCheck; import com.yahoo.elide.security.checks.UserCheck; import com.yahoo.elide.security.permissions.ExpressionResult; import com.yahoo.elide.security.permissions.ExpressionResultCache; @@ -76,7 +76,7 @@ public ExpressionResult evaluate(EvaluationMode mode) { return result; } - if (mode == EvaluationMode.INLINE_CHECKS_ONLY && ! (check instanceof InlineCheck)) { + if (mode == EvaluationMode.INLINE_CHECKS_ONLY && (resource != null && resource.isNewlyCreated())) { result = DEFERRED; return result; } @@ -112,7 +112,12 @@ public ExpressionResult evaluate(EvaluationMode mode) { */ private ExpressionResult computeCheck() { Object entity = (resource == null) ? null : resource.getObject(); - result = check.ok(entity, requestScope, changeSpec) ? PASS : FAIL; + + if (check instanceof UserCheck) { + result = ((UserCheck) check).ok(requestScope.getUser()) ? PASS : FAIL; + } else { + result = ((OperationCheck) check).ok(entity, requestScope, changeSpec) ? PASS : FAIL; + } return result; } diff --git a/elide-core/src/main/java/com/yahoo/elide/utils/ClassScanner.java b/elide-core/src/main/java/com/yahoo/elide/utils/ClassScanner.java index 767156a92b..90dcf8716e 100644 --- a/elide-core/src/main/java/com/yahoo/elide/utils/ClassScanner.java +++ b/elide-core/src/main/java/com/yahoo/elide/utils/ClassScanner.java @@ -10,6 +10,9 @@ import io.github.classgraph.ScanResult; import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -47,16 +50,24 @@ static public Set> getAnnotatedClasses(String packageName, Class> getAnnotatedClasses(Class annotation) { - try (ScanResult scanResult = new ClassGraph() - .enableClassInfo().enableAnnotationInfo().scan()) { - return scanResult.getClassesWithAnnotation(annotation.getCanonicalName()).stream() - .map((ClassInfo::loadClass)) - .collect(Collectors.toSet()); + static public Set> getAnnotatedClasses(List> annotations) { + Set> result = new HashSet<>(); + try (ScanResult scanResult = new ClassGraph().enableClassInfo().enableAnnotationInfo().scan()) { + for (Class annotation : annotations) { + result.addAll(scanResult.getClassesWithAnnotation(annotation.getCanonicalName()).stream() + .map((ClassInfo::loadClass)) + .collect(Collectors.toSet())); + } } + return result; + } + + @SafeVarargs + static public Set> getAnnotatedClasses(Class ...annotations) { + return getAnnotatedClasses(Arrays.asList(annotations)); } /** diff --git a/elide-core/src/main/java/com/yahoo/elide/utils/TypeHelper.java b/elide-core/src/main/java/com/yahoo/elide/utils/TypeHelper.java index 478fa0f334..70091c6c71 100644 --- a/elide-core/src/main/java/com/yahoo/elide/utils/TypeHelper.java +++ b/elide-core/src/main/java/com/yahoo/elide/utils/TypeHelper.java @@ -6,16 +6,24 @@ package com.yahoo.elide.utils; +import com.yahoo.elide.core.Path; + import com.google.common.collect.Sets; +import java.util.List; import java.util.Set; /** - * Utilities for checking classes and primitive types. + * Utilities for handling types and aliases. */ public class TypeHelper { + private static final String UNDERSCORE = "_"; + private static final String PERIOD = "."; private static final Set> PRIMITIVE_NUMBER_TYPES = Sets .newHashSet(short.class, int.class, long.class, float.class, double.class); + private static final Set> NUMBER_TYPES = Sets + .newHashSet(short.class, int.class, long.class, float.class, double.class, + Short.class, Integer.class, Long.class, Float.class, Double.class); /** * Determine whether a type is primitive number type @@ -26,4 +34,102 @@ public class TypeHelper { public static boolean isPrimitiveNumberType(Class type) { return PRIMITIVE_NUMBER_TYPES.contains(type); } + + /** + * Determine whether a type is number type + * + * @param type type to check + * @return True is the type is number type + */ + public static boolean isNumberType(Class type) { + return NUMBER_TYPES.contains(type); + } + + /** + * Extend an type alias to the final type of an extension path + * + * @param alias type alias to be extended, e.g. a_b + * @param extension path extension from aliased type, e.g. [b.c]/[c.d] + * @return extended type alias, e.g. a_b_c + */ + public static String extendTypeAlias(String alias, Path extension) { + String result = alias; + List elements = extension.getPathElements(); + + for (int i = 0; i < elements.size() - 1; i++) { + result = appendAlias(result, elements.get(i).getFieldName()); + } + + return result; + } + + /** + * Generate alias for representing a relationship path which dose not include the last field name. + * The path would start with the class alias of the first element, and then each field would append "_fieldName" to + * the result. + * The last field would not be included as that's not a part of the relationship path. + * + * @param path path that represents a relationship chain + * @return relationship path alias, i.e. foo.bar.baz would be foo_bar + */ + public static String getPathAlias(Path path) { + return extendTypeAlias(getTypeAlias(path.getPathElements().get(0).getType()), path); + } + + /** + * Append a new field to a parent alias to get new alias. + * + * @param parentAlias parent path alias + * @param fieldName field name + * @return alias for the field + */ + public static String appendAlias(String parentAlias, String fieldName) { + return nullOrEmpty(parentAlias) + ? fieldName + : nullOrEmpty(fieldName) + ? parentAlias + : parentAlias + UNDERSCORE + fieldName; + } + + /** + * Build an query friendly alias for a class. + * + * @param type The type to alias + * @return type name alias that will likely not conflict with other types or with reserved keywords. + */ + public static String getTypeAlias(Class type) { + return type.getCanonicalName().replace(PERIOD, UNDERSCORE); + } + + /** + * Get alias for the final field of a path. + * + * @param path path to the field + * @param fieldName physical field name + * @return combined alias + */ + public static String getFieldAlias(Path path, String fieldName) { + return getFieldAlias(getPathAlias(path), fieldName); + } + + /** + * Get alias for the final field of a path. + * + * @param tableAlias alias for table that contains the field + * @param fieldName physical field name + * @return combined alias + */ + public static String getFieldAlias(String tableAlias, String fieldName) { + return nullOrEmpty(tableAlias) ? fieldName : tableAlias + PERIOD + fieldName; + } + + /** + * Check whether an alias is null or empty string + * + * @param alias alias + * @return True if is null or empty + */ + private static boolean nullOrEmpty(String alias) { + return alias == null || alias.equals(""); + } } diff --git a/elide-core/src/main/java/com/yahoo/elide/utils/coerce/CoerceUtil.java b/elide-core/src/main/java/com/yahoo/elide/utils/coerce/CoerceUtil.java index c9d9e8ba41..31df500f7b 100644 --- a/elide-core/src/main/java/com/yahoo/elide/utils/coerce/CoerceUtil.java +++ b/elide-core/src/main/java/com/yahoo/elide/utils/coerce/CoerceUtil.java @@ -5,6 +5,8 @@ */ package com.yahoo.elide.utils.coerce; +import static com.yahoo.elide.utils.TypeHelper.isNumberType; + import com.yahoo.elide.core.exceptions.InvalidAttributeException; import com.yahoo.elide.core.exceptions.InvalidValueException; import com.yahoo.elide.utils.coerce.converters.FromMapConverter; @@ -13,12 +15,12 @@ import com.yahoo.elide.utils.coerce.converters.ToUUIDConverter; import com.google.common.collect.MapMaker; - import org.apache.commons.beanutils.BeanUtilsBean; import org.apache.commons.beanutils.ConversionException; import org.apache.commons.beanutils.ConvertUtils; import org.apache.commons.beanutils.Converter; +import java.lang.reflect.Array; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -49,6 +51,12 @@ public class CoerceUtil { public static T coerce(Object value, Class cls) { initializeCurrentClassLoaderIfNecessary(); + // null value of number type would be converted to 0, as 'null' would cause exception for primitive + // number classes + if (value == null && isNumberType(cls)) { + return (T) Array.get(Array.newInstance(cls, 1), 0); + } + if (value == null || cls == null || cls.isInstance(value)) { return (T) value; } diff --git a/elide-core/src/test/java/com/yahoo/elide/ElideCustomSerdeRegistrationTest.java b/elide-core/src/test/java/com/yahoo/elide/ElideCustomSerdeRegistrationTest.java index b7ab6bd5b6..eabd20bc4e 100644 --- a/elide-core/src/test/java/com/yahoo/elide/ElideCustomSerdeRegistrationTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/ElideCustomSerdeRegistrationTest.java @@ -41,7 +41,9 @@ public String serialize(Dummy val) { @Test public void testRegisterCustomSerde() { - HashMapDataStore wrapped = new HashMapDataStore(Dummy.class.getPackage()); + + //Create a fake Elide. Don't actually bind any entities. + HashMapDataStore wrapped = new HashMapDataStore(String.class.getPackage()); InMemoryDataStore store = new InMemoryDataStore(wrapped); ElideSettings elideSettings = new ElideSettingsBuilder(store).build(); new Elide(elideSettings); diff --git a/elide-core/src/test/java/com/yahoo/elide/audit/LogMessageTest.java b/elide-core/src/test/java/com/yahoo/elide/audit/LogMessageImplTest.java similarity index 82% rename from elide-core/src/test/java/com/yahoo/elide/audit/LogMessageTest.java rename to elide-core/src/test/java/com/yahoo/elide/audit/LogMessageImplTest.java index db3a4b3c34..428e64ebde 100644 --- a/elide-core/src/test/java/com/yahoo/elide/audit/LogMessageTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/audit/LogMessageImplTest.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.audit; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -15,7 +16,7 @@ import com.yahoo.elide.core.EntityDictionary; import com.yahoo.elide.core.PersistentResource; import com.yahoo.elide.core.RequestScope; -import com.yahoo.elide.security.User; +import com.yahoo.elide.security.TestUser; import com.google.common.collect.Sets; import example.Child; @@ -24,7 +25,6 @@ import org.junit.jupiter.api.Test; import java.io.IOException; -import java.security.Principal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -33,7 +33,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; -public class LogMessageTest { +public class LogMessageImplTest { private static transient PersistentResource childRecord; private static transient PersistentResource friendRecord; @@ -53,13 +53,8 @@ public static void init() { friend.setId(9); child.setFriends(Sets.newHashSet(friend)); - final RequestScope requestScope = new RequestScope(null, null, null, new User( - new Principal() { - @Override - public String getName() { - return "aaron"; - } - }), null, + final RequestScope requestScope = new RequestScope(null, NO_VERSION, null, null, + new TestUser("aaron"), null, new ElideSettingsBuilder(null) .withAuditLogger(new TestAuditLogger()) .withEntityDictionary(dictionary) @@ -73,7 +68,7 @@ public String getName() { @Test public void verifyOpaqueUserExpressions() { final String[] expressions = { "${opaqueUser.name}", "${opaqueUser.name}" }; - final LogMessage message = new LogMessage("{0} {1}", childRecord, expressions, 1, Optional.empty()); + final LogMessageImpl message = new LogMessageImpl("{0} {1}", childRecord, expressions, 1, Optional.empty()); assertEquals("aaron aaron", message.getMessage(), "JEXL substitution evaluates correctly."); assertEquals(Optional.empty(), message.getChangeSpec()); } @@ -81,7 +76,7 @@ public void verifyOpaqueUserExpressions() { @Test public void verifyObjectExpressions() { final String[] expressions = { "${child.id}", "${parent.getId()}" }; - final LogMessage message = new LogMessage("{0} {1}", childRecord, expressions, 1, Optional.empty()); + final LogMessageImpl message = new LogMessageImpl("{0} {1}", childRecord, expressions, 1, Optional.empty()); assertEquals("5 7", message.getMessage(), "JEXL substitution evaluates correctly."); assertEquals(Optional.empty(), message.getChangeSpec()); } @@ -90,8 +85,8 @@ public void verifyObjectExpressions() { public void verifyListExpressions() { final String[] expressions = { "${child[0].id}", "${child[1].id}", "${parent.getId()}" }; final String[] expressionForDefault = { "${child.id}" }; - final LogMessage message = new LogMessage("{0} {1} {2}", friendRecord, expressions, 1, Optional.empty()); - final LogMessage defaultMessage = new LogMessage("{0}", friendRecord, expressionForDefault, 1, Optional.empty()); + final LogMessageImpl message = new LogMessageImpl("{0} {1} {2}", friendRecord, expressions, 1, Optional.empty()); + final LogMessageImpl defaultMessage = new LogMessageImpl("{0}", friendRecord, expressionForDefault, 1, Optional.empty()); assertEquals("5 9 7", message.getMessage(), "JEXL substitution evaluates correctly."); assertEquals("9", defaultMessage.getMessage(), "JEXL substitution evaluates correctly."); assertEquals(Optional.empty(), message.getChangeSpec()); @@ -103,7 +98,7 @@ public void invalidExpression() { final String[] expressions = { "${child.id}, ${%%%}" }; assertThrows( InvalidSyntaxException.class, - () -> new LogMessage("{0} {1}", childRecord, expressions, 1, Optional.empty()).getMessage()); + () -> new LogMessageImpl("{0} {1}", childRecord, expressions, 1, Optional.empty()).getMessage()); } @Test @@ -111,7 +106,7 @@ public void invalidTemplate() { final String[] expressions = { "${child.id}" }; assertThrows( InvalidSyntaxException.class, - () -> new LogMessage("{}", childRecord, expressions, 1, Optional.empty()).getMessage()); + () -> new LogMessageImpl("{}", childRecord, expressions, 1, Optional.empty()).getMessage()); } public static class TestLoggerException extends RuntimeException { @@ -141,7 +136,7 @@ public void threadSafetyTest() { public void threadSafeLogger() throws IOException, InterruptedException { TestLoggerException testException = new TestLoggerException(); - LogMessage failMessage = new LogMessage("test", 0) { + LogMessageImpl failMessage = new LogMessageImpl("test", 0) { @Override public String getMessage() { throw testException; @@ -150,7 +145,7 @@ public String getMessage() { try { testAuditLogger.log(failMessage); Thread.sleep(Math.floorMod(ThreadLocalRandom.current().nextInt(), 100)); - testAuditLogger.commit(null); + testAuditLogger.commit(); fail("Exception expected"); } catch (TestLoggerException e) { assertSame(e, testException); @@ -158,7 +153,7 @@ public String getMessage() { // should not cause another exception try { - testAuditLogger.commit(null); + testAuditLogger.commit(); } catch (TestLoggerException e) { fail("Exception not cleared from previous logger commit"); } diff --git a/elide-core/src/test/java/com/yahoo/elide/audit/TestAuditLogger.java b/elide-core/src/test/java/com/yahoo/elide/audit/TestAuditLogger.java index 167433aa8d..0f007f8434 100644 --- a/elide-core/src/test/java/com/yahoo/elide/audit/TestAuditLogger.java +++ b/elide-core/src/test/java/com/yahoo/elide/audit/TestAuditLogger.java @@ -5,15 +5,14 @@ */ package com.yahoo.elide.audit; -import com.yahoo.elide.core.RequestScope; - import java.io.IOException; import java.util.ArrayList; import java.util.List; public class TestAuditLogger extends AuditLogger { @Override - public void commit(RequestScope requestScope) throws IOException { + public void commit() throws IOException { + //NOOP } public List getMessages() { diff --git a/elide-core/src/test/java/com/yahoo/elide/core/DataStoreTransactionTest.java b/elide-core/src/test/java/com/yahoo/elide/core/DataStoreTransactionTest.java index 09f24a0acc..0988a56e2a 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/DataStoreTransactionTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/DataStoreTransactionTest.java @@ -14,20 +14,20 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; -import com.yahoo.elide.security.User; +import com.yahoo.elide.request.Attribute; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Relationship; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.io.Serializable; import java.util.Arrays; -import java.util.Optional; public class DataStoreTransactionTest implements DataStoreTransaction { private static final String NAME = "name"; + private static final Attribute NAME_ATTRIBUTE = Attribute.builder().name(NAME).type(String.class).build(); private static final String ENTITY = "entity"; private RequestScope scope; @@ -42,12 +42,6 @@ public void setupMocks() { when(dictionary.getValue(ENTITY, NAME, scope)).thenReturn(3L); } - @Test - public void testAccessUser() { - User actualUser = accessUser(2L); - assertEquals(2L, actualUser.getOpaqueUser()); - } - @Test public void testPreCommit() { preCommit(); @@ -68,7 +62,7 @@ public void testSupportsSorting() { @Test public void testSupportsPagination() { - boolean actual = supportsPagination(null); + boolean actual = supportsPagination(null, null); assertTrue(actual); } @@ -80,14 +74,14 @@ public void testSupportsFiltering() { @Test public void testGetAttribute() { - Object actual = getAttribute(ENTITY, NAME, scope); + Object actual = getAttribute(ENTITY, NAME_ATTRIBUTE, scope); assertEquals(3L, actual); verify(scope, times(1)).getDictionary(); } @Test public void testSetAttribute() { - setAttribute(ENTITY, NAME, null, scope); + setAttribute(ENTITY, NAME_ATTRIBUTE, scope); verify(scope, never()).getDictionary(); } @@ -105,24 +99,29 @@ public void testUpdateToManyRelation() { @Test public void testGetRelation() { - Object actual = getRelation(this, ENTITY, NAME, Optional.empty(), Optional.empty(), Optional.empty(), scope); + Object actual = getRelation(this, ENTITY, Relationship.builder() + .name(NAME) + .projection(EntityProjection.builder() + .type(String.class) + .build()) + .build(), scope); assertEquals(3L, actual); - verify(scope, times(1)).getDictionary(); } @Test public void testLoadObject() { - String string = (String) loadObject(String.class, 2L, Optional.empty(), scope); + String string = (String) loadObject(EntityProjection.builder().type(String.class).build(), 2L, scope); assertEquals(ENTITY, string); - verify(scope, times(1)).getDictionary(); } /** Implemented to support the interface only. No need to test these. **/ + @Override + public Object loadObject(EntityProjection entityProjection, Serializable id, RequestScope scope) { + return ENTITY; + } @Override - @Deprecated - public Iterable loadObjects(Class entityClass, Optional filterExpression, - Optional sorting, Optional pagination, RequestScope scope) { + public Iterable loadObjects(EntityProjection entityProjection, RequestScope scope) { return Arrays.asList(ENTITY); } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/EntityDictionaryTest.java b/elide-core/src/test/java/com/yahoo/elide/core/EntityDictionaryTest.java index 70e87f4ad9..c5894c6642 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/EntityDictionaryTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/EntityDictionaryTest.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.core; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.UPDATE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -12,25 +13,23 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import com.yahoo.elide.Injector; import com.yahoo.elide.annotation.ComputedAttribute; import com.yahoo.elide.annotation.Exclude; import com.yahoo.elide.annotation.FilterExpressionPath; import com.yahoo.elide.annotation.Include; +import com.yahoo.elide.annotation.LifeCycleHookBinding; import com.yahoo.elide.annotation.MappedInterface; -import com.yahoo.elide.annotation.OnUpdatePreSecurity; import com.yahoo.elide.annotation.ReadPermission; import com.yahoo.elide.annotation.SecurityCheck; import com.yahoo.elide.core.exceptions.InvalidAttributeException; +import com.yahoo.elide.core.filter.expression.FilterExpression; import com.yahoo.elide.functions.LifeCycleHook; -import com.yahoo.elide.models.generics.Employee; -import com.yahoo.elide.models.generics.Manager; +import com.yahoo.elide.security.FilterExpressionCheck; import com.yahoo.elide.security.checks.UserCheck; import com.yahoo.elide.security.checks.prefab.Collections.AppendOnly; import com.yahoo.elide.security.checks.prefab.Collections.RemoveOnly; -import com.yahoo.elide.security.checks.prefab.Common.UpdateOnCreate; import com.yahoo.elide.security.checks.prefab.Role; import com.google.common.collect.ImmutableList; @@ -49,6 +48,9 @@ import example.Right; import example.StringId; import example.User; +import example.models.generics.Employee; +import example.models.generics.Manager; +import example.models.versioned.BookV2; import org.junit.jupiter.api.Test; @@ -61,18 +63,20 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; - +import javax.inject.Inject; import javax.persistence.AccessType; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; +import javax.persistence.OneToOne; import javax.persistence.Transient; public class EntityDictionaryTest extends EntityDictionary { //Test class to validate inheritance logic @Include(rootLevel = true, type = "friend") - private class Friend extends Child { } + private class Friend extends Child { + } public EntityDictionaryTest() { super(Collections.EMPTY_MAP, mock(Injector.class)); @@ -93,21 +97,29 @@ private void init() { bindEntity(Employee.class); bindEntity(Job.class); bindEntity(NoId.class); + bindEntity(BookV2.class); + bindEntity(Book.class); checkNames.forcePut("user has all access", Role.ALL.class); } + @Test + public void testSetId() { + Parent parent = new Parent(); + setId(parent, "123"); + assertEquals(parent.getId(), 123); + } + @Test public void testFindCheckByExpression() { assertEquals("user has all access", getCheckIdentifier(Role.ALL.class)); assertEquals("Prefab.Role.None", getCheckIdentifier(Role.NONE.class)); assertEquals("Prefab.Collections.AppendOnly", getCheckIdentifier(AppendOnly.class)); assertEquals("Prefab.Collections.RemoveOnly", getCheckIdentifier(RemoveOnly.class)); - assertEquals("Prefab.Common.UpdateOnCreate", getCheckIdentifier(UpdateOnCreate.class)); } @SecurityCheck("User is Admin") - public class Foo extends UserCheck { + public class Bar extends UserCheck { @Override public boolean ok(com.yahoo.elide.security.User user) { @@ -121,12 +133,39 @@ public void testCheckScan() { EntityDictionary testDictionary = new EntityDictionary(new HashMap<>()); testDictionary.scanForSecurityChecks(); - assertEquals("User is Admin", testDictionary.getCheckIdentifier(Foo.class)); + assertEquals("User is Admin", testDictionary.getCheckIdentifier(Bar.class)); + } + + @SecurityCheck("Filter Expression Injection Test") + public class Foo extends FilterExpressionCheck { + + @Inject + Long testLong; + + @Override + public FilterExpression getFilterExpression(Class entityClass, + com.yahoo.elide.security.RequestScope requestScope) { + assertEquals(testLong, 123L); + return null; + } + } + + @Test + public void testCheckInjection() { + EntityDictionary testDictionary = new EntityDictionary(new HashMap<>(), new Injector() { + @Override + public void inject(Object entity) { + ((Foo) entity).testLong = 123L; + } + }); + testDictionary.scanForSecurityChecks(); + + assertEquals("Filter Expression Injection Test", testDictionary.getCheckIdentifier(Foo.class)); } @Test public void testGetAttributeOrRelationAnnotation() { - String[] fields = { "field1", "field2", "field3", "relation1", "relation2" }; + String[] fields = {"field1", "field2", "field3", "relation1", "relation2"}; Annotation annotation; for (String field : fields) { annotation = getAttributeOrRelationAnnotation(FunWithPermissions.class, ReadPermission.class, field); @@ -134,28 +173,6 @@ public void testGetAttributeOrRelationAnnotation() { } } - @Test - public void testBindingInitializerPriorToBindingEntityClass() { - @Entity - @Include - class Foo { - @Id - private long id; - - private int bar; - } - - Initializer initializer = mock(Initializer.class); - bindInitializer(initializer, Foo.class); - - assertEquals(1, getAllFields(Foo.class).size()); - - Foo foo = new Foo(); - initializeEntity(foo); - - verify(initializer).initialize(foo); - } - @Test public void testBindingTriggerPriorToBindingEntityClass1() { @Entity @@ -169,7 +186,7 @@ class Foo2 { LifeCycleHook trigger = mock(LifeCycleHook.class); - bindTrigger(Foo2.class, OnUpdatePreSecurity.class, "bar", trigger); + bindTrigger(Foo2.class, "bar", UPDATE, LifeCycleHookBinding.TransactionPhase.PRESECURITY, trigger); assertEquals(1, getAllFields(Foo2.class).size()); } @@ -186,7 +203,7 @@ class Foo3 { LifeCycleHook trigger = mock(LifeCycleHook.class); - bindTrigger(Foo3.class, OnUpdatePreSecurity.class, trigger, true); + bindTrigger(Foo3.class, UPDATE, LifeCycleHookBinding.TransactionPhase.PRESECURITY, trigger, true); assertEquals(1, getAllFields(Foo3.class).size()); } @@ -203,7 +220,7 @@ class Foo4 { LifeCycleHook trigger = mock(LifeCycleHook.class); - bindTrigger(Foo4.class, OnUpdatePreSecurity.class, trigger); + bindTrigger(Foo4.class, UPDATE, LifeCycleHookBinding.TransactionPhase.PRESECURITY, trigger, false); assertEquals(1, getAllFields(Foo4.class).size()); } @@ -358,7 +375,7 @@ public void testDetectCascadeRelations() { @Test public void testGetIdAnnotations() throws Exception { - Collection expectedAnnotationClasses = Arrays.asList(new Class[] { Id.class, GeneratedValue.class }); + Collection expectedAnnotationClasses = Arrays.asList(new Class[]{Id.class, GeneratedValue.class}); Collection actualAnnotationsClasses = getIdAnnotations(new Parent()).stream() .map(Annotation::annotationType) .collect(Collectors.toList()); @@ -385,7 +402,7 @@ class NoId { @Test public void testGetIdAnnotationsSubClass() throws Exception { - Collection expectedAnnotationClasses = Arrays.asList(new Class[] { Id.class, GeneratedValue.class }); + Collection expectedAnnotationClasses = Arrays.asList(new Class[]{Id.class, GeneratedValue.class}); Collection actualAnnotationsClasses = getIdAnnotations(new Friend()).stream() .map(Annotation::annotationType) .collect(Collectors.toList()); @@ -396,12 +413,12 @@ public void testGetIdAnnotationsSubClass() throws Exception { @Test public void testIsSharableTrue() throws Exception { - assertTrue(isShareable(Right.class)); + assertTrue(isTransferable(Right.class)); } @Test public void testIsSharableFalse() throws Exception { - assertFalse(isShareable(Left.class)); + assertFalse(isTransferable(Left.class)); } @Test @@ -489,15 +506,17 @@ public void testNoExcludedFieldsReturned() { List attrs = getAttributes(Child.class); List rels = getRelationships(Child.class); assertTrue(!attrs.contains("excludedEntity") && !attrs.contains("excludedRelationship") - && !attrs.contains("excludedEntityList")); + && !attrs.contains("excludedEntityList")); assertTrue(!rels.contains("excludedEntity") && !rels.contains("excludedRelationship") - && !rels.contains("excludedEntityList")); + && !rels.contains("excludedEntityList")); } @MappedInterface - public interface SuitableInterface { } + public interface SuitableInterface { + } - public interface BadInterface { } + public interface BadInterface { + } @Test public void testMappedInterface() { @@ -528,20 +547,20 @@ class SubsubclassBinding extends SubclassBinding { bindEntity(SubclassBinding.class); bindEntity(SubsubclassBinding.class); - assertEquals(SubclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); + assertEquals(SuperclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); assertEquals(SuperclassBinding.class, getEntityBinding(SuperclassBinding.class).entityClass); assertEquals(SuperclassBinding.class, lookupEntityClass(SuperclassBinding.class)); assertEquals(SuperclassBinding.class, lookupEntityClass(SubclassBinding.class)); assertEquals(SuperclassBinding.class, lookupEntityClass(SubsubclassBinding.class)); - assertEquals("subclassBinding", getEntityFor(SubclassBinding.class)); + assertEquals("superclassBinding", getEntityFor(SubclassBinding.class)); assertEquals("superclassBinding", getEntityFor(SuperclassBinding.class)); - assertEquals(SubclassBinding.class, getEntityClass("subclassBinding")); - assertEquals(SuperclassBinding.class, getEntityClass("superclassBinding")); + assertNull(getEntityClass("subclassBinding", NO_VERSION)); + assertEquals(SuperclassBinding.class, getEntityClass("superclassBinding", NO_VERSION)); - assertEquals("subclassBinding", getJsonAliasFor(SubclassBinding.class)); + assertEquals("superclassBinding", getJsonAliasFor(SubclassBinding.class)); assertEquals("superclassBinding", getJsonAliasFor(SuperclassBinding.class)); } @@ -565,11 +584,10 @@ class SubsubclassBinding extends SubclassBinding { } bindEntity(SuperclassBinding.class); - bindEntity(SubclassBinding.class); bindEntity(SubsubclassBinding.class); assertEquals(SuperclassBinding.class, getEntityBinding(SuperclassBinding.class).entityClass); - assertEquals(SubclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); + assertEquals(SuperclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); assertEquals(SubsubclassBinding.class, getEntityBinding(SubsubclassBinding.class).entityClass); assertEquals(SuperclassBinding.class, lookupEntityClass(SuperclassBinding.class)); @@ -606,15 +624,13 @@ class SubsubclassBinding extends SubclassBinding { } bindEntity(SuperclassBinding.class); - bindEntity(SubclassBinding.class); - bindEntity(SubsubclassBinding.class); - assertEquals(SubclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); + assertEquals(SuperclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); assertEquals(SuperclassBinding.class, getEntityBinding(SuperclassBinding.class).entityClass); assertEquals(SuperclassBinding.class, lookupIncludeClass(SuperclassBinding.class)); - assertEquals(SubclassBinding.class, lookupIncludeClass(SubclassBinding.class)); - assertEquals(SubsubclassBinding.class, lookupIncludeClass(SubsubclassBinding.class)); + assertEquals(SuperclassBinding.class, lookupIncludeClass(SubclassBinding.class)); + assertEquals(SuperclassBinding.class, lookupIncludeClass(SubsubclassBinding.class)); } @Test @@ -635,15 +651,14 @@ class SubsubclassBinding extends SubclassBinding { } bindEntity(SuperclassBinding.class); - bindEntity(SubclassBinding.class); bindEntity(SubsubclassBinding.class); - assertEquals(SubclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); + assertEquals(SuperclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); assertEquals(SuperclassBinding.class, getEntityBinding(SuperclassBinding.class).entityClass); assertEquals(SubsubclassBinding.class, getEntityBinding(SubsubclassBinding.class).entityClass); assertEquals(SuperclassBinding.class, lookupIncludeClass(SuperclassBinding.class)); - assertEquals(SubclassBinding.class, lookupIncludeClass(SubclassBinding.class)); + assertEquals(SuperclassBinding.class, lookupIncludeClass(SubclassBinding.class)); assertEquals(SubsubclassBinding.class, lookupIncludeClass(SubsubclassBinding.class)); } @@ -663,24 +678,24 @@ class SubsubclassBinding extends SubclassBinding { } bindEntity(SuperclassBinding.class); - bindEntity(SubclassBinding.class); bindEntity(SubsubclassBinding.class); - assertEquals(SubclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); + assertEquals(SuperclassBinding.class, getEntityBinding(SubclassBinding.class).entityClass); assertEquals(SuperclassBinding.class, getEntityBinding(SuperclassBinding.class).entityClass); assertThrows(IllegalArgumentException.class, () -> { getEntityBinding(SubsubclassBinding.class); }); assertEquals(SuperclassBinding.class, lookupIncludeClass(SuperclassBinding.class)); - assertEquals(SubclassBinding.class, lookupIncludeClass(SubclassBinding.class)); + assertEquals(SuperclassBinding.class, lookupIncludeClass(SubclassBinding.class)); assertEquals(null, lookupIncludeClass(SubsubclassBinding.class)); } @Test public void testGetFirstAnnotation() { @Exclude - class Foo { } + class Foo { + } @Include class Bar extends Foo { @@ -699,7 +714,8 @@ class Baz extends Bar { public void testGetFirstAnnotationConflict() { @Exclude @Include - class Foo { } + class Foo { + } Annotation first = getFirstAnnotation(Foo.class, Arrays.asList(Exclude.class, Include.class)); assertTrue(first instanceof Exclude); @@ -727,6 +743,93 @@ public void testBadLookupEntityClass() { assertThrows(IllegalArgumentException.class, () -> lookupEntityClass(Object.class)); } + @Test + public void testFieldIsInjected() { + EntityDictionary testDictionary = new EntityDictionary(new HashMap<>()); + + @Include + class FieldInject { + @Inject + private String field; + } + + testDictionary.bindEntity(FieldInject.class); + + assertTrue(testDictionary.getEntityBinding(FieldInject.class).isInjected()); + } + + @Test + public void testInheritedFieldIsInjected() { + EntityDictionary testDictionary = new EntityDictionary(new HashMap<>()); + class BaseClass { + @Inject + private String field; + } + + @Include + class SubClass extends BaseClass { + private String anotherField; + } + + testDictionary.bindEntity(SubClass.class); + + assertTrue(testDictionary.getEntityBinding(SubClass.class).isInjected()); + } + + @Test + public void testMethodIsInjected() { + EntityDictionary testDictionary = new EntityDictionary(new HashMap<>()); + + @Include + class MethodInject { + @Inject + private void setField(String field) { + //NOOP + } + } + + testDictionary.bindEntity(MethodInject.class); + + assertTrue(testDictionary.getEntityBinding(MethodInject.class).isInjected()); + } + + @Test + public void testInhertedMethodIsInjected() { + EntityDictionary testDictionary = new EntityDictionary(new HashMap<>()); + class BaseClass { + @Inject + private void setField(String field) { + //NOOP + } + } + + @Include + class SubClass extends BaseClass { + private String anotherField; + } + + testDictionary.bindEntity(SubClass.class); + + assertTrue(testDictionary.getEntityBinding(SubClass.class).isInjected()); + } + + @Test + public void testConstructorIsInjected() { + EntityDictionary testDictionary = new EntityDictionary(new HashMap<>()); + + @Include + class ConstructorInject { + @Inject + public ConstructorInject(String field) { + //NOOP + } + } + + testDictionary.bindEntity(ConstructorInject.class); + + assertTrue(testDictionary.getEntityBinding(ConstructorInject.class).isInjected()); + } + @Test public void testFieldLookup() throws Exception { bindEntity(Book.class); @@ -831,4 +934,61 @@ public void testCheckLookup() throws Exception { assertThrows(IllegalArgumentException.class, () -> this.getCheck(String.class.getName())); } + + @Test + public void testAttributeOrRelationAnnotationExists() { + assertTrue(attributeOrRelationAnnotationExists(Job.class, "jobId", Id.class)); + assertFalse(attributeOrRelationAnnotationExists(Job.class, "title", OneToOne.class)); + } + + @Test + public void testIsValidField() { + assertTrue(isValidField(Job.class, "title")); + assertFalse(isValidField(Job.class, "foo")); + } + + @Test + public void testGetBoundByVersion() { + Set> models = getBoundClassesByVersion("1.0"); + assertEquals(3, models.size()); //Also includes com.yahoo.elide inner classes from this file. + assertTrue(models.contains(BookV2.class)); + + + models = getBoundClassesByVersion(NO_VERSION); + assertEquals(14, models.size()); + } + + @Test + public void testGetEntityClassByVersion() { + Class model = getEntityClass("book", NO_VERSION); + assertEquals(Book.class, model); + + model = getEntityClass("book", "1.0"); + assertEquals(BookV2.class, model); + } + + @Test + public void testGetModelVersion() { + assertEquals("1.0", getModelVersion(BookV2.class)); + assertEquals(NO_VERSION, getModelVersion(Book.class)); + } + + @Test + public void testHasBinding() { + assertTrue(hasBinding(FunWithPermissions.class)); + assertTrue(hasBinding(Parent.class)); + assertTrue(hasBinding(Child.class)); + assertTrue(hasBinding(User.class)); + assertTrue(hasBinding(Left.class)); + assertTrue(hasBinding(Right.class)); + assertTrue(hasBinding(StringId.class)); + assertTrue(hasBinding(Friend.class)); + assertTrue(hasBinding(FieldAnnotations.class)); + assertTrue(hasBinding(Manager.class)); + assertTrue(hasBinding(Employee.class)); + assertTrue(hasBinding(Job.class)); + assertTrue(hasBinding(NoId.class)); + assertTrue(hasBinding(BookV2.class)); + assertTrue(hasBinding(Book.class)); + } } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/LifeCycleTest.java b/elide-core/src/test/java/com/yahoo/elide/core/LifeCycleTest.java index 2d811cd562..1275cafd30 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/LifeCycleTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/LifeCycleTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016, Yahoo Inc. + * Copyright 2020, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ @@ -7,20 +7,27 @@ import static com.yahoo.elide.Elide.JSONAPI_CONTENT_TYPE; import static com.yahoo.elide.Elide.JSONAPI_CONTENT_TYPE_WITH_JSON_PATCH_EXTENSION; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.CREATE; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.DELETE; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.READ; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.UPDATE; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.TransactionPhase.POSTCOMMIT; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.TransactionPhase.PRECOMMIT; +import static com.yahoo.elide.annotation.LifeCycleHookBinding.TransactionPhase.PRESECURITY; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; + import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; -import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -29,150 +36,325 @@ import com.yahoo.elide.ElideResponse; import com.yahoo.elide.ElideSettings; import com.yahoo.elide.ElideSettingsBuilder; -import com.yahoo.elide.annotation.Exclude; import com.yahoo.elide.annotation.Include; -import com.yahoo.elide.annotation.OnCreatePostCommit; -import com.yahoo.elide.annotation.OnCreatePreCommit; -import com.yahoo.elide.annotation.OnCreatePreSecurity; -import com.yahoo.elide.annotation.OnDeletePostCommit; -import com.yahoo.elide.annotation.OnDeletePreCommit; -import com.yahoo.elide.annotation.OnDeletePreSecurity; -import com.yahoo.elide.annotation.OnReadPostCommit; -import com.yahoo.elide.annotation.OnReadPreCommit; -import com.yahoo.elide.annotation.OnReadPreSecurity; -import com.yahoo.elide.annotation.OnUpdatePostCommit; -import com.yahoo.elide.annotation.OnUpdatePreCommit; -import com.yahoo.elide.annotation.OnUpdatePreSecurity; +import com.yahoo.elide.annotation.LifeCycleHookBinding; import com.yahoo.elide.audit.AuditLogger; -import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore; -import com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore; import com.yahoo.elide.functions.LifeCycleHook; +import com.yahoo.elide.request.Attribute; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Relationship; import com.yahoo.elide.security.ChangeSpec; +import com.yahoo.elide.security.TestUser; import com.yahoo.elide.security.User; -import com.yahoo.elide.security.checks.Check; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; - -import example.Author; -import example.Book; -import example.Editor; -import example.Publisher; -import example.TestCheckMappings; - -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import lombok.Getter; +import lombok.Setter; + import java.util.Arrays; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.Optional; +import java.util.Set; -import javax.persistence.Entity; import javax.persistence.Id; -import javax.persistence.Transient; +import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; import javax.validation.ConstraintViolationException; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; /** * Tests the invocation & sequencing of DataStoreTransaction method invocations and life cycle events. + * Model used to mock different lifecycle test scenarios. This model uses fields instead of properties. */ -public class LifeCycleTest { +@Include(type = "testModel") +@LifeCycleHookBinding(hook = FieldTestModel.ClassPreSecurityHook.class, operation = CREATE, phase = PRESECURITY) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPreCommitHook.class, operation = CREATE, phase = PRECOMMIT) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPostCommitHook.class, operation = CREATE, phase = POSTCOMMIT) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPreSecurityHook.class, operation = DELETE, phase = PRESECURITY) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPreCommitHookEverything.class, operation = CREATE, + phase = PRECOMMIT, oncePerRequest = false) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPreCommitHook.class, operation = DELETE, phase = PRECOMMIT) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPostCommitHook.class, operation = DELETE, phase = POSTCOMMIT) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPreSecurityHook.class, operation = UPDATE, phase = PRESECURITY) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPreCommitHook.class, operation = UPDATE, phase = PRECOMMIT) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPostCommitHook.class, operation = UPDATE, phase = POSTCOMMIT) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPreSecurityHook.class, operation = READ, phase = PRESECURITY) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPreCommitHook.class, operation = READ, phase = PRECOMMIT) +@LifeCycleHookBinding(hook = FieldTestModel.ClassPostCommitHook.class, operation = READ, phase = POSTCOMMIT) +class FieldTestModel { + + @Id + private String id; + + @Getter + @Setter + @LifeCycleHookBinding(hook = FieldTestModel.AttributePreSecurityHook.class, operation = CREATE, phase = PRESECURITY) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePreCommitHook.class, operation = CREATE, phase = PRECOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePostCommitHook.class, operation = CREATE, phase = POSTCOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePreSecurityHook.class, operation = DELETE, phase = PRESECURITY) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePreCommitHook.class, operation = DELETE, phase = PRECOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePostCommitHook.class, operation = DELETE, phase = POSTCOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePreSecurityHook.class, operation = UPDATE, phase = PRESECURITY) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePreCommitHook.class, operation = UPDATE, phase = PRECOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePostCommitHook.class, operation = UPDATE, phase = POSTCOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePreSecurityHook.class, operation = READ, phase = PRESECURITY) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePreCommitHook.class, operation = READ, phase = PRECOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.AttributePostCommitHook.class, operation = READ, phase = POSTCOMMIT) + private String field; + + @Getter + @Setter + @OneToMany + @LifeCycleHookBinding(hook = FieldTestModel.RelationPreSecurityHook.class, operation = CREATE, phase = PRESECURITY) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPreCommitHook.class, operation = CREATE, phase = PRECOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPostCommitHook.class, operation = CREATE, phase = POSTCOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPreSecurityHook.class, operation = DELETE, phase = PRESECURITY) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPreCommitHook.class, operation = DELETE, phase = PRECOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPostCommitHook.class, operation = DELETE, phase = POSTCOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPreSecurityHook.class, operation = UPDATE, phase = PRESECURITY) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPreCommitHook.class, operation = UPDATE, phase = PRECOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPostCommitHook.class, operation = UPDATE, phase = POSTCOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPreSecurityHook.class, operation = READ, phase = PRESECURITY) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPreCommitHook.class, operation = READ, phase = PRECOMMIT) + @LifeCycleHookBinding(hook = FieldTestModel.RelationPostCommitHook.class, operation = READ, phase = POSTCOMMIT) + private Set models = new HashSet<>(); + + static class ClassPreSecurityHook implements LifeCycleHook { + @Override + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.classCallback(operation, PRESECURITY); + } + } - private static final AuditLogger MOCK_AUDIT_LOGGER = mock(AuditLogger.class); - private EntityDictionary dictionary; - private MockCallback callback; - private MockCallback onUpdateDeferredCallback; - private MockCallback onUpdateImmediateCallback; - private MockCallback onUpdatePostCommitCallback; - private MockCallback onUpdatePostCommitAuthor; + static class ClassPreCommitHook implements LifeCycleHook { + @Override + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.classCallback(operation, PRECOMMIT); + } + } + static class ClassPreCommitHookEverything implements LifeCycleHook { + @Override + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.classAllFieldsCallback(operation, PRECOMMIT); + } + } - public class MockCallback implements LifeCycleHook { + static class ClassPostCommitHook implements LifeCycleHook { @Override - public void execute(T object, com.yahoo.elide.security.RequestScope scope, Optional changes) { - //NOOP + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.classCallback(operation, POSTCOMMIT); } } - public class TestEntityDictionary extends EntityDictionary { - public TestEntityDictionary(Map> checks) { - super(checks); + static class AttributePreSecurityHook implements LifeCycleHook { + @Override + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.attributeCallback(operation, PRESECURITY, changes.orElse(null)); } + } + static class AttributePreCommitHook implements LifeCycleHook { @Override - public Class lookupBoundClass(Class objClass) { - // Special handling for mocked Book class which has Entity annotation - if (objClass.getName().contains("$MockitoMock$")) { - objClass = objClass.getSuperclass(); - } - return super.lookupBoundClass(objClass); + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.attributeCallback(operation, PRECOMMIT, changes.orElse(null)); } + } + static class AttributePostCommitHook implements LifeCycleHook { + @Override + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.attributeCallback(operation, POSTCOMMIT, changes.orElse(null)); + } } - LifeCycleTest() throws Exception { - callback = mock(MockCallback.class); - onUpdateDeferredCallback = mock(MockCallback.class); - onUpdateImmediateCallback = mock(MockCallback.class); - onUpdatePostCommitCallback = mock(MockCallback.class); - onUpdatePostCommitAuthor = mock(MockCallback.class); - dictionary = new TestEntityDictionary(TestCheckMappings.MAPPINGS); - dictionary.bindEntity(Book.class); - dictionary.bindEntity(Author.class); - dictionary.bindEntity(Publisher.class); - dictionary.bindEntity(Editor.class); - ImmutableList.of( - OnCreatePostCommit.class, OnCreatePreCommit.class, OnCreatePreSecurity.class, - OnReadPostCommit.class, OnReadPreCommit.class, OnReadPreSecurity.class, - OnDeletePostCommit.class, OnDeletePreCommit.class, OnDeletePreSecurity.class) - .stream().forEach(cls -> dictionary.bindTrigger(Book.class, cls, callback)); - ImmutableList.of( - OnUpdatePostCommit.class, OnUpdatePreCommit.class, OnUpdatePreSecurity.class) - .stream().forEach(cls -> dictionary.bindTrigger(Book.class, cls, "title", callback)); - dictionary.bindTrigger(Book.class, OnUpdatePreCommit.class, onUpdateDeferredCallback, true); - dictionary.bindTrigger(Book.class, OnUpdatePreSecurity.class, onUpdateImmediateCallback, true); - dictionary.bindTrigger(Book.class, OnUpdatePostCommit.class, onUpdatePostCommitCallback, true); - dictionary.bindTrigger(Author.class, OnUpdatePostCommit.class, onUpdatePostCommitAuthor, true); + static class RelationPreSecurityHook implements LifeCycleHook { + @Override + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.relationCallback(operation, PRESECURITY, changes.orElse(null)); + } + } + + static class RelationPreCommitHook implements LifeCycleHook { + @Override + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.relationCallback(operation, PRECOMMIT, changes.orElse(null)); + } + } + + static class RelationPostCommitHook implements LifeCycleHook { + @Override + public void execute(LifeCycleHookBinding.Operation operation, + FieldTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.relationCallback(operation, POSTCOMMIT, changes.orElse(null)); + } + } + + public void classCallback(LifeCycleHookBinding.Operation operation, + LifeCycleHookBinding.TransactionPhase phase) { + //NOOP - this will be mocked to verify hook invocation. } - @BeforeEach - public void clearMocks() { - clearInvocations(onUpdatePostCommitAuthor, onUpdateImmediateCallback, onUpdatePostCommitCallback, onUpdatePostCommitAuthor); + public void attributeCallback(LifeCycleHookBinding.Operation operation, + LifeCycleHookBinding.TransactionPhase phase, + ChangeSpec changes) { + //NOOP - this will be mocked to verify hook invocation. + } + + public void relationCallback(LifeCycleHookBinding.Operation operation, + LifeCycleHookBinding.TransactionPhase phase, + ChangeSpec changes) { + //NOOP - this will be mocked to verify hook invocation. + } + + public void classAllFieldsCallback(LifeCycleHookBinding.Operation operation, + LifeCycleHookBinding.TransactionPhase phase) { + //NOOP - this will be mocked to verify hook invocation. + } +} + +/** + * Model used to mock different lifecycle test scenarios. This model uses properties instead of fields. + */ +@Include +class PropertyTestModel { + private String id; + + private Set models = new HashSet<>(); + + static class RelationPostCommitHook implements LifeCycleHook { + @Override + public void execute(LifeCycleHookBinding.Operation operation, + PropertyTestModel elideEntity, + com.yahoo.elide.security.RequestScope requestScope, + Optional changes) { + elideEntity.relationCallback(operation, POSTCOMMIT, changes.orElse(null)); + } + } + + @Id + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @ManyToMany + @LifeCycleHookBinding(hook = PropertyTestModel.RelationPostCommitHook.class, + operation = CREATE, phase = POSTCOMMIT) + @LifeCycleHookBinding(hook = PropertyTestModel.RelationPostCommitHook.class, + operation = UPDATE, phase = POSTCOMMIT) + public Set getModels() { + return models; + } + + public void setModels(Set models) { + this.models = models; + } + + public void relationCallback(LifeCycleHookBinding.Operation operation, + LifeCycleHookBinding.TransactionPhase phase, + ChangeSpec changes) { + //NOOP - this will be mocked to verify hook invocation. + } +} + +/** + * Tests the invocation & sequencing of DataStoreTransaction method invocations and life cycle events. + */ +public class LifeCycleTest { + + private static final AuditLogger MOCK_AUDIT_LOGGER = mock(AuditLogger.class); + private EntityDictionary dictionary; + + LifeCycleTest() throws Exception { + dictionary = TestDictionary.getTestDictionary(); + dictionary.bindEntity(FieldTestModel.class); + dictionary.bindEntity(PropertyTestModel.class); } @Test public void testElideCreate() throws Exception { DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - Book book = mock(Book.class); + FieldTestModel mockModel = mock(FieldTestModel.class); Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); - String bookBody = "{\"data\": {\"type\":\"book\",\"attributes\": {\"title\":\"Grapes of Wrath\"}}}"; + String body = "{\"data\": {\"type\":\"testModel\",\"id\":\"1\",\"attributes\": {\"field\":\"Foo\"}}}"; when(store.beginTransaction()).thenReturn(tx); - when(tx.createNewObject(Book.class)).thenReturn(book); + when(tx.createNewObject(FieldTestModel.class)).thenReturn(mockModel); - ElideResponse response = elide.post("/book", bookBody, null); + ElideResponse response = elide.post("/testModel", body, null, NO_VERSION); assertEquals(HttpStatus.SC_CREATED, response.getResponseCode()); - /* - * This gets called for : - * - read pre-security for the book - * - create pre-security for the book - * - read pre-commit for the book - * - create pre-commit for the book - * - read post-commit for the book - * - create post-commit for the book - */ - verify(callback, times(6)).execute(eq(book), isA(RequestScope.class), any()); - verify(tx).accessUser(any()); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(POSTCOMMIT)); + verify(mockModel, times(1)).classCallback(eq(CREATE), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(CREATE), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(CREATE), eq(POSTCOMMIT)); + verify(mockModel, never()).classCallback(eq(UPDATE), any()); + verify(mockModel, never()).classCallback(eq(DELETE), any()); + + verify(mockModel, times(2)).classAllFieldsCallback(any(), any()); + verify(mockModel, times(2)).classAllFieldsCallback(eq(CREATE), eq(PRECOMMIT)); + + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(CREATE), eq(PRESECURITY), any()); + verify(mockModel, times(1)).attributeCallback(eq(CREATE), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(CREATE), eq(POSTCOMMIT), any()); + verify(mockModel, never()).attributeCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(PRESECURITY), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(POSTCOMMIT), any()); + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(DELETE), any(), any()); + verify(tx).preCommit(); - verify(tx, times(1)).createObject(eq(book), isA(RequestScope.class)); + verify(tx, times(1)).createObject(eq(mockModel), isA(RequestScope.class)); verify(tx).flush(isA(RequestScope.class)); verify(tx).commit(isA(RequestScope.class)); verify(tx).close(); @@ -182,66 +364,72 @@ public void testElideCreate() throws Exception { public void testElideCreateFailure() throws Exception { DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - Book book = mock(Book.class); - doThrow(RuntimeException.class).when(book).setTitle(anyString()); + FieldTestModel mockModel = mock(FieldTestModel.class); + doThrow(RuntimeException.class).when(mockModel).setField(anyString()); Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); - String bookBody = "{\"data\": {\"type\":\"book\",\"attributes\": {\"title\":\"Grapes of Wrath\"}}}"; + String body = "{\"data\": {\"type\":\"testModel\",\"id\":\"1\",\"attributes\": {\"field\":\"Foo\"}}}"; when(store.beginTransaction()).thenReturn(tx); - when(tx.createNewObject(Book.class)).thenReturn(book); + when(tx.createNewObject(FieldTestModel.class)).thenReturn(mockModel); - ElideResponse response = elide.post("/book", bookBody, null); + ElideResponse response = elide.post("/testModel", body, null, NO_VERSION); assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, response.getResponseCode()); assertEquals( - "{\"errors\":[{\"detail\":\"InternalServerErrorException: Unexpected exception caught\"}]}", + "{\"errors\":[{\"detail\":\"Unexpected exception caught\"}]}", response.getBody()); - /* - * This gets called for : - * - read pre-security for the book - * - create pre-security for the book - * - read pre-commit for the book - * - create pre-commit for the book - * - read post-commit for the book - * - create post-commit for the book - */ - verify(callback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(tx).accessUser(any()); + verify(mockModel, never()).classCallback(any(), any()); + verify(mockModel, never()).attributeCallback(any(), any(), any()); + verify(mockModel, never()).relationCallback(any(), any(), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(tx, never()).preCommit(); - verify(tx, never()).createObject(eq(book), isA(RequestScope.class)); + verify(tx, never()).createObject(eq(mockModel), isA(RequestScope.class)); verify(tx, never()).flush(isA(RequestScope.class)); verify(tx, never()).commit(isA(RequestScope.class)); verify(tx).close(); } - @Test public void testElideGet() throws Exception { DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - Book book = mock(Book.class); - when(book.getId()).thenReturn(1L); + FieldTestModel mockModel = mock(FieldTestModel.class); Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); when(store.beginReadTransaction()).thenCallRealMethod(); when(store.beginTransaction()).thenReturn(tx); - when(tx.loadObject(eq(Book.class), any(), any(), isA(RequestScope.class))).thenReturn(book); + when(tx.loadObject(isA(EntityProjection.class), any(), isA(RequestScope.class))).thenReturn(mockModel); MultivaluedMap headers = new MultivaluedHashMap<>(); - ElideResponse response = elide.get("/book/1", headers, null); + ElideResponse response = elide.get("/testModel/1", headers, null, NO_VERSION); assertEquals(HttpStatus.SC_OK, response.getResponseCode()); - /* - * This gets called for : - * - read pre-security for the book - * - read pre-commit for the book - * - read post-commit for the book - */ - verify(callback, times(3)).execute(eq(book), isA(RequestScope.class), any()); - verify(tx).accessUser(any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(POSTCOMMIT)); + verify(mockModel, never()).classCallback(eq(CREATE), any()); + verify(mockModel, never()).classCallback(eq(UPDATE), any()); + verify(mockModel, never()).classCallback(eq(DELETE), any()); + + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, never()).attributeCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, never()).relationCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(DELETE), any(), any()); verify(tx).preCommit(); verify(tx).flush(any()); verify(tx).commit(any()); @@ -249,33 +437,40 @@ public void testElideGet() throws Exception { } @Test - public void testElideGetRelationship() throws Exception { + public void testElideGetSparse() throws Exception { DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - Book book = mock(Book.class); - Author author = mock(Author.class); - when(book.getId()).thenReturn(1L); - when(author.getId()).thenReturn(2L); - when(book.getAuthors()).thenReturn(ImmutableSet.of(author)); + FieldTestModel mockModel = mock(FieldTestModel.class); Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); when(store.beginReadTransaction()).thenCallRealMethod(); when(store.beginTransaction()).thenReturn(tx); - when(tx.loadObject(eq(Book.class), any(), any(), isA(RequestScope.class))).thenReturn(book); + when(tx.loadObject(isA(EntityProjection.class), any(), isA(RequestScope.class))).thenReturn(mockModel); MultivaluedMap headers = new MultivaluedHashMap<>(); - ElideResponse response = elide.get("/book/1/relationships/authors", headers, null); + headers.putSingle("fields[testModel]", "field"); + ElideResponse response = elide.get("/testModel/1", headers, null, NO_VERSION); assertEquals(HttpStatus.SC_OK, response.getResponseCode()); - /* - * This gets called for : - * - read pre-security for the book - * - read pre-commit for the book - * - read post-commit for the book - */ - verify(callback, times(3)).execute(eq(book), isA(RequestScope.class), any()); - verify(tx).accessUser(any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(POSTCOMMIT)); + verify(mockModel, never()).classCallback(eq(CREATE), any()); + verify(mockModel, never()).classCallback(eq(UPDATE), any()); + verify(mockModel, never()).classCallback(eq(DELETE), any()); + + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, never()).attributeCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + + verify(mockModel, never()).relationCallback(any(), any(), any()); + verify(tx).preCommit(); verify(tx).flush(any()); verify(tx).commit(any()); @@ -283,88 +478,95 @@ public void testElideGetRelationship() throws Exception { } @Test - public void testElidePatch() throws Exception { + public void testElideGetRelationship() throws Exception { DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - Book book = mock(Book.class); + FieldTestModel mockModel = mock(FieldTestModel.class); + FieldTestModel child = mock(FieldTestModel.class); + when(mockModel.getModels()).thenReturn(ImmutableSet.of(child)); Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); - when(book.getId()).thenReturn(1L); + when(store.beginReadTransaction()).thenCallRealMethod(); when(store.beginTransaction()).thenReturn(tx); - when(tx.loadObject(eq(Book.class), any(), any(), isA(RequestScope.class))).thenReturn(book); + when(tx.loadObject(isA(EntityProjection.class), any(), isA(RequestScope.class))).thenReturn(mockModel); - String bookBody = "{\"data\":{\"type\":\"book\",\"id\":1,\"attributes\": {\"title\":\"Grapes of Wrath\"}}}"; + MultivaluedMap headers = new MultivaluedHashMap<>(); + ElideResponse response = elide.get("/testModel/1/relationships/models", headers, null, NO_VERSION); + assertEquals(HttpStatus.SC_OK, response.getResponseCode()); - String contentType = JSONAPI_CONTENT_TYPE; - ElideResponse response = elide.patch(contentType, contentType, "/book/1", bookBody, null); - assertEquals(HttpStatus.SC_NO_CONTENT, response.getResponseCode()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(POSTCOMMIT)); + verify(mockModel, never()).classCallback(eq(CREATE), any()); + verify(mockModel, never()).classCallback(eq(UPDATE), any()); + verify(mockModel, never()).classCallback(eq(DELETE), any()); + + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, never()).attributeCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, never()).relationCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(DELETE), any(), any()); - /* - * This gets called for : - * - read pre-security for the book - * - update pre-security for the book.title - * - read pre-commit for the book - * - update pre-commit for the book.title - * - read post-commit for the book - * - update post-commit for the book.title - */ - verify(callback, times(6)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateDeferredCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, never()).execute(eq(book), isA(RequestScope.class), eq(Optional.empty())); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), eq(Optional.empty())); - verify(tx).accessUser(any()); verify(tx).preCommit(); - - verify(tx).save(eq(book), isA(RequestScope.class)); - verify(tx).flush(isA(RequestScope.class)); - verify(tx).commit(isA(RequestScope.class)); + verify(tx).flush(any()); + verify(tx).commit(any()); verify(tx).close(); } @Test - public void testElidePatchFailure() throws Exception { + public void testElidePatch() throws Exception { DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - Book book = mock(Book.class); + FieldTestModel mockModel = mock(FieldTestModel.class); Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); - when(book.getId()).thenReturn(1L); - when(store.beginTransaction()).thenReturn(tx); - when(tx.loadObject(eq(Book.class), any(), any(), isA(RequestScope.class))).thenReturn(book); - doThrow(ConstraintViolationException.class).when(tx).flush(any()); + String body = "{\"data\": {\"type\":\"testModel\",\"id\":\"1\",\"attributes\": {\"field\":\"Foo\"}}}"; - String bookBody = "{\"data\":{\"type\":\"book\",\"id\":1,\"attributes\": {\"title\":\"Grapes of Wrath\"}}}"; + dictionary.setValue(mockModel, "id", "1"); + when(store.beginTransaction()).thenReturn(tx); + when(tx.loadObject(isA(EntityProjection.class), any(), isA(RequestScope.class))).thenReturn(mockModel); String contentType = JSONAPI_CONTENT_TYPE; - ElideResponse response = elide.patch(contentType, contentType, "/book/1", bookBody, null); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getResponseCode()); - assertEquals( - "{\"errors\":[{\"detail\":\"Constraint violation\"}]}", - response.getBody()); + ElideResponse response = elide.patch(contentType, contentType, "/testModel/1", body, null, NO_VERSION); + assertEquals(HttpStatus.SC_NO_CONTENT, response.getResponseCode()); - /* - * This gets called for : - * - read pre-security for the book - * - update pre-security for the book.title - * - read pre-commit for the book - * - update pre-commit for the book.title - * - read post-commit for the book - * - update post-commit for the book.title - */ - verify(callback, times(2)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, never()).execute(eq(book), isA(RequestScope.class), eq(Optional.empty())); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), eq(Optional.empty())); - verify(tx).accessUser(any()); - verify(tx, times(1)).preCommit(); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); - verify(tx, times(1)).save(eq(book), isA(RequestScope.class)); - verify(tx, times(1)).flush(isA(RequestScope.class)); - verify(tx, never()).commit(isA(RequestScope.class)); + verify(mockModel, never()).classCallback(eq(READ), any()); + verify(mockModel, never()).classCallback(eq(CREATE), any()); + verify(mockModel, never()).classCallback(eq(DELETE), any()); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(POSTCOMMIT)); + + verify(mockModel, never()).attributeCallback(eq(READ), any(), any()); + verify(mockModel, never()).attributeCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(PRESECURITY), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(POSTCOMMIT), any()); + + verify(mockModel, never()).relationCallback(eq(READ), any(), any()); + verify(mockModel, never()).relationCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(DELETE), any(), any()); + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + + verify(tx).preCommit(); + verify(tx).save(eq(mockModel), isA(RequestScope.class)); + verify(tx).flush(isA(RequestScope.class)); + verify(tx).commit(isA(RequestScope.class)); verify(tx).close(); } @@ -372,28 +574,43 @@ public void testElidePatchFailure() throws Exception { public void testElideDelete() throws Exception { DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - Book book = mock(Book.class); + FieldTestModel mockModel = mock(FieldTestModel.class); Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); - when(book.getId()).thenReturn(1L); + dictionary.setValue(mockModel, "id", "1"); when(store.beginTransaction()).thenReturn(tx); - when(tx.loadObject(eq(Book.class), any(), any(), isA(RequestScope.class))).thenReturn(book); + when(tx.loadObject(isA(EntityProjection.class), any(), isA(RequestScope.class))).thenReturn(mockModel); - ElideResponse response = elide.delete("/book/1", "", null); + ElideResponse response = elide.delete("/testModel/1", "", null, NO_VERSION); assertEquals(HttpStatus.SC_NO_CONTENT, response.getResponseCode()); - /* - * This gets called for : - * - delete pre-security for the book - * - delete pre-commit for the book - * - delete post-commit for the book - */ - verify(callback, times(3)).execute(eq(book), isA(RequestScope.class), any()); - verify(tx).accessUser(any()); - verify(tx).preCommit(); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, never()).classCallback(eq(UPDATE), any()); + verify(mockModel, never()).classCallback(eq(CREATE), any()); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(POSTCOMMIT)); + verify(mockModel, times(1)).classCallback(eq(DELETE), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(DELETE), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(DELETE), eq(POSTCOMMIT)); + + verify(mockModel, never()).attributeCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(READ), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + + //TODO - Read should not be called for a delete. + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(DELETE), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(POSTCOMMIT), any()); - verify(tx).delete(eq(book), isA(RequestScope.class)); + verify(tx).preCommit(); + verify(tx).delete(eq(mockModel), isA(RequestScope.class)); verify(tx).flush(isA(RequestScope.class)); verify(tx).commit(isA(RequestScope.class)); verify(tx).close(); @@ -542,741 +759,623 @@ public void testElidePatchExtensionDelete() throws Exception { verify(tx).close(); } - @Test - public void testCreate() { - Book book = mock(Book.class); + public void testElidePatchFailure() throws Exception { + DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.createNewObject(Book.class)).thenReturn(book); - RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = PersistentResource.createObject(null, Book.class, scope, Optional.of("uuid")); - resource.setValueChecked("title", "should not affect calls since this is create!"); - resource.setValueChecked("genre", "boring books"); - assertNotNull(resource); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).checkPermission(scope); + FieldTestModel mockModel = mock(FieldTestModel.class); - scope.runQueuedPreSecurityTriggers(); - verify(book, times(1)).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, never()).onUpdatePreSecurityTitle(scope); - verify(book, never()).onCreatePreCommitStar(eq(scope), any()); - verify(book, times(1)).onReadPreSecurity(scope); - verify(book, never()).checkPermission(scope); + Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); - scope.runQueuedPreCommitTriggers(); - verify(book, times(1)).onCreatePreCommit(scope); - verify(book, never()).onDeletePreCommit(scope); - verify(book, never()).onUpdatePreCommitTitle(scope); - verify(book, times(2)).onCreatePreCommitStar(eq(scope), any()); - verify(book, times(1)).onReadPreCommitTitle(scope); - verify(book, never()).checkPermission(scope); + String body = "{\"data\": {\"type\":\"testModel\",\"id\":\"1\",\"attributes\": {\"field\":\"Foo\"}}}"; - scope.getPermissionExecutor().executeCommitChecks(); - verify(book, times(3)).checkPermission(scope); + dictionary.setValue(mockModel, "id", "1"); + when(store.beginTransaction()).thenReturn(tx); + when(tx.loadObject(isA(EntityProjection.class), any(), isA(RequestScope.class))).thenReturn(mockModel); + doThrow(ConstraintViolationException.class).when(tx).flush(any()); - scope.runQueuedPostCommitTriggers(); - verify(book, times(1)).onCreatePostCommit(scope); - verify(book, never()).onDeletePostCommit(scope); - verify(book, never()).onUpdatePostCommitTitle(scope); - verify(book, times(2)).onCreatePreCommitStar(eq(scope), any()); - verify(book, times(1)).onReadPostCommit(scope); - verify(book, times(3)).checkPermission(scope); + String contentType = JSONAPI_CONTENT_TYPE; + ElideResponse response = elide.patch(contentType, contentType, "/testModel/1", body, null, NO_VERSION); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getResponseCode()); + assertEquals( + "{\"errors\":[{\"detail\":\"Constraint violation\"}]}", + response.getBody()); + + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, never()).classCallback(eq(READ), any()); + verify(mockModel, never()).classCallback(eq(CREATE), any()); + verify(mockModel, never()).classCallback(eq(DELETE), any()); + + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(PRESECURITY)); + verify(mockModel, times(0)).classCallback(eq(UPDATE), eq(PRECOMMIT)); + verify(mockModel, times(0)).classCallback(eq(UPDATE), eq(POSTCOMMIT)); + + verify(mockModel, never()).attributeCallback(eq(READ), any(), any()); + verify(mockModel, never()).attributeCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(PRESECURITY), any()); + verify(mockModel, times(0)).attributeCallback(eq(UPDATE), eq(PRECOMMIT), any()); + verify(mockModel, times(0)).attributeCallback(eq(UPDATE), eq(POSTCOMMIT), any()); + + verify(mockModel, never()).relationCallback(eq(READ), any(), any()); + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(DELETE), any(), any()); + + verify(tx).preCommit(); + verify(tx).save(eq(mockModel), isA(RequestScope.class)); + verify(tx).flush(isA(RequestScope.class)); + verify(tx, never()).commit(isA(RequestScope.class)); + verify(tx).close(); } @Test - public void testUpdate() { - Book book = mock(Book.class); + public void testElidePatchExtensionCreate() throws Exception { + DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); + FieldTestModel mockModel = mock(FieldTestModel.class); - RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, scope.getUUIDFor(book), scope); - resource.setValueChecked("title", "new title"); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, times(1)).onUpdatePreSecurityTitle(scope); - verify(book, times(1)).onReadPreSecurity(scope); - verify(book, never()).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(book, times(1)).checkPermission(scope); + Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); - scope.runQueuedPreSecurityTriggers(); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, times(1)).onUpdatePreSecurityTitle(scope); - verify(book, times(1)).onReadPreSecurity(scope); - verify(book, never()).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(book, times(1)).checkPermission(scope); + String body = "[{\"op\": \"add\",\"path\": \"/testModel\",\"value\":{" + + "\"type\":\"testModel\",\"id\":\"1\",\"attributes\": {\"field\":\"Foo\"}}}]"; - scope.runQueuedPreCommitTriggers(); - verify(book, never()).onCreatePreCommit(scope); - verify(book, never()).onDeletePreCommit(scope); - verify(book, times(1)).onUpdatePreCommitTitle(scope); - verify(book, times(1)).onReadPreCommitTitle(scope); - verify(book, times(1)).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(book, times(1)).checkPermission(scope); + when(store.beginTransaction()).thenReturn(tx); + when(tx.createNewObject(FieldTestModel.class)).thenReturn(mockModel); - scope.getPermissionExecutor().executeCommitChecks(); - verify(book, times(1)).checkPermission(scope); + String contentType = JSONAPI_CONTENT_TYPE_WITH_JSON_PATCH_EXTENSION; + ElideResponse response = elide.patch(contentType, contentType, "/", body, null, NO_VERSION); + assertEquals(HttpStatus.SC_OK, response.getResponseCode()); - scope.runQueuedPostCommitTriggers(); - verify(book, never()).onCreatePostCommit(scope); - verify(book, never()).onDeletePostCommit(scope); - verify(book, times(1)).onUpdatePostCommitTitle(scope); - verify(book, times(1)).onReadPostCommit(scope); - verify(book, times(1)).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitAuthor, never()).execute(any(), isA(RequestScope.class), any()); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(POSTCOMMIT)); + verify(mockModel, times(1)).classCallback(eq(CREATE), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(CREATE), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(CREATE), eq(POSTCOMMIT)); + verify(mockModel, never()).classCallback(eq(UPDATE), any()); + verify(mockModel, never()).classCallback(eq(DELETE), any()); + + verify(mockModel, times(2)).classAllFieldsCallback(any(), any()); + verify(mockModel, times(2)).classAllFieldsCallback(eq(CREATE), eq(PRECOMMIT)); + + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(CREATE), eq(PRESECURITY), any()); + verify(mockModel, times(1)).attributeCallback(eq(CREATE), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(CREATE), eq(POSTCOMMIT), any()); + verify(mockModel, never()).attributeCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(PRESECURITY), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(POSTCOMMIT), any()); + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(DELETE), any(), any()); - // verify no empty callbacks - verifyNoEmptyCallbacks(); + verify(tx).preCommit(); + verify(tx, times(1)).createObject(eq(mockModel), isA(RequestScope.class)); + verify(tx).flush(isA(RequestScope.class)); + verify(tx).commit(isA(RequestScope.class)); + verify(tx).close(); } @Test - public void testUpdateWithChangeSpec() { - Book book = mock(Book.class); + public void failElidePatchExtensionCreate() throws Exception { + DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); + FieldTestModel mockModel = mock(FieldTestModel.class); - RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, scope.getUUIDFor(book), scope); - - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, never()).onUpdatePreSecurityGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, never()).onUpdatePreSecurityGenre(any(RequestScope.class), isNull()); - verify(book, never()).onReadPreSecurity(scope); - verify(book, never()).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(book, never()).checkPermission(scope); - - //Verify changeSpec is passed to hooks - resource.setValueChecked("genre", "new genre"); - - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, times(1)).onUpdatePreSecurityGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, times(1)).onReadPreSecurity(scope); - verify(book, never()).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(book, times(1)).checkPermission(scope); - - scope.runQueuedPreSecurityTriggers(); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, times(1)).onUpdatePreSecurityGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, times(1)).onReadPreSecurity(scope); - verify(book, never()).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(book, times(1)).checkPermission(scope); + Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); - scope.runQueuedPreCommitTriggers(); - verify(book, never()).onCreatePreCommit(scope); - verify(book, never()).onDeletePreCommit(scope); - verify(book, times(1)).onUpdatePreCommitGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, times(1)).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(book, times(1)).checkPermission(scope); + String body = "[{\"op\": \"add\",\"path\": \"/testModel\",\"value\":{" + + "\"type\":\"testModel\",\"attributes\": {\"field\":\"Foo\"}}}]"; - scope.getPermissionExecutor().executeCommitChecks(); - verify(book, times(1)).checkPermission(scope); + when(store.beginTransaction()).thenReturn(tx); + when(tx.createNewObject(FieldTestModel.class)).thenReturn(mockModel); - scope.runQueuedPostCommitTriggers(); - verify(book, never()).onCreatePostCommit(scope); - verify(book, never()).onDeletePostCommit(scope); - verify(book, times(1)).onUpdatePostCommitGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, times(1)).onReadPostCommit(scope); - verify(book, times(1)).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitAuthor, never()).execute(isA(Author.class), isA(RequestScope.class), any()); + String contentType = JSONAPI_CONTENT_TYPE_WITH_JSON_PATCH_EXTENSION; + ElideResponse response = elide.patch(contentType, contentType, "/", body, null, NO_VERSION); + assertEquals(HttpStatus.SC_BAD_REQUEST, response.getResponseCode()); + assertEquals( + "[{\"errors\":[{\"detail\":\"Bad Request Body'Patch extension requires all objects to have an assigned ID (temporary or permanent) when assigning relationships.'\",\"status\":\"400\"}]}]", + response.getBody()); - // verify no empty callbacks - verifyNoEmptyCallbacks(); + verify(tx, never()).preCommit(); + verify(tx, never()).flush(isA(RequestScope.class)); + verify(tx, never()).commit(isA(RequestScope.class)); + verify(tx).close(); } @Test - public void testMultipleUpdateWithChangeSpec() { - Book book = mock(Book.class); + public void testElidePatchExtensionUpdate() throws Exception { + DataStore store = mock(DataStore.class); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + FieldTestModel mockModel = mock(FieldTestModel.class); + + Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); + + String body = "[{\"op\": \"replace\",\"path\": \"/testModel/1\",\"value\":{" + + "\"type\":\"testModel\",\"id\":\"1\",\"attributes\": {\"field\":\"Foo\"}}}]"; + + dictionary.setValue(mockModel, "id", "1"); + when(store.beginTransaction()).thenReturn(tx); + when(tx.loadObject(isA(EntityProjection.class), any(), isA(RequestScope.class))).thenReturn(mockModel); + + String contentType = JSONAPI_CONTENT_TYPE_WITH_JSON_PATCH_EXTENSION; + ElideResponse response = elide.patch(contentType, contentType, "/", body, null, NO_VERSION); + assertEquals(HttpStatus.SC_OK, response.getResponseCode()); + + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, never()).classCallback(eq(READ), any()); + verify(mockModel, never()).classCallback(eq(CREATE), any()); + verify(mockModel, never()).classCallback(eq(DELETE), any()); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(POSTCOMMIT)); + + verify(mockModel, never()).attributeCallback(eq(READ), any(), any()); + verify(mockModel, never()).attributeCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(PRESECURITY), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(POSTCOMMIT), any()); + + verify(mockModel, never()).relationCallback(eq(READ), any(), any()); + verify(mockModel, never()).relationCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(DELETE), any(), any()); + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(tx).preCommit(); + verify(tx).save(eq(mockModel), isA(RequestScope.class)); + verify(tx).flush(isA(RequestScope.class)); + verify(tx).commit(isA(RequestScope.class)); + verify(tx).close(); + } + + @Test + public void testElidePatchExtensionDelete() throws Exception { + DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); + FieldTestModel mockModel = mock(FieldTestModel.class); + + Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); + + dictionary.setValue(mockModel, "id", "1"); + when(store.beginTransaction()).thenReturn(tx); + when(tx.loadObject(isA(EntityProjection.class), any(), isA(RequestScope.class))).thenReturn(mockModel); + String body = "[{\"op\": \"remove\",\"path\": \"/testModel\",\"value\":{" + + "\"type\":\"testModel\",\"id\":\"1\"}}]"; + + String contentType = JSONAPI_CONTENT_TYPE_WITH_JSON_PATCH_EXTENSION; + ElideResponse response = elide.patch(contentType, contentType, "/", body, null, NO_VERSION); + assertEquals(HttpStatus.SC_OK, response.getResponseCode()); + + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, never()).classCallback(eq(UPDATE), any()); + verify(mockModel, never()).classCallback(eq(CREATE), any()); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(READ), eq(POSTCOMMIT)); + verify(mockModel, times(1)).classCallback(eq(DELETE), eq(PRESECURITY)); + verify(mockModel, times(1)).classCallback(eq(DELETE), eq(PRECOMMIT)); + verify(mockModel, times(1)).classCallback(eq(DELETE), eq(POSTCOMMIT)); + + verify(mockModel, never()).attributeCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).attributeCallback(eq(READ), any(), any()); + verify(mockModel, never()).attributeCallback(eq(DELETE), any(), any()); + + //TODO - Read should not be called for a delete. + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(CREATE), any(), any()); + verify(mockModel, never()).relationCallback(eq(DELETE), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(POSTCOMMIT), any()); + + verify(tx).preCommit(); + verify(tx).delete(eq(mockModel), isA(RequestScope.class)); + verify(tx).flush(isA(RequestScope.class)); + verify(tx).commit(isA(RequestScope.class)); + verify(tx).close(); + } + + @Test + public void testCreate() { + FieldTestModel mockModel = mock(FieldTestModel.class); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.createNewObject(FieldTestModel.class)).thenReturn(mockModel); RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, scope.getUUIDFor(book), scope); - - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, never()).onUpdatePreSecurityGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, never()).onUpdatePreSecurityGenre(any(RequestScope.class), isNull()); - verify(book, never()).onReadPreSecurity(scope); - verify(book, never()).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(book, never()).checkPermission(scope); - - // Verify changeSpec is passed to hooks - resource.setValueChecked("genre", "new genre"); - resource.setValueChecked("title", "new title"); - - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, times(1)).onUpdatePreSecurityGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, never()).onUpdatePreSecurityGenre(any(RequestScope.class), isNull()); - verify(book, times(1)).onReadPreSecurity(scope); - verify(book, never()).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(2)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(book, times(2)).checkPermission(scope); + PersistentResource resource = PersistentResource.createObject(FieldTestModel.class, scope, Optional.of("1")); + resource.setValueChecked("field", "should not affect calls since this is create!"); + + verify(mockModel, never()).classCallback(any(), any()); + verify(mockModel, never()).attributeCallback(any(), any(), any()); + verify(mockModel, never()).relationCallback(any(), any(), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); scope.runQueuedPreSecurityTriggers(); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, times(1)).onUpdatePreSecurityGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, times(1)).onReadPreSecurity(scope); - verify(book, never()).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(2)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(book, times(2)).checkPermission(scope); + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(CREATE), eq(PRESECURITY)); + verify(mockModel, times(1)).attributeCallback(any(), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(CREATE), eq(PRESECURITY), any()); + verify(mockModel, times(1)).relationCallback(any(), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(PRESECURITY), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + clearInvocations(mockModel); scope.runQueuedPreCommitTriggers(); - verify(book, never()).onCreatePreCommit(scope); - verify(book, never()).onDeletePreCommit(scope); - verify(book, times(1)).onUpdatePreCommitGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, times(1)).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, times(2)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(2)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(book, times(2)).checkPermission(scope); - scope.getPermissionExecutor().executeCommitChecks(); - verify(book, times(2)).checkPermission(scope); + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(CREATE), eq(PRECOMMIT)); + verify(mockModel, times(1)).attributeCallback(any(), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(CREATE), eq(PRECOMMIT), any()); + verify(mockModel, times(1)).relationCallback(any(), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(PRECOMMIT), any()); + verify(mockModel, times(2)).classAllFieldsCallback(any(), any()); + verify(mockModel, times(2)).classAllFieldsCallback(eq(CREATE), eq(PRECOMMIT)); + clearInvocations(mockModel); + scope.getPermissionExecutor().executeCommitChecks(); scope.runQueuedPostCommitTriggers(); - verify(book, never()).onCreatePostCommit(scope); - verify(book, never()).onDeletePostCommit(scope); - verify(book, times(1)).onUpdatePostCommitGenre(any(RequestScope.class), any(ChangeSpec.class)); - verify(book, times(1)).onReadPostCommit(scope); - verify(book, times(1)).onUpdatePreCommit(); - verify(onUpdateDeferredCallback, times(2)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(2)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitCallback, times(2)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitAuthor, never()).execute(any(), isA(RequestScope.class), any()); - - verifyNoEmptyCallbacks(); + + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(CREATE), eq(POSTCOMMIT)); + verify(mockModel, times(1)).attributeCallback(any(), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(CREATE), eq(POSTCOMMIT), any()); + verify(mockModel, times(1)).relationCallback(any(), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(POSTCOMMIT), any()); } @Test - public void testUpdateRelationshipWithChangeSpec() { - Book book = new Book(); - Author author = new Author(); - book.setAuthors(Sets.newHashSet(author)); - author.setBooks(Sets.newHashSet(book)); + public void testRead() { + FieldTestModel mockModel = mock(FieldTestModel.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(author), eq("books"), any(), any(), any(), any())).then((i) -> author.getBooks()); - when(tx.getRelation(any(), eq(book), eq("authors"), any(), any(), any(), any())).then((i) -> book.getAuthors()); - + when(tx.createNewObject(FieldTestModel.class)).thenReturn(mockModel); RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resourceBook = new PersistentResource(book, null, scope.getUUIDFor(book), scope); - PersistentResource resourceAuthor = new PersistentResource(author, null, scope.getUUIDFor(book), scope); + PersistentResource resource = new PersistentResource(mockModel, null, "1", scope); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitAuthor, never()).execute(eq(book), isA(RequestScope.class), any()); + resource.getValueChecked(Attribute.builder().type(String.class).name("field").build()); - //Verify changeSpec is passed to hooks - resourceAuthor.removeRelation("books", resourceBook); + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRESECURITY)); + verify(mockModel, times(1)).attributeCallback(any(), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).relationCallback(any(), any(), any()); + clearInvocations(mockModel); scope.runQueuedPreSecurityTriggers(); - verify(onUpdateDeferredCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); + verify(mockModel, never()).classCallback(any(), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).relationCallback(any(), any(), any()); + verify(mockModel, never()).attributeCallback(any(), any(), any()); + + clearInvocations(mockModel); scope.runQueuedPreCommitTriggers(); - verify(onUpdateDeferredCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - scope.getPermissionExecutor().executeCommitChecks(); - verify(onUpdatePostCommitCallback, never()).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitAuthor, never()).execute(eq(book), isA(RequestScope.class), any()); + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRECOMMIT)); + verify(mockModel, times(1)).attributeCallback(any(), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).relationCallback(any(), any(), any()); + clearInvocations(mockModel); scope.runQueuedPostCommitTriggers(); - verify(onUpdateDeferredCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdateImmediateCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitCallback, times(1)).execute(eq(book), isA(RequestScope.class), any()); - verify(onUpdatePostCommitAuthor, times(1)).execute(eq(author), isA(RequestScope.class), any()); - verifyNoEmptyCallbacks(); + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(READ), eq(POSTCOMMIT)); + verify(mockModel, times(1)).attributeCallback(any(), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).relationCallback(any(), any(), any()); } @Test - public void testOnDelete() { - Book book = mock(Book.class); + public void testDelete() { + FieldTestModel mockModel = mock(FieldTestModel.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - + when(tx.createNewObject(FieldTestModel.class)).thenReturn(mockModel); RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, scope.getUUIDFor(book), scope); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, never()).onUpdatePreSecurityTitle(scope); - verify(book, never()).onReadPreSecurity(scope); - verify(book, never()).checkPermission(scope); + PersistentResource resource = new PersistentResource(mockModel, null, "1", scope); resource.deleteResource(); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, times(1)).onDeletePreSecurity(scope); - verify(book, never()).onUpdatePreSecurityTitle(scope); - verify(book, never()).onReadPreSecurity(scope); - verify(book, times(1)).checkPermission(scope); + verify(mockModel, times(2)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(DELETE), eq(PRESECURITY)); + + //TODO - DELETE should not invoke READ. + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRESECURITY)); + verify(mockModel, times(1)).relationCallback(any(), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRESECURITY), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).attributeCallback(any(), any(), any()); + + clearInvocations(mockModel); scope.runQueuedPreSecurityTriggers(); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, times(1)).onDeletePreSecurity(scope); - verify(book, never()).onUpdatePreSecurityTitle(scope); - verify(book, never()).onReadPreSecurity(scope); + + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).classCallback(any(), any()); + verify(mockModel, never()).attributeCallback(any(), any(), any()); + verify(mockModel, never()).relationCallback(any(), any(), any()); scope.runQueuedPreCommitTriggers(); - verify(book, never()).onCreatePreCommit(scope); - verify(book, times(1)).onDeletePreCommit(scope); - verify(book, never()).onUpdatePreCommitTitle(scope); - verify(book, never()).onReadPreCommitTitle(scope); - scope.getPermissionExecutor().executeCommitChecks(); - verify(book, times(1)).checkPermission(scope); + verify(mockModel, times(2)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(DELETE), eq(PRECOMMIT)); + + //TODO - DELETE should not invoke READ. + verify(mockModel, times(1)).classCallback(eq(READ), eq(PRECOMMIT)); + verify(mockModel, times(1)).relationCallback(any(), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(PRECOMMIT), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).attributeCallback(any(), any(), any()); + clearInvocations(mockModel); + scope.getPermissionExecutor().executeCommitChecks(); scope.runQueuedPostCommitTriggers(); - verify(book, never()).onCreatePostCommit(scope); - verify(book, times(1)).onDeletePostCommit(scope); - verify(book, never()).onUpdatePostCommitTitle(scope); - verify(book, never()).onReadPostCommit(scope); + + verify(mockModel, times(2)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(DELETE), eq(POSTCOMMIT)); + + //TODO - DELETE should not invoke READ. + verify(mockModel, times(1)).classCallback(eq(READ), eq(POSTCOMMIT)); + verify(mockModel, times(1)).relationCallback(any(), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(READ), eq(POSTCOMMIT), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).attributeCallback(any(), any(), any()); } @Test - public void testOnRead() { - Book book = mock(Book.class); + public void testAttributeUpdate() { + FieldTestModel mockModel = mock(FieldTestModel.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.createNewObject(FieldTestModel.class)).thenReturn(mockModel); RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, scope.getUUIDFor(book), scope); - resource.getValueChecked("title"); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, never()).onUpdatePreSecurityTitle(scope); - verify(book, times(1)).onReadPreSecurity(scope); + PersistentResource resource = new PersistentResource(mockModel, null, scope.getUUIDFor(mockModel), scope); + resource.setValueChecked("field", "new value"); + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(PRESECURITY)); + verify(mockModel, times(1)).attributeCallback(any(), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(PRESECURITY), notNull()); + + verify(mockModel, never()).relationCallback(any(), any(), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + clearInvocations(mockModel); scope.runQueuedPreSecurityTriggers(); - verify(book, never()).onCreatePreSecurity(scope); - verify(book, never()).onDeletePreSecurity(scope); - verify(book, never()).onUpdatePreSecurityTitle(scope); - verify(book, times(1)).onReadPreSecurity(scope); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).relationCallback(any(), any(), any()); + verify(mockModel, never()).attributeCallback(any(), any(), any()); + + clearInvocations(mockModel); scope.runQueuedPreCommitTriggers(); - verify(book, never()).onCreatePreCommit(scope); - verify(book, never()).onDeletePreCommit(scope); - verify(book, never()).onUpdatePreCommitTitle(scope); - verify(book, times(1)).onReadPreCommitTitle(scope); - scope.getPermissionExecutor().executeCommitChecks(); + verify(mockModel, never()).relationCallback(any(), any(), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(PRECOMMIT)); + verify(mockModel, times(1)).attributeCallback(any(), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(PRECOMMIT), notNull()); + clearInvocations(mockModel); + scope.getPermissionExecutor().executeCommitChecks(); scope.runQueuedPostCommitTriggers(); - verify(book, never()).onCreatePostCommit(scope); - verify(book, never()).onDeletePostCommit(scope); - verify(book, never()).onUpdatePostCommitTitle(scope); - verify(book, times(1)).onReadPostCommit(scope); + + verify(mockModel, never()).relationCallback(any(), any(), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(POSTCOMMIT)); + verify(mockModel, times(1)).attributeCallback(any(), any(), any()); + verify(mockModel, times(1)).attributeCallback(eq(UPDATE), eq(POSTCOMMIT), notNull()); } @Test - public void testPreSecurityLifecycleHookException() { - @Entity - @Include - class Book { - public String title; - - @OnUpdatePreSecurity(value = "title") - public void blowUp(RequestScope scope) { - throw new IllegalStateException(); - } - } - - EntityDictionary dictionary = new EntityDictionary(new HashMap<>()); + public void testRelationshipUpdate() { + FieldTestModel mockModel = mock(FieldTestModel.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - dictionary.bindEntity(Book.class); - - Book book = new Book(); + when(tx.createNewObject(FieldTestModel.class)).thenReturn(mockModel); RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, "1", scope); - assertThrows(IllegalStateException.class, () -> resource.updateAttribute("title", "New value")); - } + FieldTestModel modelToAdd = mock(FieldTestModel.class); - @Test - public void testPreCommitLifeCycleHookException() { - @Entity - @Include - class Book { - public String title; - - @OnUpdatePreCommit(value = "title") - public void blowUp(RequestScope scope) { - throw new IllegalStateException(); - } - } + PersistentResource resource = new PersistentResource(mockModel, null, scope.getUUIDFor(mockModel), scope); + PersistentResource resourceToAdd = new PersistentResource(modelToAdd, null, scope.getUUIDFor(mockModel), scope); - EntityDictionary dictionary = new EntityDictionary(new HashMap<>()); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - dictionary.bindEntity(Book.class); + resource.addRelation("models", resourceToAdd); - Book book = new Book(); - RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, "1", scope); + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(PRESECURITY)); - resource.updateAttribute("title", "New value"); + //TODO - this should be only called once. THis is called twice because the mock has a null collection. + verify(mockModel, times(2)).relationCallback(any(), any(), any()); + verify(mockModel, times(2)).relationCallback(eq(UPDATE), eq(PRESECURITY), notNull()); - assertThrows(IllegalStateException.class, () -> scope.runQueuedPreCommitTriggers()); - } + verify(mockModel, never()).attributeCallback(any(), any(), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); - /** - * Tests that Entities that use field level access (as opposed to properties) - * can register read hooks on the entity class. - */ - @Test - public void testReadHookOnEntityFields() { - @Entity - @Include - class Book { - @Id - private String id; - private String title; - - @Exclude - @Transient - private int readPreSecurityInvoked = 0; - - @Exclude - @Transient - private int readPreCommitInvoked = 0; - - @Exclude - @Transient - private int readPostCommitInvoked = 0; - - @OnReadPreSecurity("title") - public void readPreSecurity(RequestScope scope) { - readPreSecurityInvoked++; - } - - @OnReadPreCommit("title") - public void readPreCommit(RequestScope scope) { - readPreCommitInvoked++; - } - - @OnReadPostCommit("title") - public void readPostCommit(RequestScope scope) { - readPostCommitInvoked++; - } - } + clearInvocations(mockModel); + scope.runQueuedPreSecurityTriggers(); - EntityDictionary dictionary = new EntityDictionary(new HashMap<>()); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - dictionary.bindEntity(Book.class); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + verify(mockModel, never()).relationCallback(any(), any(), any()); + verify(mockModel, never()).attributeCallback(any(), any(), any()); - Book book = new Book(); - RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, "1", scope); + clearInvocations(mockModel); + scope.runQueuedPreCommitTriggers(); - resource.getAttribute("title"); + verify(mockModel, never()).attributeCallback(any(), any(), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); - assertEquals(1, book.readPreSecurityInvoked); - assertEquals(0, book.readPreCommitInvoked); - scope.runQueuedPreCommitTriggers(); - assertEquals(1, book.readPreCommitInvoked); - assertEquals(0, book.readPostCommitInvoked); + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(PRECOMMIT)); + //TODO - this should be only called once. + verify(mockModel, times(2)).relationCallback(any(), any(), any()); + verify(mockModel, times(2)).relationCallback(eq(UPDATE), eq(PRECOMMIT), notNull()); + + clearInvocations(mockModel); + scope.getPermissionExecutor().executeCommitChecks(); scope.runQueuedPostCommitTriggers(); - assertEquals(1, book.readPreSecurityInvoked); - assertEquals(1, book.readPreCommitInvoked); - assertEquals(1, book.readPostCommitInvoked); + + verify(mockModel, never()).attributeCallback(any(), any(), any()); + verify(mockModel, never()).classAllFieldsCallback(any(), any()); + + verify(mockModel, times(1)).classCallback(any(), any()); + verify(mockModel, times(1)).classCallback(eq(UPDATE), eq(POSTCOMMIT)); + //TODO - this should be only called once. + verify(mockModel, times(2)).relationCallback(any(), any(), any()); + verify(mockModel, times(2)).relationCallback(eq(UPDATE), eq(POSTCOMMIT), notNull()); } - /** - * Tests that Entities that use field level access (as opposed to properties) - * can register update hooks on the entity class. - */ @Test - public void testUpdateHookOnEntityFields() { - @Entity - @Include - class Book { - @Id - private String id; - private String title; - - @Exclude - @Transient - private int updatePreSecurityInvoked = 0; - - @Exclude - @Transient - private int updatePreCommitInvoked = 0; - - @Exclude - @Transient - private int updatePostCommitInvoked = 0; - - @OnUpdatePreSecurity("title") - public void updatePreSecurity(RequestScope scope) { - updatePreSecurityInvoked++; - } - - @OnUpdatePreCommit("title") - public void updatePreCommit(RequestScope scope) { - updatePreCommitInvoked++; - } - - @OnUpdatePostCommit("title") - public void updatePostCommit(RequestScope scope) { - updatePostCommitInvoked++; - } - } - - EntityDictionary dictionary = new EntityDictionary(new HashMap<>()); + public void testAddToCollectionTrigger() { + PropertyTestModel mockModel = mock(PropertyTestModel.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - dictionary.bindEntity(Book.class); - - Book book = new Book(); + when(tx.createNewObject(PropertyTestModel.class)).thenReturn(mockModel); RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, "1", scope); - resource.updateAttribute("title", "foo"); + PropertyTestModel modelToAdd = mock(PropertyTestModel.class); + + //First we test adding to a newly created object. + PersistentResource resource = PersistentResource.createObject(PropertyTestModel.class, scope, Optional.of("1")); + PersistentResource resourceToAdd = new PersistentResource(modelToAdd, null, scope.getUUIDFor(mockModel), scope); + + resource.updateRelation("models", new HashSet<>(Arrays.asList(resourceToAdd))); - assertEquals(1, book.updatePreSecurityInvoked); - assertEquals(0, book.updatePreCommitInvoked); + scope.runQueuedPreSecurityTriggers(); scope.runQueuedPreCommitTriggers(); - assertEquals(1, book.updatePreCommitInvoked); - assertEquals(0, book.updatePostCommitInvoked); scope.runQueuedPostCommitTriggers(); - assertEquals(1, book.updatePreSecurityInvoked); - assertEquals(1, book.updatePreCommitInvoked); - assertEquals(1, book.updatePostCommitInvoked); - } - /** - * Tests that Entities that use field level access (as opposed to properties) - * can register create hooks on the entity class. - */ - @Test - public void testCreateHookOnEntityFields() { - @Entity - @Include - class Book { - @Id - private String id; - private String title; - - @Exclude - @Transient - private int createPreCommitInvoked = 0; - - @Exclude - @Transient - private int createPostCommitInvoked = 0; - - @Exclude - @Transient - private int createPreSecurityInvoked = 0; - - @OnCreatePreSecurity - public void createPreSecurity(RequestScope scope) { - createPreSecurityInvoked++; - } - - @OnCreatePreCommit("title") - public void createPreCommit(RequestScope scope) { - createPreCommitInvoked++; - } - - @OnCreatePostCommit("title") - public void createPostCommit(RequestScope scope) { - createPostCommitInvoked++; - } - } + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(CREATE), eq(POSTCOMMIT), notNull()); - EntityDictionary dictionary = new EntityDictionary(new HashMap<>()); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - dictionary.bindEntity(Book.class); + //Build another resource, scope & reset the mock to do a pure update (no create): + scope = buildRequestScope(dictionary, tx); + resource = new PersistentResource(mockModel, null, scope.getUUIDFor(mockModel), scope); + reset(mockModel); - Book book = new Book(); - when(tx.createNewObject(Book.class)).thenReturn(book); - RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource bookResource = PersistentResource.createObject(null, Book.class, scope, Optional.of("123")); - bookResource.updateAttribute("title", "Foo"); + resource.updateRelation("models", new HashSet<>(Arrays.asList(resourceToAdd))); - assertEquals(0, book.createPreSecurityInvoked); scope.runQueuedPreSecurityTriggers(); - assertEquals(1, book.createPreSecurityInvoked); - assertEquals(0, book.createPreCommitInvoked); scope.runQueuedPreCommitTriggers(); - assertEquals(1, book.createPreCommitInvoked); - assertEquals(0, book.createPostCommitInvoked); scope.runQueuedPostCommitTriggers(); - assertEquals(1, book.createPreSecurityInvoked); - assertEquals(1, book.createPreCommitInvoked); - assertEquals(1, book.createPostCommitInvoked); + + verify(mockModel, never()).relationCallback(eq(CREATE), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(UPDATE), eq(POSTCOMMIT), notNull()); } - /** - * Tests that Entities that use field level access (as opposed to properties) - * can register delete hooks on the entity class. - */ @Test - public void testDeleteHookOnEntityFields() { - @Entity - @Include - class Book { - @Id - private String id; - private String title; - - @Exclude - @Transient - private int deletePreSecurityInvoked = 0; - - @Exclude - @Transient - private int deletePreCommitInvoked = 0; - - @Exclude - @Transient - private int deletePostCommitInvoked = 0; - - @OnDeletePreSecurity - public void deletePreSecurity(RequestScope scope) { - deletePreSecurityInvoked++; - } - - @OnDeletePreCommit - public void deletePreCommit(RequestScope scope) { - deletePreCommitInvoked++; - } - - @OnDeletePostCommit - public void deletePostCommit(RequestScope scope) { - deletePostCommitInvoked++; - } - } - - EntityDictionary dictionary = new EntityDictionary(new HashMap<>()); + public void testRemoveFromCollectionTrigger() { + PropertyTestModel mockModel = mock(PropertyTestModel.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); - dictionary.bindEntity(Book.class); - - Book book = new Book(); - + when(tx.createNewObject(PropertyTestModel.class)).thenReturn(mockModel); RequestScope scope = buildRequestScope(dictionary, tx); - PersistentResource resource = new PersistentResource(book, null, "1", scope); - resource.deleteResource(); + PropertyTestModel childModel1 = mock(PropertyTestModel.class); + PropertyTestModel childModel2 = mock(PropertyTestModel.class); + when(childModel1.getId()).thenReturn("2"); + when(childModel2.getId()).thenReturn("3"); - assertEquals(1, book.deletePreSecurityInvoked); - assertEquals(0, book.deletePreCommitInvoked); - scope.runQueuedPreCommitTriggers(); - assertEquals(1, book.deletePreCommitInvoked); - assertEquals(0, book.deletePostCommitInvoked); - scope.runQueuedPostCommitTriggers(); - assertEquals(1, book.deletePreSecurityInvoked); - assertEquals(1, book.deletePreCommitInvoked); - assertEquals(1, book.deletePostCommitInvoked); - } + //First we test removing from a newly created object. + PersistentResource resource = PersistentResource.createObject(PropertyTestModel.class, scope, Optional.of("1")); + PersistentResource childResource1 = new PersistentResource(childModel1, null, "2", scope); + PersistentResource childResource2 = new PersistentResource(childModel2, null, "3", scope); - /** - * Tests that Update lifecycle hooks are triggered when a relationship collection has elements added. - */ - @Test - public void testAddToCollectionTrigger() { - HashMapDataStore wrapped = new HashMapDataStore(Book.class.getPackage()); - InMemoryDataStore store = new InMemoryDataStore(wrapped); - HashMap> checkMappings = new HashMap<>(); - checkMappings.put("Book operation check", Book.BookOperationCheck.class); - checkMappings.put("Field path editor check", Editor.FieldPathFilterExpression.class); - store.populateEntityDictionary(new EntityDictionary(checkMappings)); - DataStoreTransaction tx = store.beginTransaction(); - - RequestScope scope = buildRequestScope(wrapped.getDictionary(), tx); - PersistentResource publisherResource = PersistentResource.createObject(null, Publisher.class, scope, Optional.of("1")); - PersistentResource book1Resource = PersistentResource.createObject(publisherResource, Book.class, scope, Optional.of("1")); - publisherResource.updateRelation("books", new HashSet<>(Arrays.asList(book1Resource))); + resource.updateRelation("models", new HashSet<>(Arrays.asList(childResource1, childResource2))); + scope.runQueuedPreSecurityTriggers(); scope.runQueuedPreCommitTriggers(); - tx.save(publisherResource.getObject(), scope); - tx.save(book1Resource.getObject(), scope); - tx.commit(scope); - - Publisher publisher = (Publisher) publisherResource.getObject(); + scope.runQueuedPostCommitTriggers(); - /* Only the creat hooks should be triggered */ - assertFalse(publisher.isUpdateHookInvoked()); + verify(mockModel, never()).relationCallback(eq(UPDATE), any(), any()); + verify(mockModel, times(2)).relationCallback(eq(CREATE), eq(POSTCOMMIT), notNull()); + + //Build another resource, scope & reset the mock to do a pure update (no create): + scope = buildRequestScope(dictionary, tx); + resource = new PersistentResource(mockModel, null, scope.getUUIDFor(mockModel), scope); + reset(mockModel); + Relationship relationship = Relationship.builder() + .projection(EntityProjection.builder() + .type(PropertyTestModel.class) + .build()) + .name("models") + .build(); - scope = buildRequestScope(wrapped.getDictionary(), tx); + when(tx.getRelation(tx, mockModel, relationship, scope)).thenReturn(Arrays.asList(childModel1, childModel2)); - PersistentResource book2Resource = PersistentResource.createObject(publisherResource, Book.class, scope, Optional.of("2")); - publisherResource = PersistentResource.loadRecord(Publisher.class, "1", scope); - publisherResource.addRelation("books", book2Resource); + resource.updateRelation("models", new HashSet<>(Arrays.asList(childResource1))); + scope.runQueuedPreSecurityTriggers(); scope.runQueuedPreCommitTriggers(); + scope.runQueuedPostCommitTriggers(); - publisher = (Publisher) publisherResource.getObject(); - assertTrue(publisher.isUpdateHookInvoked()); + verify(mockModel, never()).relationCallback(eq(CREATE), any(), any()); + verify(mockModel, times(1)).relationCallback(eq(UPDATE), eq(POSTCOMMIT), notNull()); } - /** - * Tests that Update lifecycle hooks are triggered when a relationship collection has elements removed. - */ @Test - public void testRemoveFromCollectionTrigger() { - HashMapDataStore wrapped = new HashMapDataStore(Book.class.getPackage()); - InMemoryDataStore store = new InMemoryDataStore(wrapped); - HashMap> checkMappings = new HashMap<>(); - checkMappings.put("Book operation check", Book.BookOperationCheck.class); - checkMappings.put("Field path editor check", Editor.FieldPathFilterExpression.class); - store.populateEntityDictionary(new EntityDictionary(checkMappings)); - DataStoreTransaction tx = store.beginTransaction(); + public void testPreCommitLifecycleHookException() { + DataStoreTransaction tx = mock(DataStoreTransaction.class); + FieldTestModel testModel = mock(FieldTestModel.class); - RequestScope scope = buildRequestScope(wrapped.getDictionary(), tx); + doThrow(IllegalStateException.class) + .when(testModel) + .attributeCallback(eq(UPDATE), eq(PRECOMMIT), any(ChangeSpec.class)); - PersistentResource publisherResource = PersistentResource.createObject(null, Publisher.class, scope, Optional.of("1")); - PersistentResource book1Resource = PersistentResource.createObject(publisherResource, Book.class, scope, Optional.of("1")); - PersistentResource book2Resource = PersistentResource.createObject(publisherResource, Book.class, scope, Optional.of("2")); - publisherResource.updateRelation("books", new HashSet<>(Arrays.asList(book1Resource, book2Resource))); + RequestScope scope = buildRequestScope(dictionary, tx); + PersistentResource resource = new PersistentResource(testModel, null, "1", scope); + resource.updateAttribute("field", "New value"); + scope.runQueuedPreSecurityTriggers(); + assertThrows(IllegalStateException.class, () -> scope.runQueuedPreCommitTriggers()); + } - scope.runQueuedPreCommitTriggers(); - tx.save(publisherResource.getObject(), scope); - tx.save(book1Resource.getObject(), scope); - tx.commit(scope); + @Test + public void testPostCommitLifecycleHookException() { + DataStoreTransaction tx = mock(DataStoreTransaction.class); + FieldTestModel testModel = mock(FieldTestModel.class); - Publisher publisher = (Publisher) publisherResource.getObject(); + doThrow(IllegalStateException.class) + .when(testModel) + .attributeCallback(eq(UPDATE), eq(POSTCOMMIT), any(ChangeSpec.class)); - /* Only the creat hooks should be triggered */ - assertFalse(publisher.isUpdateHookInvoked()); + RequestScope scope = buildRequestScope(dictionary, tx); + PersistentResource resource = new PersistentResource(testModel, null, "1", scope); + resource.updateAttribute("field", "New value"); + scope.runQueuedPreSecurityTriggers(); + scope.runQueuedPreCommitTriggers(); + assertThrows(IllegalStateException.class, () -> scope.runQueuedPostCommitTriggers()); + } - scope = buildRequestScope(wrapped.getDictionary(), tx); + @Test + public void testPreSecurityLifecycleHookException() { + DataStoreTransaction tx = mock(DataStoreTransaction.class); + FieldTestModel testModel = mock(FieldTestModel.class); - book2Resource = PersistentResource.createObject(publisherResource, Book.class, scope, Optional.of("2")); - publisherResource = PersistentResource.loadRecord(Publisher.class, "1", scope); - publisherResource.updateRelation("books", new HashSet<>(Arrays.asList(book2Resource))); + doThrow(IllegalStateException.class) + .when(testModel) + .attributeCallback(eq(UPDATE), eq(PRESECURITY), any(ChangeSpec.class)); - scope.runQueuedPreCommitTriggers(); + RequestScope scope = buildRequestScope(dictionary, tx); + PersistentResource resource = new PersistentResource(testModel, null, "1", scope); - publisher = (Publisher) publisherResource.getObject(); - assertTrue(publisher.isUpdateHookInvoked()); + assertThrows(IllegalStateException.class, () -> resource.updateAttribute("field", "New value")); } private Elide getElide(DataStore dataStore, EntityDictionary dictionary, AuditLogger auditLogger) { @@ -1287,18 +1386,14 @@ private ElideSettings getElideSettings(DataStore dataStore, EntityDictionary dic return new ElideSettingsBuilder(dataStore) .withEntityDictionary(dictionary) .withAuditLogger(auditLogger) - .withReturnErrorObjects(true) + .withVerboseErrors() .build(); } - private void verifyNoEmptyCallbacks() { - verify(onUpdateDeferredCallback, never()).execute(any(), isA(RequestScope.class), eq(Optional.empty())); - verify(onUpdateImmediateCallback, never()).execute(any(), isA(RequestScope.class), eq(Optional.empty())); - verify(onUpdatePostCommitCallback, never()).execute(any(), isA(RequestScope.class), eq(Optional.empty())); - verify(onUpdatePostCommitAuthor, never()).execute(any(), isA(RequestScope.class), eq(Optional.empty())); - } - private RequestScope buildRequestScope(EntityDictionary dict, DataStoreTransaction tx) { - return new RequestScope(null, null, tx, new User(1), null, getElideSettings(null, dict, MOCK_AUDIT_LOGGER)); + User user = new TestUser("1"); + + return new RequestScope(null, NO_VERSION, null, tx, user, null, + getElideSettings(null, dict, MOCK_AUDIT_LOGGER)); } } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/PaginationLogicTest.java b/elide-core/src/test/java/com/yahoo/elide/core/PaginationImplTest.java similarity index 52% rename from elide-core/src/test/java/com/yahoo/elide/core/PaginationLogicTest.java rename to elide-core/src/test/java/com/yahoo/elide/core/PaginationImplTest.java index ee5790c0f4..b3f6fdfb69 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/PaginationLogicTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/PaginationImplTest.java @@ -14,18 +14,17 @@ import com.yahoo.elide.ElideSettingsBuilder; import com.yahoo.elide.annotation.Paginate; import com.yahoo.elide.core.exceptions.InvalidValueException; -import com.yahoo.elide.core.pagination.Pagination; +import com.yahoo.elide.core.pagination.PaginationImpl; import org.glassfish.jersey.internal.util.collection.MultivaluedStringMap; import org.junit.jupiter.api.Test; import java.util.Optional; - import javax.ws.rs.core.MultivaluedMap; /** * Tests parsing the page params for json-api pagination. */ -public class PaginationLogicTest { +public class PaginationImplTest { private final ElideSettings elideSettings = new ElideSettingsBuilder(null).build(); @@ -35,8 +34,8 @@ public void shouldParseQueryParamsForCurrentPageAndPageSize() { queryParams.add("page[size]", "10"); queryParams.add("page[number]", "2"); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - pageData = pageData.evaluate(PaginationLogicTest.class); + PaginationImpl pageData = PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings); // page based strategy uses human readable paging - non-zero index // page 2 becomes (1)*10 so 10 since we shift to zero based index assertEquals(10, pageData.getOffset()); @@ -49,8 +48,8 @@ public void shouldThrowExceptionForNegativePageNumber() { queryParams.add("page[size]", "10"); queryParams.add("page[number]", "-2"); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - assertThrows(InvalidValueException.class, () -> pageData.evaluate(PaginationLogicTest.class)); + assertThrows(InvalidValueException.class, () -> PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings)); } @Test @@ -58,8 +57,9 @@ public void shouldThrowExceptionForNegativePageSize() { MultivaluedMap queryParams = new MultivaluedStringMap(); queryParams.add("page[size]", "-10"); queryParams.add("page[number]", "2"); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - assertThrows(InvalidValueException.class, () -> pageData.evaluate(PaginationLogicTest.class)); + + assertThrows(InvalidValueException.class, () -> PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings)); } @Test @@ -68,8 +68,8 @@ public void shouldParseQueryParamsForOffsetAndLimit() { queryParams.add("page[limit]", "10"); queryParams.add("page[offset]", "2"); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - pageData = pageData.evaluate(PaginationLogicTest.class); + PaginationImpl pageData = PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings); // offset is direct correlation to start field in query assertEquals(2, pageData.getOffset()); assertEquals(10, pageData.getLimit()); @@ -78,76 +78,72 @@ public void shouldParseQueryParamsForOffsetAndLimit() { @Test public void shouldUseDefaultsWhenMissingCurrentPageAndPageSize() { MultivaluedMap queryParams = new MultivaluedStringMap(); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - assertEquals(Pagination.DEFAULT_OFFSET, pageData.getOffset()); - assertEquals(Pagination.DEFAULT_PAGE_LIMIT, pageData.getLimit()); + PaginationImpl pageData = PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings); + assertEquals(PaginationImpl.DEFAULT_OFFSET, pageData.getOffset()); + assertEquals(PaginationImpl.DEFAULT_PAGE_LIMIT, pageData.getLimit()); } @Test public void checkValidOffsetAndFirstRequest() { - Pagination pageData = Pagination.fromOffsetAndFirst(Optional.of("10"), Optional.of("1"), true, elideSettings).get(); - - // NOTE: This is always set to default until evaluate. Then the appropriate value should be used. - // This is because the particular root entity determines the pagination limits - assertEquals(0, pageData.getOffset()); - assertEquals(500, pageData.getLimit()); - - assertEquals(1, pageData.evaluate(PaginationLogicTest.class).getOffset()); - assertEquals(10, pageData.evaluate(PaginationLogicTest.class).getLimit()); - assertTrue(pageData.evaluate(PaginationLogicTest.class).isGenerateTotals()); + PaginationImpl pageData = new PaginationImpl(PaginationImplTest.class, + 1, + 10, + elideSettings.getDefaultPageSize(), + elideSettings.getDefaultMaxPageSize(), + false, + false); + + assertEquals(1, pageData.getOffset()); + assertEquals(10, pageData.getLimit()); } @Test public void checkErroneousPageLimit() { - Pagination pageData = - Pagination.fromOffsetAndFirst(Optional.of("100000"), Optional.of("1"), false, elideSettings).get(); - - // NOTE: This is always set to default until evaluate. Then the appropriate value should be used. - // This is because the particular root entity determines the pagination limits - assertEquals(0, pageData.getOffset()); - assertEquals(500, pageData.getLimit()); - assertThrows( - InvalidValueException.class, - () -> pageData.evaluate(PaginationLogicTest.class).getOffset()); assertThrows( InvalidValueException.class, - () -> pageData.evaluate(PaginationLogicTest.class).getLimit()); + () -> new PaginationImpl(PaginationImplTest.class, + 10, + 100000, + elideSettings.getDefaultPageSize(), + elideSettings.getDefaultMaxPageSize(), + false, + false)); } @Test public void checkBadOffset() { assertThrows( InvalidValueException.class, - () -> Pagination.fromOffsetAndFirst(Optional.of("-1"), Optional.of("1000"), false, elideSettings)); - } - - @Test - public void checkBadOffsetString() { - assertThrows( - InvalidValueException.class, - () -> Pagination.fromOffsetAndFirst(Optional.of("NaN"), Optional.of("1000"), false, elideSettings)); + () -> new PaginationImpl(PaginationImplTest.class, + -1, + 1000, + elideSettings.getDefaultPageSize(), + elideSettings.getDefaultMaxPageSize(), + false, + false)); } @Test public void checkBadLimit() { assertThrows( InvalidValueException.class, - () -> Pagination.fromOffsetAndFirst(Optional.of("0"), Optional.of("1"), false, elideSettings)); - } - - @Test - public void checkBadLimitString() { - assertThrows( - InvalidValueException.class, - () -> Pagination.fromOffsetAndFirst(Optional.of("1"), Optional.of("NaN"), false, elideSettings)); + () -> new PaginationImpl(PaginationImplTest.class, + 0, + -1, + elideSettings.getDefaultPageSize(), + elideSettings.getDefaultMaxPageSize(), + false, + false)); } @Test public void neverExceedMaxPageSize() { MultivaluedMap queryParams = new MultivaluedStringMap(); queryParams.add("page[size]", "25000"); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - assertThrows(InvalidValueException.class, () -> pageData.evaluate(PaginationLogicTest.class)); + assertThrows(InvalidValueException.class, + () -> PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings)); } @Test @@ -155,48 +151,54 @@ public void invalidUsageOfPaginationParameters() { MultivaluedMap queryParams = new MultivaluedStringMap(); queryParams.add("page[size]", "10"); queryParams.add("page[offset]", "100"); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - assertThrows(InvalidValueException.class, () -> pageData.evaluate(PaginationLogicTest.class)); + assertThrows(InvalidValueException.class, + () -> PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings)); } @Test public void pageBasedPaginationWithDefaultSize() { MultivaluedMap queryParams = new MultivaluedStringMap(); queryParams.add("page[number]", "2"); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - pageData = pageData.evaluate(PaginationLogicTest.class); - assertEquals(Pagination.DEFAULT_PAGE_LIMIT, pageData.getLimit()); - assertEquals(Pagination.DEFAULT_PAGE_LIMIT, pageData.getOffset()); + PaginationImpl pageData = PaginationImpl.parseQueryParams(PaginationImpl.class, + Optional.of(queryParams), elideSettings); + assertEquals(PaginationImpl.DEFAULT_PAGE_LIMIT, pageData.getLimit()); + assertEquals(PaginationImpl.DEFAULT_PAGE_LIMIT, pageData.getOffset()); } @Test public void shouldThrowExceptionForNonIntPageParamValues() { MultivaluedMap queryParams = new MultivaluedStringMap(); queryParams.add("page[size]", "2.5"); - assertThrows(InvalidValueException.class, () -> Pagination.parseQueryParams(queryParams, elideSettings)); + assertThrows(InvalidValueException.class, + () -> PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings)); } @Test public void shouldThrowExceptionForInvalidPageParams() { MultivaluedMap queryParams = new MultivaluedStringMap(); queryParams.add("page[random]", "1"); - assertThrows(InvalidValueException.class, () -> Pagination.parseQueryParams(queryParams, elideSettings)); + assertThrows(InvalidValueException.class, + () -> PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings)); } @Test public void shouldSetGenerateTotals() { MultivaluedMap queryParams = new MultivaluedStringMap(); queryParams.add("page[totals]", null); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - pageData = pageData.evaluate(PaginationLogicTest.class); - assertTrue(pageData.isGenerateTotals()); + PaginationImpl pageData = PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings); + assertTrue(pageData.returnPageTotals()); } @Test public void shouldNotSetGenerateTotals() { MultivaluedMap queryParams = new MultivaluedStringMap(); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); - assertFalse(pageData.isGenerateTotals()); + PaginationImpl pageData = PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings); + assertFalse(pageData.returnPageTotals()); } @@ -204,12 +206,13 @@ public void shouldNotSetGenerateTotals() { public void shouldUseDefaultsWhenNoParams() { MultivaluedMap queryParams = new MultivaluedStringMap(); - Pagination pageData = Pagination.parseQueryParams(queryParams, elideSettings); + PaginationImpl pageData = PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), elideSettings); assertEquals(0, pageData.getOffset()); - assertEquals(Pagination.DEFAULT_PAGE_LIMIT, pageData.getLimit()); + assertEquals(PaginationImpl.DEFAULT_PAGE_LIMIT, pageData.getLimit()); - pageData = Pagination.parseQueryParams(queryParams, - new ElideSettingsBuilder(null) + pageData = PaginationImpl.parseQueryParams(PaginationImplTest.class, + Optional.of(queryParams), new ElideSettingsBuilder(null) .withDefaultPageSize(10) .withDefaultMaxPageSize(10) .build()); @@ -223,16 +226,14 @@ public void testClassLevelOverride() { class PaginationOverrideTest { } MultivaluedMap queryParams = new MultivaluedStringMap(); - Pagination pageData = Pagination.parseQueryParams(queryParams, + PaginationImpl pageData = PaginationImpl.parseQueryParams(PaginationOverrideTest.class, + Optional.of(queryParams), new ElideSettingsBuilder(null) - .withDefaultPageSize(0) - .withDefaultMaxPageSize(0) + .withDefaultPageSize(1) + .withDefaultMaxPageSize(1) .build()); - assertEquals(0, pageData.getOffset()); - assertEquals(0, pageData.getLimit()); - Pagination result = pageData.evaluate(PaginationOverrideTest.class); assertEquals(0, pageData.getOffset()); - assertEquals(10, result.getLimit()); + assertEquals(10, pageData.getLimit()); } } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/PermissionAnnotationTest.java b/elide-core/src/test/java/com/yahoo/elide/core/PermissionAnnotationTest.java index 080eebef53..bba3efc9c3 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/PermissionAnnotationTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/PermissionAnnotationTest.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.core; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; import static org.junit.jupiter.api.Assertions.assertThrows; import com.yahoo.elide.ElideSettings; @@ -17,11 +18,11 @@ import com.yahoo.elide.audit.TestAuditLogger; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; import com.yahoo.elide.security.PermissionExecutor; +import com.yahoo.elide.security.TestUser; import com.yahoo.elide.security.User; import com.yahoo.elide.security.executors.ActivePermissionExecutor; import example.FunWithPermissions; -import example.TestCheckMappings; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -29,13 +30,13 @@ * Tests audit functions inside RecordDao. */ public class PermissionAnnotationTest { - private static final User GOOD_USER = new User(3); - private static final User BAD_USER = new User(-1); + private static final User GOOD_USER = new TestUser("3"); + private static final User BAD_USER = new TestUser("-1"); private static PersistentResource funRecord; private static PersistentResource badRecord; - private static EntityDictionary dictionary = new EntityDictionary(TestCheckMappings.MAPPINGS); + private static EntityDictionary dictionary = TestDictionary.getTestDictionary(); public PermissionAnnotationTest() { } @@ -55,9 +56,9 @@ public static void setup() { .withEntityDictionary(dictionary) .build(); - RequestScope goodScope = new RequestScope(null, null, null, GOOD_USER, null, elideSettings); + RequestScope goodScope = new RequestScope(null, NO_VERSION, null, null, GOOD_USER, null, elideSettings); funRecord = new PersistentResource<>(fun, null, goodScope.getUUIDFor(fun), goodScope); - RequestScope badScope = new RequestScope(null, null, null, BAD_USER, null, elideSettings); + RequestScope badScope = new RequestScope(null, NO_VERSION, null, null, BAD_USER, null, elideSettings); badRecord = new PersistentResource<>(fun, null, badScope.getUUIDFor(fun), badScope); } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/PersistenceResourceTestSetup.java b/elide-core/src/test/java/com/yahoo/elide/core/PersistenceResourceTestSetup.java index 2692721f95..2a2cd69feb 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/PersistenceResourceTestSetup.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/PersistenceResourceTestSetup.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.core; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; import static org.mockito.Mockito.mock; import com.yahoo.elide.ElideSettings; @@ -13,12 +14,12 @@ import com.yahoo.elide.annotation.DeletePermission; import com.yahoo.elide.annotation.Include; import com.yahoo.elide.annotation.ReadPermission; -import com.yahoo.elide.annotation.SharePermission; import com.yahoo.elide.annotation.UpdatePermission; import com.yahoo.elide.audit.AuditLogger; -import com.yahoo.elide.core.filter.expression.FilterExpression; import com.yahoo.elide.jsonapi.models.JsonApiDocument; +import com.yahoo.elide.request.EntityProjection; import com.yahoo.elide.security.ChangeSpec; +import com.yahoo.elide.security.TestUser; import com.yahoo.elide.security.User; import com.yahoo.elide.security.checks.OperationCheck; @@ -42,14 +43,14 @@ import example.Parent; import example.Publisher; import example.Right; -import example.TestCheckMappings; import example.UpdateAndCreate; -import example.packageshareable.ContainerWithPackageShare; -import example.packageshareable.ShareableWithPackageShare; -import example.packageshareable.UnshareableWithEntityUnshare; +import example.nontransferable.ContainerWithPackageShare; +import example.nontransferable.ShareableWithPackageShare; +import example.nontransferable.Untransferable; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; + import nocreate.NoCreateEntity; import java.util.Collection; @@ -71,7 +72,8 @@ public class PersistenceResourceTestSetup extends PersistentResource { protected final ElideSettings elideSettings; protected static EntityDictionary initDictionary() { - EntityDictionary dictionary = new EntityDictionary(TestCheckMappings.MAPPINGS); + EntityDictionary dictionary = TestDictionary.getTestDictionary(); + dictionary.bindEntity(UpdateAndCreate.class); dictionary.bindEntity(Author.class); dictionary.bindEntity(Book.class); @@ -97,7 +99,7 @@ protected static EntityDictionary initDictionary() { dictionary.bindEntity(ComputedBean.class); dictionary.bindEntity(ContainerWithPackageShare.class); dictionary.bindEntity(ShareableWithPackageShare.class); - dictionary.bindEntity(UnshareableWithEntityUnshare.class); + dictionary.bindEntity(Untransferable.class); return dictionary; } @@ -115,7 +117,7 @@ public PersistenceResourceTestSetup() { new Child(), null, null, // new request scope + new Child == cannot possibly be a UUID for this object - new RequestScope(null, null, null, null, null, + new RequestScope(null, NO_VERSION, null, null, null, null, initSettings() ) ); @@ -142,7 +144,7 @@ protected RequestScope buildRequestScope(DataStoreTransaction tx, User user) { } protected RequestScope buildRequestScope(String path, DataStoreTransaction tx, User user, MultivaluedMap queryParams) { - return new RequestScope(path, null, tx, user, queryParams, elideSettings); + return new RequestScope(path, NO_VERSION, null, tx, user, queryParams, elideSettings); } protected PersistentResource bootstrapPersistentResource(T obj) { @@ -150,13 +152,13 @@ protected PersistentResource bootstrapPersistentResource(T obj) { } protected PersistentResource bootstrapPersistentResource(T obj, DataStoreTransaction tx) { - User goodUser = new User(1); - RequestScope requestScope = new RequestScope(null, null, tx, goodUser, null, elideSettings); + User goodUser = new TestUser("1"); + RequestScope requestScope = new RequestScope(null, NO_VERSION, null, tx, goodUser, null, elideSettings); return new PersistentResource<>(obj, null, requestScope.getUUIDFor(obj), requestScope); } protected RequestScope getUserScope(User user, AuditLogger auditLogger) { - return new RequestScope(null, new JsonApiDocument(), null, user, null, + return new RequestScope(null, NO_VERSION, new JsonApiDocument(), null, user, null, new ElideSettingsBuilder(null) .withEntityDictionary(dictionary) .withAuditLogger(auditLogger) @@ -222,7 +224,6 @@ public ChangeSpecModel(final Function checkFunction) { @ReadPermission(expression = "allow all") @UpdatePermission(expression = "allow all") @DeletePermission(expression = "allow all") - @SharePermission public static final class ChangeSpecChild { @Id public long id; @@ -254,10 +255,17 @@ public boolean ok(Object object, com.yahoo.elide.security.RequestScope requestSc } } - public static Set getRelation(PersistentResource resource, String relation) { - Optional filterExpression = - resource.getRequestScope().getExpressionForRelation(resource, relation); + public Set getRelation(PersistentResource resource, String relation) { + return resource.getRelationCheckedFiltered(getRelationship(resource.getResourceClass(), relation)); + } - return resource.getRelationCheckedFiltered(relation, filterExpression, Optional.empty(), Optional.empty()); + public com.yahoo.elide.request.Relationship getRelationship(Class type, String name) { + return com.yahoo.elide.request.Relationship.builder() + .name(name) + .alias(name) + .projection(EntityProjection.builder() + .type(type) + .build()) + .build(); } } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceNoopUpdateTest.java b/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceNoopUpdateTest.java index b744857677..e998756626 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceNoopUpdateTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceNoopUpdateTest.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.core; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -13,6 +14,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import com.yahoo.elide.security.TestUser; import com.yahoo.elide.security.User; import example.Child; import example.FunWithPermissions; @@ -23,9 +25,11 @@ public class PersistentResourceNoopUpdateTest extends PersistenceResourceTestSetup { private final RequestScope goodUserScope; + private final User goodUser; PersistentResourceNoopUpdateTest() { - goodUserScope = new RequestScope(null, null, mock(DataStoreTransaction.class), - new User(1), null, elideSettings); + goodUser = new TestUser("1"); + goodUserScope = new RequestScope(null, NO_VERSION, null, + mock(DataStoreTransaction.class), goodUser, null, elideSettings); initDictionary(); reset(goodUserScope.getTransaction()); } @@ -35,11 +39,9 @@ public void testNOOPToOneAddRelation() { Child child = newChild(1); fun.setRelation3(child); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings); + RequestScope goodScope = new RequestScope(null, NO_VERSION, null, tx, goodUser, null, elideSettings); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); //We do not want the update to one method to be called when we add the existing entity to the relation @@ -53,11 +55,9 @@ public void testToOneAddRelation() { FunWithPermissions fun = new FunWithPermissions(); Child child = newChild(1); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings); + RequestScope goodScope = new RequestScope(null, NO_VERSION, null, tx, goodUser, null, elideSettings); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); funResource.addRelation("relation3", childResource); @@ -73,11 +73,9 @@ public void testNOOPToManyAddRelation() { children.add(child); fun.setRelation1(children); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings); + RequestScope goodScope = new RequestScope(null, NO_VERSION, null, tx, goodUser, null, elideSettings); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); PersistentResource childResource = new PersistentResource<>(child, null, null, goodScope); //We do not want the update to one method to be called when we add the existing entity to the relation @@ -90,11 +88,9 @@ public void testToManyAddRelation() { FunWithPermissions fun = new FunWithPermissions(); Child child = newChild(1); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings); + RequestScope goodScope = new RequestScope(null, NO_VERSION, null, tx, goodUser, null, elideSettings); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); PersistentResource childResource = new PersistentResource<>(child, null, null, goodScope); funResource.addRelation("relation1", childResource); diff --git a/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceTest.java b/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceTest.java index 06744d79a7..5937985c13 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceTest.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.core; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -22,6 +23,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; + import com.yahoo.elide.annotation.Audit; import com.yahoo.elide.annotation.ReadPermission; import com.yahoo.elide.audit.LogMessage; @@ -38,7 +40,10 @@ import com.yahoo.elide.jsonapi.models.Relationship; import com.yahoo.elide.jsonapi.models.Resource; import com.yahoo.elide.jsonapi.models.ResourceIdentifier; +import com.yahoo.elide.request.Attribute; +import com.yahoo.elide.request.EntityProjection; import com.yahoo.elide.security.ChangeSpec; +import com.yahoo.elide.security.TestUser; import com.yahoo.elide.security.User; import com.google.common.collect.ImmutableMap; @@ -64,14 +69,16 @@ import example.Parent; import example.Right; import example.Shape; -import example.packageshareable.ContainerWithPackageShare; -import example.packageshareable.ShareableWithPackageShare; -import example.packageshareable.UnshareableWithEntityUnshare; +import example.nontransferable.ContainerWithPackageShare; +import example.nontransferable.ShareableWithPackageShare; +import example.nontransferable.Untransferable; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.IterableUtils; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Answers; +import org.junit.jupiter.api.TestInstance; +import org.mockito.ArgumentCaptor; import nocreate.NoCreateEntity; @@ -92,19 +99,20 @@ import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; - /** * Test PersistentResource. */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class PersistentResourceTest extends PersistenceResourceTestSetup { - private final RequestScope goodUserScope; - private final RequestScope badUserScope; + private final User goodUser = new TestUser("1"); + private final User badUser = new TestUser("-1"); + + private DataStoreTransaction tx = mock(DataStoreTransaction.class); - public PersistentResourceTest() { - goodUserScope = buildRequestScope(mock(DataStoreTransaction.class), new User(1)); - badUserScope = buildRequestScope(mock(DataStoreTransaction.class), new User(-1)); - reset(goodUserScope.getTransaction()); + @BeforeEach + public void beforeTest() { + reset(tx); } @Test @@ -112,10 +120,6 @@ public void testUpdateToOneRelationHookInAddRelation() { FunWithPermissions fun = new FunWithPermissions(); Child child = newChild(1); - User goodUser = new User(1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); - RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); @@ -131,9 +135,6 @@ public void testUpdateToOneRelationHookInUpdateRelation() { Child child1 = newChild(1); Child child2 = newChild(2); fun.setRelation1(Sets.newHashSet(child1)); - User goodUser = new User(1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); @@ -150,9 +151,6 @@ public void testUpdateToOneRelationHookInRemoveRelation() { FunWithPermissions fun = new FunWithPermissions(); Child child = newChild(1); fun.setRelation3(child); - User goodUser = new User(1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); @@ -168,10 +166,8 @@ public void testUpdateToOneRelationHookInClearRelation() { FunWithPermissions fun = new FunWithPermissions(); Child child1 = newChild(1); fun.setRelation3(child1); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(fun), eq("relation3"), any(), any(), any(), any())).thenReturn(child1); + when(tx.getRelation(any(), eq(fun), any(), any())).thenReturn(child1); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); @@ -186,10 +182,6 @@ public void testUpdateToManyRelationHookInAddRelationBidirection() { Parent parent = new Parent(); Child child = newChild(1); - User goodUser = new User(1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); - RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource parentResource = new PersistentResource<>(parent, null, "3", goodScope); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); @@ -206,9 +198,6 @@ public void testUpdateToManyRelationHookInRemoveRelationBidirection() { Child child = newChild(1); parent.setChildren(Sets.newHashSet(child)); child.setParents(Sets.newHashSet(parent)); - User goodUser = new User(1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource parentResource = new PersistentResource<>(parent, null, "3", goodScope); @@ -230,10 +219,8 @@ public void testUpdateToManyRelationHookInClearRelationBidirection() { parent.setChildren(children); child1.setParents(Sets.newHashSet(parent)); child2.setParents(Sets.newHashSet(parent)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(children); + when(tx.getRelation(any(), eq(parent), any(), any())).thenReturn(children); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource parentResource = new PersistentResource<>(parent, null, "3", goodScope); @@ -257,10 +244,8 @@ public void testUpdateToManyRelationHookInUpdateRelationBidirection() { parent.setChildren(children); child1.setParents(Sets.newHashSet(parent)); child2.setParents(Sets.newHashSet(parent)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(children); + when(tx.getRelation(any(), eq(parent), any(), any())).thenReturn(children); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource parentResource = new PersistentResource<>(parent, null, "3", goodScope); @@ -281,14 +266,16 @@ public void testUpdateToManyRelationHookInUpdateRelationBidirection() { @Test public void testSetAttributeHookInUpdateAttribute() { Parent parent = newParent(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); + ArgumentCaptor attributeArgument = ArgumentCaptor.forClass(Attribute.class); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); parentResource.updateAttribute("firstName", "foobar"); - verify(tx, times(1)).setAttribute(parent, "firstName", "foobar", goodScope); + verify(tx, times(1)).setAttribute(eq(parent), attributeArgument.capture(), eq(goodScope)); + + assertEquals(attributeArgument.getValue().getName(), "firstName"); + assertEquals(attributeArgument.getValue().getArguments().iterator().next().getValue(), "foobar"); } @Test @@ -298,7 +285,9 @@ public void testGetRelationships() { fun.setRelation2(Sets.newHashSet()); fun.setRelation3(null); - PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource funResource = new PersistentResource<>(fun, null, "3", scope); Map relationships = funResource.getRelationships(); @@ -309,7 +298,9 @@ public void testGetRelationships() { assertTrue(relationships.containsKey("relation4"), "relation4 should be present"); assertTrue(relationships.containsKey("relation5"), "relation5 should be present"); - PersistentResource funResourceWithBadScope = new PersistentResource<>(fun, null, "3", badUserScope); + scope = new TestRequestScope(tx, badUser, dictionary); + + PersistentResource funResourceWithBadScope = new PersistentResource<>(fun, null, "3", scope); relationships = funResourceWithBadScope.getRelationships(); assertEquals(0, relationships.size(), "All relationships should be filtered out"); @@ -319,8 +310,6 @@ public void testGetRelationships() { public void testNoCreate() { assertNotNull(dictionary); NoCreateEntity noCreate = new NoCreateEntity(); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); when(tx.createNewObject(NoCreateEntity.class)).thenReturn(noCreate); @@ -328,7 +317,7 @@ public void testNoCreate() { assertThrows( ForbiddenAccessException.class, () -> PersistentResource.createObject( - null, NoCreateEntity.class, goodScope, Optional.of("1"))); // should throw here + NoCreateEntity.class, goodScope, Optional.of("1"))); // should throw here } @Test @@ -339,7 +328,8 @@ public void testGetAttributes() { fun.setField2(null); fun.setField4("bar"); - PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + PersistentResource funResource = new PersistentResource<>(fun, null, "3", scope); Map attributes = funResource.getAttributes(); @@ -357,6 +347,7 @@ public void testGetAttributes() { assertEquals(attributes.get("field3"), "Foobar", "field3 should be set to original value."); assertEquals(attributes.get("field4"), "bar", "field4 should be set to original value."); + RequestScope badUserScope = new TestRequestScope(tx, badUser, dictionary); PersistentResource funResourceBad = new PersistentResource<>(fun, null, "3", badUserScope); attributes = funResourceBad.getAttributes(); @@ -377,10 +368,11 @@ public void testFilter() { Child child4 = newChild(-4); { - PersistentResource child1Resource = new PersistentResource<>(child1, null, "1", goodUserScope); - PersistentResource child2Resource = new PersistentResource<>(child2, null, "-2", goodUserScope); - PersistentResource child3Resource = new PersistentResource<>(child3, null, "3", goodUserScope); - PersistentResource child4Resource = new PersistentResource<>(child4, null, "-4", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + PersistentResource child1Resource = new PersistentResource<>(child1, null, "1", scope); + PersistentResource child2Resource = new PersistentResource<>(child2, null, "-2", scope); + PersistentResource child3Resource = new PersistentResource<>(child3, null, "3", scope); + PersistentResource child4Resource = new PersistentResource<>(child4, null, "-4", scope); Set resources = Sets.newHashSet(child1Resource, child2Resource, child3Resource, child4Resource); @@ -392,10 +384,11 @@ public void testFilter() { } { - PersistentResource child1Resource = new PersistentResource<>(child1, null, "1", badUserScope); - PersistentResource child2Resource = new PersistentResource<>(child2, null, "-2", badUserScope); - PersistentResource child3Resource = new PersistentResource<>(child3, null, "3", badUserScope); - PersistentResource child4Resource = new PersistentResource<>(child4, null, "-4", badUserScope); + RequestScope scope = new TestRequestScope(tx, badUser, dictionary); + PersistentResource child1Resource = new PersistentResource<>(child1, null, "1", scope); + PersistentResource child2Resource = new PersistentResource<>(child2, null, "-2", scope); + PersistentResource child3Resource = new PersistentResource<>(child3, null, "3", scope); + PersistentResource child4Resource = new PersistentResource<>(child4, null, "-4", scope); Set resources = Sets.newHashSet(child1Resource, child2Resource, child3Resource, child4Resource); @@ -512,7 +505,8 @@ public void testDeleteBidirectionalRelation() { left.setOne2one(right); right.setOne2one(left); - PersistentResource leftResource = new PersistentResource<>(left, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + PersistentResource leftResource = new PersistentResource<>(left, null, "3", scope); leftResource.deleteInverseRelation("one2one", right); @@ -525,7 +519,8 @@ public void testDeleteBidirectionalRelation() { parent.setChildren(Sets.newHashSet(child)); parent.setSpouses(Sets.newHashSet()); - PersistentResource childResource = new PersistentResource<>(child, null, "4", goodUserScope); + scope = new TestRequestScope(tx, goodUser, dictionary); + PersistentResource childResource = new PersistentResource<>(child, null, "4", scope); childResource.deleteInverseRelation("parents", parent); @@ -538,7 +533,8 @@ public void testAddBidirectionalRelation() { Left left = new Left(); Right right = new Right(); - PersistentResource leftResource = new PersistentResource<>(left, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + PersistentResource leftResource = new PersistentResource<>(left, null, "3", scope); leftResource.addInverseRelation("one2one", right); @@ -550,7 +546,8 @@ public void testAddBidirectionalRelation() { parent.setChildren(Sets.newHashSet()); parent.setSpouses(Sets.newHashSet()); - PersistentResource childResource = new PersistentResource<>(child, null, "4", goodUserScope); + scope = new TestRequestScope(tx, goodUser, dictionary); + PersistentResource childResource = new PersistentResource<>(child, null, "4", scope); childResource.addInverseRelation("parents", parent); @@ -560,8 +557,6 @@ public void testAddBidirectionalRelation() { @Test public void testSuccessfulOneToOneRelationshipAdd() throws Exception { - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); Left left = new Left(); Right right = new Right(); left.setId(2); @@ -573,13 +568,13 @@ public void testSuccessfulOneToOneRelationshipAdd() throws Exception { Relationship ids = new Relationship(null, new Data<>(new ResourceIdentifier("right", "3").castToResource())); - when(tx.loadObject(eq(Right.class), eq(3L), any(), any())).thenReturn(right); + when(tx.loadObject(any(), eq(3L), any())).thenReturn(right); boolean updated = leftResource.updateRelation("one2one", ids.toPersistentResources(goodScope)); goodScope.saveOrCreateObjects(); verify(tx, times(1)).save(left, goodScope); verify(tx, times(1)).save(right, goodScope); - verify(tx, times(1)).getRelation(tx, left, "one2one", Optional.empty(), Optional.empty(), Optional.empty(), - goodScope); + verify(tx, times(1)).getRelation(tx, left, getRelationship(Right.class, "one2one"), goodScope); + assertTrue(updated, "The one-2-one relationship should be added."); assertEquals(3, left.getOne2one().getId(), "The correct object was set in the one-2-one relationship"); } @@ -601,8 +596,6 @@ public void testSuccessfulOneToOneRelationshipAdd() throws Exception { */ @Test public void testSuccessfulOneToOneRelationshipAddNull() throws Exception { - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); Left left = new Left(); left.setId(2); @@ -616,7 +609,7 @@ public void testSuccessfulOneToOneRelationshipAddNull() throws Exception { InvalidObjectIdentifierException.class, () -> leftResource.updateRelation("one2one", ids.toPersistentResources(goodScope))); - assertEquals("Unknown identifier 'null' for right", thrown.getMessage()); + assertEquals("Unknown identifier null for right", thrown.getMessage()); } @Test @@ -636,9 +629,6 @@ public void testSuccessfulOneToOneRelationshipAddNull() throws Exception { * final = (notMine) UNION requested */ public void testSuccessfulManyToManyRelationshipUpdate() throws Exception { - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - Parent parent = new Parent(); RequestScope goodScope = buildRequestScope(tx, goodUser); @@ -660,7 +650,7 @@ public void testSuccessfulManyToManyRelationshipUpdate() throws Exception { parent.setChildren(allChildren); parent.setSpouses(Sets.newHashSet()); - when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(allChildren); + when(tx.getRelation(any(), eq(parent), any(), any())).thenReturn(allChildren); PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); @@ -670,12 +660,11 @@ public void testSuccessfulManyToManyRelationshipUpdate() throws Exception { idList.add(new ResourceIdentifier("child", "6").castToResource()); Relationship ids = new Relationship(null, new Data<>(idList)); - - when(tx.loadObject(eq(Child.class), eq(2L), any(), any())).thenReturn(child2); - when(tx.loadObject(eq(Child.class), eq(3L), any(), any())).thenReturn(child3); - when(tx.loadObject(eq(Child.class), eq(-4L), any(), any())).thenReturn(child4); - when(tx.loadObject(eq(Child.class), eq(-5L), any(), any())).thenReturn(child5); - when(tx.loadObject(eq(Child.class), eq(6L), any(), any())).thenReturn(child6); + when(tx.loadObject(any(), eq(2L), any())).thenReturn(child2); + when(tx.loadObject(any(), eq(3L), any())).thenReturn(child3); + when(tx.loadObject(any(), eq(-4L), any())).thenReturn(child4); + when(tx.loadObject(any(), eq(-5L), any())).thenReturn(child5); + when(tx.loadObject(any(), eq(6L), any())).thenReturn(child6); //Final set after operation = (3,4,5,6) Set expected = new HashSet<>(); @@ -711,11 +700,12 @@ public void testSuccessfulManyToManyRelationshipUpdate() throws Exception { */ @Test public void testRelationshipMissingData() throws Exception { - User goodUser = new User(1); + User goodUser = new TestUser("1"); + @SuppressWarnings("resource") DataStoreTransaction tx = mock(DataStoreTransaction.class); - RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings); + RequestScope goodScope = new RequestScope(null, NO_VERSION, null, tx, goodUser, null, elideSettings); // null resource in toMany relationship is not valid List idList = new ArrayList<>(); @@ -743,7 +733,9 @@ public void testGetAttributeSuccess() { fun.setField2("blah"); fun.setField3(null); - PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource funResource = new PersistentResource<>(fun, null, "1", scope); String result = (String) funResource.getAttribute("field2"); assertEquals("blah", result, "The correct attribute should be returned."); @@ -755,7 +747,9 @@ public void testGetAttributeSuccess() { public void testGetAttributeInvalidField() { FunWithPermissions fun = new FunWithPermissions(); - PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource funResource = new PersistentResource<>(fun, null, "1", scope); assertThrows(InvalidAttributeException.class, () -> funResource.getAttribute("invalid")); } @@ -765,7 +759,9 @@ public void testGetAttributeInvalidFieldPermissions() { FunWithPermissions fun = new FunWithPermissions(); fun.setField1("foo"); - PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource funResource = new PersistentResource<>(fun, null, "1", scope); assertThrows(ForbiddenAccessException.class, () -> funResource.getAttribute("field1")); } @@ -774,7 +770,9 @@ public void testGetAttributeInvalidFieldPermissions() { public void testGetAttributeInvalidEntityPermissions() { NoReadEntity noread = new NoReadEntity(); - PersistentResource noreadResource = new PersistentResource<>(noread, null, "1", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource noreadResource = new PersistentResource<>(noread, null, "1", scope); assertThrows(ForbiddenAccessException.class, () -> noreadResource.getAttribute("field")); } @@ -788,9 +786,10 @@ public void testGetRelationSuccess() { Set children = Sets.newHashSet(child1, child2, child3); fun.setRelation2(children); - PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + PersistentResource funResource = new PersistentResource<>(fun, null, "3", scope); - when(goodUserScope.getTransaction().getRelation(any(), eq(fun), eq("relation2"), any(), any(), any(), any())).thenReturn(children); + when(scope.getTransaction().getRelation(any(), eq(fun), any(), any())).thenReturn(children); Set results = getRelation(funResource, "relation2"); @@ -807,9 +806,10 @@ public void testGetRelationFilteredSuccess() { Set children = Sets.newHashSet(child1, child2, child3); fun.setRelation2(Sets.newHashSet(child1, child2, child3)); - PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + PersistentResource funResource = new PersistentResource<>(fun, null, "3", scope); - when(goodUserScope.getTransaction().getRelation(any(), eq(fun), eq("relation2"), any(), any(), any(), any())).thenReturn(children); + when(scope.getTransaction().getRelation(any(), eq(fun), any(), any())).thenReturn(children); Set results = getRelation(funResource, "relation2"); @@ -824,9 +824,7 @@ public void testGetRelationWithPredicateSuccess() { Child child3 = newChild(3, "chris smith"); parent.setChildren(Sets.newHashSet(child1, child2, child3)); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(eq(tx), any(), any(), any(), any(), any(), any())).thenReturn(Sets.newHashSet(child1)); - User goodUser = new User(1); + when(tx.getRelation(eq(tx), any(), any(), any())).thenReturn(Sets.newHashSet(child1)); MultivaluedMap queryParams = new MultivaluedHashMap<>(); queryParams.add("filter[child.name]", "paul john"); @@ -850,11 +848,12 @@ public void testGetSingleRelationInMemory() { Set children = Sets.newHashSet(child1, child2, child3); parent.setChildren(children); - when(goodUserScope.getTransaction().getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(children); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + when(scope.getTransaction().getRelation(any(), eq(parent), any(), any())).thenReturn(children); - PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodUserScope); + PersistentResource parentResource = new PersistentResource<>(parent, null, "1", scope); - PersistentResource childResource = parentResource.getRelation("children", "2"); + PersistentResource childResource = parentResource.getRelation(getRelationship(Parent.class, "children"), "2"); assertEquals("2", childResource.getId()); assertEquals("john buzzard", ((Child) childResource.getObject()).getName()); @@ -864,7 +863,9 @@ public void testGetSingleRelationInMemory() { public void testGetRelationForbiddenByEntity() { NoReadEntity noread = new NoReadEntity(); - PersistentResource noreadResource = new PersistentResource<>(noread, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, badUser, dictionary); + + PersistentResource noreadResource = new PersistentResource<>(noread, null, "3", scope); assertThrows(ForbiddenAccessException.class, () -> getRelation(noreadResource, "child")); } @@ -872,7 +873,9 @@ public void testGetRelationForbiddenByEntity() { public void testGetRelationForbiddenByField() { FunWithPermissions fun = new FunWithPermissions(); - PersistentResource funResource = new PersistentResource<>(fun, null, "3", badUserScope); + RequestScope scope = new TestRequestScope(tx, badUser, dictionary); + + PersistentResource funResource = new PersistentResource<>(fun, null, "3", scope); assertThrows(ForbiddenAccessException.class, () -> getRelation(funResource, "relation1")); } @@ -881,6 +884,8 @@ public void testGetRelationForbiddenByField() { public void testGetRelationForbiddenByEntityAllowedByField() { FirstClassFields firstClassFields = new FirstClassFields(); + RequestScope badUserScope = new TestRequestScope(tx, badUser, dictionary); + PersistentResource fcResource = new PersistentResource<>(firstClassFields, null, "3", badUserScope); getRelation(fcResource, "public2"); @@ -890,6 +895,8 @@ public void testGetRelationForbiddenByEntityAllowedByField() { public void testGetAttributeForbiddenByEntityAllowedByField() { FirstClassFields firstClassFields = new FirstClassFields(); + RequestScope badUserScope = new TestRequestScope(tx, badUser, dictionary); + PersistentResource fcResource = new PersistentResource<>(firstClassFields, null, "3", badUserScope); fcResource.getAttribute("public1"); @@ -899,6 +906,8 @@ public void testGetAttributeForbiddenByEntityAllowedByField() { public void testGetRelationForbiddenByEntity2() { FirstClassFields firstClassFields = new FirstClassFields(); + RequestScope badUserScope = new TestRequestScope(tx, badUser, dictionary); + PersistentResource fcResource = new PersistentResource<>(firstClassFields, null, "3", badUserScope); assertThrows(ForbiddenAccessException.class, () -> getRelation(fcResource, "private2")); @@ -908,8 +917,10 @@ public void testGetRelationForbiddenByEntity2() { public void testGetAttributeForbiddenByEntity2() { FirstClassFields firstClassFields = new FirstClassFields(); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + PersistentResource fcResource = new PersistentResource<>(firstClassFields, - null, "3", goodUserScope); + null, "3", scope); assertThrows(ForbiddenAccessException.class, () -> fcResource.getAttribute("private1")); } @@ -918,7 +929,9 @@ public void testGetAttributeForbiddenByEntity2() { public void testGetRelationInvalidRelation() { FunWithPermissions fun = new FunWithPermissions(); - PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource funResource = new PersistentResource<>(fun, null, "3", scope); assertThrows(InvalidAttributeException.class, () -> getRelation(funResource, "invalid")); } @@ -931,16 +944,12 @@ public void testGetRelationByIdSuccess() { Child child3 = newChild(3); fun.setRelation2(Sets.newHashSet(child1, child2, child3)); - User goodUser = new User(1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); - - when(tx.getRelation(eq(tx), any(), any(), any(), any(), any(), any())).thenReturn(Sets.newHashSet(child1)); + when(tx.getRelation(eq(tx), any(), any(), any())).thenReturn(Sets.newHashSet(child1)); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); - PersistentResource result = funResource.getRelation("relation2", "1"); + PersistentResource result = funResource.getRelation(getRelationship(FunWithPermissions.class, "relation2"), "1"); assertEquals(1, ((Child) result.getObject()).getId(), "The correct relationship element should be returned"); } @@ -953,23 +962,22 @@ public void testGetRelationByInvalidId() { Child child3 = newChild(3); fun.setRelation2(Sets.newHashSet(child1, child2, child3)); - User goodUser = new User(1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); - - when(tx.getRelation(eq(tx), any(), any(), any(), any(), any(), any())).thenReturn(Sets.newHashSet(child1)); + when(tx.getRelation(eq(tx), any(), any(), any())).thenReturn(Sets.newHashSet(child1)); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); - assertThrows(InvalidObjectIdentifierException.class, () -> funResource.getRelation("relation2", "-1000")); + assertThrows(InvalidObjectIdentifierException.class, + () -> funResource.getRelation(getRelationship(FunWithPermissions.class, "relation2"), "-1000")); } @Test public void testGetRelationsNoEntityAccess() { FunWithPermissions fun = new FunWithPermissions(); - PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource funResource = new PersistentResource<>(fun, null, "3", scope); Set set = getRelation(funResource, "relation4"); assertEquals(0, set.size()); @@ -979,7 +987,9 @@ public void testGetRelationsNoEntityAccess() { public void testGetRelationsNoEntityAccess2() { FunWithPermissions fun = new FunWithPermissions(); - PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource funResource = new PersistentResource<>(fun, null, "3", scope); Set set = getRelation(funResource, "relation5"); assertEquals(0, set.size()); @@ -989,8 +999,6 @@ public void testGetRelationsNoEntityAccess2() { void testDeleteResourceSuccess() { Parent parent = newParent(1); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); @@ -1009,8 +1017,6 @@ void testDeleteCascades() { invoice.setItems(Sets.newHashSet(item)); item.setInvoice(invoice); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource invoiceResource = new PersistentResource<>(invoice, null, "1", goodScope); @@ -1036,9 +1042,7 @@ void testDeleteResourceUpdateRelationshipSuccess() { assertFalse(parent.getChildren().isEmpty()); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(child), eq("parents"), any(), any(), any(), any())).thenReturn(parents); + when(tx.getRelation(any(), eq(child), any(), any())).thenReturn(parents); RequestScope goodScope = buildRequestScope(tx, goodUser); @@ -1058,11 +1062,7 @@ void testDeleteResourceForbidden() { NoDeleteEntity nodelete = new NoDeleteEntity(); nodelete.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); - RequestScope goodScope = buildRequestScope(tx, goodUser); - PersistentResource nodeleteResource = new PersistentResource<>(nodelete, null, "1", goodScope); assertThrows(ForbiddenAccessException.class, nodeleteResource::deleteResource); @@ -1077,9 +1077,6 @@ void testAddRelationSuccess() { Child child = newChild(1); - User goodUser = new User(1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); @@ -1100,9 +1097,6 @@ void testAddRelationForbiddenByField() { Child child = newChild(1); - User badUser = new User(-1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope badScope = buildRequestScope(tx, badUser); PersistentResource funResource = new PersistentResource<>(fun, null, "3", badScope); PersistentResource childResource = new PersistentResource<>(child, null, "1", badScope); @@ -1116,9 +1110,6 @@ void testAddRelationForbiddenByEntity() { Child child = newChild(2); noUpdate.setChildren(Sets.newHashSet()); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); - RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource noUpdateResource = new PersistentResource<>(noUpdate, null, "1", goodScope); PersistentResource childResource = new PersistentResource<>(child, null, "2", goodScope); @@ -1131,9 +1122,6 @@ public void testAddRelationInvalidRelation() { Child child = newChild(1); - User goodUser = new User(1); - - DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); @@ -1148,8 +1136,6 @@ public void testRemoveToManyRelationSuccess() { Parent parent3 = newParent(3, child); child.setParents(Sets.newHashSet(parent1, parent2, parent3)); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); PersistentResource removeResource = new PersistentResource<>(parent1, null, "1", goodScope); @@ -1173,8 +1159,6 @@ public void testRemoveToOneRelationSuccess() { Child child = newChild(1); fun.setRelation3(child); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodScope); @@ -1208,19 +1192,47 @@ public void testNoSaveNonModifications() { child.setReadNoAccess(secret); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(fun), eq("relation3"), any(), any(), any(), any())).thenReturn(child); - when(tx.getRelation(any(), eq(fun), eq("relation1"), any(), any(), any(), any())).thenReturn(children1); - when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(children2); - when(tx.getRelation(any(), eq(child), eq("readNoAccess"), any(), any(), any(), any())).thenReturn(secret); - - User goodUser = new User(1); - RequestScope goodScope = buildRequestScope(tx, goodUser); - - PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodScope); - PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); - PersistentResource secretResource = new PersistentResource<>(secret, null, "1", goodScope); - PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); + when(tx.getRelation(any(), eq(fun), eq(com.yahoo.elide.request.Relationship.builder() + .name("relation3") + .alias("relation3") + .projection(EntityProjection.builder() + .type(Child.class) + .build()) + .build()), any())).thenReturn(child); + + when(tx.getRelation(any(), eq(fun), eq(com.yahoo.elide.request.Relationship.builder() + .name("relation1") + .alias("relation1") + .projection(EntityProjection.builder() + .type(Child.class) + .build()) + .build()), any())).thenReturn(children1); + + when(tx.getRelation(any(), eq(parent), eq(com.yahoo.elide.request.Relationship.builder() + .name("children") + .alias("children") + .projection(EntityProjection.builder() + .type(Child.class) + .build()) + .build()), any())).thenReturn(children2); + + when(tx.getRelation(any(), eq(child), eq(com.yahoo.elide.request.Relationship.builder() + .name("readNoAccess") + .alias("readNoAccess") + .projection(EntityProjection.builder() + .type(Child.class) + .build()) + .build()), any())).thenReturn(secret); + + RequestScope funScope = new TestRequestScope(tx, goodUser, dictionary); + RequestScope childScope = new TestRequestScope(tx, goodUser, dictionary); + RequestScope parentScope = new TestRequestScope(tx, goodUser, dictionary); + + + PersistentResource funResource = new PersistentResource<>(fun, null, "1", funScope); + PersistentResource childResource = new PersistentResource<>(child, null, "1", childScope); + PersistentResource secretResource = new PersistentResource<>(secret, null, "1", childScope); + PersistentResource parentResource = new PersistentResource<>(parent, null, "1", parentScope); // Add an existing to-one relationship funResource.addRelation("relation3", childResource); @@ -1252,11 +1264,13 @@ public void testNoSaveNonModifications() { // Clear empty to-one relation secretResource.clearRelation("readNoAccess"); - goodScope.saveOrCreateObjects(); - verify(tx, never()).save(fun, goodScope); - verify(tx, never()).save(child, goodScope); - verify(tx, never()).save(parent, goodScope); - verify(tx, never()).save(secret, goodScope); + parentScope.saveOrCreateObjects(); + childScope.saveOrCreateObjects(); + funScope.saveOrCreateObjects(); + verify(tx, never()).save(fun, funScope); + verify(tx, never()).save(child, childScope); + verify(tx, never()).save(parent, parentScope); + verify(tx, never()).save(secret, childScope); } @Test() @@ -1266,8 +1280,6 @@ public void testRemoveNonexistingToOneRelation() { Child unownedChild = newChild(2); fun.setRelation3(ownedChild); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodScope); @@ -1291,8 +1303,6 @@ public void testRemoveNonexistingToManyRelation() { Parent unownedParent = newParent(4, null); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); PersistentResource removeResource = new PersistentResource<>(unownedParent, null, "1", goodScope); @@ -1318,11 +1328,16 @@ public void testClearToManyRelationSuccess() { Set parents = Sets.newHashSet(parent1, parent2, parent3); child.setParents(parents); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(child), eq("parents"), any(), any(), any(), any())).thenReturn(parents); + when(tx.getRelation(any(), eq(child), any(), any())).thenReturn(parents); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); + goodScope.setEntityProjection(EntityProjection.builder() + .type(Child.class) + .relationship("parents", + EntityProjection.builder() + .type(Parent.class) + .build()) + .build()); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); @@ -1346,12 +1361,17 @@ public void testClearToOneRelationSuccess() { Child child = newChild(1); fun.setRelation3(child); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - - when(tx.getRelation(any(), eq(fun), eq("relation3"), any(), any(), any(), any())).thenReturn(child); + when(tx.getRelation(any(), eq(fun), any(), any())).thenReturn(child); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); + goodScope.setEntityProjection(EntityProjection.builder() + .type(FunWithPermissions.class) + .relationship("relation3", + EntityProjection.builder() + .type(Child.class) + .build()) + .build()); + PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodScope); funResource.clearRelation("relation3"); @@ -1364,11 +1384,17 @@ public void testClearToOneRelationSuccess() { @Test() public void testClearRelationFilteredByReadAccess() { - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); Parent parent = new Parent(); RequestScope goodScope = buildRequestScope(tx, goodUser); + goodScope.setEntityProjection(EntityProjection.builder() + .type(Parent.class) + .relationship("children", + EntityProjection.builder() + .type(Child.class) + .build()) + .build()); + Child child1 = newChild(1); Child child2 = newChild(2); Child child3 = newChild(3); @@ -1377,7 +1403,6 @@ public void testClearRelationFilteredByReadAccess() { Child child5 = newChild(-5); child5.setId(-5); //Not accessible to goodUser - //All = (1,2,3,4,5) //Mine = (1,2,3) Set allChildren = new HashSet<>(); @@ -1389,7 +1414,7 @@ public void testClearRelationFilteredByReadAccess() { parent.setChildren(allChildren); parent.setSpouses(Sets.newHashSet()); - when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(allChildren); + when(tx.getRelation(any(), eq(parent), any(), any())).thenReturn(allChildren); PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); @@ -1427,9 +1452,15 @@ public void testClearRelationInvalidToOneUpdatePermission() { left.setNoUpdateOne2One(right); right.setNoUpdateOne2One(left); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); + goodScope.setEntityProjection(EntityProjection.builder() + .type(Left.class) + .relationship("noUpdateOne2One", + EntityProjection.builder() + .type(Right.class) + .build()) + .build()); + PersistentResource leftResource = new PersistentResource<>(left, null, "1", goodScope); assertThrows( @@ -1448,8 +1479,6 @@ public void testNoChangeRelationInvalidToOneUpdatePermission() { left.setNoUpdateOne2One(right); right.setNoUpdateOne2One(left); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource leftResource = new PersistentResource<>(left, null, "1", goodScope); @@ -1474,12 +1503,17 @@ public void testClearRelationInvalidToManyUpdatePermission() { right1.setNoUpdate(Sets.newHashSet(left)); right2.setNoUpdate(Sets.newHashSet(left)); - DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(left), any(), any())).thenReturn(noInverseUpdate); - when(tx.getRelation(any(), eq(left), eq("noInverseUpdate"), any(), any(), any(), any())).thenReturn(noInverseUpdate); - - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); + goodScope.setEntityProjection(EntityProjection.builder() + .type(Left.class) + .relationship("noInverseUpdate", + EntityProjection.builder() + .type(Right.class) + .build()) + .build()); + PersistentResource leftResource = new PersistentResource<>(left, null, "1", goodScope); assertThrows( @@ -1497,11 +1531,17 @@ public void testClearRelationInvalidToOneDeletePermission() { noDelete.setId(1); left.setNoDeleteOne2One(noDelete); - DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(left), any(), any())).thenReturn(noDelete); - when(tx.getRelation(any(), eq(left), eq("noDeleteOne2One"), any(), any(), any(), any())).thenReturn(noDelete); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); + goodScope.setEntityProjection(EntityProjection.builder() + .type(Left.class) + .relationship("noDeleteOne2One", + EntityProjection.builder() + .type(NoDeleteEntity.class) + .build()) + .build()); + PersistentResource leftResource = new PersistentResource<>(left, null, "1", goodScope); assertTrue(leftResource.clearRelation("noDeleteOne2One")); assertNull(leftResource.getObject().getNoDeleteOne2One()); @@ -1514,8 +1554,6 @@ public void testClearRelationInvalidRelation() { Child child = newChild(1); fun.setRelation3(child); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodScope); assertThrows(InvalidAttributeException.class, () -> funResource.clearRelation("invalid")); @@ -1525,9 +1563,6 @@ public void testClearRelationInvalidRelation() { public void testUpdateAttributeSuccess() { Parent parent = newParent(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); - RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); parentResource.updateAttribute("firstName", "foobar"); @@ -1542,9 +1577,6 @@ public void testUpdateAttributeSuccess() { public void testUpdateAttributeInvalidAttribute() { Parent parent = newParent(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); - RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); assertThrows(InvalidAttributeException.class, () -> parentResource.updateAttribute("invalid", "foobar")); @@ -1556,9 +1588,6 @@ public void testUpdateAttributeInvalidUpdatePermission() { fun.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User badUser = new User(-1); - RequestScope badScope = buildRequestScope(tx, badUser); PersistentResource funResource = new PersistentResource<>(fun, null, "1", badScope); @@ -1575,11 +1604,8 @@ public void testUpdateAttributeInvalidUpdatePermissionNoChange() { FunWithPermissions fun = new FunWithPermissions(); fun.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User badUser = new User(-1); RequestScope badScope = buildRequestScope(tx, badUser); - PersistentResource funResource = new PersistentResource<>(fun, null, "1", badScope); assertThrows( @@ -1597,15 +1623,20 @@ public void testLoadRecords() { Child child4 = newChild(4); Child child5 = newChild(5); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); + EntityProjection collection = EntityProjection.builder() + .type(Child.class) + + .build(); - when(tx.loadObjects(eq(Child.class), any(), any(), any(), any(RequestScope.class))) + when(tx.loadObjects(eq(collection), any(RequestScope.class))) .thenReturn(Lists.newArrayList(child1, child2, child3, child4, child5)); RequestScope goodScope = buildRequestScope(tx, goodUser); - Set loaded = PersistentResource.loadRecords(Child.class, new ArrayList<>(), - Optional.empty(), Optional.empty(), Optional.empty(), goodScope); + goodScope.setEntityProjection(collection); + + Set loaded = PersistentResource.loadRecords(EntityProjection.builder() + .type(Child.class) + .build(), new ArrayList<>(), goodScope); Set expected = Sets.newHashSet(child1, child4, child5); @@ -1623,55 +1654,68 @@ public void testLoadRecords() { public void testLoadRecordSuccess() { Child child1 = newChild(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); + EntityProjection collection = EntityProjection.builder() + .type(Child.class) - when(tx.loadObject(eq(Child.class), eq(1L), any(), any())).thenReturn(child1); + .build(); + + when(tx.loadObject(eq(collection), eq(1L), any())).thenReturn(child1); RequestScope goodScope = buildRequestScope(tx, goodUser); - PersistentResource loaded = PersistentResource.loadRecord(Child.class, "1", goodScope); + goodScope.setEntityProjection(collection); + PersistentResource loaded = PersistentResource.loadRecord(EntityProjection.builder() + .type(Child.class) + .build(), "1", goodScope); assertEquals(child1, loaded.getObject(), "The load function should return the requested child object"); } @Test public void testLoadRecordInvalidId() { - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); + EntityProjection collection = EntityProjection.builder() + .type(Child.class) - when(tx.loadObject(eq(Child.class), eq("1"), any(), any())).thenReturn(null); + .build(); + + when(tx.loadObject(eq(collection), eq("1"), any())).thenReturn(null); RequestScope goodScope = buildRequestScope(tx, goodUser); + goodScope.setEntityProjection(collection); assertThrows( InvalidObjectIdentifierException.class, - () -> PersistentResource.loadRecord(Child.class, "1", goodScope)); + () -> PersistentResource.loadRecord(EntityProjection.builder() + + .type(Child.class) + .build(), "1", goodScope)); } @Test public void testLoadRecordForbidden() { NoReadEntity noRead = new NoReadEntity(); noRead.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); + EntityProjection collection = EntityProjection.builder() + .type(NoReadEntity.class) + + .build(); - when(tx.loadObject(eq(NoReadEntity.class), eq(1L), any(), any())).thenReturn(noRead); + when(tx.loadObject(eq(collection), eq(1L), any())).thenReturn(noRead); RequestScope goodScope = buildRequestScope(tx, goodUser); + goodScope.setEntityProjection(collection); + assertThrows( ForbiddenAccessException.class, - () -> PersistentResource.loadRecord(NoReadEntity.class, "1", goodScope)); + () -> PersistentResource.loadRecord(EntityProjection.builder().type(NoReadEntity.class).build(), + "1", goodScope)); } @Test() public void testCreateObjectSuccess() { Parent parent = newParent(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); - when(tx.createNewObject(Parent.class)).thenReturn(parent); RequestScope goodScope = buildRequestScope(tx, goodUser); - PersistentResource created = PersistentResource.createObject(null, Parent.class, goodScope, Optional.of("uuid")); + PersistentResource created = PersistentResource.createObject(Parent.class, goodScope, Optional.of("uuid")); parent.setChildren(new HashSet<>()); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); @@ -1687,11 +1731,10 @@ public void testCreateMappedIdObjectSuccess() { job.setTitle("day job"); job.setParent(newParent(1)); - final DataStoreTransaction tx = mock(DataStoreTransaction.class); when(tx.createNewObject(Job.class)).thenReturn(job); - final RequestScope goodScope = buildRequestScope(tx, new User(1)); - PersistentResource created = PersistentResource.createObject(null, Job.class, goodScope, Optional.empty()); + final RequestScope goodScope = buildRequestScope(tx, new TestUser("1")); + PersistentResource created = PersistentResource.createObject(Job.class, goodScope, Optional.empty()); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); assertEquals("day job", created.getObject().getTitle(), @@ -1699,7 +1742,7 @@ public void testCreateMappedIdObjectSuccess() { ); assertNull(created.getObject().getJobId(), "The create function should not override the ID"); - created = PersistentResource.createObject(null, Job.class, goodScope, Optional.of("1234")); + created = PersistentResource.createObject(Job.class, goodScope, Optional.of("1234")); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); assertEquals("day job", created.getObject().getTitle(), @@ -1712,8 +1755,6 @@ public void testCreateMappedIdObjectSuccess() { public void testCreateObjectForbidden() { NoCreateEntity noCreate = new NoCreateEntity(); noCreate.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - User goodUser = new User(1); when(tx.createNewObject(NoCreateEntity.class)).thenReturn(noCreate); @@ -1722,7 +1763,7 @@ public void testCreateObjectForbidden() { assertThrows( ForbiddenAccessException.class, () -> { - PersistentResource created = PersistentResource.createObject(null, NoCreateEntity.class, goodScope, Optional.of("1")); + PersistentResource created = PersistentResource.createObject(NoCreateEntity.class, goodScope, Optional.of("1")); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); } ); @@ -1740,9 +1781,7 @@ public void testDeletePermissionCheckedOnInverseRelationship() { right.setAllowDeleteAtFieldLevel(Sets.newHashSet(left)); //Bad User triggers the delete permission failure - User badUser = new User(-1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(left), eq("fieldLevelDelete"), any(), any(), any(), any())).thenReturn(rights); + when(tx.getRelation(any(), eq(left), any(), any())).thenReturn(rights); RequestScope badScope = buildRequestScope(tx, badUser); PersistentResource leftResource = new PersistentResource<>(left, null, badScope.getUUIDFor(left), badScope); @@ -1765,9 +1804,7 @@ public void testUpdatePermissionCheckedOnInverseRelationship() { List empty = new ArrayList<>(); Relationship ids = new Relationship(null, new Data<>(empty)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(left), eq("noInverseUpdate"), any(), any(), any(), any())).thenReturn(rights); + when(tx.getRelation(any(), eq(left), any(), any())).thenReturn(rights); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource leftResource = new PersistentResource<>(left, null, goodScope.getUUIDFor(left), goodScope); @@ -1785,7 +1822,6 @@ public void testFieldLevelAudit() throws Exception { Parent parent = newParent(7); - User goodUser = new User(1); TestAuditLogger logger = new TestAuditLogger(); RequestScope requestScope = getUserScope(goodUser, logger); PersistentResource parentResource = new PersistentResource<>(parent, null, requestScope.getUUIDFor(parent), requestScope); @@ -1806,7 +1842,6 @@ public void testClassLevelAudit() throws Exception { Child child = newChild(5); Parent parent = newParent(7); - User goodUser = new User(1); TestAuditLogger logger = new TestAuditLogger(); RequestScope requestScope = getUserScope(goodUser, logger); PersistentResource parentResource = new PersistentResource<>( @@ -1829,9 +1864,7 @@ public void testOwningRelationshipInverseUpdates() { Parent parent = newParent(1); Child child = newChild(2); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(parent.getChildren()); + when(tx.getRelation(any(), eq(parent), any(), any())).thenReturn(parent.getChildren()); RequestScope goodScope = buildRequestScope(tx, goodUser); @@ -1852,7 +1885,7 @@ public void testOwningRelationshipInverseUpdates() { assertTrue(child.getParents().contains(parent), "The non-owning relationship should also be updated"); reset(tx); - when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(parent.getChildren()); + when(tx.getRelation(any(), eq(parent), any(), any())).thenReturn(parent.getChildren()); parentResource.clearRelation("children"); @@ -1866,20 +1899,23 @@ public void testOwningRelationshipInverseUpdates() { @Test public void testIsIdGenerated() { + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); - PersistentResource generated = new PersistentResource<>(new Child(), null, "1", goodUserScope); + PersistentResource generated = new PersistentResource<>(new Child(), null, "1", scope); assertTrue(generated.isIdGenerated(), "isIdGenerated returns true when ID field has the GeneratedValue annotation"); - PersistentResource notGenerated = new PersistentResource<>(new NoCreateEntity(), null, "1", goodUserScope); + scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource notGenerated = new PersistentResource<>(new NoCreateEntity(), null, "1", scope); assertFalse(notGenerated.isIdGenerated(), "isIdGenerated returns false when ID field does not have the GeneratedValue annotation"); } @Test - public void testSharePermissionErrorOnUpdateSingularRelationship() { + public void testTransferPermissionErrorOnUpdateSingularRelationship() { example.User userModel = new example.User(); userModel.setId(1); @@ -1890,9 +1926,12 @@ public void testSharePermissionErrorOnUpdateSingularRelationship() { idList.add(new ResourceIdentifier("noshare", "1").castToResource()); Relationship ids = new Relationship(null, new Data<>(idList)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.loadObject(eq(NoShareEntity.class), eq(1L), any(), any())).thenReturn(noShare); + EntityProjection collection = EntityProjection.builder() + .type(NoShareEntity.class) + + .build(); + + when(tx.loadObject(eq(collection), eq(1L), any())).thenReturn(noShare); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource userResource = new PersistentResource<>(userModel, null, goodScope.getUUIDFor(userModel), goodScope); @@ -1903,19 +1942,18 @@ public void testSharePermissionErrorOnUpdateSingularRelationship() { } @Test - public void testSharePermissionErrorOnUpdateRelationshipPackageLevel() { + public void testTransferPermissionErrorOnUpdateRelationshipPackageLevel() { ContainerWithPackageShare containerWithPackageShare = new ContainerWithPackageShare(); - UnshareableWithEntityUnshare unshareableWithEntityUnshare = new UnshareableWithEntityUnshare(); - unshareableWithEntityUnshare.setContainerWithPackageShare(containerWithPackageShare); + Untransferable untransferable = new Untransferable(); + untransferable.setContainerWithPackageShare(containerWithPackageShare); List unShareableList = new ArrayList<>(); - unShareableList.add(new ResourceIdentifier("unshareableWithEntityUnshare", "1").castToResource()); + unShareableList.add(new ResourceIdentifier("untransferable", "1").castToResource()); Relationship unShareales = new Relationship(null, new Data<>(unShareableList)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.loadObject(eq(UnshareableWithEntityUnshare.class), eq(1L), any(), any())).thenReturn(unshareableWithEntityUnshare); + when(tx.loadObject(any(), eq(1L), any())).thenReturn(untransferable); + RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource containerResource = new PersistentResource<>(containerWithPackageShare, null, goodScope.getUUIDFor(containerWithPackageShare), goodScope); @@ -1923,11 +1961,11 @@ public void testSharePermissionErrorOnUpdateRelationshipPackageLevel() { assertThrows( ForbiddenAccessException.class, () -> containerResource.updateRelation( - "unshareableWithEntityUnshares", unShareales.toPersistentResources(goodScope))); + "untransferables", unShareales.toPersistentResources(goodScope))); } @Test - public void testSharePermissionSuccessOnUpdateManyRelationshipPackageLevel() { + public void testTransferPermissionSuccessOnUpdateManyRelationshipPackageLevel() { ContainerWithPackageShare containerWithPackageShare = new ContainerWithPackageShare(); ShareableWithPackageShare shareableWithPackageShare = new ShareableWithPackageShare(); @@ -1937,9 +1975,7 @@ public void testSharePermissionSuccessOnUpdateManyRelationshipPackageLevel() { shareableList.add(new ResourceIdentifier("shareableWithPackageShare", "1").castToResource()); Relationship shareables = new Relationship(null, new Data<>(shareableList)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.loadObject(eq(ShareableWithPackageShare.class), eq(1L), any(), any())).thenReturn(shareableWithPackageShare); + when(tx.loadObject(any(), eq(1L), any())).thenReturn(shareableWithPackageShare); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource containerResource = new PersistentResource<>(containerWithPackageShare, null, goodScope.getUUIDFor(containerWithPackageShare), goodScope); @@ -1951,7 +1987,7 @@ public void testSharePermissionSuccessOnUpdateManyRelationshipPackageLevel() { } @Test - public void testSharePermissionErrorOnUpdateManyRelationship() { + public void testTransferPermissionErrorOnUpdateManyRelationship() { example.User userModel = new example.User(); userModel.setId(1); @@ -1965,10 +2001,8 @@ public void testSharePermissionErrorOnUpdateManyRelationship() { idList.add(new ResourceIdentifier("noshare", "2").castToResource()); Relationship ids = new Relationship(null, new Data<>(idList)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.loadObject(eq(NoShareEntity.class), eq(1L), any(), any())).thenReturn(noShare1); - when(tx.loadObject(eq(NoShareEntity.class), eq(2L), any(), any())).thenReturn(noShare2); + when(tx.loadObject(any(), eq(1L), any())).thenReturn(noShare1); + when(tx.loadObject(any(), eq(2L), any())).thenReturn(noShare2); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource userResource = new PersistentResource<>(userModel, null, goodScope.getUUIDFor(userModel), goodScope); @@ -1979,7 +2013,7 @@ public void testSharePermissionErrorOnUpdateManyRelationship() { } @Test - public void testSharePermissionSuccessOnUpdateManyRelationship() { + public void testTransferPermissionSuccessOnUpdateManyRelationship() { example.User userModel = new example.User(); userModel.setId(1); @@ -1996,10 +2030,8 @@ public void testSharePermissionSuccessOnUpdateManyRelationship() { idList.add(new ResourceIdentifier("noshare", "1").castToResource()); Relationship ids = new Relationship(null, new Data<>(idList)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.loadObject(eq(NoShareEntity.class), eq(1L), any(), any())).thenReturn(noShare1); - when(tx.getRelation(any(), eq(userModel), eq("noShares"), any(), any(), any(), any())).thenReturn(noshares); + when(tx.loadObject(any(), eq(1L), any())).thenReturn(noShare1); + when(tx.getRelation(any(), eq(userModel), any(), any())).thenReturn(noshares); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource userResource = new PersistentResource<>(userModel, null, goodScope.getUUIDFor(userModel), goodScope); @@ -2012,7 +2044,7 @@ public void testSharePermissionSuccessOnUpdateManyRelationship() { } @Test - public void testSharePermissionSuccessOnUpdateSingularRelationship() { + public void testTransferPermissionSuccessOnUpdateSingularRelationship() { example.User userModel = new example.User(); userModel.setId(1); @@ -2025,11 +2057,8 @@ public void testSharePermissionSuccessOnUpdateSingularRelationship() { idList.add(new ResourceIdentifier("noshare", "1").castToResource()); Relationship ids = new Relationship(null, new Data<>(idList)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - - when(tx.getRelation(any(), eq(userModel), eq("noShare"), any(), any(), any(), any())).thenReturn(noShare); - when(tx.loadObject(eq(NoShareEntity.class), eq(1L), any(), any())).thenReturn(noShare); + when(tx.getRelation(any(), eq(userModel), any(), any())).thenReturn(noShare); + when(tx.loadObject(any(), eq(1L), any())).thenReturn(noShare); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource userResource = new PersistentResource<>(userModel, null, goodScope.getUUIDFor(userModel), goodScope); @@ -2041,7 +2070,7 @@ public void testSharePermissionSuccessOnUpdateSingularRelationship() { } @Test - public void testSharePermissionSuccessOnClearSingularRelationship() { + public void testTransferPermissionSuccessOnClearSingularRelationship() { example.User userModel = new example.User(); userModel.setId(1); @@ -2053,9 +2082,7 @@ public void testSharePermissionSuccessOnClearSingularRelationship() { List empty = new ArrayList<>(); Relationship ids = new Relationship(null, new Data<>(empty)); - User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(any(), eq(userModel), eq("noShare"), any(), any(), any(), any())).thenReturn(noShare); + when(tx.getRelation(any(), eq(userModel), any(), any())).thenReturn(noShare); RequestScope goodScope = buildRequestScope(tx, goodUser); PersistentResource userResource = new PersistentResource<>(userModel, null, goodScope.getUUIDFor(userModel), goodScope); @@ -2077,7 +2104,6 @@ public void testCollectionChangeSpecType() { return condFn.apply((Collection) spec.getOriginal(), (Collection) spec.getModified()); }; - DataStoreTransaction tx = mock(DataStoreTransaction.class); // Ensure that change specs coming from collections work properly ChangeSpecModel csModel = new ChangeSpecModel((spec) -> collectionCheck @@ -2086,7 +2112,7 @@ public void testCollectionChangeSpecType() { PersistentResource model = bootstrapPersistentResource(csModel, tx); - when(tx.getRelation(any(), eq(model.obj), eq("otherKids"), any(), any(), any(), any())).thenReturn(new HashSet<>()); + when(tx.getRelation(any(), eq(model.obj), any(), any())).thenReturn(new HashSet<>()); /* Attributes */ // Set new data from null @@ -2124,7 +2150,8 @@ public void testCollectionChangeSpecType() { model.getObject().checkFunction = (spec) -> collectionCheck.apply("otherKids").apply(spec, (original, modified) -> original.size() == 3 && original.contains(new ChangeSpecChild(1)) && original.contains(new ChangeSpecChild(2)) && original.contains(new ChangeSpecChild(3)) && modified.size() == 2 && modified.contains(new ChangeSpecChild(1)) && modified.contains(new ChangeSpecChild(3))); model.removeRelation("otherKids", bootstrapPersistentResource(child2)); - when(tx.getRelation(any(), eq(model.obj), eq("otherKids"), any(), any(), any(), any())).thenReturn(Sets.newHashSet(child1, child3)); + when(tx.getRelation(any(), eq(model.obj), any(), any())).thenReturn(Sets.newHashSet(child1, child3)); + // Clear the rest model.getObject().checkFunction = (spec) -> collectionCheck.apply("otherKids").apply(spec, (original, modified) -> original.size() <= 2 && modified.size() < original.size()); @@ -2168,24 +2195,23 @@ public void testRelationChangeSpecType() { } return checkFn.apply((ChangeSpecChild) spec.getOriginal(), (ChangeSpecChild) spec.getModified()); }; - DataStoreTransaction tx = mock(DataStoreTransaction.class); PersistentResource model = bootstrapPersistentResource(new ChangeSpecModel((spec) -> relCheck.apply(spec, (original, modified) -> (original == null) && new ChangeSpecChild(1).equals(modified))), tx); - when(tx.getRelation(any(), eq(model.obj), eq("child"), any(), any(), any(), any())).thenReturn(null); + when(tx.getRelation(any(), eq(model.obj), any(), any())).thenReturn(null); ChangeSpecChild child1 = new ChangeSpecChild(1); assertTrue(model.updateRelation("child", Sets.newHashSet(bootstrapPersistentResource(child1, tx)))); - when(tx.getRelation(any(), eq(model.obj), eq("child"), any(), any(), any(), any())).thenReturn(child1); + when(tx.getRelation(any(), eq(model.obj), any(), any())).thenReturn(child1); model.getObject().checkFunction = (spec) -> relCheck.apply(spec, (original, modified) -> new ChangeSpecChild(1).equals(original) && new ChangeSpecChild(2).equals(modified)); ChangeSpecChild child2 = new ChangeSpecChild(2); assertTrue(model.updateRelation("child", Sets.newHashSet(bootstrapPersistentResource(child2, tx)))); - when(tx.getRelation(any(), eq(model.obj), eq("child"), any(), any(), any(), any())).thenReturn(child2); + when(tx.getRelation(any(), eq(model.obj), any(), any())).thenReturn(child2); model.getObject().checkFunction = (spec) -> relCheck.apply(spec, (original, modified) -> new ChangeSpecChild(2).equals(original) && modified == null); assertTrue(model.updateRelation("child", null)); @@ -2194,13 +2220,10 @@ public void testRelationChangeSpecType() { @Test public void testPatchRequestScope() { DataStoreTransaction tx = mock(DataStoreTransaction.class); - PatchRequestScope parentScope = - new PatchRequestScope(null, tx, new User(1), elideSettings); + PatchRequestScope parentScope = new PatchRequestScope("/book", NO_VERSION, tx, new TestUser("1"), elideSettings); PatchRequestScope scope = new PatchRequestScope( parentScope.getPath(), parentScope.getJsonApiDocument(), parentScope); // verify wrap works - assertEquals(parentScope.isUseFilterExpressions(), scope.isUseFilterExpressions()); - assertEquals(parentScope.getSorting(), scope.getSorting()); assertEquals(parentScope.getUpdateStatusCode(), scope.getUpdateStatusCode()); assertEquals(parentScope.getObjectEntityCache(), scope.getObjectEntityCache()); @@ -2209,7 +2232,10 @@ public void testPatchRequestScope() { PersistentResource parentResource = new PersistentResource<>(parent, null, "1", scope); parentResource.updateAttribute("firstName", "foobar"); - verify(tx, times(1)).setAttribute(parent, "firstName", "foobar", scope); + ArgumentCaptor attributeArgument = ArgumentCaptor.forClass(Attribute.class); + verify(tx, times(1)).setAttribute(eq(parent), attributeArgument.capture(), eq(scope)); + assertEquals(attributeArgument.getValue().getName(), "firstName"); + assertEquals(attributeArgument.getValue().getArguments().iterator().next().getValue(), "foobar"); } @Test @@ -2221,7 +2247,8 @@ public void testFilterExpressionByType() { "Hemingway" ); - RequestScope scope = buildRequestScope("/", mock(DataStoreTransaction.class), new User(1), queryParams); + RequestScope scope = buildRequestScope("/", mock(DataStoreTransaction.class), + new TestUser("1"), queryParams); Optional filter = scope.getLoadFilterExpression(Author.class); FilterPredicate predicate = (FilterPredicate) filter.get(); @@ -2238,11 +2265,10 @@ public void testSparseFields() { queryParams.add("fields[author]", "name"); - RequestScope scope = buildRequestScope("/", mock(DataStoreTransaction.class), new User(1), queryParams); + RequestScope scope = buildRequestScope("/", mock(DataStoreTransaction.class), + new TestUser("1"), queryParams); Map> expected = ImmutableMap.of("author", ImmutableSet.of("name")); assertEquals(expected, scope.getSparseFields()); - assertEquals(10, scope.getPagination().getLimit()); - assertEquals(0, scope.getPagination().getPageTotals()); } @Test @@ -2250,10 +2276,12 @@ public void testEqualsAndHashcode() { Child childWithId = newChild(1); Child childWithoutId = newChild(0); - PersistentResource resourceWithId = new PersistentResource<>(childWithId, null, goodUserScope.getUUIDFor(childWithId), goodUserScope); - PersistentResource resourceWithDifferentId = new PersistentResource<>(childWithoutId, null, goodUserScope.getUUIDFor(childWithoutId), goodUserScope); - PersistentResource resourceWithUUID = new PersistentResource<>(childWithoutId, null, "abc", goodUserScope); - PersistentResource resourceWithIdAndUUID = new PersistentResource<>(childWithId, null, "abc", goodUserScope); + RequestScope scope = new TestRequestScope(tx, goodUser, dictionary); + + PersistentResource resourceWithId = new PersistentResource<>(childWithId, null, scope.getUUIDFor(childWithId), scope); + PersistentResource resourceWithDifferentId = new PersistentResource<>(childWithoutId, null, scope.getUUIDFor(childWithoutId), scope); + PersistentResource resourceWithUUID = new PersistentResource<>(childWithoutId, null, "abc", scope); + PersistentResource resourceWithIdAndUUID = new PersistentResource<>(childWithId, null, "abc", scope); assertNotEquals(resourceWithUUID, resourceWithId); assertNotEquals(resourceWithId, resourceWithUUID); diff --git a/elide-core/src/test/java/com/yahoo/elide/core/RequestScopeTest.java b/elide-core/src/test/java/com/yahoo/elide/core/RequestScopeTest.java index 9fc9d6c88e..20c89ed9a9 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/RequestScopeTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/RequestScopeTest.java @@ -5,6 +5,7 @@ */ package com.yahoo.elide.core; +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -65,14 +66,14 @@ class MyInheritedClass extends MyBaseClass { dictionary.bindEntity(MyBaseClass.class); dictionary.bindEntity(MyInheritedClass.class); - RequestScope requestScope = new RequestScope("/", null, null, null, null, + RequestScope requestScope = new RequestScope("/", NO_VERSION, null, null, null, null, new ElideSettingsBuilder(null) .withEntityDictionary(dictionary) .build()); String myId = "myId"; // Test that a new inherited class is counted for base type - requestScope.setUUIDForObject(dictionary.getJsonAliasFor(MyInheritedClass.class), myId, new MyInheritedClass()); - assertNotNull(requestScope.getObjectById(dictionary.getJsonAliasFor(MyBaseClass.class), myId)); + requestScope.setUUIDForObject(MyInheritedClass.class, myId, new MyInheritedClass()); + assertNotNull(requestScope.getObjectById(MyBaseClass.class, myId)); } } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/TestDictionary.java b/elide-core/src/test/java/com/yahoo/elide/core/TestDictionary.java new file mode 100644 index 0000000000..245d3c7ed1 --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/TestDictionary.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core; + +import com.yahoo.elide.Injector; +import com.yahoo.elide.security.checks.Check; + +import com.google.inject.Binder; +import com.google.inject.Guice; +import com.google.inject.Module; +import com.google.inject.TypeLiteral; +import com.google.inject.name.Names; +import example.TestCheckMappings; + +import java.util.Map; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +/** + * Test Entity Dictionary. + */ +@Singleton +public class TestDictionary extends EntityDictionary { + + @Inject + public TestDictionary(Injector injector, + @Named("checkMappings") Map> checks) { + super(checks, injector); + } + + @Override + public Class lookupBoundClass(Class objClass) { + // Special handling for mocked Book class which has Entity annotation + if (objClass.getName().contains("$MockitoMock$")) { + objClass = objClass.getSuperclass(); + } + return super.lookupBoundClass(objClass); + } + + /** + * Returns a test dictionary injected with Guice. + * @return a test dictionary. + */ + public static EntityDictionary getTestDictionary() { + return getTestDictionary(TestCheckMappings.MAPPINGS); + } + + /** + * Returns a test dictionary injected with Guice. + * @param checks The security checks to setup the dictionary with. + * @return a test dictionary. + */ + public static EntityDictionary getTestDictionary(Map> checks) { + return Guice.createInjector(new Module() { + @Override + public void configure(Binder binder) { + binder.bind(Injector.class).to(TestInjector.class); + binder.bind(EntityDictionary.class).to(TestDictionary.class); + binder.bind(new TypeLiteral>>() { }) + .annotatedWith(Names.named("checkMappings")) + .toInstance(checks); + } + }).getInstance(EntityDictionary.class); + } +} diff --git a/elide-core/src/test/java/com/yahoo/elide/core/TestInjector.java b/elide-core/src/test/java/com/yahoo/elide/core/TestInjector.java new file mode 100644 index 0000000000..1d88588586 --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/TestInjector.java @@ -0,0 +1,33 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core; + +import com.yahoo.elide.Injector; + +import javax.inject.Inject; + +/** + * Test Dependency Injector. + */ +public class TestInjector implements Injector { + private final com.google.inject.Injector injector; + + @Inject + public TestInjector(com.google.inject.Injector injector) { + this.injector = injector; + } + + @Override + public void inject(Object entity) { + injector.injectMembers(entity); + } + + @Override + public T instantiate(Class cls) { + return injector.getInstance(cls); + } +} diff --git a/elide-core/src/test/java/com/yahoo/elide/core/TestRequestScope.java b/elide-core/src/test/java/com/yahoo/elide/core/TestRequestScope.java new file mode 100644 index 0000000000..1ee820048f --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/TestRequestScope.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core; + +import static com.yahoo.elide.core.EntityDictionary.NO_VERSION; +import com.yahoo.elide.ElideSettingsBuilder; +import com.yahoo.elide.jsonapi.models.JsonApiDocument; +import com.yahoo.elide.security.User; + +import java.util.Optional; +import javax.ws.rs.core.MultivaluedMap; + +/** + * Utility subclass that helps construct RequestScope objects for testing. + */ +public class TestRequestScope extends RequestScope { + + private MultivaluedMap queryParamOverrides = null; + + public TestRequestScope(DataStoreTransaction transaction, + User user, + EntityDictionary dictionary) { + super(null, NO_VERSION, new JsonApiDocument(), transaction, user, null, + new ElideSettingsBuilder(null) + .withEntityDictionary(dictionary) + .build()); + } + + public TestRequestScope(EntityDictionary dictionary, + String path, + MultivaluedMap queryParams) { + super(path, NO_VERSION, new JsonApiDocument(), null, null, queryParams, + new ElideSettingsBuilder(null) + .withEntityDictionary(dictionary) + .build()); + } + + public void setQueryParams(MultivaluedMap queryParams) { + this.queryParamOverrides = queryParams; + } + + @Override + public Optional> getQueryParams() { + if (queryParamOverrides != null) { + return Optional.of(queryParamOverrides); + } else { + return super.getQueryParams(); + } + } +} diff --git a/elide-core/src/test/java/com/yahoo/elide/core/UpdateOnCreateTest.java b/elide-core/src/test/java/com/yahoo/elide/core/UpdateOnCreateTest.java index acc34dcdcf..a20631f6a1 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/UpdateOnCreateTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/UpdateOnCreateTest.java @@ -9,83 +9,39 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.security.TestUser; import com.yahoo.elide.security.User; -import com.yahoo.elide.utils.coerce.CoerceUtil; + import example.Author; import example.Book; -import example.Editor; -import example.Publisher; import example.UpdateAndCreate; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.io.Serializable; import java.util.Optional; public class UpdateOnCreateTest extends PersistenceResourceTestSetup { - private RequestScope userOneScope; - private RequestScope userTwoScope; - private RequestScope userThreeScope; - private RequestScope userFourScope; - - public UpdateOnCreateTest() { - super(); - init(); - } - - public void init() { - dictionary.bindEntity(Author.class); - dictionary.bindEntity(Book.class); - dictionary.bindEntity(Publisher.class); - dictionary.bindEntity(Editor.class); - dictionary.bindEntity(UpdateAndCreate.class); - - UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); - updateAndCreateNewObject.setId(1L); - UpdateAndCreate updateAndCreateExistingObject = new UpdateAndCreate(); - updateAndCreateExistingObject.setId(2L); - Book book = new Book(); - Author author = new Author(); - Publisher publisher = new Publisher(); - Editor editor = new Editor(); + private User userOne = new TestUser("1"); + private User userTwo = new TestUser("2"); + private User userThree = new TestUser("3"); + private User userFour = new TestUser("4"); - publisher.setEditor(editor); + private DataStoreTransaction tx = mock(DataStoreTransaction.class); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - - User userOne = new User(1); - userOneScope = new RequestScope(null, null, tx, userOne, null, elideSettings); - User userTwo = new User(2); - userTwoScope = new RequestScope(null, null, tx, userTwo, null, elideSettings); - User userThree = new User(3); - userThreeScope = new RequestScope(null, null, tx, userThree, null, elideSettings); - User userFour = new User(4); - userFourScope = new RequestScope(null, null, tx, userFour, null, elideSettings); + @BeforeEach + public void beforeMethod() { + reset(tx); + } - when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); - when(tx.loadObject(eq(UpdateAndCreate.class), - eq((Serializable) CoerceUtil.coerce(1, Long.class)), - eq(Optional.empty()), - any(RequestScope.class) - )).thenReturn(updateAndCreateExistingObject); - when(tx.loadObject(eq(Book.class), - eq((Serializable) CoerceUtil.coerce(1, Long.class)), - eq(Optional.empty()), - any(RequestScope.class) - )).thenReturn(book); - when(tx.loadObject(eq(Author.class), - eq((Serializable) CoerceUtil.coerce(1, Long.class)), - eq(Optional.empty()), - any(RequestScope.class) - )).thenReturn(author); - when(tx.loadObject(eq(Publisher.class), - eq((Serializable) CoerceUtil.coerce(1, Long.class)), - eq(Optional.empty()), - any(RequestScope.class) - )).thenReturn(publisher); + public UpdateOnCreateTest() { + super(); + initDictionary(); } //----------------------------------------- ** Entity Creation ** ------------------------------------------------- @@ -93,30 +49,56 @@ public void init() { //Create allowed based on class level expression @Test public void createPermissionCheckClassAnnotationForCreatingAnEntitySuccessCase() { - PersistentResource created = PersistentResource.createObject(null, UpdateAndCreate.class, userOneScope, Optional.of("1")); + RequestScope userOneScope = new TestRequestScope(tx, userOne, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); + + PersistentResource created = PersistentResource.createObject(UpdateAndCreate.class, userOneScope, Optional.of("1")); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); } //Create allowed based on field level expression @Test public void createPermissionCheckFieldAnnotationForCreatingAnEntitySuccessCase() { - PersistentResource created = PersistentResource.createObject(null, UpdateAndCreate.class, userThreeScope, Optional.of("2")); + RequestScope userThreeScope = new TestRequestScope(tx, userThree, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); + + PersistentResource created = PersistentResource.createObject(UpdateAndCreate.class, userThreeScope, Optional.of("2")); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); } //Create denied based on field level expression @Test public void createPermissionCheckFieldAnnotationForCreatingAnEntityFailureCase() { + RequestScope userFourScope = new TestRequestScope(tx, userFour, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); assertThrows( ForbiddenAccessException.class, - () -> PersistentResource.createObject(null, UpdateAndCreate.class, userFourScope, Optional.of("3"))); + () -> PersistentResource.createObject(UpdateAndCreate.class, userFourScope, Optional.of("3"))); } //----------------------------------------- ** Update Attribute ** ------------------------------------------------ //Expression for field inherited from class level expression @Test public void updatePermissionInheritedForAttributeSuccessCase() { - PersistentResource loaded = PersistentResource.loadRecord(UpdateAndCreate.class, + RequestScope userTwoScope = new TestRequestScope(tx, userTwo, dictionary); + + UpdateAndCreate updateAndCreateExistingObject = new UpdateAndCreate(); + + when(tx.loadObject(any(), + eq(1L), + any(RequestScope.class) + )).thenReturn(updateAndCreateExistingObject); + + PersistentResource loaded = PersistentResource.loadRecord( + EntityProjection.builder() + .type(UpdateAndCreate.class) + .build(), "1", userTwoScope); loaded.updateAttribute("name", ""); @@ -125,7 +107,19 @@ public void updatePermissionInheritedForAttributeSuccessCase() { @Test public void updatePermissionInheritedForAttributeFailureCase() { - PersistentResource loaded = PersistentResource.loadRecord(UpdateAndCreate.class, + RequestScope userOneScope = new TestRequestScope(tx, userOne, dictionary); + + UpdateAndCreate updateAndCreateExistingObject = new UpdateAndCreate(); + + when(tx.loadObject(any(), + eq(1L), + any(RequestScope.class) + )).thenReturn(updateAndCreateExistingObject); + + PersistentResource loaded = PersistentResource.loadRecord( + EntityProjection.builder() + .type(UpdateAndCreate.class) + .build(), "1", userOneScope); assertThrows(ForbiddenAccessException.class, () -> loaded.updateAttribute("name", "")); @@ -134,7 +128,19 @@ public void updatePermissionInheritedForAttributeFailureCase() { //Class level expression overwritten by field level expression @Test public void updatePermissionOverwrittenForAttributeSuccessCase() { - PersistentResource loaded = PersistentResource.loadRecord(UpdateAndCreate.class, + RequestScope userFourScope = new TestRequestScope(tx, userFour, dictionary); + + UpdateAndCreate updateAndCreateExistingObject = new UpdateAndCreate(); + + when(tx.loadObject(any(), + eq(1L), + any(RequestScope.class) + )).thenReturn(updateAndCreateExistingObject); + + PersistentResource loaded = PersistentResource.loadRecord( + EntityProjection.builder() + .type(UpdateAndCreate.class) + .build(), "1", userFourScope); loaded.updateAttribute("alias", ""); @@ -143,7 +149,19 @@ public void updatePermissionOverwrittenForAttributeSuccessCase() { @Test public void updatePermissionOverwrittenForAttributeFailureCase() { - PersistentResource loaded = PersistentResource.loadRecord(UpdateAndCreate.class, + RequestScope userThreeScope = new TestRequestScope(tx, userThree, dictionary); + + UpdateAndCreate updateAndCreateExistingObject = new UpdateAndCreate(); + + when(tx.loadObject(any(), + eq(1L), + any(RequestScope.class) + )).thenReturn(updateAndCreateExistingObject); + + PersistentResource loaded = PersistentResource.loadRecord( + EntityProjection.builder() + .type(UpdateAndCreate.class) + .build(), "1", userThreeScope); assertThrows(ForbiddenAccessException.class, () -> loaded.updateAttribute("alias", "")); @@ -154,11 +172,31 @@ public void updatePermissionOverwrittenForAttributeFailureCase() { //Expression for relation inherited from class level expression @Test public void updatePermissionInheritedForRelationSuccessCase() { - PersistentResource loaded = PersistentResource.loadRecord(UpdateAndCreate.class, + RequestScope userTwoScope = new TestRequestScope(tx, userTwo, dictionary); + + UpdateAndCreate updateAndCreateExistingObject = new UpdateAndCreate(); + + when(tx.loadObject(any(), + eq(1L), + any(RequestScope.class) + )).thenReturn(updateAndCreateExistingObject); + + when(tx.loadObject(any(), + eq(2L), + any(RequestScope.class) + )).thenReturn(new Book()); + + PersistentResource loaded = PersistentResource.loadRecord( + EntityProjection.builder() + .type(UpdateAndCreate.class) + .build(), "1", userTwoScope); - PersistentResource loadedBook = PersistentResource.loadRecord(Book.class, - "1", + PersistentResource loadedBook = PersistentResource.loadRecord( + EntityProjection.builder() + .type(Book.class) + .build(), + "2", userTwoScope); loaded.addRelation("books", loadedBook); loaded.getRequestScope().getPermissionExecutor().executeCommitChecks(); @@ -166,11 +204,31 @@ public void updatePermissionInheritedForRelationSuccessCase() { @Test public void updatePermissionInheritedForRelationFailureCase() { - PersistentResource loaded = PersistentResource.loadRecord(UpdateAndCreate.class, + RequestScope userOneScope = new TestRequestScope(tx, userOne, dictionary); + + UpdateAndCreate updateAndCreateExistingObject = new UpdateAndCreate(); + + when(tx.loadObject(any(), + eq(1L), + any(RequestScope.class) + )).thenReturn(updateAndCreateExistingObject); + + when(tx.loadObject(any(), + eq(2L), + any(RequestScope.class) + )).thenReturn(new Book()); + + PersistentResource loaded = PersistentResource.loadRecord( + EntityProjection.builder() + .type(UpdateAndCreate.class) + .build(), "1", userOneScope); - PersistentResource loadedBook = PersistentResource.loadRecord(Book.class, - "1", + PersistentResource loadedBook = PersistentResource.loadRecord( + EntityProjection.builder() + .type(Book.class) + .build(), + "2", userOneScope); assertThrows(ForbiddenAccessException.class, () -> loaded.addRelation("books", loadedBook)); } @@ -178,11 +236,33 @@ public void updatePermissionInheritedForRelationFailureCase() { //Class level expression overwritten by field level expression @Test public void updatePermissionOverwrittenForRelationSuccessCase() { - PersistentResource loaded = PersistentResource.loadRecord(UpdateAndCreate.class, + RequestScope userThreeScope = new TestRequestScope(tx, new TestUser("3"), dictionary); + + + UpdateAndCreate updateAndCreateExistingObject = new UpdateAndCreate(); + updateAndCreateExistingObject.setId(1L); + + when(tx.loadObject(any(), + eq(1L), + any(RequestScope.class) + )).thenReturn(updateAndCreateExistingObject); + + when(tx.loadObject(any(), + eq(2L), + any(RequestScope.class) + )).thenReturn(new Author()); + + PersistentResource loaded = PersistentResource.loadRecord( + EntityProjection.builder() + .type(UpdateAndCreate.class) + .build(), "1", userThreeScope); - PersistentResource loadedAuthor = PersistentResource.loadRecord(Author.class, - "1", + PersistentResource loadedAuthor = PersistentResource.loadRecord( + EntityProjection.builder() + .type(Author.class) + .build(), + "2", userThreeScope); loaded.addRelation("author", loadedAuthor); loaded.getRequestScope().getPermissionExecutor().executeCommitChecks(); @@ -190,11 +270,32 @@ public void updatePermissionOverwrittenForRelationSuccessCase() { @Test public void updatePermissionOverwrittenForRelationFailureCase() { - PersistentResource loaded = PersistentResource.loadRecord(UpdateAndCreate.class, + RequestScope userTwoScope = new TestRequestScope(tx, userTwo, dictionary); + + UpdateAndCreate updateAndCreateExistingObject = new UpdateAndCreate(); + + when(tx.loadObject(any(), + eq(1L), + any(RequestScope.class) + )).thenReturn(updateAndCreateExistingObject); + + when(tx.loadObject(any(), + eq(2L), + any(RequestScope.class) + )).thenReturn(new Author()); + + PersistentResource loaded = PersistentResource.loadRecord( + EntityProjection.builder() + .type(UpdateAndCreate.class) + + .build(), "1", userTwoScope); - PersistentResource loadedAuthor = PersistentResource.loadRecord(Author.class, - "1", + PersistentResource loadedAuthor = PersistentResource.loadRecord( + EntityProjection.builder() + .type(Author.class) + .build(), + "2", userTwoScope); assertThrows(ForbiddenAccessException.class, () -> loaded.addRelation("author", loadedAuthor)); } @@ -203,54 +304,99 @@ public void updatePermissionOverwrittenForRelationFailureCase() { //Expression for field inherited from class level expression @Test public void createPermissionInheritedForAttributeSuccessCase() { - PersistentResource created = PersistentResource.createObject(null, UpdateAndCreate.class, userOneScope, Optional.of("4")); + RequestScope userOneScope = new TestRequestScope(tx, userOne, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); + + PersistentResource created = PersistentResource.createObject(UpdateAndCreate.class, userOneScope, Optional.of("4")); created.updateAttribute("name", ""); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); } @Test public void createPermissionInheritedForAttributeFailureCase() { - PersistentResource created = PersistentResource.createObject(null, UpdateAndCreate.class, userThreeScope, Optional.of("5")); + RequestScope userThreeScope = new TestRequestScope(tx, userThree, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); + + PersistentResource created = PersistentResource.createObject(UpdateAndCreate.class, userThreeScope, Optional.of("5")); assertThrows(ForbiddenAccessException.class, () -> created.updateAttribute("name", "")); } //Class level expression overwritten by field level expression @Test public void createPermissionOverwrittenForAttributeSuccessCase() { - PersistentResource created = PersistentResource.createObject(null, UpdateAndCreate.class, userThreeScope, Optional.of("6")); + RequestScope userThreeScope = new TestRequestScope(tx, userThree, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); + + PersistentResource created = PersistentResource.createObject(UpdateAndCreate.class, userThreeScope, Optional.of("6")); created.updateAttribute("alias", ""); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); } @Test public void createPermissionOverwrittenForAttributeFailureCase() { + RequestScope userFourScope = new TestRequestScope(tx, userFour, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); assertThrows( ForbiddenAccessException.class, () -> { PersistentResource created = - PersistentResource.createObject(null, UpdateAndCreate.class, userFourScope, Optional.of("7")); + PersistentResource.createObject(UpdateAndCreate.class, userFourScope, Optional.of("7")); created.updateAttribute("alias", ""); } ); } - //----------------------------------------- ** Update Relation On Create ** -------------------------------------- //Expression for relation inherited from class level expression @Test public void createPermissionInheritedForRelationSuccessCase() { - PersistentResource created = PersistentResource.createObject(null, UpdateAndCreate.class, userOneScope, Optional.of("8")); - PersistentResource loadedBook = PersistentResource.loadRecord(Book.class, - "1", + RequestScope userOneScope = new TestRequestScope(tx, userOne, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); + + when(tx.loadObject(any(), + eq(2L), + any(RequestScope.class) + )).thenReturn(new Book()); + + PersistentResource created = PersistentResource.createObject(UpdateAndCreate.class, userOneScope, Optional.of("8")); + PersistentResource loadedBook = PersistentResource.loadRecord( + EntityProjection.builder() + .type(Book.class) + .build(), + "2", userOneScope); + created.addRelation("books", loadedBook); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); } @Test public void createPermissionInheritedForRelationFailureCase() { - PersistentResource created = PersistentResource.createObject(null, UpdateAndCreate.class, userThreeScope, Optional.of("9")); - PersistentResource loadedBook = PersistentResource.loadRecord(Book.class, - "1", + RequestScope userThreeScope = new TestRequestScope(tx, userThree, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); + + when(tx.loadObject(any(), + eq(2L), + any(RequestScope.class) + )).thenReturn(new Book()); + + PersistentResource created = PersistentResource.createObject(UpdateAndCreate.class, userThreeScope, Optional.of("9")); + PersistentResource loadedBook = PersistentResource.loadRecord( + EntityProjection.builder() + .type(Book.class) + .build(), + "2", userThreeScope); assertThrows(ForbiddenAccessException.class, () -> created.addRelation("books", loadedBook)); } @@ -258,9 +404,22 @@ public void createPermissionInheritedForRelationFailureCase() { //Class level expression overwritten by field level expression @Test public void createPermissionOverwrittenForRelationSuccessCase() { - PersistentResource created = PersistentResource.createObject(null, UpdateAndCreate.class, userTwoScope, Optional.of("10")); - PersistentResource loadedAuthor = PersistentResource.loadRecord(Author.class, - "1", + RequestScope userTwoScope = new TestRequestScope(tx, userTwo, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); + + when(tx.loadObject(any(), + eq(2L), + any(RequestScope.class) + )).thenReturn(new Author()); + + PersistentResource created = PersistentResource.createObject(UpdateAndCreate.class, userTwoScope, Optional.of("10")); + PersistentResource loadedAuthor = PersistentResource.loadRecord( + EntityProjection.builder() + .type(Author.class) + .build(), + "2", userTwoScope); created.addRelation("author", loadedAuthor); created.getRequestScope().getPermissionExecutor().executeCommitChecks(); @@ -268,9 +427,22 @@ public void createPermissionOverwrittenForRelationSuccessCase() { @Test public void createPermissionOverwrittenForRelationFailureCase() { - PersistentResource created = PersistentResource.createObject(null, UpdateAndCreate.class, userOneScope, Optional.of("11")); - PersistentResource loadedAuthor = PersistentResource.loadRecord(Author.class, - "1", + RequestScope userOneScope = new TestRequestScope(tx, userOne, dictionary); + + UpdateAndCreate updateAndCreateNewObject = new UpdateAndCreate(); + when(tx.createNewObject(UpdateAndCreate.class)).thenReturn(updateAndCreateNewObject); + + when(tx.loadObject(any(), + eq(2L), + any(RequestScope.class) + )).thenReturn(new Author()); + + PersistentResource created = PersistentResource.createObject(UpdateAndCreate.class, userOneScope, Optional.of("11")); + PersistentResource loadedAuthor = PersistentResource.loadRecord( + EntityProjection.builder() + .type(Author.class) + .build(), + "2", userOneScope); assertThrows(ForbiddenAccessException.class, () -> created.addRelation("author", loadedAuthor)); } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/VerifyFieldAccessFilterExpressionVisitorTest.java b/elide-core/src/test/java/com/yahoo/elide/core/VerifyFieldAccessFilterExpressionVisitorTest.java index b9b188a4aa..81117b9487 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/VerifyFieldAccessFilterExpressionVisitorTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/VerifyFieldAccessFilterExpressionVisitorTest.java @@ -289,6 +289,7 @@ public void testShortCircuitPass() throws Exception { verify(permissionExecutor, times(1)).checkUserPermissions(Book.class, ReadPermission.class, AUTHORS); verify(permissionExecutor, times(1)).checkUserPermissions(Author.class, ReadPermission.class, NAME); verify(permissionExecutor, never()).checkSpecificFieldPermissions(resource, null, ReadPermission.class, GENRE); +<<<<<<< HEAD verify(permissionExecutor, times(2)).checkUserPermissions(any(), any(), any()); verify(permissionExecutor, never()).handleFilterJoinReject(any(), any(), any()); verify(tx, never()).getRelation(any(), any(), any(), any(), any(), any(), any()); @@ -408,5 +409,8 @@ public void testCustomFilterJoin() throws Exception { verify(permissionExecutor, never()).checkUserPermissions(any(), any(), any()); verify(permissionExecutor, times(1)).handleFilterJoinReject(any(), any(), any()); verify(tx, never()).getRelation(any(), any(), any(), any(), any(), any(), any()); +======= + verify(tx, never()).getRelation(any(), any(), any(), any()); +>>>>>>> 4271b3c98... Fixed rebase issues } } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransactionTest.java b/elide-core/src/test/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransactionTest.java index e25f294a4d..7e5a1fad0b 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransactionTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransactionTest.java @@ -6,6 +6,7 @@ package com.yahoo.elide.core.datastore.inmemory; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -25,17 +26,22 @@ import com.yahoo.elide.core.filter.InPredicate; import com.yahoo.elide.core.filter.expression.AndFilterExpression; import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; +import com.yahoo.elide.core.pagination.PaginationImpl; +import com.yahoo.elide.core.sort.SortingImpl; +import com.yahoo.elide.request.EntityProjection; +import com.yahoo.elide.request.Relationship; +import com.yahoo.elide.request.Sorting; import com.google.common.collect.Lists; import com.google.common.collect.Sets; + import example.Author; import example.Book; import example.Editor; import example.Publisher; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import java.util.ArrayList; import java.util.Arrays; @@ -44,7 +50,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -54,7 +59,7 @@ public class InMemoryStoreTransactionTest { private RequestScope scope = mock(RequestScope.class); private InMemoryStoreTransaction inMemoryStoreTransaction = new InMemoryStoreTransaction(wrappedTransaction); private EntityDictionary dictionary; - private Set books = new HashSet<>(); + private Set books = new HashSet<>(); private Book book1; private Book book2; private Book book3; @@ -132,24 +137,17 @@ public void testFullFilterPredicatePushDown() { FilterExpression expression = new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); - when(wrappedTransaction.supportsFiltering(eq(Book.class), - any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.of(expression)), - eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .filterExpression(expression) + .build(); - Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.of(expression), - Optional.empty(), - Optional.empty(), - scope); + when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); + when(wrappedTransaction.loadObjects(eq(projection), eq(scope))).thenReturn(books); - verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.of(expression)), - eq(Optional.empty()), - eq(Optional.empty()), - eq(scope)); + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects(projection, scope); + + verify(wrappedTransaction, times(1)).loadObjects(eq(projection), eq(scope)); assertEquals(3, loaded.size()); assertTrue(loaded.contains(book1)); @@ -162,89 +160,66 @@ public void testTransactionRequiresInMemoryFilterDuringGetRelation() { FilterExpression expression = new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + Relationship relationship = Relationship.builder() + .projection(EntityProjection.builder() + .type(Book.class) + .filterExpression(expression) + .build()) + .name("books") + .alias("books") + .build(); + + ArgumentCaptor relationshipArgument = ArgumentCaptor.forClass(Relationship.class); + when(scope.getNewPersistentResources()).thenReturn(Sets.newHashSet(mock(PersistentResource.class))); when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); - when(wrappedTransaction.getRelation(eq(inMemoryStoreTransaction), eq(author), eq("books"), - eq(Optional.empty()), eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + when(wrappedTransaction.getRelation(eq(inMemoryStoreTransaction), eq(author), any(), eq(scope))).thenReturn(books); Collection loaded = (Collection) inMemoryStoreTransaction.getRelation( - inMemoryStoreTransaction, - author, - "books", - Optional.of(expression), - Optional.empty(), - Optional.empty(), - scope); + inMemoryStoreTransaction, author, relationship, scope); verify(wrappedTransaction, times(1)).getRelation( eq(inMemoryStoreTransaction), eq(author), - eq("books"), - eq(Optional.empty()), - eq(Optional.empty()), - eq(Optional.empty()), + relationshipArgument.capture(), eq(scope)); + assertNull(relationshipArgument.getValue().getProjection().getFilterExpression()); + assertNull(relationshipArgument.getValue().getProjection().getSorting()); + assertNull(relationshipArgument.getValue().getProjection().getPagination()); + assertEquals(2, loaded.size()); assertTrue(loaded.contains(book1)); assertTrue(loaded.contains(book3)); } @Test - public void testTransactionRequiresInMemoryFilterDuringLoad() { + public void testDataStoreRequiresTotalInMemoryFilter() { FilterExpression expression = new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); - when(wrappedTransaction.supportsFiltering(eq(Book.class), - any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.of(expression)), - eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .filterExpression(expression) + .build(); - Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.of(expression), - Optional.empty(), - Optional.empty(), - scope); - - verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.of(expression)), - eq(Optional.empty()), - eq(Optional.empty()), - eq(scope)); - - assertEquals(3, loaded.size()); - assertTrue(loaded.contains(book1)); - assertTrue(loaded.contains(book2)); - assertTrue(loaded.contains(book3)); - } - - @Test - public void testDataStoreRequiresTotalInMemoryFilter() { - FilterExpression expression = - new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + ArgumentCaptor projectionArgument = ArgumentCaptor.forClass(EntityProjection.class); when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.NONE); - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), - eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); - Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.of(expression), - Optional.empty(), - Optional.empty(), - scope); + when(wrappedTransaction.loadObjects(any(), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects(projection, scope); verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.empty()), - eq(Optional.empty()), - eq(Optional.empty()), + projectionArgument.capture(), eq(scope)); + assertNull(projectionArgument.getValue().getFilterExpression()); + assertNull(projectionArgument.getValue().getPagination()); + assertNull(projectionArgument.getValue().getSorting()); assertEquals(2, loaded.size()); assertTrue(loaded.contains(book1)); assertTrue(loaded.contains(book3)); @@ -258,25 +233,28 @@ public void testDataStoreRequiresPartialInMemoryFilter() { new InPredicate(new Path(Book.class, dictionary, "editor.firstName"), "Jane"); FilterExpression expression = new AndFilterExpression(expression1, expression2); + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .filterExpression(expression) + .build(); + + ArgumentCaptor projectionArgument = ArgumentCaptor.forClass(EntityProjection.class); + when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.PARTIAL); - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.of(expression1)), - eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); + when(wrappedTransaction.loadObjects(any(), eq(scope))).thenReturn(books); Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.of(expression), - Optional.empty(), - Optional.empty(), + projection, scope); verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.of(expression1)), - eq(Optional.empty()), - eq(Optional.empty()), + projectionArgument.capture(), eq(scope)); + assertEquals(projectionArgument.getValue().getFilterExpression(), expression1); + assertNull(projectionArgument.getValue().getPagination()); + assertNull(projectionArgument.getValue().getSorting()); assertEquals(1, loaded.size()); assertTrue(loaded.contains(book3)); } @@ -286,30 +264,32 @@ public void testSortingPushDown() { Map sortOrder = new HashMap<>(); sortOrder.put("title", Sorting.SortOrder.asc); - Sorting sorting = new Sorting(sortOrder); + Sorting sorting = new SortingImpl(sortOrder, Book.class, dictionary); + + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .sorting(sorting) + .build(); + + ArgumentCaptor projectionArgument = ArgumentCaptor.forClass(EntityProjection.class); when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); when(wrappedTransaction.supportsSorting(eq(Book.class), any())).thenReturn(true); - - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), - eq(Optional.of(sorting)), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); + when(wrappedTransaction.loadObjects(any(), eq(scope))).thenReturn(books); Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.empty(), - Optional.of(sorting), - Optional.empty(), + projection, scope); verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.empty()), - eq(Optional.of(sorting)), - eq(Optional.empty()), + projectionArgument.capture(), eq(scope)); + assertNull(projectionArgument.getValue().getFilterExpression()); + assertNull(projectionArgument.getValue().getPagination()); + assertEquals(projectionArgument.getValue().getSorting(), sorting); assertEquals(3, loaded.size()); } @@ -318,30 +298,32 @@ public void testDataStoreRequiresInMemorySorting() { Map sortOrder = new HashMap<>(); sortOrder.put("title", Sorting.SortOrder.asc); - Sorting sorting = new Sorting(sortOrder); + Sorting sorting = new SortingImpl(sortOrder, Book.class, dictionary); + + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .sorting(sorting) + .build(); + + ArgumentCaptor projectionArgument = ArgumentCaptor.forClass(EntityProjection.class); when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); when(wrappedTransaction.supportsSorting(eq(Book.class), any())).thenReturn(false); - - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), - eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); + when(wrappedTransaction.loadObjects(any(), eq(scope))).thenReturn(books); Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.empty(), - Optional.of(sorting), - Optional.empty(), + projection, scope); verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.empty()), - eq(Optional.empty()), - eq(Optional.empty()), + projectionArgument.capture(), eq(scope)); + assertNull(projectionArgument.getValue().getFilterExpression()); + assertNull(projectionArgument.getValue().getPagination()); + assertNull(projectionArgument.getValue().getSorting()); assertEquals(3, loaded.size()); List bookTitles = loaded.stream().map((o) -> ((Book) o).getTitle()).collect(Collectors.toList()); @@ -356,30 +338,33 @@ public void testFilteringRequiresInMemorySorting() { Map sortOrder = new HashMap<>(); sortOrder.put("title", Sorting.SortOrder.asc); - Sorting sorting = new Sorting(sortOrder); + Sorting sorting = new SortingImpl(sortOrder, Book.class, dictionary); + + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .filterExpression(expression) + .sorting(sorting) + .build(); + + ArgumentCaptor projectionArgument = ArgumentCaptor.forClass(EntityProjection.class); when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.NONE); when(wrappedTransaction.supportsSorting(eq(Book.class), any())).thenReturn(true); - - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), - eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); + when(wrappedTransaction.loadObjects(any(), eq(scope))).thenReturn(books); Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.of(expression), - Optional.of(sorting), - Optional.empty(), + projection, scope); verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.empty()), - eq(Optional.empty()), - eq(Optional.empty()), + projectionArgument.capture(), eq(scope)); + assertNull(projectionArgument.getValue().getFilterExpression()); + assertNull(projectionArgument.getValue().getPagination()); + assertNull(projectionArgument.getValue().getSorting()); assertEquals(2, loaded.size()); List bookTitles = loaded.stream().map((o) -> ((Book) o).getTitle()).collect(Collectors.toList()); @@ -388,57 +373,63 @@ public void testFilteringRequiresInMemorySorting() { @Test public void testPaginationPushDown() { - Pagination pagination = Pagination.getDefaultPagination(elideSettings); + PaginationImpl pagination = PaginationImpl.getDefaultPagination(Book.class, elideSettings); + + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .pagination(pagination) + .build(); + + ArgumentCaptor projectionArgument = ArgumentCaptor.forClass(EntityProjection.class); when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); - when(wrappedTransaction.supportsPagination(eq(Book.class))).thenReturn(true); + when(wrappedTransaction.supportsPagination(eq(Book.class), any())).thenReturn(true); - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), - eq(Optional.empty()), eq(Optional.of(pagination)), eq(scope))).thenReturn((Set) books); + when(wrappedTransaction.loadObjects(any(), eq(scope))).thenReturn(books); Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.empty(), - Optional.empty(), - Optional.of(pagination), + projection, scope); verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.empty()), - eq(Optional.empty()), - eq(Optional.of(pagination)), + projectionArgument.capture(), eq(scope)); + assertNull(projectionArgument.getValue().getFilterExpression()); + assertEquals(projectionArgument.getValue().getPagination(), pagination); + assertNull(projectionArgument.getValue().getSorting()); assertEquals(3, loaded.size()); } @Test public void testDataStoreRequiresInMemoryPagination() { - Pagination pagination = Pagination.getDefaultPagination(elideSettings); + PaginationImpl pagination = PaginationImpl.getDefaultPagination(Book.class, elideSettings); + + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .pagination(pagination) + .build(); + + ArgumentCaptor projectionArgument = ArgumentCaptor.forClass(EntityProjection.class); when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); - when(wrappedTransaction.supportsPagination(eq(Book.class))).thenReturn(false); + when(wrappedTransaction.supportsPagination(eq(Book.class), any())).thenReturn(false); - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), - eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); + when(wrappedTransaction.loadObjects(any(), eq(scope))).thenReturn(books); Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.empty(), - Optional.empty(), - Optional.of(pagination), + projection, scope); verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.empty()), - eq(Optional.empty()), - eq(Optional.empty()), + projectionArgument.capture(), eq(scope)); + assertNull(projectionArgument.getValue().getFilterExpression()); + assertNull(projectionArgument.getValue().getPagination()); + assertNull(projectionArgument.getValue().getSorting()); assertEquals(3, loaded.size()); assertTrue(loaded.contains(book1)); assertTrue(loaded.contains(book2)); @@ -450,29 +441,33 @@ public void testFilteringRequiresInMemoryPagination() { FilterExpression expression = new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); - Pagination pagination = Pagination.getDefaultPagination(elideSettings); + PaginationImpl pagination = PaginationImpl.getDefaultPagination(Book.class, elideSettings); + + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .filterExpression(expression) + .pagination(pagination) + .build(); + + ArgumentCaptor projectionArgument = ArgumentCaptor.forClass(EntityProjection.class); when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.NONE); - when(wrappedTransaction.supportsPagination(eq(Book.class))).thenReturn(true); + when(wrappedTransaction.supportsPagination(eq(Book.class), any())).thenReturn(true); - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), - eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); + when(wrappedTransaction.loadObjects(any(), eq(scope))).thenReturn(books); Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.of(expression), - Optional.empty(), - Optional.of(pagination), + projection, scope); verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.empty()), - eq(Optional.empty()), - eq(Optional.empty()), + projectionArgument.capture(), eq(scope)); + assertNull(projectionArgument.getValue().getFilterExpression()); + assertNull(projectionArgument.getValue().getPagination()); + assertNull(projectionArgument.getValue().getSorting()); assertEquals(2, loaded.size()); assertTrue(loaded.contains(book1)); assertTrue(loaded.contains(book3)); @@ -480,36 +475,40 @@ public void testFilteringRequiresInMemoryPagination() { @Test public void testSortingRequiresInMemoryPagination() { - Pagination pagination = Pagination.getDefaultPagination(elideSettings); + PaginationImpl pagination = PaginationImpl.getDefaultPagination(Book.class, elideSettings); Map sortOrder = new HashMap<>(); sortOrder.put("title", Sorting.SortOrder.asc); - Sorting sorting = new Sorting(sortOrder); + Sorting sorting = new SortingImpl(sortOrder, Book.class, dictionary); + + EntityProjection projection = EntityProjection.builder() + .type(Book.class) + .sorting(sorting) + .pagination(pagination) + .build(); + + ArgumentCaptor projectionArgument = ArgumentCaptor.forClass(EntityProjection.class); when(wrappedTransaction.supportsFiltering(eq(Book.class), any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); when(wrappedTransaction.supportsSorting(eq(Book.class), any())).thenReturn(false); - when(wrappedTransaction.supportsPagination(eq(Book.class))).thenReturn(true); + when(wrappedTransaction.supportsPagination(eq(Book.class), any())).thenReturn(true); - when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), - eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn((Set) books); + when(wrappedTransaction.loadObjects(any(), eq(scope))).thenReturn(books); Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( - Book.class, - Optional.empty(), - Optional.of(sorting), - Optional.of(pagination), + projection, scope); verify(wrappedTransaction, times(1)).loadObjects( - eq(Book.class), - eq(Optional.empty()), - eq(Optional.empty()), - eq(Optional.empty()), + projectionArgument.capture(), eq(scope)); + assertNull(projectionArgument.getValue().getFilterExpression()); + assertNull(projectionArgument.getValue().getPagination()); + assertNull(projectionArgument.getValue().getSorting()); assertEquals(3, loaded.size()); assertTrue(loaded.contains(book1)); assertTrue(loaded.contains(book2)); diff --git a/elide-core/src/test/java/com/yahoo/elide/core/datastore/wrapped/TransactionWrapperTest.java b/elide-core/src/test/java/com/yahoo/elide/core/datastore/wrapped/TransactionWrapperTest.java index 4a84d6a615..2cd67f3d74 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/datastore/wrapped/TransactionWrapperTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/datastore/wrapped/TransactionWrapperTest.java @@ -10,41 +10,24 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.yahoo.elide.core.DataStoreTransaction; -import com.yahoo.elide.security.User; +import com.yahoo.elide.request.Attribute; import org.junit.jupiter.api.Test; -import java.util.Optional; - public class TransactionWrapperTest { private class TestTransactionWrapper extends TransactionWrapper { - public TestTransactionWrapper(DataStoreTransaction wrapped) { super(wrapped); } } - @Test - public void testAccessUser() { - DataStoreTransaction wrapped = mock(DataStoreTransaction.class); - DataStoreTransaction wrapper = new TestTransactionWrapper(wrapped); - - Object wrappedUser = new Object(); - User expectedUser = new User(wrappedUser); - when(wrapped.accessUser(eq(wrappedUser))).thenReturn(expectedUser); - - User actualUser = wrapper.accessUser(wrappedUser); - - verify(wrapped, times(1)).accessUser(eq(wrappedUser)); - assertEquals(expectedUser, actualUser); - } - @Test public void testPreCommit() { DataStoreTransaction wrapped = mock(DataStoreTransaction.class); @@ -84,12 +67,11 @@ public void testLoadObjects() throws Exception { DataStoreTransaction wrapper = new TestTransactionWrapper(wrapped); Iterable expected = mock(Iterable.class); - when(wrapped.loadObjects(any(), any(), any(), any(), any())).thenReturn(expected); + when(wrapped.loadObjects(any(), any())).thenReturn(expected); - Iterable actual = wrapper.loadObjects(null, Optional.empty(), - Optional.empty(), Optional.empty(), null); + Iterable actual = wrapper.loadObjects(null, null); - verify(wrapped, times(1)).loadObjects(any(), any(), any(), any(), any()); + verify(wrapped, times(1)).loadObjects(any(), any()); assertEquals(expected, actual); } @@ -160,10 +142,10 @@ public void testSupportsPagination() { DataStoreTransaction wrapped = mock(DataStoreTransaction.class); DataStoreTransaction wrapper = new TestTransactionWrapper(wrapped); - when(wrapped.supportsPagination(any())).thenReturn(true); - boolean actual = wrapper.supportsPagination(null); + when(wrapped.supportsPagination(any(), any())).thenReturn(true); + boolean actual = wrapper.supportsPagination(null, null); - verify(wrapped, times(1)).supportsPagination(any()); + verify(wrapped, times(1)).supportsPagination(any(), any()); assertTrue(actual); } @@ -184,11 +166,11 @@ public void testGetAttribute() { DataStoreTransaction wrapped = mock(DataStoreTransaction.class); DataStoreTransaction wrapper = new TestTransactionWrapper(wrapped); - when(wrapped.getAttribute(any(), any(), any())).thenReturn(1L); + when(wrapped.getAttribute(any(), isA(Attribute.class), any())).thenReturn(1L); - Object actual = wrapper.getAttribute(null, null, null); + Object actual = wrapper.getAttribute(null, Attribute.builder().name("foo").type(String.class).build(), null); - verify(wrapped, times(1)).getAttribute(any(), any(), any()); + verify(wrapped, times(1)).getAttribute(any(), isA(Attribute.class), any()); assertEquals(1L, actual); } @@ -197,9 +179,9 @@ public void testSetAttribute() { DataStoreTransaction wrapped = mock(DataStoreTransaction.class); DataStoreTransaction wrapper = new TestTransactionWrapper(wrapped); - wrapper.setAttribute(null, null, null, null); + wrapper.setAttribute(null, null, null); - verify(wrapped, times(1)).setAttribute(any(), any(), any(), any()); + verify(wrapped, times(1)).setAttribute(any(), any(), any()); } @Test @@ -227,12 +209,11 @@ public void testGetRelation() { DataStoreTransaction wrapped = mock(DataStoreTransaction.class); DataStoreTransaction wrapper = new TestTransactionWrapper(wrapped); - when(wrapped.getRelation(any(), any(), any(), any(), any(), any(), any())).thenReturn(1L); + when(wrapped.getRelation(any(), any(), any(), any())).thenReturn(1L); - Object actual = wrapper.getRelation(null, null, null, null, - null, null, null); + Object actual = wrapper.getRelation(null, null, null, null); - verify(wrapped, times(1)).getRelation(any(), any(), any(), any(), any(), any(), any()); + verify(wrapped, times(1)).getRelation(any(), any(), any(), any()); assertEquals(1L, actual); } @@ -241,11 +222,11 @@ public void testLoadObject() { DataStoreTransaction wrapped = mock(DataStoreTransaction.class); DataStoreTransaction wrapper = new TestTransactionWrapper(wrapped); - when(wrapped.loadObject(any(), any(), any(), any())).thenReturn(1L); + when(wrapped.loadObject(any(), any(), any())).thenReturn(1L); - Object actual = wrapper.loadObject(null, null, null, null); + Object actual = wrapper.loadObject(null, null, null); - verify(wrapped, times(1)).loadObject(any(), any(), any(), any()); + verify(wrapped, times(1)).loadObject(any(), any(), any()); assertEquals(1L, actual); } } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/exceptions/HttpStatusExceptionTest.java b/elide-core/src/test/java/com/yahoo/elide/core/exceptions/HttpStatusExceptionTest.java index a11a744c1d..29102d6dc9 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/exceptions/HttpStatusExceptionTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/exceptions/HttpStatusExceptionTest.java @@ -15,57 +15,39 @@ public class HttpStatusExceptionTest { - @Test - public void testGetResponse() { - // result should not be encoded - String expected = "{\"errors\":[\": test