initial architecture
This commit is contained in:
parent
bdbc189921
commit
e29286cce3
|
|
@ -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>
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.kirillius.XCP.api.Commons;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
|
||||||
|
public interface ResourceHandler<T> extends Closeable {
|
||||||
|
T get();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.kirillius.XCP.api.Commons;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
|
||||||
|
public interface Service extends Closeable {
|
||||||
|
void initialize(Context context);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package ru.kirillius.XCP.api.Commons;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public interface StreamHandler<T> extends ResourceHandler<Stream<T>> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package ru.kirillius.XCP.api.Data;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public interface DataTransferProtocol {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package ru.kirillius.XCP.api.Data;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public interface ValueModifier {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package ru.kirillius.XCP.Persistence;
|
||||||
|
|
||||||
|
public interface DatabaseConfiguration {
|
||||||
|
String getConnectionUrl();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"), "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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> <!– TODO cahnge to UPDATE ! –>-->
|
||||||
|
<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>
|
||||||
|
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue