diff --git a/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/NestedEntity.java b/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/NestedEntity.java new file mode 100644 index 0000000000..277c9c24ff --- /dev/null +++ b/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/NestedEntity.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Haulmont. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jmix.samples.rest.entity; + +import io.jmix.core.entity.annotation.JmixGeneratedValue; +import io.jmix.core.metamodel.annotation.InstanceName; +import io.jmix.core.metamodel.annotation.JmixEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +@JmixEntity +@Table(name = "REST_NESTED_ENTITY") +@Entity(name = "rest_NestedEntity") +public class NestedEntity { + @JmixGeneratedValue + @Column(name = "ID", nullable = false) + @Id + private UUID id; + + @NotNull + @InstanceName + @Column(name = "NAME") + private String name; + + @NotNull + @Column(name = "CODE") + private String code; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } +} \ No newline at end of file diff --git a/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/RootEntity.java b/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/RootEntity.java new file mode 100644 index 0000000000..6cf4974f95 --- /dev/null +++ b/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/RootEntity.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Haulmont. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jmix.samples.rest.entity; + +import io.jmix.core.entity.annotation.JmixGeneratedValue; +import io.jmix.core.metamodel.annotation.InstanceName; +import io.jmix.core.metamodel.annotation.JmixEntity; +import io.jmix.core.validation.group.UiCrossFieldChecks; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +@JmixEntity +@Table(name = "REST_ROOT_ENTITY") +@Entity(name = "rest_RootEntity") +@ValidRootEntity(/*groups = {UiCrossFieldChecks.class}*/) +public class RootEntity { + @JmixGeneratedValue + @Column(name = "ID", nullable = false) + @Id + private UUID id; + + @NotNull + @InstanceName + @Column(name = "NAME") + private String name; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "NESTED_ENTITY_ID") + private NestedEntity nestedEntity; + + public NestedEntity getNestedEntity() { + return nestedEntity; + } + + public void setNestedEntity(NestedEntity nestedEntity) { + this.nestedEntity = nestedEntity; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } +} \ No newline at end of file diff --git a/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/ValidRootEntity.java b/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/ValidRootEntity.java new file mode 100644 index 0000000000..10c9796be8 --- /dev/null +++ b/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/ValidRootEntity.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Haulmont. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jmix.samples.rest.entity; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ValidRootEntityValidator.class) +public @interface ValidRootEntity { + + String message() default "Entity is not valid"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/ValidRootEntityValidator.java b/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/ValidRootEntityValidator.java new file mode 100644 index 0000000000..487f645faa --- /dev/null +++ b/jmix-rest/sample-rest/src/main/java/io/jmix/samples/rest/entity/ValidRootEntityValidator.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025 Haulmont. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.jmix.samples.rest.entity; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class ValidRootEntityValidator implements ConstraintValidator { + + @Override + public boolean isValid(RootEntity entity, ConstraintValidatorContext constraintValidatorContext) { + return entity.getNestedEntity().getCode().equals("test"); + } +} diff --git a/jmix-rest/sample-rest/src/test/java/entities/EntitiesControllerFT.java b/jmix-rest/sample-rest/src/test/java/entities/EntitiesControllerFT.java index 697f9339e3..6f9a4fdb14 100644 --- a/jmix-rest/sample-rest/src/test/java/entities/EntitiesControllerFT.java +++ b/jmix-rest/sample-rest/src/test/java/entities/EntitiesControllerFT.java @@ -2356,6 +2356,33 @@ public void jsonNullSearchFilterCondition() throws Exception { } } + @Test + void createComplexValidatedEntity() throws Exception { + Map replacements = new HashMap<>(); + UUID rootEntityId = dirtyData.createRootEntityId(); + replacements.put("$ROOT_ID$", rootEntityId.toString()); + UUID nestedEntityId = dirtyData.createNestedEntityId(); + replacements.put("$NESTED_ID$", nestedEntityId.toString()); + + String json = getFileContent("nestedEntity.json", replacements); + String url = baseUrl + "/entities/rest_NestedEntity"; + try (CloseableHttpResponse response = sendPost(url, oauthToken, json, null)) { + assertEquals(HttpStatus.SC_CREATED, statusCode(response)); + ReadContext ctx = parseResponse(response); + assertEquals("rest_NestedEntity", ctx.read("$._entityName")); + assertEquals(nestedEntityId.toString(), ctx.read("$.id")); + } + + json = getFileContent("rootEntity.json", replacements); + url = baseUrl + "/entities/rest_RootEntity"; + try (CloseableHttpResponse response = sendPost(url, oauthToken, json, null)) { + assertEquals(HttpStatus.SC_CREATED, statusCode(response)); + ReadContext ctx = parseResponse(response); + assertEquals("rest_RootEntity", ctx.read("$._entityName")); + assertEquals(rootEntityId.toString(), ctx.read("$.id")); + } + } + private void executePrepared(String sql, Object... params) throws SQLException { try (PreparedStatement stmt = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { diff --git a/jmix-rest/sample-rest/src/test/java/test_support/DataSet.java b/jmix-rest/sample-rest/src/test/java/test_support/DataSet.java index 4e50e82501..31984f4cc2 100644 --- a/jmix-rest/sample-rest/src/test/java/test_support/DataSet.java +++ b/jmix-rest/sample-rest/src/test/java/test_support/DataSet.java @@ -59,6 +59,8 @@ public class DataSet { private Set compositeKeyEntityIds = new HashSet<>(); private Set compositeKeyEntityTenantIds = new HashSet<>(); private Set nonStandardIdNameEntityIds = new HashSet<>(); + private Set rootEntityIds = new HashSet<>(); + private Set nestedEntityIds = new HashSet<>(); private static AtomicLong compositeKeyEntityIdGen = new AtomicLong(); private static AtomicInteger compositeKeyEntityTenantIdGen = new AtomicInteger(); @@ -205,6 +207,26 @@ public void cleanup(Connection conn) throws SQLException { deleteInstances(conn, "REST_SECRET_ENTITY", secretEntityIds); deleteStringInstances(conn, "REF_CURRENCY", "CODE", currencyIds); deleteNonStandardIdEntities(conn); + deleteRootEntities(conn); + deleteNestedEntities(conn); + } + + private void deleteRootEntities(Connection conn) throws SQLException { + try (PreparedStatement stmt = conn.prepareStatement("delete from rest_root_entity where id = ?")) { + for (UUID uuid : rootEntityIds) { + stmt.setObject(1, uuid); + stmt.executeUpdate(); + } + } + } + + private void deleteNestedEntities(Connection conn) throws SQLException { + try (PreparedStatement stmt = conn.prepareStatement("delete from rest_nested_entity where id = ?")) { + for (UUID uuid : nestedEntityIds) { + stmt.setObject(1, uuid); + stmt.executeUpdate(); + } + } } private void deleteSellers(Connection conn) throws SQLException { @@ -675,4 +697,16 @@ public UUID createSecretEntityId() { secretEntityIds.add(result); return result; } + + public UUID createRootEntityId() { + UUID result = UUID.randomUUID(); + rootEntityIds.add(result); + return result; + } + + public UUID createNestedEntityId() { + UUID result = UUID.randomUUID(); + nestedEntityIds.add(result); + return result; + } } diff --git a/jmix-rest/sample-rest/src/test/resources/test_support/data/service/nestedEntity.json b/jmix-rest/sample-rest/src/test/resources/test_support/data/service/nestedEntity.json new file mode 100644 index 0000000000..c9d8d2bed7 --- /dev/null +++ b/jmix-rest/sample-rest/src/test/resources/test_support/data/service/nestedEntity.json @@ -0,0 +1,5 @@ +{ + "id": "$NESTED_ID$", + "name": "Nested Entity 1", + "code": "test" +} \ No newline at end of file diff --git a/jmix-rest/sample-rest/src/test/resources/test_support/data/service/rootEntity.json b/jmix-rest/sample-rest/src/test/resources/test_support/data/service/rootEntity.json new file mode 100644 index 0000000000..22ba6ff1ed --- /dev/null +++ b/jmix-rest/sample-rest/src/test/resources/test_support/data/service/rootEntity.json @@ -0,0 +1,7 @@ +{ + "id": "$ROOT_ID$", + "name": "Entity 1", + "nestedEntity": { + "id": "$NESTED_ID$" + } +} \ No newline at end of file