Правки JSON-RPC, ConfigManager, системы логгирования. Бамп версии H2

This commit is contained in:
kirill.labutin 2026-01-12 14:46:08 +03:00
parent 898a170d7b
commit 7e52c4735f
10 changed files with 181 additions and 36 deletions

2
.gitignore vendored
View File

@ -38,3 +38,5 @@ build/
.DS_Store
/.idea/
/.mvn/
xcp.conf
xcpdata.mv.db

View File

@ -16,6 +16,7 @@ import ru.kirillius.XCP.Services.ServiceLoadPriority;
import ru.kirillius.XCP.Services.WebService;
import ru.kirillius.XCP.web.WebServiceImpl;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
@ -39,23 +40,44 @@ public class Application implements Context {
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);
if (configManager.isExist()) {
try {
config = configManager.load();
log.info("Loaded config file: " + configManager.getConfigFile().getAbsolutePath());
} catch (IOException e) {
log.error("Error loading config file " + configManager.getConfigFile(), e);
throw new RuntimeException(e);
}
} else {
log.warning("Unable to find config file " + configManager.getConfigFile().getAbsolutePath() + ". Using default values.");
config = configManager.create();
}
config = loadConfig();
securityManager = new SecurityManagerImpl();
try {
loadServices();
@ -76,17 +98,18 @@ public class Application implements Context {
var order = aClass.getAnnotation(ServiceLoadPriority.class);
return order == null ? 100000 : order.value();
})).forEach(aClass -> {
log.info("Loading service " + aClass.getSimpleName());
@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);
@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()));
services.put(facade, service);
} catch (Throwable e) {
try {
@ -94,14 +117,14 @@ public class Application implements Context {
} catch (IOException ex) {
e.addSuppressed(ex);
}
throw new RuntimeException("Failed to start service " + aClass.getSimpleName(), e);
throw new RuntimeException("Failed to start " + facade.getSimpleName(), e);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Failed to instantiate service " + aClass.getSimpleName(), e);
throw new RuntimeException("Failed to instantiate " + facade.getSimpleName(), e);
}
} catch (NoSuchMethodException e) {
throw new RuntimeException("Failed to find default constructor of service " + aClass.getSimpleName(), e);
throw new RuntimeException("Failed to find default constructor of " + facade.getSimpleName(), e);
}
});
}

View File

