initial architecture

This commit is contained in:
kirillius 2025-12-26 01:35:13 +03:00
parent bdbc189921
commit e29286cce3
42 changed files with 1519 additions and 0 deletions

27
api/pom.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.kirillius</groupId>
<artifactId>XCP</artifactId>
<version>1.0.0.0</version>
</parent>
<artifactId>api</artifactId>
<dependencies>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>java-events</artifactId>
<version>1.1.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/tools.jackson.core/jackson-databind -->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

@ -0,0 +1,11 @@
package ru.kirillius.XCP.api.Commons;
public interface Context {
Config getConfig();
ConfigManager getConfigManager();
<S extends Service> S getService(Class<S> serviceClass);
void shutdown();
}

View File

@ -0,0 +1,7 @@
package ru.kirillius.XCP.api.Commons;
import java.io.Closeable;
public interface ResourceHandler<T> extends Closeable {
T get();
}

View File

@ -0,0 +1,7 @@
package ru.kirillius.XCP.api.Commons;
import java.io.Closeable;
public interface Service extends Closeable {
void initialize(Context context);
}

View File

@ -0,0 +1,7 @@
package ru.kirillius.XCP.api.Commons;
import java.util.stream.Stream;
public interface StreamHandler<T> extends ResourceHandler<Stream<T>> {
}

View File

@ -0,0 +1,6 @@
package ru.kirillius.XCP.api.Data;
@Deprecated
public interface DataTransferProtocol {
//TODO
}

View File

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

View File

@ -0,0 +1,6 @@
package ru.kirillius.XCP.api.Data;
@Deprecated
public interface ValueModifier {
//TODO
}

View File

@ -0,0 +1,14 @@
package ru.kirillius.XCP.api.Data;
import tools.jackson.databind.node.ObjectNode;
public interface ValueModifierSettings {
Class<? extends ValueModifier> getModifierClass();
void setModifierClass(Class<? extends ValueModifier> modifierClass);
ObjectNode getParameters();
void setParameters(ObjectNode parameters);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<ValueModifierSettings> getModifiers();
void setModifiers(List<ValueModifierSettings> modifiers);
Class<? extends DataTransferProtocol> getProtocol();
void setProtocol(Class<? extends DataTransferProtocol> protocol);
}

View File

@ -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<String> getTags();
void setTags(Set<String> tags);
}

View File

@ -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<E extends NodeEntity> extends Repository<E> {
StreamHandler<E> getByGroup(Group group);
StreamHandler<E> getByTags(Collection<String> tags, TagSearchMode searchMode);
enum TagSearchMode {
MatchAnyTag,
MatchAllTags,
MatchExactTags
}
}

View File

@ -0,0 +1,11 @@
package ru.kirillius.XCP.api.Persistence;
import java.util.UUID;
public interface PersistenceEntity {
long getId();
UUID getUUID();
Class<? extends PersistenceEntity> getBaseType();
}

View File

@ -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<Input> {
StreamHandler<Group> getChildrenOf(Group dataGroup);
StreamHandler<Group> getChildrenRecursiveOf(Group dataGroup);
Group getRoot();
}

View File

@ -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<Input> {
}

View File

@ -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<Input> {
}

View File

@ -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> {
User getByLoginAndPassword(String login, String password);
User getByLogin(String login);
}

View File

@ -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 extends PersistenceEntity> {
E create();
E load(long id);
E load(UUID uuid);
StreamHandler<E> load(Collection<Long> ids);
StreamHandler<E> search(String query, Collection<Object> queryParameters);
EventBindings<E> events();
StreamHandler<E> loadAll();
long getCount();
void store(E entity);
void store(Collection<E> entities);
void remove(E entity);
void remove(Collection<E> entities);
ObjectNode serialize(E entity);
ArrayNode serialize(Collection<E> entities);
E deserialize(ObjectNode object);
Collection<E> deserialize(ArrayNode array);
interface EventBindings<E> {
EventHandler<E> entityStored();
EventHandler<E> entityRemoved();
}
}

View File

@ -0,0 +1,9 @@
package ru.kirillius.XCP.api.Persistence;
import ru.kirillius.XCP.api.Commons.Service;
public interface RepositoryService extends Service {
<E extends PersistenceEntity> Repository<E> getRepositoryForEntity(Class<E> entityType);
<R extends Repository<?>> R getRepository(Class<R> repositoryType);
}

View File

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

46
database/pom.xml Normal file
View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.kirillius</groupId>
<artifactId>XCP</artifactId>
<version>1.0.0.0</version>
</parent>
<artifactId>database</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>7.1.10.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-c3p0 -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>7.1.10.Final</version>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>api</artifactId>
<version>1.0.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<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 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();
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<? extends Repository<E>> getBaseClass();
@Override
public StreamHandler<E> search(String query, Collection<Object> 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<E> load(Collection<Long> ids) {
if (ids != null && !ids.isEmpty()) {
return buildQueryParametrized("where id IN (" + joinIdentifiers(ids) + ")");
} else {
return new EmptyRequest<>();
}
}
@Override
public Repository.EventBindings<E> events() {
return eventBindings;
}
@Override
public StreamHandler<E> 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<E> 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<E> entities) {
entities.forEach(this::remove);
}
@Override
public ObjectNode serialize(E entity) {
return repositoryService.getMapper().valueToTree(entity);
}
@Override
public ArrayNode serialize(Collection<E> 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<E> deserialize(ArrayNode array) {
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(", ");
for (var id : ids) {
joiner.add(id.toString());
}
return joiner.toString();
}
@SuppressWarnings({"unchecked", "SqlSourceToSinkFlow"})
protected StreamHandler<E> buildQuery(String condition, Object[] parameters) {
var session = repositoryService.openSession();
var transaction = session.beginTransaction();
var queryString = "from " + tableName + " " + condition;
var query = (Query<E>) 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<E> buildQueryParametrized(String condition, Object... parameters) {
return buildQuery(condition, parameters);
}
private static class EventBindingsImpl<T> implements Repository.EventBindings<T> {
private final EventHandler<T> stored = new ConcurrentEventHandler<>();
private final EventHandler<T> removed = new ConcurrentEventHandler<>();
@Override
public EventHandler<T> entityStored() {
return stored;
}
@Override
public EventHandler<T> entityRemoved() {
return removed;
}
}
protected static class EmptyRequest<T> implements StreamHandler<T> {
@Override
public Stream<T> get() {
return Stream.empty();
}
@Override
public void close() throws IOException {
}
}
protected static class ResourceHandlerImpl<T> implements StreamHandler<T> {
private final Query<T> query;
private final Transaction transaction;
private final Session session;
public ResourceHandlerImpl(Query<T> query, Transaction transaction, Session session) {
this.query = query;
this.transaction = transaction;
this.session = session;
}
@Override
public Stream<T> get() {
return query.getResultStream();
}
@Override
public void close() {
if (transaction != null && transaction.isActive()) {
transaction.commit();
}
if (session.isOpen()) {
session.close();
}
}
}
}

View File

@ -0,0 +1,5 @@
package ru.kirillius.XCP.Persistence;
public interface DatabaseConfiguration {
String getConnectionUrl();
}

View File

@ -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<? extends PersistenceEntity> value();
}

View File

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

View File

@ -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<PersistenceEntity> {
public EntityReferenceSerializer() {
this(PersistenceEntity.class);
}
protected EntityReferenceSerializer(Class<PersistenceEntity> 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();
}
}

View File

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

View File

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

View File

@ -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<Class<? extends Repository<?>>, Repository<?>> repositoryBindings = new ConcurrentHashMap<>();
private final Map<Class<? extends PersistenceEntity>, Class<? extends Repository<?>>> entityBindings = new ConcurrentHashMap<>();
private final Collection<Class<? extends AbstractRepository<?>>> managedRepositoryClasses;
public RepositoryServiceImpl(DatabaseConfiguration databaseConfiguration, Collection<Class<? extends AbstractRepository<?>>> 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<Class<? extends AbstractRepository<?>>> repositoryImplClasses) {
managedRepositoryClasses = repositoryImplClasses;
configuration = new Configuration();
configuration.configure();
registerClasses(repositoryImplClasses);
}
private void registerClasses(Collection<Class<? extends AbstractRepository<?>>> 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<? extends AbstractRepository<?>> 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 <E extends PersistenceEntity> Repository<E> getRepositoryForEntity(Class<E> entityType) {
//noinspection unchecked
return (Repository<E>) getRepository(entityBindings.get(entityType));
}
@Override
public <R extends Repository<?>> R getRepository(Class<R> repositoryType) {
//noinspection unchecked
return (R) repositoryBindings.get(repositoryType);
}
}

View File

@ -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<User> implements UserRepository {
public UserRepositoryImpl(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@Override
public Class<? extends Repository<User>> 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<? extends PersistenceEntity> getBaseType() {
return User.class;
}
}
}

View File

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

View File

@ -0,0 +1,39 @@
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!-- JDBC Database connection settings -->
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.url"></property>
<property name="connection.username">sa</property>
<property name="connection.password">sa</property>
<!-- JDBC connection pool settings ... using built-in test pool -->
<property name="connection.pool_size">1</property>
<!-- Select our SQL dialect -->
<!-- Echo the SQL to stdout -->
<property name="show_sql">true</property>
<!-- Set the current session context -->
<property name="current_session_context_class">thread</property>
<!-- Drop and re-create the database schema on startup -->
<!-- <property name="hbm2ddl.auto">create-drop</property> &lt;!&ndash; TODO cahnge to UPDATE ! &ndash;&gt;-->
<property name="hbm2ddl.auto">update</property>
<!-- dbcp connection pool configuration -->
<property name="hibernate.dbcp.initialSize">5</property>
<property name="hibernate.dbcp.maxTotal">20</property>
<property name="hibernate.dbcp.maxIdle">10</property>
<property name="hibernate.dbcp.minIdle">5</property>
<property name="hibernate.dbcp.maxWaitMillis">-1</property>
<!-- c3p0 config http://www.hibernate.org/214.html -->
<property name="connection.provider_class">org.hibernate.connection.C3P0ConnectionProvider</property>
<property name="hibernate.c3p0.acquire_increment">1</property>
<property name="hibernate.c3p0.idle_test_period">60</property>
<property name="hibernate.c3p0.min_size">1</property>
<property name="hibernate.c3p0.max_size">2</property>
<property name="hibernate.c3p0.max_statements">50</property>
<property name="hibernate.c3p0.timeout">0</property>
<property name="hibernate.c3p0.acquireRetryAttempts">1</property>
<property name="hibernate.c3p0.acquireRetryDelay">250</property>
</session-factory>
</hibernate-configuration>

View File

@ -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 extends Service> S getService(Class<S> serviceClass) {
return null;
}
@Override
public void shutdown() {
}
}

View File

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

View File

@ -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<Class<? extends AbstractRepository<?>>> 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<TestEntity> {
}
@EntityImplementation(RepoImpl.EntityImpl.class)
public static class RepoImpl extends AbstractRepository<TestEntity> implements TestRepository {
public RepoImpl(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@Override
public Class<? extends Repository<TestEntity>> 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<? extends PersistenceEntity> 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<TestEntity>();
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();
}
}
}

122
pom.xml Normal file
View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.kirillius</groupId>
<artifactId>XCP</artifactId>
<version>1.0.0.0</version>
<packaging>pom</packaging>
<modules>
<module>api</module>
<module>database</module>
</modules>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.40</lombok.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>kirillius</id>
<name>kirillius</name>
<url>https://repo.kirillius.ru/maven</url>
<releases>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</releases>
<layout>default</layout>
</repository>
</repositories>
<dependencies>
<!-- <dependency>-->
<!-- <groupId>ru.kirillius</groupId>-->
<!-- <artifactId>json-convert</artifactId>-->
<!-- <version>2.2.0.0</version>-->
<!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.13.0-M2</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.21.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>4.0.0-M1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.javatuples</groupId>
<artifactId>javatuples</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>ru.kirillius.utils</groupId>
<artifactId>common-logging</artifactId>
<version>1.3.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>