diff --git a/.github/workflows/verify_on_pr.yml b/.github/workflows/verify_on_pr.yml new file mode 100644 index 000000000..ffa7a2b61 --- /dev/null +++ b/.github/workflows/verify_on_pr.yml @@ -0,0 +1,29 @@ +name: 'verify PR' + +on: pull_request + +jobs: + verify: + runs-on: ubuntu-latest + if: github.repository == 'agrestio/agrest' + + name: JDK ${{ matrix.java }} + + strategy: + matrix: + java: [ "11", "17", "21" ] + fail-fast: true + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + cache: 'maven' + + - name: Build and test... + run: mvn clean verify \ No newline at end of file diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 57414d6ff..99ab49cf9 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,6 +1,94 @@ +## Release 5.0-M20 + +* #651 Exp: numeric scalars to support "_" +* #655 Exp syntax: deprecate case-insensitivity of NULL and booleans +* #658 DeleteBuilder: byIds(..), byMultiIds(..) +* #659 Idempotent DELETE by ids, optimizing performance +* #660 Builders: auto-detect multi-column IDs +* #662 Update expression parser with JavaCC 7.0.13 + +## Release 5.0.M19 + +* #648 NPE in CayenneExpParser for a combination of null and positional parameter +* #649 AgExpression internal cleanup + +## Release 5.0.M18 + +* #602 Expressions - immutable parameter binding +* #603 Expressions: compact and/or chains +* #638 EntityUpdate.getToMany(String) should return null if the key is not a part of update +* #640 DataResponse to support "304 Not Modified" +* #641 Cayenne IPathResolver may get "poisoned" by per-request overlayed AgEntities +* #642 TokenMgrException is not handled by Agrest +* #643 Static factory methods in Exp for conditions +* #646 org.apache.cayenne.exp.ExpressionException: [v.4.2 May 16 2023 08:31:12] In: invalid parent - Equal + +## Release 5.0.M17 + +* #630 POST/PUT breaks for Short and BigInteger types +* #631 POST/PUT - can't update multi-key relationships +* #632 POST/PUT gets confused when relationship is passed as object +* #633 Optimize and clean up update parser +* #634 Recursive EntityUpdate parser +* #635 Write constraint only applied to "id" in the path, but not in the body +* #636 Clean up the API of EntityUpdate + +## Release 5.0.M16 + +* #601 Exp.keyValue() only works with a narrow range of value types +* #621 Problem with timestamp formatting +* #622 Upgrade Jackson to 2.14.2 +* #624 Optimization: "relatedViaQueryWithParentIds" strategy when limit is involved +* #629 Upgrade to Cayenne 4.2 GA + +## Release 5.0.M15 + +* #614 Improve support for Java records as Agrest entities +* #618 Expand the definition of "getter" and "setter" +* #619 Swagger: do not expose UriInfo unless annotated with @Parameter + +## Release 5.0.M14 + +* #613 Exception on include when entity id is unreadable +* #615 Swagger: ignore UriInfo if @Parameter(hidden=true) is set +* #616 Swagger: duplicate param properties +* #617 Proper handling of ObjectId annotated with @AgId + +## Release 5.0.M13 + +* #607 OpenAPI docs for EntityUpdate should include relationships +* #610 OpenAPI descriptors should be able to resolve multi-column AgId +* #611 OpenAPI: properties must be sorted in same order as responses +* #612 OpenAPI descriptors - exclude inaccessible properties + +## Release 5.0.M12 + +* #606 Jackson upgrade, dependency management +* #608 "Path exceeds the max allowed depth" exception for attribute sort and 0 depth +* #609 EntityUpdate-style endpoints behave differently from String and List + +## Release 5.0.M11 + +* #604 Allow to exclude null properties in responses +* #605 Hide deprecated protocol keys from OpenAPI descriptors + +## Release 5.0.M10 + +* #582 AgRest own full-featured expression parser +* #594 Support for escape char in "like" expressions +* #597 Upgrade Cayenne to 4.2.RC2 +* #598 Max path depth for "Include" +* #600 Max path depth for "mapBy", "sort" + +## Release 5.0.M9 + +* #590 Can't use inherited relationships in includes + ## Release 5.0.M8 -* + * #583 Upgrade jackson-databind to 2.13.4.2 +* #584 GET - Inheritance support - Cayenne backend +* #587 Inheritance-aware AgEntityOverlay ## Release 5.0.M7 diff --git a/UPGRADE-NOTES.md b/UPGRADE-NOTES.md index 97c5907a9..4eccd22e4 100644 --- a/UPGRADE-NOTES.md +++ b/UPGRADE-NOTES.md @@ -1,6 +1,42 @@ _This document contains upgrade notes for Agrest 5.x and newer. Older versions are documented in [UPGRADE-NOTES-1-4](./UPGRADE-NOTES-1-to-4.md)._ +## Upgrading to 5.0.M17 + +### No more single-value syntax for updating to-many relationships [#633](https://github.com/agrestio/agrest/issues/633) +A number of changes were implemented to improve consistency and functionality of the update pipeline. As a result +single value syntax for updating to-many relationships is no longer supported as it is semantically invalid. E.g. the +following won't work anymore: `{"toMany":1}` -> `{"toMany":[1]}`, and the value should be replaced with an array. + +### New EntityUpdate API, no direct update's Maps modification [#636](https://github.com/agrestio/agrest/issues/636) +EntityUpdate object API was almost completely redone to better track different update components. If you used that API +directly, you will need to update your code. An important part of the change is that property maps are no longer +directly editable. Instead, you should be using the new mutator methods. So instead of `update.getAttributes().put("a", 1)` +your would need to call `update.setAttribute("a", 1)`, etc. + +## Upgrading to 5.0.M16 + +### Got rid of custom date/time formatters [#621](https://github.com/agrestio/agrest/issues/621) + +To fix formatting issues of the older dates (19th century and earlier), date/time parsing and encoding code was +refactored. Instead of a set of custom DateTimeFormatters, we are using the standard ISO formatters from the JDK. +This introduced slight behavior changes (most are actually desirable in any sane codebase) : + +* No more arbitrary truncation of time precision. E.g. a time with 1ns would render as `00:00:00.000000001`, where it would +previously render as `00:00:00` +* If a property is modeled as the old `java.sql.Time`, we will no longer allow to parse times starting with "T". So +`T00:00:00` will no longer work, while `00:00:00` will. This does not affect parsing of `java.time.LocalTime`, as it +already disallowed the leading "T". + +## Upgrading to 5.0.M15 + +### To include UriInfo in OpenAPI, an explicit annotation is required [#619](https://github.com/agrestio/agrest/issues/619) +This only affects projects that auto-generate OpenAPI documentation descriptors from Agrest endpoints. +Previously, the framework would automatically add every single Ag Protocol parameter to the endpoint signature +if an endpoint method had `@Context UriInfo` as one of the parameters. This proved to be rather inflexible +in many situations. So now by default `UriInfo` will be ignored for documentation purposes, and the way to +turn on the old behavior is to annotate it explicitly like this: `@Parameter UriInfo` (with or without `@Context`). + ## Upgrading to 5.0.M1 5.0 is a major release with a number of breaking changes. Still the upgrade should be fairly straightforward in most diff --git a/agrest-annotations/pom.xml b/agrest-annotations/pom.xml index 8d4a19fad..79463eede 100644 --- a/agrest-annotations/pom.xml +++ b/agrest-annotations/pom.xml @@ -5,10 +5,11 @@ io.agrest agrest-parent - 5.0.M8-SNAPSHOT + 5.0-SNAPSHOT agrest-annotations + 5.0-SNAPSHOT jar agrest-annotations: model annotations diff --git a/agrest-annotations/src/main/java/io/agrest/annotation/AgAttribute.java b/agrest-annotations/src/main/java/io/agrest/annotation/AgAttribute.java index 1cbc8f291..d78879b75 100644 --- a/agrest-annotations/src/main/java/io/agrest/annotation/AgAttribute.java +++ b/agrest-annotations/src/main/java/io/agrest/annotation/AgAttribute.java @@ -7,7 +7,8 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Annotates a getter of a property in a Java object to indicate that the property is an attribute exposed via Agrest. + * Annotates a getter of a property in a POJO or a record component to indicate that the property is an "attribute" + * in the Agrest schema, and set it default access rules. * * @since 1.15 */ diff --git a/agrest-annotations/src/main/java/io/agrest/annotation/AgId.java b/agrest-annotations/src/main/java/io/agrest/annotation/AgId.java index 8ab514785..c291761e3 100644 --- a/agrest-annotations/src/main/java/io/agrest/annotation/AgId.java +++ b/agrest-annotations/src/main/java/io/agrest/annotation/AgId.java @@ -7,8 +7,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Annotates a getter of a property in an object to indicate that it is the entity "id". Multiple methods can be - * annotated with "@AgId" in a given entity if a unique identifier of the object id is made of multiple properties. + * Annotates a getter of a property in a POJO or a record component to indicate that the property is a part of an entity + * "id" in the Agrest schema, and set it default access rules. Multiple properties can be annotated with "@AgId" in a + * given entity if a unique identifier of the object id is made of multiple properties. * * @since 1.15 */ diff --git a/agrest-annotations/src/main/java/io/agrest/annotation/AgRelationship.java b/agrest-annotations/src/main/java/io/agrest/annotation/AgRelationship.java index b6f8d885e..6f074be4e 100644 --- a/agrest-annotations/src/main/java/io/agrest/annotation/AgRelationship.java +++ b/agrest-annotations/src/main/java/io/agrest/annotation/AgRelationship.java @@ -7,9 +7,9 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * Annotates a getter of a property in a POJO to indicate that the property is a - * Agrest-exposed relationship. The property type should be either another - * entity, or a collection of entities. + * Annotates a getter of a property in a POJO or a record component to indicate that the property is a "relationship" + * in the Agrest schema, and set it default access rules. The property type should be either another entity, or a + * collection of entities. * * @since 1.15 */ diff --git a/agrest-bom/pom.xml b/agrest-bom/pom.xml index a94999f21..0d4b804ac 100644 --- a/agrest-bom/pom.xml +++ b/agrest-bom/pom.xml @@ -6,7 +6,7 @@ (e.g. "bootique-bom"). As a result have to duplicate certain distribution-related sections. --> io.agrest - 5.0.M8-SNAPSHOT + 5.0-SNAPSHOT agrest-bom pom agrest-bom: Bill of Materials w/all public modules diff --git a/agrest-cayenne/pom.xml b/agrest-cayenne/pom.xml index 3a9c17b31..0816de9bd 100644 --- a/agrest-cayenne/pom.xml +++ b/agrest-cayenne/pom.xml @@ -5,10 +5,11 @@ io.agrest agrest-parent - 5.0.M8-SNAPSHOT + 5.0-SNAPSHOT agrest-cayenne + 5.0-SNAPSHOT agrest-cayenne: Cayenne backend for Agrest Cayenne backend for Agrest @@ -44,13 +45,13 @@ io.agrest - agrest-jaxrs2 + agrest-jaxrs3 ${project.version} test io.agrest - agrest-jaxrs2 + agrest-jaxrs3 ${project.version} test-jar test @@ -67,12 +68,12 @@ io.bootique.jetty - bootique-jetty-junit5 + bootique-jetty-jakarta-junit5 test io.bootique.jersey - bootique-jersey + bootique-jersey-jakarta test diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/AgCayenneModule.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/AgCayenneModule.java index 9bb81e9b3..0071d1236 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/AgCayenneModule.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/AgCayenneModule.java @@ -42,7 +42,7 @@ import io.agrest.runtime.processor.delete.stage.DeleteStartStage; import io.agrest.runtime.processor.select.stage.SelectApplyServerParamsStage; import io.agrest.runtime.processor.unrelate.stage.UnrelateStartStage; -import io.agrest.runtime.processor.unrelate.stage.UnrelateUpdateDateStoreStage; +import io.agrest.runtime.processor.unrelate.stage.UnrelateUpdateDataStoreStage; import io.agrest.runtime.processor.update.UpdateFlavorDIKeys; import io.agrest.runtime.processor.update.stage.UpdateApplyServerParamsStage; import io.agrest.runtime.processor.update.stage.UpdateCommitStage; @@ -144,7 +144,7 @@ public void configure(Binder binder) { // Cayenne overrides for unrelate stages binder.bind(UnrelateStartStage.class).to(CayenneUnrelateStartStage.class); - binder.bind(UnrelateUpdateDateStoreStage.class).to(CayenneUnrelateDataStoreStage.class); + binder.bind(UnrelateUpdateDataStoreStage.class).to(CayenneUnrelateDataStoreStage.class); } public static class Builder { diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/compiler/CayenneAgEntityBuilder.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/compiler/CayenneAgEntityBuilder.java index 9da9c801d..6077c7e66 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/compiler/CayenneAgEntityBuilder.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/compiler/CayenneAgEntityBuilder.java @@ -1,5 +1,6 @@ package io.agrest.cayenne.compiler; +import io.agrest.AgException; import io.agrest.PathConstants; import io.agrest.access.CreateAuthorizer; import io.agrest.access.DeleteAuthorizer; @@ -7,11 +8,11 @@ import io.agrest.access.UpdateAuthorizer; import io.agrest.compiler.AnnotationsAgEntityBuilder; import io.agrest.meta.AgAttribute; -import io.agrest.meta.AgSchema; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.agrest.meta.AgIdPart; import io.agrest.meta.AgRelationship; +import io.agrest.meta.AgSchema; import io.agrest.meta.DefaultAttribute; import io.agrest.meta.DefaultEntity; import io.agrest.meta.DefaultIdPart; @@ -19,18 +20,26 @@ import io.agrest.resolver.RelatedDataResolver; import io.agrest.resolver.RootDataResolver; import io.agrest.resolver.ThrowingRootDataResolver; +import org.apache.cayenne.ObjectId; import org.apache.cayenne.dba.TypesMapping; import org.apache.cayenne.exp.parser.ASTDbPath; import org.apache.cayenne.map.DbAttribute; +import org.apache.cayenne.map.EntityInheritanceTree; import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.map.ObjRelationship; +import org.apache.cayenne.reflect.ClassDescriptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.stream.Collectors; import static io.agrest.reflect.Types.typeForName; @@ -45,11 +54,12 @@ public class CayenneAgEntityBuilder { private final Class type; private final AgSchema schema; private final ObjEntity cayenneEntity; + private final Collection> subEntities; private final Map ids; private final Map attributes; private final Map relationships; - private AgEntityOverlay overlay; + private Map> overlays; private RootDataResolver dataResolver; private RelatedDataResolver relatedDataResolver; @@ -61,13 +71,15 @@ public CayenneAgEntityBuilder(Class type, AgSchema schema, EntityResolver cay this.schema = schema; this.cayenneEntity = cayenneResolver.getObjEntity(type); + this.subEntities = new HashSet<>(); + this.ids = new HashMap<>(); this.attributes = new HashMap<>(); this.relationships = new HashMap<>(); } - public CayenneAgEntityBuilder overlay(AgEntityOverlay overlay) { - this.overlay = overlay; + public CayenneAgEntityBuilder overlays(Map> overlays) { + this.overlays = overlays; return this; } @@ -105,6 +117,17 @@ private AgRelationship addRelationship(AgRelationship r) { protected void buildCayenneEntity() { + EntityInheritanceTree inheritanceTree = cayenneResolver.getInheritanceTree(cayenneEntity.getName()); + for (ObjEntity cayenneSubEntity : inheritanceTree.allSubEntities()) { + + // include only the direct sub-entities + if (cayenneSubEntity.getSuperEntity() == cayenneEntity) { + ClassDescriptor subEntityDesc = cayenneResolver.getClassDescriptor(cayenneSubEntity.getName()); + AgEntity subEntity = (AgEntity) schema.getEntity(subEntityDesc.getObjectClass()); + subEntities.add(subEntity); + } + } + for (ObjAttribute a : cayenneEntity.getAttributes()) { // check for naming conflicts with Agrest "id" @@ -186,20 +209,21 @@ protected void buildAnnotatedProperties() { // Load a separate entity built purely from annotations, then merge it with our entity. AgEntity annotatedEntity = new AnnotationsAgEntityBuilder<>(type, schema).build(); + Collection annotatedIdParts = annotatedEntity.getIdParts(); - if (annotatedEntity.getIdParts().size() > 0) { - - // if annotated ids are present, remove all Cayenne-originated ids, regardless of their type and count - if (!ids.isEmpty()) { - LOGGER.debug("Cayenne ObjectId is overridden from annotations."); - ids.clear(); - } - - // TODO: smarter handling of overridden Cayenne IDs the same way for we do for attributes and relationships + if (!annotatedIdParts.isEmpty()) { // TODO: we should remove a possible matching regular persistent attributes from the model, since it was // also declared as ID, and hence should not be exposed as an attribute anymore - annotatedEntity.getIdParts().forEach(this::addId); + + switch (IdMergeType.of(cayenneEntity.getName(), annotatedIdParts)) { + case replace: + replaceIds(annotatedIdParts); + break; + case merge_single: + mergeSingleId(annotatedIdParts.iterator().next()); + break; + } } for (AgAttribute attribute : annotatedEntity.getAttributes()) { @@ -224,6 +248,37 @@ protected void buildAnnotatedProperties() { } } + protected void replaceIds(Collection annotatedIds) { + ids.clear(); + annotatedIds.forEach(this::addId); + } + + protected void mergeSingleId(AgIdPart annotatedId) { + + if (this.ids.isEmpty()) { + return; + } + + // merge read/write access to the existing IDs + // we can't iterate and change the ids collection. So iterate by copy + for (AgIdPart id : new ArrayList<>(this.ids.values())) { + addId(merge(annotatedId, id)); + } + } + + protected AgIdPart merge(AgIdPart annotatedId, AgIdPart cayenneId) { + // TODO: use Overlays here.. Overlays are intended for merging on top of entities + return new DefaultIdPart( + cayenneId.getName(), + cayenneId.getType(), + + // only read/write access can be overridden by annotations as of now + annotatedId.isReadable(), + annotatedId.isWritable(), + + cayenneId.getDataReader()); + } + protected AgAttribute merge(AgAttribute annotatedAttribute, AgAttribute cayenneAttribute) { // TODO: use Overlays here.. Overlays are intended for merging on top of entities return new DefaultAttribute( @@ -262,6 +317,8 @@ protected AgEntity buildEntity() { return new DefaultEntity<>( cayenneEntity.getName(), type, + cayenneEntity.isAbstract(), + subEntities, ids, attributes, relationships, @@ -276,6 +333,58 @@ protected AgEntity buildEntity() { * @since 4.8 */ protected AgEntity applyOverlay(AgEntity entity) { - return overlay != null ? overlay.resolve(schema, entity) : entity; + return entity.resolveOverlayHierarchy(schema, entityOverlayMap()); + } + + private Map, AgEntityOverlay> entityOverlayMap() { + // TODO: inefficiency - Cayenne DI MapBuilder only supports String keys, so we convert a Class to a String + // in the runtime builder, and then convert the String back to the Class here + + if (overlays == null || overlays.isEmpty()) { + return Collections.emptyMap(); + } + + Map, AgEntityOverlay> byClass = new HashMap<>(); + + AgEntityOverlay rootOverlay = overlays.get(type.getName()); + if (rootOverlay != null) { + byClass.put(type, rootOverlay); + } + + for (AgEntity se : subEntities) { + AgEntityOverlay seOverlay = overlays.get(se.getType().getName()); + if (seOverlay != null) { + byClass.put(seOverlay.getType(), seOverlay); + } + } + + return byClass; + } + + enum IdMergeType { + replace, merge_single; + + static IdMergeType of(String entityName, Collection annotatedIds) { + + if (annotatedIds.size() == 1) { + AgIdPart part = annotatedIds.iterator().next(); + return isCayenneObjectId(part) ? merge_single : replace; + } + + for (AgIdPart p : annotatedIds) { + if (isCayenneObjectId(p)) { + throw AgException.internalServerError( + "Entity '%s': unsupported combination of @AgId-annotated properties - %s (both 'objectId' and custom properties)", + entityName, + annotatedIds.stream().map(AgIdPart::getName).collect(Collectors.joining(","))); + } + } + + return replace; + } + + static boolean isCayenneObjectId(AgIdPart part) { + return "objectId".equals(part.getName()) && ObjectId.class.equals(part.getType()); + } } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/compiler/CayenneAgEntityCompiler.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/compiler/CayenneAgEntityCompiler.java index 939363b15..be360725f 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/compiler/CayenneAgEntityCompiler.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/compiler/CayenneAgEntityCompiler.java @@ -29,14 +29,14 @@ public class CayenneAgEntityCompiler implements AgEntityCompiler { private static final Logger LOGGER = LoggerFactory.getLogger(CayenneAgEntityCompiler.class); private final EntityResolver cayenneEntityResolver; - private final Map entityOverlays; + private final Map> entityOverlays; private final RootDataResolver defaultRootResolver; private final RelatedDataResolver defaultRelatedDataResolver; public CayenneAgEntityCompiler( @Inject ICayennePersister cayennePersister, @Inject ICayenneQueryAssembler queryAssembler, - @Inject Map entityOverlays) { + @Inject Map> entityOverlays) { this.cayenneEntityResolver = cayennePersister.entityResolver(); this.entityOverlays = entityOverlays; @@ -73,10 +73,11 @@ protected RelatedDataResolver createDefaultRelatedResolver( private AgEntity doCompile(Class type, AgSchema schema) { LOGGER.debug("compiling Cayenne entity for type: {}", type); + return new CayenneAgEntityBuilder<>(type, schema, cayenneEntityResolver) - .overlay(entityOverlays.get(type.getName())) - .dataResolver(defaultRootResolver) - .relatedDataResolver(defaultRelatedDataResolver) + .overlays(entityOverlays) + .dataResolver((RootDataResolver) defaultRootResolver) + .relatedDataResolver((RelatedDataResolver) defaultRelatedDataResolver) .build(); } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/converter/valuestring/JsonConverter.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/converter/valuestring/JsonConverter.java index 555fd5137..77ef6bbef 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/converter/valuestring/JsonConverter.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/converter/valuestring/JsonConverter.java @@ -1,17 +1,16 @@ package io.agrest.cayenne.converter.valuestring; import io.agrest.converter.valuestring.AbstractConverter; -import io.agrest.converter.valuestring.ValueStringConverter; import org.apache.cayenne.value.Json; /** * @since 5.0 */ -public class JsonConverter extends AbstractConverter { +public class JsonConverter extends AbstractConverter { - private static final ValueStringConverter instance = new JsonConverter(); + private static final JsonConverter instance = new JsonConverter(); - public static ValueStringConverter converter() { + public static JsonConverter converter() { return instance; } @@ -19,8 +18,7 @@ private JsonConverter() { } @Override - protected String asStringNonNull(Object object) { - Json json = (Json) object; + protected String asStringNonNull(Json json) { return json.getRawJson(); } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/encoder/JsonEncoder.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/encoder/JsonEncoder.java index 081fa6510..0701e1f43 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/encoder/JsonEncoder.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/encoder/JsonEncoder.java @@ -22,7 +22,7 @@ private JsonEncoder() { } @Override - protected void encodeNonNullObject(Object object, JsonGenerator out) throws IOException { + protected void encodeNonNullObject(Object object, boolean skipNullProperties, JsonGenerator out) throws IOException { Json json = (Json) object; // note that we can't use ValueEncoder for rg.apache.cayenne.value.Json, as it needs to write a raw value diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpParser.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpParser.java index e06ec3413..0c5b51d98 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpParser.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpParser.java @@ -1,21 +1,7 @@ package io.agrest.cayenne.exp; -import io.agrest.AgException; import io.agrest.protocol.Exp; -import io.agrest.exp.CompositeExp; -import io.agrest.exp.ExpVisitor; -import io.agrest.exp.KeyValueExp; -import io.agrest.exp.NamedParamsExp; -import io.agrest.exp.PositionalParamsExp; -import io.agrest.exp.SimpleExp; -import io.agrest.cayenne.path.PathOps; import org.apache.cayenne.exp.Expression; -import org.apache.cayenne.exp.ExpressionFactory; -import org.apache.cayenne.exp.parser.ASTPath; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; /** * @since 3.3 @@ -23,110 +9,12 @@ public class CayenneExpParser implements ICayenneExpParser { @Override - public Expression parse(Exp qualifier) { - - if (qualifier == null) { + public Expression parse(Exp exp) { + if (exp == null) { return null; } - List stack = new ArrayList<>(); - - qualifier.visit(new ExpVisitor() { - - @Override - public void visitSimpleExp(SimpleExp exp) { - stack.add(ExpressionFactory.exp(exp.getTemplate())); - } - - @Override - public void visitNamedParamsExp(NamedParamsExp exp) { - stack.add(ExpressionFactory.exp(exp.getTemplate()).params(exp.getParams())); - } - - @Override - public void visitPositionalParamsExp(PositionalParamsExp exp) { - stack.add(ExpressionFactory.exp(exp.getTemplate(), exp.getParams())); - } - - @Override - public void visitKeyValueExp(KeyValueExp exp) { - stack.add(parseKeyValueExpression(exp.getKey(), exp.getOp(), exp.getValue())); - } - - @Override - public void visitCompositeExp(CompositeExp exp) { - - Exp[] children = exp.getParts(); - Expression[] parsedChildren = new Expression[children.length]; - - // here the stack would become temporarily inconsistent (contain children without the parent) - // Suppose it is benign, as we are controlling eh walk, still worse mentioning - for (int i = 0; i < children.length; i++) { - children[i].visit(this); - parsedChildren[i] = stack.remove(stack.size() - 1); - } - - switch (exp.getCombineOperand()) { - case CompositeExp.AND: - stack.add(ExpressionFactory.and(parsedChildren)); - break; - case CompositeExp.OR: - stack.add(ExpressionFactory.or(parsedChildren)); - break; - default: - throw new IllegalStateException("Unknown combine operand: " + exp.getCombineOperand()); - } - } - }); - - return stack.isEmpty() ? null : stack.get(0); - } - - static Expression parseKeyValueExpression(String key, String op, Object value) { - - ASTPath path = PathOps.parsePath(key); - - switch (op) { - case "=": - return ExpressionFactory.matchExp(path, value); - case "<": - return ExpressionFactory.lessExp(path, value); - case ">": - return ExpressionFactory.greaterExp(path, value); - case "<=": - return ExpressionFactory.lessOrEqualExp(path, value); - case ">=": - return ExpressionFactory.greaterOrEqualExp(path, value); - case "like": - return ExpressionFactory.likeExp(path, value); - case "likeIgnoreCase": - return ExpressionFactory.likeIgnoreCaseExp(path, value); - case "in": - return ExpressionFactory.inExp(path, inValues(value)); - default: - throw new IllegalArgumentException("Unsupported operation in Expression: " + op); - } - } - - static Object[] inValues(Object value) { - if (value == null) { - return new Object[]{null}; - } - - if (value instanceof Collection) { - return ((Collection) value).toArray(new Object[0]); - } - - if (value.getClass().isArray()) { - Class componentType = value.getClass().getComponentType(); - if (componentType.isPrimitive()) { - // TODO: this limitation is arbitrary. Wrap primitives to objects - throw AgException.internalServerError("Primitive array is not supported as an 'in' exp parameter"); - } else { - return (Object[]) value; - } - } - - return new Object[]{value}; + CayenneExpressionVisitor visitor = new CayenneExpressionVisitor(); + return exp.accept(visitor, null); } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpPostProcessor.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpPostProcessor.java index a6a88f0d8..6defef97d 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpPostProcessor.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpPostProcessor.java @@ -3,15 +3,24 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.TextNode; import io.agrest.AgException; -import io.agrest.converter.jsonvalue.JsonValueConverter; -import io.agrest.converter.jsonvalue.UtcDateConverter; -import io.agrest.meta.AgEntity; import io.agrest.cayenne.path.IPathResolver; import io.agrest.cayenne.path.PathDescriptor; +import io.agrest.cayenne.persister.ICayennePersister; +import io.agrest.converter.jsonvalue.JsonValueConverter; +import io.agrest.converter.jsonvalue.SqlDateConverter; +import io.agrest.converter.jsonvalue.SqlTimeConverter; +import io.agrest.converter.jsonvalue.SqlTimestampConverter; +import io.agrest.converter.jsonvalue.UtilDateConverter; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.TraversalHelper; -import org.apache.cayenne.exp.parser.*; +import org.apache.cayenne.exp.parser.ASTDbPath; +import org.apache.cayenne.exp.parser.ASTObjPath; +import org.apache.cayenne.exp.parser.ASTPath; +import org.apache.cayenne.exp.parser.ConditionNode; +import org.apache.cayenne.exp.parser.SimpleNode; +import org.apache.cayenne.map.EntityResolver; +import org.apache.cayenne.map.ObjEntity; import java.util.Date; import java.util.HashMap; @@ -20,33 +29,37 @@ public class CayenneExpPostProcessor implements ICayenneExpPostProcessor { - private IPathResolver pathCache; - private Map, JsonValueConverter> converters; - private Map, ExpressionProcessor> postProcessors; + private final EntityResolver entityResolver; + private final IPathResolver pathCache; + private final Map> converters; + private final Map postProcessors; + + public CayenneExpPostProcessor( + @Inject IPathResolver pathCache, + @Inject ICayennePersister persister) { - public CayenneExpPostProcessor(@Inject IPathResolver pathCache) { this.pathCache = pathCache; + this.entityResolver = persister.entityResolver(); // TODO: instead of manually assembling converters we must switch to // IJsonValueConverterFactory already used by DataObjectProcessor. // The tricky part is the "id" attribute that is converted to DbPath // , so its type can not be mapped with existing tools - Map, JsonValueConverter> converters = new HashMap<>(); - converters.put(Date.class, UtcDateConverter.converter()); - converters.put(java.sql.Date.class, UtcDateConverter.converter()); - converters.put(java.sql.Time.class, UtcDateConverter.converter()); - converters.put(java.sql.Timestamp.class, UtcDateConverter.converter()); - this.converters = converters; + this.converters = new HashMap<>(); + converters.put(Date.class.getName(), UtilDateConverter.converter()); + converters.put(java.sql.Date.class.getName(), SqlDateConverter.converter()); + converters.put(java.sql.Time.class.getName(), SqlTimeConverter.converter()); + converters.put(java.sql.Timestamp.class.getName(), SqlTimestampConverter.converter()); postProcessors = new ConcurrentHashMap<>(); } @Override - public Expression process(AgEntity entity, Expression exp) { - return (exp == null) ? null : validateAndCleanup(entity, exp); + public Expression process(String entityName, Expression exp) { + return exp == null ? null : validateAndCleanup(entityResolver.getObjEntity(entityName), exp); } - private Expression validateAndCleanup(AgEntity entity, Expression exp) { + private Expression validateAndCleanup(ObjEntity entity, Expression exp) { // change expression in-place // note - this will not fully handle an expression whose root is @@ -57,21 +70,21 @@ private Expression validateAndCleanup(AgEntity entity, Expression exp) { // 'expressionPostProcessor'. If it happens to be "id", it will be // converted to "db:id". if (exp instanceof ASTObjPath) { - exp = pathCache.resolve(entity, ((ASTObjPath) exp).getPath()).getPathExp(); + exp = pathCache.resolve(entity.getName(), ((ASTObjPath) exp).getPath()).getPathExp(); } return exp; } - private ExpressionProcessor getOrCreateExpressionProcessor(AgEntity entity) { - return postProcessors.computeIfAbsent(entity, e -> new ExpressionProcessor(e)); + private ExpressionProcessor getOrCreateExpressionProcessor(ObjEntity entity) { + return postProcessors.computeIfAbsent(entity.getName(), e -> new ExpressionProcessor(entity)); } private class ExpressionProcessor extends TraversalHelper { - private AgEntity entity; + private final ObjEntity entity; - ExpressionProcessor(AgEntity entity) { + ExpressionProcessor(ObjEntity entity) { this.entity = entity; } @@ -95,7 +108,7 @@ public void finishedChild(Expression parentNode, int childIndex, boolean hasMore // validate and replace if needed ... note that we can only // replace non-root nodes during the traversal. Root node is // validated and replaced explicitly by the caller. - ASTPath replacement = pathCache.resolve(entity, ((ASTObjPath) childNode).getPath()).getPathExp(); + ASTPath replacement = pathCache.resolve(entity.getName(), ((ASTObjPath) childNode).getPath()).getPathExp(); if (replacement != childNode) { parentNode.setOperand(childIndex, replacement); } @@ -136,7 +149,7 @@ private Object convert(SimpleNode parentExp, JsonNode node) { if (peerPath != null) { - PathDescriptor pd = pathCache.resolve(entity, peerPath); + PathDescriptor pd = pathCache.resolve(entity.getName(), peerPath); if (pd.isAttributeOrId()) { JsonValueConverter converter = converters.get(pd.getType()); if (converter != null) { diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpressionVisitor.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpressionVisitor.java new file mode 100644 index 000000000..f0720c906 --- /dev/null +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/CayenneExpressionVisitor.java @@ -0,0 +1,354 @@ +package io.agrest.cayenne.exp; + +import io.agrest.cayenne.path.PathOps; +import io.agrest.exp.parser.SimpleNode; +import io.agrest.exp.parser.*; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionParameter; +import org.apache.cayenne.exp.parser.*; + +import java.lang.reflect.Constructor; +import java.util.Collection; +import java.util.function.BiFunction; + +class CayenneExpressionVisitor implements AgExpressionParserVisitor { + + @Override + public Expression visit(SimpleNode node, Expression data) { + return node.jjtAccept(this, data); + } + + @Override + public Expression visit(ExpOr node, Expression parent) { + return process(node, parent, new ASTOr()); + } + + @Override + public Expression visit(ExpAnd node, Expression data) { + return process(node, data, new ASTAnd()); + } + + @Override + public Expression visit(ExpNot node, Expression data) { + return process(node, data, new ASTNot()); + } + + @Override + public Expression visit(ExpTrue node, Expression data) { + return process(node, data, new ASTTrue()); + } + + @Override + public Expression visit(ExpFalse node, Expression data) { + return process(node, data, new ASTFalse()); + } + + @Override + public Expression visit(ExpEqual node, Expression parent) { + return process(node, parent, new ASTEqual()); + } + + @Override + public Expression visit(ExpNotEqual node, Expression data) { + return process(node, data, new ASTNotEqual()); + } + + @Override + public Expression visit(ExpLessOrEqual node, Expression data) { + return process(node, data, new ASTLessOrEqual()); + } + + @Override + public Expression visit(ExpLess node, Expression data) { + return process(node, data, new ASTLess()); + } + + @Override + public Expression visit(ExpGreater node, Expression data) { + return process(node, data, new ASTGreater()); + } + + @Override + public Expression visit(ExpGreaterOrEqual node, Expression data) { + return process(node, data, new ASTGreaterOrEqual()); + } + + @Override + public Expression visit(ExpLike node, Expression data) { + return process(node, data, new ASTLike()); + } + + @Override + public Expression visit(ExpLikeIgnoreCase node, Expression data) { + return process(node, data, new ASTLikeIgnoreCase()); + } + + @Override + public Expression visit(ExpIn node, Expression data) { + return process(node, data, new ASTIn()); + } + + @Override + public Expression visit(ExpBetween node, Expression data) { + return process(node, data, new ASTBetween()); + } + + @Override + public Expression visit(ExpNotLike node, Expression data) { + return process(node, data, new ASTNotLike()); + } + + @Override + public Expression visit(ExpNotLikeIgnoreCase node, Expression data) { + return process(node, data, new ASTNotLikeIgnoreCase()); + } + + @Override + public Expression visit(ExpNotIn node, Expression data) { + return process(node, data, new ASTNotIn()); + } + + @Override + public Expression visit(ExpNotBetween node, Expression data) { + return process(node, data, new ASTNotBetween()); + } + + @Override + public Expression visit(ExpScalarList node, Expression data) { + // NOTE: we are skipping all the List children as they are processed by the getValue() call + Collection value = node.getValue(); + Object[] cayenneValues = new Object[value.size()]; + int i = 0; + for (Object next : value) { + if (next instanceof ExpNamedParameter) { + cayenneValues[i++] = new ExpressionParameter(((ExpNamedParameter) next).getName()); + } else { + cayenneValues[i++] = next; + } + } + + ASTList list = new ASTList(cayenneValues); + if (data != null) { + data.setOperand(data.getOperandCount(), list); + return data; + } + return list; + } + + @Override + public Expression visit(ExpScalar node, Expression data) { + return process(node, data, new ASTScalar(node.jjtGetValue())); + } + + @Override + public Expression visit(ExpBitwiseOr node, Expression data) { + return process(node, data, new ASTBitwiseOr()); + } + + @Override + public Expression visit(ExpBitwiseXor node, Expression data) { + return process(node, data, new ASTBitwiseXor()); + } + + @Override + public Expression visit(ExpBitwiseAnd node, Expression data) { + return process(node, data, new ASTBitwiseAnd()); + } + + @Override + public Expression visit(ExpBitwiseLeftShift node, Expression data) { + return process(node, data, new ASTBitwiseLeftShift()); + } + + @Override + public Expression visit(ExpBitwiseRightShift node, Expression data) { + return process(node, data, new ASTBitwiseRightShift()); + } + + @Override + public Expression visit(ExpAdd node, Expression data) { + return process(node, data, new ASTAdd()); + } + + @Override + public Expression visit(ExpSubtract node, Expression data) { + return process(node, data, new ASTSubtract()); + } + + @Override + public Expression visit(ExpMultiply node, Expression data) { + return process(node, data, new ASTMultiply()); + } + + @Override + public Expression visit(ExpDivide node, Expression data) { + return process(node, data, new ASTDivide()); + } + + @Override + public Expression visit(ExpBitwiseNot node, Expression data) { + return process(node, data, new ASTBitwiseNot()); + } + + @Override + public Expression visit(ExpNegate node, Expression data) { + return process(node, data, new ASTNegate()); + } + + @Override + public Expression visit(ExpConcat node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTConcat.class)); + } + + @Override + public Expression visit(ExpSubstring node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTSubstring.class)); + } + + @Override + public Expression visit(ExpTrim node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTTrim.class)); + } + + @Override + public Expression visit(ExpLower node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTLower.class)); + } + + @Override + public Expression visit(ExpUpper node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTUpper.class)); + } + + @Override + public Expression visit(ExpLength node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTLength.class)); + } + + @Override + public Expression visit(ExpLocate node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTLocate.class)); + } + + @Override + public Expression visit(ExpAbs node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTAbs.class)); + } + + @Override + public Expression visit(ExpSqrt node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTSqrt.class)); + } + + @Override + public Expression visit(ExpMod node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTMod.class)); + } + + @Override + public Expression visit(ExpCurrentDate node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTCurrentDate.class)); + } + + @Override + public Expression visit(ExpCurrentTime node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTCurrentTime.class)); + } + + @Override + public Expression visit(ExpCurrentTimestamp node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTCurrentTimestamp.class)); + } + + @Override + public Expression visit(ExpExtract node, Expression data) { + // no public constructor that we could use directly + return process(node, data, constructExpression(ASTExtract.class)); + } + + @Override + public Expression visit(ExpNamedParameter node, Expression data) { + return process(node, data, new ASTNamedParameter(node.jjtGetValue())); + } + + @Override + public Expression visit(ExpPath node, Expression parent) { + ASTPath path = PathOps.parsePath((String) node.jjtGetValue()); + return process(node, parent, path); + } + + private Expression process(SimpleNode node, Expression parent, Expression exp) { + if (node.jjtGetNumChildren() > 0) { + for (int i = 0; i < node.jjtGetNumChildren(); ++i) { + exp = node.jjtGetChild(i).jjtAccept(this, exp); + } + } + if (parent != null) { + BiFunction childMerger = getMergerForNode(parent); + return childMerger.apply(parent, exp); + } else { + return exp; + } + } + + BiFunction getMergerForNode(Expression node) { + if (node instanceof PatternMatchNode) { + return this::addToLikeNode; + } else { + return this::addToParent; + } + } + + private Expression addToParent(Expression parent, Expression child) { + parent.setOperand(parent.getOperandCount(), child); + return parent; + } + + private Expression addToLikeNode(Expression parent, Expression child) { + if (!(parent instanceof PatternMatchNode)) { + throw new IllegalArgumentException("ParentMatchNode expected, got " + parent.getClass().getSimpleName()); + } + PatternMatchNode patternMatchNode = (PatternMatchNode) parent; + if (parent.getOperandCount() == 2) { + if (!(child instanceof ASTScalar)) { + throw new IllegalArgumentException("ASTScalar expected, got " + child.getClass().getSimpleName()); + } + String escape = ((ASTScalar) child).getValue().toString(); + if (escape.length() != 1) { + throw new IllegalArgumentException("Single escape char expected, got '" + escape + "'"); + } + patternMatchNode.setEscapeChar(escape.charAt(0)); + return parent; + } else { + return addToParent(parent, child); + } + } + + // A hack - must use reflection to create Cayenne expressions, as the common int constructor is not public + // in any of them. + // TODO: refactor this in Cayenne to provide public constructors + private Expression constructExpression(Class expClass) { + Expression exp; + try { + Constructor constructor = expClass.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + exp = constructor.newInstance(0); + } catch (Exception e) { + throw new RuntimeException(e); + } + return exp; + } +} \ No newline at end of file diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/ICayenneExpParser.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/ICayenneExpParser.java index 6db7f23ae..f7f295601 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/ICayenneExpParser.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/ICayenneExpParser.java @@ -8,5 +8,5 @@ */ public interface ICayenneExpParser { - Expression parse(Exp qualifier); + Expression parse(Exp exp); } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/ICayenneExpPostProcessor.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/ICayenneExpPostProcessor.java index 633262617..e19966d8c 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/ICayenneExpPostProcessor.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/exp/ICayenneExpPostProcessor.java @@ -1,7 +1,6 @@ package io.agrest.cayenne.exp; import io.agrest.AgException; -import io.agrest.meta.AgEntity; import org.apache.cayenne.exp.Expression; /** @@ -15,5 +14,5 @@ public interface ICayenneExpPostProcessor { * @return Expression that is ready for execution * @throws AgException if expression is malformed or violates validation constraints */ - Expression process(AgEntity entity, Expression exp); + Expression process(String entityName, Expression exp); } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/EntityPathCache.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/EntityPathCache.java index 7cd81e3ce..359a38747 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/EntityPathCache.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/EntityPathCache.java @@ -2,21 +2,24 @@ import io.agrest.AgException; import io.agrest.PathConstants; -import io.agrest.meta.AgAttribute; -import io.agrest.meta.AgEntity; -import io.agrest.meta.AgIdPart; -import io.agrest.meta.AgRelationship; +import org.apache.cayenne.dba.TypesMapping; +import org.apache.cayenne.exp.parser.ASTDbPath; import org.apache.cayenne.exp.parser.ASTObjPath; +import org.apache.cayenne.map.DbAttribute; +import org.apache.cayenne.map.ObjAttribute; +import org.apache.cayenne.map.ObjEntity; +import org.apache.cayenne.map.ObjRelationship; +import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; class EntityPathCache { - private final AgEntity entity; + private final ObjEntity entity; private final Map pathCache; - EntityPathCache(AgEntity entity) { + EntityPathCache(ObjEntity entity) { this.entity = entity; this.pathCache = new ConcurrentHashMap<>(); @@ -26,12 +29,14 @@ class EntityPathCache { // TODO: this is a hack - we are treating "id" as a "virtual" attribute, as there's generally no "id" // property in AgEntity. See the same note in EncodablePropertyFactory - if (entity.getIdParts().size() == 1) { + // store "pks" in a var for reuse, as ObjEntity would rebuild them on every call + Collection pks = entity.getPrimaryKeys(); + if (pks.size() == 1) { // TODO: here we are ignoring the name of the ID attribute and are using the fixed name instead. // Same issue as the above - AgIdPart id = entity.getIdParts().iterator().next(); - pathCache.put(PathConstants.ID_PK_ATTRIBUTE, new PathDescriptor(id.getType(), PathOps.parsePath(id.getName()), true)); + ObjAttribute a = pks.iterator().next(); + pathCache.put(PathConstants.ID_PK_ATTRIBUTE, new PathDescriptor(a.getType(), new ASTDbPath(a.getDbAttributePath()), true)); } } @@ -41,26 +46,25 @@ PathDescriptor getOrCreate(String agPath) { private PathDescriptor computePathDescriptor(String agPath) { - final Object last = lastPathComponent(entity, agPath); + Object last = lastPathComponent(entity, agPath); - if (last instanceof AgAttribute) { - return new PathDescriptor(((AgAttribute) last).getType(), new ASTObjPath(agPath), true); + if (last instanceof ObjAttribute) { + ObjAttribute a = (ObjAttribute) last; + return a.isPrimaryKey() + ? new PathDescriptor(a.getType(), new ASTDbPath(a.getDbAttributePath()), true) + : new PathDescriptor(a.getType(), new ASTObjPath(agPath), true); } - if (last instanceof AgIdPart) { - AgIdPart id = (AgIdPart) last; - if (entity.getIdParts().contains(id)) { - return new PathDescriptor(id.getType(), PathOps.parsePath(id.getName()), true); - } else { - return new PathDescriptor(id.getType(), PathOps.parsePath(agPath), true); - } + if (last instanceof DbAttribute) { + DbAttribute a = (DbAttribute) last; + return new PathDescriptor(TypesMapping.getJavaBySqlType(a.getType()), new ASTDbPath(a.getName()), true); } - AgRelationship relationship = (AgRelationship) last; - return new PathDescriptor(relationship.getTargetEntity().getType(), new ASTObjPath(agPath), false); + ObjRelationship relationship = (ObjRelationship) last; + return new PathDescriptor(relationship.getTargetEntity().getClassName(), new ASTObjPath(agPath), false); } - Object lastPathComponent(AgEntity entity, String path) { + Object lastPathComponent(ObjEntity entity, String path) { int dot = path.indexOf(PathConstants.DOT); @@ -75,33 +79,35 @@ Object lastPathComponent(AgEntity entity, String path) { if (dot > 0) { String segment = toRelationshipName(path.substring(0, dot)); - // must be a relationship .. - AgRelationship relationship = entity.getRelationship(segment); + // followed by dot, so must be a relationship + ObjRelationship relationship = entity.getRelationship(segment); if (relationship == null) { throw AgException.badRequest("Invalid path '%s' for '%s'. Not a relationship", path, entity.getName()); } - AgEntity targetEntity = relationship.getTargetEntity(); + ObjEntity targetEntity = relationship.getTargetEntity(); return lastPathComponent(targetEntity, path.substring(dot + 1)); } - AgAttribute attribute = entity.getAttribute(path); + ObjAttribute attribute = entity.getAttribute(path); if (attribute != null) { return attribute; } - AgIdPart idPart = entity.getIdPart(path); - if (idPart != null) { - return idPart; - } - - AgRelationship relationship = entity.getRelationship(toRelationshipName(path)); + ObjRelationship relationship = entity.getRelationship(toRelationshipName(path)); if (relationship != null) { return relationship; } + if (path.startsWith(ASTDbPath.DB_PREFIX)) { + DbAttribute dbAttribute = entity.getDbEntity().getAttribute(path.substring(ASTDbPath.DB_PREFIX.length())); + if (dbAttribute != null) { + return dbAttribute; + } + } + throw AgException.badRequest("Invalid path '%s' for '%s'", path, entity.getName()); } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/IPathResolver.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/IPathResolver.java index a505ecaf4..23b9a75c0 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/IPathResolver.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/IPathResolver.java @@ -1,8 +1,5 @@ package io.agrest.cayenne.path; -import io.agrest.meta.AgEntity; - - /** * Resolves Agrest expression paths to Cayenne paths. * @@ -10,5 +7,5 @@ */ public interface IPathResolver { - PathDescriptor resolve(AgEntity entity, String agPath); + PathDescriptor resolve(String entityName, String agPath); } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathDescriptor.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathDescriptor.java index d68141e19..7c879dc88 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathDescriptor.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathDescriptor.java @@ -9,9 +9,9 @@ public class PathDescriptor { private final boolean attributeOrId; private final ASTPath path; - private final Class type; + private final String type; - public PathDescriptor(Class type, ASTPath path, boolean attributeOrId) { + public PathDescriptor(String type, ASTPath path, boolean attributeOrId) { this.path = path; this.type = type; this.attributeOrId = attributeOrId; @@ -24,7 +24,7 @@ public boolean isAttributeOrId() { return attributeOrId; } - public Class getType() { + public String getType() { return type; } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathOps.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathOps.java index 07709bbba..0b4c34ba9 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathOps.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathOps.java @@ -31,13 +31,13 @@ public static ASTPath parsePath(String path) { /** * @since 5.0 */ - public static ASTDbPath resolveAsDbPath(ObjEntity entity, ASTPath p) { + public static ASTDbPath resolveAsDbPath(ASTPath p) { switch (p.getType()) { case Expression.DB_PATH: return (ASTDbPath) p; case Expression.OBJ_PATH: - return resolveAsDbPath(entity, p); + return resolveAsDbPath(p); default: throw new IllegalArgumentException("Unexpected p type: " + p.getType()); } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathResolver.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathResolver.java index 50f6a37ae..b95b555a4 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathResolver.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/path/PathResolver.java @@ -1,6 +1,8 @@ package io.agrest.cayenne.path; -import io.agrest.meta.AgEntity; +import io.agrest.cayenne.persister.ICayennePersister; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.map.EntityResolver; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -12,21 +14,23 @@ */ public class PathResolver implements IPathResolver { + private final EntityResolver entityResolver; private final Map pathCaches; - public PathResolver() { + public PathResolver(@Inject ICayennePersister persister) { + this.entityResolver = persister.entityResolver(); this.pathCaches = new ConcurrentHashMap<>(); } @Override - public PathDescriptor resolve(AgEntity entity, String agPath) { - return entityPathCache(entity).getOrCreate(agPath); + public PathDescriptor resolve(String entityName, String agPath) { + return entityPathCache(entityName).getOrCreate(agPath); } - EntityPathCache entityPathCache(AgEntity entity) { + EntityPathCache entityPathCache(String entityName) { return pathCaches.computeIfAbsent( - entity.getName(), - k -> new EntityPathCache(entity)); + entityName, + k -> new EntityPathCache(entityResolver.getObjEntity(k))); } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/CayenneQueryAssembler.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/CayenneQueryAssembler.java index ea911c68e..cdf9cbea2 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/CayenneQueryAssembler.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/CayenneQueryAssembler.java @@ -1,8 +1,6 @@ package io.agrest.cayenne.processor; import io.agrest.AgException; -import io.agrest.id.AgObjectId; -import io.agrest.runtime.EntityParent; import io.agrest.RelatedResourceEntity; import io.agrest.ResourceEntity; import io.agrest.RootResourceEntity; @@ -11,10 +9,12 @@ import io.agrest.cayenne.path.IPathResolver; import io.agrest.cayenne.path.PathOps; import io.agrest.cayenne.persister.ICayennePersister; +import io.agrest.id.AgObjectId; import io.agrest.meta.AgEntity; import io.agrest.meta.AgIdPart; import io.agrest.protocol.Direction; import io.agrest.protocol.Sort; +import io.agrest.runtime.EntityParent; import io.agrest.runtime.processor.select.SelectContext; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.Expression; @@ -37,6 +37,7 @@ import java.util.Iterator; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; /** * @since 3.4 @@ -69,7 +70,7 @@ public ObjectSelect createRootQuery(SelectContext context) { EntityParent parent = context.getParent(); if (parent != null) { - query.and(CayenneUtil.parentQualifier(pathResolver, parent, entityResolver)); + query.and(CayenneUtil.parentQualifier(pathResolver, context.getSchema().getEntity(parent.getType()), parent, entityResolver)); } return query; @@ -100,7 +101,7 @@ public Property[] queryColumns(RelatedResourceEntity entity) { AgEntity parentEntity = entity.getParent().getAgEntity(); ObjEntity parentObjEntity = entityResolver.getObjEntity(entity.getParent().getName()); - ObjRelationship objRelationship = parentObjEntity.getRelationship(entity.getIncoming().getName()); + ObjRelationship objRelationship = findRelationship(parentObjEntity, entity.getIncoming().getName()); ASTDbPath reversePath = new ASTDbPath(objRelationship.getReverseDbRelationshipPath()); Property[] columns = new Property[parentEntity.getIdParts().size() + 1]; @@ -111,7 +112,7 @@ public Property[] queryColumns(RelatedResourceEntity entity) { // columns must be added in the order of id parts iteration, as this is how they will be read from result int i = 1; for (AgIdPart idPart : parentEntity.getIdParts()) { - ASTPath idPartPath = pathResolver.resolve(parentEntity, idPart.getName()).getPathExp(); + ASTPath idPartPath = pathResolver.resolve(parentEntity.getName(), idPart.getName()).getPathExp(); Expression propertyExp = PathOps.concatWithDbPath(parentObjEntity, reversePath, idPartPath); columns[i++] = PropertyFactory.createBase(propertyExp, idPart.getType()); } @@ -119,6 +120,82 @@ public Property[] queryColumns(RelatedResourceEntity entity) { return columns; } + @Override + public ObjectSelect createQueryForIds(AgEntity entity, Collection ids) { + + if (ids.isEmpty()) { + throw AgException.badRequest("No ids specified"); + } + + ObjEntity objEntity = entityResolver.getObjEntity(entity.getType()); + + // sanity checking... + if (objEntity == null) { + throw AgException.internalServerError("Unknown entity class: %s", entity.getType()); + } + + Function qualifierMaker = idQualifierMaker(entity); + List idQualifiers = new ArrayList<>(ids.size()); + + // TODO: for single-column IDs, a better qualifier would be "IN (..)" instead of "OR" + for (AgObjectId id : ids) { + idQualifiers.add(qualifierMaker.apply(id)); + } + + return ObjectSelect.query(entity.getType()).where(ExpressionFactory.or(idQualifiers)); + } + + private Function idQualifierMaker(AgEntity entity) { + int idSize = entity.getIdParts().size(); + + if (idSize == 1) { + String partName = entity.getIdParts().iterator().next().getName(); + ASTPath idPath = pathResolver.resolve(entity.getName(), partName).getPathExp(); + return id -> ExpressionFactory.matchExp(idPath, id.get(partName)); + } else { + + List partNames = new ArrayList<>(idSize); + List paths = new ArrayList<>(idSize); + for (AgIdPart idPart : entity.getIdParts()) { + partNames.add(idPart.getName()); + paths.add(pathResolver.resolve(entity.getName(), idPart.getName()).getPathExp()); + } + + return id -> { + List idQualifier = new ArrayList<>(idSize); + for (int i = 0; i < idSize; i++) { + idQualifier.add(ExpressionFactory.matchExp(paths.get(i), id.get(partNames.get(i)))); + } + + return ExpressionFactory.and(idQualifier); + }; + } + } + + private ObjRelationship findRelationship(ObjEntity entity, String name) { + + // Take inheritance into account... ObjRelationship may not be present in the superclass, but the underlying + // DB path is there. So let's find it in subclasses, and resolve the path. + // TODO: this may not work with the future Cayenne horizontal inheritance + + ObjRelationship re = entity.getRelationship(name); + ObjRelationship r = re != null ? re : findRelationshipInHierarchy(entity, name); + if (r == null) { + throw new IllegalStateException("ObjRelationship '" + name + "' is not found in '" + + entity.getName() + "' or in its inheritance hierarchy"); + } + + return r; + } + + private ObjRelationship findRelationshipInHierarchy(ObjEntity parentObjEntity, String name) { + return entityResolver.getInheritanceTree(parentObjEntity.getName()).allSubEntities().stream() + .filter(e -> e != parentObjEntity) + .map(e -> e.getRelationship(name)) + .findFirst() + .orElse(null); + } + // using dbpaths for all expression operations on the theory that some object paths can be unidirectional, and // hence may be missing for some relationships (although all "incoming" relationships along the parents chain // should be present, no?) @@ -141,21 +218,16 @@ protected Expression resolveParentQualifier(RelatedResourceEntity entity, Str return null; } - ObjEntity parentObjEntity = entityResolver.getObjEntity(parent.getType()); - ObjRelationship incoming = parentObjEntity.getRelationship(entity.getIncoming().getName()); - - if (incoming == null) { - throw new IllegalStateException("No such relationship: " + parentObjEntity.getName() + "." + entity.getIncoming().getName()); - } + String incomingPath = dbPath(parent.getType(), entity.getIncoming().getName()); + String fullDbPath = concatWithParentDbPath(incomingPath, outgoingDbPath); - String fullDbPath = concatWithParentDbPath(incoming, outgoingDbPath); + ObjEntity parentObjEntity = entityResolver.getObjEntity(parent.getType()); Expression dbParentQualifier = parentObjEntity.translateToDbPath(parentQualifier); return parentObjEntity.getDbEntity().translateToRelatedEntity(dbParentQualifier, fullDbPath); } - ObjEntity parentObjEntity = entityResolver.getObjEntity(entity.getParent().getType()); - ObjRelationship incoming = parentObjEntity.getRelationship(entity.getIncoming().getName()); - String fullDbPath = concatWithParentDbPath(incoming, outgoingDbPath); + String incomingPath = dbPath(parent.getType(), entity.getIncoming().getName()); + String fullDbPath = concatWithParentDbPath(incomingPath, outgoingDbPath); // shouldn't really happen with any of the current built-in root strategies, but who knows what customizations // can be applied @@ -170,13 +242,40 @@ protected Expression resolveParentQualifier(RelatedResourceEntity entity, Str return resolveParentQualifier((RelatedResourceEntity) parent, fullDbPath); } - private String concatWithParentDbPath(ObjRelationship incoming, String outgoingDbPath) { - String dbPath = incoming.getDbRelationshipPath(); - return outgoingDbPath != null ? dbPath + "." + outgoingDbPath : dbPath; + private String dbPath(Class entityType, String relationshipName) { + + // we can pick "relationshipName" from anywhere in the inheritance hierarchy, as it will be converted to a db path, + // which is inheritance agnostic (at least until Cayenne starts supporting horizontal inheritance) + + return objRelationshipInInheritanceHierarchy(entityType, relationshipName).getDbRelationshipPath(); + } + + private ObjRelationship objRelationshipInInheritanceHierarchy(Class superclassType, String relationshipName) { + ObjEntity e = entityResolver.getObjEntity(superclassType); + ObjRelationship r = e.getRelationship(relationshipName); + + if (r != null) { + return r; + } + + for (ObjEntity se : entityResolver.getInheritanceTree(e.getName()).allSubEntities()) { + if (se != e) { + ObjRelationship sr = se.getRelationship(relationshipName); + if (sr != null) { + return sr; + } + } + } + + throw new IllegalStateException("No such relationship in entity or sub-entities: " + e.getName() + "." + relationshipName); + } + + private String concatWithParentDbPath(String incomingDbPath, String outgoingDbPath) { + return outgoingDbPath != null ? incomingDbPath + "." + outgoingDbPath : incomingDbPath; } @Override - public ColumnSelect createQueryWithParentIdsQualifier(RelatedResourceEntity entity, Iterator

parentData) { + public ColumnSelect createQueryWithParentIdsQualifier(RelatedResourceEntity entity, Iterable

parentData) { ColumnSelect query = createBaseQuery(entity).columns(queryColumns(entity)); @@ -199,10 +298,19 @@ public ColumnSelect createQueryWithParentIdsQualifier(RelatedRe return query.and(ExpressionFactory.or(qualifiers)); } - static

void consumeRange(Iterator

parentData, int offset, int len, Consumer

consumer) { + static

void consumeRange(Iterable

parentData, int offset, int limit, Consumer

consumer) { + if (parentData instanceof List) { + // this optimization prevents faulting of paginated lists + consumeRangeList((List

) parentData, offset, limit, consumer); + } else { + consumeRangeIterator(parentData.iterator(), offset, limit, consumer); + } + } + + static

void consumeRangeIterator(Iterator

parentData, int offset, int limit, Consumer

consumer) { int from = Math.max(0, offset); - int to = len > 0 ? Math.min(from + len, Integer.MAX_VALUE) : Integer.MAX_VALUE; + int to = limit > 0 ? Math.min(from + limit, Integer.MAX_VALUE) : Integer.MAX_VALUE; for (int i = 0; i < from && parentData.hasNext(); i++) { parentData.next(); @@ -213,6 +321,26 @@ static

void consumeRange(Iterator

parentData, int offset, int len, Consum } } + static

void consumeRangeList(List

parents, int offset, int limit, Consumer

consumer) { + + + int len = parents.size(); + if (len == 0) { + return; + } + + int from = Math.max(0, offset); + if (from >= len) { + return; + } + + int to = limit > 0 ? Math.min(from + limit, len) : len; + + for (int i = from; i < to; i++) { + consumer.accept(parents.get(i)); + } + } + protected ObjectSelect createRootIdQuery(ResourceEntity entity, AgObjectId rootId) { return ObjectSelect.query(entity.getType()) .where(buildIdQualifier(entity.getAgEntity(), rootId)); @@ -230,7 +358,7 @@ protected ObjectSelect createBaseQuery(ResourceEntity entity) { } Expression parsedExp = qualifierParser.parse(entity.getExp()); - Expression finalExp = qualifierPostProcessor.process(entity.getAgEntity(), parsedExp); + Expression finalExp = qualifierPostProcessor.process(entity.getAgEntity().getName(), parsedExp); query.where(finalExp); for (Sort o : entity.getOrderings()) { @@ -257,7 +385,7 @@ protected Expression buildIdQualifier(AgEntity entity, AgObjectId id) { idAttribute.getName()); } - ASTPath path = pathResolver.resolve(entity, idAttribute.getName()).getPathExp(); + ASTPath path = pathResolver.resolve(entity.getName(), idAttribute.getName()).getPathExp(); qualifiers.add(ExpressionFactory.matchExp(path, idValue)); } return ExpressionFactory.and(qualifiers); @@ -265,7 +393,7 @@ protected Expression buildIdQualifier(AgEntity entity, AgObjectId id) { protected Ordering toOrdering(ResourceEntity entity, Sort sort) { return new Ordering( - pathResolver.resolve(entity.getAgEntity(), sort.getPath()).getPathExp(), + pathResolver.resolve(entity.getAgEntity().getName(), sort.getPath()).getPathExp(), toSortOrder(sort.getDirection())); } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/CayenneUtil.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/CayenneUtil.java index b3f026be8..3a0896ef1 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/CayenneUtil.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/CayenneUtil.java @@ -1,24 +1,29 @@ package io.agrest.cayenne.processor; import io.agrest.AgException; -import io.agrest.id.AgObjectId; -import io.agrest.runtime.EntityParent; import io.agrest.cayenne.path.IPathResolver; import io.agrest.cayenne.path.PathOps; +import io.agrest.id.AgObjectId; import io.agrest.meta.AgEntity; import io.agrest.meta.AgIdPart; +import io.agrest.runtime.EntityParent; import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.ObjectId; import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.ExpressionFactory; import org.apache.cayenne.exp.parser.ASTDbPath; import org.apache.cayenne.exp.parser.ASTPath; import org.apache.cayenne.map.EntityResolver; +import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.map.ObjEntity; import org.apache.cayenne.map.ObjRelationship; import org.apache.cayenne.query.ObjectSelect; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.function.Function; public final class CayenneUtil { @@ -26,7 +31,57 @@ public final class CayenneUtil { private CayenneUtil() { } - @SuppressWarnings("unchecked") + /** + * @since 5.0 + */ + public static ObjectId toObjectId( + IPathResolver pathResolver, + ObjectContext context, + AgEntity agEntity, + Object idValueOrMap) { + + if (idValueOrMap == null) { + throw AgException.badRequest("No id specified"); + } + + ObjEntity entity = context.getEntityResolver().getObjEntity(agEntity.getType()); + if (entity == null) { + throw AgException.internalServerError("Unknown entity class: %s", agEntity.getType()); + } + + Collection pks = entity.getPrimaryKeys(); + if (pks.size() == 1) { + ObjAttribute pk = pks.iterator().next(); + return ObjectId.of(entity.getName(), pk.getDbAttributeName(), idValueOrMap); + } else { + + if (!(idValueOrMap instanceof Map)) { + throw AgException.internalServerError("Expected a map of id values for entity: %s", agEntity.getName()); + } + + Map idMap = (Map) idValueOrMap; + Map normalizedIdMap = new HashMap<>(pks.size() * 2); + idMap.forEach((k, v) -> { + + ASTPath kp = pathResolver.resolve(agEntity.getName(), k).getPathExp(); + if (kp instanceof ASTDbPath) { + normalizedIdMap.put(kp.getPath(), v); + } else { + ObjAttribute pk = entity.getAttribute(kp.getPath()); + if (pk == null) { + throw AgException.internalServerError("No pk attribute %s.%s", entity.getName(), kp.getPath()); + } + + normalizedIdMap.put(pk.getDbAttributeName(), v); + } + }); + + return ObjectId.of(entity.getName(), normalizedIdMap); + } + } + + + // TODO: this logic is somewhat duplicated in CayenneQueryAssembler.createQueryForIds. Maybe it belongs there to begin with? public static A findById( IPathResolver pathResolver, ObjectContext context, @@ -46,7 +101,7 @@ public static A findById( ObjectSelect query = ObjectSelect.query(agEntity.getType()); for (AgIdPart idPart : agEntity.getIdParts()) { - ASTPath idPath = pathResolver.resolve(agEntity, idPart.getName()).getPathExp(); + ASTPath idPath = pathResolver.resolve(agEntity.getName(), idPart.getName()).getPathExp(); query.and(ExpressionFactory.matchExp(idPath, id.get(idPart.getName()))); } @@ -55,12 +110,11 @@ public static A findById( public static Expression parentQualifier( IPathResolver pathResolver, + AgEntity parentAgEntity, EntityParent parent, EntityResolver resolver) { - AgEntity parentEntity = parent.getEntity(); - - ObjEntity parentObjEntity = resolver.getObjEntity(parentEntity.getName()); + ObjEntity parentObjEntity = resolver.getObjEntity(parent.getType()); ObjRelationship objRelationship = parentObjEntity.getRelationship(parent.getRelationship()); if (objRelationship == null) { @@ -73,7 +127,7 @@ public static Expression parentQualifier( AgObjectId id = parent.getId(); Function expBuilder = p -> { - ASTPath idPartPath = pathResolver.resolve(parentEntity, p.getName()).getPathExp(); + ASTPath idPartPath = pathResolver.resolve(parentObjEntity.getName(), p.getName()).getPathExp(); Expression pathExp = PathOps.concatWithDbPath(parentObjEntity, reversePath, idPartPath); Object val = id.get(p.getName()); @@ -89,11 +143,11 @@ public static Expression parentQualifier( }; if (id.size() == 1) { - return expBuilder.apply(parentEntity.getIdParts().iterator().next()); + return expBuilder.apply(parentAgEntity.getIdParts().iterator().next()); } List expressions = new ArrayList<>(id.size()); - for (AgIdPart idPart : parentEntity.getIdParts()) { + for (AgIdPart idPart : parentAgEntity.getIdParts()) { expressions.add(expBuilder.apply(idPart)); } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/ICayenneQueryAssembler.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/ICayenneQueryAssembler.java index 9b80d8cdb..c7fcafa84 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/ICayenneQueryAssembler.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/ICayenneQueryAssembler.java @@ -1,12 +1,14 @@ package io.agrest.cayenne.processor; import io.agrest.RelatedResourceEntity; +import io.agrest.id.AgObjectId; +import io.agrest.meta.AgEntity; import io.agrest.runtime.processor.select.SelectContext; import org.apache.cayenne.exp.property.Property; import org.apache.cayenne.query.ColumnSelect; import org.apache.cayenne.query.ObjectSelect; -import java.util.Iterator; +import java.util.Collection; /** * @since 3.7 @@ -19,9 +21,14 @@ public interface ICayenneQueryAssembler { ColumnSelect createQueryWithParentIdsQualifier( RelatedResourceEntity entity, - Iterator

parentData); + Iterable

parentData); Property[] queryColumns(RelatedResourceEntity entity); + /** + * @since 5.0 + */ + ObjectSelect createQueryForIds(AgEntity entity, Collection ids); + } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/delete/stage/CayenneDeleteMapChangesStage.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/delete/stage/CayenneDeleteMapChangesStage.java index cb9a225fa..5345be3c7 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/delete/stage/CayenneDeleteMapChangesStage.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/delete/stage/CayenneDeleteMapChangesStage.java @@ -1,12 +1,12 @@ package io.agrest.cayenne.processor.delete.stage; import io.agrest.AgException; -import io.agrest.id.AgObjectId; -import io.agrest.runtime.EntityParent; import io.agrest.cayenne.path.IPathResolver; import io.agrest.cayenne.processor.CayenneUtil; +import io.agrest.cayenne.processor.ICayenneQueryAssembler; import io.agrest.meta.AgEntity; import io.agrest.processor.ProcessorOutcome; +import io.agrest.runtime.EntityParent; import io.agrest.runtime.processor.delete.DeleteContext; import io.agrest.runtime.processor.delete.stage.DeleteMapChangesStage; import io.agrest.runtime.processor.update.ChangeOperation; @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; /** * A processor for the {@link io.agrest.DeleteStage#MAP_CHANGES} stage that associates persistent objects with delete @@ -29,9 +30,13 @@ public class CayenneDeleteMapChangesStage extends DeleteMapChangesStage { private final IPathResolver pathResolver; + private final ICayenneQueryAssembler queryAssembler; - public CayenneDeleteMapChangesStage(@Inject IPathResolver pathResolver) { + public CayenneDeleteMapChangesStage( + @Inject IPathResolver pathResolver, + @Inject ICayenneQueryAssembler queryAssembler) { this.pathResolver = pathResolver; + this.queryAssembler = queryAssembler; } @Override @@ -56,10 +61,10 @@ protected void mapDeleteOperations(DeleteContext conte protected List findObjectsToDelete(DeleteContext context) { - if (context.isById()) { - return findById(context); + if (context.isByIds()) { + return findByIds(context); } else if (context.getParent() != null) { - return findByParent(context, context.getParent().getEntity()); + return findByParent(context, context.getParent()); } // delete all !! else { @@ -67,42 +72,39 @@ protected List findObjectsToDelete(DeleteContext co } } - protected List findById(DeleteContext context) { + protected List findByIds(DeleteContext context) { - List objects = new ArrayList<>(context.getIds().size()); ObjectContext cayenneContext = CayenneDeleteStartStage.cayenneContext(context); + List objects = queryAssembler.createQueryForIds(context.getAgEntity(), context.getIds()).select(cayenneContext); - for (AgObjectId id : context.getIds()) { - - // TODO: batch objects retrieval into a single query - - T o = CayenneUtil.findById(pathResolver, cayenneContext, context.getAgEntity(), id); + // DELETE is idempotent, so if some objects are missing, we should proceed ... + // Also, only return 404 if zero objects matched - if (o == null) { - ObjEntity entity = cayenneContext.getEntityResolver().getObjEntity(context.getType()); - throw AgException.notFound("No object for ID '%s' and entity '%s'", id, entity.getName()); - } + if (objects.isEmpty()) { + ObjEntity entity = cayenneContext.getEntityResolver().getObjEntity(context.getType()); - objects.add(o); + String idsString = context.getIds().stream().map(id -> id.toString()).collect(Collectors.joining(",")); + throw AgException.notFound("No matching objects for entity '%s' and ids: %s", + entity.getName(), idsString); } return objects; } - protected List findByParent(DeleteContext context, AgEntity agParentEntity) { + protected List findByParent(DeleteContext context, EntityParent parent) { - EntityParent parent = context.getParent(); + AgEntity parentAgEntity = context.getSchema().getEntity(parent.getType()); ObjectContext cayenneContext = CayenneDeleteStartStage.cayenneContext(context); - Object parentObject = CayenneUtil.findById(pathResolver, cayenneContext, agParentEntity, parent.getId()); + Object parentObject = CayenneUtil.findById(pathResolver, cayenneContext, parentAgEntity, parent.getId()); if (parentObject == null) { // TODO: resolve entity by name, not type? - ObjEntity entity = cayenneContext.getEntityResolver().getObjEntity(parent.getEntity().getType()); + ObjEntity entity = cayenneContext.getEntityResolver().getObjEntity(parent.getType()); throw AgException.notFound("No parent object for ID '%s' and entity '%s'", parent.getId(), entity.getName()); } return ObjectSelect.query(context.getType()) - .where(CayenneUtil.parentQualifier(pathResolver, parent, cayenneContext.getEntityResolver())) + .where(CayenneUtil.parentQualifier(pathResolver, context.getSchema().getEntity(parent.getType()), parent, cayenneContext.getEntityResolver())) .select(CayenneDeleteStartStage.cayenneContext(context)); } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/delete/stage/CayenneDeleteStartStage.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/delete/stage/CayenneDeleteStartStage.java index 344d31337..6007a9fb5 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/delete/stage/CayenneDeleteStartStage.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/delete/stage/CayenneDeleteStartStage.java @@ -1,9 +1,9 @@ package io.agrest.cayenne.processor.delete.stage; import io.agrest.cayenne.persister.ICayennePersister; -import io.agrest.meta.AgSchema; import io.agrest.processor.ProcessingContext; import io.agrest.processor.ProcessorOutcome; +import io.agrest.runtime.entity.IIdResolver; import io.agrest.runtime.processor.delete.DeleteContext; import io.agrest.runtime.processor.delete.stage.DeleteStartStage; import org.apache.cayenne.ObjectContext; @@ -26,8 +26,10 @@ public static ObjectContext cayenneContext(ProcessingContext context) { private final ICayennePersister persister; - public CayenneDeleteStartStage(@Inject AgSchema schema, @Inject ICayennePersister persister) { - super(schema); + public CayenneDeleteStartStage( + @Inject IIdResolver idResolver, + @Inject ICayennePersister persister) { + super(idResolver); this.persister = persister; } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/ContextualCayenneRelatedDataResolver.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/ContextualCayenneRelatedDataResolver.java index aa0ff423e..08d29519e 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/ContextualCayenneRelatedDataResolver.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/ContextualCayenneRelatedDataResolver.java @@ -2,6 +2,7 @@ import io.agrest.AgException; import io.agrest.RelatedResourceEntity; +import io.agrest.ResourceEntity; import io.agrest.cayenne.processor.CayenneProcessor; import io.agrest.cayenne.processor.CayenneResourceEntityExt; import io.agrest.processor.ProcessingContext; @@ -41,17 +42,29 @@ public DataReader dataReader(RelatedResourceEntity entity, ProcessingContext< } protected RelatedDataResolver pickResolver(RelatedResourceEntity entity) { - CayenneResourceEntityExt parentExt = CayenneProcessor.getEntity(entity.getParent()); - // depending on the parent Cayenne semantics, we have some choices to make + // depending on the parent entity semantics, we have some choices to make + + ResourceEntity parentEntity = entity.getParent(); + CayenneResourceEntityExt parentExt = CayenneProcessor.getEntity(parentEntity); if (parentExt == null) { throw AgException.internalServerError( "Parent entity '%s' of entity '%s' is not managed by the Cayenne backend", - entity.getParent().getName(), + parentEntity.getName(), entity.getName()); } - return parentExt.getSelect() != null ? parentQueryResolver : parentIdsResolver; + return parentExt.getSelect() != null && !preferIdsResolver(parentEntity) + ? parentQueryResolver + : parentIdsResolver; + } + + protected boolean preferIdsResolver(ResourceEntity entity) { + + // prefer IDs resolver if the result is likely large (i.e. explicit pagination is requested), + // but the page size is reasonable (i.e. by IDs query will be saner than a full expression) + + return entity.getLimit() > 0 && entity.getLimit() < 5000; } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/ViaQueryWithParentIdsResolver.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/ViaQueryWithParentIdsResolver.java index ec4c2ca53..d548fd1a8 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/ViaQueryWithParentIdsResolver.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/ViaQueryWithParentIdsResolver.java @@ -9,7 +9,6 @@ import org.apache.cayenne.query.ColumnSelect; import java.util.Collections; -import java.util.Iterator; /** * A related resolver that waits for the parent query to complete, and resolves its entity objects based on the collection @@ -35,13 +34,12 @@ protected Iterable doOnParentDataResolved( SelectContext context) { // no parents, no need to fetch children - Iterator parentIt = parentData.iterator(); - if (!parentIt.hasNext()) { + if (!parentData.iterator().hasNext()) { return Collections.emptyList(); } // assemble query here, where we have access to all parent ids - ColumnSelect select = queryAssembler.createQueryWithParentIdsQualifier(entity, parentIt); + ColumnSelect select = queryAssembler.createQueryWithParentIdsQualifier(entity, parentData); if (select == null) { // no parents - nothing to fetch for this entity, and no need to descend into children return Collections.emptyList(); diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/stage/CayenneSelectApplyServerParamsStage.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/stage/CayenneSelectApplyServerParamsStage.java index 4015d81e8..cc62eca41 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/stage/CayenneSelectApplyServerParamsStage.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/select/stage/CayenneSelectApplyServerParamsStage.java @@ -4,9 +4,9 @@ import io.agrest.RootResourceEntity; import io.agrest.cayenne.persister.ICayennePersister; import io.agrest.cayenne.processor.CayenneProcessor; -import io.agrest.runtime.constraints.IConstraintsHandler; -import io.agrest.runtime.processor.select.stage.SelectApplyServerParamsStage; +import io.agrest.runtime.constraints.SelectConstraints; import io.agrest.runtime.processor.select.SelectContext; +import io.agrest.runtime.processor.select.stage.SelectApplyServerParamsStage; import org.apache.cayenne.di.Inject; import org.apache.cayenne.map.EntityResolver; @@ -18,9 +18,9 @@ public class CayenneSelectApplyServerParamsStage extends SelectApplyServerParams private final EntityResolver entityResolver; public CayenneSelectApplyServerParamsStage( - @Inject IConstraintsHandler constraintsHandler, + @Inject SelectConstraints constraints, @Inject ICayennePersister persister) { - super(constraintsHandler); + super(constraints); this.entityResolver = persister.entityResolver(); } @@ -36,12 +36,12 @@ private void tagRootEntity(RootResourceEntity entity) { } if (entity.getMapBy() != null) { - for (RelatedResourceEntity child : entity.getMapBy().getChildren().values()) { + for (RelatedResourceEntity child : entity.getMapBy().getChildren()) { tagRelatedEntity(child); } } - for (RelatedResourceEntity child : entity.getChildren().values()) { + for (RelatedResourceEntity child : entity.getChildren()) { tagRelatedEntity(child); } } @@ -52,12 +52,12 @@ private void tagRelatedEntity(RelatedResourceEntity entity) { } if (entity.getMapBy() != null) { - for (RelatedResourceEntity child : entity.getMapBy().getChildren().values()) { + for (RelatedResourceEntity child : entity.getMapBy().getChildren()) { tagRelatedEntity(child); } } - for (RelatedResourceEntity child : entity.getChildren().values()) { + for (RelatedResourceEntity child : entity.getChildren()) { tagRelatedEntity(child); } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/unrelate/stage/CayenneUnrelateDataStoreStage.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/unrelate/stage/CayenneUnrelateDataStoreStage.java index a654f1269..a34042e5b 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/unrelate/stage/CayenneUnrelateDataStoreStage.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/unrelate/stage/CayenneUnrelateDataStoreStage.java @@ -1,14 +1,14 @@ package io.agrest.cayenne.processor.unrelate.stage; import io.agrest.AgException; -import io.agrest.id.AgObjectId; import io.agrest.cayenne.path.IPathResolver; import io.agrest.cayenne.processor.CayenneUtil; +import io.agrest.id.AgObjectId; import io.agrest.meta.AgRelationship; import io.agrest.meta.AgSchema; -import io.agrest.processor.ProcessorOutcome; +import io.agrest.runtime.entity.IIdResolver; import io.agrest.runtime.processor.unrelate.UnrelateContext; -import io.agrest.runtime.processor.unrelate.stage.UnrelateUpdateDateStoreStage; +import io.agrest.runtime.processor.unrelate.stage.UnrelateUpdateDataStoreStage; import org.apache.cayenne.DataObject; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.di.Inject; @@ -20,33 +20,29 @@ /** * @since 2.7 */ -public class CayenneUnrelateDataStoreStage extends UnrelateUpdateDateStoreStage { +public class CayenneUnrelateDataStoreStage extends UnrelateUpdateDataStoreStage { private final AgSchema schema; private final IPathResolver pathResolver; public CayenneUnrelateDataStoreStage( + @Inject IIdResolver idResolver, @Inject AgSchema schema, @Inject IPathResolver pathResolver) { + + super(idResolver); this.schema = schema; this.pathResolver = pathResolver; } @Override - public ProcessorOutcome execute(UnrelateContext context) { - doExecute((UnrelateContext) context); - return ProcessorOutcome.CONTINUE; - } - - - protected void doExecute(UnrelateContext context) { - + protected void unrelate(UnrelateContext context) { ObjectContext cayenneContext = CayenneUnrelateStartStage.cayenneContext(context); if (context.getTargetId() != null) { - unrelateSingle(context, cayenneContext); + unrelateSingle((UnrelateContext) context, cayenneContext); } else { - unrelateAll(context, cayenneContext); + unrelateAll((UnrelateContext) context, cayenneContext); } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneFillResponseStage.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneFillResponseStage.java index bc748d243..e4d320000 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneFillResponseStage.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneFillResponseStage.java @@ -1,10 +1,10 @@ package io.agrest.cayenne.processor.update.stage; -import io.agrest.id.AgObjectId; import io.agrest.EntityUpdate; import io.agrest.RelatedResourceEntity; import io.agrest.ResourceEntity; import io.agrest.ToManyResourceEntity; +import io.agrest.id.AgObjectId; import io.agrest.processor.ProcessorOutcome; import io.agrest.reader.DataReader; import io.agrest.runtime.processor.update.UpdateContext; @@ -14,6 +14,7 @@ import org.apache.cayenne.ObjectId; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -35,7 +36,7 @@ public ProcessorOutcome execute(UpdateContext context) { @SuppressWarnings("unchecked") protected void doExecute(UpdateContext context) { - context.setStatus(getHttpStatus(context)); + context.setResponseStatus(getHttpStatus(context)); if (context.isIncludingDataInResponse()) { @@ -50,7 +51,7 @@ protected void doExecute(UpdateContext context) { for (EntityUpdate u : context.getUpdates()) { - T o = (T) u.getMergedTo(); + T o = (T) u.getTargetObject(); if (o != null && seen.add(o.getObjectId())) { objects.add(o); @@ -67,14 +68,13 @@ protected void doExecute(UpdateContext context) { protected void assignChildrenToParent(DataObject root, ResourceEntity entity) { DataReader idReader = entity.getAgEntity().getIdReader(); - Map> children = entity.getChildren(); + Collection> children = entity.getChildren(); if (!children.isEmpty()) { - for (Map.Entry> e : children.entrySet()) { - RelatedResourceEntity childEntity = e.getValue(); + for (RelatedResourceEntity childEntity : children) { - Object result = root.readPropertyDirectly(e.getKey()); + Object result = root.readPropertyDirectly(childEntity.getIncoming().getName()); if (result == null || result instanceof Fault) { continue; } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMapIdempotentFullSyncStage.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMapIdempotentFullSyncStage.java index b0b72cc56..2646543f2 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMapIdempotentFullSyncStage.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMapIdempotentFullSyncStage.java @@ -75,7 +75,7 @@ protected List existingObjects( EntityParent parent = context.getParent(); Expression rootQualifier = parent != null - ? CayenneUtil.parentQualifier(pathResolver, parent, entityResolver) + ? CayenneUtil.parentQualifier(pathResolver, context.getSchema().getEntity(parent.getType()), parent, entityResolver) : null; buildRootQuery(context.getEntity(), rootQualifier); diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMapUpdateStage.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMapUpdateStage.java index 789de8233..7dda122ef 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMapUpdateStage.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMapUpdateStage.java @@ -197,8 +197,8 @@ protected ObjectSelect buildRootQuery(RootResourceEntity entity, Expre ObjectSelect query = ObjectSelect.query(entity.getType()).where(qualifier); - for (Map.Entry> e : entity.getChildren().entrySet()) { - buildRelatedQuery(e.getValue(), query.getWhere()); + for (RelatedResourceEntity e : entity.getChildren()) { + buildRelatedQuery(e, query.getWhere()); } CayenneProcessor.getRootEntity(entity).setSelect(query); @@ -221,8 +221,8 @@ protected ColumnSelect buildRelatedQuery( .where(translateExpressionToSource(incomingObjRelationship, parentQualifier)) .columns(queryAssembler.queryColumns(entity)); - for (Map.Entry> e : entity.getChildren().entrySet()) { - buildRelatedQuery(e.getValue(), query.getWhere()); + for (RelatedResourceEntity e : entity.getChildren()) { + buildRelatedQuery(e, query.getWhere()); } CayenneProcessor.getRelatedEntity(entity).setSelect(query); @@ -232,7 +232,7 @@ protected ColumnSelect buildRelatedQuery( protected List fetchRootEntity(ObjectContext context, RootResourceEntity entity) { List objects = CayenneProcessor.getRootEntity(entity).getSelect().select(context); - for (RelatedResourceEntity c : entity.getChildren().values()) { + for (RelatedResourceEntity c : entity.getChildren()) { fetchRelatedEntity(context, c); } @@ -246,7 +246,7 @@ protected void fetchRelatedEntity(ObjectContext context, RelatedResourceEnti List objects = ext.getSelect().select(context); assignChildrenToParent(entity, objects); - for (RelatedResourceEntity c : entity.getChildren().values()) { + for (RelatedResourceEntity c : entity.getChildren()) { fetchRelatedEntity(context, c); } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMergeChangesStage.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMergeChangesStage.java index 3982dea09..d69d74595 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMergeChangesStage.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneMergeChangesStage.java @@ -1,15 +1,15 @@ package io.agrest.cayenne.processor.update.stage; import io.agrest.AgException; -import io.agrest.id.MultiValueId; -import io.agrest.runtime.EntityParent; import io.agrest.EntityUpdate; import io.agrest.cayenne.path.IPathResolver; import io.agrest.cayenne.persister.ICayennePersister; import io.agrest.cayenne.processor.CayenneUtil; +import io.agrest.id.MultiValueId; import io.agrest.meta.AgEntity; import io.agrest.meta.AgRelationship; import io.agrest.processor.ProcessorOutcome; +import io.agrest.runtime.EntityParent; import io.agrest.runtime.processor.update.ChangeOperation; import io.agrest.runtime.processor.update.ChangeOperationType; import io.agrest.runtime.processor.update.UpdateContext; @@ -18,6 +18,7 @@ import org.apache.cayenne.DataObject; import org.apache.cayenne.DataRow; import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.ObjectId; import org.apache.cayenne.di.Inject; import org.apache.cayenne.exp.ExpressionFactory; import org.apache.cayenne.exp.parser.ASTPath; @@ -26,15 +27,15 @@ import org.apache.cayenne.map.EntityResolver; import org.apache.cayenne.map.ObjAttribute; import org.apache.cayenne.map.ObjEntity; -import org.apache.cayenne.map.ObjRelationship; import org.apache.cayenne.query.ObjectSelect; -import org.apache.cayenne.reflect.ClassDescriptor; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; /** * A processor invoked for {@link io.agrest.UpdateStage#MERGE_CHANGES} stage. @@ -65,13 +66,13 @@ protected void merge(UpdateContext context) { return; } - ObjectRelator relator = createRelator(context); + Consumer parentRelator = createParentRelator(context); for (ChangeOperation op : ops.get(ChangeOperationType.CREATE)) { - create(context, relator, op.getUpdate()); + create(context, parentRelator, op.getUpdate()); } for (ChangeOperation op : ops.get(ChangeOperationType.UPDATE)) { - update(relator, op.getObject(), op.getUpdate()); + update(parentRelator, op.getObject(), op.getUpdate()); } for (ChangeOperation op : ops.get(ChangeOperationType.DELETE)) { @@ -83,45 +84,39 @@ protected void delete(T o) { o.getObjectContext().deleteObject(o); } - protected void create(UpdateContext context, ObjectRelator relator, EntityUpdate update) { + protected void create(UpdateContext context, Consumer parentRelator, EntityUpdate update) { ObjectContext objectContext = CayenneUpdateStartStage.cayenneContext(context); - DataObject o = objectContext.newObject(context.getType()); + T o = objectContext.newObject(context.getType()); - - Map idByAgAttribute = update.getId(); + Map idParts = update.getIdParts(); // set explicit ID - if (idByAgAttribute != null) { - - if (context.isIdUpdatesDisallowed() && update.isExplicitId()) { - throw AgException.badRequest("Setting ID explicitly is not allowed: %s", idByAgAttribute); - } + if (!idParts.isEmpty()) { ObjEntity objEntity = objectContext.getEntityResolver().getObjEntity(context.getType()); DbEntity dbEntity = objEntity.getDbEntity(); AgEntity agEntity = context.getEntity().getAgEntity(); - Map idByDbAttribute = mapToDbAttributes(agEntity, idByAgAttribute); + Map idByDbAttribute = mapToDbAttributes(agEntity, idParts); // need to make an additional check that the AgId is unique - checkExisting(objectContext, agEntity, idByDbAttribute, idByAgAttribute); + checkExisting(objectContext, agEntity, idByDbAttribute, idParts); if (isPrimaryKey(dbEntity, idByDbAttribute.keySet())) { createSingleFromPk(objEntity, idByDbAttribute, o); } else { - createSingleFromIdValues(objEntity, idByDbAttribute, idByAgAttribute, o); + createSingleFromIdValues(objEntity, idByDbAttribute, idParts, o); } } - mergeChanges(update, o, relator); - - relator.relateToParent(o); + mergeChanges(update, o); + parentRelator.accept(o); } - protected void update(ObjectRelator relator, T o, EntityUpdate update) { - mergeChanges(update, o, relator); - relator.relateToParent(o); + protected void update(Consumer parentRelator, T o, EntityUpdate update) { + mergeChanges(update, o); + parentRelator.accept(o); } // translate "id" expressed in terms on public Ag names to Cayenne DbAttributes @@ -237,180 +232,142 @@ private boolean isPrimaryKey(DbEntity entity, Collection maybePk) { return countPk >= pkSize; } - private void mergeChanges(EntityUpdate entityUpdate, DataObject o, ObjectRelator relator) { + private void mergeChanges(EntityUpdate entityUpdate, T o) { // attributes - for (Map.Entry e : entityUpdate.getValues().entrySet()) { + for (Map.Entry e : entityUpdate.getAttributes().entrySet()) { o.writeProperty(e.getKey(), e.getValue()); } // relationships ObjectContext context = o.getObjectContext(); - ObjEntity entity = context.getEntityResolver().getObjEntity(o); + for (Map.Entry e : entityUpdate.getToOneIds().entrySet()) { - for (Map.Entry> e : entityUpdate.getRelatedIds().entrySet()) { - - ObjRelationship relationship = entity.getRelationship(e.getKey()); - AgRelationship agRelationship = entityUpdate.getEntity().getRelationship(e.getKey()); + String name = e.getKey(); + AgRelationship relationship = entityUpdate.getEntity().getRelationship(name); + if (relationship == null) { + continue; + } - // sanity check - if (agRelationship == null) { + Object relatedId = e.getValue(); + if (relatedId == null) { + o.setToOneTarget(name, null, true); continue; } - final Set relatedIds = e.getValue(); - if (relatedIds == null || relatedIds.isEmpty() || allElementsNull(relatedIds)) { + ObjectId relatedCayenneId = CayenneUtil.toObjectId( + pathResolver, + context, + relationship.getTargetEntity(), + relatedId); + + DataObject oldRelated = (DataObject) o.readProperty(name); - relator.unrelateAll(agRelationship, o); + // TODO: a bug (but mostly just dead code) - this check does not work, as we are comparing "relatedId" + // scalar with ObjectId, so it will return false no matter what + if (oldRelated != null && oldRelated.getObjectId().equals(relatedId)) { continue; } - if (!agRelationship.isToMany() && relatedIds.size() > 1) { - throw AgException.badRequest( - "Relationship is to-one, but received update with multiple objects: %s", - agRelationship.getName()); + // TODO: Note that "parent" (a special flavor of related object) is resolved via CayenneUtil. So + // here we should use CayenneUtil as well for consistency, and preferably batch-faulting related objects + DataObject related = (DataObject) Cayenne.objectForPK(context, relatedCayenneId); + if (related == null) { + throw AgException.notFound("Related object '%s' with id of '%s' is not found", + relationship.getTargetEntity().getName(), + e.getValue()); } - ClassDescriptor relatedDescriptor = context.getEntityResolver().getClassDescriptor( - relationship.getTargetEntityName()); + o.setToOneTarget(name, related, true); + } - relator.unrelateAll(agRelationship, o, new RelationshipUpdate() { - @Override - public boolean containsRelatedObject(DataObject relatedObject) { - return relatedIds.contains(Cayenne.pkForObject(relatedObject)); - } - @Override - public void removeUpdateForRelatedObject(DataObject relatedObject) { - relatedIds.remove(Cayenne.pkForObject(relatedObject)); - } - }); + for (Map.Entry> e : entityUpdate.getToManyIds().entrySet()) { - for (Object relatedId : relatedIds) { + String name = e.getKey(); + AgRelationship relationship = entityUpdate.getEntity().getRelationship(name); + if (relationship == null) { + continue; + } - if (relatedId == null) { - continue; + // using set with predictable order that gives a predictable state of the final relationship list + Set relatedCayenneIds = new LinkedHashSet<>(e.getValue().size() * 2); + for (Object id : e.getValue()) { + if (id != null) { + relatedCayenneIds.add(CayenneUtil.toObjectId( + pathResolver, + context, + relationship.getTargetEntity(), + id)); } + } - DataObject related = (DataObject) Cayenne.objectForPK(context, relatedDescriptor.getObjectClass(), - relatedId); - - if (related == null) { - throw AgException.notFound("Related object '%s' with ID '%s' is not found", - relationship.getTargetEntityName(), - e.getValue()); + // unrelate objects no longer in relationship + List relatedObjects = (List) o.readProperty(name); + for (int i = 0; i < relatedObjects.size(); i++) { + DataObject relatedObject = relatedObjects.get(i); + if (!relatedCayenneIds.remove(relatedObject.getObjectId())) { + o.removeToManyTarget(relationship.getName(), relatedObject, true); + // a hack: we removed an object from relationship list, so need to reset the iteration index + i--; } - - relator.relate(agRelationship, o, related); } - } - entityUpdate.setMergedTo(o); - } + // link remaining added objects + for (ObjectId id : relatedCayenneIds) { - private boolean allElementsNull(Collection elements) { + // TODO: Note that "parent" (a special flavor of related object) is resolved via CayenneUtil. So + // here we should use CayenneUtil as well for consistency, and preferably batch-faulting related objects + DataObject related = (DataObject) Cayenne.objectForPK(context, id); - for (Object element : elements) { - if (element != null) { - return false; + if (related == null) { + throw AgException.notFound("Related object '%s' with id of '%s' is not found", + relationship.getTargetEntity().getName(), + e.getValue()); + } + + o.addToManyTarget(name, related, true); } } - return true; + entityUpdate.setTargetObject(o); } - protected ObjectRelator createRelator(UpdateContext context) { - - final EntityParent parent = context.getParent(); - + protected Consumer createParentRelator(UpdateContext context) { + EntityParent parent = context.getParent(); if (parent == null) { - return new ObjectRelator(); + return o -> { + }; } - ObjectContext objectContext = CayenneUpdateStartStage.cayenneContext(context); + AgEntity parentAgEntity = context.getSchema().getEntity(parent.getType()); + + DataObject parentObject = findParent(context, parentAgEntity, parent); + return parentAgEntity.getRelationship(parent.getRelationship()).isToMany() + ? o -> parentObject.addToManyTarget(parent.getRelationship(), o, true) + : o -> parentObject.setToOneTarget(parent.getRelationship(), o, true); + } - ObjEntity parentEntity = objectContext.getEntityResolver().getObjEntity(parent.getEntity().getType()); - AgEntity parentAgEntity = parent.getEntity(); - final DataObject parentObject = (DataObject) CayenneUtil.findById( + private DataObject findParent(UpdateContext context, AgEntity parentAgEntity, EntityParent parent) { + DataObject parentObject = (DataObject) CayenneUtil.findById( pathResolver, - objectContext, + CayenneUpdateStartStage.cayenneContext(context), parentAgEntity, parent.getId()); if (parentObject == null) { - throw AgException.notFound("No parent object for ID '%s' and entity '%s'", parent.getId(), parentEntity.getName()); + throw AgException.notFound("No parent object for ID '%s' and entity '%s'", + parent.getId(), + parentAgEntity.getName()); } - // TODO: check that relationship target is the same as ?? - if (parentEntity.getRelationship(parent.getRelationship()).isToMany()) { - return new ObjectRelator() { - @Override - public void relateToParent(DataObject object) { - parentObject.addToManyTarget(parent.getRelationship(), object, true); - } - }; - } else { - return new ObjectRelator() { - @Override - public void relateToParent(DataObject object) { - parentObject.setToOneTarget(parent.getRelationship(), object, true); - } - }; - } + return parentObject; } protected DbAttribute dbAttributeForAgAttribute(AgEntity agEntity, String attributeName) { - ASTPath path = pathResolver.resolve(agEntity, attributeName).getPathExp(); + ASTPath path = pathResolver.resolve(agEntity.getName(), attributeName).getPathExp(); Object attribute = path.evaluate(entityResolver.getObjEntity(agEntity.getName())); return attribute instanceof ObjAttribute ? ((ObjAttribute) attribute).getDbAttribute() : (DbAttribute) attribute; } - - interface RelationshipUpdate { - boolean containsRelatedObject(DataObject o); - - void removeUpdateForRelatedObject(DataObject o); - } - - static class ObjectRelator { - - void relateToParent(DataObject object) { - // do nothing - } - - void relate(AgRelationship agRelationship, DataObject object, DataObject relatedObject) { - if (agRelationship.isToMany()) { - object.addToManyTarget(agRelationship.getName(), relatedObject, true); - } else { - object.setToOneTarget(agRelationship.getName(), relatedObject, true); - } - } - - void unrelateAll(AgRelationship agRelationship, DataObject object) { - unrelateAll(agRelationship, object, null); - } - - void unrelateAll(AgRelationship agRelationship, DataObject object, RelationshipUpdate relationshipUpdate) { - - if (agRelationship.isToMany()) { - - @SuppressWarnings("unchecked") - List relatedObjects = - (List) object.readProperty(agRelationship.getName()); - - for (int i = 0; i < relatedObjects.size(); i++) { - DataObject relatedObject = relatedObjects.get(i); - if (relationshipUpdate == null || !relationshipUpdate.containsRelatedObject(relatedObject)) { - object.removeToManyTarget(agRelationship.getName(), relatedObject, true); - i--; - } else { - relationshipUpdate.removeUpdateForRelatedObject(relatedObject); - } - } - - } else { - object.setToOneTarget(agRelationship.getName(), null, true); - } - } - } } diff --git a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneUpdateApplyServerParamsStage.java b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneUpdateApplyServerParamsStage.java index 4974c854d..c3336313a 100644 --- a/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneUpdateApplyServerParamsStage.java +++ b/agrest-cayenne/src/main/java/io/agrest/cayenne/processor/update/stage/CayenneUpdateApplyServerParamsStage.java @@ -1,10 +1,8 @@ package io.agrest.cayenne.processor.update.stage; import io.agrest.AgException; -import io.agrest.runtime.EntityParent; import io.agrest.EntityUpdate; import io.agrest.RelatedResourceEntity; -import io.agrest.ResourceEntity; import io.agrest.RootResourceEntity; import io.agrest.cayenne.path.IPathResolver; import io.agrest.cayenne.path.PathOps; @@ -15,7 +13,8 @@ import io.agrest.meta.AgIdPart; import io.agrest.meta.AgRelationship; import io.agrest.processor.ProcessorOutcome; -import io.agrest.runtime.constraints.IConstraintsHandler; +import io.agrest.runtime.EntityParent; +import io.agrest.runtime.constraints.UpdateConstraints; import io.agrest.runtime.processor.update.UpdateContext; import io.agrest.runtime.processor.update.stage.UpdateApplyServerParamsStage; import org.apache.cayenne.di.Inject; @@ -39,17 +38,17 @@ */ public class CayenneUpdateApplyServerParamsStage extends UpdateApplyServerParamsStage { - private final IConstraintsHandler constraintsHandler; + private final UpdateConstraints constraints; private final EntityResolver entityResolver; private final IPathResolver pathResolver; public CayenneUpdateApplyServerParamsStage( @Inject IPathResolver pathResolver, - @Inject IConstraintsHandler constraintsHandler, + @Inject UpdateConstraints constraints, @Inject ICayennePersister persister) { this.pathResolver = pathResolver; - this.constraintsHandler = constraintsHandler; + this.constraints = constraints; this.entityResolver = persister.entityResolver(); } @@ -61,8 +60,6 @@ public ProcessorOutcome execute(UpdateContext context) { protected void doExecute(UpdateContext context) { - ResourceEntity entity = context.getEntity(); - // this creates a new EntityUpdate if there's no request payload, but the context ID is present, // so execute it unconditionally fillIdsFromExplicitContextId(context); @@ -73,11 +70,7 @@ protected void doExecute(UpdateContext context) { fillIdsFromParentId(context); } - constraintsHandler.constrainUpdate(context); - - // apply read constraints - // TODO: should we only care about response constraints after the commit? - constraintsHandler.constrainResponse(entity, null); + constraints.apply(context); tagRootEntity(context.getEntity()); } @@ -88,12 +81,12 @@ private void tagRootEntity(RootResourceEntity entity) { } if (entity.getMapBy() != null) { - for (RelatedResourceEntity child : entity.getMapBy().getChildren().values()) { + for (RelatedResourceEntity child : entity.getMapBy().getChildren()) { tagRelatedEntity(child); } } - for (RelatedResourceEntity child : entity.getChildren().values()) { + for (RelatedResourceEntity child : entity.getChildren()) { tagRelatedEntity(child); } } @@ -104,12 +97,12 @@ private void tagRelatedEntity(RelatedResourceEntity entity) { } if (entity.getMapBy() != null) { - for (RelatedResourceEntity child : entity.getMapBy().getChildren().values()) { + for (RelatedResourceEntity child : entity.getMapBy().getChildren()) { tagRelatedEntity(child); } } - for (RelatedResourceEntity child : entity.getChildren().values()) { + for (RelatedResourceEntity child : entity.getChildren()) { tagRelatedEntity(child); } } @@ -167,23 +160,21 @@ private void fillIdsFromMeaningfulPk(UpdateContext context) { private void fillIdsFromMappedPk(UpdateContext context, String propertyName) { for (EntityUpdate u : context.getUpdates()) { - Object pk = u.getValues().get(propertyName); + Object pk = u.getAttributes().get(propertyName); // Unlike id parts mapped from the DB layer, this one is tracked by its normal property name - u.getOrCreateId().putIfAbsent(propertyName, pk); + u.addIdPartIfAbsent(propertyName, pk); } } private void fillIdsFromRelatedId(UpdateContext context, String agRelationshipName, DbJoin outgoingJoin) { for (EntityUpdate u : context.getUpdates()) { - // 'getSourceName' assumes AgEntity's id attribute name is based on DbAttribute name - // TODO: check if PK is a map? - Set pk = u.getRelatedIds().get(agRelationshipName); - // if size != 1 : throw? - if (pk != null && pk.size() == 1) { - u.getOrCreateId().putIfAbsent(ASTDbPath.DB_PREFIX + outgoingJoin.getSourceName(), pk.iterator().next()); + // 'getSourceName' assumes AgEntity's id attribute name is based on DbAttribute name + Object id = u.getToOneIds().get(agRelationshipName); + if (id != null) { + u.addIdPartIfAbsent(ASTDbPath.DB_PREFIX + outgoingJoin.getSourceName(), id); } } } @@ -192,20 +183,19 @@ private void fillIdsFromRelatedIds(UpdateContext context, String agRelationsh for (EntityUpdate u : context.getUpdates()) { - Set pk = u.getRelatedIds().get(agRelationshipName); + Object pk = u.getToOneIds().get(agRelationshipName); - // if size != 1 : throw? - if (pk != null && pk.size() == 1) { + if (pk != null) { // TODO: should we allow null Map though? if (!(pk instanceof Map)) { throw new IllegalStateException("Expected more than one value in related 'id' for " + agRelationshipName); } // TODO: pretty sure this case has no unit tests - Map pkMap = (Map) pk.iterator().next(); + Map pkMap = (Map) pk; for (DbJoin join : outgoingJoins) { // 'getSourceName' and 'getTargetName' assumes AgEntity's id attribute name is based on DbAttribute name - u.getOrCreateId().putIfAbsent(ASTDbPath.DB_PREFIX + join.getSourceName(), pkMap.get(join.getTargetName())); + u.addIdPartIfAbsent(ASTDbPath.DB_PREFIX + join.getSourceName(), pkMap.get(join.getTargetName())); } } } @@ -226,8 +216,7 @@ private void fillIdsFromExplicitContextId(UpdateContext context) { AgEntity entity = context.getEntity().getAgEntity(); EntityUpdate u = context.getFirst(); - u.getOrCreateId().putAll(context.getId().asMap(entity)); - u.setExplicitId(); + u.setIdParts(context.getId().asMap(entity)); } } @@ -237,7 +226,9 @@ private void fillIdsFromParentId(UpdateContext context) { if (parent != null && parent.getId() != null) { - Map parentAgKeysToChildDbPaths = parentAgKeysToChildDbPaths(parent); + Map parentAgKeysToChildDbPaths = parentAgKeysToChildDbPaths( + context.getSchema().getEntity(parent.getType()), + parent); if (!parentAgKeysToChildDbPaths.isEmpty()) { Map idTranslated = new HashMap<>(5); @@ -250,19 +241,18 @@ private void fillIdsFromParentId(UpdateContext context) { } for (EntityUpdate u : context.getUpdates()) { - Map updateId = u.getOrCreateId(); - idTranslated.forEach(updateId::putIfAbsent); + idTranslated.forEach(u::addIdPartIfAbsent); } } } } - private Map parentAgKeysToChildDbPaths(EntityParent parent) { + private Map parentAgKeysToChildDbPaths(AgEntity parentAgEntity, EntityParent parent) { // TODO: too much Ag to Cayenne metadata translation happens here. // We should cache and reuse the returned map - ObjEntity parentCayenneEntity = entityResolver.getObjEntity(parent.getEntity().getType()); + ObjEntity parentCayenneEntity = entityResolver.getObjEntity(parent.getType()); ObjRelationship fromParent = parentCayenneEntity.getRelationship(parent.getRelationship()); if (fromParent == null) { @@ -280,13 +270,11 @@ private Map parentAgKeysToChildDbPaths(EntityParent parent) { sourceToTargetJoins.put(join.getSourceName(), join.getTargetName()); } - AgEntity parentEntity = parent.getEntity(); - Map parentAgKeysToChildDbPaths = new HashMap<>(); - for (AgIdPart idPart : parentEntity.getIdParts()) { + for (AgIdPart idPart : parentAgEntity.getIdParts()) { - ASTPath idPath = pathResolver.resolve(parentEntity, idPart.getName()).getPathExp(); - ASTDbPath dbPath = PathOps.resolveAsDbPath(parentCayenneEntity, idPath); + ASTPath idPath = pathResolver.resolve(parentCayenneEntity.getName(), idPart.getName()).getPathExp(); + ASTDbPath dbPath = PathOps.resolveAsDbPath(idPath); String targetPath = sourceToTargetJoins.remove(dbPath.getPath()); if (targetPath == null) { // not a part of our relationship, ignore diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_AuthorizerIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/AuthorizerIT.java similarity index 82% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_AuthorizerIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/AuthorizerIT.java index 5c601ac7c..42c95c1e0 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_AuthorizerIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/AuthorizerIT.java @@ -1,33 +1,33 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.DELETE; import io.agrest.SimpleResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.DELETE; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; -public class DELETE_AuthorizerIT extends DbTest { +public class AuthorizerIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class) .agCustomizer(ab -> ab .entityOverlay(AgEntity.overlay(E2.class).deleteAuthorizer(o -> !"dont_delete".equals(o.getName()))) ).build(); @Test - public void testInStack_Allowed() { + public void inStack_Allowed() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -41,7 +41,7 @@ public void testInStack_Allowed() { } @Test - public void testInStack_Blocked() { + public void inStack_Blocked() { tester.e2().insertColumns("id_", "name") .values(1, "dont_delete") .values(2, "b") @@ -57,7 +57,7 @@ public void testInStack_Blocked() { } @Test - public void testInRequestAndStack_Allowed() { + public void inRequestAndStack_Allowed() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -73,7 +73,7 @@ public void testInRequestAndStack_Allowed() { } @Test - public void testInRequestAndStack_Blocked() { + public void inRequestAndStack_Blocked() { tester.e2().insertColumns("id_", "name") .values(1, "dont_delete_this_either") .values(2, "b") @@ -98,7 +98,7 @@ public static class Resource { @Path("e2_stack_authorizer/{id}") public SimpleResponse putE2StackFilter(@PathParam("id") int id) { return AgJaxrs.delete(E2.class, config) - .byId(id) + .byIds(id) .sync(); } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/BasicIT.java similarity index 66% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_IT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/BasicIT.java index a0da6881c..9ae2c3b96 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_IT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/BasicIT.java @@ -1,36 +1,38 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.DELETE; +import io.agrest.DeleteBuilder; import io.agrest.SimpleResponse; import io.agrest.cayenne.cayenne.main.E17; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E24; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import org.junit.jupiter.api.Test; -import javax.ws.rs.DELETE; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; import java.util.HashMap; +import java.util.List; import java.util.Map; -public class DELETE_IT extends DbTest { +public class BasicIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class, E17.class, E24.class) .build(); @Test - public void testDeleteAll() { + public void deleteAll() { tester.e4().insertColumns("id", "c_varchar") .values(1, "xxx") @@ -46,7 +48,7 @@ public void testDeleteAll() { } @Test - public void testDeleteAll_Empty() { + public void deleteAll_Empty() { tester.target("/e4") .delete() @@ -55,7 +57,7 @@ public void testDeleteAll_Empty() { } @Test - public void testDeleteById() { + public void deleteById() { tester.e4().insertColumns("id", "c_varchar") .values(1, "xxx") @@ -70,7 +72,24 @@ public void testDeleteById() { } @Test - public void testDeleteById_CompoundId() { + public void deleteByIds() { + + tester.e4().insertColumns("id", "c_varchar") + .values(1, "xxx") + .values(7, "aaa") + .values(8, "yyy").exec(); + + tester.target("/e4") + .queryParam("ids", 1, 8) + .delete() + .wasOk() + .bodyEquals("{}"); + + tester.e4().matcher().assertOneMatch(); + } + + @Test + public void deleteById_CompoundId() { tester.e17().insertColumns("id1", "id2", "name").values(1, 1, "aaa").values(2, 2, "bbb").exec(); @@ -80,24 +99,24 @@ public void testDeleteById_CompoundId() { .bodyEquals("{}"); tester.e17().matcher().assertOneMatch(); - tester.e17().matcher().eq("id2", 2).eq("id2", 2).eq("name", "bbb").assertOneMatch(); + tester.e17().matcher().eq("id2", 2).andEq("id2", 2).andEq("name", "bbb").assertOneMatch(); } @Test - public void testDeleteById_BadId() { + public void deleteById_BadId() { tester.e4().insertColumns("id", "c_varchar").values(1, "xxx").exec(); tester.target("/e4/7") .delete() .wasNotFound() - .bodyEquals("{\"message\":\"No object for ID '7' and entity 'E4'\"}"); + .bodyEquals("{\"message\":\"No matching objects for entity 'E4' and ids: 7\"}"); tester.e4().matcher().assertMatches(1); } @Test - public void testDeleteTwice() { + public void deleteTwice() { tester.e4().insertColumns("id", "c_varchar") .values(1, "xxx") @@ -111,11 +130,11 @@ public void testDeleteTwice() { tester.target("/e4/8") .delete() .wasNotFound() - .bodyEquals("{\"message\":\"No object for ID '8' and entity 'E4'\"}"); + .bodyEquals("{\"message\":\"No matching objects for entity 'E4' and ids: 8\"}"); } @Test - public void testDelete_UpperCasePK() { + public void delete_UpperCasePK() { tester.e24().insertColumns("TYPE", "name").values(1, "xyz").exec(); @@ -126,7 +145,7 @@ public void testDelete_UpperCasePK() { } @Test - public void testDelete_ByParentId() { + public void delete_ByParentId() { tester.e2().insertColumns("id_") .values(1) @@ -163,8 +182,13 @@ public SimpleResponse deleteByParent(@PathParam("e2_id") int e2Id) { @DELETE @Path("e4") - public SimpleResponse deleteAll() { - return AgJaxrs.delete(E4.class, config).sync(); + public SimpleResponse deleteAll(@QueryParam("ids") List ids) { + DeleteBuilder builder = AgJaxrs.delete(E4.class, config); + if (ids != null && !ids.isEmpty()) { + builder.byIds(ids); + } + + return builder.sync(); } @DELETE @@ -180,11 +204,11 @@ public SimpleResponse deleteByMultiId( @QueryParam("id1") Integer id1, @QueryParam("id2") Integer id2) { - Map ids = new HashMap<>(); - ids.put(E17.ID1.getName(), id1); - ids.put(E17.ID2.getName(), id2); + Map id = new HashMap<>(); + id.put(E17.ID1.getName(), id1); + id.put(E17.ID2.getName(), id2); - return AgJaxrs.delete(E17.class, config).byId(ids).sync(); + return AgJaxrs.delete(E17.class, config).byId(id).sync(); } @DELETE diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_NaturalIdIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/NaturalIdIT.java similarity index 71% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_NaturalIdIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/NaturalIdIT.java index 95b8f1b48..db58df289 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_NaturalIdIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/NaturalIdIT.java @@ -1,33 +1,33 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.DELETE; import io.agrest.SimpleResponse; import io.agrest.cayenne.cayenne.main.E20; import io.agrest.cayenne.cayenne.main.E21; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.DELETE; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.HashMap; import java.util.Map; -public class DELETE_NaturalIdIT extends DbTest { +public class NaturalIdIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E20.class, E21.class) + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E20.class, E21.class) .build(); @Test - public void testSingleId() { + public void singleId() { tester.e20().insertColumns("name_col") .values("John") @@ -42,7 +42,7 @@ public void testSingleId() { } @Test - public void testMultiId() { + public void multiId() { tester.e21().insertColumns("age", "name") .values(18, "John") @@ -56,7 +56,7 @@ public void testMultiId() { .bodyEquals("{}"); tester.e21().matcher().assertOneMatch(); - tester.e21().matcher().eq("name", "Brian").eq("age", 27).assertOneMatch(); + tester.e21().matcher().eq("name", "Brian").andEq("age", 27).assertOneMatch(); } @Path("") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_RelatedIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/RelatedIT.java similarity index 82% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_RelatedIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/RelatedIT.java index c0dd6050f..e95a7a03b 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_RelatedIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/RelatedIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.DELETE; import io.agrest.EntityDelete; import io.agrest.SimpleResponse; @@ -6,29 +6,28 @@ import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E7; import io.agrest.cayenne.cayenne.main.E8; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; import org.junit.jupiter.api.Test; -import javax.ws.rs.DELETE; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; import java.util.Collection; -public class DELETE_RelatedIT extends DbTest { +public class RelatedIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(E2Resource.class, E3Resource.class, E8Resource.class) - .entities(E2.class, E3.class, E7.class, E8.class) + static final MainModelTester tester = tester(E2Resource.class, E3Resource.class, E8Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E7.class, E8.class) .build(); @Test - public void testAll_ToMany() { + public void all_ToMany() { // make sure we have e3s for more than one e2 - this will help us // confirm that relationship queries are properly filtered. @@ -47,7 +46,7 @@ public void testAll_ToMany() { } @Test - public void testValidRel_ToMany() { + public void validRel_ToMany() { // make sure we have e3s for more than one e2 - this will help us // confirm that relationship queries are properly filtered. @@ -63,11 +62,11 @@ public void testValidRel_ToMany() { tester.target("/e2/1/e3s/9").delete().wasOk().bodyEquals("{}"); - tester.e3().matcher().eq("id_", 9).eq("e2_id", null).assertOneMatch(); + tester.e3().matcher().eq("id_", 9).andEq("e2_id", null).assertOneMatch(); } @Test - public void testValidRel_ToOne() { + public void validRel_ToOne() { // make sure we have e3s for more than one e2 - this will help us // confirm that relationship queries are properly filtered. @@ -83,11 +82,11 @@ public void testValidRel_ToOne() { tester.target("/e3/9/e2/1").delete().wasOk().bodyEquals("{}"); - tester.e3().matcher().eq("id_", 9).eq("e2_id", null).assertOneMatch(); + tester.e3().matcher().eq("id_", 9).andEq("e2_id", null).assertOneMatch(); } @Test - public void testValidRel_ToOne_All() { + public void validRel_ToOne_All() { // make sure we have e3s for more than one e2 - this will help us // confirm that relationship queries are properly filtered. @@ -102,11 +101,11 @@ public void testValidRel_ToOne_All() { .values(9, "zzz", 1).exec(); tester.target("/e3/9/e2").delete().wasOk().bodyEquals("{}"); - tester.e3().matcher().eq("id_", 9).eq("e2_id", null).assertOneMatch(); + tester.e3().matcher().eq("id_", 9).andEq("e2_id", null).assertOneMatch(); } @Test - public void testInvalidRel() { + public void invalidRel() { tester.target("/e2/1/dummyRel/9") .delete() .wasBadRequest() @@ -114,7 +113,7 @@ public void testInvalidRel() { } @Test - public void testNoSuchId_Source() { + public void noSuchId_Source() { tester.target("/e2/22/e3s/9") .delete() .wasNotFound() @@ -129,13 +128,13 @@ public static class E2Resource { @DELETE @Path("{id}") - public SimpleResponse deleteE2ById(@PathParam("id") int id, @Context UriInfo uriInfo) { + public SimpleResponse deleteE2ById(@PathParam("id") int id) { return AgJaxrs.delete(E2.class, config).byId(id).sync(); } @Deprecated @DELETE - public SimpleResponse deleteE2_Batch(Collection> deleted, @Context UriInfo uriInfo) { + public SimpleResponse deleteE2_Batch(Collection> deleted) { return AgJaxrs.runtime(config).delete(E2.class, deleted); } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_StagesIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/StagesIT.java similarity index 88% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_StagesIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/StagesIT.java index 44ee9bb20..441c9570d 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE_StagesIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/DELETE/StagesIT.java @@ -1,29 +1,29 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.DELETE; import io.agrest.DeleteStage; import io.agrest.SimpleResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.runtime.processor.delete.DeleteContext; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import javax.ws.rs.DELETE; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -public class DELETE_StagesIT extends DbTest { +public class StagesIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class) .build(); @@ -36,7 +36,7 @@ public void resetCallbacks() { } @Test - public void testDeleteAll() { + public void deleteAll() { tester.e3().insertColumns("id_").values(3).values(4).exec(); tester.target("/e3").delete().wasOk(); @@ -50,7 +50,7 @@ public void testDeleteAll() { } @Test - public void testDeleteNone() { + public void deleteNone() { tester.target("/e3").delete().wasOk(); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_AgRequestIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/AgRequestIT.java similarity index 88% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_AgRequestIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/AgRequestIT.java index 32c2db616..ad02a25e5 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_AgRequestIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/AgRequestIT.java @@ -1,33 +1,33 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.AgRequest; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.protocol.Exp; import io.agrest.protocol.Sort; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_AgRequestIT extends DbTest { +public class AgRequestIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class) .build(); @Test - public void test_Exp_OverrideByAgRequest() { + public void exp_OverrideByAgRequest() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -44,7 +44,7 @@ public void test_Exp_OverrideByAgRequest() { } @Test - public void testIncludes_OverrideByAgRequest() { + public void includes_OverrideByAgRequest() { tester.e3().insertColumns("id_", "name") .values(6, "yyy") @@ -60,7 +60,7 @@ public void testIncludes_OverrideByAgRequest() { } @Test - public void testExcludes_OverrideByAgRequest() { + public void excludes_OverrideByAgRequest() { tester.e3().insertColumns("id_", "name") .values(6, "yyy") @@ -79,7 +79,7 @@ public void testExcludes_OverrideByAgRequest() { } @Test - public void test_Sort_OverrideByAgRequest() { + public void sort_OverrideByAgRequest() { tester.e4().insertColumns("id") .values(2) @@ -96,7 +96,7 @@ public void test_Sort_OverrideByAgRequest() { } @Test - public void test_MapBy_OverrideByAgRequest() { + public void mapBy_OverrideByAgRequest() { tester.e4().insertColumns("c_varchar", "c_int") .values("xxx", 1) @@ -125,7 +125,7 @@ public static class Resource { @GET @Path("e2_exp") public DataResponse getE2(@Context UriInfo uriInfo) { - Exp exp = Exp.simple("name = 'xxx'"); + Exp exp = Exp.parse("name = 'xxx'"); AgRequest agRequest = AgJaxrs.request(config).andExp(exp).build(); return AgJaxrs.select(E2.class, config) diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/BasicIT.java similarity index 54% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/BasicIT.java index bafec9833..9e044ce0c 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/BasicIT.java @@ -1,52 +1,40 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E17; -import io.agrest.cayenne.cayenne.main.E19; import io.agrest.cayenne.cayenne.main.E2; -import io.agrest.cayenne.cayenne.main.E28; import io.agrest.cayenne.cayenne.main.E29; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E31; import io.agrest.cayenne.cayenne.main.E4; import io.agrest.cayenne.cayenne.main.E6; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.converter.jsonvalue.UtcDateConverter; -import io.agrest.encoder.DateTimeFormatters; -import io.agrest.jaxrs2.AgJaxrs; -import io.agrest.meta.AgEntity; -import io.agrest.meta.AgEntityOverlay; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; -import java.nio.charset.StandardCharsets; -import java.sql.Time; -import java.time.Instant; -import java.time.LocalTime; -import java.util.Date; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; import java.util.HashMap; import java.util.Map; -public class GET_IT extends DbTest { +public class BasicIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E2.class, E3.class, E4.class, E6.class, E17.class, E19.class, E28.class, E31.class) - .entitiesAndDependencies(E29.class) + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E4.class, E6.class, E17.class, E29.class, E31.class) .build(); @Test - public void testResponse() { + public void basic() { tester.e4().insertColumns("id", "c_varchar", "c_int").values(1, "xxx", 5).exec(); @@ -59,7 +47,7 @@ public void testResponse() { } @Test - public void testIdCalledId() { + public void idCalledId() { tester.e31().insertColumns("id", "name").values(5, "30").values(4, "31").exec(); @@ -71,77 +59,7 @@ public void testIdCalledId() { } @Test - public void testDateTime() { - - Date date = Date.from(Instant.from(UtcDateConverter.dateParser().fromString("2012-02-03T11:01:02Z"))); - tester.e4().insertColumns("c_timestamp").values(date).exec(); - - String dateString = DateTimeFormatters.isoLocalDateTime().format(Instant.ofEpochMilli(date.getTime())); - - tester.target("/e4").queryParam("include", E4.C_TIMESTAMP.getName()).get() - .wasOk().bodyEquals(1, "{\"cTimestamp\":\"" + dateString + "\"}"); - } - - @Test - public void testDate() { - - Date date = Date.from(Instant.from(UtcDateConverter.dateParser().fromString("2012-02-03"))); - tester.e4().insertColumns("c_date").values(date).exec(); - - String dateString = DateTimeFormatters.isoLocalDateTime().format(Instant.ofEpochMilli(date.getTime())); - - tester.target("/e4").queryParam("include", E4.C_DATE.getName()) - .get() - .wasOk() - .bodyEquals(1, "{\"cDate\":\"" + dateString + "\"}"); - } - - @Test - public void testTime() { - - LocalTime lt = LocalTime.of(14, 0, 1); - - // "14:00:01" - Time time = Time.valueOf(lt); - - tester.e4().insertColumns("c_time").values(time).exec(); - - String timeString = DateTimeFormatters.isoLocalDateTime().format(Instant.ofEpochMilli(time.getTime())); - - tester.target("/e4").queryParam("include", E4.C_TIME.getName()).get().wasOk().bodyEquals(1, "{\"cTime\":\"" + timeString + "\"}"); - } - - // TODO: add tests for java.sql attributes - - @Test - public void testSort_ById() { - - tester.e4().insertColumns("id") - .values(2) - .values(1) - .values(3).exec(); - - tester.target("/e4") - .queryParam("sort", "[{\"property\":\"id\",\"direction\":\"DESC\"}]") - .queryParam("include", "id") - .get() - .wasOk() - .bodyEquals(3, "{\"id\":3}", "{\"id\":2}", "{\"id\":1}"); - } - - @Test - public void testSort_Invalid() { - - tester.target("/e4") - .queryParam("sort", "[{\"property\":\"xyz\",\"direction\":\"DESC\"}]") - .queryParam("include", "id") - .get() - .wasBadRequest() - .bodyEquals("{\"message\":\"Invalid path 'xyz' for 'E4'\"}"); - } - - @Test - public void testById() { + public void byId() { tester.e4().insertColumns("id") .values(2) @@ -157,7 +75,7 @@ public void testById() { } @Test - public void testById_Params() { + public void byId_Params() { tester.e4().insertColumns("id") .values(2) @@ -174,14 +92,14 @@ public void testById_Params() { } @Test - public void testById_NotFound() { + public void byId_NotFound() { tester.target("/e4/2").get() .wasNotFound() .bodyEquals("{\"message\":\"No object for ID '2' and entity 'E4'\"}"); } @Test - public void testById_IncludeRelationship() { + public void byId_IncludeRelationship() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id") @@ -202,7 +120,7 @@ public void testById_IncludeRelationship() { } @Test - public void testRelationshipSort() { + public void relationshipSort() { tester.e2().insertColumns("id_", "name") .values(1, "zzz") @@ -226,7 +144,7 @@ public void testRelationshipSort() { } @Test - public void testRelationshipStartLimit() { + public void relationshipStartLimit() { // TODO: run this test with different combinations of Resolvers. // Suspect that not all resolvers would support limits filtering of the result @@ -252,7 +170,7 @@ public void testRelationshipStartLimit() { } @Test - public void testToOne_Null() { + public void toOne_Null() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); @@ -269,7 +187,7 @@ public void testToOne_Null() { } @Test - public void testCharPK() { + public void charPK() { tester.e6().insertColumns("char_id", "char_column").values("a", "aaa").exec(); @@ -277,7 +195,7 @@ public void testCharPK() { } @Test - public void testByCompoundId() { + public void byCompoundId() { tester.e17().insertColumns("id1", "id2", "name").values(1, 1, "aaa").exec(); @@ -290,7 +208,7 @@ public void testByCompoundId() { @Test // Reproduces https://github.com/agrestio/agrest/issues/478 - public void testCompoundId_PartiallyMapped_DiffPropNames() { + public void compoundId_PartiallyMapped_DiffPropNames() { tester.e29().insertColumns("id1", "id2").values(1, 15).exec(); tester.target("/e29") @@ -301,7 +219,7 @@ public void testCompoundId_PartiallyMapped_DiffPropNames() { } @Test - public void testByCompoundDbId() { + public void byCompoundDbId() { tester.e29().insertColumns("id1", "id2") .values(1, 15) @@ -315,65 +233,7 @@ public void testByCompoundDbId() { } @Test - public void testMapByRootEntity() { - - tester.e4().insertColumns("c_varchar", "c_int").values("xxx", 1) - .values("yyy", 2) - .values("zzz", 2).exec(); - - tester.target("/e4") - .queryParam("mapBy", "cInt") - .queryParam("include", "cVarchar") - - .get().wasOk().bodyEqualsMapBy(3, - "\"1\":[{\"cVarchar\":\"xxx\"}]", - "\"2\":[{\"cVarchar\":\"yyy\"},{\"cVarchar\":\"zzz\"}]"); - } - - @Test - public void testMapBy_RelatedId() { - - tester.e2().insertColumns("id_", "name") - .values(1, "zzz") - .values(2, "yyy").exec(); - - tester.e3().insertColumns("id_", "name", "e2_id") - .values(8, "aaa", 1) - .values(9, "bbb", 1) - .values(10, "ccc", 2).exec(); - - tester.target("/e3") - .queryParam("mapBy", "e2.id") - .queryParam("exclude", "phoneNumber") - - .get().wasOk().bodyEqualsMapBy(3, - "\"1\":[{\"id\":8,\"name\":\"aaa\"},{\"id\":9,\"name\":\"bbb\"}]", - "\"2\":[{\"id\":10,\"name\":\"ccc\"}]"); - } - - @Test - public void testMapBy_OverRelationship() { - - tester.e2().insertColumns("id_", "name") - .values(1, "zzz") - .values(2, "yyy").exec(); - - tester.e3().insertColumns("id_", "name", "e2_id") - .values(8, "aaa", 1) - .values(9, "bbb", 1) - .values(10, "ccc", 2).exec(); - - tester.target("/e3") - .queryParam("mapBy", "e2") - .queryParam("exclude", "phoneNumber") - - .get().wasOk().bodyEqualsMapBy(3, - "\"1\":[{\"id\":8,\"name\":\"aaa\"},{\"id\":9,\"name\":\"bbb\"}]", - "\"2\":[{\"id\":10,\"name\":\"ccc\"}]"); - } - - @Test - public void testById_EscapeLineSeparators() { + public void byId_EscapeLineSeparators() { tester.e4().insertColumns("id", "c_varchar").values(1, "First line\u2028Second line...\u2029").exec(); @@ -383,50 +243,6 @@ public void testById_EscapeLineSeparators() { .get().wasOk().bodyEquals(1, "{\"cVarchar\":\"First line\\u2028Second line...\\u2029\"}"); } - @Test - public void testByteArrayProperty() { - - tester.e19().insertColumns("id", "guid").values(35, "someValue123".getBytes(StandardCharsets.UTF_8)).exec(); - - tester.target("/e19/35") - .queryParam("include", E19.GUID.getName()) - .get().wasOk().bodyEquals(1, "{\"guid\":\"c29tZVZhbHVlMTIz\"}"); - } - - @Test - public void testJsonProperty() { - - tester.e28().insertColumns("id", "json") - .values(35, "[1,2]") - .values(36, "{\"a\":1}") - .values(37, "{}") - .values(38, "5") - .values(39, null) - .exec(); - - tester.target("/e28") - .queryParam("include", E28.JSON.getName()) - .queryParam("sort", "id") - .get() - .wasOk() - .bodyEquals(5, "{\"json\":[1,2]}", "{\"json\":{\"a\":1}}", "{\"json\":{}}", "{\"json\":5}", "{\"json\":null}"); - } - - @Test - public void testJsonProperty_WithOtherProps() { - - tester.e28().insertColumns("id", "json") - .values(35, "[1,2]") - .values(37, "{}") - .exec(); - - tester.target("/e28/expanded") - .queryParam("include", "a", E28.JSON.getName(), "z") - .queryParam("sort", "id") - .get() - .wasOk() - .bodyEquals(2, "{\"a\":\"A\",\"json\":[1,2],\"z\":\"Z\"}", "{\"a\":\"A\",\"json\":{},\"z\":\"Z\"}"); - } @Path("") @Produces(MediaType.APPLICATION_JSON) @@ -477,33 +293,6 @@ public DataResponse getOneE6(@PathParam("id") String id) { return AgJaxrs.select(E6.class, config).byId(id).get(); } - @GET - @Path("e19/{id}") - public DataResponse getById(@Context UriInfo uriInfo, @PathParam("id") Integer id) { - return AgJaxrs.select(E19.class, config).clientParams(uriInfo.getQueryParameters()).byId(id).getOne(); - } - - @GET - @Path("e28") - public DataResponse get28(@Context UriInfo uriInfo) { - return AgJaxrs.select(E28.class, config).clientParams(uriInfo.getQueryParameters()).get(); - } - - @GET - @Path("e28/expanded") - public DataResponse get28Expanded(@Context UriInfo uriInfo) { - - // adding regular properties to see if JSON property can be encoded when other properties are present - AgEntityOverlay overlay = AgEntity.overlay(E28.class) - .attribute("a", String.class, o -> "A") - .attribute("z", String.class, o -> "Z"); - - return AgJaxrs.select(E28.class, config) - .entityOverlay(overlay) - .clientParams(uriInfo.getQueryParameters()) - .get(); - } - @GET @Path("e17") public DataResponse getByCompoundId( diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_CayenneExpIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/CayenneExpIT.java similarity index 89% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_CayenneExpIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/CayenneExpIT.java index e5ecdbb1d..be7ed5b73 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_CayenneExpIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/CayenneExpIT.java @@ -1,30 +1,30 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; @Deprecated -public class GET_CayenneExpIT extends DbTest { +public class CayenneExpIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class) .build(); @Test - public void testMap() { + public void map() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -38,7 +38,7 @@ public void testMap() { } @Test - public void testMap_Params() { + public void map_Params() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -53,7 +53,7 @@ public void testMap_Params() { } @Test - public void testBare() { + public void bare() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -69,7 +69,7 @@ public void testBare() { } @Test - public void testList() { + public void list() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -84,7 +84,7 @@ public void testList() { } @Test - public void testList_Params() { + public void list_Params() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -99,7 +99,7 @@ public void testList_Params() { } @Test - public void testIn_Array() { + public void in_Array() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -118,7 +118,7 @@ public void testIn_Array() { } @Test - public void testNotIn_Array() { + public void notIn_Array() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -136,7 +136,7 @@ public void testNotIn_Array() { } @Test - public void testOuter() { + public void outer() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -154,7 +154,7 @@ public void testOuter() { } @Test - public void testOuter_Relationship() { + public void outer_Relationship() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -172,7 +172,7 @@ public void testOuter_Relationship() { } @Test - public void testOuter_To_Many_Relationship() { + public void outer_To_Many_Relationship() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -190,7 +190,7 @@ public void testOuter_To_Many_Relationship() { } @Test - public void testIn_TwoObjects() { + public void in_TwoObjects() { tester.e3().insertColumns("id_", "name") .values(8, "yyy") @@ -204,7 +204,7 @@ public void testIn_TwoObjects() { } @Test - public void testIn_TwoRelatedObjects() { + public void in_TwoRelatedObjects() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -222,7 +222,7 @@ public void testIn_TwoRelatedObjects() { } @Test - public void testNotIn_ById() { + public void notIn_ById() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -239,7 +239,7 @@ public void testNotIn_ById() { } @Test - public void testNotIn_By2Ids() { + public void notIn_By2Ids() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ConvertersIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ConvertersIT.java new file mode 100644 index 000000000..570b4ff45 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ConvertersIT.java @@ -0,0 +1,266 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E19; +import io.agrest.cayenne.cayenne.main.E28; +import io.agrest.cayenne.cayenne.main.E4; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.agrest.meta.AgEntityOverlay; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class ConvertersIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entities(E4.class, E19.class, E28.class) + .build(); + + @Test + public void timestampAsUtilDate() { + + LocalDateTime dateTime = LocalDateTime.of(2012, 2, 3, 11, 1, 2); + tester.e4().insertColumns("c_timestamp").values(dateTime).exec(); + + tester.target("/e4").queryParam("include", E4.C_TIMESTAMP.getName()).get() + .wasOk().bodyEquals(1, "{\"cTimestamp\":\"2012-02-03T11:01:02\"}"); + } + + @Test + public void dateAsUtilDate() { + + LocalDate date = LocalDate.of(2012, 2, 3); + tester.e4().insertColumns("c_date").values(date).exec(); + + tester.target("/e4").queryParam("include", E4.C_DATE.getName()) + .get() + .wasOk() + .bodyEquals(1, "{\"cDate\":\"2012-02-03T00:00:00\"}"); + } + + @Test + public void timeAsUtilDate() { + LocalTime lt = LocalTime.of(14, 0, 1); + tester.e4().insertColumns("c_time").values(lt).exec(); + tester.target("/e4").queryParam("include", E4.C_TIME.getName()) + .get() + .wasOk() + .bodyEquals(1, "{\"cTime\":\"1970-01-01T14:00:01\"}"); + } + + @Test + public void sqlTimestamp() { + + LocalDateTime ts = LocalDateTime.of(2012, 2, 3, 11, 1, 2); + tester.e19().insertColumns("id", "c_timestamp") + .values(35, ts).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.C_TIMESTAMP.getName()) + .get().wasOk() + .bodyEquals(1, "{\"cTimestamp\":\"2012-02-03T11:01:02\"}"); + } + + @Test + public void sqlDate() { + + LocalDate date = LocalDate.of(2012, 2, 3); + tester.e19().insertColumns("id", "c_date") + .values(35, date).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.C_DATE.getName()) + .get().wasOk() + .bodyEquals(1, "{\"cDate\":\"2012-02-03\"}"); + } + + @Test + public void sqlTime() { + LocalTime t = LocalTime.of(14, 0, 1); + tester.e19().insertColumns("id", "c_time") + .values(35, t).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.C_TIME.getName()) + .get().wasOk() + .bodyEquals(1, "{\"cTime\":\"14:00:01\"}"); + } + + @Test + public void byteArray() { + + tester.e19().insertColumns("id", "guid").values(35, "someValue123".getBytes(StandardCharsets.UTF_8)).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.GUID.getName()) + .get().wasOk().bodyEquals(1, "{\"guid\":\"c29tZVZhbHVlMTIz\"}"); + } + + @Test + public void _boolean() { + + tester.e19().insertColumns("id", "boolean_object", "boolean_primitive") + .values(35, true, true).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.BOOLEAN_OBJECT.getName(), E19.BOOLEAN_PRIMITIVE.getName()) + .get().wasOk().bodyEquals(1, "{\"booleanObject\":true,\"booleanPrimitive\":true}"); + } + + @Test + public void _byte() { + + tester.e19().insertColumns("id", "byte_object", "byte_primitive") + .values(35, 1, 2).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.BYTE_OBJECT.getName(), E19.BYTE_PRIMITIVE.getName()) + .get().wasOk().bodyEquals(1, "{\"byteObject\":1,\"bytePrimitive\":2}"); + } + + + @Test + public void _short() { + + tester.e19().insertColumns("id", "short_object", "short_primitive") + .values(35, 1, 2).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.SHORT_OBJECT.getName(), E19.SHORT_PRIMITIVE.getName()) + .get().wasOk().bodyEquals(1, "{\"shortObject\":1,\"shortPrimitive\":2}"); + } + + + @Test + public void _long() { + + tester.e19().insertColumns("id", "long_object", "long_primitive") + .values(35, 13434234234L, 13434234235L).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.LONG_OBJECT.getName(), E19.LONG_PRIMITIVE.getName()) + .get().wasOk().bodyEquals(1, "{\"longObject\":13434234234,\"longPrimitive\":13434234235}"); + } + + @Test + public void bigInteger() { + + tester.e19().insertColumns("id", "big_integer") + .values(35, new BigInteger("1234567890")).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.BIG_INTEGER.getName()) + .get().wasOk().bodyEquals(1, "{\"bigInteger\":1234567890}"); + } + + @Test + public void bigDecimal() { + + tester.e19().insertColumns("id", "big_decimal") + .values(35, new BigDecimal("123456789.12")).exec(); + + tester.target("/e19/35") + .queryParam("include", E19.BIG_DECIMAL.getName()) + .get().wasOk().bodyEquals(1, "{\"bigDecimal\":123456789.12}"); + } + + @Test + public void json() { + + tester.e28().insertColumns("id", "json") + .values(35, "[1,2]") + .values(36, "{\"a\":1}") + .values(37, "{}") + .values(38, "5") + .values(39, null) + .exec(); + + tester.target("/e28") + .queryParam("include", E28.JSON.getName()) + .queryParam("sort", "id") + .get() + .wasOk() + .bodyEquals(5, "{\"json\":[1,2]}", "{\"json\":{\"a\":1}}", "{\"json\":{}}", "{\"json\":5}", "{\"json\":null}"); + } + + @Test + public void json_WithOtherProps() { + + tester.e28().insertColumns("id", "json") + .values(35, "[1,2]") + .values(37, "{}") + .exec(); + + tester.target("/e28/expanded") + .queryParam("include", "a", E28.JSON.getName(), "z") + .queryParam("sort", "id") + .get() + .wasOk() + .bodyEquals(2, "{\"a\":\"A\",\"json\":[1,2],\"z\":\"Z\"}", "{\"a\":\"A\",\"json\":{},\"z\":\"Z\"}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e4") + public DataResponse getE4(@Context UriInfo uriInfo) { + return AgJaxrs.select(E4.class, config).clientParams(uriInfo.getQueryParameters()).get(); + } + + @GET + @Path("e4/{id}") + public DataResponse getE4_WithIncludeExclude(@PathParam("id") int id, @Context UriInfo uriInfo) { + return AgJaxrs.select(E4.class, config).clientParams(uriInfo.getQueryParameters()).byId(id).get(); + } + + @GET + @Path("e19/{id}") + public DataResponse getById(@Context UriInfo uriInfo, @PathParam("id") Integer id) { + return AgJaxrs.select(E19.class, config).clientParams(uriInfo.getQueryParameters()).byId(id).getOne(); + } + + @GET + @Path("e28") + public DataResponse get28(@Context UriInfo uriInfo) { + return AgJaxrs.select(E28.class, config).clientParams(uriInfo.getQueryParameters()).get(); + } + + @GET + @Path("e28/expanded") + public DataResponse get28Expanded(@Context UriInfo uriInfo) { + + // adding regular properties to see if JSON property can be encoded when other properties are present + AgEntityOverlay overlay = AgEntity.overlay(E28.class) + .attribute("a", String.class, o -> "A") + .attribute("z", String.class, o -> "Z"); + + return AgJaxrs.select(E28.class, config) + .entityOverlay(overlay) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/DefaultSelectBuilder_CustomPipelineIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/DefaultSelectBuilder_CustomPipelineIT.java similarity index 91% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/DefaultSelectBuilder_CustomPipelineIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/DefaultSelectBuilder_CustomPipelineIT.java index d2f5418f1..ac88d5328 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/DefaultSelectBuilder_CustomPipelineIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/DefaultSelectBuilder_CustomPipelineIT.java @@ -1,10 +1,10 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.SelectBuilder; import io.agrest.SelectStage; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; import io.agrest.runtime.DefaultSelectBuilder; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; @@ -21,10 +21,10 @@ * The test class for operations that execute and verify the callbacks and custom stack functions, but do not check * the data. So no need to run DB cleanup. */ -public class DefaultSelectBuilder_CustomPipelineIT extends DbTest { +public class DefaultSelectBuilder_CustomPipelineIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester().build(); + static final MainModelTester tester = tester().build(); private DefaultSelectBuilder createBuilder(Class type) { SelectBuilder builder = tester.runtime().select(type); @@ -33,7 +33,7 @@ private DefaultSelectBuilder createBuilder(Class type) { } @Test - public void testStage_AllStages() { + public void stage_AllStages() { Map stages = new EnumMap<>(SelectStage.class); Consumer stageRecorder = s -> stages.put(s, stages.size()); @@ -54,7 +54,7 @@ public void testStage_AllStages() { } @Test - public void testStage_Composition() { + public void stage_Composition() { Map stages = new EnumMap<>(SelectStage.class); BiConsumer stageRecorder = (stage, value) -> { diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/DefaultSelectBuilder_CustomPipeline_DataIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/DefaultSelectBuilder_CustomPipeline_DataIT.java similarity index 84% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/DefaultSelectBuilder_CustomPipeline_DataIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/DefaultSelectBuilder_CustomPipeline_DataIT.java index 05f290b80..5498ddc8e 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/DefaultSelectBuilder_CustomPipeline_DataIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/DefaultSelectBuilder_CustomPipeline_DataIT.java @@ -1,13 +1,13 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.SelectBuilder; import io.agrest.SelectStage; -import io.agrest.protocol.Exp; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.protocol.Exp; import io.agrest.runtime.DefaultSelectBuilder; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; @@ -15,10 +15,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class DefaultSelectBuilder_CustomPipeline_DataIT extends DbTest { +public class DefaultSelectBuilder_CustomPipeline_DataIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester() + static final MainModelTester tester = tester() .entities(E2.class, E3.class) .build(); @@ -29,14 +29,14 @@ private DefaultSelectBuilder createBuilder(Class type) { } @Test - public void testStage() { + public void stage() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") .values(2, "yyy").exec(); DataResponse dr = createBuilder(E2.class) - .stage(SelectStage.CREATE_ENTITY, c -> c.getEntity().andExp(Exp.simple("name = 'yyy'"))) + .stage(SelectStage.CREATE_ENTITY, c -> c.getEntity().andExp(Exp.parse("name = 'yyy'"))) .get(); assertEquals(1, dr.getData().size()); @@ -44,7 +44,7 @@ public void testStage() { } @Test - public void testTerminalStage() { + public void terminalStage() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_EntityOverlayIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/EntityOverlayIT.java similarity index 88% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_EntityOverlayIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/EntityOverlayIT.java index cb3690738..79f15aff2 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_EntityOverlayIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/EntityOverlayIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.annotation.AgAttribute; @@ -7,9 +7,9 @@ import io.agrest.cayenne.cayenne.main.E4; import io.agrest.cayenne.cayenne.main.E7; import io.agrest.cayenne.cayenne.main.E8; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.agrest.runtime.AgRuntimeBuilder; @@ -18,21 +18,21 @@ import org.apache.cayenne.ObjectId; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.List; import static java.util.Arrays.asList; -public class GET_EntityOverlayIT extends DbTest { +public class EntityOverlayIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E2.class, E3.class, E4.class, E7.class, E8.class) - .agCustomizer(GET_EntityOverlayIT::addOverlay) + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E4.class, E7.class, E8.class) + .agCustomizer(EntityOverlayIT::addOverlay) .build(); private static AgRuntimeBuilder addOverlay(AgRuntimeBuilder builder) { @@ -61,7 +61,7 @@ private static AgRuntimeBuilder addOverlay(AgRuntimeBuilder builder) { } @Test - public void testExclude() { + public void exclude() { tester.e2().insertColumns("id_", "name", "address").values(1, "N", "A").exec(); @@ -72,7 +72,7 @@ public void testExclude() { } @Test - public void testRedefineAttribute_Transient() { + public void redefineAttribute_Transient() { tester.e4().insertColumns("id", "c_varchar") .values(1, "x") @@ -85,7 +85,7 @@ public void testRedefineAttribute_Transient() { } @Test - public void testRedefineAttribute_AdHocRelated() { + public void redefineAttribute_AdHocRelated() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", 1).exec(); @@ -98,7 +98,7 @@ public void testRedefineAttribute_AdHocRelated() { } @Test - public void testRedefineAttribute_AdHoc() { + public void redefineAttribute_AdHoc() { tester.e4().insertColumns("id", "c_varchar") .values(1, "x") @@ -111,7 +111,7 @@ public void testRedefineAttribute_AdHoc() { } @Test - public void testRedefineToOne_AdHoc() { + public void redefineToOne_AdHoc() { tester.e4().insertColumns("id", "c_varchar") .values(1, "x") @@ -127,7 +127,7 @@ public void testRedefineToOne_AdHoc() { } @Test - public void testRedefineToMany_AdHoc() { + public void redefineToMany_AdHoc() { tester.e4().insertColumns("id", "c_varchar") .values(1, "x") @@ -143,7 +143,7 @@ public void testRedefineToMany_AdHoc() { } @Test - public void testRedefineToOne_Replaced() { + public void redefineToOne_Replaced() { tester.e7().insertColumns("id", "name") .values(1, "x1") @@ -159,7 +159,7 @@ public void testRedefineToOne_Replaced() { } @Test - public void testRedefineAttribute_Replaced() { + public void redefineAttribute_Replaced() { tester.e7().insertColumns("id", "name") .values(1, "01") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_EntityOverlay_PerRequestIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/EntityOverlay_PerRequestIT.java similarity index 91% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_EntityOverlay_PerRequestIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/EntityOverlay_PerRequestIT.java index 1a1dd09ae..fae219318 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_EntityOverlay_PerRequestIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/EntityOverlay_PerRequestIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E10; @@ -6,9 +6,9 @@ import io.agrest.cayenne.cayenne.main.E22; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.bootique.junit5.BQTestTool; @@ -17,24 +17,24 @@ import org.apache.cayenne.query.SelectById; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.function.Function; -public class GET_EntityOverlay_PerRequestIT extends DbTest { +public class EntityOverlay_PerRequestIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class, E22.class) .entitiesAndDependencies(E10.class) .build(); @Test - public void test_DefaultIncludes() { + public void defaultIncludes() { tester.e4().insertColumns("id", "c_varchar").values(2, "a").values(4, "b").exec(); @@ -49,7 +49,7 @@ public void test_DefaultIncludes() { } @Test - public void test_Includes() { + public void includes() { tester.e4().insertColumns("id", "c_varchar").values(2, "a").values(4, "b").exec(); @@ -63,7 +63,7 @@ public void test_Includes() { } @Test - public void test_Overlay_NoReaderCaching() { + public void overlay_NoReaderCaching() { tester.e4().insertColumns("id").values(2).values(4).exec(); @@ -84,7 +84,7 @@ public void test_Overlay_NoReaderCaching() { } @Test - public void test_RequestOverlaidProperties_ConstrainedEntity() { + public void requestOverlaidProperties_ConstrainedEntity() { tester.e10().insertColumns("id", "c_int").values(2, 5).values(4, 8).exec(); tester.e22().insertColumns("id", "name") @@ -104,7 +104,7 @@ public void test_RequestOverlaidProperties_ConstrainedEntity() { } @Test - public void test_OverlaidRelationship() { + public void overlaidRelationship() { tester.e4().insertColumns("id", "c_varchar").values(2, "a").values(4, "b").exec(); tester.e22().insertColumns("id", "name") @@ -124,7 +124,7 @@ public void test_OverlaidRelationship() { } @Test - public void test_OverlaidRelationship_ExpOnParent() { + public void overlaidRelationship_ExpOnParent() { tester.e4().insertColumns("id", "c_varchar").values(2, "a").values(4, "b").exec(); tester.e22().insertColumns("id", "name") @@ -141,7 +141,7 @@ public void test_OverlaidRelationship_ExpOnParent() { } @Test - public void test_OverlaidRelationship_ExpOnParent_RelatedToOne() { + public void overlaidRelationship_ExpOnParent_RelatedToOne() { tester.e4().insertColumns("id", "c_varchar").values(2, "a").values(4, "b").exec(); @@ -163,7 +163,7 @@ public void test_OverlaidRelationship_ExpOnParent_RelatedToOne() { } @Test - public void test_OverlaidRelatedExclude() { + public void overlaidRelatedExclude() { tester.e2().insertColumns("id_", "name", "address").values(1, "N", "A").exec(); tester.e3().insertColumns("id_", "name", "phone_number", "e2_id") @@ -178,7 +178,7 @@ public void test_OverlaidRelatedExclude() { } @Test - public void test_OverlaidExclude() { + public void overlaidExclude() { tester.e2().insertColumns("id_", "name", "address").values(1, "N", "A").exec(); @@ -189,7 +189,7 @@ public void test_OverlaidExclude() { } @Test - public void test_OverlaidDataReaderException() { + public void overlaidDataReaderException() { tester.e4().insertColumns("id", "c_varchar").values(2, "a").values(4, "b").exec(); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_EntityOverlay_PerStackIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/EntityOverlay_PerStackIT.java similarity index 81% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_EntityOverlay_PerStackIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/EntityOverlay_PerStackIT.java index d40187eb1..7e05bbd52 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_EntityOverlay_PerStackIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/EntityOverlay_PerStackIT.java @@ -1,13 +1,13 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E22; import io.agrest.cayenne.cayenne.main.E25; import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.agrest.runtime.AgRuntimeBuilder; @@ -16,18 +16,18 @@ import org.apache.cayenne.query.SelectById; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_EntityOverlay_PerStackIT extends DbTest { +public class EntityOverlay_PerStackIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E22.class, E25.class, E2.class, E3.class) - .agCustomizer(GET_EntityOverlay_PerStackIT::addOverlay) + .agCustomizer(EntityOverlay_PerStackIT::addOverlay) .build(); private static AgRuntimeBuilder addOverlay(AgRuntimeBuilder builder) { @@ -35,7 +35,7 @@ private static AgRuntimeBuilder addOverlay(AgRuntimeBuilder builder) { // creating an adhoc relationship between two persistent objects with a custom resolver AgEntityOverlay e22Overlay = AgEntity .overlay(E22.class) - .toOne("overlayToOne", E25.class, GET_EntityOverlay_PerStackIT::findForParent); + .toOne("overlayToOne", E25.class, EntityOverlay_PerStackIT::findForParent); AgEntityOverlay e2Overlay = AgEntity .overlay(E2.class) @@ -51,7 +51,7 @@ private static E25 findForParent(E22 parent) { } @Test - public void testRedefineToOne() { + public void redefineToOne() { tester.e22().insertColumns("id") .values(1) @@ -74,7 +74,7 @@ public void testRedefineToOne() { } @Test - public void test_Overlay_HidingId() { + public void overlay_HidingId() { tester.e2().insertColumns("id_", "name", "address").values(1, "N", "A").exec(); tester.e3().insertColumns("id_", "name", "phone_number", "e2_id") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ExpIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ExpIT.java similarity index 67% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ExpIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ExpIT.java index 1a01ff3b2..5a385452c 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ExpIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ExpIT.java @@ -1,32 +1,32 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.time.LocalDateTime; -public class GET_ExpIT extends DbTest { +public class ExpIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class) .build(); @Test - public void testMap() { + public void map() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -40,7 +40,7 @@ public void testMap() { } @Test - public void testMap_Params() { + public void map_Params() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -55,7 +55,7 @@ public void testMap_Params() { } @Test - public void testBare() { + public void bare() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -71,7 +71,7 @@ public void testBare() { } @Test - public void testList() { + public void list() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -86,7 +86,7 @@ public void testList() { } @Test - public void testList_Params() { + public void list_Params() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -102,7 +102,7 @@ public void testList_Params() { @Test @DisplayName("positional binding of repeating name") - public void testList_Params_Repeating() { + public void list_Params_Repeating() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -117,7 +117,7 @@ public void testList_Params_Repeating() { } @Test - public void testList_Params_Timestamp() { + public void list_Params_Timestamp() { tester.e4().insertColumns("id", "c_timestamp") .values(1, LocalDateTime.of(2017, 2, 3, 5, 6, 7)) @@ -131,7 +131,7 @@ public void testList_Params_Timestamp() { } @Test - public void testIn_Array() { + public void in_Array() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -150,7 +150,7 @@ public void testIn_Array() { } @Test - public void testNotIn_Array() { + public void notIn_Array() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -168,7 +168,7 @@ public void testNotIn_Array() { } @Test - public void testOuter() { + public void outer() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -186,7 +186,7 @@ public void testOuter() { } @Test - public void testOuter_Relationship() { + public void outer_Relationship() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -204,7 +204,7 @@ public void testOuter_Relationship() { } @Test - public void testOuter_To_Many_Relationship() { + public void outer_To_Many_Relationship() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -222,7 +222,7 @@ public void testOuter_To_Many_Relationship() { } @Test - public void testIn_TwoObjects() { + public void in_TwoObjects() { tester.e3().insertColumns("id_", "name") .values(8, "yyy") @@ -236,7 +236,7 @@ public void testIn_TwoObjects() { } @Test - public void testIn_TwoRelatedObjects() { + public void in_TwoRelatedObjects() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -254,7 +254,7 @@ public void testIn_TwoRelatedObjects() { } @Test - public void testNotIn_ById() { + public void notIn_ById() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -271,7 +271,7 @@ public void testNotIn_ById() { } @Test - public void testNotIn_By2Ids() { + public void notIn_By2Ids() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -288,6 +288,116 @@ public void testNotIn_By2Ids() { .wasOk().bodyEquals(1, "{\"id\":9}"); } + @Test + public void like_MatchSingleQuote() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(2, "yxy") + .values(3, "y'y") + .values(4, "yyy") + .exec(); + + tester.target("/e2") + .queryParam("include", "id") + .queryParam("exp", "name like 'y\\'y'") + .queryParam("sort", "id") + .get() + .wasOk().bodyEquals(1, "{\"id\":3}"); + } + + @Test + public void like_MatchDoubleQuote() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(2, "yxy") + .values(3, "y\"y") + .values(4, "yyy") + .exec(); + + tester.target("/e2") + .queryParam("include", "id") + .queryParam("exp", "name like 'y\\\"y'") + .queryParam("sort", "id") + .get() + .wasOk().bodyEquals(1, "{\"id\":3}"); + } + + @Test + public void like_SingleChar_Pattern() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(2, "yxy") + .values(3, "yxxy") + .values(4, "yyy") + .exec(); + + tester.target("/e2") + .queryParam("include", "id") + .queryParam("exp", "name like 'y_y'") + .queryParam("sort", "id") + .get() + .wasOk().bodyEquals(2, "{\"id\":2}", "{\"id\":4}"); + } + + @Test + public void like_MultiChar_Pattern() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(2, "yxy") + .values(3, "yxxy") + .values(4, "yyy") + .exec(); + + tester.target("/e2") + .queryParam("include", "id") + .queryParam("exp", "name like 'y%y'") + .queryParam("sort", "id") + .get() + .wasOk().bodyEquals(3, "{\"id\":2}", "{\"id\":3}", "{\"id\":4}"); + } + + @Test + public void like_SingleChar_Pattern_Escape() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(2, "yxy") + .values(3, "y_y") + .values(4, "y_ay") + .values(5, "yyy") + .exec(); + + tester.target("/e2") + .queryParam("include", "id") + .queryParam("exp", "name like 'y@__y' escape '@'") + .queryParam("sort", "id") + .get() + .wasOk().bodyEquals(1, "{\"id\":4}"); + } + + @Test + public void like_MultiChar_Pattern_Escape() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(2, "yxy") + .values(3, "y%y") + .values(4, "y%ay") + .values(5, "yyy") + .exec(); + + tester.target("/e2") + .queryParam("include", "id") + .queryParam("exp", "name like 'y@%%y' escape '@'") + .queryParam("sort", "id") + .get() + .wasOk().bodyEquals(2, "{\"id\":3}", "{\"id\":4}"); + } + @Path("") public static class Resource { diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Exp_DisallowedIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Exp_DisallowedIT.java new file mode 100644 index 000000000..d43389f0c --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Exp_DisallowedIT.java @@ -0,0 +1,51 @@ +package io.agrest.cayenne.GET; + +import io.agrest.AgRequest; +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import org.junit.jupiter.api.Test; + +public class Exp_DisallowedIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class) + .build(); + + @Test + public void dbExp() { + + tester.e2() + .insertColumns("id_", "name", "address") + .values(1, "n1", "a1") + .values(2, "n2", "a2") + .exec(); + + tester.target("") + .queryParam("exp", "db:id_ = 1") + .get() + .wasBadRequest(); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @GET + public DataResponse all(@QueryParam("exp") String exp) { + AgRequest request = AgJaxrs.request(config).andExp(exp).build(); + return AgJaxrs.select(E2.class, config).request(request).get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Exp_PropFilter_Overlay_RequestIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Exp_PropFilter_Overlay_RequestIT.java new file mode 100644 index 000000000..b99cc8ccf --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Exp_PropFilter_Overlay_RequestIT.java @@ -0,0 +1,117 @@ +package io.agrest.cayenne.GET; + +import io.agrest.AgRequest; +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import org.junit.jupiter.api.Test; + +import java.sql.Date; +import java.time.LocalDate; + +public class Exp_PropFilter_Overlay_RequestIT extends MainDbTest { + + // testing condition described in https://github.com/agrestio/agrest/issues/641 + // running in "TEST_METHOD" scope to reset the runtime from possible cache poisoning + + @BQTestTool + final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class) + .build(); + + @Test + public void noNameThanName() { + + tester.e2() + .insertColumns("id_", "name", "address") + .values(1, "n1", "a1") + .values(2, "n2", "a2") + .exec(); + + tester.target("e2-sans-name") + // TODO: is this correct - we allow filtering on a non-readable attribute + .queryParam("exp", "name = 'n1'") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":1,\"address\":\"a1\"}"); + + tester.target("e2-all") + .queryParam("exp", "name = 'n1'") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":1,\"address\":\"a1\",\"name\":\"n1\"}"); + } + + @Test + public void nameAsDateThenAsString() { + + tester.e2() + .insertColumns("id_", "name", "address") + .values(1, "2021-01-01", "a1") + .values(2, "2021-01-02", "a2") + .exec(); + + tester.target("e2-name-as-date") + // TODO: is this correct - filtering on a redefined attribute? + .queryParam("exp", "name = '2021-01-01'") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":1,\"address\":\"a1\",\"name\":\"2021-01-01\"}"); + + tester.target("e2-all") + .queryParam("exp", "name = '2021-01-01'") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":1,\"address\":\"a1\",\"name\":\"2021-01-01\"}"); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e2-all") + public DataResponse all(@QueryParam("exp") String exp) { + AgRequest request = AgJaxrs.request(config).andExp(exp).build(); + return AgJaxrs.select(E2.class, config).request(request).get(); + } + + @GET + @Path("e2-sans-name") + public DataResponse sansName(@QueryParam("exp") String exp) { + AgRequest request = AgJaxrs.request(config).andExp(exp).build(); + return AgJaxrs.select(E2.class, config) + .request(request) + .propFilter(E2.class, b -> b.property("name", false)) + .get(); + } + + @GET + @Path("e2-name-as-date") + public DataResponse nameAsDate(@QueryParam("exp") String exp) { + AgRequest request = AgJaxrs.request(config) + .andExp(exp) + .build(); + + return AgJaxrs.select(E2.class, config) + .request(request) + .entityOverlay(AgEntity.overlay(E2.class).attribute( + "name", + // using SQL date for tests to match an existing converter in CayenneExpPostProcessor + Date.class, + e2 -> Date.valueOf(LocalDate.parse(e2.getName())))) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Exp_PropFilter_Overlay_RuntimeIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Exp_PropFilter_Overlay_RuntimeIT.java new file mode 100644 index 000000000..5704542d5 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Exp_PropFilter_Overlay_RuntimeIT.java @@ -0,0 +1,57 @@ +package io.agrest.cayenne.GET; + +import io.agrest.AgRequest; +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import org.junit.jupiter.api.Test; + +public class Exp_PropFilter_Overlay_RuntimeIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class) + .agCustomizer(c -> c.entityOverlay(AgEntity.overlay(E2.class) + .readablePropFilter(b -> b.property("name", false)) + .writablePropFilter(b -> b.property("name", false)))) + .build(); + + // TODO: is this correct - we allow filtering on a non-readable attribute? + @Test + public void expOnHiddenProperty() { + + tester.e2() + .insertColumns("id_", "name", "address") + .values(1, "n1", "a1") + .values(2, "n2", "a2") + .exec(); + + tester.target("") + .queryParam("exp", "name = 'n1'") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":1,\"address\":\"a1\"}"); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @GET + public DataResponse all(@QueryParam("exp") String exp) { + AgRequest request = AgJaxrs.request(config).andExp(exp).build(); + return AgJaxrs.select(E2.class, config).request(request).get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ExposedIdIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ExposedIdIT.java similarity index 79% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ExposedIdIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ExposedIdIT.java index 14297c079..1ebd93c26 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ExposedIdIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ExposedIdIT.java @@ -1,30 +1,30 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E23; import io.agrest.cayenne.cayenne.main.E26; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_ExposedIdIT extends DbTest { +public class ExposedIdIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E23.class, E26.class) .build(); @Test - public void testById() { + public void byId() { tester.e23().insertColumns("id", "name") .values(1, "abc") @@ -36,7 +36,7 @@ public void testById() { } @Test - public void testIncludeFrom() { + public void includeFrom() { tester.e23().insertColumns("id", "name").values(1, "abc").exec(); tester.e26().insertColumns("id", "e23_id").values(41, 1).exec(); @@ -49,7 +49,7 @@ public void testIncludeFrom() { } @Test - public void testIncludeTo() { + public void includeTo() { tester.e23().insertColumns("id", "name").values(1, "abc").exec(); tester.e26().insertColumns("id", "e23_id").values(41, 1).exec(); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IncludeIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/IncludeIT.java similarity index 79% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IncludeIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/IncludeIT.java index 3efe80019..de480c5cf 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IncludeIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/IncludeIT.java @@ -1,33 +1,33 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E15; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E5; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; -public class GET_IncludeIT extends DbTest { +public class IncludeIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entitiesAndDependencies(E2.class, E3.class, E5.class, E15.class) .build(); @Test - public void testRelated() { + public void related() { tester.e2().insertColumns("id_", "name") .values(1, "xxx").exec(); @@ -47,7 +47,7 @@ public void testRelated() { } @Test - public void testOrderOfInclude() { + public void orderOfInclude() { tester.e5().insertColumns("id", "name", "date").values(45, "T", "2013-01-03").exec(); tester.e2().insertColumns("id_", "name").values(8, "yyy").exec(); @@ -67,7 +67,7 @@ public void testOrderOfInclude() { } @Test - public void testPhantom() { + public void phantom() { tester.e5().insertColumns("id", "name", "date").values(45, "T", "2013-01-03").exec(); tester.e2().insertColumns("id_", "name").values(8, "yyy").exec(); tester.e3().insertColumns("id_", "name", "e2_id", "e5_id").values(3, "zzz", 8, 45).exec(); @@ -83,7 +83,7 @@ public void testPhantom() { } @Test - public void testPhantom_OverExplicitJoinTable() { + public void phantom_OverExplicitJoinTable() { tester.e1().insertColumns("id", "name") .values(1, "xxx") .values(2, "yyy").exec(); @@ -111,7 +111,7 @@ public void testPhantom_OverExplicitJoinTable() { } @Test - public void testStartLimit() { + public void startLimit() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); @@ -121,20 +121,32 @@ public void testStartLimit() { .values(10, "zzz", 1) .values(11, "zzz", 1).exec(); + // aligned with Cayenne "page" boundaries tester.target("/e3") .queryParam("include", "id", "e2.id") .queryParam("sort", "id") - .queryParam("start", "1") + .queryParam("start", "2") .queryParam("limit", "2") .get().wasOk().bodyEquals(4, - "{\"id\":9,\"e2\":{\"id\":1}}", - "{\"id\":10,\"e2\":{\"id\":1}}"); + "{\"id\":10,\"e2\":{\"id\":1}}", + "{\"id\":11,\"e2\":{\"id\":1}}"); // There are 3 queries, while our counter catches only 2 (the last query in paginated result is not reported). tester.assertQueryCount(2); - // TODO: e2 is fetched via a join.. If we have lots of E2s, this will be problematic + tester.target("/e3") + .queryParam("include", "id", "e2.id") + .queryParam("sort", "id") + .queryParam("start", "1") + .queryParam("limit", "2") + + .get().wasOk().bodyEquals(4, + "{\"id\":9,\"e2\":{\"id\":1}}", + "{\"id\":10,\"e2\":{\"id\":1}}"); + + // not aligned with Cayenne "page" boundaries ... extra query + tester.assertQueryCount(2 + 3); } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IncludeObjectCayenneExpIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/IncludeObjectCayenneExpIT.java similarity index 84% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IncludeObjectCayenneExpIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/IncludeObjectCayenneExpIT.java index 018f0ca2c..993dfc0f3 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IncludeObjectCayenneExpIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/IncludeObjectCayenneExpIT.java @@ -1,32 +1,32 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; import io.agrest.cayenne.cayenne.main.E5; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; @Deprecated -public class GET_IncludeObjectCayenneExpIT extends DbTest { +public class IncludeObjectCayenneExpIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entitiesAndDependencies(E2.class, E3.class, E4.class, E5.class) .build(); @Test - public void testMapBy_ToMany_WithCayenneExp() { + public void mapBy_ToMany_WithCayenneExp() { // see LF-294 - filter applied too late may cause a AgException @@ -47,7 +47,7 @@ public void testMapBy_ToMany_WithCayenneExp() { } @Test - public void testToMany_CayenneExp() { + public void toMany_CayenneExp() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); @@ -66,7 +66,7 @@ public void testToMany_CayenneExp() { } @Test - public void testToMany_CayenneExpById() { + public void toMany_CayenneExpById() { tester.e5().insertColumns("id", "name") .values(545, "B") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IncludeObjectIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/IncludeObjectIT.java similarity index 92% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IncludeObjectIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/IncludeObjectIT.java index 4ec1ab7d7..259269056 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_IncludeObjectIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/IncludeObjectIT.java @@ -1,31 +1,31 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; import io.agrest.cayenne.cayenne.main.E5; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_IncludeObjectIT extends DbTest { +public class IncludeObjectIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entitiesAndDependencies(E2.class, E3.class, E4.class, E5.class) .build(); @Test - public void testPathAttribute() { + public void pathAttribute() { tester.e4().insertColumns("c_int").values(55).exec(); @@ -36,7 +36,7 @@ public void testPathAttribute() { } @Test - public void testPathRelationship() { + public void pathRelationship() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id").values(8, "yyy", 1).exec(); @@ -50,7 +50,7 @@ public void testPathRelationship() { } @Test - public void testMapBy_ToOne() { + public void mapBy_ToOne() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id").values(8, "yyy", 1).exec(); @@ -65,7 +65,7 @@ public void testMapBy_ToOne() { } @Test - public void testMapBy_ToMany() { + public void mapBy_ToMany() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id") @@ -84,7 +84,7 @@ public void testMapBy_ToMany() { } @Test - public void testMapBy_ToMany_ById() { + public void mapBy_ToMany_ById() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id") @@ -104,7 +104,7 @@ public void testMapBy_ToMany_ById() { } @Test - public void testMapBy_ToMany_ByRelatedId() { + public void mapBy_ToMany_ByRelatedId() { tester.e5().insertColumns("id").values(45).values(46).exec(); tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); @@ -124,7 +124,7 @@ public void testMapBy_ToMany_ByRelatedId() { } @Test - public void testMapBy_ToMany_ByRelatedAttribute() { + public void mapBy_ToMany_ByRelatedAttribute() { tester.e5().insertColumns("id", "name") .values(45, "T") @@ -148,7 +148,7 @@ public void testMapBy_ToMany_ByRelatedAttribute() { } @Test - public void testMapBy_ToMany_ByRelatedDate() { + public void mapBy_ToMany_ByRelatedDate() { tester.e5().insertColumns("id", "name", "date") .values(45, "T", "2013-01-03") @@ -172,7 +172,7 @@ public void testMapBy_ToMany_ByRelatedDate() { } @Test - public void testMapBy_ToMany_WithExp() { + public void mapBy_ToMany_WithExp() { // see LF-294 - filter applied too late may cause a AgException @@ -193,7 +193,7 @@ public void testMapBy_ToMany_WithExp() { } @Test - public void testToMany_Sort() { + public void toMany_Sort() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id") @@ -211,7 +211,7 @@ public void testToMany_Sort() { } @Test - public void testToMany_SortPath() { + public void toMany_SortPath() { tester.e5().insertColumns("id", "name") .values(145, "B") @@ -236,7 +236,7 @@ public void testToMany_SortPath() { } @Test - public void testToMany_SortPath_Dir() { + public void toMany_SortPath_Dir() { tester.e5().insertColumns("id", "name") .values(245, "B") @@ -260,7 +260,7 @@ public void testToMany_SortPath_Dir() { } @Test - public void testToMany_Exp() { + public void toMany_Exp() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); @@ -279,7 +279,7 @@ public void testToMany_Exp() { } @Test - public void testToMany_ExpById() { + public void toMany_ExpById() { tester.e5().insertColumns("id", "name") .values(545, "B") @@ -300,7 +300,7 @@ public void testToMany_ExpById() { } @Test - public void testToMany_Exclude() { + public void toMany_Exclude() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id") @@ -318,7 +318,7 @@ public void testToMany_Exclude() { } @Test - public void testToMany_IncludeRelated() { + public void toMany_IncludeRelated() { tester.e5().insertColumns("id", "name") .values(345, "B") @@ -342,7 +342,7 @@ public void testToMany_IncludeRelated() { } @Test - public void testToMany_IncludeArrayRelated() { + public void toMany_IncludeArrayRelated() { tester.e5().insertColumns("id", "name") .values(345, "B") @@ -365,7 +365,7 @@ public void testToMany_IncludeArrayRelated() { } @Test - public void testToMany_IncludeMapRelated() { + public void toMany_IncludeMapRelated() { tester.e5().insertColumns("id", "name") .values(345, "B") @@ -388,7 +388,7 @@ public void testToMany_IncludeMapRelated() { } @Test - public void testToMany_IncludeExtMapRelated() { + public void toMany_IncludeExtMapRelated() { tester.e5().insertColumns("id", "name") .values(345, "B") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Include_MaxPathDepthIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Include_MaxPathDepthIT.java new file mode 100644 index 000000000..e09bc0ca7 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Include_MaxPathDepthIT.java @@ -0,0 +1,118 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.SelectBuilder; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.cayenne.main.E5; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class Include_MaxPathDepthIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E5.class) + .build(); + + @BeforeEach + void insertTestData() { + tester.e5().insertColumns("id", "name", "date").values(45, "T", "2013-01-03").exec(); + tester.e2().insertColumns("id_", "name").values(8, "yyy").exec(); + tester.e3().insertColumns("id_", "name", "e2_id", "e5_id").values(3, "zzz", 8, 45).exec(); + } + + @Test + public void depth100_Default() { + + tester.target("/e2") + .queryParam("include", "id", "e3s.id", "e3s.e5.id") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":8,\"e3s\":[{\"id\":3,\"e5\":{\"id\":45}}]}"); + } + + @Test + public void depth0_DefaultIncludes() { + + tester.target("/e2") + .queryParam("depth", 0) + .get() + .wasOk() + .bodyEquals(1, "{\"id\":8,\"address\":null,\"name\":\"yyy\"}"); + } + + @Test + public void depth0() { + + tester.target("/e2") + .queryParam("depth", 0) + .queryParam("include", "id", "e3s.id", "e3s.e5.id") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":8}"); + } + + @Test + public void depth1() { + + tester.target("/e2") + .queryParam("depth", 1) + .queryParam("include", "id", "e3s.id", "e3s.e5.id") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":8,\"e3s\":[{\"id\":3}]}"); + } + + @Test + public void depth2() { + + tester.target("/e2") + .queryParam("depth", 2) + .queryParam("include", "id", "e3s.id", "e3s.e5.id") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":8,\"e3s\":[{\"id\":3,\"e5\":{\"id\":45}}]}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e2") + public DataResponse getE2( + @Context UriInfo uriInfo, + + // This is for test only. Don't do that at home. Max include depth must not be + // controlled by the client + @QueryParam("depth") Integer depth) { + + SelectBuilder builder = AgJaxrs + .select(E2.class, config) + .clientParams(uriInfo.getQueryParameters()); + + if (depth != null) { + builder.maxPathDepth(depth); + } + + return builder.get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/InheritanceIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/InheritanceIT.java new file mode 100644 index 000000000..4b2c17a0e --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/InheritanceIT.java @@ -0,0 +1,241 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class InheritanceIT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .build(); + + @Test + public void superclass() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2", "a3") + .values(10, 1, "v01", "v11", null, null) + .values(20, 2, "v02", null, "v21", null) + .values(30, 3, "v03", "v13", null, "v31") + .exec(); + + tester.target("/ie1-super") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":10,\"a0\":\"v01\",\"a1\":\"v11\",\"type\":1}", + "{\"id\":20,\"a0\":\"v02\",\"a2\":\"v21\",\"type\":2}", + "{\"id\":30,\"a0\":\"v03\",\"a1\":\"v13\",\"a3\":\"v31\",\"type\":3}"); + } + + @Test + public void superclass_Includes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2", "a3") + .values(10, 1, "v01", "v11", null, null) + .values(20, 2, "v02", null, "v21", null) + .values(30, 3, "v03", "v13", null, "v31") + .exec(); + + tester.target("/ie1-super") + // super and sub attributes should be supported + .queryParam("include", "id", "a0", "a1", "a3") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":10,\"a0\":\"v01\",\"a1\":\"v11\"}", + "{\"id\":20,\"a0\":\"v02\"}", + "{\"id\":30,\"a0\":\"v03\",\"a1\":\"v13\",\"a3\":\"v31\"}"); + } + + @Test + public void superclass_Includes_Relationships() { + + tester.ie2().insertColumns("id") + .values(1) + .values(2) + .exec(); + + tester.ie1().insertColumns("id", "type", "e2_id") + .values(10, 1, 2) + .values(20, 2, null) + .exec(); + + tester.ie3().insertColumns("id", "e1_id") + .values(1, 10) + .values(2, 20) + .exec(); + + + tester.target("/ie1-super") + // super and sub relationships should be supported + .queryParam("include", "id", "ie2", "ie3s") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"ie2\":{\"id\":2},\"ie3s\":[{\"id\":1}]}", + "{\"id\":20,\"ie3s\":[{\"id\":2}]}"); + } + + @Test + public void superclass_Excludes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2", "a3") + .values(10, 1, "v01", "v11", null, null) + .values(20, 2, "v02", null, "v21", null) + .values(30, 3, "v03", "v13", null, "v31") + .exec(); + + tester.target("/ie1-super") + // super and sub attributes should be supported + .queryParam("exclude", "a0", "a1", "a3") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":10,\"type\":1}", + "{\"id\":20,\"a2\":\"v21\",\"type\":2}", + "{\"id\":30,\"type\":3}"); + } + + @Test + public void subclass() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(20, 2, "v02", null, "v21") + .exec(); + + tester.target("/ie1-sub1") + .get() + .wasOk() + .bodyEquals(1, "{\"id\":10,\"a0\":\"v01\",\"a1\":\"v11\",\"type\":1}"); + } + + @Test + public void relatedSuperclass() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2", "a3") + .values(10, 1, "v01", "v11", null, null) + .values(20, 2, "v02", null, "v21", null) + .values(30, 3, "v03", "v13", null, "v31") + .exec(); + + tester.ie3().insertColumns("id", "e1_id") + .values(1, 10) + .values(2, 20) + .values(3, 30) + .exec(); + + tester.target("/ie3") + .queryParam("include", "id", "ie1") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":1,\"ie1\":{\"id\":10,\"a0\":\"v01\",\"a1\":\"v11\",\"type\":1}}", + "{\"id\":2,\"ie1\":{\"id\":20,\"a0\":\"v02\",\"a2\":\"v21\",\"type\":2}}", + "{\"id\":3,\"ie1\":{\"id\":30,\"a0\":\"v03\",\"a1\":\"v13\",\"a3\":\"v31\",\"type\":3}}"); + } + + @Test + public void relatedSuperclass_IncludeRelationships() { + + tester.ie2().insertColumns("id") + .values(1) + .values(2) + .exec(); + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2", "a3", "e2_id") + .values(10, 1, "v01", "v11", null, null, 2) + .values(20, 2, "v02", null, "v21", null, null) + .values(30, 3, "v03", "v13", null, "v31", 1) + .exec(); + + tester.ie3().insertColumns("id", "e1_id") + .values(1, 10) + .values(2, 20) + .values(3, 30) + .exec(); + + tester.target("/ie3") + .queryParam("include", "id", "ie1.type", "ie1.ie2.id", "ie1.ie3s.id") + // adding an "exp" caused issue #590 + .queryParam("exp", "id > 0") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":1,\"ie1\":{\"ie2\":{\"id\":2},\"ie3s\":[{\"id\":1}],\"type\":1}}", + "{\"id\":2,\"ie1\":{\"ie3s\":[{\"id\":2}],\"type\":2}}", + "{\"id\":3,\"ie1\":{\"ie2\":{\"id\":1},\"ie3s\":[{\"id\":3}],\"type\":3}}"); + } + + @Test + public void relatedSubclass() { + + tester.ie2().insertColumns("id") + .values(1) + .values(2) + .exec(); + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2", "e2_id") + .values(10, 1, "v01", "v10", null, 1) + .values(15, 1, "v02", "v15", null, 2) + .values(20, 2, "v03", null, "v2", null) + .exec(); + + tester.target("/ie2") + .queryParam("include", "id", "ie1s") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":1,\"ie1s\":[{\"id\":10,\"a0\":\"v01\",\"a1\":\"v10\",\"type\":1}]}", + "{\"id\":2,\"ie1s\":[{\"id\":15,\"a0\":\"v02\",\"a1\":\"v15\",\"type\":1}]}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("ie1-super") + public DataResponse getIe1Super(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config).clientParams(uriInfo.getQueryParameters()).get(); + } + + @GET + @Path("ie1-sub1") + public DataResponse getIe1Sub1(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Sub1.class, config).clientParams(uriInfo.getQueryParameters()).get(); + } + + @GET + @Path("ie2") + public DataResponse getIE2(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie2.class, config).clientParams(uriInfo.getQueryParameters()).get(); + } + + @GET + @Path("ie3") + public DataResponse getIE3(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie3.class, config).clientParams(uriInfo.getQueryParameters()).get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/MapByIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/MapByIT.java new file mode 100644 index 000000000..423513883 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/MapByIT.java @@ -0,0 +1,106 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.cayenne.main.E4; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class MapByIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E4.class) + .build(); + + @Test + public void mapByRootEntity() { + + tester.e4().insertColumns("c_varchar", "c_int").values("xxx", 1) + .values("yyy", 2) + .values("zzz", 2).exec(); + + tester.target("/e4") + .queryParam("mapBy", "cInt") + .queryParam("include", "cVarchar") + + .get().wasOk().bodyEqualsMapBy(3, + "\"1\":[{\"cVarchar\":\"xxx\"}]", + "\"2\":[{\"cVarchar\":\"yyy\"},{\"cVarchar\":\"zzz\"}]"); + } + + @Test + public void mapBy_RelatedId() { + + tester.e2().insertColumns("id_", "name") + .values(1, "zzz") + .values(2, "yyy").exec(); + + tester.e3().insertColumns("id_", "name", "e2_id") + .values(8, "aaa", 1) + .values(9, "bbb", 1) + .values(10, "ccc", 2).exec(); + + tester.target("/e3") + .queryParam("mapBy", "e2.id") + .queryParam("exclude", "phoneNumber") + + .get().wasOk().bodyEqualsMapBy(3, + "\"1\":[{\"id\":8,\"name\":\"aaa\"},{\"id\":9,\"name\":\"bbb\"}]", + "\"2\":[{\"id\":10,\"name\":\"ccc\"}]"); + } + + @Test + public void mapBy_OverRelationship() { + + tester.e2().insertColumns("id_", "name") + .values(1, "zzz") + .values(2, "yyy").exec(); + + tester.e3().insertColumns("id_", "name", "e2_id") + .values(8, "aaa", 1) + .values(9, "bbb", 1) + .values(10, "ccc", 2).exec(); + + tester.target("/e3") + .queryParam("mapBy", "e2") + .queryParam("exclude", "phoneNumber") + + .get().wasOk().bodyEqualsMapBy(3, + "\"1\":[{\"id\":8,\"name\":\"aaa\"},{\"id\":9,\"name\":\"bbb\"}]", + "\"2\":[{\"id\":10,\"name\":\"ccc\"}]"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e3") + public DataResponse getE3(@Context UriInfo uriInfo) { + return AgJaxrs.select(E3.class, config).clientParams(uriInfo.getQueryParameters()).get(); + } + + @GET + @Path("e4") + public DataResponse getE4(@Context UriInfo uriInfo) { + return AgJaxrs.select(E4.class, config).clientParams(uriInfo.getQueryParameters()).get(); + } + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/MapBy_MaxPathDepthIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/MapBy_MaxPathDepthIT.java new file mode 100644 index 000000000..23499b230 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/MapBy_MaxPathDepthIT.java @@ -0,0 +1,83 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.SelectBuilder; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class MapBy_MaxPathDepthIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class) + .build(); + + @BeforeEach + void insertTestData() { + tester.e2().insertColumns("id_", "name").values(8, "yyy").exec(); + tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", 8).exec(); + } + + @Test + public void depth100_Default() { + tester.target("/e3") + .queryParam("include", "id") + .queryParam("mapBy", "e2.id") + .get() + .wasOk() + .bodyEquals("{\"data\":{\"8\":[{\"id\":3}]},\"total\":1}"); + } + + @Test + public void depth0() { + tester.target("/e3") + .queryParam("depth", 0) + .queryParam("include", "id") + .queryParam("mapBy", "e2.id") + .get() + .wasBadRequest(); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e3") + public DataResponse getE3( + @Context UriInfo uriInfo, + + // This is for test only. Don't do that at home. Max include depth must not be + // controlled by the client + @QueryParam("depth") Integer depth) { + + SelectBuilder builder = AgJaxrs + .select(E3.class, config) + .clientParams(uriInfo.getQueryParameters()); + + if (depth != null) { + builder.maxPathDepth(depth); + } + + return builder.get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_NaturalIdIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/NaturalIdIT.java similarity index 80% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_NaturalIdIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/NaturalIdIT.java index 13660383c..1c5ae68e1 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_NaturalIdIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/NaturalIdIT.java @@ -1,34 +1,33 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E20; import io.agrest.cayenne.cayenne.main.E21; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.HashMap; import java.util.Map; -public class GET_NaturalIdIT extends DbTest { +public class NaturalIdIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - - .entities(E20.class, E21.class) + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E20.class, E21.class) .build(); @Test - public void test_SelectById() { + public void selectById() { tester.e20().insertColumns("name_col").values("John").exec(); @@ -43,7 +42,7 @@ public void test_SelectById() { } @Test - public void test_SelectById_MultiId() { + public void selectById_MultiId() { tester.e21().insertColumns("age", "name").values(18, "John").exec(); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_PersistentWithExtraAnnotatedPropertiesIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PersistentWithExtraAnnotatedPropertiesIT.java similarity index 76% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_PersistentWithExtraAnnotatedPropertiesIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PersistentWithExtraAnnotatedPropertiesIT.java index 589c2cb0f..de2926a10 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_PersistentWithExtraAnnotatedPropertiesIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PersistentWithExtraAnnotatedPropertiesIT.java @@ -1,37 +1,33 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.SelectStage; import io.agrest.cayenne.cayenne.main.E14; import io.agrest.cayenne.cayenne.main.E15; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; -import io.agrest.jaxrs2.pojo.model.P7; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.jaxrs3.junit.pojo.P7; import io.agrest.runtime.processor.select.SelectContext; import io.bootique.junit5.BQTestTool; import org.apache.cayenne.Cayenne; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_PersistentWithExtraAnnotatedPropertiesIT extends DbTest { +public class PersistentWithExtraAnnotatedPropertiesIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - - .entities(E14.class, E15.class) + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E14.class, E15.class) .build(); - // TODO: each test is using the same dataset... if we could only do data cleanup once per class, then we can load - // the test data in constructor - @Test - public void testGET_Root() { + public void root() { tester.e15().insertColumns("long_id", "name").values(1L, "xxx").exec(); tester.e14().insertColumns("e15_id", "long_id", "name").values(1L, 8L, "yyy").exec(); @@ -42,7 +38,7 @@ public void testGET_Root() { } @Test - public void testIncludeRelationship() { + public void includeRelationship() { tester.e15().insertColumns("long_id", "name").values(1L, "xxx").exec(); tester.e14().insertColumns("e15_id", "long_id", "name").values(1L, 8L, "yyy").exec(); @@ -54,7 +50,7 @@ public void testIncludeRelationship() { } @Test - public void testGET_Related() { + public void includeRelationshipAttributes() { tester.e15().insertColumns("long_id", "name").values(1L, "xxx").exec(); tester.e14().insertColumns("e15_id", "long_id", "name").values(1L, 8L, "yyy").exec(); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Annotations_InheritanceIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Annotations_InheritanceIT.java new file mode 100644 index 000000000..63df83e25 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Annotations_InheritanceIT.java @@ -0,0 +1,89 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Aie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Aie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class PropFilter_Annotations_InheritanceIT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Aie1Super.class, Ie2.class, Ie3.class) + .build(); + + @Test + public void aie1super() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2", "a3") + .values(10, 1, "v01", "v11", null, null) + .values(15, 3, "v01", "v12", null, "v31") + .values(20, 2, "v02", null, "v21", null) + .exec(); + + tester.target("/aie1super") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":10,\"a0\":\"v01\",\"a1\":\"v11\",\"type\":1}", + "{\"id\":15,\"a0\":\"v01\",\"a1\":\"v12\",\"a3\":\"v31\",\"type\":3}", + "{\"id\":20,\"a2\":\"v21\",\"type\":2}"); + } + + @Test + public void aie1super_RequestOverlay() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2", "a3") + .values(10, 1, "v01", "v11", null, null) + .values(15, 3, "v01", "v12", null, "v31") + .values(20, 2, "v02", null, "v21", null) + .exec(); + + tester.target("/aie1super-request-overlay") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":10,\"a0\":\"v01\",\"type\":1}", + "{\"id\":15,\"a0\":\"v01\",\"a3\":\"v31\",\"type\":3}", + "{\"id\":20,\"a2\":\"v21\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("aie1super") + public DataResponse ie1super1(@Context UriInfo uriInfo) { + return AgJaxrs.select(Aie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + + @GET + @Path("aie1super-request-overlay") + public DataResponse ie1super2(@Context UriInfo uriInfo) { + return AgJaxrs.select(Aie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .propFilter(Aie1Sub1.class, p -> p.property("a1", false)) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ReadAccess_OverlayIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_OverlayIT.java similarity index 84% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ReadAccess_OverlayIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_OverlayIT.java index c9688a986..ddc05f371 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ReadAccess_OverlayIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_OverlayIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.access.PropertyFilteringRulesBuilder; @@ -6,28 +6,28 @@ import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_ReadAccess_OverlayIT extends DbTest { +public class PropFilter_OverlayIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class, E14.class) .build(); @Test - public void testImplicit() { + public void implicit() { tester.e4().insertColumns("id", "c_varchar", "c_int").values(1, "xxx", 5).exec(); @@ -37,7 +37,7 @@ public void testImplicit() { } @Test - public void testExplicit() { + public void explicit() { tester.e4().insertColumns("id", "c_varchar", "c_int").values(1, "xxx", 5).exec(); @@ -48,7 +48,7 @@ public void testExplicit() { } @Test - public void testExplicit_ToMany() { + public void explicit_ToMany() { // make sure we have e3s for more than one e2 - this will help us // confirm that relationship queries are properly filtered. @@ -66,7 +66,7 @@ public void testExplicit_ToMany() { } @Test - public void testId() { + public void id() { tester.e14().insertColumns("long_id", "name") .values(5L, "aaa") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Request_InheritanceIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Request_InheritanceIT.java new file mode 100644 index 000000000..cd992074c --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Request_InheritanceIT.java @@ -0,0 +1,112 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class PropFilter_Overlay_Request_InheritanceIT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .build(); + + @Test + public void super_ExcludeSuperAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(20, 2, "v02", null, "v21") + .exec(); + + tester.target("/ie1super-exclude-super-attributes") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a1\":\"v11\",\"type\":1}", + "{\"id\":20,\"a2\":\"v21\",\"type\":2}"); + } + + @Test + public void super_ExcludeSubAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(20, 2, "v02", null, "v21") + .exec(); + + tester.target("/ie1super-exclude-sub-attributes") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a0\":\"v01\",\"type\":1}", + "{\"id\":20,\"a0\":\"v02\",\"a2\":\"v21\",\"type\":2}"); + } + + @Test + public void super_ReincludeSuperAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(20, 2, "v02", null, "v21") + .exec(); + + tester.target("/ie1super-reinclude-super-attributes") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a0\":\"v01\",\"a1\":\"v11\",\"type\":1}", + "{\"id\":20,\"a2\":\"v21\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("ie1super-exclude-super-attributes") + public DataResponse ie1super1(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .propFilter(Ie1Super.class, r -> r.attributes(true).property("a0", false)) + .get(); + } + + @GET + @Path("ie1super-exclude-sub-attributes") + public DataResponse ie1super2(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .propFilter(Ie1Sub1.class, r -> r.attributes(true).property("a1", false)) + .get(); + } + + @GET + @Path("ie1super-reinclude-super-attributes") + public DataResponse ie1super3(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .propFilter(Ie1Super.class, r -> r.attributes(true).property("a0", false)) + .propFilter(Ie1Sub1.class, r -> r.attributes(true).property("a0", true)) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Runtime_Inheritance1IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Runtime_Inheritance1IT.java new file mode 100644 index 000000000..368048779 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Runtime_Inheritance1IT.java @@ -0,0 +1,60 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class PropFilter_Overlay_Runtime_Inheritance1IT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .agCustomizer(b -> b.entityOverlay(AgEntity.overlay(Ie1Super.class).readablePropFilter(r -> r.attributes(true).property("a0", false)))) + .build(); + + @Test + public void super_ExcludeSuperAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(20, 2, "v02", null, "v21") + .exec(); + + tester.target("/") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a1\":\"v11\",\"type\":1}", + "{\"id\":20,\"a2\":\"v21\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + public DataResponse ie1super1(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Runtime_Inheritance2IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Runtime_Inheritance2IT.java new file mode 100644 index 000000000..2367c6ebd --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Runtime_Inheritance2IT.java @@ -0,0 +1,61 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class PropFilter_Overlay_Runtime_Inheritance2IT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .agCustomizer(b -> b.entityOverlay(AgEntity.overlay(Ie1Sub1.class).readablePropFilter(r -> r.attributes(true).property("a1", false)))) + .build(); + + @Test + public void super_ExcludeSubAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(20, 2, "v02", null, "v21") + .exec(); + + tester.target("/") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a0\":\"v01\",\"type\":1}", + "{\"id\":20,\"a0\":\"v02\",\"a2\":\"v21\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + public DataResponse ie1super2(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Runtime_Inheritance3IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Runtime_Inheritance3IT.java new file mode 100644 index 000000000..f575a6d2a --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/PropFilter_Overlay_Runtime_Inheritance3IT.java @@ -0,0 +1,63 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class PropFilter_Overlay_Runtime_Inheritance3IT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .agCustomizer(b -> b + .entityOverlay(AgEntity.overlay(Ie1Super.class).readablePropFilter(r -> r.attributes(true).property("a0", false))) + .entityOverlay(AgEntity.overlay(Ie1Sub1.class).readablePropFilter(r -> r.attributes(true).property("a0", true)))) + .build(); + + @Test + public void super_ReincludeSuperAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(20, 2, "v02", null, "v21") + .exec(); + + tester.target("/") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a0\":\"v01\",\"a1\":\"v11\",\"type\":1}", + "{\"id\":20,\"a2\":\"v21\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + public DataResponse ie1super3(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ReadAccess_AnnotationsIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_AnnotationsIT.java similarity index 57% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ReadAccess_AnnotationsIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_AnnotationsIT.java index 222ba279d..3d9269f7b 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ReadAccess_AnnotationsIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_AnnotationsIT.java @@ -1,29 +1,29 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E10; import io.agrest.cayenne.cayenne.main.E11; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_ReadAccess_AnnotationsIT extends DbTest { +public class ReadFilter_AnnotationsIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E10.class, E11.class) .build(); @Test - public void testAnnotationReadableFlag_Attributes() { + public void annotationReadableFlag_Attributes() { tester.e10().insertColumns("id", "c_varchar", "c_int", "c_boolean", "c_date") .values(1, "xxx", 5, true, "2014-01-02").exec(); @@ -35,7 +35,7 @@ public void testAnnotationReadableFlag_Attributes() { } @Test - public void testAnnotationReadableFlag_Relationship() { + public void annotationReadableFlag_Relationship() { tester.e10().insertColumns("id", "c_varchar", "c_int", "c_boolean", "c_date") .values(1, "xxx", 5, true, "2014-01-02").exec(); @@ -49,6 +49,22 @@ public void testAnnotationReadableFlag_Relationship() { .bodyEquals(1, "{\"id\":1,\"cBoolean\":true,\"cInt\":5,\"e11s\":[{\"address\":\"aaa\"}]}"); } + @Test + public void readWithInclude_ToManyToOne_MiddleIdNotReadable() { + + tester.e10().insertColumns("id", "c_boolean") + .values(1, true).exec(); + + tester.e11().insertColumns("id", "e10_id") + .values(15, 1).exec(); + + tester.target("e10") + .queryParam("include", "cBoolean", "e11s.e10.cBoolean") + .get() + .wasOk() + .bodyEquals(1, "{\"cBoolean\":true,\"e11s\":[{\"e10\":{\"cBoolean\":true}}]}"); + } + @Path("") public static class Resource { diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ReadFilterIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_OverlayIT.java similarity index 89% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ReadFilterIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_OverlayIT.java index 012971933..6550da07d 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_ReadFilterIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_OverlayIT.java @@ -1,30 +1,30 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.access.ReadFilter; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.bootique.junit5.BQTestTool; import org.apache.cayenne.Cayenne; import org.apache.cayenne.DataObject; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_ReadFilterIT extends DbTest { +public class ReadFilter_OverlayIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class) .agCustomizer(ab -> ab .entityOverlay(AgEntity.overlay(E2.class).readFilter(evenFilter())) @@ -42,7 +42,7 @@ static ReadFilter oddFilter() { } @Test - public void testRootFilter_InRequest() { + public void rootFilter_InRequest() { tester.e4().insertColumns("id", "c_varchar") .values(1, "a") @@ -61,7 +61,7 @@ public void testRootFilter_InRequest() { } @Test - public void testRelatedFilter_ToOne() { + public void relatedFilter_ToOne() { tester.e2().insertColumns("id_") .values(2) @@ -83,7 +83,7 @@ public void testRelatedFilter_ToOne() { } @Test - public void testRelatedFilter_ToMany() { + public void relatedFilter_ToMany() { tester.e2().insertColumns("id_") .values(2) @@ -104,7 +104,7 @@ public void testRelatedFilter_ToMany() { } @Test - public void testFilter_InStack() { + public void filter_InStack() { tester.e4().insertColumns("id").values(1).values(2).exec(); @@ -116,7 +116,7 @@ public void testFilter_InStack() { } @Test - public void testFilteredPagination1() { + public void filteredPagination1() { tester.e4().insertColumns("id") .values(1) @@ -140,7 +140,7 @@ public void testFilteredPagination1() { } @Test - public void testFilteredPagination2() { + public void filteredPagination2() { tester.e4().insertColumns("id") .values(1) @@ -164,7 +164,7 @@ public void testFilteredPagination2() { } @Test - public void testFilteredPagination3() { + public void filteredPagination3() { tester.e4().insertColumns("id") .values(1) diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Request_InheritanceIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Request_InheritanceIT.java new file mode 100644 index 000000000..63d8ae2d5 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Request_InheritanceIT.java @@ -0,0 +1,150 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class ReadFilter_Overlay_Request_InheritanceIT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .build(); + + @Test + public void super_FilterSuperAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "allowed_v01", "v11", null) + .values(20, 2, "v02", null, "v21") + .values(30, 2, "allowed_v02", null, "v22") + .exec(); + + tester.target("/ie1super-filter-super-attributes") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a0\":\"allowed_v01\",\"a1\":\"v11\",\"type\":1}", + "{\"id\":30,\"a0\":\"allowed_v02\",\"a2\":\"v22\",\"type\":2}"); + } + + @Test + public void super_FilterSubAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "allowed_v11", null) + .values(15, 1, "v01", "v12", null) + .values(20, 2, "v02", null, "v21") + .exec(); + + tester.target("/ie1super-filter-sub-attributes") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a0\":\"v01\",\"a1\":\"allowed_v11\",\"type\":1}", + "{\"id\":20,\"a0\":\"v02\",\"a2\":\"v21\",\"type\":2}"); + } + + @Test + public void super_FilterSuperAttributesAtSub() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(15, 1, "allowed_v01", "v12", null) + .values(20, 2, "allowed_v02", null, "v21") + .values(25, 2, "v02", null, "v22") + .exec(); + + tester.target("/ie1super-filter-super-attributes-at-sub") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":15,\"a0\":\"allowed_v01\",\"a1\":\"v12\",\"type\":1}", + "{\"id\":20,\"a0\":\"allowed_v02\",\"a2\":\"v21\",\"type\":2}"); + } + + @Test + public void super_FilterSuperAttributesAtSubIgnoreSuper() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(15, 1, "v01", "v12", null) + .values(20, 2, "allowed_v02", null, "v21") + .values(25, 2, "v02", null, "v22") + .exec(); + + tester.target("/ie1super-filter-super-attributes-at-sub-ignore-super") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":10,\"a0\":\"v01\",\"a1\":\"v11\",\"type\":1}", + "{\"id\":15,\"a0\":\"v01\",\"a1\":\"v12\",\"type\":1}", + "{\"id\":20,\"a0\":\"allowed_v02\",\"a2\":\"v21\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("ie1super-filter-super-attributes") + public DataResponse ie1super1(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .entityOverlay(AgEntity.overlay(Ie1Super.class).readFilter(o -> o.getA0().startsWith("allowed"))) + .get(); + } + + @GET + @Path("ie1super-filter-sub-attributes") + public DataResponse ie1super2(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .entityOverlay(AgEntity.overlay(Ie1Sub1.class).readFilter(o -> o.getA1().startsWith("allowed"))) + .get(); + } + + @GET + @Path("ie1super-filter-super-attributes-at-sub") + public DataResponse ie1super3(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .entityOverlay(AgEntity.overlay(Ie1Super.class).readFilter(o -> o.getA0().startsWith("allowed"))) + + // this filter is combined with the super filter, and hence has no effect + .entityOverlay(AgEntity.overlay(Ie1Sub1.class).readFilter(o -> true)) + .get(); + } + + @GET + @Path("ie1super-filter-super-attributes-at-sub-ignore-super") + public DataResponse ie1super4(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .entityOverlay(AgEntity.overlay(Ie1Super.class).readFilter(o -> o.getA0().startsWith("allowed"))) + + // this filter overrides the super filter + .entityOverlay(AgEntity.overlay(Ie1Sub1.class).readFilter(o -> true).ignoreSuperReadFilter()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance1IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance1IT.java new file mode 100644 index 000000000..399bb517c --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance1IT.java @@ -0,0 +1,61 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class ReadFilter_Overlay_Runtime_Inheritance1IT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .agCustomizer(c -> c.entityOverlay(AgEntity.overlay(Ie1Super.class).readFilter(o -> o.getA0().startsWith("allowed")))) + .build(); + + @Test + public void super_FilterSuperAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "allowed_v01", "v11", null) + .values(20, 2, "v02", null, "v21") + .values(30, 2, "allowed_v02", null, "v22") + .exec(); + + tester.target("/") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a0\":\"allowed_v01\",\"a1\":\"v11\",\"type\":1}", + "{\"id\":30,\"a0\":\"allowed_v02\",\"a2\":\"v22\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + public DataResponse ie1super1(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance2IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance2IT.java new file mode 100644 index 000000000..2becee17c --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance2IT.java @@ -0,0 +1,62 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class ReadFilter_Overlay_Runtime_Inheritance2IT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .agCustomizer(c -> c.entityOverlay(AgEntity.overlay(Ie1Sub1.class).readFilter(o -> o.getA1().startsWith("allowed")))) + .build(); + + @Test + public void super_FilterSubAttributes() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "allowed_v11", null) + .values(15, 1, "v01", "v12", null) + .values(20, 2, "v02", null, "v21") + .exec(); + + tester.target("/") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":10,\"a0\":\"v01\",\"a1\":\"allowed_v11\",\"type\":1}", + "{\"id\":20,\"a0\":\"v02\",\"a2\":\"v21\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + public DataResponse ie1super2(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance3IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance3IT.java new file mode 100644 index 000000000..a68292132 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance3IT.java @@ -0,0 +1,67 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class ReadFilter_Overlay_Runtime_Inheritance3IT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .agCustomizer(c -> c + .entityOverlay(AgEntity.overlay(Ie1Super.class).readFilter(o -> o.getA0().startsWith("allowed"))) + + // this filter is combined with the super filter, and hence has no effect + .entityOverlay(AgEntity.overlay(Ie1Sub1.class).readFilter(o -> true))) + .build(); + + @Test + public void super_FilterSuperAttributesAtSub() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(15, 1, "allowed_v01", "v12", null) + .values(20, 2, "allowed_v02", null, "v21") + .values(25, 2, "v02", null, "v22") + .exec(); + + tester.target("/") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":15,\"a0\":\"allowed_v01\",\"a1\":\"v12\",\"type\":1}", + "{\"id\":20,\"a0\":\"allowed_v02\",\"a2\":\"v21\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + public DataResponse ie1super3(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance4IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance4IT.java new file mode 100644 index 000000000..313adc6cb --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/ReadFilter_Overlay_Runtime_Inheritance4IT.java @@ -0,0 +1,68 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.cayenne.inheritance.Ie3; +import io.agrest.cayenne.unit.inheritance.InheritanceDbTest; +import io.agrest.cayenne.unit.inheritance.InheritanceModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class ReadFilter_Overlay_Runtime_Inheritance4IT extends InheritanceDbTest { + + @BQTestTool + static final InheritanceModelTester tester = tester(Resource.class) + .entities(Ie1Super.class, Ie2.class, Ie3.class) + .agCustomizer(c -> c + .entityOverlay(AgEntity.overlay(Ie1Super.class).readFilter(o -> o.getA0().startsWith("allowed"))) + + // this filter overrides the super filter + .entityOverlay(AgEntity.overlay(Ie1Sub1.class).readFilter(o -> true).ignoreSuperReadFilter())) + .build(); + + @Test + public void super_FilterSuperAttributesAtSubIgnoreSuper() { + + tester.ie1().insertColumns("id", "type", "a0", "a1", "a2") + .values(10, 1, "v01", "v11", null) + .values(15, 1, "v01", "v12", null) + .values(20, 2, "allowed_v02", null, "v21") + .values(25, 2, "v02", null, "v22") + .exec(); + + tester.target("/") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":10,\"a0\":\"v01\",\"a1\":\"v11\",\"type\":1}", + "{\"id\":15,\"a0\":\"v01\",\"a1\":\"v12\",\"type\":1}", + "{\"id\":20,\"a0\":\"allowed_v02\",\"a2\":\"v21\",\"type\":2}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + public DataResponse ie1super4(@Context UriInfo uriInfo) { + return AgJaxrs.select(Ie1Super.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Related_IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/RelatedIT.java similarity index 90% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Related_IT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/RelatedIT.java index 3dfb6c02f..679d46e59 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Related_IT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/RelatedIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E12; @@ -10,32 +10,32 @@ import io.agrest.cayenne.cayenne.main.E29; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E30; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.MatrixParam; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.HashMap; import java.util.Map; -public class GET_Related_IT extends DbTest { +public class RelatedIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E17.class, E18.class) .entitiesAndDependencies(E12.class, E13.class, E29.class) .build(); @Test - public void testToMany_CompoundId() { + public void toMany_CompoundId() { tester.e17().insertColumns("id1", "id2", "name") .values(1, 1, "aaa") @@ -53,7 +53,7 @@ public void testToMany_CompoundId() { } @Test - public void testValidRel_ToOne_CompoundId() { + public void validRel_ToOne_CompoundId() { tester.e17().insertColumns("id1", "id2", "name") .values(1, 1, "aaa") @@ -69,7 +69,7 @@ public void testValidRel_ToOne_CompoundId() { } @Test - public void testValidRel_ToMany() { + public void validRel_ToMany() { // make sure we have e3s for more than one e2 - this will help us // confirm that relationship queries are properly filtered. @@ -87,7 +87,7 @@ public void testValidRel_ToMany() { } @Test - public void testValidRel_ToOne() { + public void validRel_ToOne() { // make sure we have e3s for more than one e2 - this will help us // confirm that relationship queries are properly filtered. @@ -105,14 +105,14 @@ public void testValidRel_ToOne() { } @Test - public void testInvalidRel() { + public void invalidRel() { tester.target("/e2/1/dummyrel").get() .wasServerError() .bodyEquals("{\"message\":\"Invalid parent relationship: 'dummyrel'\"}"); } @Test - public void testToManyJoin() { + public void toManyJoin() { tester.e12().insertColumns("id") .values(11) @@ -136,7 +136,7 @@ public void testToManyJoin() { } @Test - public void testByParentCompoundDbId() { + public void byParentCompoundDbId() { tester.e29().insertColumns("id1", "id2") .values(1, 15) diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Request_EntityAttributeIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Request_EntityAttributeIT.java similarity index 79% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Request_EntityAttributeIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Request_EntityAttributeIT.java index 5c0dd1c40..4ce1e15cc 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Request_EntityAttributeIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Request_EntityAttributeIT.java @@ -1,30 +1,30 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.apache.cayenne.Cayenne; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_Request_EntityAttributeIT extends DbTest { +public class Request_EntityAttributeIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E3.class, E4.class) .build(); @Test - public void testRequest_Property() { + public void request_Property() { tester.e4().insertColumns("id").values(1).values(2).exec(); @@ -37,7 +37,7 @@ public void testRequest_Property() { } @Test - public void testRequest_Property_Exclude() { + public void request_Property_Exclude() { tester.e4().insertColumns("id").values(1).values(2).exec(); @@ -49,7 +49,7 @@ public void testRequest_Property_Exclude() { } @Test - public void testRequest_ShadowProperty() { + public void request_ShadowProperty() { tester.e3().insertColumns("id_", "name") .values(1, "x") @@ -63,7 +63,7 @@ public void testRequest_ShadowProperty() { } @Test - public void testRequest_ShadowProperty_Exclude() { + public void request_ShadowProperty_Exclude() { tester.e3().insertColumns("id_", "name") .values(1, "x") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_AutoDetectIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_AutoDetectIT.java new file mode 100644 index 000000000..f655e19b7 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_AutoDetectIT.java @@ -0,0 +1,97 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class Resolvers_AutoDetectIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entities(E2.class, E3.class) + .build(); + + @Test + public void viaParentExpResolver() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(2, "yyy") + .exec(); + + tester.e3().insertColumns("id_", "name", "e2_id") + .values(8, "yyy", 1) + .values(9, "zzz", null) + .values(10, "aaa", 2) + .exec(); + + tester.target("/e3") + .queryParam("include", "id", "name", "e2.name") + .get() + .wasOk() + .bodyEquals(3, + "{\"id\":8,\"e2\":{\"name\":\"xxx\"},\"name\":\"yyy\"}", + "{\"id\":9,\"e2\":null,\"name\":\"zzz\"}", + "{\"id\":10,\"e2\":{\"name\":\"yyy\"},\"name\":\"aaa\"}"); + + tester.assertQueryCount(2); + } + + @Test + public void viaParentIdsResolver() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(2, "yyy") + .exec(); + + tester.e3().insertColumns("id_", "name", "e2_id") + .values(8, "yyy", 1) + .values(9, "zzz", null) + .values(10, "aaa", 2) + .values(11, "bbb", 2) + .values(12, "ccc", 2) + .exec(); + + tester.target("/e3") + .queryParam("include", "id", "name", "e2.name") + .queryParam("start", 1) + .queryParam("limit", 2) + .get() + .wasOk() + .bodyEquals(5, + "{\"id\":9,\"e2\":null,\"name\":\"zzz\"}", + "{\"id\":10,\"e2\":{\"name\":\"yyy\"},\"name\":\"aaa\"}"); + + tester.assertQueryCount(3); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e3") + public DataResponse e3(@Context UriInfo uriInfo) { + return AgJaxrs.select(E3.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_CombinationsIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_CombinationsIT.java similarity index 81% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_CombinationsIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_CombinationsIT.java index 0346b2af8..b4d444738 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_CombinationsIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_CombinationsIT.java @@ -1,62 +1,43 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; +import io.agrest.cayenne.CayenneResolvers; import io.agrest.cayenne.cayenne.main.E15; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E5; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.agrest.resolver.RelatedDataResolverFactory; import io.bootique.junit5.BQTestTool; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; import java.util.stream.Stream; -public class GET_Resolvers_CombinationsIT extends DbTest { +public class Resolvers_CombinationsIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E5.class) .entitiesAndDependencies(E15.class) - // manually manage data... we only create it once for all test permutations - .doNotCleanData() .build(); - @AfterAll - static void cleanup() { - tester.e3().deleteAll(); - tester.e15_5().deleteAll(); - tester.e5().deleteAll(); - tester.e2().deleteAll(); - tester.e14().deleteAll(); - tester.e15().deleteAll(); - } - - @BeforeAll - static void loadData() { - - tester.e3().deleteAll(); - tester.e15_5().deleteAll(); - tester.e5().deleteAll(); - tester.e2().deleteAll(); - tester.e14().deleteAll(); - tester.e15().deleteAll(); + @BeforeEach + void loadData() { tester.e5().insertColumns("id", "name") .values(1, "e5_1") @@ -108,7 +89,7 @@ private static Stream provideTestInputs() { @ParameterizedTest @MethodSource("provideTestInputs") - public void test_ToManyToOne(Overlay o1, Overlay o2, int queryCount) { + public void toManyToOne(Overlay o1, Overlay o2, int queryCount) { tester.target("/tomany_toone") .queryParam("include", "id") @@ -129,7 +110,7 @@ public void test_ToManyToOne(Overlay o1, Overlay o2, int queryCount) { @ParameterizedTest @MethodSource("provideTestInputs") - public void test_ToOneToMany(Overlay o1, Overlay o2, int queryCount) { + public void toOneToMany(Overlay o1, Overlay o2, int queryCount) { tester.target("/toone_tomany") .queryParam("include", "id") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_MixedIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_MixedIT.java similarity index 88% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_MixedIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_MixedIT.java index f0e34f53f..2b2dd80b2 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_MixedIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_MixedIT.java @@ -1,13 +1,14 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; +import io.agrest.cayenne.CayenneResolvers; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E5; import io.agrest.cayenne.persister.ICayennePersister; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.bootique.junit5.BQTestTool; @@ -18,23 +19,23 @@ import org.apache.cayenne.query.SelectById; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; -public class GET_Resolvers_MixedIT extends DbTest { +public class Resolvers_MixedIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E5.class) .build(); @Test - public void test_alt_resolver__parentids_joint_prefetch() { + public void alt_resolver__parentids_joint_prefetch() { tester.e5().insertColumns("id", "name") .values(1, "e5_1") @@ -67,7 +68,7 @@ public void test_alt_resolver__parentids_joint_prefetch() { } @Test - public void test_alt_mix_up_relations() { + public void alt_mix_up_relations() { tester.e5().insertColumns("id", "name") .values(1, "e5_1") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_PojoToPersistentIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_PojoToPersistentIT.java similarity index 73% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_PojoToPersistentIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_PojoToPersistentIT.java index fbdd04822..9e9a8f707 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_PojoToPersistentIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_PojoToPersistentIT.java @@ -1,38 +1,39 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.AgException; import io.agrest.AgResponse; import io.agrest.SimpleResponse; +import io.agrest.cayenne.CayenneResolvers; import io.agrest.cayenne.cayenne.main.E25; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; -import io.agrest.meta.AgEntity; -import io.agrest.meta.AgEntityOverlay; import io.agrest.cayenne.pojo.model.PX1; import io.agrest.cayenne.pojo.runtime.PX1RootResolver; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.meta.AgEntity; +import io.agrest.meta.AgEntityOverlay; import io.agrest.resolver.RelatedDataResolverFactory; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; -public class GET_Resolvers_PojoToPersistentIT extends DbTest { +public class Resolvers_PojoToPersistentIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class).entities(E25.class).build(); + static final MainModelTester tester = tester(Resource.class).entities(E25.class).build(); @ParameterizedTest - @EnumSource(GET_Resolvers_CombinationsIT.Overlay.class) - public void testFail(GET_Resolvers_CombinationsIT.Overlay overlay) { + @EnumSource(Resolvers_CombinationsIT.Overlay.class) + public void fail(Resolvers_CombinationsIT.Overlay overlay) { tester.target("/px1") .queryParam("overlay", overlay) @@ -52,7 +53,7 @@ public static class Resource { @GET @Path("px1") public AgResponse px1( - @QueryParam("overlay") GET_Resolvers_CombinationsIT.Overlay overlay, + @QueryParam("overlay") Resolvers_CombinationsIT.Overlay overlay, @Context UriInfo uriInfo) { AgEntityOverlay px1Overlay = AgEntity @@ -73,7 +74,7 @@ public AgResponse px1( } } - RelatedDataResolverFactory resolverFactory(GET_Resolvers_CombinationsIT.Overlay o) { + RelatedDataResolverFactory resolverFactory(Resolvers_CombinationsIT.Overlay o) { switch (o) { case joint: return CayenneResolvers.relatedViaParentPrefetch(); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_Related_ToManyIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_Related_ToManyIT.java similarity index 83% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_Related_ToManyIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_Related_ToManyIT.java index f9628e3c8..671600eec 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_Related_ToManyIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_Related_ToManyIT.java @@ -1,33 +1,34 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; +import io.agrest.cayenne.CayenneResolvers; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; -public class GET_Resolvers_Related_ToManyIT extends DbTest { +public class Resolvers_Related_ToManyIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class) .build(); @Test - public void test_JointPrefetchResolver() { + public void jointPrefetchResolver() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -53,7 +54,7 @@ public void test_JointPrefetchResolver() { } @Test - public void test_QueryWithParentIdsResolver() { + public void queryWithParentIdsResolver() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_Related_ToOneIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_Related_ToOneIT.java similarity index 87% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_Related_ToOneIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_Related_ToOneIT.java index 99f069cf4..d3232abd1 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_Related_ToOneIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_Related_ToOneIT.java @@ -1,33 +1,34 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; +import io.agrest.cayenne.CayenneResolvers; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; -public class GET_Resolvers_Related_ToOneIT extends DbTest { +public class Resolvers_Related_ToOneIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class) .build(); @Test - public void test_JointPrefetchResolver() { + public void jointPrefetchResolver() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id") @@ -50,7 +51,7 @@ public void test_JointPrefetchResolver() { } @Test - public void test_QueryWithParentIdsResolver() { + public void queryWithParentIdsResolver() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id") @@ -72,7 +73,7 @@ public void test_QueryWithParentIdsResolver() { } @Test - public void test_QueryWithParentIdsResolver_Pagination() { + public void queryWithParentIdsResolver_Pagination() { tester.e2().insertColumns("id_", "name") .values(1, "aaa") @@ -101,7 +102,7 @@ public void test_QueryWithParentIdsResolver_Pagination() { } @Test - public void test_QueryWithParentQualifierResolver() { + public void queryWithParentQualifierResolver() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id") @@ -123,7 +124,7 @@ public void test_QueryWithParentQualifierResolver() { } @Test - public void test_QueryWithParentQualifierResolver_NoFetchIfNoParent() { + public void queryWithParentQualifierResolver_NoFetchIfNoParent() { tester.e2().insertColumns("id_", "name").values(1, "xxx").exec(); tester.e3().insertColumns("id_", "name", "e2_id") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_RootIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_RootIT.java similarity index 87% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_RootIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_RootIT.java index 64f182288..bf0990bce 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_Resolvers_RootIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Resolvers_RootIT.java @@ -1,12 +1,13 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; +import io.agrest.cayenne.CayenneResolvers; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.auto._E2; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.agrest.resolver.BaseRootDataResolver; @@ -15,26 +16,26 @@ import org.apache.cayenne.ObjectId; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; import java.util.List; import static java.util.Arrays.asList; -public class GET_Resolvers_RootIT extends DbTest { +public class Resolvers_RootIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class) .build(); @Test - public void test_ViaQueryResolver() { + public void viaQueryResolver() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -59,7 +60,7 @@ public void test_ViaQueryResolver() { } @Test - public void test_ViaCustomResolver() { + public void viaCustomResolver() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_SizeConstraintsIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/SizeConstraintsIT.java similarity index 75% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_SizeConstraintsIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/SizeConstraintsIT.java index 47e45f495..5c1bae55e 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_SizeConstraintsIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/SizeConstraintsIT.java @@ -1,30 +1,30 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class GET_SizeConstraintsIT extends DbTest { +public class SizeConstraintsIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E4.class) .build(); // TODO: unclear what server-side fetch offset protects? so not testing it here. @Test - public void testNoClientLimit() { + public void noClientLimit() { tester.e4().insertColumns("id") .values(1) @@ -39,7 +39,7 @@ public void testNoClientLimit() { } @Test - public void testClientLimitBelowServerLimit() { + public void clientLimitBelowServerLimit() { tester.e4().insertColumns("id") .values(1) @@ -55,7 +55,7 @@ public void testClientLimitBelowServerLimit() { } @Test - public void testClientLimitExceedsServerLimit() { + public void clientLimitExceedsServerLimit() { tester.e4().insertColumns("id") .values(1) diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/SkipNullsIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/SkipNullsIT.java new file mode 100644 index 000000000..218bba218 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/SkipNullsIT.java @@ -0,0 +1,102 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.agrest.runtime.AgRuntimeBuilder; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; + +public class SkipNullsIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class) + .agCustomizer(AgRuntimeBuilder::skipNullProperties) + .build(); + + @Test + public void attributes() { + + tester.e3().insertColumns("id_", "name") + .values(6, null) + .values(7, "yyy") + .exec(); + + tester.target("/e3") + .queryParam("include", "id", "name") + .queryParam("sort", "id") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":6}", + "{\"id\":7,\"name\":\"yyy\"}"); + } + + @Test + public void relationships() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx").exec(); + + tester.e3().insertColumns("id_", "e2_id") + .values(6, 1) + .values(7, null) + .exec(); + + tester.target("/e3") + .queryParam("include", "id", "e2.id") + .queryParam("sort", "id") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":6,\"e2\":{\"id\":1}}", + "{\"id\":7}"); + } + + @Test + public void relatedAttributes() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(2, null).exec(); + + tester.e3().insertColumns("id_", "e2_id") + .values(6, 1) + .values(7, 2) + .exec(); + + tester.target("/e3") + .queryParam("include", "id", "e2") + .queryParam("sort", "id") + .get() + .wasOk() + .bodyEquals(2, + "{\"id\":6,\"e2\":{\"id\":1,\"name\":\"xxx\"}}", + "{\"id\":7,\"e2\":{\"id\":2}}"); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e3") + public DataResponse getE3(@Context UriInfo uriInfo) { + return AgJaxrs.select(E3.class, config) + .clientParams(uriInfo.getQueryParameters()) + .get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/SortIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/SortIT.java new file mode 100644 index 000000000..6999205b6 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/SortIT.java @@ -0,0 +1,68 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E4; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class SortIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E4.class) + .build(); + + + @Test + public void sort_ById() { + + tester.e4().insertColumns("id") + .values(2) + .values(1) + .values(3).exec(); + + tester.target("/e4") + .queryParam("sort", "[{\"property\":\"id\",\"direction\":\"DESC\"}]") + .queryParam("include", "id") + .get() + .wasOk() + .bodyEquals(3, "{\"id\":3}", "{\"id\":2}", "{\"id\":1}"); + } + + @Test + public void sort_Invalid() { + + tester.target("/e4") + .queryParam("sort", "[{\"property\":\"xyz\",\"direction\":\"DESC\"}]") + .queryParam("include", "id") + .get() + .wasBadRequest() + .bodyEquals("{\"message\":\"Invalid path 'xyz' for 'E4'\"}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e4") + public DataResponse getE4(@Context UriInfo uriInfo) { + return AgJaxrs.select(E4.class, config).clientParams(uriInfo.getQueryParameters()).get(); + } + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Sort_MaxPathDepthIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Sort_MaxPathDepthIT.java new file mode 100644 index 000000000..53246f156 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/Sort_MaxPathDepthIT.java @@ -0,0 +1,94 @@ +package io.agrest.cayenne.GET; + +import io.agrest.DataResponse; +import io.agrest.SelectBuilder; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class Sort_MaxPathDepthIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class) + .build(); + + @BeforeEach + void insertTestData() { + tester.e2().insertColumns("id_", "name").values(8, "yyy").values(7, "aaa").exec(); + tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", 8).values(4, "bbb", 7).exec(); + } + + @Test + public void depth100_Default() { + tester.target("/e3") + .queryParam("include", "id") + .queryParam("sort", "e2.name") + .get() + .wasOk() + .bodyEquals(2, "{\"id\":4}", "{\"id\":3}"); + } + + @Test + public void depth0() { + tester.target("/e3") + .queryParam("depth", 0) + .queryParam("include", "id") + .queryParam("sort", "name") + .get() + .wasOk() + .bodyEquals(2, "{\"id\":4}", "{\"id\":3}"); + } + + @Test + public void depth0_ByRelated() { + tester.target("/e3") + .queryParam("depth", 0) + .queryParam("include", "id") + .queryParam("sort", "e2.name") + .get() + .wasBadRequest(); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @GET + @Path("e3") + public DataResponse getE3( + @Context UriInfo uriInfo, + + // This is for test only. Don't do that at home. Max include depth must not be + // controlled by the client + @QueryParam("depth") Integer depth) { + + SelectBuilder builder = AgJaxrs + .select(E3.class, config) + .clientParams(uriInfo.getQueryParameters()); + + if (depth != null) { + builder.maxPathDepth(depth); + } + + return builder.get(); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_StagesIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/StagesIT.java similarity index 54% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/GET_StagesIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/GET/StagesIT.java index 3506ec286..5c07ed01b 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/GET_StagesIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/GET/StagesIT.java @@ -1,44 +1,51 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.GET; import com.fasterxml.jackson.core.JsonGenerator; import io.agrest.DataResponse; import io.agrest.SelectStage; import io.agrest.cayenne.cayenne.main.E27Nopk; +import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.persister.ICayennePersister; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; import io.agrest.encoder.DataResponseEncoder; import io.agrest.encoder.Encoder; -import io.agrest.encoder.GenericEncoder; -import io.agrest.encoder.ListEncoder; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.runtime.processor.select.SelectContext; import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import org.apache.cayenne.query.ObjectSelect; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.util.List; -public class GET_StagesIT extends DbTest { +public class StagesIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E27Nopk.class) + static final MainModelTester tester = tester(Resource.class) + .entities(E3.class, E27Nopk.class) .build(); @Test - public void testNoId() { + public void terminalStageOnSTART() { + tester.target("/e3-terminate-on-START") + .get() + .wasOk() + .bodyEquals("{\"data\":[],\"total\":0}"); + } + + @Test + public void terminalStageOnAPPLY_SERVER_PARAMS() { tester.e27NoPk().insertColumns("name") .values("z") .values("a").exec(); - tester.target("/e27") + tester.target("/e27-terminate-on-APPLY_SERVER_PARAMS") .get() .wasOk() .bodyEquals(2, "{\"name\":\"a\"},{\"name\":\"z\"}"); @@ -51,8 +58,20 @@ public static class Resource { private Configuration config; @GET - @Path("e27") - public DataResponse get(@Context UriInfo uriInfo) { + @Path("e3-terminate-on-START") + public DataResponse e27_TerminateOnSTART(@Context UriInfo uriInfo) { + + // terminate early with no action - must result in an empty DataResponse + return AgJaxrs.select(E3.class, config) + .clientParams(uriInfo.getQueryParameters()) + .terminalStage(SelectStage.START, c -> { + }) + .get(); + } + + @GET + @Path("e27-terminate-on-APPLY_SERVER_PARAMS") + public DataResponse e27(@Context UriInfo uriInfo) { // since Cayenne won't be able to fetch objects with no id, our only option is ColumnSelect and a custom encoder return AgJaxrs.select(E27Nopk.class, config) @@ -69,9 +88,7 @@ private void fetchAll(SelectContext context) { context.getEntity().setData(names); - Encoder rowEncoder = new NoIdEncoder(); - ListEncoder listEncoder = new ListEncoder(rowEncoder); - Encoder encoder = new DataResponseEncoder("data", listEncoder, "total", GenericEncoder.encoder()); + Encoder encoder = DataResponseEncoder.withElementEncoder(new NoIdEncoder()); context.setEncoder(encoder); } } @@ -79,8 +96,7 @@ private void fetchAll(SelectContext context) { static class NoIdEncoder implements Encoder { @Override - public void encode(String propertyName, Object object, JsonGenerator out) throws IOException { - + public void encode(String propertyName, Object object, boolean skipNullProperties, JsonGenerator out) throws IOException { out.writeStartObject(); out.writeStringField("name", object == null ? null : object.toString()); out.writeEndObject(); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_AgRequestIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/AgRequestIT.java similarity index 79% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/POST_AgRequestIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/POST/AgRequestIT.java index 9a9a18f2a..51b423d15 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_AgRequestIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/AgRequestIT.java @@ -1,30 +1,30 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.POST; import io.agrest.AgRequest; import io.agrest.DataResponse; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import org.junit.jupiter.api.Test; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; -public class POST_AgRequestIT extends DbTest { +public class AgRequestIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E3.class) .build(); @Test - public void testIncludes_OverriddenByAgRequest() { + public void includes_OverriddenByAgRequest() { tester.target("/e3_includes") .queryParam("include", "id") @@ -34,7 +34,7 @@ public void testIncludes_OverriddenByAgRequest() { } @Test - public void testExcludes_OverriddenByAgRequest() { + public void excludes_OverriddenByAgRequest() { tester.target("/e3_excludes") .queryParam("exclude", "name") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/BasicIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/BasicIT.java new file mode 100644 index 000000000..610fab846 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/BasicIT.java @@ -0,0 +1,146 @@ +package io.agrest.cayenne.POST; + + +import io.agrest.DataResponse; +import io.agrest.SimpleResponse; +import io.agrest.cayenne.cayenne.main.E17; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.cayenne.main.E31; +import io.agrest.cayenne.cayenne.main.E4; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; +import java.util.HashMap; +import java.util.Map; + +public class BasicIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entities(E3.class, E4.class, E17.class, E31.class) + .build(); + + @Test + public void basic() { + + tester.target("/e4").post("{\"cVarchar\":\"zzz\"}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"id\":RID,\"cBoolean\":null,\"cDate\":null,\"cDecimal\":null,\"cInt\":null," + + "\"cTime\":null,\"cTimestamp\":null,\"cVarchar\":\"zzz\"}"); + + tester.e4().matcher().assertOneMatch(); + tester.e4().matcher().eq("c_varchar", "zzz").assertOneMatch(); + + tester.target("/e4").post("{\"cVarchar\":\"TTTT\"}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"id\":RID,\"cBoolean\":null,\"cDate\":null,\"cDecimal\":null,\"cInt\":null," + + "\"cTime\":null,\"cTimestamp\":null,\"cVarchar\":\"TTTT\"}"); + + tester.e4().matcher().assertMatches(2); + tester.e4().matcher().eq("c_varchar", "TTTT").assertOneMatch(); + } + + @Test + public void idCalledId() { + + tester.target("/e31").post("{\"id\":5,\"name\":\"31\"}") + .wasCreated() + .bodyEquals(1, "{\"id\":5,\"name\":\"31\"}"); + + tester.e31().matcher().assertOneMatch(); + tester.e31().matcher().eq("id", 5L).assertOneMatch(); + } + + @Test + public void compoundId() { + + tester.target("/e17") + .queryParam("id1", 1) + .queryParam("id2", 1) + .post("{\"name\":\"xxx\"}") + .wasCreated() + .bodyEquals(1, "{\"id\":{\"id1\":1,\"id2\":1},\"id1\":1,\"id2\":1,\"name\":\"xxx\"}"); + } + + @Test + public void sync_NoData() { + + tester.target("/e4/sync") + .post("{\"cVarchar\":\"zzz\"}") + .wasCreated() + .bodyEquals("{}"); + + tester.e4().matcher().assertOneMatch(); + tester.e4().matcher().eq("c_varchar", "zzz").assertOneMatch(); + } + + @Test + public void bulk() { + + tester.target("/e3/") + .queryParam("exclude", "id") + .queryParam("include", E3.NAME.getName()) + .post("[{\"name\":\"aaa\"},{\"name\":\"zzz\"},{\"name\":\"bbb\"},{\"name\":\"yyy\"}]") + .wasCreated() + // ordering from request must be preserved... + .bodyEquals(4, "{\"name\":\"aaa\"},{\"name\":\"zzz\"},{\"name\":\"bbb\"},{\"name\":\"yyy\"}"); + } + + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @POST + @Path("e3") + public DataResponse create(@Context UriInfo uriInfo, String requestBody) { + return AgJaxrs.create(E3.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(requestBody); + } + + @POST + @Path("e4") + public DataResponse createE4(String requestBody) { + return AgJaxrs.create(E4.class, config).syncAndSelect(requestBody); + } + + @POST + @Path("e4/sync") + public SimpleResponse createE4_DefaultData(String requestBody) { + return AgJaxrs.create(E4.class, config).sync(requestBody); + } + + @POST + @Path("e17") + public DataResponse createE17( + @Context UriInfo uriInfo, + @QueryParam("id1") Integer id1, + @QueryParam("id2") Integer id2, + String requestBody) { + + Map ids = new HashMap<>(); + ids.put(E17.ID1.getName(), id1); + ids.put(E17.ID2.getName(), id2); + + return AgJaxrs.create(E17.class, config).byId(ids).syncAndSelect(requestBody); + } + + @POST + @Path("e31") + public DataResponse createE31(@Context UriInfo uriInfo, String requestBody) { + return AgJaxrs.create(E31.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(requestBody); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/ConvertersIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/ConvertersIT.java new file mode 100644 index 000000000..ded68c52d --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/ConvertersIT.java @@ -0,0 +1,215 @@ +package io.agrest.cayenne.POST; + + +import io.agrest.DataResponse; +import io.agrest.SimpleResponse; +import io.agrest.cayenne.cayenne.main.E16; +import io.agrest.cayenne.cayenne.main.E19; +import io.agrest.cayenne.cayenne.main.E4; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.List; + +public class ConvertersIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entities(E4.class, E16.class, E19.class) + .build(); + + @Test + public void string() { + + tester.target("/e4") + .queryParam("include", "cVarchar") + .post("{\"cVarchar\":\"zzz\"}") + .wasCreated() + .bodyEquals(1, "{\"cVarchar\":\"zzz\"}"); + + tester.e4().matcher().assertOneMatch(); + tester.e4().matcher().eq("c_varchar", "zzz").assertOneMatch(); + } + + @Test + public void _boolean() { + + tester.target("/e4") + .queryParam("include", "cBoolean") + .post("{\"cBoolean\":true}") + .wasCreated() + .bodyEquals(1, "{\"cBoolean\":true}"); + + tester.e4().matcher().assertOneMatch(); + tester.e4().matcher().eq("c_boolean", "true").assertOneMatch(); + } + + @Test + public void sqlDate() { + tester.target("e16") + .queryParam("include", "cDate") + .post("{\"cDate\":\"2015-03-14\"}") + .wasCreated() + .bodyEquals(1, "{\"cDate\":\"2015-03-14\"}"); + } + + @Test + public void sqlTime() { + tester.target("e16") + .queryParam("include", "cTime") + .post("{\"cTime\":\"19:00:00\"}") + .wasCreated() + // TODO: why is time returned back without a "T" prefix? + .bodyEquals(1, "{\"cTime\":\"19:00:00\"}"); + } + + @Test + public void sqlTimestamp() { + tester.target("e16") + .queryParam("include", "cTimestamp") + .post("{\"cTimestamp\":\"2015-03-14T19:00:00.000\"}") + .wasCreated() + // TODO: why is time returned back without a "T" prefix? + .bodyEquals(1, "{\"cTimestamp\":\"2015-03-14T19:00:00\"}"); + } + + @Test + public void byteArray() { + + String base64Encoded = "c29tZVZhbHVlMTIz"; // someValue123 + + tester.target("/e19") + .queryParam("include", E19.GUID.getName()) + .post("{\"guid\":\"" + base64Encoded + "\"}") + .wasCreated() + .bodyEquals(1, "{\"guid\":\"" + base64Encoded + "\"}"); + } + + @Test + public void bigDecimal() { + + tester.target("/e19") + .queryParam("include", E19.BIG_DECIMAL.getName()) + .post("{\"bigDecimal\":123456789.12}") + .wasCreated() + .bodyEquals(1, "{\"bigDecimal\":123456789.12}"); + tester.e19().matcher().eq("big_decimal", new BigDecimal("123456789.12")).assertOneMatch(); + } + + @Test + public void bigInteger() { + + tester.target("/e19") + .queryParam("include", E19.BIG_INTEGER.getName()) + .post("{\"bigInteger\":123456789}") + .wasCreated() + .bodyEquals(1, "{\"bigInteger\":123456789}"); + tester.e19().matcher().eq("big_integer", new BigInteger("123456789")).assertOneMatch(); + } + + @Test + public void _byte() { + + tester.target("/e19") + .queryParam("include", E19.BYTE_OBJECT.getName(), E19.BYTE_PRIMITIVE.getName()) + .post("{\"byteObject\":1,\"bytePrimitive\":2}") + .wasCreated() + .bodyEquals(1, "{\"byteObject\":1,\"bytePrimitive\":2}"); + tester.e19().matcher().eq("byte_object", 1).andEq("byte_primitive", 2).assertOneMatch(); + } + + @Test + public void _short() { + + tester.target("/e19") + .queryParam("include", E19.SHORT_OBJECT.getName(), E19.SHORT_PRIMITIVE.getName()) + .post("{\"shortObject\":1,\"shortPrimitive\":2}") + .wasCreated() + .bodyEquals(1, "{\"shortObject\":1,\"shortPrimitive\":2}"); + tester.e19().matcher().eq("short_object", 1).andEq("short_primitive", 2).assertOneMatch(); + } + + @Test + public void _float() { + tester.target("/e19/float") + .queryParam("include", "floatObject", "floatPrimitive") + .post("{\"floatObject\":1.0,\"floatPrimitive\":2.0}") + .wasCreated() + .bodyEquals(1, "{\"floatObject\":1.0,\"floatPrimitive\":2.0}"); + tester.e19().matcher().eq("float_object", 1.0).andEq("float_primitive", 2.0).assertOneMatch(); + } + + @Test + public void float_FromInt() { + tester.target("/e19/float") + .queryParam("include", "floatObject", "floatPrimitive") + .post("{\"floatObject\":1,\"floatPrimitive\":2}") + .wasCreated() + .bodyEquals(1, "{\"floatObject\":1.0,\"floatPrimitive\":2.0}"); + tester.e19().matcher().eq("float_object", 1.0).andEq("float_primitive", 2.0).assertOneMatch(); + } + + @Test + public void _double() { + tester.target("/e19/double").post("{\"doubleObject\":1.0,\"doublePrimitive\":2.0}").wasCreated(); + tester.e19().matcher().eq("double_object", 1.0).andEq("double_primitive", 2.0).assertOneMatch(); + } + + @Test + public void double_FromInt() { + tester.target("/e19/double").post("{\"doubleObject\":1,\"doublePrimitive\":2}").wasCreated(); + tester.e19().matcher().eq("double_object", 1.0).andEq("double_primitive", 2.0).assertOneMatch(); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @POST + @Path("e4") + public DataResponse createE4(@QueryParam("include") List includes, String requestBody) { + return AgJaxrs.create(E4.class, config) + .request(AgJaxrs.request(config).addIncludes(includes).build()) + .syncAndSelect(requestBody); + } + + @POST + @Path("e16") + public DataResponse createE16(@QueryParam("include") List includes, String requestBody) { + return AgJaxrs.create(E16.class, config) + .request(AgJaxrs.request(config).addIncludes(includes).build()) + .syncAndSelect(requestBody); + } + + @POST + @Path("e19") + public DataResponse createE19(@Context UriInfo uriInfo, String data) { + return AgJaxrs.create(E19.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(data); + } + + @POST + @Path("e19/float") + public DataResponse createE19_FloatAttribute(@Context UriInfo uriInfo, String data) { + return AgJaxrs.create(E19.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(data); + } + + @POST + @Path("e19/double") + public SimpleResponse create_E19_DoubleAttribute(String entityData) { + return AgJaxrs.create(E19.class, config).sync(entityData); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_NaturalIdIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/NaturalIdIT.java similarity index 79% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/POST_NaturalIdIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/POST/NaturalIdIT.java index 7f6cfdf4e..216fc8816 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_NaturalIdIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/NaturalIdIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.POST; import io.agrest.DataResponse; @@ -6,27 +6,27 @@ import io.agrest.cayenne.cayenne.main.E20; import io.agrest.cayenne.cayenne.main.E21; import io.agrest.cayenne.cayenne.main.E29; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class POST_NaturalIdIT extends DbTest { +public class NaturalIdIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E20.class, E21.class, E29.class) + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E20.class, E21.class, E29.class) .build(); @Test - public void testSingleId() { + public void singleId() { tester.target("/single-id") .queryParam("exclude", "age", "description") @@ -44,7 +44,7 @@ public void testSingleId() { } @Test - public void testMultiId() { + public void multiId() { tester.target("/multi-id") .queryParam("exclude", "description") @@ -52,7 +52,7 @@ public void testMultiId() { .wasCreated() .bodyEquals(1, "{\"id\":{\"age\":18,\"name\":\"John\"},\"age\":18,\"name\":\"John\"}"); - tester.e21().matcher().eq("name", "John").eq("age", 18).assertOneMatch(); + tester.e21().matcher().eq("name", "John").andEq("age", 18).assertOneMatch(); tester.target("/multi-id").queryParam("exclude", "description") .post("{\"id\":{\"age\":18,\"name\":\"John\"}}") @@ -61,14 +61,14 @@ public void testMultiId() { } @Test - public void testMultiId_MixedDbObj() { + public void multiId_MixedDbObj() { tester.target("/mixed-multi-id") .post("{\"id\":{\"db:id1\":18,\"id2Prop\":345}}") .wasCreated() .bodyEquals(1, "{\"id\":{\"db:id1\":18,\"id2Prop\":345},\"id2Prop\":345}"); - tester.e29().matcher().eq("id1", 18).eq("id2", 345).assertOneMatch(); + tester.e29().matcher().eq("id1", 18).andEq("id2", 345).assertOneMatch(); tester.target("/mixed-multi-id") .post("{\"id\":{\"db:id1\":18,\"id2Prop\":345}}") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/NestedIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/NestedIT.java new file mode 100644 index 000000000..ba4b271db --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/NestedIT.java @@ -0,0 +1,69 @@ +package io.agrest.cayenne.POST; + +import io.agrest.DataResponse; +import io.agrest.EntityUpdate; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; +import org.apache.cayenne.Cayenne; +import org.junit.jupiter.api.Test; + +public class NestedIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class) + .build(); + + @Test + public void toOne_AsNewObject() { + + tester.target("/e3") + .queryParam("include", "name", "e2.id") + .post("{\"e2\":{\"name\":\"new_to_one\"},\"name\":\"MM\"}") + .wasCreated() + .bodyEquals(1, "{\"e2\":null,\"name\":\"MM\"}"); + + tester.e3().matcher().assertOneMatch(); + tester.e2().matcher().assertOneMatch(); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @POST + @Path("e3") + public DataResponse create(@Context UriInfo uriInfo, EntityUpdate update) { + + // While Agrest does not yet support processing full related objects, + // we should be able to manually save related objects + + DataResponse e3Response = AgJaxrs.create(E3.class, config) + .clientParams(uriInfo.getQueryParameters()) + .syncAndSelect(update); + + int e2Id = Cayenne.intPKForObject(e3Response.getData().get(0)); + + EntityUpdate e2Update = update.getToOne(E3.E2.getName()); + if (e2Update != null) { + AgJaxrs + .create(E2.class, config) + .parent(E3.class, e2Id, E3.E2.getName()) + .sync(e2Update); + } + + return e3Response; + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_Related_IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/ParentIT.java similarity index 77% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/POST_Related_IT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/POST/ParentIT.java index f4d960bec..1c1fe61a8 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_Related_IT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/ParentIT.java @@ -1,34 +1,40 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.POST; import io.agrest.DataResponse; -import io.agrest.cayenne.cayenne.main.*; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.cayenne.main.E12; +import io.agrest.cayenne.cayenne.main.E12E13; +import io.agrest.cayenne.cayenne.main.E13; +import io.agrest.cayenne.cayenne.main.E17; +import io.agrest.cayenne.cayenne.main.E18; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.MatrixParam; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.MatrixParam; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.HashMap; import java.util.Map; -public class POST_Related_IT extends DbTest { +public class ParentIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E17.class, E18.class) .entitiesAndDependencies(E12.class, E13.class) .build(); @Test - public void testRelate_ToMany_New() { + public void relate_ToMany_New() { tester.e2().insertColumns("id_", "name").values(24, "xxx").exec(); @@ -39,11 +45,11 @@ public void testRelate_ToMany_New() { .bodyEquals(1, "{\"id\":RID,\"name\":\"zzz\",\"phoneNumber\":null}"); tester.e3().matcher().assertOneMatch(); - tester.e3().matcher().eq("e2_id", 24).eq("name", "zzz").assertOneMatch(); + tester.e3().matcher().eq("e2_id", 24).andEq("name", "zzz").assertOneMatch(); } @Test - public void testRelate_ToMany_New_CompoundId() { + public void relate_ToMany_New_CompoundId() { tester.e17().insertColumns("id1", "id2", "name") .values(1, 1, "aaa").exec(); @@ -58,11 +64,11 @@ public void testRelate_ToMany_New_CompoundId() { tester.e18().matcher().assertOneMatch(); - tester.e18().matcher().eq("e17_id1", 1).eq("e17_id2", 1).eq("name", "xxx").assertOneMatch(); + tester.e18().matcher().eq("e17_id1", 1).andEq("e17_id2", 1).andEq("name", "xxx").assertOneMatch(); } @Test - public void testRelate_ToMany_MixedCollection() { + public void relate_ToMany_MixedCollection() { tester.e2().insertColumns("id_", "name") .values(15, "xxx") @@ -96,7 +102,7 @@ public void testRelate_ToMany_MixedCollection() { } @Test - public void testToManyJoin() { + public void toManyJoin() { tester.e12().insertColumns("id") .values(11) @@ -114,8 +120,8 @@ public void testToManyJoin() { .bodyEquals(2, "{},{}"); tester.e12_13().matcher().assertMatches(2); - tester.e12_13().matcher().eq("e12_id", 12).eq("e13_id", 14).assertOneMatch(); - tester.e12_13().matcher().eq("e12_id", 12).eq("e13_id", 15).assertOneMatch(); + tester.e12_13().matcher().eq("e12_id", 12).andEq("e13_id", 14).assertOneMatch(); + tester.e12_13().matcher().eq("e12_id", 12).andEq("e13_id", 15).assertOneMatch(); } @Path("") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/ReadablePropFilterIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/ReadablePropFilterIT.java new file mode 100644 index 000000000..accebcab5 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/ReadablePropFilterIT.java @@ -0,0 +1,75 @@ +package io.agrest.cayenne.POST; + + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; +import org.junit.jupiter.api.Test; + +public class ReadablePropFilterIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E3.class) + .build(); + + @Test + public void readConstraints1() { + + tester.target("/e3/constrained") + .post("{\"name\":\"zzz\"}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"id\":RID,\"name\":\"zzz\"}"); + } + + @Test + public void include_ReadConstraints() { + + // writing "phoneNumber" is allowed, but reading is not ... must be in DB, but not in response + + tester.target("/e3/constrained") + .queryParam("include", "name") + .queryParam("include", "phoneNumber") + .post("{\"name\":\"zzz\",\"phoneNumber\":\"123456\"}") + .wasCreated() + .bodyEquals(1, "{\"name\":\"zzz\"}"); + + tester.e3().matcher().assertOneMatch(); + tester.e3().matcher().eq("name", "zzz").andEq("phone_number", "123456").assertOneMatch(); + } + + @Test + public void readConstraints_DisallowRelated() { + + tester.target("/e3/constrained") + .queryParam("include", E3.E2.getName()) + .post("{\"name\":\"zzz\"}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"id\":RID,\"name\":\"zzz\"}"); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @POST + @Path("e3/constrained") + public DataResponse insertE3ReadConstrained(@Context UriInfo uriInfo, String requestBody) { + return AgJaxrs.create(E3.class, config).clientParams(uriInfo.getQueryParameters()) + .readablePropFilter(E3.class, b -> b.idOnly().property("name", true)) + .syncAndSelect(requestBody); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/RelateIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/RelateIT.java new file mode 100644 index 000000000..bd45f6267 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/RelateIT.java @@ -0,0 +1,193 @@ +package io.agrest.cayenne.POST; + + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E29; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.cayenne.main.E30; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class RelateIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E29.class, E30.class) + .build(); + + @Test + public void toOne() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(8, "yyy").exec(); + + tester.target("/e3") + .post("{\"e2\":8,\"name\":\"MM\"}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"id\":RID,\"name\":\"MM\",\"phoneNumber\":null}"); + + tester.e3().matcher().assertOneMatch(); + tester.e3().matcher().eq("e2_id", 8).andEq("name", "MM").assertOneMatch(); + } + + @Test + public void toOne_Null() { + + tester.target("/e3") + .post("{\"e2\":null,\"name\":\"MM\"}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"id\":RID,\"name\":\"MM\",\"phoneNumber\":null}"); + + tester.e3().matcher().assertOneMatch(); + tester.e3().matcher().eq("e2_id", null).assertOneMatch(); + } + + @Test + public void toOne_CompoundId() { + + tester.e29().insertColumns("id1", "id2") + .values(11, 21) + .values(12, 22).exec(); + + tester.target("/e30") + .queryParam("include", "e29.id") + .post("{\"e29\":{\"db:id1\":11,\"id2Prop\":21}}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"id\":RID,\"e29\":{\"id\":{\"db:id1\":11,\"id2Prop\":21}}}"); + + tester.e30().matcher().assertOneMatch(); + } + + @Test + public void toOne_BadFK() { + + tester.target("/e3") + .post("{\"e2\":15,\"name\":\"MM\"}") + .wasNotFound() + .bodyEquals("{\"message\":\"Related object 'E2' with id of '15' is not found\"}"); + + tester.e3().matcher().assertNoMatches(); + } + + @Test + public void toMany() { + + tester.e3().insertColumns("id_", "name") + .values(1, "xxx") + .values(8, "yyy").exec(); + + Long id = tester.target("/e2") + .queryParam("include", E2.E3S.getName()) + .queryParam("exclude", E2.ADDRESS.getName(), E2.E3S.dot(E3.NAME).getName(), E2.E3S.dot(E3.PHONE_NUMBER).getName()) + .post("{\"e3s\":[1,8],\"name\":\"MM\"}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"id\":RID,\"e3s\":[{\"id\":1},{\"id\":8}],\"name\":\"MM\"}") + .getId(); + + assertNotNull(id); + + tester.e3().matcher().eq("e2_id", id).assertMatches(2); + } + + @Test + // so while e29 -> e30 is a multi-column join, e30's own ID is single column + public void toMany_OverMultiKeyRelationship() { + + tester.e30().insertColumns("id") + .values(100) + .values(101) + .values(102).exec(); + + tester.target("/e29") + .queryParam("include", "e30s.id") + .queryParam("exclude", "id") + .post("{\"id2Prop\":54,\"e30s\":[100, 102]}") + .wasCreated() + .bodyEquals(1, "{\"e30s\":[{\"id\":100},{\"id\":102}],\"id2Prop\":54}"); + + tester.e29().matcher().assertOneMatch(); + } + + @Test + public void toMany_AsNewObjects() { + + tester.target("/e2") + .queryParam("include", "name", "e3s.name") + .post("{\"e3s\":[{\"name\":\"new_to_many1\"},{\"name\":\"new_to_many2\"}],\"name\":\"MM\"}") + .wasCreated() + .bodyEquals(1, "{\"e3s\":[],\"name\":\"MM\"}") + .getId(); + + tester.e2().matcher().assertOneMatch(); + tester.e3().matcher().assertNoMatches(); + } + + + @Test + public void toOne_AsNewObject() { + + // While Agrest does not yet support processing full related objects, it should not fail either. + // Testing this condition here. + + tester.target("/e3") + .queryParam("include", "name", "e2.id") + .post("{\"e2\":{\"name\":\"new_to_one\"},\"name\":\"MM\"}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"e2\":null,\"name\":\"MM\"}"); + + tester.e3().matcher().assertOneMatch(); + tester.e2().matcher().assertNoMatches(); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @POST + @Path("e2") + public DataResponse createE2(String targetData, @Context UriInfo uriInfo) { + return AgJaxrs.create(E2.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(targetData); + } + + @POST + @Path("e3") + public DataResponse create(@Context UriInfo uriInfo, String requestBody) { + return AgJaxrs.create(E3.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(requestBody); + } + + @POST + @Path("e29") + public DataResponse createE29(String targetData, @Context UriInfo uriInfo) { + return AgJaxrs.create(E29.class, config) + .clientParams(uriInfo.getQueryParameters()) + .syncAndSelect(targetData); + } + + @POST + @Path("e30") + public DataResponse createE30(String targetData, @Context UriInfo uriInfo) { + return AgJaxrs.create(E30.class, config) + .clientParams(uriInfo.getQueryParameters()) + .syncAndSelect(targetData); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/WritablePropFilterIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/WritablePropFilterIT.java new file mode 100644 index 000000000..96894f63e --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST/WritablePropFilterIT.java @@ -0,0 +1,147 @@ +package io.agrest.cayenne.POST; + + +import io.agrest.DataResponse; +import io.agrest.SimpleResponse; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.cayenne.main.E8; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; + +public class WritablePropFilterIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E8.class) + .build(); + + @Test + public void writeConstraints_Id() { + + // endpoint constraint allows "name" and "id" + + tester.target("/e8/578") + .post("{\"name\":\"zzz\"}") + .wasCreated() + .bodyEquals("{}"); + + tester.e8().matcher().assertOneMatch(); + tester.e8().matcher().eq("id", 578).andEq("name", "zzz").assertOneMatch(); + } + + @Test + public void writeConstraints_IdInBody_Blocked() { + + // endpoint constraint allows "name", but not "id" + + tester.target("/e8/no-id") + .post("{\"id\":578,\"name\":\"zzz\"}") + .wasBadRequest() + .bodyEquals("{\"message\":\"Setting ID explicitly is not allowed: {db:id=578}\"}"); + + tester.e8().matcher().assertNoMatches(); + } + + @Test + public void writeConstraints_IdInPath_Blocked() { + + // endpoint constraint allows "name", but not "id" + + tester.target("/e8/no-id/578") + .post("{\"name\":\"zzz\"}") + .wasBadRequest() + .bodyEquals("{\"message\":\"Setting ID explicitly is not allowed: {db:id=578}\"}"); + + tester.e8().matcher().assertNoMatches(); + } + + @Test + public void writeConstraints1() { + + tester.target("/e3") + .post("{\"name\":\"zzz\"}") + .wasCreated() + .replaceId("RID") + .bodyEquals(1, "{\"id\":RID,\"name\":\"zzz\",\"phoneNumber\":null}"); + } + + @Test + public void writeConstraints2() { + + tester.target("/e3") + .post("{\"name\":\"zzz\",\"phoneNumber\":\"12345\"}") + .wasCreated() + .replaceId("RID") + // writing phone number is not allowed, so it was ignored + .bodyEquals(1, "{\"id\":RID,\"name\":\"zzz\",\"phoneNumber\":null}"); + + tester.e3().matcher().assertOneMatch(); + tester.e3().matcher().eq("phone_number", null).assertOneMatch(); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @POST + @Path("e3") + public DataResponse constrained(@Context UriInfo uriInfo, String requestBody) { + return AgJaxrs.create(E3.class, config) + .clientParams(uriInfo.getQueryParameters()) + .writablePropFilter(E3.class, b -> b.idOnly().property("name", true)) + .syncAndSelect(requestBody); + } + + @POST + @Path("e8/{id}") + public SimpleResponse constrainedId( + @PathParam("id") int id, + @Context UriInfo uriInfo, + String requestBody) { + + return AgJaxrs.create(E8.class, config) + .clientParams(uriInfo.getQueryParameters()).byId(id) + .writablePropFilter(E8.class, b -> b.idOnly().property("name", true)) + .sync(requestBody); + } + + @POST + @Path("e8/no-id") + public SimpleResponse constrainedIdInBodyBlocked( + @Context UriInfo uriInfo, + String requestBody) { + + return AgJaxrs.create(E8.class, config) + .clientParams(uriInfo.getQueryParameters()) + .writablePropFilter(E8.class, b -> b.empty().property("name", true)) + .sync(requestBody); + } + + @POST + @Path("e8/no-id/{id}") + public SimpleResponse constrainedIdInParamsBlocked( + @PathParam("id") int id, + @Context UriInfo uriInfo, + String requestBody) { + + return AgJaxrs.create(E8.class, config) + .clientParams(uriInfo.getQueryParameters()) + .byId(id) + .writablePropFilter(E8.class, b -> b.empty().property("name", true)) + .sync(requestBody); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_IT.java deleted file mode 100644 index 0232c7ac9..000000000 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_IT.java +++ /dev/null @@ -1,295 +0,0 @@ -package io.agrest.cayenne; - - -import io.agrest.DataResponse; -import io.agrest.SimpleResponse; -import io.agrest.cayenne.cayenne.main.E16; -import io.agrest.cayenne.cayenne.main.E17; -import io.agrest.cayenne.cayenne.main.E19; -import io.agrest.cayenne.cayenne.main.E2; -import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.cayenne.cayenne.main.E31; -import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; -import io.bootique.junit5.BQTestTool; -import org.junit.jupiter.api.Test; - -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertNotNull; - -public class POST_IT extends DbTest { - - @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E2.class, E3.class, E4.class, E16.class, E17.class, E19.class, E31.class) - .build(); - - @Test - public void test() { - - tester.target("/e4").post("{\"cVarchar\":\"zzz\"}") - .wasCreated() - .replaceId("RID") - .bodyEquals(1, "{\"id\":RID,\"cBoolean\":null,\"cDate\":null,\"cDecimal\":null,\"cInt\":null," - + "\"cTime\":null,\"cTimestamp\":null,\"cVarchar\":\"zzz\"}"); - - tester.e4().matcher().assertOneMatch(); - tester.e4().matcher().eq("c_varchar", "zzz").assertOneMatch(); - - tester.target("/e4").post("{\"cVarchar\":\"TTTT\"}") - .wasCreated() - .replaceId("RID") - .bodyEquals(1, "{\"id\":RID,\"cBoolean\":null,\"cDate\":null,\"cDecimal\":null,\"cInt\":null," - + "\"cTime\":null,\"cTimestamp\":null,\"cVarchar\":\"TTTT\"}"); - - tester.e4().matcher().assertMatches(2); - tester.e4().matcher().eq("c_varchar", "TTTT").assertOneMatch(); - } - - @Test - public void testIdCalledId() { - - tester.target("/e31").post("{\"id\":5,\"name\":\"31\"}") - .wasCreated() - .bodyEquals(1, "{\"id\":5,\"name\":\"31\"}"); - - tester.e31().matcher().assertOneMatch(); - tester.e31().matcher().eq("id", 5L).assertOneMatch(); - } - - @Test - public void testCompoundId() { - - tester.target("/e17") - .queryParam("id1", 1) - .queryParam("id2", 1) - .post("{\"name\":\"xxx\"}") - .wasCreated() - .bodyEquals(1, "{\"id\":{\"id1\":1,\"id2\":1},\"id1\":1,\"id2\":1,\"name\":\"xxx\"}"); - } - - @Test - public void testDateTime() { - tester.target("e16") - .post("{\"cDate\":\"2015-03-14\", \"cTime\":\"T19:00:00\", \"cTimestamp\":\"2015-03-14T19:00:00.000\"}") - .wasCreated() - // TODO: why is time returned back without a "T" prefix? - .bodyEquals(1, "{\"id\":1,\"cDate\":\"2015-03-14\",\"cTime\":\"19:00:00\",\"cTimestamp\":\"2015-03-14T19:00:00\"}"); - } - - @Test - public void testSync_NoData() { - - tester.target("/e4/sync") - .post("{\"cVarchar\":\"zzz\"}") - .wasCreated() - .bodyEquals("{}"); - - tester.e4().matcher().assertOneMatch(); - tester.e4().matcher().eq("c_varchar", "zzz").assertOneMatch(); - } - - @Test - public void testToOne() { - - tester.e2().insertColumns("id_", "name") - .values(1, "xxx") - .values(8, "yyy").exec(); - - tester.target("/e3") - .post("{\"e2\":8,\"name\":\"MM\"}") - .wasCreated() - .replaceId("RID") - .bodyEquals(1, "{\"id\":RID,\"name\":\"MM\",\"phoneNumber\":null}"); - - tester.e3().matcher().assertOneMatch(); - tester.e3().matcher().eq("e2_id", 8).eq("name", "MM").assertOneMatch(); - } - - @Test - public void testToOne_Null() { - - tester.target("/e3") - .post("{\"e2_id\":null,\"name\":\"MM\"}") - .wasCreated() - .replaceId("RID") - .bodyEquals(1, "{\"id\":RID,\"name\":\"MM\",\"phoneNumber\":null}"); - - tester.e3().matcher().assertOneMatch(); - tester.e3().matcher().eq("e2_id", null).assertOneMatch(); - } - - @Test - public void testToOne_BadFK() { - - tester.target("/e3") - .post("{\"e2\":15,\"name\":\"MM\"}") - .wasNotFound() - .bodyEquals("{\"message\":\"Related object 'E2' with ID '[15]' is not found\"}"); - - tester.e3().matcher().assertNoMatches(); - } - - @Test - public void testBulk() { - - tester.target("/e3/") - .queryParam("exclude", "id") - .queryParam("include", E3.NAME.getName()) - .post("[{\"name\":\"aaa\"},{\"name\":\"zzz\"},{\"name\":\"bbb\"},{\"name\":\"yyy\"}]") - .wasCreated() - // ordering from request must be preserved... - .bodyEquals(4, "{\"name\":\"aaa\"},{\"name\":\"zzz\"},{\"name\":\"bbb\"},{\"name\":\"yyy\"}"); - } - - @Test - public void testToMany() { - - tester.e3().insertColumns("id_", "name") - .values(1, "xxx") - .values(8, "yyy").exec(); - - Long id = tester.target("/e2") - .queryParam("include", E2.E3S.getName()) - .queryParam("exclude", E2.ADDRESS.getName(), E2.E3S.dot(E3.NAME).getName(), E2.E3S.dot(E3.PHONE_NUMBER).getName()) - .post("{\"e3s\":[1,8],\"name\":\"MM\"}") - .wasCreated() - .replaceId("RID") - .bodyEquals(1, "{\"id\":RID,\"e3s\":[{\"id\":1},{\"id\":8}],\"name\":\"MM\"}") - .getId(); - - assertNotNull(id); - - tester.e3().matcher().eq("e2_id", id).assertMatches(2); - } - - @Test - public void testByteArrayProperty() { - - String base64Encoded = "c29tZVZhbHVlMTIz"; // someValue123 - - tester.target("/e19") - .queryParam("include", E19.GUID.getName()) - .post("{\"guid\":\"" + base64Encoded + "\"}") - .wasCreated() - .bodyEquals(1, "{\"guid\":\"" + base64Encoded + "\"}"); - } - - @Test - public void testFloatProperty() { - tester.target("/e19/float") - .queryParam("include", "floatObject", "floatPrimitive") - .post("{\"floatObject\":1.0,\"floatPrimitive\":2.0}") - .wasCreated() - .bodyEquals(1, "{\"floatObject\":1.0,\"floatPrimitive\":2.0}"); - tester.e19().matcher().eq("float_object", 1.0).eq("float_primitive", 2.0).assertOneMatch(); - } - - @Test - public void testFloatProperty_FromInt() { - tester.target("/e19/float") - .queryParam("include", "floatObject", "floatPrimitive") - .post("{\"floatObject\":1,\"floatPrimitive\":2}") - .wasCreated() - .bodyEquals(1, "{\"floatObject\":1.0,\"floatPrimitive\":2.0}"); - tester.e19().matcher().eq("float_object", 1.0).eq("float_primitive", 2.0).assertOneMatch(); - } - - @Test - public void testDoubleProperty() { - tester.target("/e19/double").post("{\"doubleObject\":1.0,\"doublePrimitive\":2.0}").wasCreated(); - tester.e19().matcher().eq("double_object", 1.0).eq("double_primitive", 2.0).assertOneMatch(); - } - - @Test - public void testDoubleProperty_FromInt() { - tester.target("/e19/double").post("{\"doubleObject\":1,\"doublePrimitive\":2}").wasCreated(); - tester.e19().matcher().eq("double_object", 1.0).eq("double_primitive", 2.0).assertOneMatch(); - } - - @Path("") - public static class Resource { - - @Context - private Configuration config; - - @POST - @Path("e2") - public DataResponse createE2(String targetData, @Context UriInfo uriInfo) { - return AgJaxrs.create(E2.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(targetData); - } - - @POST - @Path("e3") - public DataResponse create(@Context UriInfo uriInfo, String requestBody) { - return AgJaxrs.create(E3.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(requestBody); - } - - @POST - @Path("e4") - public DataResponse createE4(String requestBody) { - return AgJaxrs.create(E4.class, config).syncAndSelect(requestBody); - } - - @POST - @Path("e4/sync") - public SimpleResponse createE4_DefaultData(String requestBody) { - return AgJaxrs.create(E4.class, config).sync(requestBody); - } - - @POST - @Path("e16") - public DataResponse createE16(String requestBody) { - return AgJaxrs.create(E16.class, config).syncAndSelect(requestBody); - } - - @POST - @Path("e17") - public DataResponse createE17( - @Context UriInfo uriInfo, - @QueryParam("id1") Integer id1, - @QueryParam("id2") Integer id2, - String requestBody) { - - Map ids = new HashMap<>(); - ids.put(E17.ID1.getName(), id1); - ids.put(E17.ID2.getName(), id2); - - return AgJaxrs.create(E17.class, config).byId(ids).syncAndSelect(requestBody); - } - - @POST - @Path("e19") - public DataResponse createE19(@Context UriInfo uriInfo, String data) { - return AgJaxrs.create(E19.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(data); - } - - @POST - @Path("e19/float") - public DataResponse createE19_FloatAttribute(@Context UriInfo uriInfo, String data) { - return AgJaxrs.create(E19.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(data); - } - - @POST - @Path("e19/double") - public SimpleResponse create_E19_DoubleAttribute(String entityData) { - return AgJaxrs.create(E19.class, config).sync(entityData); - } - - @POST - @Path("e31") - public DataResponse createE31(@Context UriInfo uriInfo, String requestBody) { - return AgJaxrs.create(E31.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(requestBody); - } - } -} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_ReadAccess_OverlayIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_ReadAccess_OverlayIT.java deleted file mode 100644 index 835461073..000000000 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/POST_ReadAccess_OverlayIT.java +++ /dev/null @@ -1,162 +0,0 @@ -package io.agrest.cayenne; - - -import io.agrest.DataResponse; -import io.agrest.SimpleResponse; -import io.agrest.cayenne.cayenne.main.E2; -import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.cayenne.cayenne.main.E8; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; -import io.bootique.junit5.BQTestTool; -import org.junit.jupiter.api.Test; - -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; - -public class POST_ReadAccess_OverlayIT extends DbTest { - - @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E2.class, E3.class, E8.class) - .build(); - - @Test - public void testWriteConstraints_Id_Allowed() { - - // endpoint constraint allows "name" and "id" - - tester.target("/e8/w/constrainedid/578") - .post("{\"name\":\"zzz\"}") - .wasCreated() - .bodyEquals("{}"); - - tester.e8().matcher().assertOneMatch(); - tester.e8().matcher().eq("id", 578).eq("name", "zzz").assertOneMatch(); - } - - @Test - public void testWriteConstraints_Id_Blocked() { - - // endpoint constraint allows "name", but not "id" - - tester.target("/e8/w/constrainedidblocked/578") - .post("{\"name\":\"zzz\"}") - .wasBadRequest() - .bodyEquals("{\"message\":\"Setting ID explicitly is not allowed: {db:id=578}\"}"); - - tester.e8().matcher().assertNoMatches(); - } - - @Test - public void testWriteConstraints1() { - - tester.target("/e3/w/constrained") - .post("{\"name\":\"zzz\"}") - .wasCreated() - .replaceId("RID") - .bodyEquals(1, "{\"id\":RID,\"name\":\"zzz\",\"phoneNumber\":null}"); - } - - @Test - public void testWriteConstraints2() { - - tester.target("/e3/w/constrained") - .post("{\"name\":\"zzz\",\"phoneNumber\":\"12345\"}") - .wasCreated() - .replaceId("RID") - // writing phone number is not allowed, so it was ignored - .bodyEquals(1, "{\"id\":RID,\"name\":\"zzz\",\"phoneNumber\":null}"); - - tester.e3().matcher().assertOneMatch(); - tester.e3().matcher().eq("phone_number", null).assertOneMatch(); - } - - @Test - public void testReadConstraints1() { - - tester.target("/e3/constrained") - .post("{\"name\":\"zzz\"}") - .wasCreated() - .replaceId("RID") - .bodyEquals(1, "{\"id\":RID,\"name\":\"zzz\"}"); - } - - @Test - public void testInclude_ReadConstraints() { - - // writing "phoneNumber" is allowed, but reading is not ... must be in DB, but not in response - - tester.target("/e3/constrained") - .queryParam("include", "name") - .queryParam("include", "phoneNumber") - .post("{\"name\":\"zzz\",\"phoneNumber\":\"123456\"}") - .wasCreated() - .bodyEquals(1, "{\"name\":\"zzz\"}"); - - tester.e3().matcher().assertOneMatch(); - tester.e3().matcher().eq("name", "zzz").eq("phone_number", "123456").assertOneMatch(); - } - - @Test - public void testReadConstraints_DisallowRelated() { - - tester.target("/e3/constrained") - .queryParam("include", E3.E2.getName()) - .post("{\"name\":\"zzz\"}") - .wasCreated() - .replaceId("RID") - .bodyEquals(1, "{\"id\":RID,\"name\":\"zzz\"}"); - } - - @Path("") - public static class Resource { - - @Context - private Configuration config; - - @POST - @Path("e3/constrained") - public DataResponse insertE3ReadConstrained(@Context UriInfo uriInfo, String requestBody) { - return AgJaxrs.create(E3.class, config).clientParams(uriInfo.getQueryParameters()) - .readablePropFilter(E3.class, b -> b.idOnly().property("name", true)) - .syncAndSelect(requestBody); - } - - @POST - @Path("e3/w/constrained") - public DataResponse insertE3WriteConstrained(@Context UriInfo uriInfo, String requestBody) { - return AgJaxrs.create(E3.class, config).clientParams(uriInfo.getQueryParameters()) - .writablePropFilter(E3.class, b -> b.idOnly().property("name", true)) - .syncAndSelect(requestBody); - } - - @POST - @Path("e8/w/constrainedid/{id}") - public SimpleResponse create_WriteConstrainedId( - @PathParam("id") int id, - @Context UriInfo uriInfo, - String requestBody) { - - return AgJaxrs.create(E8.class, config).clientParams(uriInfo.getQueryParameters()).byId(id) - .writablePropFilter(E8.class, b -> b.idOnly().property("name", true)) - .sync(requestBody); - } - - @POST - @Path("e8/w/constrainedidblocked/{id}") - public SimpleResponse create_WriteConstrainedIdBlocked( - @PathParam("id") int id, - @Context UriInfo uriInfo, - String requestBody) { - return AgJaxrs.create(E8.class, config).clientParams(uriInfo.getQueryParameters()).byId(id) - .writablePropFilter(E8.class, b -> b.empty().property("name", true)) - .sync(requestBody); - } - } -} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_AgRequestIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/AgRequestIT.java similarity index 85% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_AgRequestIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/AgRequestIT.java index 2e9a28fbc..139b46059 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_AgRequestIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/AgRequestIT.java @@ -1,32 +1,32 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.AgRequest; import io.agrest.DataResponse; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.protocol.Exclude; import io.agrest.protocol.Include; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.cayenne.cayenne.main.E3; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class PUT_AgRequestIT extends DbTest { +public class AgRequestIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E3.class) .build(); @Test - public void testPUT_Includes_OverrideByAgRequest() { + public void includes_OverrideByAgRequest() { tester.e3().insertColumns("id_", "name") .values(5, "aaa") @@ -49,7 +49,7 @@ public void testPUT_Includes_OverrideByAgRequest() { } @Test - public void testPUT_Excludes_OverrideByAgRequest() { + public void excludes_OverrideByAgRequest() { tester.e3().insertColumns("id_", "name") .values(5, "aaa") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/BasicIT.java similarity index 59% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_IT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/BasicIT.java index 49c51db26..decb9811b 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_IT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/BasicIT.java @@ -1,64 +1,60 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; -import com.fasterxml.jackson.core.JsonGenerator; import io.agrest.DataResponse; -import io.agrest.SimpleResponse; import io.agrest.UpdateStage; import io.agrest.cayenne.cayenne.main.E14; import io.agrest.cayenne.cayenne.main.E17; -import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E23; import io.agrest.cayenne.cayenne.main.E26; -import io.agrest.cayenne.cayenne.main.E28; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E31; import io.agrest.cayenne.cayenne.main.E4; import io.agrest.cayenne.cayenne.main.E7; import io.agrest.cayenne.cayenne.main.E8; import io.agrest.cayenne.cayenne.main.E9; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; import io.agrest.encoder.Encoder; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; -import java.io.IOException; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; -public class PUT_IT extends DbTest { +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class BasicIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E2.class, E3.class, E4.class, E7.class, E8.class, E9.class, E14.class, E17.class, E23.class, E26.class, E28.class, E31.class) + static final MainModelTester tester = tester(Resource.class) + .entities(E3.class, E4.class, E7.class, E8.class, E9.class, E14.class, E17.class, E23.class, E26.class, E31.class) .build(); @Test - public void testCreateOrUpdate_ById() { + public void createOrUpdate_ById() { tester.target("/e23_create_or_update/8").put("{\"name\":\"zzz\"}") .wasCreated() .bodyEquals(1, "{\"id\":8,\"exposedId\":8,\"name\":\"zzz\"}"); - tester.e23().matcher().eq("id", 8).eq("name", "zzz").assertOneMatch(); + tester.e23().matcher().eq("id", 8).andEq("name", "zzz").assertOneMatch(); tester.target("/e23_create_or_update/8").put("{\"name\":\"aaa\"}") .wasOk() .bodyEquals(1, "{\"id\":8,\"exposedId\":8,\"name\":\"aaa\"}"); - tester.e23().matcher().eq("id", 8).eq("name", "aaa").assertOneMatch(); + tester.e23().matcher().eq("id", 8).andEq("name", "aaa").assertOneMatch(); } @Test - public void testUpdate() { + public void update() { tester.e4().insertColumns("id", "c_varchar", "c_decimal") .values(1, "xxx", new BigDecimal("11.23")) @@ -83,11 +79,11 @@ public void testUpdate() { .where("id", 8) .selectOne(); - Assertions.assertArrayEquals(new Object[]{"zzz", new BigDecimal("12.99")}, data); + assertArrayEquals(new Object[]{"zzz", new BigDecimal("12.99")}, data); } @Test - public void testUpdateIdCalledId() { + public void updateIdCalledId() { tester.e31().insertColumns("id", "name").values(5, "30").exec(); @@ -96,11 +92,11 @@ public void testUpdateIdCalledId() { .bodyEquals(1, "{\"id\":5,\"name\":\"31\"}"); tester.e31().matcher().assertOneMatch(); - tester.e31().matcher().eq("id", 5L).eq("name", "31").assertOneMatch(); + tester.e31().matcher().eq("id", 5L).andEq("name", "31").assertOneMatch(); } @Test - public void testExplicitCompoundId() { + public void explicitCompoundId() { tester.e17().insertColumns("id1", "id2", "name") .values(1, 1, "aaa") @@ -112,74 +108,11 @@ public void testExplicitCompoundId() { .put("{\"name\":\"xxx\"}") .wasOk().bodyEquals(1, "{\"id\":{\"id1\":1,\"id2\":1},\"id1\":1,\"id2\":1,\"name\":\"xxx\"}"); - tester.e17().matcher().eq("id1", 1).eq("id2", 1).eq("name", "xxx").assertOneMatch(); - } - - @Test - public void testToOne() { - - tester.e2().insertColumns("id_", "name") - .values(1, "xxx") - .values(8, "yyy").exec(); - tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", 8).exec(); - - tester.target("/e3/3") - .put("{\"id\":3,\"e2\":1}") - .wasOk() - .bodyEquals(1, "{\"id\":3,\"name\":\"zzz\",\"phoneNumber\":null}"); - - tester.e3().matcher().eq("id_", 3).eq("e2_id", 1).assertOneMatch(); + tester.e17().matcher().eq("id1", 1).andEq("id2", 1).andEq("name", "xxx").assertOneMatch(); } @Test - public void testToOne_ArraySyntax() { - - tester.e2().insertColumns("id_", "name") - .values(1, "xxx") - .values(8, "yyy").exec(); - tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", 8).exec(); - - tester.target("/e3/3") - .put("{\"id\":3,\"e2\":[1]}") - .wasOk() - .bodyEquals(1, "{\"id\":3,\"name\":\"zzz\",\"phoneNumber\":null}"); - - tester.e3().matcher().eq("id_", 3).eq("e2_id", 1).assertOneMatch(); - } - - @Test - public void testToOne_ToNull() { - - tester.e2().insertColumns("id_", "name") - .values(1, "xxx") - .values(8, "yyy").exec(); - tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", 8).exec(); - - tester.target("/e3/3") - .put("{\"id\":3,\"e2\":null}") - .wasOk().bodyEquals(1, "{\"id\":3,\"name\":\"zzz\",\"phoneNumber\":null}"); - - tester.e3().matcher().assertOneMatch(); - tester.e3().matcher().eq("id_", 3).eq("e2_id", null).assertOneMatch(); - } - - @Test - public void testToOne_FromNull() { - - tester.e2().insertColumns("id_", "name") - .values(1, "xxx") - .values(8, "yyy").exec(); - tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", null).exec(); - - tester.target("/e3/3").put("{\"id\":3,\"e2\":8}") - .wasOk() - .bodyEquals(1, "{\"id\":3,\"name\":\"zzz\",\"phoneNumber\":null}"); - - tester.e3().matcher().eq("id_", 3).eq("e2_id", 8).assertOneMatch(); - } - - @Test - public void testBulk() { + public void bulk() { tester.e3().insertColumns("id_", "name") .values(5, "aaa") @@ -202,7 +135,7 @@ public void testBulk() { } @Test - public void testSingle_LongId_Small() { + public void single_LongId_Small() { tester.e14().insertColumns("long_id", "name").values(5L, "aaa").exec(); @@ -213,11 +146,11 @@ public void testSingle_LongId_Small() { .wasOk().bodyEquals(1, "{\"id\":5,\"name\":\"bbb\",\"prettyName\":\"bbb_pretty\"}"); tester.e14().matcher().assertOneMatch(); - tester.e14().matcher().eq("long_id", 5).eq("name", "bbb").assertOneMatch(); + tester.e14().matcher().eq("long_id", 5).andEq("name", "bbb").assertOneMatch(); } @Test - public void testBulk_LongId_Small() { + public void bulk_LongId_Small() { tester.e14().insertColumns("long_id", "name") .values(5L, "aaa") @@ -252,7 +185,7 @@ public void testBulk_LongId_Small() { } @Test - public void testBulk_LongId() { + public void bulk_LongId() { tester.e14().insertColumns("long_id", "name") .values(8147483647L, "aaa") @@ -282,7 +215,7 @@ public void testBulk_LongId() { } @Test - public void testCustomEncoder() { + public void customEncoder() { tester.target("/e7_custom_encoder") .put("[{\"id\":4,\"name\":\"zzz\"}]") @@ -291,7 +224,7 @@ public void testCustomEncoder() { } @Test - public void testBulk_ResponseAttributesFilter() { + public void bulk_ResponseAttributesFilter() { tester.target("/e7") .queryParam("exclude", "id") @@ -311,7 +244,7 @@ public void testBulk_ResponseAttributesFilter() { } @Test - public void testBulk_ResponseToOneRelationshipFilter() { + public void bulk_ResponseToOneRelationshipFilter() { tester.e8().insertColumns("id", "name").values(5, "aaa").values(6, "ert").exec(); tester.e9().insertColumns("e8_id").values(5).values(6).exec(); @@ -354,7 +287,7 @@ public void testBulk_ResponseToOneRelationshipFilter() { } @Test - public void testBulk_ResponseToManyRelationshipFilter() { + public void bulk_ResponseToManyRelationshipFilter() { tester.e8().insertColumns("id", "name") .values(5, "aaa") @@ -375,97 +308,6 @@ public void testBulk_ResponseToManyRelationshipFilter() { "{\"id\":5,\"e7s\":[{\"name\":\"her\"},{\"name\":\"him\"}]}"); } - @Test - public void testSingle_ResponseToOneRelationshipFilter() { - - tester.e8().insertColumns("id", "name").values(5, "aaa").values(6, "ert").exec(); - tester.e9().insertColumns("e8_id").values(5).values(6).exec(); - - tester.target("/e7/6") - .queryParam("include", "id", E7.E8.dot(E8.E9).getName()) - .queryParam("exclude", E7.NAME.getName()) - .put("[{\"name\":\"yyy\",\"e8\":6}]") - .wasCreated() - .bodyEquals(1, "{\"id\":6,\"e8\":{\"e9\":{\"id\":6}}}"); - } - - @Test - public void testToMany() { - - tester.e2().insertColumns("id_", "name") - .values(1, "xxx") - .values(8, "yyy").exec(); - tester.e3().insertColumns("id_", "name", "e2_id") - .values(3, "zzz", null) - .values(4, "aaa", 8) - .values(5, "bbb", 8).exec(); - - tester.target("/e2/1") - .queryParam("include", E2.E3S.getName()) - .queryParam("exclude", E2.ADDRESS.getName(), E2.NAME.getName(), E2.E3S.dot(E3.NAME).getName(), E2.E3S.dot(E3.PHONE_NUMBER).getName()) - .put("{\"e3s\":[3,4,5]}") - .wasOk().bodyEquals(1, "{\"id\":1,\"e3s\":[{\"id\":3},{\"id\":4},{\"id\":5}]}"); - - tester.e3().matcher().eq("e2_id", 1).assertMatches(3); - } - - @Test - public void testToMany_UnrelateAll() { - - tester.e2().insertColumns("id_", "name") - .values(1, "xxx") - .values(8, "yyy").exec(); - tester.e3().insertColumns("id_", "name", "e2_id") - .values(3, "zzz", null) - .values(4, "aaa", 8) - .values(5, "bbb", 8).exec(); - - tester.target("/e2/8") - .queryParam("include", E2.E3S.getName()) - .queryParam("exclude", E2.ADDRESS.getName(), E2.NAME.getName(), E2.E3S.dot(E3.NAME).getName(), E2.E3S.dot(E3.PHONE_NUMBER).getName()) - .put("{\"e3s\":[]}") - .wasOk().bodyEquals(1, "{\"id\":8,\"e3s\":[]}"); - - tester.e3().matcher().eq("e2_id", null).assertMatches(3); - } - - @Test - public void testToMany_UnrelateOne() { - - tester.e2().insertColumns("id_", "name") - .values(1, "xxx") - .values(8, "yyy").exec(); - tester.e3().insertColumns("id_", "name", "e2_id") - .values(3, "zzz", null) - .values(4, "aaa", 8) - .values(5, "bbb", 8).exec(); - - tester.target("/e2/1") - .queryParam("include", E2.E3S.getName()) - .queryParam("exclude", E2.ADDRESS.getName(), E2.NAME.getName(), E2.E3S.dot(E3.NAME).getName(), E2.E3S.dot(E3.PHONE_NUMBER).getName()) - .put("{\"e3s\":[4]}") - .wasOk().bodyEquals(1, "{\"id\":1,\"e3s\":[{\"id\":4}]}"); - - tester.e3().matcher().eq("e2_id", 1).eq("id_", 4).assertOneMatch(); - tester.e3().matcher().eq("e2_id", 8).eq("id_", 5).assertOneMatch(); - } - - @Test - public void testJson() { - - String e1 = "[{\"id\":5,\"json\":[1,2]},{\"id\":6,\"json\":{\"a\":1}},{\"id\":7,\"json\":5}]"; - tester.target("/e28/").put(e1).wasCreated(); - tester.e28().matcher().assertMatches(3); - tester.e28().matcher().eq("id", 5).eq("json", "[1,2]").assertOneMatch(); - tester.e28().matcher().eq("id", 6).eq("json", "{\"a\":1}").assertOneMatch(); - tester.e28().matcher().eq("id", 7).eq("json", "5").assertOneMatch(); - - // try updating - String e2 = "[{\"id\":5,\"json\":[1,3]}]"; - tester.target("/e28/").put(e2).wasOk(); - tester.e28().matcher().assertMatches(3); - tester.e28().matcher().eq("id", 5).eq("json", "[1,3]").assertOneMatch(); - } @Path("") public static class Resource { @@ -473,24 +315,12 @@ public static class Resource { @Context private Configuration config; - @PUT - @Path("e2/{id}") - public DataResponse createOrUpdate_E2(@PathParam("id") int id, String entityData, @Context UriInfo uriInfo) { - return AgJaxrs.idempotentCreateOrUpdate(E2.class, config).byId(id).clientParams(uriInfo.getQueryParameters()).syncAndSelect(entityData); - } - @PUT @Path("e3") public DataResponse syncE3(@Context UriInfo uriInfo, String requestBody) { return AgJaxrs.idempotentFullSync(E3.class, config).clientParams(uriInfo.getQueryParameters()).syncAndSelect(requestBody); } - @PUT - @Path("e3/{id}") - public DataResponse updateE3(@PathParam("id") int id, String requestBody) { - return AgJaxrs.update(E3.class, config).byId(id).syncAndSelect(requestBody); - } - @PUT @Path("e4/{id}") public DataResponse updateE4(@PathParam("id") int id, String requestBody) { @@ -507,13 +337,10 @@ public DataResponse syncE7(@Context UriInfo uriInfo, String data) { @Path("e7_custom_encoder") public DataResponse syncE7_CustomEncoder(@Context UriInfo uriInfo, String data) { - Encoder encoder = new Encoder() { - @Override - public void encode(String propertyName, Object object, JsonGenerator out) throws IOException { - out.writeStartObject(); - out.writeObjectField("encoder", "custom"); - out.writeEndObject(); - } + Encoder encoder = (propertyName, object, skipNullProperties, out) -> { + out.writeStartObject(); + out.writeObjectField("encoder", "custom"); + out.writeEndObject(); }; return AgJaxrs.idempotentFullSync(E7.class, config).clientParams(uriInfo.getQueryParameters()) @@ -521,12 +348,6 @@ public void encode(String propertyName, Object object, JsonGenerator out) throws .syncAndSelect(data); } - @PUT - @Path("e7/{id}") - public DataResponse syncOneE7(@PathParam("id") int id, @Context UriInfo uriInfo, String data) { - return AgJaxrs.idempotentFullSync(E7.class, config).byId(id).clientParams(uriInfo.getQueryParameters()).syncAndSelect(data); - } - @PUT @Path("e8") public DataResponse sync(@Context UriInfo uriInfo, String data) { @@ -571,11 +392,5 @@ public DataResponse updateE31(@PathParam("id") long id, @Context UriInfo ur public DataResponse createOrUpdateE4(@PathParam("id") int id, String requestBody) { return AgJaxrs.createOrUpdate(E23.class, config).byId(id).syncAndSelect(requestBody); } - - @PUT - @Path("e28") - public SimpleResponse syncE28(String data) { - return AgJaxrs.createOrUpdate(E28.class, config).sync(data); - } } } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ConvertersIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ConvertersIT.java new file mode 100644 index 000000000..b8393b255 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ConvertersIT.java @@ -0,0 +1,52 @@ +package io.agrest.cayenne.PUT; + +import io.agrest.SimpleResponse; +import io.agrest.cayenne.cayenne.main.E28; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; + +public class ConvertersIT extends MainDbTest { + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entities(E28.class) + .build(); + + @Test + public void json() { + + String e1 = "[{\"id\":5,\"json\":[1,2]},{\"id\":6,\"json\":{\"a\":1}},{\"id\":7,\"json\":5}]"; + tester.target("/e28/").put(e1).wasCreated(); + tester.e28().matcher().assertMatches(3); + tester.e28().matcher().eq("id", 5).andEq("json", "[1,2]").assertOneMatch(); + tester.e28().matcher().eq("id", 6).andEq("json", "{\"a\":1}").assertOneMatch(); + tester.e28().matcher().eq("id", 7).andEq("json", "5").assertOneMatch(); + + // try updating + String e2 = "[{\"id\":5,\"json\":[1,3]}]"; + tester.target("/e28/").put(e2).wasOk(); + tester.e28().matcher().assertMatches(3); + tester.e28().matcher().eq("id", 5).andEq("json", "[1,3]").assertOneMatch(); + } + + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @PUT + @Path("e28") + public SimpleResponse syncE28(String data) { + return AgJaxrs.createOrUpdate(E28.class, config).sync(data); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_CreateAuthorizerIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/CreateAuthorizerIT.java similarity index 78% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_CreateAuthorizerIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/CreateAuthorizerIT.java index 9525f4369..5792bb7a8 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_CreateAuthorizerIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/CreateAuthorizerIT.java @@ -1,36 +1,36 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.EntityUpdate; import io.agrest.SimpleResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.List; -public class PUT_CreateAuthorizerIT extends DbTest { +public class CreateAuthorizerIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class) .agCustomizer(ab -> ab - .entityOverlay(AgEntity.overlay(E2.class).createAuthorizer(u -> !"blocked".equals(u.getValues().get("name")))) + .entityOverlay(AgEntity.overlay(E2.class).createAuthorizer(u -> !"blocked".equals(u.getAttributes().get("name")))) ).build(); @Test - public void testInStack_Allowed() { + public void inStack_Allowed() { tester.target("/e2_stack_authorizer") .put("[{\"name\":\"Bb\"},{\"name\":\"Aa\"}]") @@ -42,7 +42,7 @@ public void testInStack_Allowed() { } @Test - public void testInStack_Blocked() { + public void inStack_Blocked() { tester.target("/e2_stack_authorizer") .put("[{\"name\":\"Bb\"},{\"name\":\"blocked\"}]") @@ -52,7 +52,7 @@ public void testInStack_Blocked() { } @Test - public void testInRequestAndStack_Allowed() { + public void inRequestAndStack_Allowed() { tester.target("/e2_request_and_stack_authorizer/not_this") .put("[{\"name\":\"Bb\"},{\"name\":\"Aa\"}]") @@ -64,7 +64,7 @@ public void testInRequestAndStack_Allowed() { } @Test - public void testInRequestAndStack_Blocked() { + public void inRequestAndStack_Blocked() { tester.target("/e2_request_and_stack_authorizer/not_this") .put("[{\"name\":\"Bb\"},{\"name\":\"blocked\"}]") @@ -100,7 +100,7 @@ public SimpleResponse putE2RequestAndStackFilter( return AgJaxrs.createOrUpdate(E2.class, config) .clientParams(uriInfo.getQueryParameters()) - .createAuthorizer(E2.class, u -> !name.equals(u.getValues().get("name"))) + .createAuthorizer(E2.class, u -> !name.equals(u.getAttributes().get("name"))) .sync(updates); } } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_DeleteAuthorizerIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/DeleteAuthorizerIT.java similarity index 83% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_DeleteAuthorizerIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/DeleteAuthorizerIT.java index cf4896e69..ef4fac656 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_DeleteAuthorizerIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/DeleteAuthorizerIT.java @@ -1,35 +1,35 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.EntityUpdate; import io.agrest.SimpleResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; import java.util.List; -public class PUT_DeleteAuthorizerIT extends DbTest { +public class DeleteAuthorizerIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class) .agCustomizer(ab -> ab .entityOverlay(AgEntity.overlay(E2.class).deleteAuthorizer(o -> !"dont_delete".equals(o.getName()))) ).build(); @Test - public void testInStack_Allowed() { + public void inStack_Allowed() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -45,7 +45,7 @@ public void testInStack_Allowed() { } @Test - public void testInStack_Blocked() { + public void inStack_Blocked() { tester.e2().insertColumns("id_", "name") .values(1, "dont_delete") .values(2, "b") @@ -61,7 +61,7 @@ public void testInStack_Blocked() { } @Test - public void testInRequestAndStack_Allowed() { + public void inRequestAndStack_Allowed() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -77,7 +77,7 @@ public void testInRequestAndStack_Allowed() { } @Test - public void testInRequestAndStack_Blocked() { + public void inRequestAndStack_Blocked() { tester.e2().insertColumns("id_", "name") .values(1, "dont_delete_this_either") .values(2, "b") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_EntityOverlay_PerRequestIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/EntityOverlay_PerRequestIT.java similarity index 87% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_EntityOverlay_PerRequestIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/EntityOverlay_PerRequestIT.java index d92dada3d..6ecd69e8b 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_EntityOverlay_PerRequestIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/EntityOverlay_PerRequestIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.DataResponse; import io.agrest.EntityUpdate; @@ -8,9 +8,9 @@ import io.agrest.cayenne.cayenne.main.E22; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.agrest.meta.AgEntityOverlay; import io.bootique.junit5.BQTestTool; @@ -19,23 +19,23 @@ import org.apache.cayenne.query.SelectById; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.List; -public class PUT_EntityOverlay_PerRequestIT extends DbTest { +public class EntityOverlay_PerRequestIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class, E10.class, E11.class, E22.class) .build(); @Test - public void test_RequestOverlaidProperties_ConstrainedEntity() { + public void requestOverlaidProperties_ConstrainedEntity() { tester.e10().insertColumns("id", "c_int").values(2, 5).values(4, 8).exec(); tester.e22().insertColumns("id", "name").values(1, "a").values(2, "b").exec(); @@ -50,7 +50,7 @@ public void test_RequestOverlaidProperties_ConstrainedEntity() { } @Test - public void test_DefaultIncludes() { + public void defaultIncludes() { tester.e4().insertColumns("id", "c_varchar").values(2, "a").values(4, "b").exec(); @@ -67,7 +67,7 @@ public void test_DefaultIncludes() { } @Test - public void test_OverlaidRelatedExclude() { + public void overlaidRelatedExclude() { tester.e2().insertColumns("id_", "name", "address").values(1, "N", "A").exec(); tester.e3().insertColumns("id_", "name", "phone_number", "e2_id") @@ -80,11 +80,11 @@ public void test_OverlaidRelatedExclude() { .wasOk() .bodyEquals(1, "{\"id\":1,\"e3s\":[{\"id\":1,\"name\":\"N\"}],\"name\":\"Nn\"}"); - tester.e2().matcher().eq("id_", 1).eq("name", "Nn").eq("address", "A").assertOneMatch(); + tester.e2().matcher().eq("id_", 1).andEq("name", "Nn").andEq("address", "A").assertOneMatch(); } @Test - public void test_OverlaidExclude() { + public void overlaidExclude() { tester.e2().insertColumns("id_", "name", "address").values(1, "N", "A").exec(); @@ -93,11 +93,11 @@ public void test_OverlaidExclude() { .wasOk() .bodyEquals(1, "{\"id\":1,\"name\":\"Nn\"}"); - tester.e2().matcher().eq("id_", 1).eq("name", "Nn").eq("address", "A").assertOneMatch(); + tester.e2().matcher().eq("id_", 1).andEq("name", "Nn").andEq("address", "A").assertOneMatch(); } @Test - public void test_WritablePropFilter_ToManyBlocked() { + public void writablePropFilter_ToManyBlocked() { tester.e2().insertColumns("id_", "name", "address").values(11, "N", "A").exec(); tester.e3().insertColumns("id_", "name", "phone_number", "e2_id") @@ -110,12 +110,12 @@ public void test_WritablePropFilter_ToManyBlocked() { .wasOk() .bodyEquals(1, "{\"id\":11,\"e3s\":[{\"id\":35}],\"name\":\"Nn\"}"); - tester.e2().matcher().eq("id_", 11).eq("name", "Nn").assertOneMatch(); - tester.e3().matcher().eq("e2_id", 11).eq("name", "N").assertOneMatch(); + tester.e2().matcher().eq("id_", 11).andEq("name", "Nn").assertOneMatch(); + tester.e3().matcher().eq("e2_id", 11).andEq("name", "N").assertOneMatch(); } @Test - public void test_WritablePropFilter_ToOneBlocked() { + public void writablePropFilter_ToOneBlocked() { tester.e2().insertColumns("id_", "name", "address").values(11, "N", "A").exec(); tester.e3().insertColumns("id_", "name", "phone_number", "e2_id") @@ -128,7 +128,7 @@ public void test_WritablePropFilter_ToOneBlocked() { .wasOk() .bodyEquals(1, "{\"id\":35,\"e2\":{\"id\":11},\"name\":\"Nn\"}"); - tester.e3().matcher().eq("id_", 35).eq("name", "Nn").eq("e2_id", 11).assertOneMatch(); + tester.e3().matcher().eq("id_", 35).andEq("name", "Nn").andEq("e2_id", 11).assertOneMatch(); } @Path("") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_EntityUpdateBindingIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/EntityUpdateBindingIT.java similarity index 68% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_EntityUpdateBindingIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/EntityUpdateBindingIT.java index b196125c2..4b2c02ee0 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_EntityUpdateBindingIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/EntityUpdateBindingIT.java @@ -1,33 +1,33 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.EntityUpdate; import io.agrest.SimpleResponse; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.Collection; -public class PUT_EntityUpdateBindingIT extends DbTest { +public class EntityUpdateBindingIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E3.class) .build(); @Test - public void testSingle() { + public void single() { tester.e3().insertColumns("id_", "name").values(3, "zzz").exec(); @@ -35,11 +35,11 @@ public void testSingle() { .put("{\"id\":3,\"name\":\"yyy\"}") .wasOk().bodyEquals("{}"); - tester.e3().matcher().eq("id_", 3).eq("name", "yyy").assertOneMatch(); + tester.e3().matcher().eq("id_", 3).andEq("name", "yyy").assertOneMatch(); } @Test - public void testCollection() { + public void collection() { tester.e3().insertColumns("id_", "name") .values(3, "zzz") @@ -51,8 +51,8 @@ public void testCollection() { .wasOk().bodyEquals("{}"); tester.e3().matcher().assertMatches(2); - tester.e3().matcher().eq("id_", 3).eq("name", "yyy").assertOneMatch(); - tester.e3().matcher().eq("id_", 5).eq("name", "nnn").assertOneMatch(); + tester.e3().matcher().eq("id_", 3).andEq("name", "yyy").assertOneMatch(); + tester.e3().matcher().eq("id_", 5).andEq("name", "nnn").assertOneMatch(); } @Path("") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/EntityUpdateVsStringIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/EntityUpdateVsStringIT.java new file mode 100644 index 000000000..89fe59b0c --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/EntityUpdateVsStringIT.java @@ -0,0 +1,70 @@ +package io.agrest.cayenne.PUT; + +import io.agrest.EntityUpdate; +import io.agrest.SimpleResponse; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; +import java.util.List; + +public class EntityUpdateVsStringIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E3.class) + .build(); + + @ParameterizedTest + @ValueSource(strings = {"full_sync_as_string", "full_sync_as_single_update", "full_sync_as_list_update"}) + public void fullSync_EmptyBodyMustDelete(String path) { + + tester.e3().insertColumns("id_", "name").values(3, "aaa").exec(); + + tester.target(path).put("") + .wasOk() + .bodyEquals("{}"); + tester.e3().matcher().assertNoMatches(); + } + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @PUT + @Path("full_sync_as_string") + public SimpleResponse syncAsString(@Context UriInfo uriInfo, String requestBody) { + return AgJaxrs.idempotentFullSync(E3.class, config) + .clientParams(uriInfo.getQueryParameters()) + .sync(requestBody); + } + + @PUT + @Path("full_sync_as_single_update") + public SimpleResponse syncAsSingleUpdate(@Context UriInfo uriInfo, EntityUpdate u) { + return AgJaxrs.idempotentFullSync(E3.class, config) + .clientParams(uriInfo.getQueryParameters()) + .sync(u); + } + + @PUT + @Path("full_sync_as_list_update") + public SimpleResponse syncAsListUpdate(@Context UriInfo uriInfo, List> us) { + return AgJaxrs.idempotentFullSync(E3.class, config) + .clientParams(uriInfo.getQueryParameters()) + .sync(us); + } + + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/Include_MaxPathDepthIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/Include_MaxPathDepthIT.java new file mode 100644 index 000000000..8369a287e --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/Include_MaxPathDepthIT.java @@ -0,0 +1,108 @@ +package io.agrest.cayenne.PUT; + +import io.agrest.DataResponse; +import io.agrest.UpdateBuilder; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.cayenne.main.E5; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +public class Include_MaxPathDepthIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E5.class) + .build(); + + @BeforeEach + void insertTestData() { + tester.e5().insertColumns("id", "name", "date").values(45, "T", "2013-01-03").exec(); + tester.e2().insertColumns("id_", "name").values(8, "A").exec(); + tester.e3().insertColumns("id_", "name", "e2_id", "e5_id").values(3, "zzz", 8, 45).exec(); + } + + @Test + public void depth100_Default() { + + tester.target("/e2/8") + .queryParam("include", "name", "e3s.id", "e3s.e5.id") + .put("{\"name\":\"B\"}") + .wasOk() + .bodyEquals(1, "{\"e3s\":[{\"id\":3,\"e5\":{\"id\":45}}],\"name\":\"B\"}"); + } + + @Test + public void depth0() { + + tester.target("/e2/8") + .queryParam("depth", 0) + .queryParam("include", "name", "e3s.id", "e3s.e5.id") + .put("{\"name\":\"B\"}") + .wasOk() + .bodyEquals(1, "{\"name\":\"B\"}"); + } + + @Test + public void depth1() { + + tester.target("/e2/8") + .queryParam("depth", 1) + .queryParam("include", "name", "e3s.id", "e3s.e5.id") + .put("{\"name\":\"B\"}") + .wasOk() + .bodyEquals(1, "{\"e3s\":[{\"id\":3}],\"name\":\"B\"}"); + } + + @Test + public void depth2() { + + tester.target("/e2/8") + .queryParam("depth", 2) + .queryParam("include", "name", "e3s.id", "e3s.e5.id") + .put("{\"name\":\"B\"}") + .wasOk() + .bodyEquals(1, "{\"e3s\":[{\"id\":3,\"e5\":{\"id\":45}}],\"name\":\"B\"}"); + } + + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public static class Resource { + + @Context + private Configuration config; + + @PUT + @Path("e2/{id}") + public DataResponse createOrUpdate_E2( + @PathParam("id") int id, String entityData, + @Context UriInfo uriInfo, + + // This is for test only. Don't do that at home. Max include depth must not be + // controlled by the client + @QueryParam("depth") Integer depth) { + UpdateBuilder builder = AgJaxrs.idempotentCreateOrUpdate(E2.class, config).byId(id) + .clientParams(uriInfo.getQueryParameters()); + + if (depth != null) { + builder.maxPathDepth(depth); + } + + return builder.syncAndSelect(entityData); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_NaturalIdIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/NaturalIdIT.java similarity index 82% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_NaturalIdIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/NaturalIdIT.java index f7487c2ad..12b0cecd5 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_NaturalIdIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/NaturalIdIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.DataResponse; @@ -8,32 +8,32 @@ import io.agrest.cayenne.cayenne.main.E21; import io.agrest.cayenne.cayenne.main.E23; import io.agrest.cayenne.cayenne.main.E29; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.Collection; import java.util.HashMap; import java.util.Map; -public class PUT_NaturalIdIT extends DbTest { +public class NaturalIdIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entitiesAndDependencies(E20.class, E21.class, E23.class, E29.class) .build(); @Test - public void testSingleId() { + public void singleId() { tester.e20().insertColumns("name_col") .values("John") @@ -45,11 +45,11 @@ public void testSingleId() { .wasOk() .bodyEquals(1, "{\"id\":\"John\",\"age\":28,\"description\":\"zzz\",\"name\":\"John\"}"); - tester.e20().matcher().eq("age", 28).eq("description", "zzz").assertOneMatch(); + tester.e20().matcher().eq("age", 28).andEq("description", "zzz").assertOneMatch(); } @Test - public void testSingle_Id_SeveralExistingObjects() { + public void single_Id_SeveralExistingObjects() { tester.e20().insertColumns("name_col") .values("John") .values("John").exec(); @@ -60,7 +60,7 @@ public void testSingle_Id_SeveralExistingObjects() { } @Test - public void testMultiId() { + public void multiId() { tester.e21().insertColumns("age", "name") .values(18, "John") @@ -73,11 +73,11 @@ public void testMultiId() { .bodyEquals(1, "{\"id\":{\"age\":28,\"name\":\"John\"},\"age\":28,\"description\":\"zzz\",\"name\":\"John\"}"); - tester.e21().matcher().eq("age", 28).eq("description", "zzz").assertOneMatch(); + tester.e21().matcher().eq("age", 28).andEq("description", "zzz").assertOneMatch(); } @Test - public void testSeveralExistingObjects_MultiId() { + public void severalExistingObjects_MultiId() { tester.e21().insertColumns("age", "name") .values(18, "John") .values(18, "John").exec(); @@ -91,7 +91,7 @@ public void testSeveralExistingObjects_MultiId() { } @Test - public void testNaturalIdInPayload() { + public void naturalIdInPayload() { tester.e23().insertColumns("id", "name").values(12, "John").exec(); @@ -103,7 +103,7 @@ public void testNaturalIdInPayload() { } @Test - public void testNaturalIdInPayload_MasqueradingAsId() { + public void naturalIdInPayload_MasqueradingAsId() { tester.e23().insertColumns("id", "name").values(12, "John").exec(); @@ -115,14 +115,14 @@ public void testNaturalIdInPayload_MasqueradingAsId() { } @Test - public void testMultiId_MixedDbObj() { + public void multiId_MixedDbObj() { tester.target("/mixed-multi-id") .put("{\"id\":{\"db:id1\":18,\"id2Prop\":345}}") .wasCreated() .bodyEquals(1, "{\"id\":{\"db:id1\":18,\"id2Prop\":345},\"id2Prop\":345}"); - tester.e29().matcher().eq("id1", 18).eq("id2", 345).assertOneMatch(); + tester.e29().matcher().eq("id1", 18).andEq("id2", 345).assertOneMatch(); tester.target("/mixed-multi-id") .put("{\"id\":{\"db:id1\":18,\"id2Prop\":345}}") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_ObjectIncludeIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ObjectIncludeIT.java similarity index 80% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_ObjectIncludeIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ObjectIncludeIT.java index 3dbd2a48c..d6590d3da 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_ObjectIncludeIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ObjectIncludeIT.java @@ -1,32 +1,32 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.DataResponse; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E5; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class PUT_ObjectIncludeIT extends DbTest { +public class ObjectIncludeIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E2.class, E3.class, E5.class) + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E5.class) .build(); @Test - public void testOverlap() { + public void overlap() { tester.e5().insertColumns("id", "name", "date").values(45, "T", "2013-01-03").exec(); tester.e2().insertColumns("id_", "name").values(8, "yyy").exec(); tester.e3().insertColumns("id_", "name", "e2_id", "e5_id").values(3, "zzz", 8, 45).exec(); @@ -37,11 +37,11 @@ public void testOverlap() { .wasOk() .bodyEquals(1, "{\"id\":3,\"e2\":{\"id\":8},\"e5\":{\"id\":45},\"name\":\"zzz\",\"phoneNumber\":null}"); - tester.e3().matcher().eq("id_", 3).eq("e2_id", 8).assertOneMatch(); + tester.e3().matcher().eq("id_", 3).andEq("e2_id", 8).assertOneMatch(); } @Test - public void testToOne() { + public void toOne() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") @@ -54,11 +54,11 @@ public void testToOne() { .wasOk() .bodyEquals(1, "{\"id\":3,\"e2\":{\"id\":1,\"address\":null,\"name\":\"xxx\"},\"name\":\"zzz\",\"phoneNumber\":null}"); - tester.e3().matcher().eq("id_", 3).eq("e2_id", 1).assertOneMatch(); + tester.e3().matcher().eq("id_", 3).andEq("e2_id", 1).assertOneMatch(); } @Test - public void testToMany() { + public void toMany() { tester.e2().insertColumns("id_", "name") .values(1, "xxx") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_Related_IT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ParentIT.java similarity index 79% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_Related_IT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ParentIT.java index e06ff9b68..d4c9aff75 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_Related_IT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ParentIT.java @@ -1,31 +1,42 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.DataResponse; -import io.agrest.cayenne.cayenne.main.*; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.cayenne.main.E1; +import io.agrest.cayenne.cayenne.main.E12; +import io.agrest.cayenne.cayenne.main.E12E13; +import io.agrest.cayenne.cayenne.main.E13; +import io.agrest.cayenne.cayenne.main.E15; +import io.agrest.cayenne.cayenne.main.E15E1; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.cayenne.main.E5; +import io.agrest.cayenne.cayenne.main.E7; +import io.agrest.cayenne.cayenne.main.E8; +import io.agrest.cayenne.cayenne.main.E9; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; -public class PUT_Related_IT extends DbTest { +public class ParentIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E1.class, E2.class, E3.class, E9.class) .entitiesAndDependencies(E5.class, E7.class, E8.class, E12.class, E13.class, E15.class) .build(); @Test - public void testRelate_EmptyPutWithID() { + public void relate_EmptyPutWithID() { tester.e2().insertColumns("id_", "name") .values(24, "xxx").exec(); @@ -39,11 +50,11 @@ public void testRelate_EmptyPutWithID() { .wasOk() .bodyEquals(1, "{\"id\":24,\"address\":null,\"name\":\"xxx\"}"); - tester.e3().matcher().eq("e2_id", 24).eq("name", "yyy").assertOneMatch(); + tester.e3().matcher().eq("e2_id", 24).andEq("name", "yyy").assertOneMatch(); } @Test - public void testRelate_ValidRel_ToOne_Existing() { + public void relate_ValidRel_ToOne_Existing() { tester.e2().insertColumns("id_", "name") .values(24, "xxx").exec(); @@ -55,11 +66,11 @@ public void testRelate_ValidRel_ToOne_Existing() { .wasOk() .bodyEquals(1, "{\"id\":24,\"address\":null,\"name\":\"xxx\"}"); - tester.e3().matcher().eq("e2_id", 24).eq("name", "yyy").assertOneMatch(); + tester.e3().matcher().eq("e2_id", 24).andEq("name", "yyy").assertOneMatch(); } @Test - public void testRelate_ValidRel_ToOne_Existing_WithUpdate() { + public void relate_ValidRel_ToOne_Existing_WithUpdate() { tester.e2().insertColumns("id_", "name") .values(24, "xxx").exec(); @@ -74,11 +85,11 @@ public void testRelate_ValidRel_ToOne_Existing_WithUpdate() { .bodyEquals(1, "{\"id\":24,\"address\":null,\"name\":\"123\"}"); tester.e2().matcher().assertOneMatch(); - tester.e3().matcher().eq("e2_id", 24).eq("name", "yyy").assertOneMatch(); + tester.e3().matcher().eq("e2_id", 24).andEq("name", "yyy").assertOneMatch(); } @Test - public void testRelate_ToMany_MixedCollection() { + public void relate_ToMany_MixedCollection() { tester.e8().insertColumns("id", "name") .values(15, "xxx") @@ -106,7 +117,7 @@ public void testRelate_ToMany_MixedCollection() { } @Test - public void test_ToMany_CreateUpdateDelete() { + public void toMany_CreateUpdateDelete() { tester.e8().insertColumns("id", "name") .values(15, "xxx") @@ -138,7 +149,7 @@ public void test_ToMany_CreateUpdateDelete() { } @Test - public void testRelate_ValidRel_ToOne_New_AutogenId() { + public void relate_ValidRel_ToOne_New_AutogenId() { tester.e3().insertColumns("id_", "name") .values(7, "zzz") @@ -153,7 +164,7 @@ public void testRelate_ValidRel_ToOne_New_AutogenId() { } @Test - public void testRelate_ValidRel_ToOne_New_DefaultId() { + public void relate_ValidRel_ToOne_New_DefaultId() { tester.e7().insertColumns("id") .values(7) @@ -167,8 +178,8 @@ public void testRelate_ValidRel_ToOne_New_DefaultId() { tester.e8().matcher().eq("name", "aaa").assertOneMatch(); tester.e8().matcher().eq("id", 24).assertOneMatch(); - tester.e7().matcher().eq("id", 8).eq("e8_id", 24).assertOneMatch(); - tester.e7().matcher().eq("id", 7).eq("e8_id", null).assertOneMatch(); + tester.e7().matcher().eq("id", 8).andEq("e8_id", 24).assertOneMatch(); + tester.e7().matcher().eq("id", 7).andEq("e8_id", null).assertOneMatch(); // PUT is idempotent... doing another update should not change the picture tester.target("/e7/8/e8/24") @@ -179,12 +190,12 @@ public void testRelate_ValidRel_ToOne_New_DefaultId() { tester.e8().matcher().assertOneMatch(); tester.e8().matcher().eq("name", "aaa").assertOneMatch(); tester.e8().matcher().eq("id", 24).assertOneMatch(); - tester.e7().matcher().eq("id", 8).eq("e8_id", 24).assertOneMatch(); - tester.e7().matcher().eq("id", 7).eq("e8_id", null).assertOneMatch(); + tester.e7().matcher().eq("id", 8).andEq("e8_id", 24).assertOneMatch(); + tester.e7().matcher().eq("id", 7).andEq("e8_id", null).assertOneMatch(); } @Test - public void testRelate_ValidRel_ToOne_New_PropagatedId() { + public void relate_ValidRel_ToOne_New_PropagatedId() { tester.e8().insertColumns("id") .values(7) @@ -205,7 +216,7 @@ public void testRelate_ValidRel_ToOne_New_PropagatedId() { } @Test - public void testRelate_ToMany_NoIds() { + public void relate_ToMany_NoIds() { tester.e2().insertColumns("id_", "name") .values(15, "xxx") @@ -227,7 +238,7 @@ public void testRelate_ToMany_NoIds() { } @Test - public void testToMany_Join() { + public void toMany_Join() { tester.e12().insertColumns("id").values(11).values(12).exec(); tester.e13().insertColumns("id").values(14).values(15).values(16).exec(); @@ -240,8 +251,8 @@ public void testToMany_Join() { tester.e12_13().matcher().assertMatches(2); - tester.e12_13().matcher().eq("e12_id", 12).eq("e13_id", 14).assertOneMatch(); - tester.e12_13().matcher().eq("e12_id", 12).eq("e13_id", 15).assertOneMatch(); + tester.e12_13().matcher().eq("e12_id", 12).andEq("e13_id", 14).assertOneMatch(); + tester.e12_13().matcher().eq("e12_id", 12).andEq("e13_id", 15).assertOneMatch(); // testing idempotency tester.target("/e12/12/e1213") @@ -251,8 +262,8 @@ public void testToMany_Join() { tester.e12_13().matcher().assertMatches(2); - tester.e12_13().matcher().eq("e12_id", 12).eq("e13_id", 14).assertOneMatch(); - tester.e12_13().matcher().eq("e12_id", 12).eq("e13_id", 15).assertOneMatch(); + tester.e12_13().matcher().eq("e12_id", 12).andEq("e13_id", 14).assertOneMatch(); + tester.e12_13().matcher().eq("e12_id", 12).andEq("e13_id", 15).assertOneMatch(); // add one and delete another record tester.target("/e12/12/e1213") @@ -261,12 +272,12 @@ public void testToMany_Join() { .wasOk().bodyEquals(2, "{},{}"); tester.e12_13().matcher().assertMatches(2); - tester.e12_13().matcher().eq("e12_id", 12).eq("e13_id", 14).assertOneMatch(); - tester.e12_13().matcher().eq("e12_id", 12).eq("e13_id", 16).assertOneMatch(); + tester.e12_13().matcher().eq("e12_id", 12).andEq("e13_id", 14).assertOneMatch(); + tester.e12_13().matcher().eq("e12_id", 12).andEq("e13_id", 16).assertOneMatch(); } @Test - public void testToMany_DifferentIdTypes() { + public void toMany_DifferentIdTypes() { tester.e1().insertColumns("id", "name") .values(1, "xxx") @@ -286,14 +297,14 @@ public void testToMany_DifferentIdTypes() { .put("[{\"e1\":1}]").wasOk(); tester.e15_1().matcher().assertOneMatch(); - tester.e15_1().matcher().eq("e15_id", 14).eq("e1_id", 1).assertOneMatch(); + tester.e15_1().matcher().eq("e15_id", 14).andEq("e1_id", 1).assertOneMatch(); tester.e1().matcher().assertMatches(2); tester.e15().matcher().assertMatches(3); } @Test - public void testToMany_Flattened_DifferentIdTypes() { + public void toMany_Flattened_DifferentIdTypes() { tester.e5().insertColumns("id", "name") .values(1, "xxx") @@ -311,7 +322,7 @@ public void testToMany_Flattened_DifferentIdTypes() { tester.e15_5().matcher().assertOneMatch(); tester.e5().matcher().assertMatches(2); tester.e15().matcher().assertMatches(3); - tester.e15_5().matcher().eq("e15_id", 14).eq("e5_id", 1).assertOneMatch(); + tester.e15_5().matcher().eq("e15_id", 14).andEq("e5_id", 1).assertOneMatch(); } @Path("") @@ -395,7 +406,10 @@ public DataResponse createOrUpdate_Joins(@PathParam("id") long id, @Conte @PUT @Path("e15/{id}") public DataResponse createOrUpdate_Joins_FlattenedRel(@PathParam("id") long id, @Context UriInfo info, String entityData) { - return AgJaxrs.createOrUpdate(E15.class, config).byId(id).clientParams(info.getQueryParameters()).syncAndSelect(entityData); + return AgJaxrs.createOrUpdate(E15.class, config) + .byId(id) + .clientParams(info.getQueryParameters()) + .syncAndSelect(entityData); } } } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_Related_ByKeyIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/Parent_MapperIT.java similarity index 82% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_Related_ByKeyIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/Parent_MapperIT.java index 0c43f5593..e77205171 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_Related_ByKeyIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/Parent_MapperIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.DataResponse; import io.agrest.cayenne.cayenne.main.E14; @@ -6,28 +6,28 @@ import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E7; import io.agrest.cayenne.cayenne.main.E8; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.runtime.processor.update.ByKeyObjectMapperFactory; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; -public class PUT_Related_ByKeyIT extends DbTest { +public class Parent_MapperIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) - .entities(E7.class, E8.class, E14.class, E15.class) + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E7.class, E8.class, E14.class, E15.class) .build(); @Test - public void testRelate_ToMany_MixedCollection() { + public void relate_ToMany_MixedCollection() { tester.e8().insertColumns("id", "name") .values(15, "xxx") @@ -59,7 +59,7 @@ public void testRelate_ToMany_MixedCollection() { } @Test - public void testRelate_ToMany_PropertyMapper() { + public void relate_ToMany_PropertyMapper() { tester.e8().insertColumns("id", "name") .values(15, "xxx") @@ -79,7 +79,7 @@ public void testRelate_ToMany_PropertyMapper() { } @Test - public void testToMany_LongId() { + public void toMany_LongId() { tester.e15().insertColumns("long_id", "name") .values(5L, "aaa") @@ -100,11 +100,7 @@ public void testToMany_LongId() { "{\"id\":11,\"name\":\"new\",\"prettyName\":\"new_pretty\"}"); tester.e14().matcher().eq("e15_id", 44).assertMatches(2); - - // TODO: checking individual records one by one until "in()" becomes available in BQ 1.1 per - // https://github.com/bootique/bootique-jdbc/issues/92 - tester.e14().matcher().eq("e15_id", 44).eq("long_id", 4L).assertOneMatch(); - tester.e14().matcher().eq("e15_id", 44).eq("long_id", 11L).assertOneMatch(); + tester.e14().matcher().eq("e15_id", 44).andIn("long_id", 4L, 11L).assertMatches(2); } @Path("") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_ReadFilterIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ReadFilter_OverlayIT.java similarity index 79% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_ReadFilterIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ReadFilter_OverlayIT.java index a68569703..f98075fbd 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_ReadFilterIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/ReadFilter_OverlayIT.java @@ -1,4 +1,4 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.DataResponse; @@ -7,27 +7,27 @@ import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.bootique.junit5.BQTestTool; import org.apache.cayenne.Cayenne; import org.apache.cayenne.DataObject; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import java.util.List; -public class PUT_ReadFilterIT extends DbTest { +public class ReadFilter_OverlayIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class) .agCustomizer(ab -> ab .entityOverlay(AgEntity.overlay(E2.class).readFilter(evenFilter())) @@ -45,7 +45,7 @@ static ReadFilter oddFilter() { } @Test - public void testInStack() { + public void inStack() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -61,13 +61,13 @@ public void testInStack() { .wasOk().bodyEquals(2, "{\"id\":2,\"name\":\"Bb\"}", "{\"id\":4,\"name\":\"Cc\"}"); tester.e2().matcher().assertMatches(3); - tester.e2().matcher().eq("id_", 1).eq("name", "Aa").assertOneMatch(); - tester.e2().matcher().eq("id_", 2).eq("name", "Bb").assertOneMatch(); - tester.e2().matcher().eq("id_", 4).eq("name", "Cc").assertOneMatch(); + tester.e2().matcher().eq("id_", 1).andEq("name", "Aa").assertOneMatch(); + tester.e2().matcher().eq("id_", 2).andEq("name", "Bb").assertOneMatch(); + tester.e2().matcher().eq("id_", 4).andEq("name", "Cc").assertOneMatch(); } @Test - public void testInStack_Related() { + public void inStack_Related() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -89,7 +89,7 @@ public void testInStack_Related() { } @Test - public void testInStackAndRequest() { + public void inStackAndRequest() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -105,9 +105,9 @@ public void testInStackAndRequest() { .wasOk().bodyEquals(1, "{\"id\":2,\"name\":\"Bb\"}"); tester.e2().matcher().assertMatches(3); - tester.e2().matcher().eq("id_", 1).eq("name", "Aa").assertOneMatch(); - tester.e2().matcher().eq("id_", 2).eq("name", "Bb").assertOneMatch(); - tester.e2().matcher().eq("id_", 4).eq("name", "Cc").assertOneMatch(); + tester.e2().matcher().eq("id_", 1).andEq("name", "Aa").assertOneMatch(); + tester.e2().matcher().eq("id_", 2).andEq("name", "Bb").assertOneMatch(); + tester.e2().matcher().eq("id_", 4).andEq("name", "Cc").assertOneMatch(); } @Path("") diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/RelateIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/RelateIT.java new file mode 100644 index 000000000..3c3c97877 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/RelateIT.java @@ -0,0 +1,205 @@ +package io.agrest.cayenne.PUT; + +import io.agrest.DataResponse; +import io.agrest.cayenne.cayenne.main.E2; +import io.agrest.cayenne.cayenne.main.E29; +import io.agrest.cayenne.cayenne.main.E3; +import io.agrest.cayenne.cayenne.main.E30; +import io.agrest.cayenne.cayenne.main.E7; +import io.agrest.cayenne.cayenne.main.E8; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; +import io.bootique.junit5.BQTestTool; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; +import org.junit.jupiter.api.Test; + +public class RelateIT extends MainDbTest { + + @BQTestTool + static final MainModelTester tester = tester(Resource.class) + .entitiesAndDependencies(E2.class, E3.class, E7.class, E29.class, E30.class) + .build(); + + @Test + public void toOne() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(8, "yyy").exec(); + tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", 8).exec(); + + tester.target("/e3/3") + .put("{\"id\":3,\"e2\":1}") + .wasOk() + .bodyEquals(1, "{\"id\":3,\"name\":\"zzz\",\"phoneNumber\":null}"); + + tester.e3().matcher().eq("id_", 3).andEq("e2_id", 1).assertOneMatch(); + } + + @Test + public void toOne_CompoundId() { + + tester.e29().insertColumns("id1", "id2") + .values(11, 21) + .values(12, 22).exec(); + + tester.e30().insertColumns("id", "e29_id1", "e29_id2") + .values(3, 11, 21).exec(); + + tester.target("/e30/3") + .queryParam("include", "e29.id") + .put("{\"id\":3,\"e29\":{\"db:id1\":12,\"id2Prop\":22}}") + .wasOk() + .bodyEquals(1, "{\"id\":3,\"e29\":{\"id\":{\"db:id1\":12,\"id2Prop\":22}}}"); + + tester.e30().matcher().eq("id", 3).andEq("e29_id1", 12).andEq("e29_id2", 22).assertOneMatch(); + } + + @Test + public void toOne_ToNull() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(8, "yyy").exec(); + tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", 8).exec(); + + tester.target("/e3/3") + .put("{\"id\":3,\"e2\":null}") + .wasOk().bodyEquals(1, "{\"id\":3,\"name\":\"zzz\",\"phoneNumber\":null}"); + + tester.e3().matcher().assertOneMatch(); + tester.e3().matcher().eq("id_", 3).andEq("e2_id", null).assertOneMatch(); + } + + @Test + public void toOne_FromNull() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(8, "yyy").exec(); + tester.e3().insertColumns("id_", "name", "e2_id").values(3, "zzz", null).exec(); + + tester.target("/e3/3").put("{\"id\":3,\"e2\":8}") + .wasOk() + .bodyEquals(1, "{\"id\":3,\"name\":\"zzz\",\"phoneNumber\":null}"); + + tester.e3().matcher().eq("id_", 3).andEq("e2_id", 8).assertOneMatch(); + } + + @Test + public void single_ResponseToOneRelationshipFilter() { + + tester.e8().insertColumns("id", "name").values(5, "aaa").values(6, "ert").exec(); + tester.e9().insertColumns("e8_id").values(5).values(6).exec(); + + tester.target("/e7/6") + .queryParam("include", "id", E7.E8.dot(E8.E9).getName()) + .queryParam("exclude", E7.NAME.getName()) + .put("[{\"name\":\"yyy\",\"e8\":6}]") + .wasCreated() + .bodyEquals(1, "{\"id\":6,\"e8\":{\"e9\":{\"id\":6}}}"); + } + + @Test + public void toMany() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(8, "yyy").exec(); + tester.e3().insertColumns("id_", "name", "e2_id") + .values(3, "zzz", null) + .values(4, "aaa", 8) + .values(5, "bbb", 8).exec(); + + tester.target("/e2/1") + .queryParam("include", E2.E3S.getName()) + .queryParam("exclude", E2.ADDRESS.getName(), E2.NAME.getName(), E2.E3S.dot(E3.NAME).getName(), E2.E3S.dot(E3.PHONE_NUMBER).getName()) + .put("{\"e3s\":[3,4,5]}") + .wasOk().bodyEquals(1, "{\"id\":1,\"e3s\":[{\"id\":3},{\"id\":4},{\"id\":5}]}"); + + tester.e3().matcher().eq("e2_id", 1).assertMatches(3); + } + + @Test + public void toMany_UnrelateAll() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(8, "yyy").exec(); + tester.e3().insertColumns("id_", "name", "e2_id") + .values(3, "zzz", null) + .values(4, "aaa", 8) + .values(5, "bbb", 8).exec(); + + tester.target("/e2/8") + .queryParam("include", E2.E3S.getName()) + .queryParam("exclude", E2.ADDRESS.getName(), E2.NAME.getName(), E2.E3S.dot(E3.NAME).getName(), E2.E3S.dot(E3.PHONE_NUMBER).getName()) + .put("{\"e3s\":[]}") + .wasOk() + .bodyEquals(1, "{\"id\":8,\"e3s\":[]}"); + + tester.e3().matcher().eq("e2_id", null).assertMatches(3); + } + + @Test + public void toMany_UnrelateOne() { + + tester.e2().insertColumns("id_", "name") + .values(1, "xxx") + .values(8, "yyy").exec(); + tester.e3().insertColumns("id_", "name", "e2_id") + .values(3, "zzz", null) + .values(4, "aaa", 8) + .values(5, "bbb", 8).exec(); + + tester.target("/e2/1") + .queryParam("include", E2.E3S.getName()) + .queryParam("exclude", E2.ADDRESS.getName(), E2.NAME.getName(), E2.E3S.dot(E3.NAME).getName(), E2.E3S.dot(E3.PHONE_NUMBER).getName()) + .put("{\"e3s\":[4]}") + .wasOk().bodyEquals(1, "{\"id\":1,\"e3s\":[{\"id\":4}]}"); + + tester.e3().matcher().eq("e2_id", 1).andEq("id_", 4).assertOneMatch(); + tester.e3().matcher().eq("e2_id", 8).andEq("id_", 5).assertOneMatch(); + } + + + @Path("") + public static class Resource { + + @Context + private Configuration config; + + @PUT + @Path("e2/{id}") + public DataResponse createOrUpdate_E2(@PathParam("id") int id, String entityData, @Context UriInfo uriInfo) { + return AgJaxrs.idempotentCreateOrUpdate(E2.class, config).byId(id).clientParams(uriInfo.getQueryParameters()).syncAndSelect(entityData); + } + + @PUT + @Path("e3/{id}") + public DataResponse updateE3(@PathParam("id") int id, String requestBody) { + return AgJaxrs.update(E3.class, config).byId(id).syncAndSelect(requestBody); + } + + @PUT + @Path("e7/{id}") + public DataResponse syncOneE7(@PathParam("id") int id, @Context UriInfo uriInfo, String data) { + return AgJaxrs.idempotentFullSync(E7.class, config).byId(id).clientParams(uriInfo.getQueryParameters()).syncAndSelect(data); + } + + @PUT + @Path("e30/{id}") + public DataResponse updateE30(@PathParam("id") int id, @Context UriInfo uriInfo, String requestBody) { + return AgJaxrs.update(E30.class, config) + .byId(id) + .clientParams(uriInfo.getQueryParameters()) + .syncAndSelect(requestBody); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_StagesIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/StagesIT.java similarity index 93% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_StagesIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/StagesIT.java index e70d39b3f..371d855af 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_StagesIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/StagesIT.java @@ -1,30 +1,30 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.DataResponse; import io.agrest.UpdateStage; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; import io.agrest.cayenne.cayenne.main.E3; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.runtime.processor.update.UpdateContext; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.UriInfo; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -public class PUT_StagesIT extends DbTest { +public class StagesIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E3.class) .build(); @@ -41,7 +41,7 @@ public void resetCallbacks() { } @Test - public void testToOne() { + public void toOne() { tester.e3().insertColumns("id_", "name") .values(3, "z") @@ -52,7 +52,7 @@ public void testToOne() { .wasOk() .bodyEquals(1, "{\"id\":3,\"name\":\"x\",\"phoneNumber\":null}"); - tester.e3().matcher().eq("id_", 3).eq("name", "x").assertOneMatch(); + tester.e3().matcher().eq("id_", 3).andEq("name", "x").assertOneMatch(); tester.e3().matcher().eq("id_", 4).assertNoMatches(); assertTrue(Resource.START_CALLED); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_UpdateAuthorizerIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/UpdateAuthorizerIT.java similarity index 67% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_UpdateAuthorizerIT.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/UpdateAuthorizerIT.java index 804bad91d..358dd86c3 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT_UpdateAuthorizerIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/UpdateAuthorizerIT.java @@ -1,35 +1,35 @@ -package io.agrest.cayenne; +package io.agrest.cayenne.PUT; import io.agrest.EntityUpdate; import io.agrest.SimpleResponse; import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.cayenne.main.E4; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.agrest.meta.AgEntity; import io.bootique.junit5.BQTestTool; import org.junit.jupiter.api.Test; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; import java.util.List; -public class PUT_UpdateAuthorizerIT extends DbTest { +public class UpdateAuthorizerIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class) + static final MainModelTester tester = tester(Resource.class) .entities(E2.class, E3.class, E4.class) .agCustomizer(ab -> ab - .entityOverlay(AgEntity.overlay(E2.class).updateAuthorizer((o, u) -> o.getName().equals(u.getValues().get("name")))) + .entityOverlay(AgEntity.overlay(E2.class).updateAuthorizer((o, u) -> o.getName().equals(u.getAttributes().get("name")))) ).build(); @Test - public void testInStack_Allowed() { + public void inStack_Allowed() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -41,12 +41,12 @@ public void testInStack_Allowed() { .wasOk(); tester.e2().matcher().assertMatches(2); - tester.e2().matcher().eq("name", "a").eq("address", "Aa").assertOneMatch(); - tester.e2().matcher().eq("name", "b").eq("address", "Bb").assertOneMatch(); + tester.e2().matcher().eq("name", "a").andEq("address", "Aa").assertOneMatch(); + tester.e2().matcher().eq("name", "b").andEq("address", "Bb").assertOneMatch(); } @Test - public void testInStack_Blocked() { + public void inStack_Blocked() { tester.e2().insertColumns("id_", "name") .values(1, "a") .values(2, "b") @@ -57,12 +57,12 @@ public void testInStack_Blocked() { .wasForbidden(); tester.e2().matcher().assertMatches(2); - tester.e2().matcher().eq("name", "a").eq("address", null).assertOneMatch(); - tester.e2().matcher().eq("name", "b").eq("address", null).assertOneMatch(); + tester.e2().matcher().eq("name", "a").andEq("address", null).assertOneMatch(); + tester.e2().matcher().eq("name", "b").andEq("address", null).assertOneMatch(); } @Test - public void testInRequestAndStack_Allowed() { + public void inRequestAndStack_Allowed() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -74,12 +74,12 @@ public void testInRequestAndStack_Allowed() { .wasOk(); tester.e2().matcher().assertMatches(2); - tester.e2().matcher().eq("name", "a").eq("address", "Aa").assertOneMatch(); - tester.e2().matcher().eq("name", "b").eq("address", "Bb").assertOneMatch(); + tester.e2().matcher().eq("name", "a").andEq("address", "Aa").assertOneMatch(); + tester.e2().matcher().eq("name", "b").andEq("address", "Bb").assertOneMatch(); } @Test - public void testInRequestAndStack_Blocked() { + public void inRequestAndStack_Blocked() { tester.e2().insertColumns("id_", "name") .values(1, "a") @@ -91,8 +91,8 @@ public void testInRequestAndStack_Blocked() { .wasForbidden(); tester.e2().matcher().assertMatches(2); - tester.e2().matcher().eq("name", "a").eq("address", null).assertOneMatch(); - tester.e2().matcher().eq("name", "b").eq("address", null).assertOneMatch(); + tester.e2().matcher().eq("name", "a").andEq("address", null).assertOneMatch(); + tester.e2().matcher().eq("name", "b").andEq("address", null).assertOneMatch(); } @@ -116,7 +116,7 @@ public SimpleResponse putE2RequestAndStackFilter( return AgJaxrs .createOrUpdate(E2.class, config) - .updateAuthorizer(E2.class, (o, u) -> !name.equals(u.getValues().get("name"))) + .updateAuthorizer(E2.class, (o, u) -> !name.equals(u.getAttributes().get("name"))) .sync(updates); } } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Sub1.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Sub1.java new file mode 100644 index 000000000..31760c355 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Sub1.java @@ -0,0 +1,13 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.annotation.AgAttribute; +import io.agrest.cayenne.cayenne.inheritance.auto._Aie1Sub1; + +public class Aie1Sub1 extends _Aie1Sub1 { + + @AgAttribute(readable = true) + @Override + public String getA0() { + return super.getA0(); + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Sub1Sub1.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Sub1Sub1.java new file mode 100644 index 000000000..c5a393b87 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Sub1Sub1.java @@ -0,0 +1,9 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.cayenne.cayenne.inheritance.auto._Aie1Sub1Sub1; + +public class Aie1Sub1Sub1 extends _Aie1Sub1Sub1 { + + private static final long serialVersionUID = 1L; + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Sub2.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Sub2.java new file mode 100644 index 000000000..eda6e8a8f --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Sub2.java @@ -0,0 +1,9 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.cayenne.cayenne.inheritance.auto._Aie1Sub2; + +public class Aie1Sub2 extends _Aie1Sub2 { + + private static final long serialVersionUID = 1L; + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Super.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Super.java new file mode 100644 index 000000000..8970b073a --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Aie1Super.java @@ -0,0 +1,13 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.annotation.AgAttribute; +import io.agrest.cayenne.cayenne.inheritance.auto._Aie1Super; + +public abstract class Aie1Super extends _Aie1Super { + + @AgAttribute(readable = false) + @Override + public String getA0() { + return super.getA0(); + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Sub1.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Sub1.java new file mode 100644 index 000000000..74c113d97 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Sub1.java @@ -0,0 +1,9 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.cayenne.cayenne.inheritance.auto._Ie1Sub1; + +public class Ie1Sub1 extends _Ie1Sub1 { + + private static final long serialVersionUID = 1L; + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Sub1Sub1.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Sub1Sub1.java new file mode 100644 index 000000000..dd28e3c64 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Sub1Sub1.java @@ -0,0 +1,9 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.cayenne.cayenne.inheritance.auto._Ie1Sub1Sub1; + +public class Ie1Sub1Sub1 extends _Ie1Sub1Sub1 { + + private static final long serialVersionUID = 1L; + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Sub2.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Sub2.java new file mode 100644 index 000000000..56ec863aa --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Sub2.java @@ -0,0 +1,9 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.cayenne.cayenne.inheritance.auto._Ie1Sub2; + +public class Ie1Sub2 extends _Ie1Sub2 { + + private static final long serialVersionUID = 1L; + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Super.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Super.java new file mode 100644 index 000000000..f1c6d4a3f --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie1Super.java @@ -0,0 +1,9 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.cayenne.cayenne.inheritance.auto._Ie1Super; + +public abstract class Ie1Super extends _Ie1Super { + + private static final long serialVersionUID = 1L; + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie2.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie2.java new file mode 100644 index 000000000..abac15b63 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie2.java @@ -0,0 +1,9 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.cayenne.cayenne.inheritance.auto._Ie2; + +public class Ie2 extends _Ie2 { + + private static final long serialVersionUID = 1L; + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie3.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie3.java new file mode 100644 index 000000000..562dbd242 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/Ie3.java @@ -0,0 +1,9 @@ +package io.agrest.cayenne.cayenne.inheritance; + +import io.agrest.cayenne.cayenne.inheritance.auto._Ie3; + +public class Ie3 extends _Ie3 { + + private static final long serialVersionUID = 1L; + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Sub1.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Sub1.java new file mode 100644 index 000000000..704383198 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Sub1.java @@ -0,0 +1,107 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.cayenne.exp.property.EntityProperty; +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.StringProperty; + +import io.agrest.cayenne.cayenne.inheritance.Aie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; + +/** + * Class _Aie1Sub1 was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Aie1Sub1 extends Aie1Super { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final StringProperty A1 = PropertyFactory.createString("a1", String.class); + public static final EntityProperty IE2 = PropertyFactory.createEntity("ie2", Ie2.class); + + protected String a1; + + protected Object ie2; + + public void setA1(String a1) { + beforePropertyWrite("a1", this.a1, a1); + this.a1 = a1; + } + + public String getA1() { + beforePropertyRead("a1"); + return this.a1; + } + + public void setIe2(Ie2 ie2) { + setToOneTarget("ie2", ie2, true); + } + + public Ie2 getIe2() { + return (Ie2)readProperty("ie2"); + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "a1": + return this.a1; + case "ie2": + return this.ie2; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "a1": + this.a1 = (String)val; + break; + case "ie2": + this.ie2 = val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.a1); + out.writeObject(this.ie2); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.a1 = (String)in.readObject(); + this.ie2 = in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Sub1Sub1.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Sub1Sub1.java new file mode 100644 index 000000000..5b54eaed5 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Sub1Sub1.java @@ -0,0 +1,88 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.StringProperty; + +import io.agrest.cayenne.cayenne.inheritance.Aie1Sub1; + +/** + * Class _Aie1Sub1Sub1 was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Aie1Sub1Sub1 extends Aie1Sub1 { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final StringProperty A3 = PropertyFactory.createString("a3", String.class); + + protected String a3; + + + public void setA3(String a3) { + beforePropertyWrite("a3", this.a3, a3); + this.a3 = a3; + } + + public String getA3() { + beforePropertyRead("a3"); + return this.a3; + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "a3": + return this.a3; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "a3": + this.a3 = (String)val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.a3); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.a3 = (String)in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Sub2.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Sub2.java new file mode 100644 index 000000000..aa10f7f82 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Sub2.java @@ -0,0 +1,88 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.StringProperty; + +import io.agrest.cayenne.cayenne.inheritance.Aie1Super; + +/** + * Class _Aie1Sub2 was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Aie1Sub2 extends Aie1Super { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final StringProperty A2 = PropertyFactory.createString("a2", String.class); + + protected String a2; + + + public void setA2(String a2) { + beforePropertyWrite("a2", this.a2, a2); + this.a2 = a2; + } + + public String getA2() { + beforePropertyRead("a2"); + return this.a2; + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "a2": + return this.a2; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "a2": + this.a2 = (String)val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.a2); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.a2 = (String)in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Super.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Super.java new file mode 100644 index 000000000..7fa4d7178 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Aie1Super.java @@ -0,0 +1,133 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.List; + +import org.apache.cayenne.BaseDataObject; +import org.apache.cayenne.exp.property.ListProperty; +import org.apache.cayenne.exp.property.NumericProperty; +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.StringProperty; + +import io.agrest.cayenne.cayenne.inheritance.Ie3; + +/** + * Class _Aie1Super was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Aie1Super extends BaseDataObject { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final StringProperty A0 = PropertyFactory.createString("a0", String.class); + public static final NumericProperty TYPE = PropertyFactory.createNumeric("type", Integer.class); + public static final ListProperty IE3S = PropertyFactory.createList("ie3s", Ie3.class); + + protected String a0; + protected int type; + + protected Object ie3s; + + public void setA0(String a0) { + beforePropertyWrite("a0", this.a0, a0); + this.a0 = a0; + } + + public String getA0() { + beforePropertyRead("a0"); + return this.a0; + } + + public void setType(int type) { + beforePropertyWrite("type", this.type, type); + this.type = type; + } + + public int getType() { + beforePropertyRead("type"); + return this.type; + } + + public void addToIe3s(Ie3 obj) { + addToManyTarget("ie3s", obj, true); + } + + public void removeFromIe3s(Ie3 obj) { + removeToManyTarget("ie3s", obj, true); + } + + @SuppressWarnings("unchecked") + public List getIe3s() { + return (List)readProperty("ie3s"); + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "a0": + return this.a0; + case "type": + return this.type; + case "ie3s": + return this.ie3s; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "a0": + this.a0 = (String)val; + break; + case "type": + this.type = val == null ? 0 : (int)val; + break; + case "ie3s": + this.ie3s = val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.a0); + out.writeInt(this.type); + out.writeObject(this.ie3s); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.a0 = (String)in.readObject(); + this.type = in.readInt(); + this.ie3s = in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Sub1.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Sub1.java new file mode 100644 index 000000000..cbca0d8e8 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Sub1.java @@ -0,0 +1,107 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.cayenne.exp.property.EntityProperty; +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.StringProperty; + +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; + +/** + * Class _Ie1Sub1 was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Ie1Sub1 extends Ie1Super { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final StringProperty A1 = PropertyFactory.createString("a1", String.class); + public static final EntityProperty IE2 = PropertyFactory.createEntity("ie2", Ie2.class); + + protected String a1; + + protected Object ie2; + + public void setA1(String a1) { + beforePropertyWrite("a1", this.a1, a1); + this.a1 = a1; + } + + public String getA1() { + beforePropertyRead("a1"); + return this.a1; + } + + public void setIe2(Ie2 ie2) { + setToOneTarget("ie2", ie2, true); + } + + public Ie2 getIe2() { + return (Ie2)readProperty("ie2"); + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "a1": + return this.a1; + case "ie2": + return this.ie2; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "a1": + this.a1 = (String)val; + break; + case "ie2": + this.ie2 = val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.a1); + out.writeObject(this.ie2); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.a1 = (String)in.readObject(); + this.ie2 = in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Sub1Sub1.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Sub1Sub1.java new file mode 100644 index 000000000..c018589c1 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Sub1Sub1.java @@ -0,0 +1,88 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.StringProperty; + +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; + +/** + * Class _Ie1Sub1Sub1 was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Ie1Sub1Sub1 extends Ie1Sub1 { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final StringProperty A3 = PropertyFactory.createString("a3", String.class); + + protected String a3; + + + public void setA3(String a3) { + beforePropertyWrite("a3", this.a3, a3); + this.a3 = a3; + } + + public String getA3() { + beforePropertyRead("a3"); + return this.a3; + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "a3": + return this.a3; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "a3": + this.a3 = (String)val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.a3); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.a3 = (String)in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Sub2.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Sub2.java new file mode 100644 index 000000000..c74e53b03 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Sub2.java @@ -0,0 +1,88 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.StringProperty; + +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; + +/** + * Class _Ie1Sub2 was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Ie1Sub2 extends Ie1Super { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final StringProperty A2 = PropertyFactory.createString("a2", String.class); + + protected String a2; + + + public void setA2(String a2) { + beforePropertyWrite("a2", this.a2, a2); + this.a2 = a2; + } + + public String getA2() { + beforePropertyRead("a2"); + return this.a2; + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "a2": + return this.a2; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "a2": + this.a2 = (String)val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.a2); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.a2 = (String)in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Super.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Super.java new file mode 100644 index 000000000..507d9e509 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie1Super.java @@ -0,0 +1,133 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.List; + +import org.apache.cayenne.BaseDataObject; +import org.apache.cayenne.exp.property.ListProperty; +import org.apache.cayenne.exp.property.NumericProperty; +import org.apache.cayenne.exp.property.PropertyFactory; +import org.apache.cayenne.exp.property.StringProperty; + +import io.agrest.cayenne.cayenne.inheritance.Ie3; + +/** + * Class _Ie1Super was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Ie1Super extends BaseDataObject { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final StringProperty A0 = PropertyFactory.createString("a0", String.class); + public static final NumericProperty TYPE = PropertyFactory.createNumeric("type", Integer.class); + public static final ListProperty IE3S = PropertyFactory.createList("ie3s", Ie3.class); + + protected String a0; + protected int type; + + protected Object ie3s; + + public void setA0(String a0) { + beforePropertyWrite("a0", this.a0, a0); + this.a0 = a0; + } + + public String getA0() { + beforePropertyRead("a0"); + return this.a0; + } + + public void setType(int type) { + beforePropertyWrite("type", this.type, type); + this.type = type; + } + + public int getType() { + beforePropertyRead("type"); + return this.type; + } + + public void addToIe3s(Ie3 obj) { + addToManyTarget("ie3s", obj, true); + } + + public void removeFromIe3s(Ie3 obj) { + removeToManyTarget("ie3s", obj, true); + } + + @SuppressWarnings("unchecked") + public List getIe3s() { + return (List)readProperty("ie3s"); + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "a0": + return this.a0; + case "type": + return this.type; + case "ie3s": + return this.ie3s; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "a0": + this.a0 = (String)val; + break; + case "type": + this.type = val == null ? 0 : (int)val; + break; + case "ie3s": + this.ie3s = val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.a0); + out.writeInt(this.type); + out.writeObject(this.ie3s); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.a0 = (String)in.readObject(); + this.type = in.readInt(); + this.ie3s = in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie2.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie2.java new file mode 100644 index 000000000..b360b92ed --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie2.java @@ -0,0 +1,93 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.List; + +import org.apache.cayenne.BaseDataObject; +import org.apache.cayenne.exp.property.ListProperty; +import org.apache.cayenne.exp.property.PropertyFactory; + +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; + +/** + * Class _Ie2 was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Ie2 extends BaseDataObject { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final ListProperty IE1S = PropertyFactory.createList("ie1s", Ie1Sub1.class); + + + protected Object ie1s; + + public void addToIe1s(Ie1Sub1 obj) { + addToManyTarget("ie1s", obj, true); + } + + public void removeFromIe1s(Ie1Sub1 obj) { + removeToManyTarget("ie1s", obj, true); + } + + @SuppressWarnings("unchecked") + public List getIe1s() { + return (List)readProperty("ie1s"); + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "ie1s": + return this.ie1s; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "ie1s": + this.ie1s = val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.ie1s); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.ie1s = in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie3.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie3.java new file mode 100644 index 000000000..a42426f6e --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/inheritance/auto/_Ie3.java @@ -0,0 +1,87 @@ +package io.agrest.cayenne.cayenne.inheritance.auto; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.cayenne.BaseDataObject; +import org.apache.cayenne.exp.property.EntityProperty; +import org.apache.cayenne.exp.property.PropertyFactory; + +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; + +/** + * Class _Ie3 was generated by Cayenne. + * It is probably a good idea to avoid changing this class manually, + * since it may be overwritten next time code is regenerated. + * If you need to make any customizations, please use subclass. + */ +public abstract class _Ie3 extends BaseDataObject { + + private static final long serialVersionUID = 1L; + + public static final String ID_PK_COLUMN = "id"; + + public static final EntityProperty IE1 = PropertyFactory.createEntity("ie1", Ie1Super.class); + + + protected Object ie1; + + public void setIe1(Ie1Super ie1) { + setToOneTarget("ie1", ie1, true); + } + + public Ie1Super getIe1() { + return (Ie1Super)readProperty("ie1"); + } + + @Override + public Object readPropertyDirectly(String propName) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch(propName) { + case "ie1": + return this.ie1; + default: + return super.readPropertyDirectly(propName); + } + } + + @Override + public void writePropertyDirectly(String propName, Object val) { + if(propName == null) { + throw new IllegalArgumentException(); + } + + switch (propName) { + case "ie1": + this.ie1 = val; + break; + default: + super.writePropertyDirectly(propName, val); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + writeSerialized(out); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + readSerialized(in); + } + + @Override + protected void writeState(ObjectOutputStream out) throws IOException { + super.writeState(out); + out.writeObject(this.ie1); + } + + @Override + protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException { + super.readState(in); + this.ie1 = in.readObject(); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/main/E14.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/main/E14.java index 0a08b4b12..0c1be0f19 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/main/E14.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/cayenne/main/E14.java @@ -3,7 +3,7 @@ import io.agrest.annotation.AgAttribute; import io.agrest.annotation.AgRelationship; import io.agrest.cayenne.cayenne.main.auto._E14; -import io.agrest.jaxrs2.pojo.model.P7; +import io.agrest.jaxrs3.junit.pojo.P7; public class E14 extends _E14 { diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/compiler/CayenneCompiler_InheritanceIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/compiler/CayenneCompiler_InheritanceIT.java new file mode 100644 index 000000000..dfebf02f1 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/compiler/CayenneCompiler_InheritanceIT.java @@ -0,0 +1,84 @@ +package io.agrest.cayenne.compiler; + +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Ie1Sub2; +import io.agrest.cayenne.cayenne.inheritance.Ie1Super; +import io.agrest.cayenne.cayenne.inheritance.Ie2; +import io.agrest.cayenne.unit.inheritance.InheritanceNoDbTest; +import io.agrest.meta.AgAttribute; +import io.agrest.meta.AgEntity; +import io.agrest.meta.AgRelationship; +import org.junit.jupiter.api.Test; + +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CayenneCompiler_InheritanceIT extends InheritanceNoDbTest { + + @Test + public void getAttributes_Sub() { + AgEntity ieSub2 = getAgEntity(Ie1Sub2.class); + String as = ieSub2.getAttributes().stream() + .map(AgAttribute::getName) + .sorted() + .collect(Collectors.joining(",")); + assertEquals("a0,a2,type", as, "Must include own and inherited attributes"); + } + + @Test + public void getAttributes_SubSub() { + AgEntity ieSub1Sub1 = getAgEntity(Ie1Sub1Sub1.class); + String as = ieSub1Sub1.getAttributes().stream() + .map(AgAttribute::getName) + .sorted() + .collect(Collectors.joining(",")); + assertEquals("a0,a1,a3,type", as, "Must include own and inherited attributes"); + } + + @Test + public void getAttributes_Super() { + String as = getAgEntity(Ie1Super.class).getAttributes().stream() + .map(AgAttribute::getName) + .sorted() + .collect(Collectors.joining(",")); + assertEquals("a0,type", as, "Must include own, inherited and subclass attributes"); + } + + @Test + public void getRelationships_Sub() { + AgEntity ieSub1 = getAgEntity(Ie1Sub1.class); + String rs = ieSub1.getRelationships().stream() + .map(AgRelationship::getName) + .sorted() + .collect(Collectors.joining(",")); + assertEquals("ie2,ie3s", rs, "Must include own and inherited relationships"); + } + + @Test + public void getSubEntities_Super() { + String es1 = getAgEntity(Ie1Super.class).getSubEntities().stream() + .map(AgEntity::getName) + .sorted() + .collect(Collectors.joining(",")); + assertEquals("Ie1Sub1,Ie1Sub2", es1, "Must include direct sub-entities"); + + String es2 = getAgEntity(Ie1Sub1.class).getSubEntities().stream() + .map(AgEntity::getName) + .sorted() + .collect(Collectors.joining(",")); + assertEquals("Ie1Sub1Sub1", es2, "Must include direct sub-entities"); + } + + @Test + public void getSubEntities_Sub() { + assertTrue(getAgEntity(Ie1Sub2.class).getSubEntities().isEmpty()); + } + + @Test + public void getSubEntities_NoInheritance() { + assertTrue(getAgEntity(Ie2.class).getSubEntities().isEmpty()); + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/compiler/CayenneCompiler_Inheritance_AnnotatedIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/compiler/CayenneCompiler_Inheritance_AnnotatedIT.java new file mode 100644 index 000000000..d79577488 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/compiler/CayenneCompiler_Inheritance_AnnotatedIT.java @@ -0,0 +1,100 @@ +package io.agrest.cayenne.compiler; + +import io.agrest.cayenne.cayenne.inheritance.Aie1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Aie1Sub1Sub1; +import io.agrest.cayenne.cayenne.inheritance.Aie1Sub2; +import io.agrest.cayenne.cayenne.inheritance.Aie1Super; +import io.agrest.cayenne.unit.inheritance.InheritanceNoDbTest; +import io.agrest.meta.AgAttribute; +import io.agrest.meta.AgEntity; +import io.agrest.meta.AgRelationship; +import org.junit.jupiter.api.Test; + +import java.util.Comparator; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CayenneCompiler_Inheritance_AnnotatedIT extends InheritanceNoDbTest { + + @Test + public void aie1Super() { + AgEntity e = getAgEntity(Aie1Super.class); + String attributes = e.getAttributes().stream() + .sorted(Comparator.comparing(AgAttribute::getName)) + .map(AgAttribute::getName) + .collect(Collectors.joining(",")); + + String readables = e.getAttributes().stream() + .sorted(Comparator.comparing(AgAttribute::getName)) + .map(AgAttribute::isReadable) + .map(String::valueOf) + .collect(Collectors.joining(",")); + + assertEquals("a0,type", attributes); + assertEquals("false,true", readables); + } + + @Test + public void aie1Sub2() { + AgEntity sub2 = getAgEntity(Aie1Sub2.class); + String attributes = sub2.getAttributes().stream() + .sorted(Comparator.comparing(AgAttribute::getName)) + .map(AgAttribute::getName) + .collect(Collectors.joining(",")); + + String readables = sub2.getAttributes().stream() + .sorted(Comparator.comparing(AgAttribute::getName)) + .map(AgAttribute::isReadable) + .map(String::valueOf) + .collect(Collectors.joining(",")); + + assertEquals("a0,a2,type", attributes, "Must include own and inherited attributes"); + assertEquals("false,true,true", readables); + } + + @Test + public void aie1Sub1() { + AgEntity sub1 = getAgEntity(Aie1Sub1.class); + + String attributes = sub1.getAttributes().stream() + .sorted(Comparator.comparing(AgAttribute::getName)) + .map(AgAttribute::getName) + .collect(Collectors.joining(",")); + + String relationships = sub1.getRelationships().stream() + .sorted(Comparator.comparing(AgRelationship::getName)) + .map(AgRelationship::getName) + .sorted() + .collect(Collectors.joining(",")); + + String attributesReadable = sub1.getAttributes().stream() + .sorted(Comparator.comparing(AgAttribute::getName)) + .map(AgAttribute::isReadable) + .map(String::valueOf) + .collect(Collectors.joining(",")); + + assertEquals("a0,a1,type", attributes, "Must include own and inherited attributes"); + assertEquals("true,true,true", attributesReadable); + assertEquals("ie2,ie3s", relationships, "Must include own and inherited relationships"); + } + + @Test + public void aie1Sub1Sub1() { + AgEntity sub1Sub1 = getAgEntity(Aie1Sub1Sub1.class); + + String attributes = sub1Sub1.getAttributes().stream() + .sorted(Comparator.comparing(AgAttribute::getName)) + .map(AgAttribute::getName) + .collect(Collectors.joining(",")); + + String readables = sub1Sub1.getAttributes().stream() + .sorted(Comparator.comparing(AgAttribute::getName)) + .map(AgAttribute::isReadable) + .map(String::valueOf) + .collect(Collectors.joining(",")); + + assertEquals("a0,a1,a3,type", attributes, "Must include own and inherited attributes"); + assertEquals("true,true,true,true", readables); + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/encoder/EncoderServiceTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/encoder/EncoderFactoryTest.java similarity index 73% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/encoder/EncoderServiceTest.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/encoder/EncoderFactoryTest.java index 9ba25d3e7..0bd278a2e 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/encoder/EncoderServiceTest.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/encoder/EncoderFactoryTest.java @@ -1,6 +1,5 @@ package io.agrest.cayenne.encoder; -import io.agrest.id.AgObjectId; import io.agrest.DataResponse; import io.agrest.ResourceEntity; import io.agrest.RootResourceEntity; @@ -10,15 +9,15 @@ import io.agrest.cayenne.cayenne.main.E2; import io.agrest.cayenne.cayenne.main.E3; import io.agrest.cayenne.processor.CayenneProcessor; -import io.agrest.cayenne.unit.CayenneNoDbTest; +import io.agrest.cayenne.unit.main.MainNoDbTest; import io.agrest.converter.valuestring.ValueStringConverters; import io.agrest.converter.valuestring.ValueStringConvertersProvider; import io.agrest.encoder.Encoder; import io.agrest.encoder.ValueEncodersProvider; -import io.agrest.meta.DefaultAttribute; +import io.agrest.id.AgObjectId; import io.agrest.processor.ProcessingContext; import io.agrest.runtime.encoder.EncodablePropertyFactory; -import io.agrest.runtime.encoder.EncoderService; +import io.agrest.runtime.encoder.EncoderFactory; import io.agrest.runtime.encoder.IEncodablePropertyFactory; import io.agrest.runtime.jackson.IJacksonService; import io.agrest.runtime.jackson.JacksonService; @@ -37,10 +36,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; -public class EncoderServiceTest extends CayenneNoDbTest { +public class EncoderFactoryTest extends MainNoDbTest { private static final IJacksonService jacksonService = JacksonService.create(); - private EncoderService encoderService; + private EncoderFactory encoderFactory; @BeforeEach public void before() { @@ -49,14 +48,14 @@ public void before() { IEncodablePropertyFactory epf = new EncodablePropertyFactory( new ValueEncodersProvider(converters, Collections.emptyMap()).get()); - encoderService = new EncoderService( + this.encoderFactory = new EncoderFactory( epf, converters, new RelationshipMapper()); } @Test - public void testGetRootEncoder_ExcludedAttributes() { + public void getRootEncoder_ExcludedAttributes() { // empty filter - must only include id ResourceEntity descriptor = getResourceEntity(E1.class); descriptor.includeId(); @@ -71,17 +70,18 @@ public void testGetRootEncoder_ExcludedAttributes() { } @Test - public void testGetRootEncoder_ExcludedRelationshipAttributes() { + public void getRootEncoder_ExcludedRelationshipAttributes() { - RootResourceEntity descriptor = getResourceEntity(E2.class); - descriptor.includeId(); - CayenneProcessor.getOrCreateRootEntity(descriptor); + RootResourceEntity re = getResourceEntity(E2.class); + re.includeId(); + CayenneProcessor.getOrCreateRootEntity(re); - ToManyResourceEntity e3Descriptor = getToManyChildEntity(E3.class, descriptor, E2.E3S.getName()); - e3Descriptor.includeId(); - CayenneProcessor.getOrCreateRelatedEntity(e3Descriptor); - e3Descriptor.addAttribute(new DefaultAttribute("name", String.class, true, true, o -> ((E3)o).getName()), false); - descriptor.getChildren().put(E2.E3S.getName(), e3Descriptor); + ToManyResourceEntity reE3 = getToManyChildEntity(E3.class, re, E2.E3S.getName()); + reE3.includeId(); + CayenneProcessor.getOrCreateRelatedEntity(reE3); + reE3.ensureAttribute("name", false); + re.ensureChild("e3s", (e, r) -> reE3); + re.getBaseProjection().ensureRelationship("e3s"); ObjectContext context = mockCayennePersister.newContext(); E2 e2 = new E2(); @@ -105,20 +105,20 @@ public void testGetRootEncoder_ExcludedRelationshipAttributes() { e2.addToE3s(e32); // saves result set in ResourceEntity - descriptor.setData(List.of(e2)); - e3Descriptor.addData(AgObjectId.of(7), e31); - e3Descriptor.addData(AgObjectId.of(7), e32); + re.setData(List.of(e2)); + reE3.addData(AgObjectId.of(7), e31); + reE3.addData(AgObjectId.of(7), e32); assertEquals("{\"data\":[{\"id\":7,\"e3s\":[{\"id\":5,\"name\":\"31\"},{\"id\":6,\"name\":\"32\"}]}],\"total\":1}", - toJson(e2, descriptor)); + toJson(e2, re)); } @Test - public void testEncoder_BinaryAttribute() { + public void encoder_BinaryAttribute() { ResourceEntity descriptor = getResourceEntity(E19.class); descriptor.includeId(); - descriptor.addAttribute(getAgEntity(E19.class).getAttribute(E19.GUID.getName()), false); + descriptor.ensureAttribute("guid", false); E19 e19 = new E19(); e19.setObjectId(ObjectId.of("E19", E19.ID_PK_COLUMN, 1)); @@ -128,8 +128,8 @@ public void testEncoder_BinaryAttribute() { } private String toJson(Object object, ResourceEntity resourceEntity) { - Encoder encoder = encoderService.dataEncoder(resourceEntity, mock(ProcessingContext.class)); - return toJson(encoder, DataResponse.of(List.of(object)).build()); + Encoder encoder = encoderFactory.encoder(resourceEntity, mock(ProcessingContext.class)); + return toJson(encoder, DataResponse.of(200, List.of(object)).build()); } private String toJson(Encoder encoder, Object value) { @@ -137,7 +137,7 @@ private String toJson(Encoder encoder, Object value) { ByteArrayOutputStream out = new ByteArrayOutputStream(); try { - jacksonService.outputJson(g -> encoder.encode(null, value, g), out); + jacksonService.outputJson(g -> encoder.encode(null, value, false, g), out); } catch (IOException e) { throw new RuntimeException("Encoding error: " + e.getMessage()); } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpParserTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpParserTest.java index d8d720a29..791a405c6 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpParserTest.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpParserTest.java @@ -5,9 +5,9 @@ import org.apache.cayenne.exp.ExpressionFactory; import org.junit.jupiter.api.Test; +import java.time.LocalDate; import java.util.Map; -import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; public class CayenneExpParserTest { @@ -15,56 +15,77 @@ public class CayenneExpParserTest { static final CayenneExpParser parser = new CayenneExpParser(); @Test - public void testParseSimple() { - Expression e = parser.parse(Exp.simple("a = 'b'")); - assertEquals(ExpressionFactory.exp("a = 'b'"), e); + public void parseNamedParams() { + Expression e = parser.parse(Exp.parse("a = $a").namedParams(Map.of("a", "x"))); + assertEquals(ExpressionFactory.exp("a = 'x'"), e); } @Test - public void testParseNamedParams() { - Expression e = parser.parse(Exp.withNamedParams("a = $a", Map.of("a", "x"))); + public void parsePositionalParams() { + Expression e = parser.parse(Exp.parse("a = $a").positionalParams("x")); assertEquals(ExpressionFactory.exp("a = 'x'"), e); } @Test - public void testParsePositionalParams() { - Expression e = parser.parse(Exp.withPositionalParams("a = $a", "x")); - assertEquals(ExpressionFactory.exp("a = 'x'"), e); + public void parsePositionalParams_NullAndParam() { + Exp agExp = Exp.parse("a = null or a.b = $b").positionalParams("B"); + Expression e = parser.parse(agExp); + assertEquals(ExpressionFactory.exp("a = null or a.b = 'B'"), e); } @Test - public void testParseKeyValue_Eq() { - Expression e = parser.parse(Exp.keyValue("a", "=", 5)); + public void parseEqual() { + Expression e = parser.parse(Exp.equal("a", 5)); assertEquals(ExpressionFactory.exp("a = 5"), e); } @Test - public void testParseKeyValue_In() { - Expression e1 = parser.parse(Exp.keyValue("a", "in", asList(5, 6, 7))); + public void parseNotEqual() { + Expression e = parser.parse(Exp.notEqual("a", 5)); + assertEquals(ExpressionFactory.exp("a != 5"), e); + } + + @Test + public void parseEqualDate() { + LocalDate d = LocalDate.of(1999, 8, 7); + Expression e = parser.parse(Exp.equal("a", d)); + assertEquals(ExpressionFactory.exp("a = $a").paramsArray(d), e); + } + + @Test + public void parseEqual_Object() { + Object o = new Object(); + Expression e = parser.parse(Exp.equal("a", o)); + assertEquals(ExpressionFactory.exp("a = $a").paramsArray(o), e); + } + + @Test + public void parseIn() { + Expression e1 = parser.parse(Exp.in("a", 5, 6, 7)); assertEquals(ExpressionFactory.exp("a in (5, 6, 7)"), e1); - Expression e2 = parser.parse(Exp.keyValue("a", "in", new String[]{"x", "y", "z"})); + Expression e2 = parser.parse(Exp.in("a", "x", "y", "z")); assertEquals(ExpressionFactory.exp("a in ('x','y','z')"), e2); - Expression e3 = parser.parse(Exp.keyValue("a", "in", new Integer[]{5, 6, 7})); + Expression e3 = parser.parse(Exp.in("a", 5, 6, 7)); assertEquals(ExpressionFactory.exp("a in (5, 6, 7)"), e3); } @Test - public void testParseKeyValue_DB_Path_Eq() { - Expression e = parser.parse(Exp.keyValue("db:a", "=", 5)); + public void parseEqual_DbPath() { + Expression e = parser.parse(Exp.equal("db:a", 5)); assertEquals(ExpressionFactory.exp("db:a = 5"), e); } @Test - public void testParseComposite() { + public void parseCompositeCondition() { - Exp e0 = Exp.simple("a = 'b'"); - Exp e1 = Exp.withNamedParams("b = $a", Map.of("a", "x")); - Exp e2 = Exp.withPositionalParams("c = $a", "y"); + Exp e0 = Exp.parse("a = 'b'"); + Exp e1 = Exp.parse("b = $a").namedParams(Map.of("a", "x")); + Exp e2 = Exp.parse("c = $a").positionalParams("y"); // multilevel composite with heterogeneous params - Exp e3 = Exp.simple("d = 'z'") + Exp e3 = Exp.parse("d = 'z'") .and(e0) .or(e1) .and(e2); @@ -72,4 +93,17 @@ public void testParseComposite() { Expression e = parser.parse(e3); assertEquals(ExpressionFactory.exp("(((d = 'z') and (a = 'b')) or (b = 'x')) and (c = 'y')"), e); } + + @Test + public void parseComposite_DifferentOrder() { + Exp e1 = Exp.parse("id = $id").positionalParams(1); + Exp e2 = Exp.in("otherId", 1, 2, 3); + Exp composite1 = e1.and(e2); + Exp composite2 = e2.and(e1); + + Expression cayenneComposite1 = parser.parse(composite1); + Expression cayenneComposite2 = parser.parse(composite2); + assertEquals(ExpressionFactory.exp("(id = 1) and (otherId in (1, 2, 3))"), cayenneComposite1); + assertEquals(ExpressionFactory.exp("(otherId in (1, 2, 3)) and (id = 1)"), cayenneComposite2); + } } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpressionVisitorTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpressionVisitorTest.java new file mode 100644 index 000000000..8d675ff85 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/exp/CayenneExpressionVisitorTest.java @@ -0,0 +1,93 @@ +package io.agrest.cayenne.exp; + +import io.agrest.protocol.Exp; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.parser.PatternMatchNode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CayenneExpressionVisitorTest { + + static final CayenneExpressionVisitor visitor = new CayenneExpressionVisitor(); + + @ParameterizedTest + @CsvSource(delimiterString = "_|", value = { + "abs(1)_|org.apache.cayenne.exp.parser.ASTAbs", + "1 + 2_|org.apache.cayenne.exp.parser.ASTAdd", + "t.isA = true and t.isB = true_|org.apache.cayenne.exp.parser.ASTAnd", + "t.value between 10 and 20_|org.apache.cayenne.exp.parser.ASTBetween", + "0xFF & 0x01_|org.apache.cayenne.exp.parser.ASTBitwiseAnd", + "0xFF << 2_|org.apache.cayenne.exp.parser.ASTBitwiseLeftShift", + "~0xA7_|org.apache.cayenne.exp.parser.ASTBitwiseNot", + "0xFF | 0x01_|org.apache.cayenne.exp.parser.ASTBitwiseOr", + "0xFF >> 2_|org.apache.cayenne.exp.parser.ASTBitwiseRightShift", + "0xFF ^ 0x01_|org.apache.cayenne.exp.parser.ASTBitwiseXor", + "concat(t.v, '10')_|org.apache.cayenne.exp.parser.ASTConcat", + "currentDate()_|org.apache.cayenne.exp.parser.ASTCurrentDate", + "currentTime()_|org.apache.cayenne.exp.parser.ASTCurrentTime", + "currentTimestamp()_|org.apache.cayenne.exp.parser.ASTCurrentTimestamp", + "t.value / 2_|org.apache.cayenne.exp.parser.ASTDivide", + "t.v1 = t.v2_|org.apache.cayenne.exp.parser.ASTEqual", + "day(t.dateTime)_|org.apache.cayenne.exp.parser.ASTExtract", + "false_|org.apache.cayenne.exp.parser.ASTFalse", + "t.v > 0_|org.apache.cayenne.exp.parser.ASTGreater", + "t.v >= 0_|org.apache.cayenne.exp.parser.ASTGreaterOrEqual", + "t.v in (0, 5)_|org.apache.cayenne.exp.parser.ASTIn", + "length(a.v)_|org.apache.cayenne.exp.parser.ASTLength", + "t.v < 0_|org.apache.cayenne.exp.parser.ASTLess", + "t.v <= 0_|org.apache.cayenne.exp.parser.ASTLessOrEqual", + "t.name like '%s'_|org.apache.cayenne.exp.parser.ASTLike", + "t.name likeIgnoreCase '%s'_|org.apache.cayenne.exp.parser.ASTLikeIgnoreCase", + "locate(t.v, 'id')_|org.apache.cayenne.exp.parser.ASTLocate", + "lower(t.v)_|org.apache.cayenne.exp.parser.ASTLower", + "mod(t.v, 10)_|org.apache.cayenne.exp.parser.ASTMod", + "1 * 4_|org.apache.cayenne.exp.parser.ASTMultiply", + "$a_|org.apache.cayenne.exp.parser.ASTNamedParameter", + "-a.v_|org.apache.cayenne.exp.parser.ASTNegate", + "!(t.a = 1 and t.b = 3)_|org.apache.cayenne.exp.parser.ASTNot", + "t.value !between 10 and 20_|org.apache.cayenne.exp.parser.ASTNotBetween", + "t.v1 != t.v2_|org.apache.cayenne.exp.parser.ASTNotEqual", + "t.v !in (0, 5)_|org.apache.cayenne.exp.parser.ASTNotIn", + "t.name !like '%s'_|org.apache.cayenne.exp.parser.ASTNotLike", + "t.name !likeIgnoreCase '%s'_|org.apache.cayenne.exp.parser.ASTNotLikeIgnoreCase", + "a.v_|org.apache.cayenne.exp.parser.ASTObjPath", + "t.isA = true or t.isB = true_|org.apache.cayenne.exp.parser.ASTOr", + "1.2_|org.apache.cayenne.exp.parser.ASTScalar", + "null_|org.apache.cayenne.exp.parser.ASTScalar", + "1_|org.apache.cayenne.exp.parser.ASTScalar", + "\"value\"_|org.apache.cayenne.exp.parser.ASTScalar", + "sqrt(2)_|org.apache.cayenne.exp.parser.ASTSqrt", + "substring(a.v, 3)_|org.apache.cayenne.exp.parser.ASTSubstring", + "3 - 1_|org.apache.cayenne.exp.parser.ASTSubtract", + "trim(a.v)_|org.apache.cayenne.exp.parser.ASTTrim", + "true_|org.apache.cayenne.exp.parser.ASTTrue", + "upper(t.v)_|org.apache.cayenne.exp.parser.ASTUpper" + + // TODO: Cayenne doesn't allow objPath as operand for logical operators (see AggregateConditionNode). + // It will be reasonable to change this. + //"!t.m", ASTNot + //"t.isA and t.isB ", ASTAnd + //"t.isA or t.isB ", ASTOr + }) + public void accept_ReturnedType(String agrestExp, Class cayenneExpExpectedType) { + Expression cayenneExp = Exp.parse(agrestExp).accept(visitor, null); + assertEquals(cayenneExpExpectedType, cayenneExp.getClass()); + } + + @ParameterizedTest(name = "case {index}") + @ValueSource(strings = { + "a like 'bcd' escape '$'", + "a likeIgnoreCase 'bcd' escape '$'", + "a not like 'bcd' escape '$'", + "a not likeIgnoreCase 'bcd' escape '$'"}) + public void accept_escapeChar(String agrestExp) { + Expression cayenneExp = Exp.parse(agrestExp).accept(visitor, null); + assertTrue(cayenneExp instanceof PatternMatchNode); + PatternMatchNode matchNode = (PatternMatchNode) cayenneExp; + assertEquals('$', matchNode.getEscapeChar()); + } +} \ No newline at end of file diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/path/EntityPathCacheTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/path/EntityPathCacheTest.java index f8e22b1ac..bd01ec526 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/path/EntityPathCacheTest.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/path/EntityPathCacheTest.java @@ -1,131 +1,124 @@ package io.agrest.cayenne.path; import io.agrest.AgException; -import io.agrest.annotation.AgAttribute; -import io.agrest.annotation.AgId; -import io.agrest.annotation.AgRelationship; -import io.agrest.compiler.AgEntityCompiler; -import io.agrest.compiler.AnnotationsAgEntityCompiler; -import io.agrest.meta.AgSchema; -import io.agrest.meta.LazySchema; +import org.apache.cayenne.map.DataMap; +import org.apache.cayenne.map.DbAttribute; +import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.ObjAttribute; +import org.apache.cayenne.map.ObjEntity; +import org.apache.cayenne.map.ObjRelationship; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.Collections; -import java.util.List; +import java.sql.Types; import static org.junit.jupiter.api.Assertions.*; public class EntityPathCacheTest { - private AgSchema schema; + private ObjEntity x; @BeforeEach public void setUp() { - AgEntityCompiler compiler = new AnnotationsAgEntityCompiler(Collections.emptyMap()); - this.schema = new LazySchema(List.of(compiler)); + + DataMap map = new DataMap(); + + DbEntity dy = new DbEntity("Y"); + dy.addAttribute(new DbAttribute("pk1", Types.INTEGER, dy)); + dy.getAttribute("pk1").setPrimaryKey(true); + map.addDbEntity(dy); + + ObjEntity y = new ObjEntity("Y"); + y.setDbEntityName("Y"); + y.setClassName("test.Y"); + y.addAttribute(new ObjAttribute("name", "java.lang.String", x)); + map.addObjEntity(y); + + DbEntity dx = new DbEntity("X"); + dx.addAttribute(new DbAttribute("pkx1", Types.VARCHAR, dy)); + dx.getAttribute("pkx1").setPrimaryKey(true); + map.addDbEntity(dx); + + ObjEntity x = new ObjEntity("X"); + x.setDbEntityName("X"); + x.addAttribute(new ObjAttribute("name", "java.lang.String", x)); + x.addRelationship(new ObjRelationship("y")); + x.getRelationship("y").setTargetEntityName("Y"); + map.addObjEntity(x); + + this.x = x; } @Test - public void testGetOrCreate_Attribute() { - EntityPathCache cache = new EntityPathCache(schema.getEntity(X.class)); + public void getOrCreate_Attribute() { + EntityPathCache cache = new EntityPathCache(x); PathDescriptor pd = cache.getOrCreate("name"); assertNotNull(pd); assertTrue(pd.isAttributeOrId()); - assertEquals(String.class, pd.getType()); + assertEquals("java.lang.String", pd.getType()); assertEquals("name", pd.getPathExp().getPath()); assertSame(pd, cache.getOrCreate("name")); } @Test - public void testGetOrCreate_Id() { - EntityPathCache cache = new EntityPathCache(schema.getEntity(X.class)); + public void getOrCreate_Id() { + EntityPathCache cache = new EntityPathCache(x); PathDescriptor pd = cache.getOrCreate("id"); assertNotNull(pd); assertTrue(pd.isAttributeOrId()); - assertEquals(Integer.class, pd.getType()); + assertEquals("java.lang.Integer", pd.getType()); assertEquals("id", pd.getPathExp().getPath()); assertSame(pd, cache.getOrCreate("id")); } @Test - public void testGetOrCreate_Relationship() { - EntityPathCache cache = new EntityPathCache(schema.getEntity(X.class)); + public void getOrCreate_Relationship() { + EntityPathCache cache = new EntityPathCache(x); PathDescriptor pd = cache.getOrCreate("y"); assertNotNull(pd); assertFalse(pd.isAttributeOrId()); - assertEquals(Y.class, pd.getType()); + assertEquals("test.Y", pd.getType()); assertEquals("y", pd.getPathExp().getPath()); assertSame(pd, cache.getOrCreate("y")); } @Test - public void testGetOrCreate_RelatedAttribute() { - EntityPathCache cache = new EntityPathCache(schema.getEntity(X.class)); + public void getOrCreate_RelatedAttribute() { + EntityPathCache cache = new EntityPathCache(x); PathDescriptor pd = cache.getOrCreate("y.name"); assertNotNull(pd); assertTrue(pd.isAttributeOrId()); - assertEquals(String.class, pd.getType()); + assertEquals("java.lang.String", pd.getType()); assertEquals("y.name", pd.getPathExp().getPath()); assertSame(pd, cache.getOrCreate("y.name")); } @Test - public void testGetOrCreate_RelatedId() { - EntityPathCache cache = new EntityPathCache(schema.getEntity(X.class)); + public void getOrCreate_RelatedId() { + EntityPathCache cache = new EntityPathCache(x); PathDescriptor pd = cache.getOrCreate("y.id"); assertNotNull(pd); assertTrue(pd.isAttributeOrId()); - assertEquals(Integer.class, pd.getType()); + assertEquals("java.lang.Integer", pd.getType()); assertEquals("y.id", pd.getPathExp().getPath()); assertSame(pd, cache.getOrCreate("y.id")); } @Test - public void tesGetOrCreate_BadPath() { - EntityPathCache cache = new EntityPathCache(schema.getEntity(X.class)); + public void getOrCreate_BadPath() { + EntityPathCache cache = new EntityPathCache(x); assertThrows(AgException.class, () -> cache.getOrCreate("y.xyz")); } @Test - public void testGetOrCreate_OuterRelatedAttribute() { - EntityPathCache cache = new EntityPathCache(schema.getEntity(X.class)); + public void getOrCreate_OuterRelatedAttribute() { + EntityPathCache cache = new EntityPathCache(x); PathDescriptor pd = cache.getOrCreate("y+.name"); assertNotNull(pd); assertTrue(pd.isAttributeOrId()); - assertEquals(String.class, pd.getType()); + assertEquals("java.lang.String", pd.getType()); assertEquals("y+.name", pd.getPathExp().getPath()); assertSame(pd, cache.getOrCreate("y+.name")); assertNotSame(pd, cache.getOrCreate("y.name")); } - - public static class X { - - @AgId - public Integer getId() { - throw new UnsupportedOperationException(); - } - - @AgAttribute - public String getName() { - throw new UnsupportedOperationException(); - } - - @AgRelationship - public Y getY() { - throw new UnsupportedOperationException(); - } - } - - public static class Y { - @AgAttribute - public String getName() { - throw new UnsupportedOperationException(); - } - - @AgId - public Integer getId() { - throw new UnsupportedOperationException(); - } - } } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/path/PathOpsTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/path/PathOpsTest.java index 5b03e0995..e83d6f21d 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/path/PathOpsTest.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/path/PathOpsTest.java @@ -1,7 +1,7 @@ package io.agrest.cayenne.path; import io.agrest.cayenne.cayenne.main.E2; -import io.agrest.cayenne.unit.CayenneNoDbTest; +import io.agrest.cayenne.unit.main.MainNoDbTest; import org.apache.cayenne.exp.parser.ASTDbPath; import org.apache.cayenne.exp.parser.ASTObjPath; import org.apache.cayenne.exp.parser.ASTPath; @@ -10,10 +10,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -public class PathOpsTest extends CayenneNoDbTest { +public class PathOpsTest extends MainNoDbTest { @Test - public void testConcat_ObjObj() { + public void concat_ObjObj() { ObjEntity e2 = getEntity(E2.class); ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s"), new ASTObjPath("name")); @@ -21,42 +21,42 @@ public void testConcat_ObjObj() { } @Test - public void testConcat_ObjDb() { + public void concat_ObjDb() { ObjEntity e2 = getEntity(E2.class); ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s"), new ASTDbPath("name")); assertEquals(new ASTDbPath("e3s.name"), p); } @Test - public void testConcat_DbDb() { + public void concat_DbDb() { ObjEntity e2 = getEntity(E2.class); ASTPath p = PathOps.concat(e2, new ASTDbPath("e3s"), new ASTDbPath("name")); assertEquals(new ASTDbPath("e3s.name"), p); } @Test - public void testConcat_DbObj() { + public void concat_DbObj() { ObjEntity e2 = getEntity(E2.class); ASTPath p = PathOps.concat(e2, new ASTDbPath("e3s"), new ASTObjPath("name")); assertEquals(new ASTDbPath("e3s.name"), p); } @Test - public void testConcat_ObjDb_MultiStep() { + public void concat_ObjDb_MultiStep() { ObjEntity e2 = getEntity(E2.class); ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s.e5"), new ASTDbPath("date")); assertEquals(new ASTDbPath("e3s.e5.date"), p); } @Test - public void testConcat_ObjDb_Id() { + public void concat_ObjDb_Id() { ObjEntity e2 = getEntity(E2.class); ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s"), new ASTDbPath("_id")); assertEquals(new ASTDbPath("e3s._id"), p); } @Test - public void testConcat_ObjDb_EndsInRel() { + public void concat_ObjDb_EndsInRel() { ObjEntity e2 = getEntity(E2.class); ASTPath p = PathOps.concat(e2, new ASTObjPath("e3s"), new ASTDbPath("e5")); assertEquals(new ASTDbPath("e3s.e5"), p); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/processor/CayenneQueryAssemblerTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/processor/CayenneQueryAssemblerTest.java index fc45409c9..d74912784 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/processor/CayenneQueryAssemblerTest.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/processor/CayenneQueryAssemblerTest.java @@ -1,13 +1,16 @@ package io.agrest.cayenne.processor; +import io.agrest.access.PathChecker; import io.agrest.id.AgObjectId; import io.agrest.AgRequestBuilder; import io.agrest.RootResourceEntity; import io.agrest.cayenne.cayenne.main.E1; -import io.agrest.cayenne.unit.CayenneNoDbTest; +import io.agrest.cayenne.unit.main.MainNoDbTest; +import io.agrest.meta.AgSchema; import io.agrest.protocol.Direction; import io.agrest.protocol.Exp; import io.agrest.protocol.Sort; +import io.agrest.runtime.meta.RequestSchema; import io.agrest.runtime.processor.select.SelectContext; import org.apache.cayenne.di.Injector; import org.apache.cayenne.query.ObjectSelect; @@ -17,15 +20,17 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; -public class CayenneQueryAssemblerTest extends CayenneNoDbTest { +public class CayenneQueryAssemblerTest extends MainNoDbTest { @Test - public void testCreateRootQuery_Ordering() { + public void createRootQuery_Ordering() { RootResourceEntity entity = getResourceEntity(E1.class); entity.getOrderings().add(new Sort("name", Direction.asc)); SelectContext c = new SelectContext<>(E1.class, + new RequestSchema(mock(AgSchema.class)), mock(AgRequestBuilder.class), + PathChecker.ofDefault(), mock(Injector.class)); c.setEntity(entity); @@ -38,7 +43,7 @@ public void testCreateRootQuery_Ordering() { } @Test - public void testCreateRootQuery_Pagination() { + public void createRootQuery_Pagination() { RootResourceEntity entity = new RootResourceEntity<>(getAgEntity(E1.class)); entity.setLimit(10); @@ -46,7 +51,9 @@ public void testCreateRootQuery_Pagination() { SelectContext c = new SelectContext<>( E1.class, + new RequestSchema(mock(AgSchema.class)), mock(AgRequestBuilder.class), + PathChecker.ofDefault(), mock(Injector.class)); c.setEntity(entity); @@ -75,31 +82,35 @@ public void testCreateRootQuery_Pagination() { } @Test - public void testCreateRootQuery_Qualifier() { + public void createRootQuery_Qualifier() { RootResourceEntity entity = getResourceEntity(E1.class); SelectContext c = new SelectContext<>( E1.class, + new RequestSchema(mock(AgSchema.class)), mock(AgRequestBuilder.class), + PathChecker.ofDefault(), mock(Injector.class)); c.setEntity(entity); - entity.andExp(Exp.simple("name = 'X'")); + entity.andExp(Exp.parse("name = 'X'")); ObjectSelect q1 = queryAssembler.createRootQuery(c); assertEquals(E1.NAME.eq("X"), q1.getWhere()); - entity.andExp(Exp.simple("name in ('a', 'b')")); + entity.andExp(Exp.parse("name in ('a', 'b')")); ObjectSelect q2 = queryAssembler.createRootQuery(c); assertEquals(E1.NAME.eq("X").andExp(E1.NAME.in("a", "b")), q2.getWhere()); } @Test - public void testCreateRootQuery_ById() { + public void createRootQuery_ById() { SelectContext c = new SelectContext<>( E1.class, + new RequestSchema(mock(AgSchema.class)), mock(AgRequestBuilder.class), + PathChecker.ofDefault(), mock(Injector.class)); c.setId(AgObjectId.of(1)); c.setEntity(getResourceEntity(E1.class)); @@ -110,12 +121,14 @@ public void testCreateRootQuery_ById() { } @Test - public void testCreateRootQuery_ById_WithQuery() { + public void createRootQuery_ById_WithQuery() { ObjectSelect select = ObjectSelect.query(E1.class); SelectContext c = new SelectContext<>( E1.class, + new RequestSchema(mock(AgSchema.class)), mock(AgRequestBuilder.class), + PathChecker.ofDefault(), mock(Injector.class)); c.setId(AgObjectId.of(1)); c.setEntity(getResourceEntity(E1.class)); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/processor/CayenneQueryAssembler_StaticsTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/processor/CayenneQueryAssembler_StaticsTest.java index 505d40b35..fbc0ad026 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/processor/CayenneQueryAssembler_StaticsTest.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/processor/CayenneQueryAssembler_StaticsTest.java @@ -11,38 +11,75 @@ public class CayenneQueryAssembler_StaticsTest { @Test - public void testConsumeRange_NoOffset_NoLimit_Negative() { + public void consumeRangeIterator_NoOffset_NoLimit_Negative() { List list = asList("a", "b", "c", "d"); List consumed = new ArrayList<>(); - CayenneQueryAssembler.consumeRange(list.iterator(), -1, -1, consumed::add); + CayenneQueryAssembler.consumeRangeIterator(list.iterator(), -1, -1, consumed::add); assertEquals(asList("a", "b", "c", "d"), consumed); } @Test - public void testConsumeRange_NoOffset_NoLimit() { + public void consumeRangeIterator_NoOffset_NoLimit() { List list = asList("a", "b", "c", "d"); List consumed = new ArrayList<>(); - CayenneQueryAssembler.consumeRange(list.iterator(), 0, 0, consumed::add); + CayenneQueryAssembler.consumeRangeIterator(list.iterator(), 0, 0, consumed::add); assertEquals(asList("a", "b", "c", "d"), consumed); } @Test - public void testConsumeRange_Offset_Limit() { + public void consumeRangeIterator_Offset_Limit() { List list = asList("a", "b", "c", "d"); List consumed = new ArrayList<>(); - CayenneQueryAssembler.consumeRange(list.iterator(), 1, 2, consumed::add); + CayenneQueryAssembler.consumeRangeIterator(list.iterator(), 1, 2, consumed::add); assertEquals(asList("b", "c"), consumed); } @Test - public void testConsumeRange_Offset_NoLimit() { + public void consumeRangeIterator_Offset_NoLimit() { List list = asList("a", "b", "c", "d"); List consumed = new ArrayList<>(); - CayenneQueryAssembler.consumeRange(list.iterator(), 1, 0, consumed::add); + CayenneQueryAssembler.consumeRangeIterator(list.iterator(), 1, 0, consumed::add); + assertEquals(asList("b", "c", "d"), consumed); + } + + + @Test + public void consumeRangeList_NoOffset_NoLimit_Negative() { + + List list = asList("a", "b", "c", "d"); + List consumed = new ArrayList<>(); + CayenneQueryAssembler.consumeRangeList(list, -1, -1, consumed::add); + assertEquals(asList("a", "b", "c", "d"), consumed); + } + + @Test + public void consumeRangeList_NoOffset_NoLimit() { + + List list = asList("a", "b", "c", "d"); + List consumed = new ArrayList<>(); + CayenneQueryAssembler.consumeRangeList(list, 0, 0, consumed::add); + assertEquals(asList("a", "b", "c", "d"), consumed); + } + + @Test + public void consumeRangeList_Offset_Limit() { + + List list = asList("a", "b", "c", "d"); + List consumed = new ArrayList<>(); + CayenneQueryAssembler.consumeRangeList(list, 1, 2, consumed::add); + assertEquals(asList("b", "c"), consumed); + } + + @Test + public void consumeRangeList_Offset_NoLimit() { + + List list = asList("a", "b", "c", "d"); + List consumed = new ArrayList<>(); + CayenneQueryAssembler.consumeRangeList(list, 1, 0, consumed::add); assertEquals(asList("b", "c", "d"), consumed); } diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/spi/CayenneRuntimeExceptionMapperIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/spi/CayenneRuntimeExceptionMapperIT.java index 9002bce2b..18d8a5180 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/spi/CayenneRuntimeExceptionMapperIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/spi/CayenneRuntimeExceptionMapperIT.java @@ -4,28 +4,28 @@ import io.agrest.DataResponse; import io.agrest.SelectStage; import io.agrest.cayenne.cayenne.main.E2; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.apache.cayenne.CayenneRuntimeException; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; -public class CayenneRuntimeExceptionMapperIT extends DbTest { +public class CayenneRuntimeExceptionMapperIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class).build(); + static final MainModelTester tester = tester(Resource.class).build(); @Test - public void testException() { + public void exception() { String cayenneVersion = CayenneRuntimeException.getExceptionLabel(); String expected = String.format( "{\"message\":\"CayenneRuntimeException %s_something_w_cayenne_\"}", diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/spi/ValidationExceptionMapperIT.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/spi/ValidationExceptionMapperIT.java index b571dc296..8bc6cb29b 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/spi/ValidationExceptionMapperIT.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/spi/ValidationExceptionMapperIT.java @@ -3,32 +3,32 @@ import io.agrest.DataResponse; import io.agrest.SelectStage; -import io.agrest.cayenne.unit.AgCayenneTester; -import io.agrest.cayenne.unit.DbTest; import io.agrest.cayenne.cayenne.main.E2; -import io.agrest.jaxrs2.AgJaxrs; +import io.agrest.cayenne.unit.main.MainDbTest; +import io.agrest.cayenne.unit.main.MainModelTester; +import io.agrest.jaxrs3.AgJaxrs; import io.bootique.junit5.BQTestTool; import org.apache.cayenne.validation.SimpleValidationFailure; import org.apache.cayenne.validation.ValidationException; import org.apache.cayenne.validation.ValidationResult; import org.junit.jupiter.api.Test; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Configuration; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.UriInfo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; -public class ValidationExceptionMapperIT extends DbTest { +public class ValidationExceptionMapperIT extends MainDbTest { @BQTestTool - static final AgCayenneTester tester = tester(Resource.class).build(); + static final MainModelTester tester = tester(Resource.class).build(); @Test - public void testException() { + public void exception() { tester.target("/g1/1").get() .wasBadRequest() .bodyEquals("{\"message\":\"Object validation failed\"}"); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/AgCayenneTester.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/AgCayenneTester.java index f03abae66..be3f742c3 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/AgCayenneTester.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/AgCayenneTester.java @@ -3,8 +3,8 @@ import io.agrest.cayenne.AgCayenneModule; import io.agrest.cayenne.persister.CayennePersister; import io.agrest.cayenne.persister.ICayennePersister; -import io.agrest.jaxrs2.junit.AgHttpTester; -import io.agrest.jaxrs2.junit.AgTestJaxrsFeature; +import io.agrest.jaxrs3.junit.AgHttpTester; +import io.agrest.jaxrs3.junit.AgTestJaxrsFeature; import io.agrest.runtime.AgRuntime; import io.agrest.runtime.AgRuntimeBuilder; import io.bootique.BQRuntime; @@ -16,7 +16,6 @@ import io.bootique.di.Binder; import io.bootique.di.Provides; import io.bootique.jdbc.junit5.DbTester; -import io.bootique.jdbc.junit5.Table; import io.bootique.jersey.JerseyModule; import io.bootique.jersey.JerseyModuleExtender; import io.bootique.jetty.junit5.JettyTester; @@ -25,19 +24,19 @@ import io.bootique.junit5.scope.BQAfterScopeCallback; import io.bootique.junit5.scope.BQBeforeMethodCallback; import io.bootique.junit5.scope.BQBeforeScopeCallback; +import jakarta.ws.rs.client.WebTarget; import org.apache.cayenne.Persistent; import org.apache.cayenne.configuration.server.ServerRuntime; import org.junit.jupiter.api.extension.ExtensionContext; import javax.inject.Singleton; -import javax.ws.rs.client.WebTarget; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.UnaryOperator; -import java.util.stream.Stream; +import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -45,24 +44,20 @@ * Agrest endpoints. Under the hood combines multiple Bootique JUnit 5 tools. Users must annotate AgCayenneTester field * with {@link io.bootique.junit5.BQTestTool} to tie it to the JUnit 5 lifecycle. */ -public class AgCayenneTester implements BQBeforeScopeCallback, BQAfterScopeCallback, BQBeforeMethodCallback, BQAfterMethodCallback { +public abstract class AgCayenneTester implements BQBeforeScopeCallback, BQAfterScopeCallback, BQBeforeMethodCallback, BQAfterMethodCallback { - private DbTester db; - private String cayenneProject; - private final List> resources; - private Class[] entities; - private Class[] entitiesAndDependencies; - private BiFunction agCustomizer; - private boolean doNotCleanData; + protected DbTester db; + protected String cayenneProject; + protected final List> resources; + protected Class[] entities; + protected Class[] entitiesAndDependencies; + protected BiFunction agCustomizer; + protected boolean doNotCleanData; private JettyTester jettyInScope; private CayenneTester cayenneInScope; private BQRuntime appInScope; - public static AgCayenneTester.Builder forDb(DbTester db) { - return new Builder().db(db); - } - protected AgCayenneTester() { this.resources = new ArrayList<>(); this.agCustomizer = (b, p) -> b; @@ -95,138 +90,6 @@ public AgRuntime runtime() { return getAppInScope().getInstance(AgRuntime.class); } - public Table e1() { - return db.getTable("e1"); - } - - public Table e2() { - return db.getTable("e2"); - } - - public Table e3() { - return db.getTable("e3"); - } - - public Table e4() { - return db.getTable("e4"); - } - - public Table e5() { - return db.getTable("e5"); - } - - public Table e6() { - return db.getTable("e6"); - } - - public Table e7() { - return db.getTable("e7"); - } - - public Table e8() { - return db.getTable("e8"); - } - - public Table e9() { - return db.getTable("e9"); - } - - public Table e10() { - return db.getTable("e10"); - } - - public Table e11() { - return db.getTable("e11"); - } - - public Table e12() { - return db.getTable("e12"); - } - - public Table e13() { - return db.getTable("e13"); - } - - public Table e12_13() { - return db.getTable("e12_e13"); - } - - public Table e14() { - return db.getTable("e14"); - } - - public Table e15() { - return db.getTable("e15"); - } - - public Table e15_1() { - return db.getTable("e15_e1"); - } - - public Table e15_5() { - return db.getTable("e15_e5"); - } - - public Table e17() { - return db.getTable("e17"); - } - - public Table e18() { - return db.getTable("e18"); - } - - public Table e19() { - return db.getTable("e19"); - } - - public Table e20() { - return db.getTable("e20"); - } - - public Table e21() { - return db.getTable("e21"); - } - - public Table e22() { - return db.getTable("e22"); - } - - public Table e23() { - return db.getTable("e23"); - } - - public Table e24() { - return db.getTable("e24"); - } - - public Table e25() { - return db.getTable("e25"); - } - - public Table e26() { - return db.getTable("e26"); - } - - public Table e27NoPk() { - return db.getTable("e27_nopk"); - } - - public Table e28() { - return db.getTable("e28"); - } - - public Table e29() { - return db.getTable("e29"); - } - - public Table e30() { - return db.getTable("e30"); - } - - public Table e31() { - return db.getTable("e31"); - } - protected CayenneTester getCayenneInScope() { return Objects.requireNonNull(cayenneInScope, "Not in test scope"); } @@ -240,7 +103,7 @@ protected BQRuntime getAppInScope() { } @Override - public void beforeScope(BQTestScope scope, ExtensionContext context) throws Exception { + public void beforeScope(BQTestScope scope, ExtensionContext context) { this.jettyInScope = JettyTester.create(); this.cayenneInScope = createCayenneInScope(); @@ -296,52 +159,51 @@ protected BQRuntime createAppInScope(JettyTester jetty, CayenneTester cayenne) { return builder.createRuntime(); } - public static class Builder { + public static abstract class Builder { - private final AgCayenneTester tester = new AgCayenneTester(); + protected final T tester; - public Builder db(DbTester db) { - tester.db = db; - return this; + protected Builder(T tester) { + this.tester = tester; } - public Builder doNotCleanData() { - tester.doNotCleanData = true; + public Builder db(DbTester db) { + tester.db = db; return this; } @SafeVarargs - public final Builder entities(Class... entities) { + public final Builder entities(Class... entities) { tester.entities = Objects.requireNonNull(entities); return this; } @SafeVarargs - public final Builder entitiesAndDependencies(Class... entitiesWithDependencies) { + public final Builder entitiesAndDependencies(Class... entitiesWithDependencies) { tester.entitiesAndDependencies = Objects.requireNonNull(entitiesWithDependencies); return this; } - public Builder resources(Class... resources) { - Stream.of(resources).forEach(tester.resources::add); + public Builder resources(Class... resources) { + tester.resources.addAll(asList(resources)); return this; } - public Builder agCustomizer(UnaryOperator agCustomizer) { + public Builder agCustomizer(UnaryOperator agCustomizer) { return agCustomizer((b, p) -> agCustomizer.apply(b)); } - public Builder agCustomizer(BiFunction agCustomizer) { + public Builder agCustomizer(BiFunction agCustomizer) { tester.agCustomizer = Objects.requireNonNull(agCustomizer); return this; } - public Builder cayenneProject(String cayenneProject) { + public Builder cayenneProject(String cayenneProject) { tester.cayenneProject = Objects.requireNonNull(cayenneProject); return this; } - public AgCayenneTester build() { + public T build() { Objects.requireNonNull(tester.db); Objects.requireNonNull(tester.cayenneProject); diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/DbTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/DbTest.java deleted file mode 100644 index 34a88e320..000000000 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/DbTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.agrest.cayenne.unit; - -import io.bootique.jdbc.junit5.derby.DerbyTester; -import io.bootique.junit5.BQTest; -import io.bootique.junit5.BQTestScope; -import io.bootique.junit5.BQTestTool; - -/** - * An abstract superclass of integration tests that starts Bootique test runtime with JAX-RS service and Derby DB. - */ -@BQTest -public abstract class DbTest { - - @BQTestTool(BQTestScope.GLOBAL) - static final DerbyTester db = DerbyTester.db().initDB("classpath:schema-derby.sql"); - - protected static AgCayenneTester.Builder tester(Class... resources) { - return AgCayenneTester - .forDb(db) - .cayenneProject("cayenne-project.xml") - .resources(resources); - } -} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/CayenneNoDbTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/NoDbTest.java similarity index 81% rename from agrest-cayenne/src/test/java/io/agrest/cayenne/unit/CayenneNoDbTest.java rename to agrest-cayenne/src/test/java/io/agrest/cayenne/unit/NoDbTest.java index b87b894c7..aa6859cdf 100644 --- a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/CayenneNoDbTest.java +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/NoDbTest.java @@ -13,16 +13,14 @@ import io.agrest.cayenne.processor.CayenneQueryAssembler; import io.agrest.compiler.AgEntityCompiler; import io.agrest.compiler.AnnotationsAgEntityCompiler; -import io.agrest.meta.AgSchema; import io.agrest.meta.AgEntity; +import io.agrest.meta.AgSchema; import io.agrest.meta.LazySchema; import org.apache.cayenne.ObjectContext; import org.apache.cayenne.configuration.server.DataSourceFactory; import org.apache.cayenne.configuration.server.ServerRuntime; import org.apache.cayenne.di.Module; import org.apache.cayenne.map.ObjEntity; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import java.util.Collections; @@ -36,52 +34,45 @@ * A superclass of Cayenne-aware test cases that do not need to access the DB, but need to work with EntityResolver * and higher levels of the stack. */ -public abstract class CayenneNoDbTest { - - protected static ServerRuntime runtime; +public abstract class NoDbTest { protected ICayennePersister mockCayennePersister; protected IPathResolver pathDescriptorManager; protected AgSchema schema; protected CayenneQueryAssembler queryAssembler; - @BeforeAll - public static void setUpClass() { + protected static ServerRuntime createRuntime(String project) { Module module = binder -> { DataSourceFactory dsf = mock(DataSourceFactory.class); binder.bind(DataSourceFactory.class).toInstance(dsf); }; - runtime = ServerRuntime + return ServerRuntime .builder() - .addConfig("cayenne-project.xml") + .addConfig(project) .addModule(module) .build(); } - @AfterAll - public static void tearDownClass() { - runtime.shutdown(); - runtime = null; - } + protected abstract ServerRuntime getRuntime(); @BeforeEach public void initAgSchema() { - ObjectContext sharedContext = runtime.newContext(); + ObjectContext sharedContext = getRuntime().newContext(); this.mockCayennePersister = mock(ICayennePersister.class); - when(mockCayennePersister.entityResolver()).thenReturn(runtime.getChannel().getEntityResolver()); + when(mockCayennePersister.entityResolver()).thenReturn(getRuntime().getChannel().getEntityResolver()); when(mockCayennePersister.sharedContext()).thenReturn(sharedContext); - when(mockCayennePersister.newContext()).thenReturn(runtime.newContext()); + when(mockCayennePersister.newContext()).thenReturn(getRuntime().newContext()); this.schema = new LazySchema(createEntityCompilers()); - this.pathDescriptorManager = new PathResolver(); + this.pathDescriptorManager = new PathResolver(mockCayennePersister); this.queryAssembler = new CayenneQueryAssembler( mockCayennePersister, pathDescriptorManager, new CayenneExpParser(), - new CayenneExpPostProcessor(pathDescriptorManager)); + new CayenneExpPostProcessor(pathDescriptorManager, mockCayennePersister)); } protected List createEntityCompilers() { @@ -101,7 +92,7 @@ protected AgEntity getAgEntity(Class type) { } protected ObjEntity getEntity(Class type) { - return runtime.getChannel().getEntityResolver().getObjEntity(type); + return getRuntime().getChannel().getEntityResolver().getObjEntity(type); } protected RootResourceEntity getResourceEntity(Class type) { diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/inheritance/InheritanceDbTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/inheritance/InheritanceDbTest.java new file mode 100644 index 000000000..91dd9ab5a --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/inheritance/InheritanceDbTest.java @@ -0,0 +1,30 @@ +package io.agrest.cayenne.unit.inheritance; + +import io.agrest.cayenne.unit.AgCayenneTester; +import io.bootique.jdbc.junit5.derby.DerbyTester; +import io.bootique.junit5.BQTest; +import io.bootique.junit5.BQTestScope; +import io.bootique.junit5.BQTestTool; + +/** + * An abstract superclass of integration tests that starts Bootique test runtime with JAX-RS service and Derby DB. + */ +@BQTest +public abstract class InheritanceDbTest { + + @BQTestTool(BQTestScope.GLOBAL) + static final DerbyTester db = DerbyTester.db().initDB("classpath:inheritance/schema-derby.sql"); + + protected static AgCayenneTester.Builder tester(Class... resources) { + return new Builder() + .db(db) + .cayenneProject("inheritance/cayenne-project.xml") + .resources(resources); + } + + public static class Builder extends AgCayenneTester.Builder { + public Builder() { + super(new InheritanceModelTester()); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/inheritance/InheritanceModelTester.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/inheritance/InheritanceModelTester.java new file mode 100644 index 000000000..193796d80 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/inheritance/InheritanceModelTester.java @@ -0,0 +1,21 @@ +package io.agrest.cayenne.unit.inheritance; + + +import io.agrest.cayenne.unit.AgCayenneTester; +import io.bootique.jdbc.junit5.Table; + +public class InheritanceModelTester extends AgCayenneTester { + + public Table ie1() { + return db.getTable("ie1"); + } + + public Table ie2() { + return db.getTable("ie2"); + } + + public Table ie3() { + return db.getTable("ie3"); + } + +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/inheritance/InheritanceNoDbTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/inheritance/InheritanceNoDbTest.java new file mode 100644 index 000000000..fc9f5637c --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/inheritance/InheritanceNoDbTest.java @@ -0,0 +1,31 @@ +package io.agrest.cayenne.unit.inheritance; + +import io.agrest.cayenne.unit.NoDbTest; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +/** + * A superclass of Cayenne-aware test cases that do not need to access the DB, but need to work with EntityResolver + * and higher levels of the stack. + */ +public abstract class InheritanceNoDbTest extends NoDbTest { + + protected static ServerRuntime runtime; + + @BeforeAll + public static void setUpClass() { + runtime = createRuntime("inheritance/cayenne-project.xml"); + } + + @AfterAll + public static void tearDownClass() { + runtime.shutdown(); + runtime = null; + } + + @Override + public ServerRuntime getRuntime() { + return runtime; + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/main/MainDbTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/main/MainDbTest.java new file mode 100644 index 000000000..f48777b58 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/main/MainDbTest.java @@ -0,0 +1,30 @@ +package io.agrest.cayenne.unit.main; + +import io.agrest.cayenne.unit.AgCayenneTester; +import io.bootique.jdbc.junit5.derby.DerbyTester; +import io.bootique.junit5.BQTest; +import io.bootique.junit5.BQTestScope; +import io.bootique.junit5.BQTestTool; + +/** + * An abstract superclass of integration tests that starts Bootique test runtime with JAX-RS service and Derby DB. + */ +@BQTest +public abstract class MainDbTest { + + @BQTestTool(BQTestScope.GLOBAL) + static final DerbyTester db = DerbyTester.db().initDB("classpath:main/schema-derby.sql"); + + protected static AgCayenneTester.Builder tester(Class... resources) { + return new Builder() + .db(db) + .cayenneProject("main/cayenne-project.xml") + .resources(resources); + } + + public static class Builder extends AgCayenneTester.Builder { + public Builder() { + super(new MainModelTester()); + } + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/main/MainModelTester.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/main/MainModelTester.java new file mode 100644 index 000000000..7118228b7 --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/main/MainModelTester.java @@ -0,0 +1,139 @@ +package io.agrest.cayenne.unit.main; + +import io.agrest.cayenne.unit.AgCayenneTester; +import io.bootique.jdbc.junit5.Table; + +public class MainModelTester extends AgCayenneTester { + + public Table e1() { + return db.getTable("e1"); + } + + public Table e2() { + return db.getTable("e2"); + } + + public Table e3() { + return db.getTable("e3"); + } + + public Table e4() { + return db.getTable("e4"); + } + + public Table e5() { + return db.getTable("e5"); + } + + public Table e6() { + return db.getTable("e6"); + } + + public Table e7() { + return db.getTable("e7"); + } + + public Table e8() { + return db.getTable("e8"); + } + + public Table e9() { + return db.getTable("e9"); + } + + public Table e10() { + return db.getTable("e10"); + } + + public Table e11() { + return db.getTable("e11"); + } + + public Table e12() { + return db.getTable("e12"); + } + + public Table e13() { + return db.getTable("e13"); + } + + public Table e12_13() { + return db.getTable("e12_e13"); + } + + public Table e14() { + return db.getTable("e14"); + } + + public Table e15() { + return db.getTable("e15"); + } + + public Table e15_1() { + return db.getTable("e15_e1"); + } + + public Table e15_5() { + return db.getTable("e15_e5"); + } + + public Table e17() { + return db.getTable("e17"); + } + + public Table e18() { + return db.getTable("e18"); + } + + public Table e19() { + return db.getTable("e19"); + } + + public Table e20() { + return db.getTable("e20"); + } + + public Table e21() { + return db.getTable("e21"); + } + + public Table e22() { + return db.getTable("e22"); + } + + public Table e23() { + return db.getTable("e23"); + } + + public Table e24() { + return db.getTable("e24"); + } + + public Table e25() { + return db.getTable("e25"); + } + + public Table e26() { + return db.getTable("e26"); + } + + public Table e27NoPk() { + return db.getTable("e27_nopk"); + } + + public Table e28() { + return db.getTable("e28"); + } + + public Table e29() { + return db.getTable("e29"); + } + + public Table e30() { + return db.getTable("e30"); + } + + public Table e31() { + return db.getTable("e31"); + } +} diff --git a/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/main/MainNoDbTest.java b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/main/MainNoDbTest.java new file mode 100644 index 000000000..56443c20e --- /dev/null +++ b/agrest-cayenne/src/test/java/io/agrest/cayenne/unit/main/MainNoDbTest.java @@ -0,0 +1,31 @@ +package io.agrest.cayenne.unit.main; + +import io.agrest.cayenne.unit.NoDbTest; +import org.apache.cayenne.configuration.server.ServerRuntime; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +/** + * A superclass of Cayenne-aware test cases that do not need to access the DB, but need to work with EntityResolver + * and higher levels of the stack. + */ +public abstract class MainNoDbTest extends NoDbTest { + + protected static ServerRuntime runtime; + + @BeforeAll + public static void setUpClass() { + runtime = createRuntime("main/cayenne-project.xml"); + } + + @AfterAll + public static void tearDownClass() { + runtime.shutdown(); + runtime = null; + } + + @Override + public ServerRuntime getRuntime() { + return runtime; + } +} diff --git a/agrest-cayenne/src/test/resources/cayenne-project.xml b/agrest-cayenne/src/test/resources/inheritance/cayenne-project.xml similarity index 100% rename from agrest-cayenne/src/test/resources/cayenne-project.xml rename to agrest-cayenne/src/test/resources/inheritance/cayenne-project.xml diff --git a/agrest-cayenne/src/test/resources/inheritance/datamap.map.xml b/agrest-cayenne/src/test/resources/inheritance/datamap.map.xml new file mode 100644 index 000000000..2b56f70bc --- /dev/null +++ b/agrest-cayenne/src/test/resources/inheritance/datamap.map.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../../java + entity + + templates/v4_1/superclass.vm + templates/v4_1/embeddable-subclass.vm + templates/v4_1/embeddable-superclass.vm + templates/v4_1/datamap-subclass.vm + templates/v4_1/datamap-superclass.vm + *.java + true + true + false + false + false + false + + diff --git a/agrest-cayenne/src/test/resources/inheritance/schema-derby.sql b/agrest-cayenne/src/test/resources/inheritance/schema-derby.sql new file mode 100644 index 000000000..11d53fd16 --- /dev/null +++ b/agrest-cayenne/src/test/resources/inheritance/schema-derby.sql @@ -0,0 +1,24 @@ +CREATE TABLE "ie2" ("id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY, PRIMARY KEY ("id")) +; + +CREATE TABLE "ie1" ("a0" VARCHAR (100), "a1" VARCHAR (100), "a2" VARCHAR (100), "a3" VARCHAR (100), "e2_id" INTEGER , "id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY, "type" INTEGER NOT NULL, PRIMARY KEY ("id")) +; + +CREATE TABLE "ie3" ("e1_id" INTEGER NOT NULL, "id" INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY, PRIMARY KEY ("id")) +; + +ALTER TABLE "ie1" ADD FOREIGN KEY ("e2_id") REFERENCES "ie2" ("id") +; + +ALTER TABLE "ie3" ADD FOREIGN KEY ("e1_id") REFERENCES "ie1" ("id") +; + +CREATE SEQUENCE "PK_IE1" AS BIGINT START WITH 200 INCREMENT BY 20 NO MAXVALUE NO CYCLE +; + +CREATE SEQUENCE "PK_IE3" AS BIGINT START WITH 200 INCREMENT BY 20 NO MAXVALUE NO CYCLE +; + +CREATE SEQUENCE "PK_IE2" AS BIGINT START WITH 200 INCREMENT BY 20 NO MAXVALUE NO CYCLE +; + diff --git a/agrest-cayenne/src/test/resources/main/cayenne-project.xml b/agrest-cayenne/src/test/resources/main/cayenne-project.xml new file mode 100644 index 000000000..9f629d374 --- /dev/null +++ b/agrest-cayenne/src/test/resources/main/cayenne-project.xml @@ -0,0 +1,7 @@ + + + + diff --git a/agrest-cayenne/src/test/resources/datamap.map.xml b/agrest-cayenne/src/test/resources/main/datamap.map.xml similarity index 97% rename from agrest-cayenne/src/test/resources/datamap.map.xml rename to agrest-cayenne/src/test/resources/main/datamap.map.xml index db09ae19a..44a005882 100644 --- a/agrest-cayenne/src/test/resources/datamap.map.xml +++ b/agrest-cayenne/src/test/resources/main/datamap.map.xml @@ -464,14 +464,24 @@ + + + TABLE + VIEW + + false + false + org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator + false + false + false + true + - E31 - ../java + ../../java entity templates/v4_1/superclass.vm - - templates/v4_1/superclass.vm templates/v4_1/embeddable-subclass.vm templates/v4_1/embeddable-superclass.vm templates/v4_1/datamap-subclass.vm diff --git a/agrest-cayenne/src/test/resources/schema-derby.sql b/agrest-cayenne/src/test/resources/main/schema-derby.sql similarity index 98% rename from agrest-cayenne/src/test/resources/schema-derby.sql rename to agrest-cayenne/src/test/resources/main/schema-derby.sql index a7d4f0cba..08e29f831 100644 --- a/agrest-cayenne/src/test/resources/schema-derby.sql +++ b/agrest-cayenne/src/test/resources/main/schema-derby.sql @@ -1,4 +1,4 @@ --- main +-- inheritance CREATE TABLE "e15" ("long_id" BIGINT NOT NULL, "name" VARCHAR (100), PRIMARY KEY ("long_id")) ; @@ -231,6 +231,9 @@ CREATE SEQUENCE "PK_E8" AS BIGINT START WITH 200 INCREMENT BY 20 NO MAXVALUE NO CREATE SEQUENCE "PK_E28" AS BIGINT START WITH 200 INCREMENT BY 20 NO MAXVALUE NO CYCLE ; +CREATE SEQUENCE "PK_E29" AS BIGINT START WITH 200 INCREMENT BY 20 NO MAXVALUE NO CYCLE +; + CREATE SEQUENCE "PK_E30" AS BIGINT START WITH 200 INCREMENT BY 20 NO MAXVALUE NO CYCLE ; diff --git a/agrest-engine/pom.xml b/agrest-engine/pom.xml index d3b5d41f9..5732f66e7 100644 --- a/agrest-engine/pom.xml +++ b/agrest-engine/pom.xml @@ -5,10 +5,11 @@ io.agrest agrest-parent - 5.0.M8-SNAPSHOT + 5.0-SNAPSHOT agrest-engine + 5.0-SNAPSHOT agrest-engine: core server engine Core Agrest server engine @@ -43,6 +44,11 @@ bootique-junit5 test + + org.junit.jupiter + junit-jupiter-params + test + org.slf4j slf4j-simple diff --git a/agrest-engine/src/main/java/io/agrest/AgException.java b/agrest-engine/src/main/java/io/agrest/AgException.java index c1ccec791..f489a5b47 100644 --- a/agrest-engine/src/main/java/io/agrest/AgException.java +++ b/agrest-engine/src/main/java/io/agrest/AgException.java @@ -31,7 +31,7 @@ public static AgException of(int httpStatus, Throwable th, String message, Objec * @since 4.7 */ public static AgException badRequest() { - return new AgException(HttpStatus.BAD_REQUEST, null, null, null); + return new AgException(HttpStatus.BAD_REQUEST, null, null); } /** @@ -52,7 +52,7 @@ public static AgException badRequest(Throwable th, String message, Object... mes * @since 4.7 */ public static AgException forbidden() { - return new AgException(HttpStatus.FORBIDDEN, null, null, null); + return new AgException(HttpStatus.FORBIDDEN, null, null); } /** @@ -75,7 +75,7 @@ public static AgException forbidden(Throwable th, String message, Object... mess * @since 4.7 */ public static AgException notFound() { - return new AgException(HttpStatus.NOT_FOUND, null, null, null); + return new AgException(HttpStatus.NOT_FOUND, null, null); } /** @@ -96,7 +96,7 @@ public static AgException notFound(Throwable th, String message, Object... messa * @since 4.7 */ public static AgException internalServerError() { - return new AgException(HttpStatus.INTERNAL_SERVER_ERROR, null, null, null); + return new AgException(HttpStatus.INTERNAL_SERVER_ERROR, null, null); } /** diff --git a/agrest-engine/src/main/java/io/agrest/AgRequestBuilder.java b/agrest-engine/src/main/java/io/agrest/AgRequestBuilder.java index 434150fad..21b715e86 100644 --- a/agrest-engine/src/main/java/io/agrest/AgRequestBuilder.java +++ b/agrest-engine/src/main/java/io/agrest/AgRequestBuilder.java @@ -27,6 +27,11 @@ public interface AgRequestBuilder { AgRequestBuilder addExclude(String unparsedExclude); + /** + * @since 5.0 + */ + AgRequestBuilder addSorts(List unparsedSorts); + /** * @since 5.0 */ diff --git a/agrest-engine/src/main/java/io/agrest/AgResponse.java b/agrest-engine/src/main/java/io/agrest/AgResponse.java index 49b17af08..6ace519c1 100644 --- a/agrest-engine/src/main/java/io/agrest/AgResponse.java +++ b/agrest-engine/src/main/java/io/agrest/AgResponse.java @@ -1,5 +1,8 @@ package io.agrest; +import java.util.List; +import java.util.Map; + /** * A base response object in Agrest. * @@ -8,9 +11,15 @@ public abstract class AgResponse { protected final int status; + protected final Map> headers; + + protected AgResponse(int status, Map> headers) { + if (!HttpStatus.isValid(status)) { + throw new IllegalArgumentException("Invalid HTTP status: " + status); + } - public AgResponse(int status) { this.status = status; + this.headers = headers; } /** @@ -19,4 +28,11 @@ public AgResponse(int status) { public int getStatus() { return status; } + + /** + * @since 5.0 + */ + public Map> getHeaders() { + return headers; + } } diff --git a/agrest-engine/src/main/java/io/agrest/DataResponse.java b/agrest-engine/src/main/java/io/agrest/DataResponse.java index 2c19de3cb..71042fab2 100644 --- a/agrest-engine/src/main/java/io/agrest/DataResponse.java +++ b/agrest-engine/src/main/java/io/agrest/DataResponse.java @@ -2,12 +2,11 @@ import io.agrest.encoder.DataResponseEncoder; import io.agrest.encoder.Encoder; -import io.agrest.encoder.GenericEncoder; -import io.agrest.encoder.ListEncoder; import io.agrest.protocol.CollectionResponse; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -22,29 +21,42 @@ public class DataResponse extends AgResponse implements CollectionResponse /** * @since 5.0 */ - public static Builder of(List data) { - return new Builder(data); + public static Builder of(int status) { + return of(status, Collections.emptyList()); } /** - * @deprecated since 5.0 in favor of the new builder created via {@link #of(List)} + * @since 5.0 */ - @Deprecated + public static Builder of(int status, List data) { + return new Builder<>(status, data); + } + + /** + * @deprecated in favor of the new builder created via {@link #of(int, List)} + */ + @Deprecated(since = "5.0") public static DataResponse forObject(T object) { Objects.requireNonNull(object); - return of(List.of(object)).build(); + return of(HttpStatus.OK, List.of(object)).build(); } /** - * @deprecated since 5.0 in favor oof the new builder created via {@link #of(List)} + * @deprecated in favor of the new builder created via {@link #of(int, List)} */ - @Deprecated + @Deprecated(since = "5.0") public static DataResponse forObjects(List data) { - return of(data).build(); + return of(HttpStatus.OK, data).build(); } - protected DataResponse(int status, List data, int total, Encoder encoder) { - super(status); + protected DataResponse( + int status, + Map> headers, + List data, + int total, + Encoder encoder) { + + super(status, headers); this.total = total; this.data = data; this.encoder = encoder; @@ -88,16 +100,28 @@ public static class Builder { private Integer status; private Integer total; private Encoder encoder; + private Map> headers; - public Builder(List data) { + private Builder(int status, List data) { + this.status = status; this.data = data; } + /** + * @since 5.0 + */ + public Builder headers(Map> headers) { + this.headers = headers; + return this; + } + + @Deprecated(since = "5.0") public Builder data(List data) { this.data = data; return this; } + @Deprecated(since = "5.0") public Builder status(int status) { this.status = status; return this; @@ -109,7 +133,7 @@ public Builder total(int total) { } public Builder elementEncoder(Encoder elementEncoder) { - return encoder(defaultEncoder(elementEncoder)); + return encoder(DataResponseEncoder.withElementEncoder(elementEncoder)); } public Builder encoder(Encoder encoder) { @@ -118,17 +142,16 @@ public Builder encoder(Encoder encoder) { } public DataResponse build() { + List data = this.data != null ? this.data : Collections.emptyList(); + return new DataResponse<>( status != null ? status : HttpStatus.OK, + headers != null ? headers : Collections.emptyMap(), data, total != null ? total : data.size(), - encoder != null ? encoder : defaultEncoder(GenericEncoder.encoder()) + encoder != null ? encoder : DataResponseEncoder.defaultEncoder() ); } - - private Encoder defaultEncoder(Encoder elementEncoder) { - return new DataResponseEncoder("data", new ListEncoder(elementEncoder), "total", GenericEncoder.encoder()); - } } } diff --git a/agrest-engine/src/main/java/io/agrest/DeleteBuilder.java b/agrest-engine/src/main/java/io/agrest/DeleteBuilder.java index 8ebcb17c3..85e711411 100644 --- a/agrest-engine/src/main/java/io/agrest/DeleteBuilder.java +++ b/agrest-engine/src/main/java/io/agrest/DeleteBuilder.java @@ -6,44 +6,62 @@ import io.agrest.processor.ProcessorOutcome; import io.agrest.runtime.processor.delete.DeleteContext; +import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.function.Consumer; +import static java.util.Arrays.asList; + /** * @since 1.4 */ public interface DeleteBuilder { /** - * Will delete na object with a given id. Can be called multiple times for multiple ids. + * Will delete an object with the specified id. Any previously set IDs are replaced with this ID. For single-value + * ID entities, the ID argument should be a simple value (e.g. an Integer). For multi-value IDs, it should be a Map + * of values. * * @since 5.0 */ - DeleteBuilder byId(Object id); + default DeleteBuilder byId(Object id) { + return byIds(List.of(id)); + } /** - * @deprecated since 5.0 in favor of {@link #byId(Object)} + * Will delete objects identified by the specified single-value IDs. Any previously set IDs are replaced with the + * new collection. For single-value ID entities, array elements should be simple values (e.g. Integers). For + * multi-value IDs, they should be represented as Maps. + * + * @since 5.0 */ - @Deprecated - default DeleteBuilder id(Object id) { - return byId(id); + default DeleteBuilder byIds(Object... ids) { + return byIds(asList(ids)); } /** - * Will delete na object with a given id. Can be called multiple times for multiple ids. + * Will delete objects identified by the specified single-value IDs. Any previously set IDs are replaced with the + * new collection. For single-value ID entities, array elements should be simple values (e.g. Integers). For + * multi-value IDs, they should be represented as Maps. * - * @param id multi-attribute ID * @since 5.0 */ - DeleteBuilder byId(Map id); + DeleteBuilder byIds(Collection ids); /** - * @deprecated since 5.0 in favor of {@link #byId(Map)} + * @deprecated since 5.0 in favor of {@link #byId(Object)}, but keep in mind that the new API does not append the + * ID to the internal collection, but rather replaces it. See also {@link #byIds(Object...)}. */ - @Deprecated - default DeleteBuilder id(Map id) { - return byId(id); - } + @Deprecated(since = "5.0", forRemoval = true) + DeleteBuilder id(Object id); + + /** + * @deprecated since 5.0 in favor of {@link #byId(Object)}, but keep in mind that the new API does not append the + * ID to the internal collection, but rather replaces it. See also {@link #byIds(Object...)} + */ + @Deprecated(since = "5.0", forRemoval = true) + DeleteBuilder id(Map id); DeleteBuilder parent(Class parentType, Object parentId, String relationshipFromParent); diff --git a/agrest-engine/src/main/java/io/agrest/EntityUpdate.java b/agrest-engine/src/main/java/io/agrest/EntityUpdate.java index a358be497..796f53a84 100644 --- a/agrest-engine/src/main/java/io/agrest/EntityUpdate.java +++ b/agrest-engine/src/main/java/io/agrest/EntityUpdate.java @@ -3,31 +3,36 @@ import io.agrest.meta.AgEntity; import io.agrest.protocol.UpdateRequest; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** - * Contains update data of a single object. + * A mutable object with update data of a single model object. For to-many relationships makes a distinction between + * "no update present" (null collection) and "all related objects should be removed" (empty collection). * * @since 1.3 */ public class EntityUpdate implements UpdateRequest { private final AgEntity entity; - private final Map values; - private final Map> relatedIds; - private Map id; - private boolean explicitId; - private Object mergedTo; + private Map idParts; + private Map attributes; + private Map> toOnes; + private Map>> toManys; + private Map toOneIds; + private Map> toManyIds; + + private T targetObject; public EntityUpdate(AgEntity entity) { this.entity = Objects.requireNonNull(entity); - this.values = new HashMap<>(); - this.relatedIds = new HashMap<>(); } /** @@ -38,15 +43,15 @@ public EntityUpdate(AgEntity entity) { */ public EntityUpdate merge(EntityUpdate anotherUpdate) { - this.values.putAll(anotherUpdate.values); - this.relatedIds.putAll(anotherUpdate.relatedIds); - - if (anotherUpdate.id != null && !anotherUpdate.id.isEmpty()) { - getOrCreateId().putAll(anotherUpdate.id); - } + this.idParts = mergeMaps(idParts, anotherUpdate.idParts); + this.attributes = mergeMaps(attributes, anotherUpdate.attributes); + this.toOnes = mergeMaps(toOnes, anotherUpdate.toOnes); + this.toManys = mergeMaps(toManys, anotherUpdate.toManys); + this.toOneIds = mergeMaps(toOneIds, anotherUpdate.toOneIds); + this.toManyIds = mergeMaps(toManyIds, anotherUpdate.toManyIds); - // If we are merging a compatible update, "explicitId", "entity", "mergedTo" should all be identical already. - // Do not override them. + // We are presumably merging a compatible update, so "entity", "targetObject" should all be identical and + // require no override return this; } @@ -58,69 +63,246 @@ public AgEntity getEntity() { return entity; } - public boolean hasChanges() { - return !values.isEmpty() || !relatedIds.isEmpty(); + /** + * Returns an object targeted by this update. The object is null until it is initialized by Agrest, usually during + * {@link UpdateStage#MERGE_CHANGES} step of the pipeline. + * + * @since 5.0 + */ + public T getTargetObject() { + return targetObject; } - public Map getValues() { - return values; + /** + * Sets an object targeted by this update. + * + * @since 5.0 + */ + public void setTargetObject(T targetObject) { + this.targetObject = targetObject; } - public Map> getRelatedIds() { - return relatedIds; + /** + * @since 5.0 + */ + public Map getIdParts() { + return idParts != null ? idParts : Collections.emptyMap(); } - public void addRelatedId(String relationshipName, Object value) { - relatedIds.computeIfAbsent(relationshipName, n -> new HashSet<>()).add(value); + /** + * @since 5.0 + */ + public Object getIdPart(String idPart) { + return idParts != null ? idParts.get(idPart) : null; } /** - * @since 1.8 + * @since 5.0 */ - public Map getId() { - return id; + public void setIdParts(Map id) { + mutableIdParts().putAll(id); } /** - * @since 1.8 + * @since 5.0 */ - public Map getOrCreateId() { - if (id == null) { - id = new HashMap<>(); - } + public void addIdPart(String idPart, Object value) { + mutableIdParts().put(idPart, value); + } + + /** + * @since 5.0 + */ + public void addIdPartIfAbsent(String idPart, Object value) { + mutableIdParts().putIfAbsent(idPart, value); + } - return id; + /** + * @since 5.0 + */ + public Map getAttributes() { + return attributes != null ? attributes : Collections.emptyMap(); + } + + public Object getAttribute(String attribute) { + return attributes != null ? attributes.get(attribute) : null; } /** - * @since 1.8 + * @since 5.0 */ - public void setExplicitId() { - this.explicitId = true; + public void setAttribute(String attribute, Object value) { + mutableAttributes().put(attribute, value); } /** - * @since 1.5 + * @since 5.0 */ - public boolean isExplicitId() { - return explicitId; + public Map> getToOnes() { + return toOnes != null ? toOnes : Collections.emptyMap(); } /** - * Returns an object that was used to merge this update to. + * @since 5.0 + */ + public EntityUpdate getToOne(String relationship) { + return toOnes != null ? (EntityUpdate) toOnes.get(relationship) : null; + } + + /** + * @since 5.0 + */ + public void setToOne(String relationshipName, EntityUpdate update) { + mutableToOnes().put(relationshipName, update); + } + + /** + * Returns a map of EntityUpdates for related entities. This is an experimental feature. As of now, Agrest does not + * support processing nested updates, but this map allows the users to manually extract the values and process them + * by hand. + * + * @since 5.0 + */ + public Map>> getToManys() { + return toManys != null ? toManys : Collections.emptyMap(); + } + + /** + * @since 5.0 + */ + public List> getToMany(String relationship) { + // Do not return empty list of the key is not there. We must distinguish between no update and empty list. + // Empty list may mean "remove all target objects". + return toManys != null ? (List) toManys.get(relationship) : null; + } + + /** + * @since 5.0 + */ + public void addToMany(String relationship, EntityUpdate update) { + mutableToManys().computeIfAbsent(relationship, n -> new ArrayList<>()).add(update); + } + + /** + * A mutator that resets the update to a state indicating an explicit need to remove all related objects. + * This will result in an empty array for related objects, which is different from a null array. * - * @since 1.8 + * @since 5.0 + */ + public void emptyToMany(String relationship) { + mutableToManys().computeIfAbsent(relationship, n -> new ArrayList<>()).clear(); + } + + /** + * @since 5.0 + */ + public Map getToOneIds() { + return toOneIds != null ? toOneIds : Collections.emptyMap(); + } + + /** + * @since 5.0 + */ + public Object getToOneId(String relationship) { + return toOneIds != null ? toOneIds.get(relationship) : null; + } + + /** + * @since 5.0 */ - public Object getMergedTo() { - return mergedTo; + public void setToOneId(String relationship, Object value) { + mutableToOneIds().put(relationship, value); } /** - * Sets an object that was used to merge this update to. + * @since 5.0 + */ + public Map> getToManyIds() { + return toManyIds != null ? toManyIds : Collections.emptyMap(); + } + + /** + * @since 5.0 + */ + public Set getToManyIds(String relationship) { + return toManyIds != null ? toManyIds.get(relationship) : null; + } + + /** + * @since 5.0 + */ + public void addToManyId(String relationship, Object value) { + mutableToManyIds().computeIfAbsent(relationship, n -> new HashSet<>()).add(value); + } + + /** + * A mutator that resets the update to a state indicating an explicit need to remove all related ids. + * This will result in an empty array for ids, which is different from a null array. * - * @since 1.8 + * @since 5.0 */ - public void setMergedTo(Object mergedTo) { - this.mergedTo = mergedTo; + public void emptyToManyIds(String relationship) { + mutableToManyIds().computeIfAbsent(relationship, n -> new HashSet<>()).clear(); + } + + private Map mergeMaps(Map to, Map from) { + if (from == null || from.isEmpty()) { + return to; + } + + if (to != null) { + to.putAll(from); + return to; + } + + return new HashMap<>(from); + } + + private Map mutableIdParts() { + if (idParts == null) { + idParts = new HashMap<>(); + } + + return idParts; + } + + private Map mutableAttributes() { + if (attributes == null) { + attributes = new HashMap<>(); + } + + return attributes; + } + + private Map> mutableToOnes() { + if (toOnes == null) { + toOnes = new HashMap<>(); + } + + return toOnes; + } + + private Map>> mutableToManys() { + if (toManys == null) { + toManys = new HashMap<>(); + } + + return toManys; + } + + private Map mutableToOneIds() { + if (toOneIds == null) { + toOneIds = new HashMap<>(); + } + + return toOneIds; + } + + private Map> mutableToManyIds() { + if (toManyIds == null) { + toManyIds = new HashMap<>(); + } + + return toManyIds; } } diff --git a/agrest-engine/src/main/java/io/agrest/HttpStatus.java b/agrest-engine/src/main/java/io/agrest/HttpStatus.java index 5b698bca4..708435fe0 100644 --- a/agrest-engine/src/main/java/io/agrest/HttpStatus.java +++ b/agrest-engine/src/main/java/io/agrest/HttpStatus.java @@ -11,6 +11,13 @@ public final class HttpStatus { private HttpStatus() { } + /** + * @since 5.0 + */ + public static boolean isValid(int status) { + return status >= 100 && status <= 599; + } + public static final int OK = 200; public static final int CREATED = 201; public static final int ACCEPTED = 202; diff --git a/agrest-engine/src/main/java/io/agrest/PathConstants.java b/agrest-engine/src/main/java/io/agrest/PathConstants.java index 308ba96fc..ed4970b65 100644 --- a/agrest-engine/src/main/java/io/agrest/PathConstants.java +++ b/agrest-engine/src/main/java/io/agrest/PathConstants.java @@ -1,8 +1,16 @@ package io.agrest; +import io.agrest.access.PathChecker; + public interface PathConstants { char DOT = '.'; String ID_PK_ATTRIBUTE = "id"; + + /** + * @deprecated in favor of {@link PathChecker#exceedsLength(String)}. The new size + * limit is 1000 chars. + */ + @Deprecated(since = "5.0") int MAX_PATH_LENGTH = 300; } diff --git a/agrest-engine/src/main/java/io/agrest/ResourceEntity.java b/agrest-engine/src/main/java/io/agrest/ResourceEntity.java index 51166e51f..2c23466c1 100644 --- a/agrest-engine/src/main/java/io/agrest/ResourceEntity.java +++ b/agrest-engine/src/main/java/io/agrest/ResourceEntity.java @@ -1,17 +1,16 @@ package io.agrest; -import io.agrest.meta.AgAttribute; import io.agrest.meta.AgEntity; import io.agrest.protocol.Exp; import io.agrest.protocol.Sort; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; +import java.util.function.BiFunction; /** * A model of a resource entity for a given client request. ResourceEntity is based on an {@link AgEntity} with @@ -22,10 +21,8 @@ public abstract class ResourceEntity { private boolean idIncluded; - private final AgEntity agEntity; - - private final Map attributes; - private final Set defaultAttributes; + private final ResourceEntityProjection baseProjection; + private final Map, ResourceEntityProjection> projections; private final Map> children; private final Map properties; @@ -37,28 +34,100 @@ public abstract class ResourceEntity { public ResourceEntity(AgEntity agEntity) { - this.agEntity = agEntity; - + List> projections = buildProjections(agEntity, new ArrayList<>()); + this.baseProjection = (ResourceEntityProjection) projections.get(0); + this.projections = projectionsByType(projections); this.idIncluded = false; - this.attributes = new HashMap<>(); - this.defaultAttributes = new HashSet<>(); this.children = new HashMap<>(); this.orderings = new ArrayList<>(2); this.properties = new HashMap<>(5); } + private static List> buildProjections( + AgEntity entity, + List> projections) { + + // generally we should not need a projection for abstract entities, but since there's still too much + // code relying on "baseProjection", we don't check for "abstract" flag here, and create projections + // for every entity + + projections.add(new ResourceEntityProjection<>(entity)); + entity.getSubEntities().forEach(se -> buildProjections(se, projections)); + return projections; + } + + private static Map, ResourceEntityProjection> projectionsByType( + List> projections) { + + if (projections.size() == 1) { + return Map.of(projections.get(0).getAgEntity().getType(), projections.get(0)); + } + + Map, ResourceEntityProjection> byType = new HashMap<>(); + projections.forEach(p -> byType.put(p.getAgEntity().getType(), p)); + return byType; + } + /** * @since 3.4 */ public String getName() { - return agEntity.getName(); + return getAgEntity().getName(); + } + + public Class getType() { + return getAgEntity().getType(); + } + + /** + * Returns the projection object associated with this ResourceEntity that corresponds to the topmost superclass + * of the entity. I.e. the same type as the T parameter of the entity. + * + * @since 5.0 + */ + public ResourceEntityProjection getBaseProjection() { + return baseProjection; + } + + /** + * @since 5.0 + */ + public ResourceEntityProjection getProjection(Class type) { + return (ResourceEntityProjection) projections.get(type); + } + + /** + * @since 5.0 + */ + public Collection> getProjections() { + return projections.values(); } /** * @since 1.12 */ public AgEntity getAgEntity() { - return agEntity; + return getBaseProjection().getAgEntity(); + } + + /** + * Returns a previously stored custom object for a given name. The properties mechanism allows pluggable processing + * pipelines to store and exchange data within a given request. + * + * @since 5.0 + */ + public

P getProperty(String name) { + return (P) properties.get(name); + } + + /** + * Sets a property value for a given name. The properties mechanism allows pluggable processing pipelines to + * store and exchange data within a given request. + * + * @since 5.0 + */ + public void setProperty(String name, Object value) { + properties.put(name, value); } /** @@ -96,61 +165,86 @@ public List getOrderings() { } /** - * @since 4.7 + * @return true if the attribute was added to one of the underlying the projections or was already a part of one + * of the projections + * @since 5.0 */ - public AgAttribute getAttribute(String name) { - return attributes.get(name); + public boolean ensureAttribute(String attributeName, boolean isDefault) { + + boolean success = false; + for (ResourceEntityProjection p : projections.values()) { + success = p.ensureAttribute(attributeName, isDefault) || success; + } + return success; } /** - * @since 1.12 + * @since 5.0 */ - public Map getAttributes() { - return attributes; + public boolean removeAttribute(String attributeName) { + + boolean removed = false; + for (ResourceEntityProjection p : projections.values()) { + removed = p.removeAttribute(attributeName) || removed; + } + return removed; } /** - * Returns whether the named attribute was added to the entity implicitly, via the default rules, instead of being - * explicitly requested by the client. - * * @since 3.7 */ - public boolean isDefaultAttribute(String name) { - return defaultAttributes.contains(name); + public boolean removeChild(String name) { + + boolean removed = false; + for (ResourceEntityProjection p : projections.values()) { + removed = p.removeRelationship(name) || removed; + } + + if (removed) { + children.remove(name); + } + + return removed; } /** - * @since 3.7 + * @since 5.0 */ - public void addAttribute(AgAttribute attribute, boolean isDefault) { - attributes.put(attribute.getName(), attribute); - if (isDefault) { - defaultAttributes.add(attribute.getName()); + public boolean hasRelationship(String relationshipName) { + for (ResourceEntityProjection p : projections.values()) { + if (p.getAgEntity().getRelationship(relationshipName) != null) { + return true; + } } + return false; } /** - * @since 3.7 + * @since 5.0 */ - public AgAttribute removeAttribute(String name) { - - AgAttribute removed = attributes.remove(name); - if (removed != null) { - defaultAttributes.remove(name); + public boolean ensureRelationship(String relationshipName) { + boolean success = false; + for (ResourceEntityProjection p : projections.values()) { + success = p.ensureRelationship(relationshipName) || success; } - - return removed; + return success; } /** - * @since 3.7 + * @since 5.0 */ - public RelatedResourceEntity removeChild(String name) { - return children.remove(name); + public RelatedResourceEntity ensureChild( + String relationshipName, + BiFunction, String, RelatedResourceEntity> childCreator) { + + return children.computeIfAbsent(relationshipName, r -> childCreator.apply(this, r)); } - public Map> getChildren() { - return children; + /** + * @since 5.0 + */ + public Collection> getChildren() { + return children.values(); } /** @@ -184,8 +278,8 @@ public ResourceEntity getMapBy() { } /** - * @deprecated in favor of {@link #mapBy(ResourceEntity)}. mapByPath parameter is ignored. * @since 1.1 + * @deprecated in favor of {@link #mapBy(ResourceEntity)}. mapByPath parameter is ignored. */ @Deprecated(since = "5.0") public ResourceEntity mapBy(ResourceEntity mapBy, String mapByPath) { @@ -200,10 +294,6 @@ public ResourceEntity mapBy(ResourceEntity mapBy) { return this; } - public Class getType() { - return agEntity.getType(); - } - /** * @since 5.0 */ @@ -268,27 +358,13 @@ public void setFetchLimit(int fetchLimit) { * @since 1.23 */ public boolean isFiltered() { - return !agEntity.getReadFilter().allowsAll(); - } - - /** - * Returns a previously stored custom object for a given name. The properties mechanism allows pluggable processing - * pipelines to store and exchange data within a given request. - * - * @since 5.0 - */ - public

P getProperty(String name) { - return (P) properties.get(name); - } + for (ResourceEntityProjection p : projections.values()) { + if (!p.getAgEntity().getReadFilter().allowsAll()) { + return true; + } + } - /** - * Sets a property value for a given name. The properties mechanism allows pluggable processing pipelines to - * store and exchange data within a given request. - * - * @since 5.0 - */ - public void setProperty(String name, Object value) { - properties.put(name, value); + return false; } /** diff --git a/agrest-engine/src/main/java/io/agrest/ResourceEntityProjection.java b/agrest-engine/src/main/java/io/agrest/ResourceEntityProjection.java new file mode 100644 index 000000000..77c36b879 --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/ResourceEntityProjection.java @@ -0,0 +1,98 @@ +package io.agrest; + +import io.agrest.meta.AgAttribute; +import io.agrest.meta.AgEntity; +import io.agrest.meta.AgRelationship; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Stores attributes and relationships of a ResourceEntity for a single AgEntity. When inheritance is in play, + * root entity and each sub-entity will have its own "projection". + * + * @since 5.0 + */ +public class ResourceEntityProjection { + + private final AgEntity agEntity; + private final Map attributes; + private final Set defaultAttributes; + private final Map relationships; + + public ResourceEntityProjection(AgEntity agEntity) { + this.agEntity = agEntity; + this.attributes = new HashMap<>(); + this.defaultAttributes = new HashSet<>(); + this.relationships = new HashMap<>(); + } + + public AgEntity getAgEntity() { + return agEntity; + } + + public Collection getAttributes() { + return attributes.values(); + } + + public AgAttribute getAttribute(String name) { + return attributes.get(name); + } + + /** + * @return true if the attribute was added to the projection or was already a part of the projection + */ + public boolean ensureAttribute(String name, boolean isDefault) { + AgAttribute projectionAttribute = agEntity.getAttribute(name); + if (projectionAttribute != null) { + attributes.put(name, projectionAttribute); + + if (isDefault) { + defaultAttributes.add(name); + } + + return true; + } + + return false; + } + + public boolean removeAttribute(String name) { + AgAttribute removed = attributes.remove(name); + if (removed != null) { + defaultAttributes.remove(name); + return true; + } + + return false; + } + + public boolean isDefaultAttribute(String name) { + return defaultAttributes.contains(name); + } + + public Collection getRelationships() { + return relationships.values(); + } + + public AgRelationship getRelationship(String name) { + return relationships.get(name); + } + + public boolean ensureRelationship(String name) { + AgRelationship projectionRelationship = agEntity.getRelationship(name); + if (projectionRelationship != null) { + relationships.put(name, projectionRelationship); + return true; + } + + return false; + } + + public boolean removeRelationship(String name) { + return relationships.remove(name) != null; + } +} diff --git a/agrest-engine/src/main/java/io/agrest/RootResourceEntity.java b/agrest-engine/src/main/java/io/agrest/RootResourceEntity.java index fbf9b6e6d..d445ca093 100644 --- a/agrest-engine/src/main/java/io/agrest/RootResourceEntity.java +++ b/agrest-engine/src/main/java/io/agrest/RootResourceEntity.java @@ -38,9 +38,9 @@ public List getData() { } /** - * @deprecated since 5.0 in favor of {@link #getData()} + * @deprecated in favor of {@link #getData()} */ - @Deprecated + @Deprecated(since = "5.0") public List getResult() { return data; } @@ -53,27 +53,27 @@ public void setData(List data) { } /** - * @deprecated since 5.0 in favor of {@link #setData(List)} + * @deprecated in favor of {@link #setData(List)} */ - @Deprecated + @Deprecated(since = "5.0") public void setResult(List data) { this.data = data; } /** * @since 1.20 - * @deprecated since 5.0 as metadata encoding that uses this will soon be removed from Agrest + * @deprecated as metadata encoding that used this was removed from Agrest */ - @Deprecated + @Deprecated(since = "5.0") public String getApplicationBase() { return applicationBase; } /** * @since 1.20 - * @deprecated since 5.0 as metadata encoding that uses this will soon be removed from Agrest + * @deprecated as metadata encoding that used this was removed from Agrest */ - @Deprecated + @Deprecated(since = "5.0") public void setApplicationBase(String applicationBase) { this.applicationBase = applicationBase; } diff --git a/agrest-engine/src/main/java/io/agrest/SelectBuilder.java b/agrest-engine/src/main/java/io/agrest/SelectBuilder.java index d27e0a134..ae3de7325 100644 --- a/agrest-engine/src/main/java/io/agrest/SelectBuilder.java +++ b/agrest-engine/src/main/java/io/agrest/SelectBuilder.java @@ -58,17 +58,11 @@ public interface SelectBuilder { SelectBuilder entityAttribute(String name, Class valueType, Function reader); /** - * Forces the builder to select a single object by ID. + * Forces the builder to select a single object by ID. For single-value ID entities, the ID argument should be a + * simple value (e.g. an Integer). For multi-value IDs, it should be a Map of values. */ SelectBuilder byId(Object id); - /** - * Forces the builder to select a single object by compound ID. - * - * @since 1.20 - */ - SelectBuilder byId(Map ids); - /** * Appends a {@link PropertyFilter} that defines property access rules for the current request and a given entity. I.e. * which entity attributes, relationships and ids a client is allowed to see. Can be called multiple times to add @@ -134,6 +128,17 @@ default SelectBuilder fetchLimit(int limit) { return limit(limit); } + /** + * Sets the policy for the maximum depth of relationship paths, such as includes. This overrides runtime-defined + * policy for this one request. Depth is counted from the root of the request. Only non-negative depths are allowed. + * Zero depth blocks all relationships, "1" - blocks anything beyond direct relationships, and so on. Attribute + * paths are not counted towards depth (either root or nested). + * + * @return this builder instance + * @since 5.0 + */ + SelectBuilder maxPathDepth(int maxPathDepth); + /** * Registers a consumer to be executed after a specified standard execution stage. The consumer can inspect and * modify provided {@link SelectContext}. diff --git a/agrest-engine/src/main/java/io/agrest/SimpleResponse.java b/agrest-engine/src/main/java/io/agrest/SimpleResponse.java index ea854b3a0..fd442dca5 100644 --- a/agrest-engine/src/main/java/io/agrest/SimpleResponse.java +++ b/agrest-engine/src/main/java/io/agrest/SimpleResponse.java @@ -2,6 +2,10 @@ import io.agrest.protocol.MessageResponse; +import java.util.Collections; +import java.util.List; +import java.util.Map; + /** * Default implementation of {@link MessageResponse}. */ @@ -13,18 +17,29 @@ public class SimpleResponse extends AgResponse implements MessageResponse { * @since 5.0 */ public static SimpleResponse of(int status) { - return new SimpleResponse(status, null); + return of(status, Collections.emptyMap(), null); } /** * @since 5.0 */ public static SimpleResponse of(int status, String message) { - return new SimpleResponse(status, message); + return of(status, Collections.emptyMap(), message); } - protected SimpleResponse(int status, String message) { - super(status); + /** + * @since 5.0 + */ + public static SimpleResponse of(int status, Map> headers, String message) { + return new SimpleResponse( + status, + headers != null ? headers : Collections.emptyMap(), + message); + } + + + protected SimpleResponse(int status, Map> headers, String message) { + super(status, headers); this.message = message; } diff --git a/agrest-engine/src/main/java/io/agrest/UnrelateBuilder.java b/agrest-engine/src/main/java/io/agrest/UnrelateBuilder.java index 78b4b96ad..73fe5dc12 100644 --- a/agrest-engine/src/main/java/io/agrest/UnrelateBuilder.java +++ b/agrest-engine/src/main/java/io/agrest/UnrelateBuilder.java @@ -1,7 +1,5 @@ package io.agrest; -import java.util.Map; - /** * @since 5.0 */ @@ -9,20 +7,10 @@ public interface UnrelateBuilder { UnrelateBuilder sourceId(Object id); - /** - * @since 5.0 - */ - UnrelateBuilder sourceId(Map ids); - UnrelateBuilder allRelated(String relationship); UnrelateBuilder related(String relationship, Object targetId); - /** - * @since 5.0 - */ - UnrelateBuilder related(String relationship, Map targetId); - /** * Executes "unrelate" pipeline for the request configured in this builder. */ diff --git a/agrest-engine/src/main/java/io/agrest/UpdateBuilder.java b/agrest-engine/src/main/java/io/agrest/UpdateBuilder.java index 2b6af47fb..77c2c3824 100644 --- a/agrest-engine/src/main/java/io/agrest/UpdateBuilder.java +++ b/agrest-engine/src/main/java/io/agrest/UpdateBuilder.java @@ -43,14 +43,7 @@ default UpdateBuilder id(Object id) { } /** - * Set an explicit multi-value id for the update. In this case only a single object is allowed in the update. - * - * @since 5.0 - */ - UpdateBuilder byId(Map id); - - /** - * @deprecated since 5.0 in favor of {@link #byId(Map)}. + * @deprecated since 5.0 in favor of {@link #byId(Object)}. */ @Deprecated default UpdateBuilder id(Map id) { @@ -169,6 +162,17 @@ default UpdateBuilder mapper(String propertyName) { return mapper(ByKeyObjectMapperFactory.byKey(propertyName)); } + /** + * Sets the policy for the maximum depth of relationship paths, such as includes. This overrides runtime-defined + * policy for this one request. Depth is counted from the root of the request. Only non-negative depths are allowed. + * Zero depth blocks all relationships, "1" - blocks anything beyond direct relationships, and so on. Attribute + * paths are not counted towards depth (either root or nested). + * + * @return this builder instance + * @since 5.0 + */ + UpdateBuilder maxPathDepth(int maxPathDepth); + /** * Registers a consumer to be executed after a specified standard execution stage. The consumer can inspect and * modify provided {@link UpdateContext}. diff --git a/agrest-engine/src/main/java/io/agrest/access/MultiTypeReadFilter.java b/agrest-engine/src/main/java/io/agrest/access/MultiTypeReadFilter.java new file mode 100644 index 000000000..4feb6f8ce --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/access/MultiTypeReadFilter.java @@ -0,0 +1,32 @@ +package io.agrest.access; + +import java.util.function.Function; + +/** + * @param + * @since 5.0 + */ +public class MultiTypeReadFilter implements ReadFilter { + + private final ReadFilter nullFilter; + private final Function, ReadFilter> filterFactory; + + public MultiTypeReadFilter(Function, ReadFilter> filterFactory, ReadFilter nullFilter) { + this.nullFilter = nullFilter; + this.filterFactory = filterFactory; + } + + @Override + public boolean isAllowed(T object) { + + if (object == null) { + return nullFilter.isAllowed(null); + } + + ReadFilter filter = filterFactory.apply(object.getClass()); + return filter != null + ? filter.isAllowed(object) + // unknown type - just deny it... TODO: print a debug message? + : false; + } +} diff --git a/agrest-engine/src/main/java/io/agrest/access/PathChecker.java b/agrest-engine/src/main/java/io/agrest/access/PathChecker.java new file mode 100644 index 000000000..e774673f8 --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/access/PathChecker.java @@ -0,0 +1,91 @@ +package io.agrest.access; + +import io.agrest.AgException; +import io.agrest.PathConstants; + +/** + * A policy defining maximum String length and number of relationships (depths) of property paths. + * + * @since 5.0 + */ +public class PathChecker { + + private static final int DEFAULT_DEPTH = 100; + + // TODO: make this a configurable object property + private static final int MAX_PATH_LENGTH = 1000; + + private final int depth; + + public static PathChecker ofDefault() { + return new PathChecker(DEFAULT_DEPTH); + } + + /** + * Creates a policy for the maximum depth of relationship paths, such as includes. Depth is counted from the root of + * the request. Only non-negative depths are allowed. Zero depth blocks all relationships, "1" - blocks anything + * beyond direct relationships, and so on. Attribute paths are not counted towards depth (either root or nested). + */ + public static PathChecker of(int depth) { + if (depth < 0) { + throw new IllegalArgumentException("Negative max include depth is invalid: " + depth); + } + + return new PathChecker(depth); + } + + /** + * Checks whether a given path exceeds hardcoded max length in characters. Unlike {@link #exceedsDepth(String)}, + * this method is intended for fast sanity checks that don't require String content analysis. + */ + public static void exceedsLength(String path) { + if (path != null && path.length() > MAX_PATH_LENGTH) { + throw AgException.badRequest("Include/exclude path too long: %s", path); + } + } + + protected PathChecker(int depth) { + this.depth = depth; + } + + public int getDepth() { + return depth; + } + + + /** + * Checks whether a given path exceeds this max depth. If the depth is exceeded, an exception is thrown. Successful + * return is somewhat fuzzy, because the last component may be an attribute or a relationship, and we have no way + * of telling without resolving the full path against the model. So the method will also succeed when there is one + * extra path component above the threshold. + */ + public void exceedsDepth(String path) { + + if (path == null) { + return; + } + + exceedsLength(path); + + int len = path.length(); + if (len <= depth) { + return; + } + + int dots = 0; + for (int i = 0; i < len; i++) { + if (path.charAt(i) == PathConstants.DOT) { + + // fuzzy logic: treating the last path component as an attribute, and do not count it in depth total + if (++dots > depth) { + throw AgException.badRequest( + "Path exceeds the max allowed depth of %s, the remaining path '%s' can't be processed", + depth, + path); + } + } + } + + // if no dots are found, allow even if the depth == 0 (same fuzzy logic as mentioned above) + } +} diff --git a/agrest-engine/src/main/java/io/agrest/compiler/AnnotationsAgEntityBuilder.java b/agrest-engine/src/main/java/io/agrest/compiler/AnnotationsAgEntityBuilder.java index 963d74013..d5b014fd3 100644 --- a/agrest-engine/src/main/java/io/agrest/compiler/AnnotationsAgEntityBuilder.java +++ b/agrest-engine/src/main/java/io/agrest/compiler/AnnotationsAgEntityBuilder.java @@ -24,11 +24,13 @@ import org.slf4j.LoggerFactory; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -218,6 +220,9 @@ protected AgEntity buildEntity() { return new DefaultEntity<>( name, type, + Modifier.isAbstract(type.getModifiers()), + // TODO: support for inheritance in Ag core + Collections.emptyList(), ids, attributes, relationships, @@ -232,6 +237,6 @@ protected AgEntity buildEntity() { * @since 4.8 */ protected AgEntity applyOverlay(AgEntity entity) { - return overlay != null ? overlay.resolve(schema, entity) : entity; + return entity.resolveOverlay(schema, overlay); } } diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/BigIntegerConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/BigIntegerConverter.java new file mode 100644 index 000000000..13eb0561c --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/BigIntegerConverter.java @@ -0,0 +1,28 @@ +package io.agrest.converter.jsonvalue; + +import com.fasterxml.jackson.databind.JsonNode; +import io.agrest.AgException; + +import java.math.BigInteger; + +/** + * @since 5.0 + */ +public class BigIntegerConverter extends AbstractConverter { + + private static final BigIntegerConverter instance = new BigIntegerConverter(); + + public static BigIntegerConverter converter() { + return instance; + } + + @Override + protected BigInteger valueNonNull(JsonNode node) { + + try { + return new BigInteger(node.asText()); + } catch (NumberFormatException e) { + throw AgException.badRequest("Expected numeric value, got: %s", node.asText()); + } + } +} diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ByteConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ByteConverter.java new file mode 100644 index 000000000..f15f38aa7 --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ByteConverter.java @@ -0,0 +1,31 @@ +package io.agrest.converter.jsonvalue; + +import com.fasterxml.jackson.databind.JsonNode; +import io.agrest.AgException; + +/** + * @since 5.0 + */ +public class ByteConverter extends AbstractConverter { + + private static final ByteConverter instance = new ByteConverter(); + + public static ByteConverter converter() { + return instance; + } + + @Override + protected Byte valueNonNull(JsonNode node) { + + if (!node.isNumber()) { + throw AgException.badRequest("Expected numeric value, got: %s", node.asText()); + } + + int i = node.asInt(); + if (i < Byte.MIN_VALUE || i > Byte.MAX_VALUE) { + throw AgException.badRequest("Value is out of range for 'java.lang.Byte': %s", node.asText()); + } + + return (byte) i; + } +} diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/JsonValueConverters.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/JsonValueConverters.java index 50e95ed06..6746bdc60 100644 --- a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/JsonValueConverters.java +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/JsonValueConverters.java @@ -1,8 +1,8 @@ package io.agrest.converter.jsonvalue; -import io.agrest.reflect.Types; import io.agrest.reflect.BeanAnalyzer; import io.agrest.reflect.PropertySetter; +import io.agrest.reflect.Types; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOLocalDateConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/LocalDateConverter.java similarity index 56% rename from agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOLocalDateConverter.java rename to agrest-engine/src/main/java/io/agrest/converter/jsonvalue/LocalDateConverter.java index 3cf93b7de..6c3d72438 100644 --- a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOLocalDateConverter.java +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/LocalDateConverter.java @@ -4,11 +4,11 @@ import java.time.LocalDate; -public class ISOLocalDateConverter extends AbstractConverter { +public class LocalDateConverter extends AbstractConverter { - private static final ISOLocalDateConverter instance = new ISOLocalDateConverter(); + private static final LocalDateConverter instance = new LocalDateConverter(); - public static ISOLocalDateConverter converter() { + public static LocalDateConverter converter() { return instance; } diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOLocalDateTimeConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/LocalDateTimeConverter.java similarity index 54% rename from agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOLocalDateTimeConverter.java rename to agrest-engine/src/main/java/io/agrest/converter/jsonvalue/LocalDateTimeConverter.java index 70656a55c..1fdbf3b6b 100644 --- a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOLocalDateTimeConverter.java +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/LocalDateTimeConverter.java @@ -4,11 +4,11 @@ import java.time.LocalDateTime; -public class ISOLocalDateTimeConverter extends AbstractConverter { +public class LocalDateTimeConverter extends AbstractConverter { - private static final ISOLocalDateTimeConverter instance = new ISOLocalDateTimeConverter(); + private static final LocalDateTimeConverter instance = new LocalDateTimeConverter(); - public static ISOLocalDateTimeConverter converter() { + public static LocalDateTimeConverter converter() { return instance; } diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOLocalTimeConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/LocalTimeConverter.java similarity index 56% rename from agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOLocalTimeConverter.java rename to agrest-engine/src/main/java/io/agrest/converter/jsonvalue/LocalTimeConverter.java index 334d905c4..c56be3759 100644 --- a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOLocalTimeConverter.java +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/LocalTimeConverter.java @@ -4,11 +4,11 @@ import java.time.LocalTime; -public class ISOLocalTimeConverter extends AbstractConverter { +public class LocalTimeConverter extends AbstractConverter { - private static final ISOLocalTimeConverter instance = new ISOLocalTimeConverter(); + private static final LocalTimeConverter instance = new LocalTimeConverter(); - public static ISOLocalTimeConverter converter() { + public static LocalTimeConverter converter() { return instance; } diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/MapConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/MapConverter.java new file mode 100644 index 000000000..dab9246f8 --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/MapConverter.java @@ -0,0 +1,33 @@ +package io.agrest.converter.jsonvalue; + +import com.fasterxml.jackson.databind.JsonNode; +import io.agrest.AgException; + +import java.util.HashMap; +import java.util.Map; + +/** + * @since 5.0 + */ +public class MapConverter extends AbstractConverter> { + + private final Map> propertyConverters; + private final int capacity; + + public MapConverter(Map> propertyConverters) { + this.propertyConverters = propertyConverters; + this.capacity = (int) Math.ceil(propertyConverters.size() * 4 / 3.); + } + + @Override + protected Map valueNonNull(JsonNode node) { + + if (!node.isObject()) { + throw AgException.badRequest("Expected object value, got: %s", node.asText()); + } + + Map map = new HashMap<>(capacity); + propertyConverters.forEach((k, v) -> map.put(k, v.value(node.get(k)))); + return map; + } +} diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOOffsetDateTimeConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/OffsetDateTimeConverter.java similarity index 54% rename from agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOOffsetDateTimeConverter.java rename to agrest-engine/src/main/java/io/agrest/converter/jsonvalue/OffsetDateTimeConverter.java index f83726edd..9129548e8 100644 --- a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ISOOffsetDateTimeConverter.java +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/OffsetDateTimeConverter.java @@ -1,14 +1,14 @@ package io.agrest.converter.jsonvalue; -import java.time.OffsetDateTime; - import com.fasterxml.jackson.databind.JsonNode; -public class ISOOffsetDateTimeConverter extends AbstractConverter { +import java.time.OffsetDateTime; + +public class OffsetDateTimeConverter extends AbstractConverter { - private static final ISOOffsetDateTimeConverter instance = new ISOOffsetDateTimeConverter(); + private static final OffsetDateTimeConverter instance = new OffsetDateTimeConverter(); - public static ISOOffsetDateTimeConverter converter() { + public static OffsetDateTimeConverter converter() { return instance; } diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ShortConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ShortConverter.java new file mode 100644 index 000000000..dfbf93c0a --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/ShortConverter.java @@ -0,0 +1,31 @@ +package io.agrest.converter.jsonvalue; + +import com.fasterxml.jackson.databind.JsonNode; +import io.agrest.AgException; + +/** + * @since 5.0 + */ +public class ShortConverter extends AbstractConverter { + + private static final ShortConverter instance = new ShortConverter(); + + public static ShortConverter converter() { + return instance; + } + + @Override + protected Short valueNonNull(JsonNode node) { + + if (!node.isNumber()) { + throw AgException.badRequest("Expected numeric value, got: %s", node.asText()); + } + + int i = node.asInt(); + if (i < Short.MIN_VALUE || i > Short.MAX_VALUE) { + throw AgException.badRequest("Value is out of range for 'java.lang.Short': %s", node.asText()); + } + + return (short) i; + } +} diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/SqlDateConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/SqlDateConverter.java new file mode 100644 index 000000000..ec24e097b --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/SqlDateConverter.java @@ -0,0 +1,26 @@ +package io.agrest.converter.jsonvalue; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.sql.Date; +import java.time.LocalDate; + +/** + * @since 5.0 + */ +public class SqlDateConverter extends AbstractConverter { + + private static final SqlDateConverter instance = new SqlDateConverter(); + + public static SqlDateConverter converter() { + return instance; + } + + private SqlDateConverter() {} + + @Override + protected Date valueNonNull(JsonNode node) { + LocalDate date = LocalDate.parse(node.asText()); + return Date.valueOf(date); + } +} diff --git a/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/SqlTimeConverter.java b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/SqlTimeConverter.java new file mode 100644 index 000000000..871a27abc --- /dev/null +++ b/agrest-engine/src/main/java/io/agrest/converter/jsonvalue/SqlTimeConverter.java @@ -0,0 +1,35 @@ +package io.agrest.converter.jsonvalue; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.sql.Time; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; + +/** + * @since 5.0 + */ +public class SqlTimeConverter extends AbstractConverter