diff --git a/api/pom.xml b/api/pom.xml new file mode 100644 index 0000000..afe8d2c --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + ru.kirillius + XCP + 1.0.0.0 + + + api + + + + ru.kirillius + java-events + 1.1.0.0 + + + + tools.jackson.core + jackson-databind + 3.0.3 + + + \ No newline at end of file diff --git a/api/src/main/java/ru/kirillius/XCP/api/Commons/Config.java b/api/src/main/java/ru/kirillius/XCP/api/Commons/Config.java new file mode 100644 index 0000000..5618bb0 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Commons/Config.java @@ -0,0 +1,21 @@ +package ru.kirillius.XCP.api.Commons; + +import java.io.File; + +public interface Config { + + File getLoadedConfigFile(); + + String getHost(); + + void setHost(String host); + + File getDatabaseFile(); + + void setDatabaseFile(File databaseFile); + + int getHttpPort(); + + void setHttpPort(int httpPort); + +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Commons/ConfigManager.java b/api/src/main/java/ru/kirillius/XCP/api/Commons/ConfigManager.java new file mode 100644 index 0000000..43bebbd --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Commons/ConfigManager.java @@ -0,0 +1,14 @@ +package ru.kirillius.XCP.api.Commons; + +import java.io.IOException; + +public interface ConfigManager { + + boolean isExist(); + + Config load(); + + Config create(); + + void save(Config config) throws IOException; +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Commons/Context.java b/api/src/main/java/ru/kirillius/XCP/api/Commons/Context.java new file mode 100644 index 0000000..51e475c --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Commons/Context.java @@ -0,0 +1,11 @@ +package ru.kirillius.XCP.api.Commons; + +public interface Context { + Config getConfig(); + + ConfigManager getConfigManager(); + + S getService(Class serviceClass); + + void shutdown(); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Commons/ResourceHandler.java b/api/src/main/java/ru/kirillius/XCP/api/Commons/ResourceHandler.java new file mode 100644 index 0000000..4c5ebd3 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Commons/ResourceHandler.java @@ -0,0 +1,7 @@ +package ru.kirillius.XCP.api.Commons; + +import java.io.Closeable; + +public interface ResourceHandler extends Closeable { + T get(); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Commons/Service.java b/api/src/main/java/ru/kirillius/XCP/api/Commons/Service.java new file mode 100644 index 0000000..27a8803 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Commons/Service.java @@ -0,0 +1,7 @@ +package ru.kirillius.XCP.api.Commons; + +import java.io.Closeable; + +public interface Service extends Closeable { + void initialize(Context context); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Commons/StreamHandler.java b/api/src/main/java/ru/kirillius/XCP/api/Commons/StreamHandler.java new file mode 100644 index 0000000..fde73df --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Commons/StreamHandler.java @@ -0,0 +1,7 @@ +package ru.kirillius.XCP.api.Commons; + +import java.util.stream.Stream; + +public interface StreamHandler extends ResourceHandler> { + +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Data/DataTransferProtocol.java b/api/src/main/java/ru/kirillius/XCP/api/Data/DataTransferProtocol.java new file mode 100644 index 0000000..05a8817 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Data/DataTransferProtocol.java @@ -0,0 +1,6 @@ +package ru.kirillius.XCP.api.Data; + +@Deprecated +public interface DataTransferProtocol { + //TODO +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Data/PollSettings.java b/api/src/main/java/ru/kirillius/XCP/api/Data/PollSettings.java new file mode 100644 index 0000000..a9beb62 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Data/PollSettings.java @@ -0,0 +1,20 @@ +package ru.kirillius.XCP.api.Data; + +public interface PollSettings { + int getMaxValueCount(); + + void setMaxValueCount(int maxValueCount); + + long getPollInterval(); + + void setPollInterval(long pollInterval); + + boolean isInterruptIfBusy(); + + void setInterruptIfBusy(boolean interruptIfBusy); + + boolean setEnabled(); + + boolean isEnabled(); + +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Data/ValueModifier.java b/api/src/main/java/ru/kirillius/XCP/api/Data/ValueModifier.java new file mode 100644 index 0000000..fd578ac --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Data/ValueModifier.java @@ -0,0 +1,6 @@ +package ru.kirillius.XCP.api.Data; + +@Deprecated +public interface ValueModifier { + //TODO +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Data/ValueModifierSettings.java b/api/src/main/java/ru/kirillius/XCP/api/Data/ValueModifierSettings.java new file mode 100644 index 0000000..533d548 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Data/ValueModifierSettings.java @@ -0,0 +1,14 @@ +package ru.kirillius.XCP.api.Data; + +import tools.jackson.databind.node.ObjectNode; + +public interface ValueModifierSettings { + + Class getModifierClass(); + + void setModifierClass(Class modifierClass); + + ObjectNode getParameters(); + + void setParameters(ObjectNode parameters); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/Group.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/Group.java new file mode 100644 index 0000000..82a9f69 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/Group.java @@ -0,0 +1,8 @@ +package ru.kirillius.XCP.api.Persistence.Entities; + +import ru.kirillius.XCP.api.Persistence.NodeEntity; + +public interface Group extends NodeEntity { + String getIcon(); + void setIcon(String icon); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/Input.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/Input.java new file mode 100644 index 0000000..11a4430 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/Input.java @@ -0,0 +1,13 @@ +package ru.kirillius.XCP.api.Persistence.Entities; + +import ru.kirillius.XCP.api.Data.PollSettings; +import ru.kirillius.XCP.api.Persistence.IOEntity; +import ru.kirillius.XCP.api.Persistence.NodeEntity; + +public interface Input extends IOEntity, NodeEntity { + + PollSettings getPollSettings(); + + void setPollSettings(PollSettings pollSettings); + +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/Output.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/Output.java new file mode 100644 index 0000000..cc6ed5f --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/Output.java @@ -0,0 +1,7 @@ +package ru.kirillius.XCP.api.Persistence.Entities; + +import ru.kirillius.XCP.api.Persistence.IOEntity; +import ru.kirillius.XCP.api.Persistence.NodeEntity; + +public interface Output extends IOEntity, NodeEntity { +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/User.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/User.java new file mode 100644 index 0000000..87de17f --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Entities/User.java @@ -0,0 +1,25 @@ +package ru.kirillius.XCP.api.Persistence.Entities; + +import ru.kirillius.XCP.api.Persistence.PersistenceEntity; +import ru.kirillius.XCP.api.Security.UserRole; +import tools.jackson.databind.node.ObjectNode; + +public interface User extends PersistenceEntity { + void changePassword(String newPass); + + String getLogin(); + + void setLogin(String login); + + UserRole getRole(); + + void setRole(UserRole role); + + ObjectNode getValues(); + + void setValues(ObjectNode values); + + String getName(); + + void setName(String name); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/IOEntity.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/IOEntity.java new file mode 100644 index 0000000..65123a2 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/IOEntity.java @@ -0,0 +1,16 @@ +package ru.kirillius.XCP.api.Persistence; + +import ru.kirillius.XCP.api.Data.DataTransferProtocol; +import ru.kirillius.XCP.api.Data.ValueModifierSettings; + +import java.util.List; + +public interface IOEntity extends NodeEntity { + List getModifiers(); + + void setModifiers(List modifiers); + + Class getProtocol(); + + void setProtocol(Class protocol); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/NodeEntity.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/NodeEntity.java new file mode 100644 index 0000000..f1402ca --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/NodeEntity.java @@ -0,0 +1,32 @@ +package ru.kirillius.XCP.api.Persistence; + +import ru.kirillius.XCP.api.Persistence.Entities.Group; +import tools.jackson.databind.node.ObjectNode; + +import java.util.Set; + +public interface NodeEntity extends PersistenceEntity { + String getName(); + + void setName(String name); + + boolean isEssential(); + + void setEssential(boolean essential); + + boolean isEnabled(); + + void setEnabled(boolean enabled); + + Group getParent(); + + void setParent(Group parent); + + ObjectNode getProperties(); + + void setProperties(ObjectNode properties); + + Set getTags(); + + void setTags(Set tags); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/NodeRepository.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/NodeRepository.java new file mode 100644 index 0000000..30737e1 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/NodeRepository.java @@ -0,0 +1,18 @@ +package ru.kirillius.XCP.api.Persistence; + +import ru.kirillius.XCP.api.Commons.StreamHandler; +import ru.kirillius.XCP.api.Persistence.Entities.Group; + +import java.util.Collection; + +public interface NodeRepository extends Repository { + StreamHandler getByGroup(Group group); + + StreamHandler getByTags(Collection tags, TagSearchMode searchMode); + + enum TagSearchMode { + MatchAnyTag, + MatchAllTags, + MatchExactTags + } +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/PersistenceEntity.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/PersistenceEntity.java new file mode 100644 index 0000000..0d52384 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/PersistenceEntity.java @@ -0,0 +1,11 @@ +package ru.kirillius.XCP.api.Persistence; + +import java.util.UUID; + +public interface PersistenceEntity { + long getId(); + + UUID getUUID(); + + Class getBaseType(); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/GroupRepository.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/GroupRepository.java new file mode 100644 index 0000000..e68def2 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/GroupRepository.java @@ -0,0 +1,14 @@ +package ru.kirillius.XCP.api.Persistence.Repositories; + +import ru.kirillius.XCP.api.Commons.StreamHandler; +import ru.kirillius.XCP.api.Persistence.Entities.Group; +import ru.kirillius.XCP.api.Persistence.Entities.Input; +import ru.kirillius.XCP.api.Persistence.NodeRepository; + +public interface GroupRepository extends NodeRepository { + StreamHandler getChildrenOf(Group dataGroup); + + StreamHandler getChildrenRecursiveOf(Group dataGroup); + + Group getRoot(); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/InputRepository.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/InputRepository.java new file mode 100644 index 0000000..2fc0d6e --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/InputRepository.java @@ -0,0 +1,8 @@ +package ru.kirillius.XCP.api.Persistence.Repositories; + +import ru.kirillius.XCP.api.Persistence.Entities.Input; +import ru.kirillius.XCP.api.Persistence.NodeRepository; + +public interface InputRepository extends NodeRepository { + +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/OutputRepository.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/OutputRepository.java new file mode 100644 index 0000000..ed541e3 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/OutputRepository.java @@ -0,0 +1,8 @@ +package ru.kirillius.XCP.api.Persistence.Repositories; + +import ru.kirillius.XCP.api.Persistence.Entities.Input; +import ru.kirillius.XCP.api.Persistence.NodeRepository; + +public interface OutputRepository extends NodeRepository { + +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/UserRepository.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/UserRepository.java new file mode 100644 index 0000000..dede83b --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repositories/UserRepository.java @@ -0,0 +1,10 @@ +package ru.kirillius.XCP.api.Persistence.Repositories; + +import ru.kirillius.XCP.api.Persistence.Entities.User; +import ru.kirillius.XCP.api.Persistence.Repository; + +public interface UserRepository extends Repository { + User getByLoginAndPassword(String login, String password); + + User getByLogin(String login); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repository.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repository.java new file mode 100644 index 0000000..c45a68b --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/Repository.java @@ -0,0 +1,49 @@ +package ru.kirillius.XCP.api.Persistence; + +import ru.kirillius.XCP.api.Commons.StreamHandler; +import ru.kirillius.java.utils.events.EventHandler; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; + +import java.util.Collection; +import java.util.UUID; + +public interface Repository { + E create(); + + E load(long id); + + E load(UUID uuid); + + StreamHandler load(Collection ids); + + StreamHandler search(String query, Collection queryParameters); + + EventBindings events(); + + StreamHandler loadAll(); + + long getCount(); + + void store(E entity); + + void store(Collection entities); + + void remove(E entity); + + void remove(Collection entities); + + ObjectNode serialize(E entity); + + ArrayNode serialize(Collection entities); + + E deserialize(ObjectNode object); + + Collection deserialize(ArrayNode array); + + interface EventBindings { + EventHandler entityStored(); + + EventHandler entityRemoved(); + } +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Persistence/RepositoryService.java b/api/src/main/java/ru/kirillius/XCP/api/Persistence/RepositoryService.java new file mode 100644 index 0000000..092add9 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Persistence/RepositoryService.java @@ -0,0 +1,9 @@ +package ru.kirillius.XCP.api.Persistence; + +import ru.kirillius.XCP.api.Commons.Service; + +public interface RepositoryService extends Service { + Repository getRepositoryForEntity(Class entityType); + + > R getRepository(Class repositoryType); +} diff --git a/api/src/main/java/ru/kirillius/XCP/api/Security/UserRole.java b/api/src/main/java/ru/kirillius/XCP/api/Security/UserRole.java new file mode 100644 index 0000000..e8b2dfa --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/api/Security/UserRole.java @@ -0,0 +1,17 @@ +package ru.kirillius.XCP.api.Security; + +import lombok.Getter; + +@Getter +public enum UserRole { + Guest(0), + User(1), + Operator(2), + Admin(3); + + private final int level; + + UserRole(int level) { + this.level = level; + } +} \ No newline at end of file diff --git a/database/pom.xml b/database/pom.xml new file mode 100644 index 0000000..e33ac8a --- /dev/null +++ b/database/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + ru.kirillius + XCP + 1.0.0.0 + + + database + + + + + com.h2database + h2 + 2.1.214 + + + + org.hibernate.orm + hibernate-core + 7.1.10.Final + + + + org.hibernate.orm + hibernate-c3p0 + 7.1.10.Final + + + tools.jackson.core + jackson-databind + 3.0.3 + + + ru.kirillius + api + 1.0.0.0 + compile + + + + \ No newline at end of file diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java b/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java new file mode 100644 index 0000000..2794abb --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java @@ -0,0 +1,306 @@ +package ru.kirillius.XCP.Persistence; + +import jakarta.persistence.Table; +import lombok.Getter; +import org.hibernate.Session; +import org.hibernate.Transaction; +import org.hibernate.query.Query; +import ru.kirillius.XCP.api.Commons.StreamHandler; +import ru.kirillius.XCP.api.Persistence.PersistenceEntity; +import ru.kirillius.XCP.api.Persistence.Repository; +import ru.kirillius.java.utils.events.ConcurrentEventHandler; +import ru.kirillius.java.utils.events.EventHandler; +import tools.jackson.databind.node.ArrayNode; +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; +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 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(); + + if (entityImplementationClass.isAnnotationPresent(Table.class)) { + tableName = entityImplementationClass.getAnnotation(Table.class).name(); + } + if (tableName == null || tableName.isEmpty()) { + tableName = entityImplementationClass.getName(); + } + + } + + @Override + public E create() { + try { + var constructor = entityImplementationClass.getConstructor(); + return constructor.newInstance(); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | + IllegalAccessException e) { + throw new RuntimeException("Unable to instantiate entity", e); + } + } + + public abstract Class> getBaseClass(); + + @Override + public StreamHandler search(String query, Collection queryParameters) { + return buildQuery(query, queryParameters.toArray()); + } + + @Override + public E load(UUID uuid) { + try (var query = buildQueryParametrized("where uuid = ?1", uuid)) { + return query.get().findFirst().orElse(null); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public E load(long id) { + try (var query = buildQueryParametrized("where id = ?1", id)) { + return query.get().findFirst().orElse(null); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public StreamHandler load(Collection ids) { + if (ids != null && !ids.isEmpty()) { + return buildQueryParametrized("where id IN (" + joinIdentifiers(ids) + ")"); + } else { + return new EmptyRequest<>(); + } + } + + @Override + public Repository.EventBindings events() { + return eventBindings; + } + + @Override + public StreamHandler loadAll() { + return buildQueryParametrized("order by id"); + } + + @SuppressWarnings({"JpaQlInspection", "SqlSourceToSinkFlow"}) + @Override + public long getCount() { + var session = repositoryService.openSession(); + var transaction = session.beginTransaction(); + try { + return session.createQuery("select count(id) as c from " + tableName, Long.class).uniqueResult(); + } finally { + transaction.commit(); + session.close(); + } + } + + @Override + public void store(E entity) { + var session = repositoryService.openSession(); + var transaction = session.beginTransaction(); + try { + if (entity.getId() == 0L) { + session.persist(entity); + } else { + session.merge(entity); + } + transaction.commit(); + } catch (Exception e) { + transaction.rollback(); + throw new RuntimeException(e); + } + + try { + eventBindings.entityStored().invoke(entity); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + session.close(); + } + } + + @Override + public void store(Collection entities) { + entities.forEach(this::store); + } + + @Override + public void remove(E entity) { + var session = repositoryService.openSession(); + var transaction = session.beginTransaction(); + try { + session.remove(entity); + transaction.commit(); + } catch (Exception e) { + transaction.rollback(); + throw new RuntimeException(e); + } + + try { + eventBindings.entityRemoved().invoke(entity); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + session.close(); + } + } + + @Override + public void remove(Collection entities) { + entities.forEach(this::remove); + } + + @Override + public ObjectNode serialize(E entity) { + return repositoryService.getMapper().valueToTree(entity); + } + + @Override + public ArrayNode serialize(Collection entities) { + var array = repositoryService.getMapper().createArrayNode(); + for (E entity : entities) { + array.add(repositoryService.getMapper().valueToTree(entity)); + } + return array; + } + + @Override + public E deserialize(ObjectNode object) { + return repositoryService.getMapper().convertValue(object, entityImplementationClass); + } + + @Override + public Collection deserialize(ArrayNode array) { + 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(", "); + + for (var id : ids) { + joiner.add(id.toString()); + } + return joiner.toString(); + } + + @SuppressWarnings({"unchecked", "SqlSourceToSinkFlow"}) + protected StreamHandler buildQuery(String condition, Object[] parameters) { + var session = repositoryService.openSession(); + var transaction = session.beginTransaction(); + var queryString = "from " + tableName + " " + condition; + var query = (Query) session.createQuery(queryString, entityImplementationClass); + for (var key = 0; key < parameters.length; ++key) { + query.setParameter(key + 1, parameters[key]); + } + return new ResourceHandlerImpl<>(query, transaction, session); + } + + protected StreamHandler buildQueryParametrized(String condition, Object... parameters) { + return buildQuery(condition, parameters); + } + + private static class EventBindingsImpl implements Repository.EventBindings { + private final EventHandler stored = new ConcurrentEventHandler<>(); + private final EventHandler removed = new ConcurrentEventHandler<>(); + + @Override + public EventHandler entityStored() { + return stored; + } + + @Override + public EventHandler entityRemoved() { + return removed; + } + } + + protected static class EmptyRequest implements StreamHandler { + + @Override + public Stream get() { + return Stream.empty(); + } + + @Override + public void close() throws IOException { + + } + } + + protected static class ResourceHandlerImpl implements StreamHandler { + + private final Query query; + private final Transaction transaction; + private final Session session; + + public ResourceHandlerImpl(Query query, Transaction transaction, Session session) { + this.query = query; + this.transaction = transaction; + this.session = session; + } + + @Override + public Stream get() { + return query.getResultStream(); + } + + @Override + public void close() { + if (transaction != null && transaction.isActive()) { + transaction.commit(); + } + + if (session.isOpen()) { + session.close(); + } + + } + } + +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/DatabaseConfiguration.java b/database/src/main/java/ru/kirillius/XCP/Persistence/DatabaseConfiguration.java new file mode 100644 index 0000000..d1851c4 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/DatabaseConfiguration.java @@ -0,0 +1,5 @@ +package ru.kirillius.XCP.Persistence; + +public interface DatabaseConfiguration { + String getConnectionUrl(); +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityImplementation.java b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityImplementation.java new file mode 100644 index 0000000..1190195 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityImplementation.java @@ -0,0 +1,14 @@ +package ru.kirillius.XCP.Persistence; + +import ru.kirillius.XCP.api.Persistence.PersistenceEntity; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface EntityImplementation { + Class value(); +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java new file mode 100644 index 0000000..938164c --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java @@ -0,0 +1,31 @@ +package ru.kirillius.XCP.Persistence; + +import ru.kirillius.XCP.api.Persistence.PersistenceEntity; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; + +public class EntityReferenceDeserializer extends ValueDeserializer { + + private final RepositoryServiceImpl repositoryService; + + public EntityReferenceDeserializer(RepositoryServiceImpl repositoryService) { + 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 { + + 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 new file mode 100644 index 0000000..916ea3f --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceSerializer.java @@ -0,0 +1,27 @@ +package ru.kirillius.XCP.Persistence; + +import ru.kirillius.XCP.api.Persistence.PersistenceEntity; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ser.std.StdSerializer; + +public class EntityReferenceSerializer extends StdSerializer { + + public EntityReferenceSerializer() { + this(PersistenceEntity.class); + } + + protected EntityReferenceSerializer(Class t) { + super(t); + } + + @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(); + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/H2DatabaseInFileConfiguration.java b/database/src/main/java/ru/kirillius/XCP/Persistence/H2DatabaseInFileConfiguration.java new file mode 100644 index 0000000..dd911aa --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/H2DatabaseInFileConfiguration.java @@ -0,0 +1,17 @@ +package ru.kirillius.XCP.Persistence; + +import java.io.File; +import java.util.regex.Pattern; + +public class H2DatabaseInFileConfiguration implements DatabaseConfiguration { + private final File databaseFile; + + public H2DatabaseInFileConfiguration(File databaseFile) { + this.databaseFile = databaseFile; + } + + @Override + public String getConnectionUrl() { + return "jdbc:h2:file:" + databaseFile.getPath().replaceAll(Pattern.quote(".mv.db"), ""); + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/PersistenceSerializationModule.java b/database/src/main/java/ru/kirillius/XCP/Persistence/PersistenceSerializationModule.java new file mode 100644 index 0000000..e6b13e6 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/PersistenceSerializationModule.java @@ -0,0 +1,36 @@ +package ru.kirillius.XCP.Persistence; + +import lombok.Getter; +import ru.kirillius.XCP.api.Persistence.PersistenceEntity; +import tools.jackson.core.Version; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.module.SimpleDeserializers; +import tools.jackson.databind.module.SimpleSerializers; + +import java.util.List; +import java.util.Map; + +class PersistenceSerializationModule extends JacksonModule { + public PersistenceSerializationModule(RepositoryServiceImpl repositoryService) { + this.repositoryService = repositoryService; + } + + @Getter + private final RepositoryServiceImpl repositoryService; + + @Override + public String getModuleName() { + return getClass().getSimpleName(); + } + + @Override + public Version version() { + return new Version(1, 0, 0, null, null, null); + } + + @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)))); + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java new file mode 100644 index 0000000..5cfcf27 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java @@ -0,0 +1,116 @@ +package ru.kirillius.XCP.Persistence; + +import lombok.Getter; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; +import ru.kirillius.XCP.api.Commons.Context; +import ru.kirillius.XCP.api.Persistence.PersistenceEntity; +import ru.kirillius.XCP.api.Persistence.Repository; +import ru.kirillius.XCP.api.Persistence.RepositoryService; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class RepositoryServiceImpl implements RepositoryService { + @Getter + private ObjectMapper mapper = new ObjectMapper(); + private final Configuration configuration; + private SessionFactory sessionFactory; + private DatabaseConfiguration databaseConfiguration; + private final Map>, Repository> repositoryBindings = new ConcurrentHashMap<>(); + private final Map, Class>> entityBindings = new ConcurrentHashMap<>(); + + private final Collection>> managedRepositoryClasses; + + public RepositoryServiceImpl(DatabaseConfiguration databaseConfiguration, Collection>> repositoryImplClasses) { + managedRepositoryClasses = repositoryImplClasses; + configuration = new Configuration(); + configuration.configure(); + registerClasses(repositoryImplClasses); + loadDatabaseConfig(databaseConfiguration); + } + + private void loadDatabaseConfig(DatabaseConfiguration databaseConfiguration) { + this.databaseConfiguration = databaseConfiguration; + configuration.getProperties().setProperty("hibernate.connection.url", databaseConfiguration.getConnectionUrl()); + } + + public RepositoryServiceImpl(Collection>> repositoryImplClasses) { + managedRepositoryClasses = repositoryImplClasses; + configuration = new Configuration(); + configuration.configure(); + registerClasses(repositoryImplClasses); + } + + private void registerClasses(Collection>> repositoryImplClasses) { + managedRepositoryClasses.forEach(aClass -> { + var implementation = aClass.getAnnotation(EntityImplementation.class); + if (implementation == null) { + throw new IllegalStateException("@" + EntityImplementation.class.getSimpleName() + " is not present in class " + aClass.getSimpleName()); + } + + configuration.addAnnotatedClass(implementation.value()); + }); + } + + private AbstractRepository instantiateRepository(Class> sCls) { + try { + var constructor = sCls.getDeclaredConstructor(RepositoryServiceImpl.class); + constructor.setAccessible(true); + return constructor.newInstance(this); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException | + NoSuchMethodException e) { + throw new RuntimeException("Failed to instantiate Service", e); + } + } + + public Session openSession() { + return sessionFactory.openSession(); + } + + @Override + public void close() { + repositoryBindings.clear(); + entityBindings.clear(); + sessionFactory.close(); + } + + private volatile boolean initialized; + + @Override + public void initialize(Context context) { + if (initialized) { + throw new IllegalStateException("Initialized already"); + } + initialized = true; + if (databaseConfiguration == null) { + loadDatabaseConfig(new H2DatabaseInFileConfiguration(context.getConfig().getDatabaseFile())); + } + managedRepositoryClasses.forEach(aClass -> { + var instance = instantiateRepository(aClass); + var baseClass = instance.getBaseClass(); + repositoryBindings.put(baseClass, instance); + var entityClass = instance.getEntityClass(); + entityBindings.put(entityClass, baseClass); + }); + mapper = JsonMapper.builder().addModule(new PersistenceSerializationModule(this)).build(); + sessionFactory = this.configuration.buildSessionFactory(); + } + + @Override + public Repository getRepositoryForEntity(Class entityType) { + //noinspection unchecked + return (Repository) getRepository(entityBindings.get(entityType)); + } + + @Override + public > R getRepository(Class repositoryType) { + //noinspection unchecked + return (R) repositoryBindings.get(repositoryType); + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Services/UserRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Services/UserRepositoryImpl.java new file mode 100644 index 0000000..d55a5ab --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Services/UserRepositoryImpl.java @@ -0,0 +1,130 @@ +package ru.kirillius.XCP.Persistence.Services; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.UuidGenerator; +import ru.kirillius.XCP.Persistence.AbstractRepository; +import ru.kirillius.XCP.Persistence.EntityImplementation; +import ru.kirillius.XCP.Persistence.RepositoryServiceImpl; +import ru.kirillius.XCP.Serialization.SerializationUtils; +import ru.kirillius.XCP.api.Persistence.Entities.User; +import ru.kirillius.XCP.api.Persistence.PersistenceEntity; +import ru.kirillius.XCP.api.Persistence.Repositories.UserRepository; +import ru.kirillius.XCP.api.Persistence.Repository; +import ru.kirillius.XCP.api.Security.UserRole; +import tools.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.UUID; + +@EntityImplementation(UserRepositoryImpl.UserEntity.class) +public class UserRepositoryImpl extends AbstractRepository implements UserRepository { + + public UserRepositoryImpl(RepositoryServiceImpl repositoryService) { + super(repositoryService); + } + + @Override + public Class> getBaseClass() { + return UserRepository.class; + } + + @Override + public User getByLoginAndPassword(String login, String password) { + var user = (UserEntity) getByLogin(login); + if (user == null) { + return null; + } + if (user.getPasswordHash().equals(UserEntity.hash(password, user.getPasswordHashSalt()))) { + return user; + } + return null; + } + + @Override + public User getByLogin(String login) { + try (var request = buildQueryParametrized("WHERE login = ?1", login)) { + return request.get().findFirst().orElse(null); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Entity + @Table(name = "UserEntity") + @Builder + @AllArgsConstructor + @NoArgsConstructor + @Getter + @Setter + public static class UserEntity implements User { + @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 login; + + @Column(nullable = false) + @JsonProperty + private String name = ""; + + @Column(nullable = false) + @JsonProperty + private String passwordHash; + + @Column(nullable = false) + @JsonProperty + private String passwordHashSalt; + + @Column(nullable = false) + @JsonProperty + @Enumerated(EnumType.STRING) + private UserRole role; + + @Column(name = "custom_values", nullable = false) + @Getter + @Setter + @JsonProperty + private ObjectNode values = SerializationUtils.EmptyObject(); + + public static String hash(String data, String salt) { + String generatedPassword; + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + md.update(salt.getBytes(StandardCharsets.UTF_8)); + var bytes = md.digest(data.getBytes(StandardCharsets.UTF_8)); + var sb = new StringBuilder(); + for (byte aByte : bytes) { + sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1)); + } + generatedPassword = sb.toString(); + return generatedPassword; + } + + public void changePassword(String newPass) { + passwordHashSalt = java.util.UUID.randomUUID().toString(); + passwordHash = hash(newPass, passwordHashSalt); + } + + @Override + public Class getBaseType() { + return User.class; + } + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Serialization/SerializationUtils.java b/database/src/main/java/ru/kirillius/XCP/Serialization/SerializationUtils.java new file mode 100644 index 0000000..d3c67e3 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Serialization/SerializationUtils.java @@ -0,0 +1,17 @@ +package ru.kirillius.XCP.Serialization; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; + +public final class SerializationUtils { + public final static ObjectMapper mapper = new ObjectMapper(); + + public static ObjectNode EmptyObject() { + return mapper.createObjectNode(); + } + + public static ArrayNode EmptyArray() { + return mapper.createArrayNode(); + } +} diff --git a/database/src/main/resources/hibernate.cfg.xml b/database/src/main/resources/hibernate.cfg.xml new file mode 100644 index 0000000..2fdfdcf --- /dev/null +++ b/database/src/main/resources/hibernate.cfg.xml @@ -0,0 +1,39 @@ + + + + + org.h2.Driver + + sa + sa + + 1 + + + + true + + thread + + + update + + 5 + 20 + 10 + 5 + -1 + + org.hibernate.connection.C3P0ConnectionProvider + 1 + 60 + 1 + 2 + 50 + 0 + 1 + 250 + + \ No newline at end of file diff --git a/database/src/test/java/TestContext.java b/database/src/test/java/TestContext.java new file mode 100644 index 0000000..4cf31c5 --- /dev/null +++ b/database/src/test/java/TestContext.java @@ -0,0 +1,26 @@ +import ru.kirillius.XCP.api.Commons.Config; +import ru.kirillius.XCP.api.Commons.ConfigManager; +import ru.kirillius.XCP.api.Commons.Context; +import ru.kirillius.XCP.api.Commons.Service; + +public class TestContext implements Context { + @Override + public Config getConfig() { + return null; + } + + @Override + public ConfigManager getConfigManager() { + return null; + } + + @Override + public S getService(Class serviceClass) { + return null; + } + + @Override + public void shutdown() { + + } +} diff --git a/database/src/test/java/ru/kirillius/XCP/Persistence/H2InMemoryConfiguration.java b/database/src/test/java/ru/kirillius/XCP/Persistence/H2InMemoryConfiguration.java new file mode 100644 index 0000000..7bcbe9d --- /dev/null +++ b/database/src/test/java/ru/kirillius/XCP/Persistence/H2InMemoryConfiguration.java @@ -0,0 +1,10 @@ +package ru.kirillius.XCP.Persistence; + +import java.util.UUID; + +public class H2InMemoryConfiguration implements DatabaseConfiguration { + @Override + public String getConnectionUrl() { + return "jdbc:h2:mem:" + "testdb_" + UUID.randomUUID().toString().substring(0, 8); + } +} diff --git a/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java b/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java new file mode 100644 index 0000000..0249671 --- /dev/null +++ b/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java @@ -0,0 +1,192 @@ +package ru.kirillius.XCP.Persistence; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.UuidGenerator; +import org.junit.jupiter.api.Test; +import ru.kirillius.XCP.api.Commons.Context; +import ru.kirillius.XCP.api.Persistence.PersistenceEntity; +import ru.kirillius.XCP.api.Persistence.Repository; + +import java.io.IOException; +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class RepositoryServiceImplTest { + private RepositoryServiceImpl instantiateTestService(Collection>> classes) { + var service = new RepositoryServiceImpl(new H2InMemoryConfiguration(), classes); + service.initialize(mock(Context.class)); + return service; + } + + public interface TestEntity extends PersistenceEntity { + String getTestField(); + + void setTestField(String data); + } + + public interface TestRepository extends Repository { + } + + @EntityImplementation(RepoImpl.EntityImpl.class) + public static class RepoImpl extends AbstractRepository implements TestRepository { + + public RepoImpl(RepositoryServiceImpl repositoryService) { + super(repositoryService); + } + + @Override + public Class> getBaseClass() { + return TestRepository.class; + } + + @Entity + @Table + public static class EntityImpl implements TestEntity { + @Getter + @Setter + @JsonProperty + private String testField = "empty"; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonProperty + @Getter + @Setter + private long id = 0; + + @JsonProperty + @Column(unique = true, nullable = false) + @UuidGenerator + @Getter + @Setter + private UUID UUID; + + @Override + public Class getBaseType() { + return null; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof EntityImpl entity)) return false; + return id == entity.id && Objects.equals(testField, entity.testField) && Objects.equals(UUID, entity.UUID); + } + + @Override + public int hashCode() { + return Objects.hash(testField, id, UUID); + } + } + } + + @Test + public void TestERepositoryInstantiate() { + try (var service = instantiateTestService(List.of(RepoImpl.class))) { + var repository = service.getRepository(TestRepository.class); + assertThat(repository).isNotNull().isInstanceOf(TestRepository.class); + } + } + + @Test + public void TestEntityCreate() { + try (var service = instantiateTestService(List.of(RepoImpl.class))) { + var repository = service.getRepository(TestRepository.class); + var testEntity = repository.create(); + assertThat(testEntity).isNotNull().isInstanceOf(TestEntity.class); + } + } + + @Test + public void TestEntityStore() { + try (var service = instantiateTestService(List.of(RepoImpl.class))) { + var repository = service.getRepository(TestRepository.class); + var testEntity = repository.create(); + testEntity.setTestField("new"); + repository.store(List.of(testEntity)); + assertThat(testEntity.getId()).isNotZero(); + } + } + + @Test + public void TestEntityLoad() { + try (var service = instantiateTestService(List.of(RepoImpl.class))) { + var repository = service.getRepository(TestRepository.class); + + assertThat(repository.load(1)).isNull(); + + var testEntity = repository.create(); + testEntity.setTestField("new"); + repository.store(List.of(testEntity)); + + var receivedEntity = repository.load(testEntity.getId()); + assertThat(receivedEntity).isNotNull().isEqualTo(testEntity); + assertThat(testEntity == receivedEntity).isFalse();//not exact instance + } + } + + @Test + public void TestEntityLoadMultiple() throws IOException { + try (var service = instantiateTestService(List.of(RepoImpl.class))) { + var repository = service.getRepository(TestRepository.class); + try (var bundle = repository.loadAll()) { + assertThat(bundle.get().toList()).isEmpty(); + } + + var entitiesToSave = new ArrayList(); + + for (var i = 0; i < 10; i++) { + var entity = repository.create(); + entity.setTestField("instance " + i); + entitiesToSave.add(entity); + } + + repository.store(entitiesToSave); + + try (var bundle = repository.load(entitiesToSave.stream().map(PersistenceEntity::getId).toList())) { + var loaded = bundle.get().toList(); + assertThat(loaded).containsExactlyInAnyOrderElementsOf(entitiesToSave); + } + + try (var bundle = repository.loadAll()) { + var loaded = bundle.get().toList(); + assertThat(loaded).containsExactlyInAnyOrderElementsOf(entitiesToSave); + } + } + } + + @Test + public void TestEntityUpdate() { + try (var service = instantiateTestService(List.of(RepoImpl.class))) { + var repository = service.getRepository(TestRepository.class); + var testEntity = repository.create(); + testEntity.setTestField("new"); + repository.store(List.of(testEntity)); + + testEntity.setTestField("updated"); + repository.store(testEntity); + + var receivedEntity = repository.load(testEntity.getId()); + assertThat(receivedEntity).isNotNull().isEqualTo(testEntity); + assertThat(testEntity == receivedEntity).isFalse();//not exact instance + } + } + + @Test + public void TestEntityRemove() { + try (var service = instantiateTestService(List.of(RepoImpl.class))) { + var repository = service.getRepository(TestRepository.class); + var testEntity = repository.create(); + repository.store(List.of(testEntity)); + assertThat(repository.getCount()).isEqualTo(1); + repository.remove(List.of(testEntity)); + assertThat(repository.load(testEntity.getId())).isNull(); + assertThat(repository.getCount()).isZero(); + + } + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e256c5c --- /dev/null +++ b/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + + ru.kirillius + XCP + 1.0.0.0 + pom + + api + database + + + + 21 + 21 + UTF-8 + 1.18.40 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${maven.compiler.source} + ${maven.compiler.target} + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + + + + + kirillius + kirillius + https://repo.kirillius.ru/maven + + true + always + fail + + default + + + + + + + + + + + + org.junit.jupiter + junit-jupiter-api + 5.13.0-M2 + test + + + + + org.mockito + mockito-core + 5.21.0 + test + + + + + org.assertj + assertj-core + 4.0.0-M1 + test + + + + org.javatuples + javatuples + 1.2 + + + + + ru.kirillius.utils + common-logging + 1.3.0.0 + + + + + org.javassist + javassist + 3.29.2-GA + + + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + + \ No newline at end of file