Compare commits

...

2 Commits

17 changed files with 429 additions and 3 deletions

4
.gitignore vendored
View File

@ -46,3 +46,7 @@ api-sandbox/app/api.spec.json
/xcpdata.trace.db
web-ui/vue-app/TODO.md
web-ui/vue-app/src/generated/*
/api-sandbox/src/main/resources/htdocs/api-sandbox/
api-sandbox/app/dist/api.spec.json
api-sandbox/app/node/
api.spec.json

View File

@ -2,6 +2,7 @@ import $ from 'jquery'
let apiSpec = null
let currentUser = null
const apiEndpoint = "/api";
async function loadUserProfile() {
try {
@ -12,7 +13,7 @@ async function loadUserProfile() {
id: Date.now()
}
const response = await fetch('http://localhost:8080/api', {
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@ -215,7 +216,7 @@ async function sendRequest() {
$('#request').text('')
try {
const response = await fetch('http://localhost:8080/api', {
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'

View File

@ -5,7 +5,7 @@ import copy from 'rollup-plugin-copy'
export default defineConfig({
root: '.',
build: {
outDir: 'dist',
outDir: '../src/main/resources/htdocs/api-sandbox',
rollupOptions: {
plugins: [
copy({

View File

@ -17,4 +17,48 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>
<configuration>
<workingDirectory>app</workingDirectory>
<installDirectory>app</installDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v22.12.0</nodeVersion>
<npmVersion>10.8.2</npmVersion>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>npm build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -122,9 +122,28 @@ public class SpecGenerator {
continue;
}
for (var aClass : classAnnotation.directInheritors()) {
if (!types.contains(aClass)) {
types.add(aClass);
typesQueue.add(aClass);
}
}
var typeDescriptor = typesArray.addObject();
typeDescriptor.put("name", classAnnotation.alias().isEmpty() ? type.getSimpleName() : classAnnotation.alias());
typeDescriptor.put("type", "class");
var parents = typeDescriptor.putArray("parents");
for (var aClass : type.getInterfaces()) {
if (!types.contains(aClass)) {
types.add(aClass);
typesQueue.add(aClass);
}
if (!aClass.isAnnotationPresent(GenerateApiSpec.class)) {
continue;
}
parents.add(aClass.getSimpleName());
}
var fields = typeDescriptor.putArray("fields");
for (var method : type.getMethods()) {
var methodAnnotation = method.getAnnotation(GenerateApiSpec.class);

View File

@ -11,4 +11,6 @@ public @interface GenerateApiSpec {
String alias() default "";
Class<?> type() default void.class;
Class<?>[] directInheritors() default {};
}

View File

@ -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);
}

View File

@ -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> {
PropertyDescriptor getByName(String name);
}

View File

@ -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();
}
}

View File

@ -0,0 +1,6 @@
package ru.kirillius.XCP.Properties;
import java.util.Set;
public interface Constraints extends Set<Constraint> {
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
package ru.kirillius.XCP.Properties;
public enum PropertyType {
Number,
Boolean,
Text,
ValueList,
JSON
}

View File

@ -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;
}

View File

@ -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<String> values;
}

View File

@ -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<PropertyDescriptor> 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<Constraint> 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<Constraints, String> {
@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));
}
}
}

View File

@ -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<PropertyDescriptor, PropertyDescriptorRepositoryImpl> {
@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());
}
}

View File

@ -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));
}
}