Compare commits

...

17 Commits

Author SHA1 Message Date
kirillius b46d1bc359 работа на ui фронтом и автогенерацией api клиента 2026-01-28 12:02:46 +03:00
kirillius 4032586662 api generation impromenet 2026-01-21 14:43:18 +03:00
kirillius f882cf4c3a Правки сериализации, persistence и api-sandbox 2026-01-19 13:09:24 +03:00
kirill.labutin 9e5b70161d добавил модули для web-ui 2026-01-14 21:57:58 +03:00
kirill.labutin 3f022de862 Удалил лишнее 2026-01-14 18:11:42 +03:00
kirillius 381fda5252 Удалил SerializationUtils, добавил генератор спецификации api 2026-01-14 18:05:40 +03:00
kirill.labutin 3ad2f762e0 Простой генератор JSON-RPC клиента для JS 2026-01-12 18:49:30 +03:00
kirill.labutin 8a2d52b9d2 add api-generator module 2026-01-12 18:21:34 +03:00
kirill.labutin 7e52c4735f Правки JSON-RPC, ConfigManager, системы логгирования. Бамп версии H2 2026-01-12 14:46:08 +03:00
kirillius 898a170d7b Добавил ApiToken entity для авторизации, контекст для RPC, реализовал ConfigManager, логгирование и RPC 2026-01-12 10:14:17 +03:00
kirillius 0d202b5574 Убрал зависимость db от core. Добавил инжекцию context в Entity 2026-01-08 17:43:20 +03:00
kirillius dcb15ebdc1 Добавил тесты и стабилизировал сущности. Рефакторинг ValueTransformationChain 2026-01-08 09:47:38 +03:00
kirill.labutin 2d7c7fea9c WIP: промежуточный коммит 2026-01-02 23:56:25 +03:00
kirillius b264b38fe4 WIP: serialization tests, entity references 2026-01-01 10:59:02 +03:00
kirillius f50fd2a474 Обновил тесты 2025-12-26 07:20:34 +03:00
kirillius 8e2412f36a Переименовал package, порефакторил юзеров 2025-12-26 06:12:03 +03:00
kirillius e29286cce3 initial architecture 2025-12-26 01:35:13 +03:00
156 changed files with 17306 additions and 0 deletions

8
.gitignore vendored
View File

