WIP: serialization tests, entity references

This commit is contained in:
kirillius 2026-01-01 10:59:02 +03:00
parent f50fd2a474
commit b264b38fe4
23 changed files with 673 additions and 90 deletions

View File

@ -4,5 +4,10 @@ import ru.kirillius.XCP.Persistence.NodeEntity;
public interface Group extends NodeEntity {
String getIcon();
void setIcon(String icon);
boolean isPrototype();
void setPrototype(boolean prototype);
}

View File

@ -0,0 +1,9 @@
package ru.kirillius.XCP.Persistence.Entities;
import ru.kirillius.XCP.Persistence.PersistenceEntity;
public interface Tag extends PersistenceEntity {
String getName();
void setName(String name);
}

View File

@ -1,6 +1,7 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import tools.jackson.databind.node.ObjectNode;
import java.util.Set;
@ -10,9 +11,9 @@ public interface NodeEntity extends PersistenceEntity {
void setName(String name);
boolean isEssential();
boolean isProtectedEntity();
void setEssential(boolean essential);
void setProtectedEntity(boolean essential);
boolean isEnabled();
@ -26,7 +27,7 @@ public interface NodeEntity extends PersistenceEntity {
void setProperties(ObjectNode properties);
Set<String> getTags();
Set<Tag> getTags();
void setTags(Set<String> tags);
void setTags(Set<Tag> tags);
}

View File

@ -2,17 +2,14 @@ package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Commons.StreamHandler;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import java.util.Collection;
public interface NodeRepository<E extends NodeEntity> extends Repository<E> {
StreamHandler<E> getByGroup(Group group);
StreamHandler<E> getByTags(Collection<String> tags, TagSearchMode searchMode);
StreamHandler<E> getByTags(Collection<Tag> tags);
enum TagSearchMode {
MatchAnyTag,
MatchAllTags,
MatchExactTags
}
}

View File

@ -6,6 +6,4 @@ public interface PersistenceEntity {
long getId();
UUID getUuid();
Class<? extends PersistenceEntity> getBaseType();
}

View File

@ -2,13 +2,12 @@ package ru.kirillius.XCP.Persistence.Repositories;
import ru.kirillius.XCP.Commons.StreamHandler;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Input;
import ru.kirillius.XCP.Persistence.NodeRepository;
public interface GroupRepository extends NodeRepository<Input> {
StreamHandler<Group> getChildrenOf(Group dataGroup);
public interface GroupRepository extends NodeRepository<Group> {
StreamHandler<Group> getChildrenOf(Group group);
StreamHandler<Group> getChildrenRecursiveOf(Group dataGroup);
StreamHandler<Group> getAllChildrenInHierarchy(Group group);
Group getRoot();
}

View File

@ -0,0 +1,14 @@
package ru.kirillius.XCP.Persistence.Repositories;
import ru.kirillius.XCP.Commons.ResourceHandler;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import ru.kirillius.XCP.Persistence.Repository;
import java.util.Collection;
import java.util.stream.Stream;
public interface TagRepository extends Repository<Tag> {
Tag getByName(String name);
ResourceHandler<Stream<Tag>> getByNames(Collection<String> names);
}

View File

@ -6,4 +6,10 @@ public interface RepositoryService extends Service {
<E extends PersistenceEntity> Repository<E> getRepositoryForEntity(Class<E> entityType);
<R extends Repository<?>> R getRepository(Class<R> repositoryType);
Class<? extends PersistenceEntity> getEntityBaseType(Class<? extends PersistenceEntity> entityClass);
Class<? extends Repository<?>> getRepositoryBaseType(Class<? extends Repository<?>> repositoryClass);
Class<? extends PersistenceEntity> getRepositoryEntityType(Class<? extends Repository<?>> repositoryClass);
}

View File

@ -1,6 +1,5 @@
package ru.kirillius.XCP.Persistence;
import lombok.Getter;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
@ -12,7 +11,6 @@ import tools.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.List;
import java.util.StringJoiner;
@ -20,28 +18,16 @@ import java.util.UUID;
import java.util.stream.Stream;
public abstract class AbstractRepository<E extends PersistenceEntity> implements Repository<E> {
protected final Class<? extends E> entityImplementationClass;
private final Repository.EventBindings<E> eventBindings = new EventBindingsImpl<>();
@Getter
protected Class<? extends E> entityClass;
protected final Class<? extends E> entityImplementationClass;
protected RepositoryServiceImpl repositoryService;
protected String tableName;
public AbstractRepository(RepositoryServiceImpl repositoryService) {
var thisClass = getClass();
if (!thisClass.isAnnotationPresent(EntityImplementation.class)) {
throw new IllegalStateException("Unable to find @" + EntityImplementation.class.getSimpleName() + " in class " + thisClass.getSimpleName());
}
this.repositoryService = repositoryService;
entityClass = getGenericParameterType();
var implClass = thisClass.getAnnotation(EntityImplementation.class).value();
if (!entityClass.isAssignableFrom(implClass)) {
throw new IllegalStateException("Class " + implClass.getSimpleName() + " should implement " + entityClass.getSimpleName());
}
//noinspection unchecked
entityImplementationClass = (Class<? extends E>) thisClass.getAnnotation(EntityImplementation.class).value();
tableName = entityImplementationClass.getName();
@ -58,8 +44,6 @@ public abstract class AbstractRepository<E extends PersistenceEntity> implements
}
}
public abstract Class<? extends Repository<E>> getBaseClass();
@Override
public StreamHandler<E> search(String query, Collection<Object> queryParameters) {
return buildQuery(query, queryParameters.toArray());
@ -195,21 +179,6 @@ public abstract class AbstractRepository<E extends PersistenceEntity> implements
return repositoryService.getMapper().treeToValue(array, repositoryService.getMapper().getTypeFactory().constructCollectionType(List.class, entityImplementationClass));
}
@SuppressWarnings("unchecked")
private Class<E> getGenericParameterType() {
try {
var parameterizedType = (ParameterizedType) getClass().getGenericSuperclass();
var typeArguments = parameterizedType.getActualTypeArguments();
if (typeArguments.length != 1) {
throw new IllegalStateException("Generic parameters count is unsupported");
}
return (Class<E>) typeArguments[0];
} catch (Exception e) {
throw new RuntimeException("Failed to determine service generic parameters for Service: " + this.getClass().getName(), e);
}
}
protected String joinIdentifiers(Collection<Long> ids) {
var joiner = new StringJoiner(", ");

View File

@ -0,0 +1,9 @@
package ru.kirillius.XCP.Persistence;
import java.util.concurrent.atomic.AtomicReference;
public class EntityReference extends AtomicReference<PersistenceEntity> {
public EntityReference(PersistenceEntity initialValue) {
super(initialValue);
}
}

View File

@ -3,28 +3,36 @@ package ru.kirillius.XCP.Persistence;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.deser.std.StdDeserializer;
public class EntityReferenceDeserializer extends ValueDeserializer<PersistenceEntity> {
import java.util.UUID;
public class EntityReferenceDeserializer extends StdDeserializer<EntityReference> {
private final RepositoryServiceImpl repositoryService;
public EntityReferenceDeserializer(RepositoryServiceImpl repositoryService) {
super(EntityReference.class);
this.repositoryService = repositoryService;
}
// @Override
// public void serialize(PersistenceEntity value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException {
// gen.writeStartObject();
// gen.writeStringProperty("type", value.getBaseType().getName());
// gen.writeNumberProperty("id", value.getId());
// gen.writeStringProperty("uuid", value.getUUID().toString());
// gen.writeEndObject();
// }
@Override
public PersistenceEntity deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
public EntityReference deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
var node = ctxt.readTree(p);
var type = node.get("type").asString();
var id = node.get("id").asLong();
var uuid = node.get("uuid").asString();
try {
@SuppressWarnings("unchecked")
var repository = repositoryService.getRepositoryForEntity((Class<? extends PersistenceEntity>) Class.forName(type));
if (uuid != null) {
return new EntityReference(repository.load(UUID.fromString(uuid)));
}
return new EntityReference(repository.load(id));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
return null;
}
}

View File

@ -5,10 +5,12 @@ import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ser.std.StdSerializer;
public class EntityReferenceSerializer extends StdSerializer<PersistenceEntity> {
public class EntityReferenceSerializer extends StdSerializer<EntityReference> {
private RepositoryServiceImpl repositoryService;
public EntityReferenceSerializer() {
this(PersistenceEntity.class);
public EntityReferenceSerializer(RepositoryServiceImpl repositoryService) {
super(EntityReference.class);
this.repositoryService = repositoryService;
}
protected EntityReferenceSerializer(Class<PersistenceEntity> t) {
@ -16,9 +18,15 @@ public class EntityReferenceSerializer extends StdSerializer<PersistenceEntity>
}
@Override
public void serialize(PersistenceEntity value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException {
public void serialize(EntityReference reference, JsonGenerator gen, SerializationContext provider) throws JacksonException {
var value = reference.get();
if(value == null) {
gen.writeNull();
return;
}
gen.writeStartObject();
gen.writeStringProperty("type", value.getBaseType().getName());
var baseType = repositoryService.getEntityBaseType(value.getClass());
gen.writeStringProperty("type", baseType.getName());
gen.writeNumberProperty("id", value.getId());
gen.writeStringProperty("uuid", value.getUuid().toString());
gen.writeEndObject();

View File

@ -29,7 +29,7 @@ class PersistenceSerializationModule extends JacksonModule {
@Override
public void setupModule(SetupContext context) {
context.addSerializers(new SimpleSerializers(List.of(new EntityReferenceSerializer())));
context.addDeserializers(new SimpleDeserializers(Map.of(PersistenceEntity.class, new EntityReferenceDeserializer(repositoryService))));
context.addSerializers(new SimpleSerializers(List.of(new EntityReferenceSerializer(repositoryService))));
context.addDeserializers(new SimpleDeserializers(Map.of(EntityReference.class, new EntityReferenceDeserializer(repositoryService))));
}
}

View File

@ -0,0 +1,41 @@
package ru.kirillius.XCP.Persistence.Repositories;
import org.hibernate.query.Query;
import ru.kirillius.XCP.Commons.StreamHandler;
import ru.kirillius.XCP.Persistence.AbstractRepository;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import ru.kirillius.XCP.Persistence.NodeEntity;
import ru.kirillius.XCP.Persistence.NodeRepository;
import ru.kirillius.XCP.Persistence.RepositoryServiceImpl;
import java.util.Collection;
import java.util.List;
public abstract class AbstractNodeRepository<E extends NodeEntity> extends AbstractRepository<E> implements NodeRepository<E> {
public AbstractNodeRepository(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@Override
public StreamHandler<E> getByGroup(Group group) {
return search("WHERE group = ?1", List.of(group));
}
@Override
public StreamHandler<E> getByTags(Collection<Tag> tags) {
var hql = "SELECT n FROM " + tableName + " n JOIN n.tags t " +
"WHERE t.name IN :tagNames " +
"GROUP BY n " +
"HAVING COUNT(DISTINCT t.name) = :tagCount";
var session = repositoryService.openSession();
var transaction = session.beginTransaction();
var query = (Query<E>) session.createQuery(hql, entityImplementationClass);
query.setParameter("tagNames", tags);
query.setParameter("tagCount", tags.size());
return new ResourceHandlerImpl<>(query, transaction, session);
}
}

View File

@ -0,0 +1,171 @@
package ru.kirillius.XCP.Persistence.Repositories;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.UuidGenerator;
import ru.kirillius.XCP.Commons.StreamHandler;
import ru.kirillius.XCP.Persistence.*;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import ru.kirillius.XCP.Serialization.SerializationUtils;
import tools.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Collectors;
@EntityImplementation(GroupRepositoryImpl.GroupEntity.class)
public class GroupRepositoryImpl extends AbstractNodeRepository<Group> implements GroupRepository {
public GroupRepositoryImpl(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@Override
public StreamHandler<Group> getChildrenOf(Group group) {
return search("WHERE parent = ?1", List.of(group));
}
@Override
public StreamHandler<Group> getAllChildrenInHierarchy(Group group) {
var children = new ArrayList<Group>();
var pendingGroups = new ConcurrentLinkedQueue<Group>();
pendingGroups.add(group);
while (!pendingGroups.isEmpty()) {
var child = pendingGroups.remove();
try (var handler = getChildrenOf(child)) {
handler.get().forEach(item -> {
children.add(item);
pendingGroups.add(item);
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return new SimpleStreamHandler<>(children.stream());
}
@Override
public Group getRoot() {
try (var handler = search("WHERE parent is null", Collections.emptyList())) {
return handler.get().findFirst().orElse(null);
} catch (IOException e) {
throw new RuntimeException("Unable to get root group", e);
}
}
@Override
public void store(Group entity) {
if (entity != null && entity.getParent() == null) {
var root = getRoot();
if (root != null && !root.equals(entity)) {
throw new IllegalStateException("Root group already exists");
}
}
super.store(entity);
}
@Entity
@Table(name = "Groups")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public static class GroupEntity implements Group {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonProperty
private long id = 0;
@JsonProperty
@Column(unique = true, nullable = false)
@UuidGenerator
private UUID uuid;
@Column(nullable = false)
@JsonProperty
private String name = "";
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private String icon = "";
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private boolean prototype;
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private boolean protectedEntity;
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private boolean enabled;
@ManyToOne(fetch = FetchType.EAGER)
@JsonIgnore
private GroupEntity parent;
@JsonProperty("parent")
public EntityReference getParentReference() {
return new EntityReference(getParent());
}
@JsonProperty("parent")
public void setParentReference(EntityReference entityReference) {
parent = (GroupEntity) entityReference.get();
}
public Group getParent() {
return parent;
}
public void setParent(Group parent) {
this.parent = (GroupEntity) parent;
}
@Override
public void setTags(Set<Tag> tags) {
this.tags = tags.stream().map(t -> (TagRepositoryImpl.TagEntity) t).collect(Collectors.toSet());
}
@Override
public Set<Tag> getTags() {
return new HashSet<>(tags);
}
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private ObjectNode properties = SerializationUtils.EmptyObject();
@JsonProperty
@ManyToMany(fetch = FetchType.EAGER)
private Set<TagRepositoryImpl.TagEntity> tags = new HashSet<>();
@Override
public boolean equals(Object o) {
if (!(o instanceof GroupEntity that)) return false;
return Objects.equals(uuid, that.uuid);
}
@Override
public int hashCode() {
return Objects.hashCode(uuid);
}
}
}

View File

@ -0,0 +1,100 @@
package ru.kirillius.XCP.Persistence.Repositories;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.UuidGenerator;
import ru.kirillius.XCP.Commons.ResourceHandler;
import ru.kirillius.XCP.Persistence.*;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@EntityImplementation(TagRepositoryImpl.TagEntity.class)
public class TagRepositoryImpl extends AbstractRepository<Tag> implements TagRepository {
public TagRepositoryImpl(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@Override
public Tag getByName(String name) {
try (var handler = buildQueryParametrized("WHERE name = ?1", name)) {
var result = handler.get().findFirst();
return result.orElse(null);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public ResourceHandler<Stream<Tag>> getByNames(Collection<String> names) {
return search("where name IN (?1)", List.of(names));
}
@Entity
@Table(name = "Tags")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public static class TagEntity implements Tag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonProperty
private long id = 0;
@JsonProperty
@Column(unique = true, nullable = false)
@UuidGenerator
private UUID uuid;
@Column(nullable = false, unique = true)
@JsonProperty
private String name = "";
private static final Pattern NAME_PATTERN =
Pattern.compile("^[a-z0-9]+(\\.[a-z0-9]+)*$");
public void setName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
name = name.trim().toLowerCase();
if (!NAME_PATTERN.matcher(name).matches()) {
throw new IllegalArgumentException(
String.format(
"Invalid name: '%s'. " +
"Name must contain only lowercase letters a-z, digits 0-9, and dots. " +
"Cannot start or end with dot, and dots cannot be consecutive.",
name
)
);
}
this.name = name;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof TagEntity tagEntity)) return false;
return Objects.equals(name, tagEntity.name);
}
@Override
public int hashCode() {
return Objects.hashCode(name);
}
}
}

View File

@ -4,8 +4,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.UuidGenerator;
import ru.kirillius.XCP.Persistence.*;
import ru.kirillius.XCP.Persistence.AbstractRepository;
import ru.kirillius.XCP.Persistence.Entities.User;
import ru.kirillius.XCP.Persistence.EntityImplementation;
import ru.kirillius.XCP.Persistence.RepositoryServiceImpl;
import ru.kirillius.XCP.Security.Argon2HashUtility;
import ru.kirillius.XCP.Security.UserRole;
import ru.kirillius.XCP.Serialization.SerializationUtils;
@ -22,10 +24,7 @@ public class UserRepositoryImpl extends AbstractRepository<User> implements User
super(repositoryService);
}
@Override
public Class<? extends Repository<User>> getBaseClass() {
return UserRepository.class;
}
@Override
public User getByLogin(String login) {
@ -37,7 +36,7 @@ public class UserRepositoryImpl extends AbstractRepository<User> implements User
}
@Entity
@Table(name = "UserEntities")
@Table(name = "Users")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ -77,10 +76,6 @@ public class UserRepositoryImpl extends AbstractRepository<User> implements User
@JsonProperty
private ObjectNode values = SerializationUtils.EmptyObject();
@Override
public Class<? extends PersistenceEntity> getBaseType() {
return User.class;
}
@Override
public void setPassword(String password) {

View File

@ -9,6 +9,7 @@ import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -19,7 +20,10 @@ public final class RepositoryServiceImpl implements RepositoryService {
private final Configuration configuration;
private SessionFactory sessionFactory;
private DatabaseConfiguration databaseConfiguration;
private final Map<Class<? extends PersistenceEntity>, Class<? extends PersistenceEntity>> entityBaseBindings = new ConcurrentHashMap<>();
private final Map<Class<? extends Repository<?>>, Class<? extends PersistenceEntity>> repositoryEntityBindings = new ConcurrentHashMap<>();
private final Map<Class<? extends Repository<?>>, Repository<?>> repositoryBindings = new ConcurrentHashMap<>();
private final Map<Class<? extends Repository<?>>, Class<? extends Repository<?>>> repositoryBaseBindings = new ConcurrentHashMap<>();
private final Map<Class<? extends PersistenceEntity>, Class<? extends Repository<?>>> entityBindings = new ConcurrentHashMap<>();
private final Collection<Class<? extends AbstractRepository<?>>> managedRepositoryClasses;
private Context context;
@ -91,9 +95,10 @@ public final class RepositoryServiceImpl implements RepositoryService {
}
managedRepositoryClasses.forEach(aClass -> {
var instance = instantiateRepository(aClass);
var baseClass = instance.getBaseClass();
var baseClass = getRepositoryBaseType(aClass);
repositoryBindings.put(baseClass, instance);
var entityClass = instance.getEntityClass();
var entityClass = getEntityBaseType(getRepositoryEntityType(aClass));
entityBindings.put(entityClass, baseClass);
});
mapper = JsonMapper.builder().addModule(new PersistenceSerializationModule(this)).build();
@ -111,4 +116,62 @@ public final class RepositoryServiceImpl implements RepositoryService {
//noinspection unchecked
return (R) repositoryBindings.get(repositoryType);
}
/**
* Returns entity base interface Class<E> from Class<? extends E>
* @param entityClass
* @return Class<E>
*/
@Override
public Class<? extends PersistenceEntity> getEntityBaseType(Class<? extends PersistenceEntity> entityClass) {
if (!entityBaseBindings.containsKey(entityClass)) {
var foundClass = Arrays.stream(entityClass.getInterfaces()).filter(PersistenceEntity.class::isAssignableFrom).findFirst();
if (foundClass.isPresent()) {
entityBaseBindings.put(entityClass, (Class<? extends PersistenceEntity>) foundClass.get());
} else {
throw new RuntimeException("Unable to determine base interface Class<? extends PersistenceEntity> of " + entityClass.getName());
}
}
return entityBaseBindings.get(entityClass);
}
/**
* Returns repository base interface type Class<E> from Class<? extends E>
* @param repositoryClass
* @return Class<E>
*/
@Override
public Class<? extends Repository<?>> getRepositoryBaseType(Class<? extends Repository<?>> repositoryClass) {
if (!repositoryBaseBindings.containsKey(repositoryClass)) {
var foundClass = Arrays.stream(repositoryClass.getInterfaces()).filter(Repository.class::isAssignableFrom).findFirst();
if (foundClass.isPresent()) {
repositoryBaseBindings.put(repositoryClass, (Class<? extends Repository<?>>) foundClass.get());
} else {
throw new RuntimeException("Unable to determine base interface Class<? extends Repository> of " + repositoryClass.getName());
}
}
return repositoryBaseBindings.get(repositoryClass);
}
/**
* Returns Entity implementation class that implements E from Class<? extends Repository<E>>
* @param repositoryImplClass
* @return Class<? extends E>
*/
@Override
public Class<? extends PersistenceEntity> getRepositoryEntityType(Class<? extends Repository<?>> repositoryImplClass) {
if (!repositoryEntityBindings.containsKey(repositoryImplClass)) {
var annotation = repositoryImplClass.getAnnotation(EntityImplementation.class);
if (annotation != null) {
repositoryEntityBindings.put(repositoryImplClass, annotation.value());
} else {
throw new RuntimeException("Unable to get @" + EntityImplementation.class.getSimpleName() + " from class " + repositoryImplClass.getName());
}
}
return repositoryEntityBindings.get(repositoryImplClass);
}
}

View File

@ -0,0 +1,23 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Commons.StreamHandler;
import java.util.stream.Stream;
public class SimpleStreamHandler<T> implements StreamHandler<T> {
private final Stream<T> stream;
public SimpleStreamHandler(Stream<T> stream) {
this.stream = stream;
}
@Override
public Stream<T> get() {
return stream;
}
@Override
public void close() {
}
}

View File

@ -96,5 +96,19 @@ abstract class GenericRepositoryTest<E extends PersistenceEntity, R extends Abst
}
}
@Test
void testSerialize() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepositoryForEntity(entityClass);
var entity = repository.create();
repository.store(entity);
var serialized = repository.serialize(entity);
var deserialized = repository.deserialize(serialized);
assertThat(deserialized).isNotNull().isEqualTo(entity);
}
}
protected abstract void modify(E entity);
}

View File

@ -0,0 +1,123 @@
package ru.kirillius.XCP.Persistence.Repositories;
import org.junit.jupiter.api.Test;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.RepositoryService;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static ru.kirillius.XCP.Persistence.TestEnvironment.instantiateTestService;
class GroupRepositoryImplTest extends GenericRepositoryTest<Group, GroupRepositoryImpl> {
@Override
protected RepositoryService spawnRepositoryService() {
return instantiateTestService(List.of(repositoryClass, TagRepositoryImpl.class));
}
@Test
void getChildrenOf() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(GroupRepository.class);
assertThat(repository.getRoot()).isNull();
var root = repository.create();
repository.store(root);
var anotherParent = repository.create();
anotherParent.setParent(root);
repository.store(anotherParent);
var children = new ArrayList<Group>();
for (var i = 0; i < 20; i++) {
var child = repository.create();
child.setParent(anotherParent);
children.add(child);
repository.store(child);
}
try (var handler = repository.getChildrenOf(root)) {
assertThat(handler.get().toList()).containsExactly(anotherParent);
}
try (var handler = repository.getChildrenOf(anotherParent)) {
assertThat(handler.get().toList()).containsExactlyElementsOf(children);
}
}
}
@Test
void getAllChildrenInHierarchy() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(GroupRepository.class);
assertThat(repository.getRoot()).isNull();
var root = repository.create();
repository.store(root);
var anotherParent = repository.create();
anotherParent.setParent(root);
repository.store(anotherParent);
var children = new ArrayList<Group>();
for (var i = 0; i < 10; i++) {
var child = repository.create();
child.setParent(anotherParent);
children.add(child);
repository.store(child);
var subchild = repository.create();
subchild.setParent(child);
repository.store(subchild);
children.add(subchild);
}
try (var handler = repository.getAllChildrenInHierarchy(anotherParent)) {
assertThat(handler.get().toList()).containsExactlyInAnyOrderElementsOf(children);
}
try (var handler = repository.getAllChildrenInHierarchy(root)) {
children.add(anotherParent);
assertThat(handler.get().toList()).containsExactlyInAnyOrderElementsOf(children);
}
}
}
@Test
void getRoot() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(GroupRepository.class);
assertThat(repository.getRoot()).isNull();
var root = repository.create();
repository.store(root);
var notARoot = repository.create();
notARoot.setParent(root);
repository.store(notARoot);
assertThat(repository.getRoot()).isNotNull().isEqualTo(root);
}
}
@SuppressWarnings("CatchMayIgnoreException")
@Test
void testManyRoots() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(GroupRepository.class);
var root = repository.create();
var secondaryRoot = repository.create();
try {
repository.store(List.of(root, secondaryRoot));
throw new Exception("Nothing is thrown");
} catch (Throwable e) {
assertThat(e).isInstanceOf(IllegalStateException.class);
}
}
}
@Override
protected void modify(Group entity) {
entity.setName(UUID.randomUUID().toString());
}
}

View File

@ -0,0 +1,27 @@
package ru.kirillius.XCP.Persistence.Repositories;
import org.junit.jupiter.api.Test;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
class TagRepositoryImplTest extends GenericRepositoryTest<Tag, TagRepositoryImpl> {
@Test
void testGetByName() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(TagRepository.class);
var tag = repository.create();
tag.setName("test");
repository.store(tag);
var found = repository.getByName(tag.getName());
assertThat(found).isEqualTo(tag);
}
}
@Override
protected void modify(Tag entity) {
entity.setName("test" + Math.random());
}
}

View File

@ -34,11 +34,6 @@ class RepositoryServiceImplTest {
super(repositoryService);
}
@Override
public Class<? extends Repository<TestEntity>> getBaseClass() {
return TestRepository.class;
}
@Entity
@Table
public static class EntityImpl implements TestEntity {
@ -61,10 +56,6 @@ class RepositoryServiceImplTest {
@Setter
private UUID uuid;
@Override
public Class<? extends PersistenceEntity> getBaseType() {
return null;
}
@Override
public boolean equals(Object o) {
@ -96,6 +87,18 @@ class RepositoryServiceImplTest {
}
}
@Test
void TestSerialization() {
try (var service = instantiateTestService(List.of(RepoImpl.class))) {
var repository = service.getRepository(TestRepository.class);
var testEntity = repository.create();
repository.store(testEntity);
var serialized = repository.serialize(testEntity);
var deserialized = repository.deserialize(serialized);
assertThat(deserialized).isEqualTo(testEntity);
}
}
@Test
public void TestEntityStore() {
try (var service = instantiateTestService(List.of(RepoImpl.class))) {