diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/Group.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/Group.java index 4da26e1..b1ab808 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/Group.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/Group.java @@ -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); } diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/Tag.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/Tag.java new file mode 100644 index 0000000..edb71cd --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/Tag.java @@ -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); +} diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/NodeEntity.java b/api/src/main/java/ru/kirillius/XCP/Persistence/NodeEntity.java index 150202b..29e9ef6 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/NodeEntity.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/NodeEntity.java @@ -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 getTags(); + Set getTags(); - void setTags(Set tags); + void setTags(Set tags); } diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/NodeRepository.java b/api/src/main/java/ru/kirillius/XCP/Persistence/NodeRepository.java index a9bd1ba..96bb22e 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/NodeRepository.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/NodeRepository.java @@ -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 extends Repository { StreamHandler getByGroup(Group group); - StreamHandler getByTags(Collection tags, TagSearchMode searchMode); + StreamHandler getByTags(Collection tags); + - enum TagSearchMode { - MatchAnyTag, - MatchAllTags, - MatchExactTags - } } diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceEntity.java b/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceEntity.java index 59a94a0..7018dde 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceEntity.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceEntity.java @@ -6,6 +6,4 @@ public interface PersistenceEntity { long getId(); UUID getUuid(); - - Class getBaseType(); } diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepository.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepository.java index 7bc598f..cc8e27d 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepository.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepository.java @@ -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 { - StreamHandler getChildrenOf(Group dataGroup); +public interface GroupRepository extends NodeRepository { + StreamHandler getChildrenOf(Group group); - StreamHandler getChildrenRecursiveOf(Group dataGroup); + StreamHandler getAllChildrenInHierarchy(Group group); Group getRoot(); } diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepository.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepository.java new file mode 100644 index 0000000..76aa8e0 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepository.java @@ -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 getByName(String name); + + ResourceHandler> getByNames(Collection names); +} diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/RepositoryService.java b/api/src/main/java/ru/kirillius/XCP/Persistence/RepositoryService.java index 6f9d8c4..58010c5 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/RepositoryService.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/RepositoryService.java @@ -6,4 +6,10 @@ public interface RepositoryService extends Service { Repository getRepositoryForEntity(Class entityType); > R getRepository(Class repositoryType); + + Class getEntityBaseType(Class entityClass); + Class> getRepositoryBaseType(Class> repositoryClass); + Class getRepositoryEntityType(Class> repositoryClass); + + } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java b/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java index edf2435..6404857 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java @@ -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 implements Repository { - protected final Class entityImplementationClass; + private final Repository.EventBindings eventBindings = new EventBindingsImpl<>(); - @Getter - protected Class entityClass; + protected final Class 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) thisClass.getAnnotation(EntityImplementation.class).value(); tableName = entityImplementationClass.getName(); @@ -58,8 +44,6 @@ public abstract class AbstractRepository implements } } - public abstract Class> getBaseClass(); - @Override public StreamHandler search(String query, Collection queryParameters) { return buildQuery(query, queryParameters.toArray()); @@ -195,21 +179,6 @@ public abstract class AbstractRepository implements return repositoryService.getMapper().treeToValue(array, repositoryService.getMapper().getTypeFactory().constructCollectionType(List.class, entityImplementationClass)); } - @SuppressWarnings("unchecked") - private Class 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) typeArguments[0]; - } catch (Exception e) { - throw new RuntimeException("Failed to determine service generic parameters for Service: " + this.getClass().getName(), e); - } - } - protected String joinIdentifiers(Collection ids) { var joiner = new StringJoiner(", "); diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReference.java b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReference.java new file mode 100644 index 0000000..03d8678 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReference.java @@ -0,0 +1,9 @@ +package ru.kirillius.XCP.Persistence; + +import java.util.concurrent.atomic.AtomicReference; + +public class EntityReference extends AtomicReference { + public EntityReference(PersistenceEntity initialValue) { + super(initialValue); + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java index 852b877..220df2b 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java @@ -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 { +import java.util.UUID; + +public class EntityReferenceDeserializer extends StdDeserializer { 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) 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; } } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceSerializer.java b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceSerializer.java index 3437a7e..8087b74 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceSerializer.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceSerializer.java @@ -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 { +public class EntityReferenceSerializer extends StdSerializer { + private RepositoryServiceImpl repositoryService; - public EntityReferenceSerializer() { - this(PersistenceEntity.class); + public EntityReferenceSerializer(RepositoryServiceImpl repositoryService) { + super(EntityReference.class); + this.repositoryService = repositoryService; } protected EntityReferenceSerializer(Class t) { @@ -16,9 +18,15 @@ public class EntityReferenceSerializer extends StdSerializer } @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(); diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/PersistenceSerializationModule.java b/database/src/main/java/ru/kirillius/XCP/Persistence/PersistenceSerializationModule.java index 5c7484d..1e990d9 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/PersistenceSerializationModule.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/PersistenceSerializationModule.java @@ -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)))); } } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/AbstractNodeRepository.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/AbstractNodeRepository.java new file mode 100644 index 0000000..e6fec8f --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/AbstractNodeRepository.java @@ -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 extends AbstractRepository implements NodeRepository { + + public AbstractNodeRepository(RepositoryServiceImpl repositoryService) { + super(repositoryService); + } + + @Override + public StreamHandler getByGroup(Group group) { + return search("WHERE group = ?1", List.of(group)); + } + + @Override + public StreamHandler getByTags(Collection 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) session.createQuery(hql, entityImplementationClass); + query.setParameter("tagNames", tags); + query.setParameter("tagCount", tags.size()); + return new ResourceHandlerImpl<>(query, transaction, session); + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java new file mode 100644 index 0000000..a7a889d --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java @@ -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 implements GroupRepository { + + public GroupRepositoryImpl(RepositoryServiceImpl repositoryService) { + super(repositoryService); + } + + @Override + public StreamHandler getChildrenOf(Group group) { + return search("WHERE parent = ?1", List.of(group)); + } + + @Override + public StreamHandler getAllChildrenInHierarchy(Group group) { + var children = new ArrayList(); + var pendingGroups = new ConcurrentLinkedQueue(); + 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 tags) { + this.tags = tags.stream().map(t -> (TagRepositoryImpl.TagEntity) t).collect(Collectors.toSet()); + } + + @Override + public Set getTags() { + return new HashSet<>(tags); + } + + @Column(nullable = false) + @JsonProperty + @Getter + @Setter + private ObjectNode properties = SerializationUtils.EmptyObject(); + + @JsonProperty + @ManyToMany(fetch = FetchType.EAGER) + private Set 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); + } + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImpl.java new file mode 100644 index 0000000..62b1d71 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImpl.java @@ -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 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> getByNames(Collection 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); + } + + + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/UserRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/UserRepositoryImpl.java index 120c642..1f2cb19 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/UserRepositoryImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/UserRepositoryImpl.java @@ -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 implements User super(repositoryService); } - @Override - public Class> getBaseClass() { - return UserRepository.class; - } + @Override public User getByLogin(String login) { @@ -37,7 +36,7 @@ public class UserRepositoryImpl extends AbstractRepository implements User } @Entity - @Table(name = "UserEntities") + @Table(name = "Users") @Builder @AllArgsConstructor @NoArgsConstructor @@ -77,10 +76,6 @@ public class UserRepositoryImpl extends AbstractRepository implements User @JsonProperty private ObjectNode values = SerializationUtils.EmptyObject(); - @Override - public Class getBaseType() { - return User.class; - } @Override public void setPassword(String password) { diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java index dad8398..29a6826 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java @@ -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> entityBaseBindings = new ConcurrentHashMap<>(); + private final Map>, Class> repositoryEntityBindings = new ConcurrentHashMap<>(); private final Map>, Repository> repositoryBindings = new ConcurrentHashMap<>(); + private final Map>, Class>> repositoryBaseBindings = new ConcurrentHashMap<>(); private final Map, Class>> entityBindings = new ConcurrentHashMap<>(); private final Collection>> 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 from Class + * @param entityClass + * @return Class + */ + @Override + public Class getEntityBaseType(Class entityClass) { + if (!entityBaseBindings.containsKey(entityClass)) { + var foundClass = Arrays.stream(entityClass.getInterfaces()).filter(PersistenceEntity.class::isAssignableFrom).findFirst(); + if (foundClass.isPresent()) { + entityBaseBindings.put(entityClass, (Class) foundClass.get()); + } else { + throw new RuntimeException("Unable to determine base interface Class of " + entityClass.getName()); + } + } + + return entityBaseBindings.get(entityClass); + } + + /** + * Returns repository base interface type Class from Class + * @param repositoryClass + * @return Class + */ + @Override + public Class> getRepositoryBaseType(Class> repositoryClass) { + if (!repositoryBaseBindings.containsKey(repositoryClass)) { + var foundClass = Arrays.stream(repositoryClass.getInterfaces()).filter(Repository.class::isAssignableFrom).findFirst(); + if (foundClass.isPresent()) { + repositoryBaseBindings.put(repositoryClass, (Class>) foundClass.get()); + } else { + throw new RuntimeException("Unable to determine base interface Class of " + repositoryClass.getName()); + } + } + + return repositoryBaseBindings.get(repositoryClass); + } + + /** + * Returns Entity implementation class that implements E from Class> + * @param repositoryImplClass + * @return Class + */ + @Override + public Class getRepositoryEntityType(Class> 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); + } + } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/SimpleStreamHandler.java b/database/src/main/java/ru/kirillius/XCP/Persistence/SimpleStreamHandler.java new file mode 100644 index 0000000..36e9d3b --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/SimpleStreamHandler.java @@ -0,0 +1,23 @@ +package ru.kirillius.XCP.Persistence; + +import ru.kirillius.XCP.Commons.StreamHandler; + +import java.util.stream.Stream; + +public class SimpleStreamHandler implements StreamHandler { + private final Stream stream; + + public SimpleStreamHandler(Stream stream) { + this.stream = stream; + } + + @Override + public Stream get() { + return stream; + } + + @Override + public void close() { + + } +} diff --git a/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/GenericRepositoryTest.java b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/GenericRepositoryTest.java index 5b7a558..ae4951e 100644 --- a/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/GenericRepositoryTest.java +++ b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/GenericRepositoryTest.java @@ -96,5 +96,19 @@ abstract class GenericRepositoryTest { + @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(); + 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(); + 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()); + } +} \ No newline at end of file diff --git a/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImplTest.java b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImplTest.java new file mode 100644 index 0000000..c041ce7 --- /dev/null +++ b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImplTest.java @@ -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 { + @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()); + } +} \ No newline at end of file diff --git a/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java b/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java index 9696b3b..a754112 100644 --- a/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java +++ b/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java @@ -34,11 +34,6 @@ class RepositoryServiceImplTest { super(repositoryService); } - @Override - public Class> 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 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))) {