@ -38,3 +38,11 @@ build/
.DS_Store
/.idea/
/.mvn/
xcp.conf
xcpdata.mv.db
/web-ui/api.spec.json
api-sandbox/app/node_modules
api-sandbox/app/api.spec.json
/xcpdata.trace.db
web-ui/vue-app/TODO.md
web-ui/vue-app/src/generated/*

124
api-sandbox/app/index.html Normal file
View File

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Debug Console</title>
<style>
body {
font-family: 'Courier New', monospace;
margin: 0;
background-color: #1a1a1a;
color: #ffffff;
height: 100vh;
overflow: hidden;
}
.container {
display: flex;
height: 100vh;
}
.left-panel {
flex: 1;
padding: 20px;
border-right: 1px solid #ffffff;
overflow-y: auto;
}
.right-panel {
flex: 1;
padding: 20px;
overflow-y: auto;
}
h1 { color: #ffffff; border-bottom: 1px solid #ffffff; padding-bottom: 10px; margin-top: 0; }
.form-group { margin: 10px 0; }
label { display: block; margin-bottom: 5px; color: #ffffff; }
select, input, textarea {
padding: 8px;
width: 100%;
max-width: 100%;
background: transparent;
border: 1px solid #ffffff;
color: #ffffff;
outline: none;
box-sizing: border-box;
}
select option { background: #1a1a1a; color: #ffffff; }
button {
padding: 10px 20px;
background: transparent;
color: #ffffff;
border: 1px solid #ffffff;
cursor: pointer;
outline: none;
margin-top: 10px;
}
button:hover {
background: #ffffff;
color: #1a1a1a;
}
#result, #request {
margin-top: 20px;
white-space: pre-wrap;
background: transparent;
padding: 10px;
font-family: 'Courier New', monospace;
overflow-y: auto;
}
.params { margin-top: 10px; }
.param { margin-bottom: 15px; }
.param-header { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; }
.checkbox-wrapper { display: flex; align-items: center; gap: 5px; }
.checkbox-wrapper input[type="checkbox"] { width: auto; margin: 0; accent-color: #ffffff; }
.checkbox-wrapper label { margin: 0; font-weight: normal; white-space: nowrap; color: #ffffff; }
input:disabled, textarea:disabled {
background: transparent;
color: #666;
border-color: #666;
}
input:focus, select:focus, textarea:focus {
border-color: #ffffff;
box-shadow: 0 0 2px #ffffff;
}
textarea {
resize: vertical;
min-height: 80px;
}
</style>
</head>
<body>
<div class="container">
<div class="left-panel">
<div id="app">
<h1>API Debug Console</h1>
<div id="user-info" class="form-group" style="margin-bottom: 20px;">
<label>Пользователь:</label>
<span id="username" style="color: #ccc;">не авторизован</span>
</div>
<div class="form-group">
<label for="service">Сервис:</label>
<select id="service"></select>
</div>
<div class="form-group">
<label for="method">Метод:</label>
<select id="method"></select>
</div>
<div id="params-container" class="params"></div>
<button id="send-btn">Отправить</button>
</div>
</div>
<div class="right-panel">
<h1>Result</h1>
<div id="result"></div>
<h1 style="margin-top: 30px;">Request</h1>
<div id="request"></div>
</div>
</div>
<script type="module" src="/main.js"></script>
</body>
</html>

266
api-sandbox/app/main.js Normal file
View File

@ -0,0 +1,266 @@
import $ from 'jquery'
let apiSpec = null
let currentUser = null
async function loadUserProfile() {
try {
const requestData = {
jsonrpc: '2.0',
method: 'Profile.get',
params: {},
id: Date.now()
}
const response = await fetch('http://localhost:8080/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(requestData)
})
const result = await response.json()
if (result.result) {
currentUser = result.result
$('#username').text(currentUser.name || 'неизвестный пользователь')
} else {
currentUser = null
$('#username').text('не авторизован')
}
} catch (error) {
console.error('Ошибка загрузки профиля:', error)
currentUser = null
$('#username').text('не авторизован')
}
}
async function loadApiSpec() {
try {
const response = await fetch('/api.spec.json', {
method: 'GET',
headers: {
'Cache-Control': 'no-cache',
'Accept': 'application/json'
}
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = await response.json()
apiSpec = data.modules
console.log('API Spec loaded:', data)
return apiSpec
} catch (error) {
console.error('Ошибка загрузки API спецификации:', error)
$('#result').text('Ошибка загрузки API спецификации: ' + error.message)
}
}
function populateServices() {
const $serviceSelect = $('#service')
$serviceSelect.empty()
$serviceSelect.append('<option value="">Выберите сервис</option>')
apiSpec.forEach(service => {
$serviceSelect.append(`<option value="${service.name}">${service.name}</option>`)
})
}
function populateMethods(serviceName) {
const $methodSelect = $('#method')
$methodSelect.empty()
$methodSelect.append('<option value="">Выберите метод</option>')
if (serviceName) {
const service = apiSpec.find(s => s.name === serviceName)
if (service && service.methods) {
service.methods.forEach(method => {
$methodSelect.append(`<option value="${method.name}">${method.name}</option>`)
})
}
}
}
function createParamInputs(methodName, serviceName) {
const $paramsContainer = $('#params-container')
$paramsContainer.empty()
if (!serviceName || !methodName) return
const service = apiSpec.find(s => s.name === serviceName)
if (!service || !service.methods) return
const method = service.methods.find(m => m.name === methodName)
if (!method || !method.params) return
method.params.forEach(param => {
const required = !param.optional ? ' (обязательно)' : ' (необязательно)'
const isOptional = param.optional
const isObjectOrArray = param.type === 'object' || param.type === 'array'
const defaultValue = param.type === 'object' ? '{}' : (param.type === 'array' ? '[]' : '')
const inputElement = isObjectOrArray
? `<textarea id="param-${param.name}" data-param="${param.name}" placeholder="${param.description}" rows="4" style="font-family: 'Courier New', monospace;">${defaultValue}</textarea>`
: `<input type="text" id="param-${param.name}" data-param="${param.name}" placeholder="${param.description}">`
if (isOptional) {
const $paramDiv = $(`
<div class="param">
<div class="param-header">
<label for="param-${param.name}">${param.name}${required} (${param.type}):</label>
<div class="checkbox-wrapper">
<input type="checkbox" id="defined-${param.name}" data-param="${param.name}" class="defined-checkbox">
<label for="defined-${param.name}">is defined</label>
</div>
</div>
${inputElement}
</div>
`)
$paramsContainer.append($paramDiv)
$(`#defined-${param.name}`).on('change', function () {
const $input = $(`#param-${param.name}`)
const isChecked = $(this).is(':checked')
$input.prop('disabled', !isChecked)
if (!isChecked) {
$input.val(isObjectOrArray ? defaultValue : '')
}
})
// Initially disable optional fields
$(`#param-${param.name}`).prop('disabled', true)
} else {
const $paramDiv = $(`
<div class="param">
<label for="param-${param.name}">${param.name}${required} (${param.type}):</label>
${inputElement}
</div>
`)
$paramsContainer.append($paramDiv)
}
})
}
async function sendRequest() {
const serviceName = $('#service').val()
const methodName = $('#method').val()
if (!serviceName || !methodName) {
$('#result').text('Выберите сервис и метод')
return
}
const params = {}
let parseError = null
$('.param').each(function () {
const $checkbox = $(this).find('.defined-checkbox')
const $input = $(this).find('input[type="text"], textarea')
const paramName = $input.data('param')
const paramType = $input.attr('id').replace('param-', '')
// Find parameter type from method definition
const service = apiSpec.find(s => s.name === serviceName)
const method = service.methods.find(m => m.name === methodName)
const paramDef = method.params.find(p => p.name === paramName)
const isObjectOrArray = paramDef && (paramDef.type === 'object' || paramDef.type === 'array')
if ($checkbox.length > 0) {
// Optional parameter
if ($checkbox.is(':checked') && $input.val().trim() !== '') {
const value = $input.val().trim()
if (isObjectOrArray) {
try {
params[paramName] = JSON.parse(value)
} catch (e) {
parseError = `Ошибка парсинга JSON для поля "${paramName}": ${e.message}`
}
} else {
params[paramName] = value
}
}
} else {
// Required parameter
const value = $input.val().trim()
if (value !== '') {
if (isObjectOrArray) {
try {
params[paramName] = JSON.parse(value)
} catch (e) {
parseError = `Ошибка парсинга JSON для поля "${paramName}": ${e.message}`
}
} else {
params[paramName] = value
}
}
}
})
if (parseError) {
$('#result').text(parseError)
$('#request').text(JSON.stringify(requestData, null, 2))
return
}
const requestData = {
jsonrpc: '2.0',
method: `${serviceName}.${methodName}`,
params: params,
id: Date.now()
}
$('#result').text('Отправка запроса...')
$('#request').text('')
try {
const response = await fetch('http://localhost:8080/api', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(requestData)
})
const result = await response.json()
$('#result').text(JSON.stringify(result, null, 2))
$('#request').text(JSON.stringify(requestData, null, 2))
// Проверяем, нужно ли обновить профиль пользователя
if (result.result && (
methodName.startsWith('authenticateBy') ||
methodName === 'logout'
)) {
await loadUserProfile()
}
} catch (error) {
$('#result').text(`Ошибка: ${error.message}`)
$('#request').text(JSON.stringify(requestData, null, 2))
}
}
$(document).ready(async () => {
apiSpec = await loadApiSpec()
if (apiSpec) {
populateServices()
// Автоматически загружаем профиль пользователя
await loadUserProfile()
$('#service').on('change', function () {
const serviceName = $(this).val()
populateMethods(serviceName)
$('#params-container').empty()
})
$('#method').on('change', function () {
const serviceName = $('#service').val()
const methodName = $(this).val()
createParamInputs(methodName, serviceName)
})
$('#send-btn').on('click', sendRequest)
}
})

1664
api-sandbox/app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite",
"predev": "node -e \"const fs=require('fs'); try{fs.copyFileSync('../../api.spec.json','./api.spec.json');console.log('Copied api.spec.json');}catch(e){console.log('api.spec.json not found');}\"",
"build": "vite build",
"preview": "vite preview",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"jquery": "^3.7.1",
"rollup-plugin-copy": "^3.5.0",
"vite": "^7.3.1"
}
}

View File

@ -0,0 +1,96 @@
import { defineConfig } from 'vite'
import { resolve } from 'path'
import copy from 'rollup-plugin-copy'
export default defineConfig({
root: '.',
build: {
outDir: 'dist',
rollupOptions: {
plugins: [
copy({
targets: [
{
src: resolve(__dirname, '../../api.spec.json'),
dest: 'dist'
}
],
hook: 'writeBundle'
})
]
}
},
resolve: {
alias: {
'@': resolve(__dirname, './')
}
},
server: {
port: 3000,
fs: {
allow: ['..', '../..']
},
historyApiFallback: false
},
publicDir: false,
configureServer(server) {
server.middlewares.use('/', (req, res, next) => {
if (req.url === '/api.spec.json' && req.method === 'GET') {
const fs = require('fs')
const path = require('path')
const apiSpecPath = path.resolve(__dirname, './api.spec.json')
console.log('Serving api.spec.json from:', apiSpecPath)
try {
const content = fs.readFileSync(apiSpecPath, 'utf-8')
res.setHeader('Content-Type', 'application/json')
res.setHeader('Cache-Control', 'no-cache')
res.end(content)
} catch (error) {
console.error('Error reading api.spec.json:', error)
res.statusCode = 404
res.setHeader('Content-Type', 'text/plain')
res.end('File not found')
}
} else {
next()
}
})
// Mock API endpoint for testing
server.middlewares.use('/api', (req, res, next) => {
if (req.method === 'POST') {
let body = ''
req.on('data', chunk => {
body += chunk.toString()
})
req.on('end', () => {
try {
const requestData = JSON.parse(body)
const response = {
jsonrpc: '2.0',
result: {
success: true,
message: `Method ${requestData.method} called with params: ${JSON.stringify(requestData.params)}`,
timestamp: new Date().toISOString()
},
id: requestData.id
}
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(response))
} catch (error) {
res.statusCode = 400
res.end(JSON.stringify({
jsonrpc: '2.0',
error: { code: -32700, message: 'Parse error' },
id: null
}))
}
})
} else {
next()
}
})
}
})

20
api-sandbox/pom.xml Normal file
View File

@ -0,0 +1,20 @@
<?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-sandbox</artifactId>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@ -0,0 +1,48 @@
<?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-spec-generator</artifactId>
<dependencies>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>rpc</artifactId>
<version>1.0.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>generate-spec</id>
<phase>compile</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>ru.kirillius.XCP.ApiGenerator.SpecGenerator</mainClass>
<arguments>
<argument>rpc/src/main/java</argument>
<argument>ru.kirillius.XCP.RPC.Services</argument>
<argument>api.spec.json</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,213 @@
package ru.kirillius.XCP.ApiGenerator;
import lombok.SneakyThrows;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod;
import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Pattern;
public class SpecGenerator {
/**
* Args:
* 1 - path to find classes
* 2 - search in package
* 3 - output spec file path
*
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
var generator = new SpecGenerator(
new File(args[0]),
args[1],
new File(args[2])
);
generator.generate();
generator.writeSpecs();
}
private final File scanDir;
private final String packageName;
private final File outputFile;
public SpecGenerator(File scanDir, String packageName, File outputFile) {
this.scanDir = scanDir;
this.packageName = packageName;
this.outputFile = outputFile;
}
private final ObjectNode specs = JsonNodeFactory.instance.objectNode();
private final Set<Class<?>> types = new HashSet<>();
private ObjectMapper mapper = new ObjectMapper();
@SneakyThrows
public void generate() {
specs.removeAll();
var modules = specs.putArray("modules");
var typesArray = specs.putArray("types");
var scanPath = new File(scanDir, packageName.replaceAll(Pattern.quote("."), "/"));
for (var file : Objects.requireNonNull(scanPath.listFiles())) {
if (!file.getName().endsWith(".java") || file.getName().contains("$")) {
continue;
}
var className = packageName + '.' + file.getName();
className = className.substring(0, className.length() - ".java".length());
var cls = Class.forName(className);
if (!JsonRpcService.class.isAssignableFrom(cls)) {
continue;
}
var classSpecs = modules.addObject();
classSpecs.put("name", cls.getSimpleName());
var methods = classSpecs.putArray("methods");
for (var method : cls.getDeclaredMethods()) {
var descriptor = method.getAnnotation(JsonRpcMethod.class);
if (descriptor == null) {
continue;
}
var methodSpecs = methods.addObject();
methodSpecs.put("name", method.getName());
methodSpecs.put("description", descriptor.description());
methodSpecs.put("return", getTypeName(descriptor.returnType()));
methodSpecs.put("accessLevel", descriptor.accessLevel().name());
registerType(descriptor.returnType());
var params = methodSpecs.putArray("params");
for (var parameter : descriptor.parameters()) {
var paramSpecs = params.addObject();
paramSpecs.put("name", parameter.name());
paramSpecs.put("type", getTypeName(parameter.type()));
paramSpecs.put("description", parameter.description());
paramSpecs.put("optional", parameter.optional());
registerType(parameter.type());
}
}
}
var typesQueue = new LinkedBlockingQueue<>(types);
Class<?> type;
while (!typesQueue.isEmpty()) {
type = typesQueue.poll();
if (type.isEnum()) {
var typeDescriptor = typesArray.addObject();
typeDescriptor.put("name", type.getSimpleName());
typeDescriptor.put("type", "enum");
var values = typeDescriptor.putArray("values");
for (var value : type.getEnumConstants()) {
values.add(value.toString());
}
continue;
}
var classAnnotation = type.getAnnotation(GenerateApiSpec.class);
if (classAnnotation == null || !type.isInterface()) {
continue;
}
var typeDescriptor = typesArray.addObject();
typeDescriptor.put("name", classAnnotation.alias().isEmpty() ? type.getSimpleName() : classAnnotation.alias());
typeDescriptor.put("type", "class");
var fields = typeDescriptor.putArray("fields");
for (var method : type.getMethods()) {
var methodAnnotation = method.getAnnotation(GenerateApiSpec.class);
if (methodAnnotation == null) {
continue;
}
var returnType = methodAnnotation.type();
if (returnType == void.class) {
returnType = method.getReturnType();
}
if (!types.contains(returnType)) {
types.add(returnType);
typesQueue.put(returnType);
}
var methodName = method.getName();
if (!methodName.startsWith("get") && !methodName.startsWith("is")) {
throw new RuntimeException("Invalid @" + GenerateApiSpec.class.getSimpleName() + " usage on " + type.getSimpleName() + "::" + methodName + ": Only getters are supported");
}
var fieldDescriptor = fields.addObject();
fieldDescriptor.put("name", methodAnnotation.alias().isEmpty() ? getVariableName(methodName) : methodAnnotation.alias());
fieldDescriptor.put("type", getTypeName(returnType));
}
}
}
private String getVariableName(String methodName) {
if (methodName.startsWith("get")) {
return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
} else if (methodName.startsWith("is")) {
return methodName.substring(2, 3).toLowerCase() + methodName.substring(3);
}
throw new RuntimeException("Invalid method name: " + methodName);
}
public void writeSpecs() throws IOException {
var parentFile = outputFile.getParentFile();
if (parentFile != null) {
parentFile.mkdirs();
}
try (var stream = new FileOutputStream(outputFile)) {
var mapper = new ObjectMapper();
mapper.writerWithDefaultPrettyPrinter().writeValue(stream, specs);
}
}
private void registerType(Class<?> type) {
if (type.isArray()) {
types.add(type.getComponentType());
} else {
types.add(type);
}
}
private String getTypeName(Class<?> type) {
if (type == null) return "unknown";
if (type == boolean.class) return "boolean";
if (type == int.class || type == long.class) return "number";
if (type == String.class) return "string";
if (type == void.class || type == Void.class) return "void";
if (type.isArray()) {
var componentType = type.getComponentType();
if (componentType != null) {
return componentType.getSimpleName() + "[]";
} else {
return "[]";
}
}
if (Collection.class.isAssignableFrom(type) || type == ArrayNode.class) {
return "array";
}
if (JsonNode.class.isAssignableFrom(type)) {
return "object";
}
return type.getSimpleName();
}
}

243
api.spec.json Normal file
View File

@ -0,0 +1,243 @@
{
"modules": [
{
"name": "Profile",
"methods": [
{
"name": "get",
"description": "Get current user profile",
"return": "User",
"accessLevel": "Guest",
"params": []
},
{
"name": "save",
"description": "edit current user profile",
"return": "boolean",
"accessLevel": "User",
"params": [
{
"name": "login",
"type": "string",
"description": "User login. Have to be unique",
"optional": false
},
{
"name": "name",
"type": "string",
"description": "User display name",
"optional": false
},
{
"name": "password",
"type": "string",
"description": "Change user password if defined",
"optional": true
},
{
"name": "values",
"type": "object",
"description": "User custom values",
"optional": false
}
]
}
]
},
{
"name": "Auth",
"methods": [
{
"name": "generateToken",
"description": "Generates a new API token and returns its details",
"return": "ApiToken",
"accessLevel": "User",
"params": [
{
"name": "permanent",
"type": "boolean",
"description": "If true, creates a token that never expires",
"optional": true
},
{
"name": "name",
"type": "string",
"description": "Display name for the token. If not provided, the User-Agent header will be used",
"optional": true
}
]
},
{
"name": "authenticateByToken",
"description": "Authenticates a user using an API token. Returns true if authentication is successful.",
"return": "boolean",
"accessLevel": "Guest",
"params": [
{
"name": "token",
"type": "string",
"description": "API token string for authentication",
"optional": false
}
]
},
{
"name": "logout",
"description": "Logs out the current user. Returns true if logout is successful, false if the user is not logged in",
"return": "boolean",
"accessLevel": "User",
"params": []
},
{
"name": "authenticateByPassword",
"description": "Authenticates a user using login and password. Returns true if authentication is successful.",
"return": "boolean",
"accessLevel": "Guest",
"params": [
{
"name": "login",
"type": "string",
"description": "User's login name",
"optional": false
},
{
"name": "password",
"type": "string",
"description": "User's password",
"optional": false
}
]
},
{
"name": "getTokens",
"description": "Retrieves all API tokens associated with the current user",
"return": "ApiToken[]",
"accessLevel": "User",
"params": []
}
]
},
{
"name": "Users",
"methods": [
{
"name": "getById",
"description": "Load user object by ID",
"return": "User",
"accessLevel": "Admin",
"params": [
{
"name": "id",
"type": "number",
"description": "User id",
"optional": false
}
]
},
{
"name": "remove",
"description": "Remove user by ID",
"return": "boolean",
"accessLevel": "Admin",
"params": [
{
"name": "id",
"type": "number",
"description": "User id",
"optional": false
}
]
},
{
"name": "save",
"description": "Save user data",
"return": "User",
"accessLevel": "Admin",
"params": [
{
"name": "user",
"type": "User",
"description": "User object to save",
"optional": false
}
]
},
{
"name": "getAll",
"description": "Get all users as list",
"return": "User[]",
"accessLevel": "Admin",
"params": []
}
]
}
],
"types": [
{
"name": "ApiToken",
"type": "class",
"fields": [
{
"name": "expirationDate",
"type": "Date"
},
{
"name": "user",
"type": "PersistenceEntity"
},
{
"name": "name",
"type": "string"
},
{
"name": "uuid",
"type": "UUID"
},
{
"name": "id",
"type": "number"
}
]
},
{
"name": "User",
"type": "class",
"fields": [
{
"name": "login",
"type": "string"
},
{
"name": "name",
"type": "string"
},
{
"name": "values",
"type": "object"
},
{
"name": "role",
"type": "UserRole"
},
{
"name": "uuid",
"type": "UUID"
},
{
"name": "id",
"type": "number"
}
]
},
{
"name": "UserRole",
"type": "enum",
"values": [
"Guest",
"User",
"Operator",
"Admin"
]
}
]
}

21
api/pom.xml Normal file
View File

@ -0,0 +1,21 @@
<?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>
</dependencies>
</project>

View File

@ -0,0 +1,22 @@
package ru.kirillius.XCP.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,16 @@
package ru.kirillius.XCP.Commons;
import java.io.File;
import java.io.IOException;
public interface ConfigManager {
File getConfigFile();
boolean isExist();
Config load() throws IOException;
Config create();
void save(Config config) throws IOException;
}

View File

@ -0,0 +1,24 @@
package ru.kirillius.XCP.Commons;
import ru.kirillius.XCP.Logging.LoggingSystem;
import ru.kirillius.XCP.Security.SecurityManager;
import java.util.List;
public interface Context {
Config getConfig();
ConfigManager getConfigManager();
<S extends Service> S getService(Class<S> serviceClass);
void shutdown();
SecurityManager getSecurityManager();
LoggingSystem getLoggingSystem();
List<String> getLaunchArgs();
boolean isDebuggingEnabled();
}

View File

@ -0,0 +1,14 @@
package ru.kirillius.XCP.Commons;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface GenerateApiSpec {
String alias() default "";
Class<?> type() default void.class;
}

View File

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

View File

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

View File

@ -0,0 +1,5 @@
package ru.kirillius.XCP.Commons;
public interface Service extends Initializable {
}

View File

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

View File

@ -0,0 +1,17 @@
package ru.kirillius.XCP.Data;
import ru.kirillius.XCP.Commons.Initializable;
import ru.kirillius.java.utils.events.EventHandler;
import tools.jackson.databind.node.ObjectNode;
public interface DataAdapter extends Initializable {
Object send(Object value, ObjectNode properties);
Object receive(ObjectNode properties);
EventHandler<Object> subscribe();
void unsubscribe(EventHandler<Object> subscription);
}

View File

@ -0,0 +1,19 @@
package ru.kirillius.XCP.Data;
public interface PollSettings {
long getPollInterval();
void setPollInterval(long pollInterval);
boolean isInterruptable();
void setInterruptable(boolean interruptable);
boolean isRateMeasurement();
void setRateMeasurement(boolean rateMeasurement);
void setEnabled(boolean enabled);
boolean isEnabled();
}

View File

@ -0,0 +1,24 @@
package ru.kirillius.XCP.Data;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import tools.jackson.databind.annotation.JsonDeserialize;
import java.util.ArrayList;
import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
public final class ValueTransformationChain {
@Getter
@Setter
@JsonSetter(nulls = Nulls.SKIP)
@JsonProperty
@JsonDeserialize(contentAs = ValueTransformationStep.class)
private List<ValueTransformationStep> steps = new ArrayList<>();
}

View File

@ -0,0 +1,19 @@
package ru.kirillius.XCP.Data;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import tools.jackson.databind.node.ObjectNode;
@NoArgsConstructor
@AllArgsConstructor
public final class ValueTransformationStep {
@Getter
@Setter
private String transformerId;
@Getter
@Setter
ObjectNode configuration;
}

View File

@ -0,0 +1,10 @@
package ru.kirillius.XCP.Data;
import org.javatuples.Pair;
import tools.jackson.databind.node.ObjectNode;
import java.util.function.Function;
public interface ValueTransformer extends Function<Pair<Object, ObjectNode>, Object> {
}

View File

@ -0,0 +1,12 @@
package ru.kirillius.XCP.Data;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ValueTransformerDescriptor {
String transformerId() default "";
}

View File

@ -0,0 +1,7 @@
package ru.kirillius.XCP.Logging;
public enum LogLevel {
INFO,
ERROR,
WARNING
}

View File

@ -0,0 +1,9 @@
package ru.kirillius.XCP.Logging;
import lombok.Builder;
import java.time.Instant;
@Builder
public record LogMessage(LogLevel level, Instant date, String message) {
}

View File

@ -0,0 +1,15 @@
package ru.kirillius.XCP.Logging;
public interface Logger {
void info(String message);
void warning(String message);
void error(String message);
void error(String message, Throwable error);
void error(Throwable error);
}

View File

@ -0,0 +1,14 @@
package ru.kirillius.XCP.Logging;
import ru.kirillius.java.utils.events.EventHandler;
import java.io.Closeable;
public interface LoggingSystem extends Closeable {
Logger createLogger(Class<?> clazz);
Logger createLogger(String name);
EventHandler<LogMessage> getEventHandler();
}

View File

@ -0,0 +1,36 @@
package ru.kirillius.XCP.Persistence.Entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import ru.kirillius.XCP.Persistence.PersistenceEntity;
import java.time.Instant;
import java.util.Date;
@GenerateApiSpec
public interface ApiToken extends PersistenceEntity {
@GenerateApiSpec(type = PersistenceEntity.class)
User getUser();
void setUser(User user);
@GenerateApiSpec
String getName();
void setName(String name);
@GenerateApiSpec
Date getExpirationDate();
void setExpirationDate(Date expirationDate);
@JsonIgnore
default boolean isExpired() {
var expirationDate = getExpirationDate();
if (expirationDate == null) {
return false;
}
return expirationDate.toInstant().isBefore(Instant.now());
}
}

View File

@ -0,0 +1,16 @@
package ru.kirillius.XCP.Persistence.Entities;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import ru.kirillius.XCP.Persistence.NodeEntity;
@GenerateApiSpec
public interface Group extends NodeEntity {
String getIcon();
void setIcon(String icon);
@GenerateApiSpec
boolean isPrototype();
void setPrototype(boolean prototype);
}

View File

@ -0,0 +1,15 @@
package ru.kirillius.XCP.Persistence.Entities;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import ru.kirillius.XCP.Data.PollSettings;
import ru.kirillius.XCP.Persistence.IOEntity;
import ru.kirillius.XCP.Persistence.NodeEntity;
@GenerateApiSpec
public interface Input extends IOEntity, NodeEntity {
@GenerateApiSpec
PollSettings getPollSettings();
void setPollSettings(PollSettings pollSettings);
}

View File

@ -0,0 +1,8 @@
package ru.kirillius.XCP.Persistence.Entities;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import ru.kirillius.XCP.Persistence.IOEntity;
import ru.kirillius.XCP.Persistence.NodeEntity;
@GenerateApiSpec
public interface Output extends IOEntity, NodeEntity {
}

View File

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

View File

@ -0,0 +1,33 @@
package ru.kirillius.XCP.Persistence.Entities;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import ru.kirillius.XCP.Persistence.PersistenceEntity;
import ru.kirillius.XCP.Security.UserRole;
import tools.jackson.databind.node.ObjectNode;
@GenerateApiSpec
public interface User extends PersistenceEntity {
void setPassword(String password);
boolean verifyPassword(String password);
@GenerateApiSpec
String getLogin();
void setLogin(String login);
@GenerateApiSpec
UserRole getRole();
void setRole(UserRole role);
@GenerateApiSpec
ObjectNode getValues();
void setValues(ObjectNode values);
@GenerateApiSpec
String getName();
void setName(String name);
}

View File

@ -0,0 +1,14 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Data.ValueTransformationChain;
public interface IOEntity extends NodeEntity {
ValueTransformationChain getTransformationChain();
void setTransformationChain(ValueTransformationChain transformationChain);
String getAdapterId();
void setAdapterId(String adapterId);
}

View File

@ -0,0 +1,37 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import ru.kirillius.XCP.Persistence.Entities.Group;
import tools.jackson.databind.node.ObjectNode;
public interface NodeEntity extends PersistenceEntity {
@GenerateApiSpec
String getName();
void setName(String name);
@GenerateApiSpec
boolean isProtectedEntity();
void setProtectedEntity(boolean essential);
@GenerateApiSpec
boolean isEnabled();
void setEnabled(boolean enabled);
@GenerateApiSpec(type = PersistenceEntity.class)
Group getParent();
void setParent(Group parent);
@GenerateApiSpec
ObjectNode getProperties();
void setProperties(ObjectNode properties);
@GenerateApiSpec
TagCollection getTags();
void setTags(TagCollection tags);
}

View File

@ -0,0 +1,15 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Commons.StreamHandler;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import java.util.Collection;
public interface NodeRepository<E extends NodeEntity> extends Repository<E> {
StreamHandler<E> getByGroup(Group group);
StreamHandler<E> getByAllTags(Collection<Tag> tags);
StreamHandler<E> getByAnyTag(Collection<Tag> tags);
}

View File

@ -0,0 +1,14 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Commons.GenerateApiSpec;
import java.util.UUID;
@GenerateApiSpec
public interface PersistenceEntity {
@GenerateApiSpec
long getId();
@GenerateApiSpec(type = String.class)
UUID getUuid();
}

View File

@ -0,0 +1,15 @@
package ru.kirillius.XCP.Persistence;
public class PersistenceException extends RuntimeException {
public PersistenceException(String message) {
super(message);
}
public PersistenceException(String message, Throwable cause) {
super(message, cause);
}
public PersistenceException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,10 @@
package ru.kirillius.XCP.Persistence.Repositories;
import ru.kirillius.XCP.Commons.StreamHandler;
import ru.kirillius.XCP.Persistence.Entities.ApiToken;
import ru.kirillius.XCP.Persistence.Entities.User;
import ru.kirillius.XCP.Persistence.Repository;
public interface ApiTokenRepository extends Repository<ApiToken> {
StreamHandler<ApiToken> getByUser(User user);
}

View File

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

View File

@ -0,0 +1,8 @@
package ru.kirillius.XCP.Persistence.Repositories;
import ru.kirillius.XCP.Persistence.Entities.Input;
import ru.kirillius.XCP.Persistence.NodeRepository;
public interface InputRepository extends NodeRepository<Input> {
}

View File

@ -0,0 +1,8 @@
package ru.kirillius.XCP.Persistence.Repositories;
import ru.kirillius.XCP.Persistence.Entities.Output;
import ru.kirillius.XCP.Persistence.NodeRepository;
public interface OutputRepository extends NodeRepository<Output> {
}

View File

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

View File

@ -0,0 +1,9 @@
package ru.kirillius.XCP.Persistence.Repositories;
import ru.kirillius.XCP.Persistence.Entities.User;
import ru.kirillius.XCP.Persistence.Repository;
public interface UserRepository extends Repository<User> {
User getByLogin(String login);
}

View File

@ -0,0 +1,49 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.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 get(long id);
E get(UUID uuid);
StreamHandler<E> get(Collection<Long> ids);
StreamHandler<E> search(String query, Collection<Object> queryParameters);
EventBindings<E> events();
StreamHandler<E> getAll();
long getCount();
void save(E entity);
void save(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.Persistence;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import java.util.Collection;
public interface TagCollection extends Collection<Tag> {
}

View File

@ -0,0 +1,9 @@
package ru.kirillius.XCP.Security;
public interface HashUtility {
String hash(String password);
default boolean validate(String password, String hash) {
return hash(password).equals(hash);
}
}

View File

@ -0,0 +1,5 @@
package ru.kirillius.XCP.Security;
public interface SecurityManager {
HashUtility getHashUtility();
}

View File

@ -0,0 +1,17 @@
package ru.kirillius.XCP.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;
}
}

View File

@ -0,0 +1,22 @@
package ru.kirillius.XCP.Services;
import ru.kirillius.XCP.Commons.Service;
import ru.kirillius.XCP.Persistence.PersistenceEntity;
import ru.kirillius.XCP.Persistence.Repository;
import tools.jackson.databind.ObjectMapper;
public interface RepositoryService extends Service {
<E extends PersistenceEntity> Repository<E> getRepositoryForEntity(Class<E> entityType);
<R extends Repository<?>> R getRepository(Class<R> repositoryType);
Class<? extends PersistenceEntity> getEntityBaseType(Class<? extends PersistenceEntity> entityClass);
Class<? extends Repository<?>> getRepositoryBaseType(Class<? extends Repository<?>> repositoryClass);
Class<? extends PersistenceEntity> getRepositoryEntityType(Class<? extends Repository<?>> repositoryClass);
ObjectMapper getMapper();
Class<? extends PersistenceEntity> getEntityTypeByName(String entityName);
}

View File

@ -0,0 +1,12 @@
package ru.kirillius.XCP.Services;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ServiceLoadPriority {
int value() default 1000;
}

View File

@ -0,0 +1,6 @@
package ru.kirillius.XCP.Services;
import ru.kirillius.XCP.Commons.Service;
public interface WebService extends Service {
}

52
app/pom.xml Normal file
View File

@ -0,0 +1,52 @@
<?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>app</artifactId>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>api</artifactId>
<version>1.0.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>core</artifactId>
<version>1.0.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>logging</artifactId>
<version>1.0.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>database</artifactId>
<version>1.0.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ru.kirillius</groupId>
<artifactId>web-server</artifactId>
<version>1.0.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,201 @@
package ru.kirillius.XCP;
import lombok.Getter;
import ru.kirillius.XCP.Commons.Config;
import ru.kirillius.XCP.Commons.ConfigManager;
import ru.kirillius.XCP.Commons.Context;
import ru.kirillius.XCP.Commons.Service;
import ru.kirillius.XCP.Logging.Logger;
import ru.kirillius.XCP.Logging.LoggingSystem;
import ru.kirillius.XCP.Logging.LoggingSystemImpl;
import ru.kirillius.XCP.Persistence.PersistenceEntity;
import ru.kirillius.XCP.Persistence.Repository;
import ru.kirillius.XCP.Persistence.RepositoryServiceImpl;
import ru.kirillius.XCP.Security.ConfigManagerImpl;
import ru.kirillius.XCP.Security.SecurityManager;
import ru.kirillius.XCP.Security.SecurityManagerImpl;
import ru.kirillius.XCP.Services.RepositoryService;
import ru.kirillius.XCP.Services.ServiceLoadPriority;
import ru.kirillius.XCP.Services.WebService;
import ru.kirillius.XCP.web.WebServiceImpl;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Getter
public class Application implements Context {
@Getter
private final SecurityManager securityManager;
@Getter
private final LoggingSystem loggingSystem;
private final Logger log;
@Getter
private final List<String> launchArgs;
@Getter
private final Config config;
@Getter
private final ConfigManager configManager;
private final Map<Class<? extends Service>, Service> services = new ConcurrentHashMap<>();
private Config loadConfig() {
var configFile = configManager.getConfigFile().getAbsolutePath();
try {
configFile = configManager.getConfigFile().getCanonicalPath();
} catch (IOException e) {
log.warning("Unable to determine real path of file " + configFile);
}
Config config;
if (configManager.isExist()) {
try {
config = configManager.load();
log.info("Loaded config file: " + configFile);
} catch (IOException e) {
log.error("Error loading config file " + configFile, e);
throw new RuntimeException(e);
}
} else {
log.warning("Unable to find config file " + configFile + ". Using default values.");
config = configManager.create();
log.info("Saving default config file to " + configFile);
try {
configManager.save(config);
} catch (IOException e) {
throw new RuntimeException("Unable to save config file", e);
}
}
return config;
}
public Application(String[] args) {
launchArgs = Arrays.stream(args).toList();
loggingSystem = new LoggingSystemImpl(this);
log = loggingSystem.createLogger(Application.class);
configManager = new ConfigManagerImpl(this);
config = loadConfig();
securityManager = new SecurityManagerImpl();
try {
loadServices();
} catch (Throwable throwable) {
log.error(throwable);
shutdown();
throw new RuntimeException("Error loading services");
}
checkDefaultInstall();
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
((WebServiceImpl) getService(WebService.class)).join();
}
private final static String DEFAULT_INSTALLER_FILE = "defaultEntities.json";
private void checkDefaultInstall() {
try (var stream = getClass().getClassLoader().getResourceAsStream(DEFAULT_INSTALLER_FILE)) {
var repositoryService = getService(RepositoryService.class);
var mapper = repositoryService.getMapper();
var array = (ArrayNode) mapper.readTree(stream);
for (var node : array) {
var entry = (ObjectNode) node;
var typeName = entry.get("type").asString();
var entityType = repositoryService.getEntityTypeByName(typeName);
@SuppressWarnings("unchecked") var repository = (Repository<PersistenceEntity>) repositoryService.getRepositoryForEntity(entityType);
var deserialized = repository.deserialize((ObjectNode) entry.get("entity"));
if (repository.get(deserialized.getUuid()) == null) {
log.warning("Installing default entity " + typeName + " (" + deserialized + ").");
repository.save(deserialized);
}
}
} catch (IOException e) {
throw new RuntimeException("Unable to load file " + DEFAULT_INSTALLER_FILE, e);
}
}
private void loadServices() {
var servicesToLoad = List.of(RepositoryServiceImpl.class, WebServiceImpl.class);
servicesToLoad.stream().sorted(Comparator.comparingInt(aClass -> {
var order = aClass.getAnnotation(ServiceLoadPriority.class);
return order == null ? 100000 : order.value();
})).forEach(aClass -> {
@SuppressWarnings("unchecked") var facade = (Class<? extends Service>) Arrays.stream(aClass.getInterfaces())
.filter(Service.class::isAssignableFrom)
.findFirst().
orElseThrow(() -> new ClassCastException("Unable to get service interface from class " + aClass.getSimpleName()));
log.info("Loading service " + facade.getSimpleName());
try {
var constructor = aClass.getConstructor();
try {
var service = constructor.newInstance();
try {
service.initialize(this);
services.put(facade, service);
} catch (Throwable e) {
try {
service.close();
} catch (IOException ex) {
e.addSuppressed(ex);
}
throw new RuntimeException("Failed to start " + facade.getSimpleName(), e);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Failed to instantiate " + facade.getSimpleName(), e);
}
} catch (NoSuchMethodException e) {
throw new RuntimeException("Failed to find default constructor of " + facade.getSimpleName(), e);
}
});
}
@SuppressWarnings("unchecked")
@Override
public <S extends Service> S getService(Class<S> serviceClass) {
return (S) services.get(serviceClass);
}
@Override
public void shutdown() {
try {
services.forEach((serviceClass, service) -> {
try {
log.info("Shutting down service " + serviceClass.getSimpleName());
service.close();
} catch (IOException e) {
log.error("Error shutting down service " + serviceClass.getSimpleName(), e);
}
});
loggingSystem.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean isDebuggingEnabled() {
return launchArgs.contains("--debug");
}
public static void main(String[] args) {
try {
new Application(args);
} catch (Throwable e) {
System.err.println("Error starting application");
e.printStackTrace(System.err);
System.exit(1);
}
}
}

View File

@ -0,0 +1,13 @@
[
{
"type": "User",
"entity": {
"login": "admin",
"name": "Administrator",
"role": "Admin",
"values": {},
"uuid": "00000000-0000-0000-0000-000000000000",
"passwordHash": "$argon2id$v=19$m=65536,t=3,p=1$SBqQtx5adxoG53V0TgqmDw$zIy0Wiq53m9r/SOldtCXWXLWbvZuS0F3HHILxpUsLhQ"
}
}
]

28
core/pom.xml Normal file
View File

@ -0,0 +1,28 @@
<?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>core</artifactId>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<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,32 @@
package ru.kirillius.XCP.Security;
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
public class Argon2HashUtility implements HashUtility {
private final Argon2 argon2;
public Argon2HashUtility() {
this.argon2 = Argon2Factory.create(
Argon2Factory.Argon2Types.ARGON2id,
16,
32
);
}
@Override
public String hash(String password) {
try {
return argon2.hash(3, 65536, 1, password.toCharArray());
} finally {
argon2.wipeArray(password.toCharArray());
}
}
@Override
public boolean validate(String password, String hash) {
return argon2.verify(hash, password.toCharArray());
}
}

View File

@ -0,0 +1,85 @@
package ru.kirillius.XCP.Security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import ru.kirillius.XCP.Commons.Config;
import ru.kirillius.XCP.Commons.ConfigManager;
import ru.kirillius.XCP.Commons.Context;
import tools.jackson.databind.ObjectMapper;
import java.io.*;
import java.util.regex.Pattern;
public final class ConfigManagerImpl implements ConfigManager {
private final Context context;
private final static String DEFAULT_CONFIG_PATH = "./xcp.conf";
private final static String DEFAULT_DB_PATH = "./xcpdata";
private final ObjectMapper mapper = new ObjectMapper();
public ConfigManagerImpl(Context context) {
this.context = context;
configFile = findConfigFile();
}
private File findConfigFile() {
return new File(context.getLaunchArgs().stream()
.filter(a -> a.startsWith("--config="))
.findFirst()
.map(a -> a.split(Pattern.quote("="))[1])
.orElse(DEFAULT_CONFIG_PATH));
}
@Getter
private final File configFile;
@Override
public boolean isExist() {
return configFile.exists();
}
@Override
public Config load() throws IOException {
try (var stream = new FileInputStream(configFile)) {
return mapper.readValue(stream, ConfigImpl.class);
}
}
@Override
public Config create() {
var config = new ConfigImpl();
config.loadedConfigFile = configFile;
return config;
}
@Override
public void save(Config config) throws IOException {
try (var stream = new FileOutputStream(configFile)) {
mapper.writerWithDefaultPrettyPrinter().writeValue(stream, config);
}
}
@NoArgsConstructor
private final static class ConfigImpl implements Config {
@Getter
@JsonIgnore
private File loadedConfigFile;
@Getter
@Setter
@JsonProperty
private String host = "127.0.0.1";
@Getter
@Setter
@JsonProperty
private File databaseFile = new File(DEFAULT_DB_PATH);
@Getter
@Setter
@JsonProperty
private int httpPort = 8080;
}
}

View File

@ -0,0 +1,9 @@
package ru.kirillius.XCP.Security;
import lombok.Getter;
public class SecurityManagerImpl implements SecurityManager {
@Getter
private final HashUtility hashUtility = new Argon2HashUtility();
}

View File

@ -0,0 +1,23 @@
package ru.kirillius.XCP.Security;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class Argon2HashUtilityTest {
@Test
void hash() {
var util = new Argon2HashUtility();
var hashed = util.hash("admin");
assertThat(hashed).isNotNull().isNotEmpty().doesNotContain("admin").isEqualTo("$argon2id$v=19$m=65536,t=3,p=1$SBqQtx5adxoG53V0TgqmDw$zIy0Wiq53m9r/SOldtCXWXLWbvZuS0F3HHILxpUsLhQ");
}
@Test
void validate() {
var util = new Argon2HashUtility();
var hashed = util.hash("admin");
assertThat(util.validate("admin", hashed)).isTrue();
}
}

42
database/pom.xml Normal file
View File

@ -0,0 +1,42 @@
<?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>
<!-- Source: https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</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>ru.kirillius</groupId>
<artifactId>api</artifactId>
<version>1.0.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,55 @@
package ru.kirillius.XCP.Persistence;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.Objects;
import java.util.UUID;
@MappedSuperclass
public abstract class AbstractEntity implements PersistenceEntity {
@Id
@Getter
@Setter
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonProperty
private long id = 0;
@Getter
@Setter
@JsonProperty
@Column(unique = true, nullable = false, updatable = false)
private UUID uuid;
@PrePersist
protected void prePersist() {
if (uuid == null) {
uuid = UUID.randomUUID();
}
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null) {
return false;
}
if (o.getClass() != getClass()) {
return false;
}
var that = (AbstractEntity) o;
return Objects.equals(uuid, that.uuid);
}
@Override
public int hashCode() {
return Objects.hashCode(uuid);
}
}

View File

@ -0,0 +1,269 @@
package ru.kirillius.XCP.Persistence;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import ru.kirillius.XCP.Commons.StreamHandler;
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.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> {
private final Repository.EventBindings<E> eventBindings = new EventBindingsImpl<>();
protected final Class<? extends E> entityImplementationClass;
protected RepositoryServiceImpl repositoryService;
protected String tableName;
public AbstractRepository(RepositoryServiceImpl repositoryService) {
var thisClass = getClass();
this.repositoryService = repositoryService;
//noinspection unchecked
entityImplementationClass = (Class<? extends E>) thisClass.getAnnotation(EntityImplementation.class).value();
tableName = entityImplementationClass.getName();
}
@Override
public E create() {
try {
var constructor = entityImplementationClass.getConstructor();
var instance = constructor.newInstance();
if (instance instanceof ContextReferencedEntity referencedEntity) {
referencedEntity.setContext(repositoryService.getContext());
}
return instance;
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
IllegalAccessException e) {
throw new PersistenceException("Unable to instantiate entity", e);
}
}
@Override
public StreamHandler<E> search(String query, Collection<Object> queryParameters) {
return buildQuery(query, queryParameters.toArray());
}
@Override
public E get(UUID uuid) {
try (var query = buildQueryParametrized("where uuid = ?1", uuid)) {
return query.get().findFirst().orElse(null);
} catch (IOException e) {
throw new PersistenceException(e);
}
}
@Override
public E get(long id) {
try (var query = buildQueryParametrized("where id = ?1", id)) {
return query.get().findFirst().orElse(null);
} catch (IOException e) {
throw new PersistenceException(e);
}
}
@Override
public StreamHandler<E> get(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> getAll() {
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 save(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 PersistenceException("Unable to save entity", e);
}
try {
eventBindings.entityStored().invoke(entity);
} catch (Exception e) {
throw new PersistenceException("Something went wrong on entity save event", e);
} finally {
session.close();
}
}
@Override
public void save(Collection<E> entities) {
entities.forEach(this::save);
}
@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 PersistenceException("Unable to remove entity", e);
}
try {
eventBindings.entityRemoved().invoke(entity);
} catch (Exception e) {
throw new PersistenceException("Something went wrong on entity deletion", 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));
}
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,8 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Commons.Context;
public interface ContextReferencedEntity {
void setContext(Context context);
Context getContext();
}

View File

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

View File

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

View File

@ -0,0 +1,33 @@
package ru.kirillius.XCP.Persistence;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.deser.std.StdDeserializer;
import java.util.UUID;
public class EntityReferenceDeserializer extends StdDeserializer<EntityReference> {
private final RepositoryServiceImpl repositoryService;
public EntityReferenceDeserializer(RepositoryServiceImpl repositoryService) {
super(EntityReference.class);
this.repositoryService = repositoryService;
}
@Override
public EntityReference deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
var node = ctxt.readTree(p);
var type = node.get("type").asString();
var id = node.get("id").asLong();
var uuid = node.get("uuid").asString();
var repository = repositoryService.getRepositoryForEntity((Class<? extends PersistenceEntity>) repositoryService.getEntityTypeByName(type));
if (uuid != null) {
return new EntityReference(repository.get(UUID.fromString(uuid)));
}
return new EntityReference(repository.get(id));
}
}

View File

@ -0,0 +1,30 @@
package ru.kirillius.XCP.Persistence;
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<EntityReference> {
private final RepositoryServiceImpl repositoryService;
public EntityReferenceSerializer(RepositoryServiceImpl repositoryService) {
super(EntityReference.class);
this.repositoryService = repositoryService;
}
@Override
public void serialize(EntityReference reference, JsonGenerator gen, SerializationContext provider) throws JacksonException {
var value = reference.get();
if(value == null) {
gen.writeNull();
return;
}
gen.writeStartObject();
var baseType = repositoryService.getEntityBaseType(value.getClass());
gen.writeStringProperty("type", baseType.getSimpleName());
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,41 @@
package ru.kirillius.XCP.Persistence;
import lombok.Getter;
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(repositoryService),
new TagCollectionSerializer()
)));
context.addDeserializers(new SimpleDeserializers(Map.of(
EntityReference.class, new EntityReferenceDeserializer(repositoryService),
TagCollection.class, new TagCollectionDeserializer(repositoryService)
)));
}
}

View File

@ -0,0 +1,21 @@
package ru.kirillius.XCP.Persistence;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import ru.kirillius.XCP.Data.PollSettings;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PollSettingsImpl implements PollSettings {
@JsonProperty
private long pollInterval = 300;
@JsonProperty
private boolean interruptable = true;
@JsonProperty
private boolean rateMeasurement = false;
@JsonProperty
private boolean enabled = true;
}

View File

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

View File

@ -0,0 +1,67 @@
package ru.kirillius.XCP.Persistence.Repositories;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import ru.kirillius.XCP.Commons.StreamHandler;
import ru.kirillius.XCP.Persistence.*;
import ru.kirillius.XCP.Persistence.Entities.ApiToken;
import ru.kirillius.XCP.Persistence.Entities.User;
import java.util.Date;
import java.util.List;
@EntityImplementation(ApiTokenRepositoryImpl.TokenEntity.class)
public class ApiTokenRepositoryImpl extends AbstractRepository<ApiToken> implements ApiTokenRepository {
public ApiTokenRepositoryImpl(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@Override
public StreamHandler<ApiToken> getByUser(User user) {
return search("WHERE user = ?1", List.of(user));
}
@Entity
@Table(name = "Tokens")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public static class TokenEntity extends AbstractEntity implements ApiToken {
@JsonProperty
@Column
private Date expirationDate;
@JsonProperty
@Column(nullable = false)
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JsonIgnore
private UserRepositoryImpl.UserEntity user;
@JsonProperty("user")
EntityReference getUserReference() {
return new EntityReference(getUser());
}
@JsonProperty("user")
void setUserReference(EntityReference entityReference) {
user = entityReference == null ? null : (UserRepositoryImpl.UserEntity) entityReference.get();
}
public User getUser() {
return user;
}
public void setUser(User parent) {
this.user = (UserRepositoryImpl.UserEntity) parent;
}
}
}

View File

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

View File

@ -0,0 +1,134 @@
package ru.kirillius.XCP.Persistence.Repositories;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import ru.kirillius.XCP.Data.PollSettings;
import ru.kirillius.XCP.Data.ValueTransformationChain;
import ru.kirillius.XCP.Persistence.*;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Input;
import ru.kirillius.XCP.Serialization.PollSettingsConverter;
import ru.kirillius.XCP.Serialization.ValueTransformationChainConverter;
import tools.jackson.databind.annotation.JsonDeserialize;
import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@EntityImplementation(InputRepositoryImpl.InputEntity.class)
public class InputRepositoryImpl extends AbstractNodeRepository<Input> implements InputRepository {
public InputRepositoryImpl(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@Override
public void save(Input entity) {
if (entity != null && entity.getParent() == null) {
throw new IllegalStateException("Saving inputs without group is prohibited");
}
super.save(entity);
}
@Entity
@Table(name = "Inputs")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public static class InputEntity extends AbstractEntity implements Input {
@Column(nullable = false)
@JsonProperty
private String name = "";
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private boolean protectedEntity;
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private boolean enabled;
@ManyToOne(fetch = FetchType.EAGER)
@JsonIgnore
private GroupRepositoryImpl.GroupEntity parent;
@JsonProperty("parent")
EntityReference getParentReference() {
return new EntityReference(getParent());
}
@JsonProperty("parent")
void setParentReference(EntityReference entityReference) {
parent = entityReference == null ? null : (GroupRepositoryImpl.GroupEntity) entityReference.get();
}
public Group getParent() {
return parent;
}
public void setParent(Group parent) {
this.parent = (GroupRepositoryImpl.GroupEntity) parent;
}
@Override
public void setTags(TagCollection tags) {
this.tags = tags.stream().map(t -> (TagRepositoryImpl.TagEntity) t).collect(Collectors.toSet());
}
@Override
public TagCollection getTags() {
return new TagCollectionImpl(tags);
}
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private ObjectNode properties = JsonNodeFactory.instance.objectNode();
@JsonIgnore
@ManyToMany(fetch = FetchType.EAGER)
private Set<TagRepositoryImpl.TagEntity> tags = new HashSet<>();
@Override
public PollSettings getPollSettings() {
return pollSettings;
}
@Override
public void setPollSettings(PollSettings pollSettings) {
this.pollSettings = (PollSettingsImpl) pollSettings;
}
@Column(nullable = false)
@JsonProperty
@Convert(converter = PollSettingsConverter.class)
@JsonDeserialize(as = PollSettingsImpl.class)
private PollSettingsImpl pollSettings = new PollSettingsImpl();
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private String adapterId = "";
@Getter
@Setter
@JsonProperty
@Convert(converter = ValueTransformationChainConverter.class)
@Column(nullable = false)
private ValueTransformationChain transformationChain = new ValueTransformationChain();
}
}

View File

@ -0,0 +1,114 @@
package ru.kirillius.XCP.Persistence.Repositories;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import ru.kirillius.XCP.Data.ValueTransformationChain;
import ru.kirillius.XCP.Persistence.*;
import ru.kirillius.XCP.Persistence.Entities.Group;
import ru.kirillius.XCP.Persistence.Entities.Output;
import ru.kirillius.XCP.Serialization.ValueTransformationChainConverter;
import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@EntityImplementation(OutputRepositoryImpl.OutputEntity.class)
public class OutputRepositoryImpl extends AbstractNodeRepository<Output> implements OutputRepository {
public OutputRepositoryImpl(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@Override
public void save(Output entity) {
if (entity != null && entity.getParent() == null) {
throw new IllegalStateException("Saving outputs without group is prohibited");
}
super.save(entity);
}
@Entity
@Table(name = "Outputs")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public static class OutputEntity extends AbstractEntity implements Output {
@Column(nullable = false)
@JsonProperty
private String name = "";
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private boolean protectedEntity;
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private boolean enabled;
@ManyToOne(fetch = FetchType.EAGER)
@JsonIgnore
private GroupRepositoryImpl.GroupEntity parent;
@JsonProperty("parent")
EntityReference getParentReference() {
return new EntityReference(getParent());
}
@JsonProperty("parent")
void setParentReference(EntityReference entityReference) {
parent = entityReference == null ? null : (GroupRepositoryImpl.GroupEntity) entityReference.get();
}
public Group getParent() {
return parent;
}
public void setParent(Group parent) {
this.parent = (GroupRepositoryImpl.GroupEntity) parent;
}
@Override
public void setTags(TagCollection tags) {
this.tags = tags.stream().map(t -> (TagRepositoryImpl.TagEntity) t).collect(Collectors.toSet());
}
@Override
public TagCollection getTags() {
return new TagCollectionImpl(tags);
}
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private ObjectNode properties = JsonNodeFactory.instance.objectNode();
@JsonIgnore
@ManyToMany(fetch = FetchType.EAGER)
private Set<TagRepositoryImpl.TagEntity> tags = new HashSet<>();
@Column(nullable = false)
@JsonProperty
@Getter
@Setter
private String adapterId = "";
@Getter
@Setter
@JsonProperty
@Convert(converter = ValueTransformationChainConverter.class)
@Column(nullable = false)
private ValueTransformationChain transformationChain = new ValueTransformationChain();
}
}

View File

@ -0,0 +1,118 @@
package ru.kirillius.XCP.Persistence.Repositories;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.*;
import ru.kirillius.XCP.Commons.ResourceHandler;
import ru.kirillius.XCP.Persistence.*;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@EntityImplementation(TagRepositoryImpl.TagEntity.class)
public class TagRepositoryImpl extends AbstractRepository<Tag> implements TagRepository {
public TagRepositoryImpl(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@Override
public Tag getByNameOrCreate(String name) {
try (var handler = buildQueryParametrized("WHERE name = ?1", name)) {
var result = handler.get().findFirst();
if (result.isPresent()) {
return result.get();
} else {
var tag = create();
tag.setName(name);
save(tag);
return tag;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public TagCollection getByNamesOrCreate(Collection<String> names) {
var tags = new TagCollectionImpl();
try (var handler = search("where name IN (?1)", List.of(names))) {
tags.addAll(handler.get().toList());
} catch (IOException e) {
throw new RuntimeException("Unable to find tags by names " + names, e);
}
if (tags.size() != names.size()) {
var foundNames = tags.stream().map(Tag::getName).toList();
names.forEach(tagName -> {
if (!foundNames.contains(tagName)) {
var tag = create();
tag.setName(tagName);
save(tag);
tags.add(tag);
}
});
}
return tags;
}
@Override
public TagCollection createCollection() {
return new TagCollectionImpl();
}
@Override
public TagCollection createCollection(ResourceHandler<Stream<Tag>> handler) {
return new TagCollectionImpl(handler.get().toList());
}
@Entity
@Table(name = "Tags")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public static class TagEntity extends AbstractEntity implements Tag {
@Override
public String toString() {
return name;
}
@Column(nullable = false, unique = true)
@JsonProperty
private String name = "";
private static final Pattern NAME_PATTERN =
Pattern.compile("^[a-z0-9]+(\\.[a-z0-9]+)*$");
public void setName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
name = name.trim().toLowerCase();
if (!NAME_PATTERN.matcher(name).matches()) {
throw new IllegalArgumentException(
String.format(
"Invalid name: '%s'. " +
"Name must contain only lowercase letters a-z, digits 0-9, and dots. " +
"Cannot start or end with dot, and dots cannot be consecutive.",
name
)
);
}
this.name = name;
}
}
}

View File

@ -0,0 +1,87 @@
package ru.kirillius.XCP.Persistence.Repositories;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.*;
import ru.kirillius.XCP.Commons.Context;
import ru.kirillius.XCP.Persistence.*;
import ru.kirillius.XCP.Persistence.Entities.User;
import ru.kirillius.XCP.Security.UserRole;
import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
import java.io.IOException;
@EntityImplementation(UserRepositoryImpl.UserEntity.class)
public class UserRepositoryImpl extends AbstractRepository<User> implements UserRepository {
public UserRepositoryImpl(RepositoryServiceImpl repositoryService) {
super(repositoryService);
}
@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 = "Users")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public static class UserEntity extends AbstractEntity implements User, ContextReferencedEntity {
@Transient
@JsonIgnore
@Getter
@Setter
private Context context;
@Column(nullable = false, unique = true)
@JsonProperty
private String login = "";
@Column(nullable = false)
@JsonProperty
private String name = "";
@Column(nullable = false)
@JsonProperty
private String passwordHash = "";
@Column(nullable = false)
@JsonProperty
@Enumerated(EnumType.STRING)
private UserRole role = UserRole.User;
@Column(name = "custom_values", nullable = false)
@Getter
@Setter
@JsonProperty
private ObjectNode values = JsonNodeFactory.instance.objectNode();
@Override
protected void prePersist() {
super.prePersist();
if (login == null || login.isEmpty()) {
login = "user" + System.currentTimeMillis();
}
}
@Override
public void setPassword(String password) {
passwordHash = context.getSecurityManager().getHashUtility().hash(password);
}
@Override
public boolean verifyPassword(String password) {
return context.getSecurityManager().getHashUtility().validate(password, passwordHash);
}
}
}

View File

@ -0,0 +1,228 @@
package ru.kirillius.XCP.Persistence;
import lombok.Getter;
import org.hibernate.Interceptor;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.type.Type;
import ru.kirillius.XCP.Commons.Context;
import ru.kirillius.XCP.Persistence.Repositories.*;
import ru.kirillius.XCP.Services.RepositoryService;
import ru.kirillius.XCP.Services.ServiceLoadPriority;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServiceLoadPriority(0)
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 PersistenceEntity>, Class<? extends PersistenceEntity>> entityBaseBindings = new ConcurrentHashMap<>();
private final Map<Class<? extends Repository<?>>, Class<? extends PersistenceEntity>> repositoryEntityBindings = new ConcurrentHashMap<>();
private final Map<Class<? extends Repository<?>>, Repository<?>> repositoryBindings = new ConcurrentHashMap<>();
private final Map<Class<? extends Repository<?>>, Class<? extends Repository<?>>> repositoryBaseBindings = new ConcurrentHashMap<>();
private final Map<Class<? extends PersistenceEntity>, Class<? extends Repository<?>>> entityBindings = new ConcurrentHashMap<>();
private final Collection<Class<? extends AbstractRepository<?>>> managedRepositoryClasses;
@Getter
private Context context;
public RepositoryServiceImpl(DatabaseConfiguration databaseConfiguration, Collection<Class<? extends AbstractRepository<?>>> repositoryImplClasses) {
managedRepositoryClasses = repositoryImplClasses;
configuration = new Configuration();
configuration.configure();
registerClasses();
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();
}
public RepositoryServiceImpl() {
managedRepositoryClasses = List.of(
ApiTokenRepositoryImpl.class,
GroupRepositoryImpl.class,
InputRepositoryImpl.class,
OutputRepositoryImpl.class,
TagRepositoryImpl.class,
UserRepositoryImpl.class
);
configuration = new Configuration();
configuration.configure();
registerClasses();
}
private void registerClasses() {
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();
if (sessionFactory != null) {
sessionFactory.close();
}
}
private volatile boolean initialized;
@Override
public void initialize(Context context) {
if (initialized) {
throw new IllegalStateException("Initialized already");
}
initialized = true;
this.context = context;
if (databaseConfiguration == null) {
loadDatabaseConfig(new H2DatabaseInFileConfiguration(context.getConfig().getDatabaseFile()));
}
managedRepositoryClasses.forEach(aClass -> {
var instance = instantiateRepository(aClass);
var baseClass = getRepositoryBaseType(aClass);
repositoryBindings.put(baseClass, instance);
var entityClass = getEntityBaseType(getRepositoryEntityType(aClass));
entityBindings.put(entityClass, baseClass);
});
mapper = JsonMapper.builder().addModule(new PersistenceSerializationModule(this)).build();
this.configuration.setInterceptor(new EntityInterceptor(context));
sessionFactory = this.configuration.buildSessionFactory();
}
public static class EntityInterceptor implements Interceptor {
private final Context context;
private EntityInterceptor(Context context) {
this.context = context;
}
@Override
public boolean onLoad(Object entity, Object id, Object[] state, String[] propertyNames, Type[] types) {
if (entity instanceof ContextReferencedEntity referencedEntity) {
if (referencedEntity.getContext() == null) {
referencedEntity.setContext(context);
}
}
return false;
}
}
@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);
}
/**
* Returns entity base interface Class<E> from Class<? extends E>
*
* @param entityClass
* @return Class<E>
*/
@Override
public Class<? extends PersistenceEntity> getEntityBaseType(Class<? extends PersistenceEntity> entityClass) {
if (!entityBaseBindings.containsKey(entityClass)) {
var foundClass = Arrays.stream(entityClass.getInterfaces()).filter(PersistenceEntity.class::isAssignableFrom).findFirst();
if (foundClass.isPresent()) {
entityBaseBindings.put(entityClass, (Class<? extends PersistenceEntity>) foundClass.get());
} else {
throw new RuntimeException("Unable to determine base interface Class<? extends PersistenceEntity> of " + entityClass.getName());
}
}
return entityBaseBindings.get(entityClass);
}
/**
* Returns repository base interface type Class<E> from Class<? extends E>
*
* @param repositoryClass
* @return Class<E>
*/
@Override
public Class<? extends Repository<?>> getRepositoryBaseType(Class<? extends Repository<?>> repositoryClass) {
if (!repositoryBaseBindings.containsKey(repositoryClass)) {
var foundClass = Arrays.stream(repositoryClass.getInterfaces()).filter(Repository.class::isAssignableFrom).findFirst();
if (foundClass.isPresent()) {
repositoryBaseBindings.put(repositoryClass, (Class<? extends Repository<?>>) foundClass.get());
} else {
throw new RuntimeException("Unable to determine base interface Class<? extends Repository> of " + repositoryClass.getName());
}
}
return repositoryBaseBindings.get(repositoryClass);
}
/**
* Returns Entity implementation class that implements E from Class<? extends Repository<E>>
*
* @param repositoryImplClass
* @return Class<? extends E>
*/
@Override
public Class<? extends PersistenceEntity> getRepositoryEntityType(Class<? extends Repository<?>> repositoryImplClass) {
if (!repositoryEntityBindings.containsKey(repositoryImplClass)) {
var annotation = repositoryImplClass.getAnnotation(EntityImplementation.class);
if (annotation != null) {
repositoryEntityBindings.put(repositoryImplClass, annotation.value());
} else {
throw new RuntimeException("Unable to get @" + EntityImplementation.class.getSimpleName() + " from class " + repositoryImplClass.getName());
}
}
return repositoryEntityBindings.get(repositoryImplClass);
}
@Override
public Class<? extends PersistenceEntity> getEntityTypeByName(String entityName) {
return entityBaseBindings.values().stream().filter(e -> e.getSimpleName().equals(entityName)).findFirst().orElse(null);
}
}

View File

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

View File

@ -0,0 +1,31 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Persistence.Repositories.TagRepository;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.deser.std.StdDeserializer;
public class TagCollectionDeserializer extends StdDeserializer<TagCollection> {
private final RepositoryServiceImpl repositoryService;
public TagCollectionDeserializer(RepositoryServiceImpl repositoryService) {
super(TagCollection.class);
this.repositoryService = repositoryService;
}
@Override
public TagCollection deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
var result = new TagCollectionImpl();
var tagNames = p.readValueAs(String[].class);
var repository = repositoryService.getRepository(TagRepository.class);
for (var name : tagNames) {
var tag = repository.getByNameOrCreate(name);
result.add(tag);
}
return result;
}
}

View File

@ -0,0 +1,15 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import java.util.Collection;
import java.util.HashSet;
public class TagCollectionImpl extends HashSet<Tag> implements TagCollection {
public TagCollectionImpl() {
}
public TagCollectionImpl(Collection<? extends Tag> c) {
super(c);
}
}

View File

@ -0,0 +1,24 @@
package ru.kirillius.XCP.Persistence;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ser.std.StdSerializer;
public class TagCollectionSerializer extends StdSerializer<TagCollection> {
public TagCollectionSerializer() {
super(TagCollection.class);
}
@Override
public void serialize(TagCollection set, JsonGenerator gen, SerializationContext provider) throws JacksonException {
if(set == null) {
gen.writeNull();
return;
}
gen.writeStartArray();
set.forEach(tag -> gen.writeString(tag.getName()));
gen.writeEndArray();
}
}