@ -11,6 +11,7 @@ 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;
@ -24,7 +25,11 @@ public final class ConfigManagerImpl implements ConfigManager {
}
private File findConfigFile() {
return new File(context.getLaunchArgs().stream().filter(a -> a.startsWith("--config=")).findFirst().orElse(DEFAULT_CONFIG_PATH));
return new File(context.getLaunchArgs().stream()
.filter(a -> a.startsWith("--config="))
.findFirst()
.map(a -> a.split(Pattern.quote("="))[1])
.orElse(DEFAULT_CONFIG_PATH));
}
@Getter
@ -52,7 +57,7 @@ public final class ConfigManagerImpl implements ConfigManager {
@Override
public void save(Config config) throws IOException {
try (var stream = new FileOutputStream(configFile)) {
mapper.writeValue(stream, config);
mapper.writerWithDefaultPrettyPrinter().writeValue(stream, config);
}
}
@ -70,7 +75,7 @@ public final class ConfigManagerImpl implements ConfigManager {
@Getter
@Setter
@JsonProperty
private File databaseFile = new File(DEFAULT_CONFIG_PATH);
private File databaseFile = new File(DEFAULT_DB_PATH);
@Getter
@Setter
@JsonProperty

View File

@ -12,11 +12,11 @@
<artifactId>database</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<!-- Source: https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<version>2.4.240</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core -->
<dependency>

View File

@ -35,7 +35,7 @@ public class LogHandlerImpl extends Handler {
builder.append(logRecord.getMessage().trim());
var thrown = logRecord.getThrown();
if (thrown != null) {
builder.append("\nError thrown ").append(thrown.getClass().getSimpleName()).append(":").append(thrown.getMessage());
builder.append("\n\tThrown ").append(thrown.getClass().getSimpleName()).append(": ").append(thrown.getMessage());
if (debugging) {
builder.append("\nStack trace:\n");
@ -47,6 +47,13 @@ public class LogHandlerImpl extends Handler {
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
var cause = thrown.getCause();
while (cause != null) {
builder.append("\n\t\tCaused by ").append(cause.getClass().getSimpleName()).append(": ").append(cause.getMessage());
cause = cause.getCause();
}
builder.append("\n\t(Stack trace is hidden due to --debug flag)");
}
}
return builder.toString();

View File

@ -31,6 +31,6 @@ public class LoggerImpl extends java.util.logging.Logger implements Logger {
@Override
public void error(Throwable error) {
log(Level.SEVERE, getName() + SEPARATOR + "Thrown error", error);
log(Level.SEVERE, getName() + SEPARATOR + "Thrown unhandled exception", error);
}
}

View File

@ -11,4 +11,23 @@ import java.lang.annotation.Target;
@Target(ElementType.METHOD)
public @interface JsonRpcMethod {
UserRole accessLevel() default UserRole.Admin;
String description() default "";
Parameter[] parameters() default {};
Class<?> returnType() default Void.class;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Parameter {
String name();
String description() default "";
Class<?> type() default Void.class;
boolean optional() default false;
}
}

View File

@ -133,8 +133,38 @@ public class JsonRpcServlet extends HttpServlet {
mapper.writeValue(httpServletResponse.getWriter(), jsonRpcResponse);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST));
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST));
}
@Override
protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException {
mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST));
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST));
}
@Override
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws IOException {
mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST));
}
@Override
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws IOException {
mapper.writeValue(resp.getWriter(), createErrorResponse(null, JsonRpcErrorCode.INVALID_REQUEST));
}
private JsonRpcResponse createErrorResponse(JsonRpcRequest request, JsonRpcErrorCode code) {
return JsonRpcResponse.builder()
return JsonRpcResponse.builder().jsonrpc("2.0")
.id(request == null ? -1L : request.getId())
.error(new JsonRpcError(code))
.build();

View File

@ -21,7 +21,23 @@ import java.util.UUID;
public class Auth extends JsonRpcService {
@JsonRpcMethod(accessLevel = UserRole.Guest)
@JsonRpcMethod(
accessLevel = UserRole.Guest,
description = "Authenticates a user using login and password. Returns true if authentication is successful.",
parameters = {
@JsonRpcMethod.Parameter(
name = "login",
description = "User's login name",
type = String.class
),
@JsonRpcMethod.Parameter(
name = "password",
description = "User's password",
type = String.class
)
},
returnType = boolean.class
)
public boolean authenticateByPassword(CallContext call) {
var login = requireParam(call, "login", JsonNode::asString);
var passwd = requireParam(call, "password", JsonNode::asString);
@ -41,7 +57,11 @@ public class Auth extends JsonRpcService {
}
@JsonRpcMethod(accessLevel = UserRole.User)
@JsonRpcMethod(
accessLevel = UserRole.User,
description = "Retrieves all API tokens associated with the current user",
returnType = ArrayNode.class
)
public ArrayNode getTokens(CallContext call) throws IOException {
var tokenRepository = call.getContext().getService(RepositoryService.class).getRepository(ApiTokenRepository.class);
var tokens = JsonNodeFactory.instance.arrayNode();
@ -57,7 +77,26 @@ public class Auth extends JsonRpcService {
return tokens;
}
@JsonRpcMethod(accessLevel = UserRole.User)
@JsonRpcMethod(
accessLevel = UserRole.User,
description = "Generates a new API token and returns its details",
parameters = {
@JsonRpcMethod.Parameter(
name = "permanent",
description = "If true, creates a token that never expires",
type = boolean.class,
optional = true
),
@JsonRpcMethod.Parameter(
name = "name",
description = "Display name for the token. If not provided, the User-Agent header will be used",
type = String.class,
optional = true
)
},
returnType = ObjectNode.class
)
public ObjectNode generateToken(CallContext call) {
var repositoryService = call.getContext().getService(RepositoryService.class);
var tokenRepository = repositoryService.getRepository(ApiTokenRepository.class);
@ -79,8 +118,19 @@ public class Auth extends JsonRpcService {
return tokenRepository.serialize(token);
}
@JsonRpcMethod(accessLevel = UserRole.Guest)
public boolean authenticateByToken(CallContext call) throws IllegalAccessException {
@JsonRpcMethod(
accessLevel = UserRole.Guest,
description = "Authenticates a user using an API token. Returns true if authentication is successful.",
parameters = {
@JsonRpcMethod.Parameter(
name = "token",
description = "API token string for authentication",
type = String.class
)
},
returnType = boolean.class
)
public boolean authenticateByToken(CallContext call) {
var repositoryService = call.getContext().getService(RepositoryService.class);
var tokenRepository = repositoryService.getRepository(ApiTokenRepository.class);
var token = tokenRepository.get(UUID.fromString(
@ -105,7 +155,11 @@ public class Auth extends JsonRpcService {
return true;
}
@JsonRpcMethod(accessLevel = UserRole.User)
@JsonRpcMethod(
accessLevel = UserRole.User,
description = "Logs out the current user. Returns true if logout is successful, false if the user is not logged in",
returnType = boolean.class
)
public boolean logout(CallContext call) {
ru.kirillius.XCP.Persistence.Entities.User user = null;
@ -117,4 +171,4 @@ public class Auth extends JsonRpcService {
return session != null && user != null;
}
}
}

View File

@ -24,7 +24,7 @@ public class WebServiceImpl implements WebService {
@Override
public void initialize(Context context) {
if (server != null) {
throw new IllegalStateException("Server already started");
throw new IllegalStateException("Server is started already");
}
var jsonRpc = new JsonRpcServlet(context);
jsonRpc.registerRpcService(
@ -47,6 +47,11 @@ public class WebServiceImpl implements WebService {
}
server.setHandler(servletContext);
try {
server.start();
} catch (Exception e) {
throw new RuntimeException("Failed to start jetty web server", e);
}
}
@Override