From 3d6b3d8059d743e5e980d127a72b3c4acc9d59fc Mon Sep 17 00:00:00 2001 From: kirillius Date: Wed, 28 Jan 2026 22:52:12 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20?= =?UTF-8?q?=D0=B8=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Entities/PropertyDescriptor.java | 29 +++++ .../PropertyDescriptorRepository.java | 8 ++ .../kirillius/XCP/Properties/Constraint.java | 16 +++ .../kirillius/XCP/Properties/Constraints.java | 6 + .../XCP/Properties/NumberConstraint.java | 22 ++++ .../XCP/Properties/PropertyType.java | 9 ++ .../XCP/Properties/StringConstraint.java | 16 +++ .../XCP/Properties/ValueListConstraint.java | 18 +++ .../PropertyDescriptorRepositoryImpl.java | 120 ++++++++++++++++++ .../PropertyDescriptorRepositoryImplTest.java | 68 ++++++++++ .../XCP/RPC/Services/Properties.java | 44 +++++++ 11 files changed, 356 insertions(+) create mode 100644 api/src/main/java/ru/kirillius/XCP/Persistence/Entities/PropertyDescriptor.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepository.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Properties/Constraint.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Properties/Constraints.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Properties/NumberConstraint.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Properties/PropertyType.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Properties/StringConstraint.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Properties/ValueListConstraint.java create mode 100644 database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepositoryImpl.java create mode 100644 database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepositoryImplTest.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Properties.java diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/PropertyDescriptor.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/PropertyDescriptor.java new file mode 100644 index 0000000..cdc6637 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/PropertyDescriptor.java @@ -0,0 +1,29 @@ +package ru.kirillius.XCP.Persistence.Entities; + +import ru.kirillius.XCP.Commons.GenerateApiSpec; +import ru.kirillius.XCP.Persistence.PersistenceEntity; +import ru.kirillius.XCP.Properties.Constraints; +import ru.kirillius.XCP.Properties.PropertyType; + +@GenerateApiSpec +public interface PropertyDescriptor extends PersistenceEntity { + @GenerateApiSpec + Constraints getConstraints(); + + void setConstraints(Constraints constraints); + + @GenerateApiSpec + boolean isArray(); + + void setArray(boolean isArray); + + @GenerateApiSpec + PropertyType getPropertyType(); + + void setPropertyType(PropertyType propertyType); + + @GenerateApiSpec + String getName(); + + void setName(String name); +} diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepository.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepository.java new file mode 100644 index 0000000..ed00ffe --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepository.java @@ -0,0 +1,8 @@ +package ru.kirillius.XCP.Persistence.Repositories; + +import ru.kirillius.XCP.Persistence.Entities.PropertyDescriptor; +import ru.kirillius.XCP.Persistence.Repository; + +public interface PropertyDescriptorRepository extends Repository { + PropertyDescriptor getByName(String name); +} diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/Constraint.java b/api/src/main/java/ru/kirillius/XCP/Properties/Constraint.java new file mode 100644 index 0000000..d6b0ae4 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Properties/Constraint.java @@ -0,0 +1,16 @@ +package ru.kirillius.XCP.Properties; + +import com.fasterxml.jackson.annotation.JsonProperty; +import ru.kirillius.XCP.Commons.GenerateApiSpec; + +@GenerateApiSpec(directInheritors = { + NumberConstraint.class, + ValueListConstraint.class, + StringConstraint.class +}) +public interface Constraint { + @JsonProperty(value = "type") + default String type() { + return getClass().getSimpleName(); + } +} diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/Constraints.java b/api/src/main/java/ru/kirillius/XCP/Properties/Constraints.java new file mode 100644 index 0000000..85d12d0 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Properties/Constraints.java @@ -0,0 +1,6 @@ +package ru.kirillius.XCP.Properties; + +import java.util.Set; + +public interface Constraints extends Set { +} diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/NumberConstraint.java b/api/src/main/java/ru/kirillius/XCP/Properties/NumberConstraint.java new file mode 100644 index 0000000..64849cd --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Properties/NumberConstraint.java @@ -0,0 +1,22 @@ +package ru.kirillius.XCP.Properties; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import ru.kirillius.XCP.Commons.GenerateApiSpec; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@GenerateApiSpec +@NoArgsConstructor +public final class NumberConstraint implements Constraint { + @JsonProperty + private double min; + @JsonProperty + private double max; + @JsonProperty + private double step; + @JsonProperty + private boolean integer; +} diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/PropertyType.java b/api/src/main/java/ru/kirillius/XCP/Properties/PropertyType.java new file mode 100644 index 0000000..6f66e46 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Properties/PropertyType.java @@ -0,0 +1,9 @@ +package ru.kirillius.XCP.Properties; + +public enum PropertyType { + Number, + Boolean, + Text, + ValueList, + JSON +} diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/StringConstraint.java b/api/src/main/java/ru/kirillius/XCP/Properties/StringConstraint.java new file mode 100644 index 0000000..bde17e8 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Properties/StringConstraint.java @@ -0,0 +1,16 @@ +package ru.kirillius.XCP.Properties; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import ru.kirillius.XCP.Commons.GenerateApiSpec; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@GenerateApiSpec +@NoArgsConstructor +public final class StringConstraint implements Constraint { + @JsonProperty + private int maxLength; +} diff --git a/api/src/main/java/ru/kirillius/XCP/Properties/ValueListConstraint.java b/api/src/main/java/ru/kirillius/XCP/Properties/ValueListConstraint.java new file mode 100644 index 0000000..56840fe --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Properties/ValueListConstraint.java @@ -0,0 +1,18 @@ +package ru.kirillius.XCP.Properties; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; +import ru.kirillius.XCP.Commons.GenerateApiSpec; + +import java.util.Collection; + +@Getter +@Setter +@Builder +@AllArgsConstructor +@GenerateApiSpec +@NoArgsConstructor +public final class ValueListConstraint implements Constraint { + @JsonProperty + private Collection values; +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepositoryImpl.java new file mode 100644 index 0000000..8dbfca6 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepositoryImpl.java @@ -0,0 +1,120 @@ +package ru.kirillius.XCP.Persistence.Repositories; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; +import lombok.*; +import ru.kirillius.XCP.Commons.GenerateApiSpec; +import ru.kirillius.XCP.Persistence.AbstractEntity; +import ru.kirillius.XCP.Persistence.AbstractRepository; +import ru.kirillius.XCP.Persistence.Entities.PropertyDescriptor; +import ru.kirillius.XCP.Persistence.EntityImplementation; +import ru.kirillius.XCP.Persistence.RepositoryServiceImpl; +import ru.kirillius.XCP.Properties.Constraint; +import ru.kirillius.XCP.Properties.Constraints; +import ru.kirillius.XCP.Properties.PropertyType; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ArrayNode; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; + +@EntityImplementation(PropertyDescriptorRepositoryImpl.PropertyDescriptorEntity.class) +public class PropertyDescriptorRepositoryImpl extends AbstractRepository implements PropertyDescriptorRepository { + + public PropertyDescriptorRepositoryImpl(RepositoryServiceImpl repositoryService) { + super(repositoryService); + } + + @Override + public PropertyDescriptor getByName(String name) { + try (var request = buildQueryParametrized("WHERE name = ?1", name)) { + return request.get().findFirst().orElse(null); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Entity + @Table(name = "PropertyDescriptors") + @Builder + @AllArgsConstructor + @NoArgsConstructor + @Getter + @Setter + public static class PropertyDescriptorEntity extends AbstractEntity implements PropertyDescriptor { + + @Column(nullable = false, unique = true) + @JsonProperty + private String name = ""; + + @Getter + @Setter + @JsonIgnore + @Column(nullable = false, name = "constraints_set") + @Convert(converter = ConstraintsConverter.class) + private Constraints constraints = new ConstraintsImpl(); + + @Transient + @JsonProperty("constraints") + ArrayNode getConstraintsSerialized() { + return ConstraintsImpl.serialize(constraints); + } + + @Transient + @JsonProperty("constraints") + void setConstraintsSerialized(ArrayNode array) { + constraints = ConstraintsImpl.deserialize(array); + } + + @Getter + @Setter + @JsonProperty + @Column(name = "is_array") + private boolean array; + + @Enumerated(EnumType.STRING) + @Getter + @Setter + @JsonProperty + @Column(nullable = false) + private PropertyType propertyType = PropertyType.Text; + } + + private final static ObjectMapper mapper = new ObjectMapper(); + + private static final class ConstraintsImpl extends HashSet implements Constraints { + + //TODO перенести в отдельный класс сериализации + static ArrayNode serialize(Constraints constraints) { + var array = mapper.createArrayNode(); + constraints.forEach(c -> array.add(mapper.valueToTree(c))); + return array; + } + + static Constraints deserialize(ArrayNode array) { + var inheritors = Constraint.class.getAnnotation(GenerateApiSpec.class).directInheritors(); + var constraints = new ConstraintsImpl(); + array.forEach(node -> { + var typeName = node.get("type").asString(); + var type = Arrays.stream(inheritors).filter(cls -> cls.getSimpleName().equalsIgnoreCase(typeName)).findFirst().orElseThrow(() -> new RuntimeException("Invalid type: " + typeName)); + constraints.add((Constraint) mapper.convertValue(node, type)); + }); + return constraints; + } + } + + public final static class ConstraintsConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(Constraints constraints) { + return mapper.writeValueAsString(ConstraintsImpl.serialize(constraints)); + } + + @Override + public Constraints convertToEntityAttribute(String s) { + return ConstraintsImpl.deserialize((ArrayNode) mapper.readTree(s)); + } + } +} diff --git a/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepositoryImplTest.java b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepositoryImplTest.java new file mode 100644 index 0000000..01181c0 --- /dev/null +++ b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/PropertyDescriptorRepositoryImplTest.java @@ -0,0 +1,68 @@ +package ru.kirillius.XCP.Persistence.Repositories; + +import org.junit.jupiter.api.Test; +import ru.kirillius.XCP.Persistence.Entities.PropertyDescriptor; +import ru.kirillius.XCP.Persistence.PersistenceException; +import ru.kirillius.XCP.Properties.NumberConstraint; +import ru.kirillius.XCP.Properties.StringConstraint; +import ru.kirillius.XCP.Services.RepositoryService; + +import java.io.IOException; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class PropertyDescriptorRepositoryImplTest extends GenericRepositoryTest { + + @Test + void saveSameName() throws IOException { + + try (var service = spawnRepositoryService()) { + var repository = service.getRepository(PropertyDescriptorRepository.class); + + var propertyDescriptor = repository.create(); + propertyDescriptor.setName("test"); + repository.save(propertyDescriptor); + + propertyDescriptor = repository.create(); + propertyDescriptor.setName("test"); + try { + repository.save(propertyDescriptor); + throw new AssertionError("Should have thrown an exception"); + } catch (Throwable t) { + assertThat(t).isInstanceOf(PersistenceException.class); + } + } + } + + @Test + void getByName() throws IOException { + var correct = "correctName"; + try (var service = spawnRepositoryService()) { + var repository = service.getRepository(PropertyDescriptorRepository.class); + for (var i = 0; i < 10; i++) { + var propertyDescriptor = repository.create(); + propertyDescriptor.setName("incorrect" + UUID.randomUUID()); + repository.save(propertyDescriptor); + } + var correctDescriptor = repository.create(); + correctDescriptor.setName(correct); + repository.save(correctDescriptor); + for (var i = 0; i < 10; i++) { + var propertyDescriptor = repository.create(); + propertyDescriptor.setName("incorrect" + UUID.randomUUID()); + repository.save(propertyDescriptor); + } + + var loaded = repository.getByName(correct); + assertThat(loaded).isNotNull().isEqualTo(correctDescriptor); + } + } + + @Override + protected void modify(PropertyDescriptor entity, RepositoryService service) { + entity.setName("test" + UUID.randomUUID()); + entity.getConstraints().add(NumberConstraint.builder().build()); + entity.getConstraints().add(StringConstraint.builder().build()); + } +} \ No newline at end of file diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Properties.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Properties.java new file mode 100644 index 0000000..fd614bf --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Properties.java @@ -0,0 +1,44 @@ +package ru.kirillius.XCP.RPC.Services; + +import ru.kirillius.XCP.Persistence.Entities.PropertyDescriptor; +import ru.kirillius.XCP.Persistence.Repositories.PropertyDescriptorRepository; +import ru.kirillius.XCP.RPC.JSONRPC.CallContext; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService; +import ru.kirillius.XCP.Security.UserRole; +import ru.kirillius.XCP.Services.RepositoryService; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; + +import java.io.IOException; + +public class Properties extends JsonRpcService { + @JsonRpcMethod( + accessLevel = UserRole.User, + description = "Get all properties as list", + parameters = {}, + returnType = PropertyDescriptor[].class + ) + public ArrayNode getAll(CallContext call) throws IOException { + var repository = call.getContext().getService(RepositoryService.class).getRepository(PropertyDescriptorRepository.class); + try (var handler = repository.getAll()) { + return repository.serialize(handler.get().toList()); + } + } + + @JsonRpcMethod( + accessLevel = UserRole.User, + description = "Load property descriptor object by name", + parameters = { + @JsonRpcMethod.Parameter(name = "name", description = "property name", type = String.class) + }, + returnType = PropertyDescriptor.class + ) + public ObjectNode getByName(CallContext call) { + var name = requireParam(call, "name", JsonNode::asString); + var repository = call.getContext().getService(RepositoryService.class).getRepository(PropertyDescriptorRepository.class); + return repository.serialize(repository.getByName(name)); + } + +}