View File

@ -0,0 +1,19 @@
package ru.kirillius.XCP.Serialization;
import jakarta.persistence.AttributeConverter;
import ru.kirillius.XCP.Persistence.PollSettingsImpl;
import tools.jackson.databind.ObjectMapper;
public class PollSettingsConverter implements AttributeConverter<PollSettingsImpl, String> {
private final static ObjectMapper mapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(PollSettingsImpl pollSettings) {
return mapper.writeValueAsString(pollSettings);
}
@Override
public PollSettingsImpl convertToEntityAttribute(String s) {
return mapper.readValue(s, PollSettingsImpl.class);
}
}

View File

@ -0,0 +1,19 @@
package ru.kirillius.XCP.Serialization;
import jakarta.persistence.AttributeConverter;
import ru.kirillius.XCP.Data.ValueTransformationChain;
import tools.jackson.databind.ObjectMapper;
public class ValueTransformationChainConverter implements AttributeConverter<ValueTransformationChain, String> {
private final static ObjectMapper mapper = new ObjectMapper();
@Override
public String convertToDatabaseColumn(ValueTransformationChain pollSettings) {
return mapper.writeValueAsString(pollSettings);
}
@Override
public ValueTransformationChain convertToEntityAttribute(String s) {
return mapper.readValue(s, ValueTransformationChain.class);
}
}

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,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,27 @@
package ru.kirillius.XCP.Persistence.Repositories;
import ru.kirillius.XCP.Persistence.Entities.ApiToken;
import ru.kirillius.XCP.Services.RepositoryService;
import java.util.List;
import java.util.UUID;
import static ru.kirillius.XCP.Persistence.TestEnvironment.instantiateTestService;
class ApiTokenRepositoryImplTest extends GenericRepositoryTest<ApiToken, ApiTokenRepositoryImpl> {
@Override
protected RepositoryService spawnRepositoryService() {
var service = instantiateTestService(List.of(repositoryClass, UserRepositoryImpl.class));
var userRepository = service.getRepository(UserRepository.class);
var user = userRepository.create();
userRepository.save(user);
return service;
}
@Override
protected void modify(ApiToken entity, RepositoryService service) {
entity.setName("test" + UUID.randomUUID());
var user = service.getRepository(UserRepository.class).get(1);
entity.setUser(user);
}
}

