Skip to content

Commit

Permalink
Multi-column ObjectMapper #676
Browse files Browse the repository at this point in the history
  • Loading branch information
andrus committed Jun 7, 2024
1 parent 213f73d commit b7b6d34
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 41 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* #660 Builders: auto-detect multi-column IDs
* #662 Update expression parser with JavaCC 7.0.13
* #673 OpenAPI should have an "array of string" schema for "include" and "exclude"
* #676 Multi-column ObjectMapper
* #677 Upgrading JUnit to 5.10.2
* #678 Upgrading Swagger to 2.2.2
* #679 Upgrading Jackson to 2.15.4
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.agrest.cayenne.processor.update.stage;

import io.agrest.AgException;
import io.agrest.id.AgObjectId;
import io.agrest.EntityUpdate;
import io.agrest.ObjectMapper;
import io.agrest.ObjectMapperFactory;
Expand All @@ -13,9 +12,9 @@
import io.agrest.cayenne.processor.CayenneProcessor;
import io.agrest.cayenne.processor.CayenneRelatedResourceEntityExt;
import io.agrest.cayenne.processor.ICayenneQueryAssembler;
import io.agrest.id.AgObjectId;
import io.agrest.meta.AgIdPart;
import io.agrest.protocol.Exp;
import io.agrest.runtime.processor.update.ByIdObjectMapperFactory;
import io.agrest.runtime.processor.update.ChangeOperation;
import io.agrest.runtime.processor.update.ChangeOperationType;
import io.agrest.runtime.processor.update.UpdateContext;
Expand Down Expand Up @@ -112,7 +111,7 @@ protected <T extends DataObject> void collectCreateOps(
protected <T extends DataObject> ObjectMapper<T> createObjectMapper(UpdateContext<T> context) {
ObjectMapperFactory mapper = context.getMapper() != null
? context.getMapper()
: ByIdObjectMapperFactory.mapper();
: ObjectMapperFactory.matchById();
return mapper.createMapper(context);
}

Expand Down
118 changes: 118 additions & 0 deletions agrest-cayenne/src/test/java/io/agrest/cayenne/PUT/MapperIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package io.agrest.cayenne.PUT;

import io.agrest.DataResponse;
import io.agrest.cayenne.cayenne.main.E1;
import io.agrest.cayenne.cayenne.main.E23;
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.QueryParam;
import jakarta.ws.rs.core.Configuration;
import jakarta.ws.rs.core.Context;
import org.junit.jupiter.api.Test;

import java.util.List;

public class MapperIT extends MainDbTest {

@BQTestTool
static final MainModelTester tester = tester(Resource.class)
.entitiesAndDependencies(E1.class, E23.class)
.build();


@Test
public void implicitIdMapper() {

tester.e23().insertColumns("id", "name")
.values(56, "N1")
.values(54, "N2").exec();

tester.target("/id")
.queryParam("include", "exposedId", "name")
.put("[ {\"exposedId\":58,\"name\":\"N4\"}, {\"exposedId\":56,\"name\":\"N3\"} ]")
.wasOk()
.bodyEquals(2, "{\"exposedId\":58,\"name\":\"N4\"},{\"exposedId\":56,\"name\":\"N3\"}");

tester.e23().matcher().assertMatches(3);
tester.e23().matcher().eq("id", 58).andEq("name", "N4").assertOneMatch();
tester.e23().matcher().eq("id", 56).andEq("name", "N3").assertOneMatch();
}

@Test
public void propertyMapper() {

tester.e1().insertColumns("id", "name", "description")
.values(56, "N1", "D1")
.values(54, "N2", "D2").exec();

tester.target("/prop")
.queryParam("include", "name", "description")
.put("[ {\"name\":\"N4\",\"description\":\"D4\"}, {\"name\":\"N1\",\"description\":\"D3\"} ]")
.wasOk()
.bodyEquals(2, "{\"description\":\"D4\",\"name\":\"N4\"},{\"description\":\"D3\",\"name\":\"N1\"}");

tester.e1().matcher().assertMatches(3);
tester.e1().matcher().eq("name", "N1").andEq("description", "D3").assertOneMatch();
tester.e1().matcher().eq("name", "N2").andEq("description", "D2").assertOneMatch();
tester.e1().matcher().eq("name", "N4").andEq("description", "D4").assertOneMatch();
}

@Test
public void propertiesMapper() {

tester.e1().insertColumns("id", "name", "age", "description")
.values(56, "N1", 1, "D1")
.values(55, "N1", 2, "D2")
.values(54, "N2", 2, "D3").exec();

tester.target("/props")
.queryParam("include", "name", "age", "description")
.put("[{\"age\":3,\"description\":\"D4\",\"name\":\"N1\"}, {\"age\":2,\"description\":\"D5\",\"name\":\"N1\"}]")
.wasOk()
.bodyEquals(2, "{\"age\":3,\"description\":\"D4\",\"name\":\"N1\"},{\"age\":2,\"description\":\"D5\",\"name\":\"N1\"}");

tester.e1().matcher().assertMatches(4);
tester.e1().matcher().eq("name", "N1").andEq("age", 1).andEq("description", "D1").assertOneMatch();
tester.e1().matcher().eq("name", "N1").andEq("age", 2).andEq("description", "D5").assertOneMatch();
tester.e1().matcher().eq("name", "N2").andEq("age", 2).andEq("description", "D3").assertOneMatch();
tester.e1().matcher().eq("name", "N1").andEq("age", 3).andEq("description", "D4").assertOneMatch();

}

@Path("")
public static class Resource {

@Context
private Configuration config;

@PUT
@Path("id")
public DataResponse<E23> implicitIdMapper(@QueryParam("include") List<String> includes, String entityData) {
return AgJaxrs.idempotentCreateOrUpdate(E23.class, config)
.request(AgJaxrs.request(config).addIncludes(includes).build())
.syncAndSelect(entityData);
}

@PUT
@Path("prop")
public DataResponse<E1> propertyMapper(@QueryParam("include") List<String> includes, String entityData) {
return AgJaxrs.idempotentCreateOrUpdate(E1.class, config)
.mapper(E1.NAME.getName())
.request(AgJaxrs.request(config).addIncludes(includes).build())
.syncAndSelect(entityData);
}

@PUT
@Path("props")
public DataResponse<E1> propertiesMapper(@QueryParam("include") List<String> includes, String entityData) {
return AgJaxrs.idempotentCreateOrUpdate(E1.class, config)
.mapper(E1.NAME.getName(), E1.AGE.getName())
.request(AgJaxrs.request(config).addIncludes(includes).build())
.syncAndSelect(entityData);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.agrest.cayenne.PUT;

import io.agrest.DataResponse;
import io.agrest.ObjectMapperFactory;
import io.agrest.cayenne.cayenne.main.E14;
import io.agrest.cayenne.cayenne.main.E15;
import io.agrest.cayenne.cayenne.main.E3;
Expand All @@ -9,15 +10,13 @@
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 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 org.junit.jupiter.api.Test;

public class Parent_MapperIT extends MainDbTest {

Expand All @@ -38,7 +37,7 @@ public void relate_ToMany_MixedCollection() {
.values(8, "yyy", 15)
.values(9, "aaa", 15).exec();

tester.target("/e8/bykey/15/e7s")
tester.target("/e8/custommapper/15/e7s")
.put("[ {\"name\":\"newname\"}, {\"name\":\"aaa\"} ]")
.wasOk()
.replaceId("XID")
Expand All @@ -48,7 +47,7 @@ public void relate_ToMany_MixedCollection() {

// testing idempotency

tester.target("/e8/bykey/15/e7s")
tester.target("/e8/custommapper/15/e7s")
.put("[ {\"name\":\"newname\"}, {\"name\":\"aaa\"} ]")
.wasOk().replaceId("XID")
.bodyEquals(2,
Expand All @@ -70,7 +69,7 @@ public void relate_ToMany_PropertyMapper() {
.values(8, "yyy", 15)
.values(9, "aaa", 15).exec();

tester.target("/e8/bypropkey/15/e7s")
tester.target("/e8/propmapper/15/e7s")
.put("[ {\"name\":\"newname\"}, {\"name\":\"aaa\"} ]")
.wasOk().replaceId("XID")
.bodyEquals(2, "{\"id\":XID,\"name\":\"newname\"},{\"id\":9,\"name\":\"aaa\"}");
Expand Down Expand Up @@ -110,16 +109,16 @@ public static class Resource {
private Configuration config;

@PUT
@Path("e8/bykey/{id}/e7s")
@Path("e8/custommapper/{id}/e7s")
public DataResponse<E7> e8CreateOrUpdateE7sByKey_Idempotent(@PathParam("id") int id, String entityData) {
return AgJaxrs.idempotentCreateOrUpdate(E7.class, config)
.mapper(ByKeyObjectMapperFactory.byKey(E7.NAME.getName()))
.mapper(ObjectMapperFactory.matchByProperties(E7.NAME.getName()))
.parent(E8.class, id, E8.E7S.getName())
.syncAndSelect(entityData);
}

@PUT
@Path("e8/bypropkey/{id}/e7s")
@Path("e8/propmapper/{id}/e7s")
public DataResponse<E7> e8CreateOrUpdateE7sByPropKey_Idempotent(@PathParam("id") int id, String entityData) {
return AgJaxrs.idempotentCreateOrUpdate(E7.class, config)
.mapper(E7.NAME.getName())
Expand Down
36 changes: 30 additions & 6 deletions agrest-engine/src/main/java/io/agrest/ObjectMapperFactory.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
package io.agrest;

import io.agrest.runtime.processor.update.ByIdObjectMapperFactory;
import io.agrest.runtime.processor.update.ByPropertiesObjectMapperFactory;
import io.agrest.runtime.processor.update.ByPropertyObjectMapperFactory;
import io.agrest.runtime.processor.update.UpdateContext;

/**
* A strategy for mapping update operations to existing objects.
*
* A factory of a strategy for mapping update operations to existing objects.
*
* @since 1.7
*/
public interface ObjectMapperFactory {

/**
* Returns a mapper to handle objects of a given response.
*/
<T> ObjectMapper<T> createMapper(UpdateContext<T> context);
/**
* @since 5.0
*/
static ObjectMapperFactory matchById() {
return ByIdObjectMapperFactory.mapper();
}

/**
* @since 5.0
*/
static ObjectMapperFactory matchByProperties(String... properties) {
switch (properties.length) {
case 0:
throw new IllegalArgumentException("No properties specified for ObjectMapperFactory");
case 1:
return new ByPropertyObjectMapperFactory(properties[0]);
default:
return new ByPropertiesObjectMapperFactory(properties);
}
}

/**
* Returns a mapper to handle objects of a given response.
*/
<T> ObjectMapper<T> createMapper(UpdateContext<T> context);
}
9 changes: 4 additions & 5 deletions agrest-engine/src/main/java/io/agrest/UpdateBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import io.agrest.processor.Processor;
import io.agrest.processor.ProcessorOutcome;
import io.agrest.protocol.ControlParams;
import io.agrest.runtime.processor.update.ByKeyObjectMapperFactory;
import io.agrest.runtime.processor.update.UpdateContext;

import java.util.Collection;
Expand Down Expand Up @@ -147,8 +146,8 @@ default <A> UpdateBuilder<T> deleteAuthorizer(Class<A> entityType, DeleteAuthori
<A> UpdateBuilder<T> entityOverlay(AgEntityOverlay<A> overlay);

/**
* Sets a custom mapper that locates existing objects based on request data.
* If not set, objects will be located by their IDs.
* Sets a custom mapper that locates existing objects based on request data. If not set explicitly, objects will
* be matched by their IDs.
*/
UpdateBuilder<T> mapper(ObjectMapperFactory mapper);

Expand All @@ -158,8 +157,8 @@ default <A> UpdateBuilder<T> deleteAuthorizer(Class<A> entityType, DeleteAuthori
*
* @since 1.20
*/
default UpdateBuilder<T> mapper(String propertyName) {
return mapper(ByKeyObjectMapperFactory.byKey(propertyName));
default UpdateBuilder<T> mapper(String... propertyNames) {
return mapper(ObjectMapperFactory.matchByProperties(propertyNames));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import java.util.Map;
import java.util.Objects;

class ByIdObjectMapper<T> implements ObjectMapper<T> {
public class ByIdObjectMapper<T> implements ObjectMapper<T> {

private final AgEntity<T> entity;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import io.agrest.ObjectMapperFactory;

/**
* A default singleton implementation of the {@link ObjectMapperFactory} that
* looks up objects based on their IDs.
* A default implementation of the {@link ObjectMapperFactory} that looks up objects based on their IDs.
*
* @since 1.4
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@
* that have a unique property within parent.
*
* @since 1.4
* @deprecated in favor of {@link ByPropertyObjectMapperFactory}
*/
@Deprecated(since = "5.0", forRemoval = true)
public class ByKeyObjectMapperFactory implements ObjectMapperFactory {

private String property;
private final String property;

/**
* @deprecated in favor of {@link ObjectMapperFactory#matchByProperties(String...)}
*/
public static ByKeyObjectMapperFactory byKey(String key) {
return new ByKeyObjectMapperFactory(key);
}
Expand All @@ -30,6 +35,6 @@ public <T> ObjectMapper<T> createMapper(UpdateContext<T> context) {

// TODO: should we account for "id" attributes here?
AgAttribute attribute = entity.getAttribute(property);
return new ByKeyObjectMapper<>(attribute);
return new ByPropertyObjectMapper<>(attribute);
}
}
Loading

0 comments on commit b7b6d34

Please sign in to comment.