View File

@ -0,0 +1,115 @@
package ru.kirillius.XCP.Persistence.Repositories;
import org.junit.jupiter.api.Test;
import ru.kirillius.XCP.Persistence.NodeEntity;
import ru.kirillius.XCP.Persistence.NodeRepository;
import java.io.IOException;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
abstract class GenericNodeRepositoryTest<E extends NodeEntity, R extends AbstractNodeRepository<E>> extends GenericRepositoryTest<E, R> {
@Test
void getByGroup() throws IOException {
try (var service = spawnRepositoryService()) {
@SuppressWarnings("unchecked")
var repository = (NodeRepository<E>) service.getRepository(service.getRepositoryBaseType(repositoryClass));
var groupRepository = service.getRepository(GroupRepository.class);
var root = groupRepository.create();
groupRepository.save(root);
var firstChildGroup = groupRepository.create();
firstChildGroup.setParent(root);
groupRepository.save(firstChildGroup);
var secondChildGroup = groupRepository.create();
secondChildGroup.setParent(root);
groupRepository.save(secondChildGroup);
var firstChild = repository.create();
modify(firstChild, service);
firstChild.setParent(firstChildGroup);
repository.save(firstChild);
var secondChild = repository.create();
modify(secondChild, service);
secondChild.setParent(secondChildGroup);
repository.save(secondChild);
try (var handler = repository.getByGroup(firstChildGroup)) {
var found = handler.get().findFirst().orElse(null);
assertThat(found).isNotNull().isEqualTo(firstChild);
}
try (var handler = repository.getByGroup(secondChildGroup)) {
var found = handler.get().findFirst().orElse(null);
assertThat(found).isNotNull().isEqualTo(secondChild);
}
}
}
@Test
void testByTags() throws IOException {
try (var service = spawnRepositoryService()) {
var tagRepository = service.getRepository(TagRepository.class);
@SuppressWarnings("unchecked")
var repository = (NodeRepository<E>) service.getRepository(service.getRepositoryBaseType(repositoryClass));
var first = repository.create();
modify(first, service);
first.setTags(tagRepository.getByNamesOrCreate(List.of(
"first",
"foo",
"bar"
)));
repository.save(first);
var second = repository.create();
modify(second, service);
second.setTags(tagRepository.getByNamesOrCreate(List.of(
"second",
"foo",
"third"
)));
repository.save(second);
var third = repository.create();
modify(third, service);
third.setTags(tagRepository.getByNamesOrCreate(List.of(
"omg",
"third",
"yabba"
)));
repository.save(third);
try (var handler = repository.getByAllTags(tagRepository.getByNamesOrCreate(List.of("first")))) {
assertThat(handler.get().toList()).containsExactlyInAnyOrder(first).doesNotContain(second, third);
}
try (var handler = repository.getByAllTags(tagRepository.getByNamesOrCreate(List.of("foo")))) {
assertThat(handler.get().toList()).containsExactlyInAnyOrder(first, second).doesNotContain(third);
}
try (var handler = repository.getByAllTags(tagRepository.getByNamesOrCreate(List.of("third")))) {
assertThat(handler.get().toList()).containsExactlyInAnyOrder(third, second).doesNotContain(first);
}
try (var handler = repository.getByAnyTag(tagRepository.getByNamesOrCreate(List.of("first", "second", "third")))) {
assertThat(handler.get().toList()).containsExactlyInAnyOrder(first, second, third);
}
try (var handler = repository.getByAnyTag(tagRepository.getByNamesOrCreate(List.of("omg", "yabba")))) {
assertThat(handler.get().toList()).containsExactlyInAnyOrder(third).doesNotContain(second, first);
}
try (var handler = repository.getByAllTags(tagRepository.getByNamesOrCreate(List.of("shrash")))) {
assertThat(handler.get().toList()).isEmpty();
}
}
}
}

View File

@ -0,0 +1,129 @@
package ru.kirillius.XCP.Persistence.Repositories;
import org.junit.jupiter.api.Test;
import ru.kirillius.XCP.Persistence.AbstractRepository;
import ru.kirillius.XCP.Persistence.PersistenceEntity;
import ru.kirillius.XCP.Services.RepositoryService;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static ru.kirillius.XCP.Persistence.TestEnvironment.instantiateTestService;
abstract class GenericRepositoryTest<E extends PersistenceEntity, R extends AbstractRepository<E>> {
protected Class<E> entityClass;
protected Class<R> repositoryClass;
@SuppressWarnings("unchecked")
public GenericRepositoryTest() {
try {
var parameterizedType = (ParameterizedType) getClass().getGenericSuperclass();
var typeArguments = parameterizedType.getActualTypeArguments();
if (typeArguments.length != 2) {
throw new IllegalStateException("Generic parameters count is unsupported");
}
entityClass = (Class<E>) typeArguments[0];
repositoryClass = (Class<R>) typeArguments[1];
} catch (Exception e) {
throw new RuntimeException("Failed to determine service generic parameters for Service: " + this.getClass().getName(), e);
}
}
protected RepositoryService spawnRepositoryService() {
return instantiateTestService(List.of(repositoryClass));
}
@Test
void testCreate() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepositoryForEntity(entityClass);
var entity = repository.create();
assertThat(entity).isNotNull();
}
}
@Test
void testSave() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepositoryForEntity(entityClass);
var entity = repository.create();
modify(entity, service);
repository.save(entity);
assertThat(entity.getId()).isNotZero();
}
}
@Test
void testGet() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepositoryForEntity(entityClass);
var entity = repository.create();
modify(entity, service);
repository.save(entity);
var loaded = repository.get(entity.getId());
assertThat(loaded).isNotNull().isEqualTo(entity);
var loadedByUUID = repository.get(entity.getUuid());
assertThat(loadedByUUID).isNotNull().isEqualTo(entity);
}
}
@Test
void testModify() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepositoryForEntity(entityClass);
var entity = repository.create();
modify(entity, service);
repository.save(entity);
modify(entity, service);
repository.save(entity);
var loaded = repository.get(entity.getId());
assertThat(loaded).isNotNull().isEqualTo(entity);
}
}
@Test
void testRemove() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepositoryForEntity(entityClass);
var entity = repository.create();
modify(entity, service);
repository.save(entity);
assertThat(repository.getCount()).isEqualTo(1);
repository.remove(entity);
assertThat(repository.getCount()).isZero();
}
}
@Test
void testSerialize() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepositoryForEntity(entityClass);
var entity = repository.create();
modify(entity, service);
repository.save(entity);
var serialized = repository.serialize(entity);
var deserialized = repository.deserialize(serialized);
assertThat(deserialized).isNotNull().isEqualTo(entity);
var anotherEntity = repository.create();
modify(anotherEntity, service);
repository.save(anotherEntity);
var anotherSerialized = repository.serialize(anotherEntity);
assertThat(anotherSerialized.toString()).isNotEqualTo(serialized.toString());
var anotherDeserialized = repository.deserialize(anotherSerialized);
assertThat(anotherDeserialized).isNotNull().isEqualTo(anotherEntity).isNotEqualTo(entity);
}
}
protected abstract void modify(E entity, RepositoryService service);
}

View File

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

View File

@ -0,0 +1,47 @@
package ru.kirillius.XCP.Persistence.Repositories;
import ru.kirillius.XCP.Persistence.Entities.Input;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import ru.kirillius.XCP.Services.RepositoryService;
import ru.kirillius.XCP.Persistence.TagCollectionImpl;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import static ru.kirillius.XCP.Persistence.TestEnvironment.instantiateTestService;
class InputRepositoryImplTest extends GenericNodeRepositoryTest<Input, InputRepositoryImpl> {
@Override
protected RepositoryService spawnRepositoryService() {
var service = instantiateTestService(List.of(repositoryClass, TagRepositoryImpl.class, GroupRepositoryImpl.class));
for (var i = 0; i < 5; i++) {
var tagRepository = service.getRepositoryForEntity(Tag.class);
var tag = tagRepository.create();
tag.setName("tag" + i);
tagRepository.save(tag);
}
return service;
}
@Override
protected void modify(Input entity, RepositoryService service) {
var groupRepository = service.getRepository(GroupRepository.class);
var root = groupRepository.getRoot();
if (root == null) {
root = groupRepository.create();
groupRepository.save(root);
}
entity.setParent(root);
entity.setName(UUID.randomUUID().toString());
//var inputRepository = service.getRepository(InputRepository.class);
try (var handler = service.getRepositoryForEntity(Tag.class).getAll()) {
entity.setTags(new TagCollectionImpl(handler.get().toList()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,47 @@
package ru.kirillius.XCP.Persistence.Repositories;
import ru.kirillius.XCP.Persistence.Entities.Output;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import ru.kirillius.XCP.Services.RepositoryService;
import ru.kirillius.XCP.Persistence.TagCollectionImpl;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import static ru.kirillius.XCP.Persistence.TestEnvironment.instantiateTestService;
class OutputRepositoryImplTest extends GenericNodeRepositoryTest<Output, OutputRepositoryImpl> {
@Override
protected RepositoryService spawnRepositoryService() {
var service = instantiateTestService(List.of(repositoryClass, TagRepositoryImpl.class, GroupRepositoryImpl.class));
for (var i = 0; i < 5; i++) {
var tagRepository = service.getRepositoryForEntity(Tag.class);
var tag = tagRepository.create();
tag.setName("tag" + i);
tagRepository.save(tag);
}
return service;
}
@Override
protected void modify(Output entity, RepositoryService service) {
var groupRepository = service.getRepository(GroupRepository.class);
var root = groupRepository.getRoot();
if (root == null) {
root = groupRepository.create();
groupRepository.save(root);
}
entity.setParent(root);
entity.setName(UUID.randomUUID().toString());
//var inputRepository = service.getRepository(InputRepository.class);
try (var handler = service.getRepositoryForEntity(Tag.class).getAll()) {
entity.setTags(new TagCollectionImpl(handler.get().toList()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,74 @@
package ru.kirillius.XCP.Persistence.Repositories;
import org.junit.jupiter.api.Test;
import ru.kirillius.XCP.Persistence.Entities.Tag;
import ru.kirillius.XCP.Services.RepositoryService;
import java.io.IOException;
import java.util.ArrayList;
import static org.assertj.core.api.Assertions.assertThat;
class TagRepositoryImplTest extends GenericRepositoryTest<Tag, TagRepositoryImpl> {
@Test
void testGetByNameOrCreate() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(TagRepository.class);
var tag = repository.create();
tag.setName("test");
repository.save(tag);
var found = repository.getByNameOrCreate(tag.getName());
assertThat(found).isEqualTo(tag);
}
}
@Test
public void testGetByNamesOrCreateAsCollectionOrCreate() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(TagRepository.class);
var tags = new ArrayList<Tag>();
for (var i = 0; i < 10; i++) {
var tag = repository.create();
tag.setName("test" + i);
repository.save(tag);
tags.add(tag);
}
var tagNames = tags.stream().map(Tag::getName).toList();
assertThat(repository.getByNamesOrCreate(tagNames)).containsExactlyInAnyOrderElementsOf(tags);
}
}
@Test
public void testCreateCollection() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(TagRepository.class);
var tags = new ArrayList<Tag>();
for (var i = 0; i < 10; i++) {
var tag = repository.create();
tag.setName("test" + i);
repository.save(tag);
tags.add(tag);
}
var emptyCollection = repository.createCollection();
assertThat(emptyCollection).isNotNull().isEmpty();
try (var handler = repository.getAll()) {
var collection = repository.createCollection(handler);
assertThat(collection).containsExactlyInAnyOrderElementsOf(tags);
}
}
}
@Override
protected void modify(Tag entity, RepositoryService service) {
entity.setName("test" + Math.random());
}
}

View File

@ -0,0 +1,77 @@
package ru.kirillius.XCP.Persistence.Repositories;
import org.junit.jupiter.api.Test;
import ru.kirillius.XCP.Persistence.Entities.User;
import ru.kirillius.XCP.Persistence.PersistenceException;
import ru.kirillius.XCP.Services.RepositoryService;
import java.io.IOException;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
class UserRepositoryImplTest extends GenericRepositoryTest<User, UserRepositoryImpl> {
@Test
void saveSameLogin() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(UserRepository.class);
var user = repository.create();
user.setLogin("test");
repository.save(user);
user = repository.create();
user.setLogin("test");
try {
repository.save(user);
throw new AssertionError("Should have thrown an exception");
} catch (Throwable t) {
assertThat(t).isInstanceOf(PersistenceException.class);
}
}
}
@Test
void getByLogin() throws IOException {
var correct = "correctlogin";
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(UserRepository.class);
for (var i = 0; i < 10; i++) {
var user = repository.create();
user.setLogin("incorrect" + UUID.randomUUID());
repository.save(user);
}
var correctUser = repository.create();
correctUser.setLogin(correct);
repository.save(correctUser);
for (var i = 0; i < 10; i++) {
var user = repository.create();
user.setLogin("incorrect" + UUID.randomUUID());
repository.save(user);
}
var loaded = repository.getByLogin(correct);
assertThat(loaded).isNotNull().isEqualTo(correctUser);
}
}
@Test
void testPasswords() throws IOException {
try (var service = spawnRepositoryService()) {
var repository = service.getRepository(UserRepository.class);
var user = repository.create();
var randPass = UUID.randomUUID().toString();
user.setPassword(randPass);
assertThat(user.verifyPassword(randPass)).isTrue();
assertThat(((UserRepositoryImpl.UserEntity) user).getPasswordHash()).doesNotContain(randPass);
}
}
@Override
protected void modify(User entity, RepositoryService service) {
entity.setName("test" + UUID.randomUUID());
}
}

View File

@ -0,0 +1,203 @@
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 java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static ru.kirillius.XCP.Persistence.TestEnvironment.instantiateTestService;
class RepositoryServiceImplTest {
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);
}
@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 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
void TestSerialization() {
try (var service = instantiateTestService(List.of(RepoImpl.class))) {
var repository = service.getRepository(TestRepository.class);
var testEntity = repository.create();
repository.save(testEntity);
var serialized = repository.serialize(testEntity);
var deserialized = repository.deserialize(serialized);
assertThat(deserialized).isEqualTo(testEntity);
}
}
@Test
public void TestEntitySave() {
try (var service = instantiateTestService(List.of(RepoImpl.class))) {
var repository = service.getRepository(TestRepository.class);
var testEntity = repository.create();
testEntity.setTestField("new");
repository.save(List.of(testEntity));
assertThat(testEntity.getId()).isNotZero();
}
}
@Test
public void TestEntityGet() {
try (var service = instantiateTestService(List.of(RepoImpl.class))) {
var repository = service.getRepository(TestRepository.class);
assertThat(repository.get(1)).isNull();
var testEntity = repository.create();
testEntity.setTestField("new");
repository.save(List.of(testEntity));
var receivedEntity = repository.get(testEntity.getId());
assertThat(receivedEntity).isNotNull().isEqualTo(testEntity);
assertThat(testEntity == receivedEntity).isFalse();//not exact instance
}
}
@Test
public void TestEntityGetMultiple() throws IOException {
try (var service = instantiateTestService(List.of(RepoImpl.class))) {
var repository = service.getRepository(TestRepository.class);
try (var bundle = repository.getAll()) {
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.save(entitiesToSave);
try (var bundle = repository.get(entitiesToSave.stream().map(PersistenceEntity::getId).toList())) {
var loaded = bundle.get().toList();
assertThat(loaded).containsExactlyInAnyOrderElementsOf(entitiesToSave);
}
try (var bundle = repository.getAll()) {
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.save(List.of(testEntity));
testEntity.setTestField("updated");
repository.save(testEntity);
var receivedEntity = repository.get(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.save(List.of(testEntity));
assertThat(repository.getCount()).isEqualTo(1);
repository.remove(List.of(testEntity));
assertThat(repository.get(testEntity.getId())).isNull();
assertThat(repository.getCount()).isZero();
}
}
@Test
public void TestGetByName() {
try (var service = instantiateTestService(List.of(RepoImpl.class))) {
var repository = service.getRepository(TestRepository.class);
var testEntity = repository.create();
repository.save(List.of(testEntity));
var typeByName = service.getEntityTypeByName(TestEntity.class.getSimpleName());
assertThat(typeByName).isNotNull().isEqualTo(TestEntity.class);
}
}
}

View File

@ -0,0 +1,23 @@
package ru.kirillius.XCP.Persistence;
import ru.kirillius.XCP.Commons.Context;
import ru.kirillius.XCP.Security.SecurityManager;
import java.util.Collection;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TestEnvironment {
public static RepositoryServiceImpl instantiateTestService(Collection<Class<? extends AbstractRepository<?>>> classes) {
var service = new RepositoryServiceImpl(new H2InMemoryConfiguration(), classes);
var context = mock(Context.class);
var securityManager = mock(SecurityManager.class);
when(context.getSecurityManager()).thenReturn(securityManager);
when(securityManager.getHashUtility()).thenReturn(new TestHashUtil());
service.initialize(context);
return service;
}
}

Some files were not shown because too many files have changed in this diff Show More