diff --git a/module-core/pom.xml b/module-core/pom.xml index 324dd5b..cb4ebee 100644 --- a/module-core/pom.xml +++ b/module-core/pom.xml @@ -20,8 +20,17 @@ UTF-8 + + yyyy-MM-dd HH:mm:ss + ${maven.build.timestamp} + + 5.2.5 + + scm:git:https://github.com/intranda/goobi-vocabulary-server + scm:git:https://github.com/intranda/goobi-vocabulary-server + io.goobi.vocabulary @@ -162,6 +171,41 @@ + + + org.codehaus.mojo + buildnumber-maven-plugin + 1.4 + + + validate + + create + + + + + 7 + false + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + ${project.name} + ${buildDate} + ${project.version} + ${buildNumber} + + + + + + maven-compiler-plugin diff --git a/module-core/src/main/java/io/goobi/vocabulary/api/MaintenanceController.java b/module-core/src/main/java/io/goobi/vocabulary/api/MaintenanceController.java index f1303a6..0cc0249 100644 --- a/module-core/src/main/java/io/goobi/vocabulary/api/MaintenanceController.java +++ b/module-core/src/main/java/io/goobi/vocabulary/api/MaintenanceController.java @@ -1,7 +1,7 @@ package io.goobi.vocabulary.api; +import io.goobi.vocabulary.maintenance.selfcheck.SelfCheckResult; import io.goobi.vocabulary.service.manager.MaintenanceManager; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -16,14 +16,7 @@ public MaintenanceController(MaintenanceManager maintenanceManager) { } @GetMapping("/selfcheck") - public ResponseEntity selfCheck() { - String status = maintenanceManager.testAllData(); - if (status.contains("FAIL")) { - return ResponseEntity.internalServerError() - .body(status); - } else { - return ResponseEntity.ok() - .body(status); - } + public SelfCheckResult selfCheck() { + return maintenanceManager.performFullSelfCheck(); } } diff --git a/module-core/src/main/java/io/goobi/vocabulary/api/MonitoringController.java b/module-core/src/main/java/io/goobi/vocabulary/api/MonitoringController.java new file mode 100644 index 0000000..9c498ea --- /dev/null +++ b/module-core/src/main/java/io/goobi/vocabulary/api/MonitoringController.java @@ -0,0 +1,25 @@ +package io.goobi.vocabulary.api; + +import io.goobi.vocabulary.maintenance.MonitoringResult; +import io.goobi.vocabulary.service.manager.MaintenanceManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/api/v1") +public class MonitoringController { + private final MaintenanceManager maintenanceManager; + + public MonitoringController(MaintenanceManager maintenanceManager) { + this.maintenanceManager = maintenanceManager; + } + + @GetMapping("/monitoring") + public MonitoringResult monitoring() { + return maintenanceManager.getMonitoringResult(); + } + +} diff --git a/module-core/src/main/java/io/goobi/vocabulary/maintenance/FlywayInformation.java b/module-core/src/main/java/io/goobi/vocabulary/maintenance/FlywayInformation.java new file mode 100644 index 0000000..d7c8b9f --- /dev/null +++ b/module-core/src/main/java/io/goobi/vocabulary/maintenance/FlywayInformation.java @@ -0,0 +1,6 @@ +package io.goobi.vocabulary.maintenance; + +import java.util.Date; + +public record FlywayInformation(String schemaVersion, String description, Date date, int executionTime, boolean success) { +} diff --git a/module-core/src/main/java/io/goobi/vocabulary/maintenance/ManifestReader.java b/module-core/src/main/java/io/goobi/vocabulary/maintenance/ManifestReader.java new file mode 100644 index 0000000..a0d860b --- /dev/null +++ b/module-core/src/main/java/io/goobi/vocabulary/maintenance/ManifestReader.java @@ -0,0 +1,36 @@ +package io.goobi.vocabulary.maintenance; + +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.Optional; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +@Getter +@Service +public class ManifestReader { + private final Manifest manifest; + + private final String revision; + private final String version; + private final String buildDate; + + public ManifestReader() throws IllegalArgumentException, IOException { + manifest = new Manifest(getClass().getResourceAsStream("/META-INF/MANIFEST.MF")); + Attributes mainAttributes = manifest.getMainAttributes(); + revision = getOptionalValue(mainAttributes, "Revision").orElse("unknown"); + version = getOptionalValue(mainAttributes, "Version").orElse("unknown"); + buildDate = getOptionalValue(mainAttributes, "Build-Date").orElse("unknown"); + } + + private Optional getOptionalValue(Attributes attributes, String attributeName) throws IllegalArgumentException { + String result = attributes.getValue(attributeName); + if (StringUtils.isBlank(result)) { + result = null; + } + return Optional.ofNullable(result); + } +} diff --git a/module-core/src/main/java/io/goobi/vocabulary/maintenance/MonitoringResult.java b/module-core/src/main/java/io/goobi/vocabulary/maintenance/MonitoringResult.java new file mode 100644 index 0000000..5da4e48 --- /dev/null +++ b/module-core/src/main/java/io/goobi/vocabulary/maintenance/MonitoringResult.java @@ -0,0 +1,6 @@ +package io.goobi.vocabulary.maintenance; + +import io.goobi.vocabulary.maintenance.selfcheck.SelfCheckResult; + +public record MonitoringResult(MonitoringState monitoring, VersionsCollection versions, FlywayInformation flyway, SelfCheckResult selfCheck) { +} diff --git a/module-core/src/main/java/io/goobi/vocabulary/maintenance/MonitoringState.java b/module-core/src/main/java/io/goobi/vocabulary/maintenance/MonitoringState.java new file mode 100644 index 0000000..a42d3de --- /dev/null +++ b/module-core/src/main/java/io/goobi/vocabulary/maintenance/MonitoringState.java @@ -0,0 +1,4 @@ +package io.goobi.vocabulary.maintenance; + +public record MonitoringState(String database, String selfCheck) { +} diff --git a/module-core/src/main/java/io/goobi/vocabulary/maintenance/VersionInformation.java b/module-core/src/main/java/io/goobi/vocabulary/maintenance/VersionInformation.java new file mode 100644 index 0000000..cd9aafd --- /dev/null +++ b/module-core/src/main/java/io/goobi/vocabulary/maintenance/VersionInformation.java @@ -0,0 +1,4 @@ +package io.goobi.vocabulary.maintenance; + +public record VersionInformation(String version, String hash) { +} diff --git a/module-core/src/main/java/io/goobi/vocabulary/maintenance/VersionsCollection.java b/module-core/src/main/java/io/goobi/vocabulary/maintenance/VersionsCollection.java new file mode 100644 index 0000000..c8b537d --- /dev/null +++ b/module-core/src/main/java/io/goobi/vocabulary/maintenance/VersionsCollection.java @@ -0,0 +1,4 @@ +package io.goobi.vocabulary.maintenance; + +public record VersionsCollection(VersionInformation core) { +} diff --git a/module-core/src/main/java/io/goobi/vocabulary/maintenance/selfcheck/SelfCheckResult.java b/module-core/src/main/java/io/goobi/vocabulary/maintenance/selfcheck/SelfCheckResult.java new file mode 100644 index 0000000..8d7f2a9 --- /dev/null +++ b/module-core/src/main/java/io/goobi/vocabulary/maintenance/selfcheck/SelfCheckResult.java @@ -0,0 +1,9 @@ +package io.goobi.vocabulary.maintenance.selfcheck; + +import java.util.Date; + +public record SelfCheckResult(Date date, ValidationResult types, ValidationResult vocabularies, ValidationResult schemas, ValidationResult records) { + public boolean success() { + return types.errors().isEmpty() && vocabularies.errors().isEmpty() && schemas.errors().isEmpty() && records.errors().isEmpty(); + } +} diff --git a/module-core/src/main/java/io/goobi/vocabulary/maintenance/selfcheck/ValidationResult.java b/module-core/src/main/java/io/goobi/vocabulary/maintenance/selfcheck/ValidationResult.java new file mode 100644 index 0000000..1c87522 --- /dev/null +++ b/module-core/src/main/java/io/goobi/vocabulary/maintenance/selfcheck/ValidationResult.java @@ -0,0 +1,6 @@ +package io.goobi.vocabulary.maintenance.selfcheck; + +import java.util.List; + +public record ValidationResult(List errors) { +} diff --git a/module-core/src/main/java/io/goobi/vocabulary/service/manager/MaintenanceManager.java b/module-core/src/main/java/io/goobi/vocabulary/service/manager/MaintenanceManager.java index abfc4e5..fde262b 100644 --- a/module-core/src/main/java/io/goobi/vocabulary/service/manager/MaintenanceManager.java +++ b/module-core/src/main/java/io/goobi/vocabulary/service/manager/MaintenanceManager.java @@ -1,6 +1,15 @@ package io.goobi.vocabulary.service.manager; +import com.fasterxml.jackson.databind.ObjectMapper; import io.goobi.vocabulary.exception.VocabularyException; +import io.goobi.vocabulary.maintenance.FlywayInformation; +import io.goobi.vocabulary.maintenance.ManifestReader; +import io.goobi.vocabulary.maintenance.MonitoringResult; +import io.goobi.vocabulary.maintenance.MonitoringState; +import io.goobi.vocabulary.maintenance.VersionInformation; +import io.goobi.vocabulary.maintenance.VersionsCollection; +import io.goobi.vocabulary.maintenance.selfcheck.SelfCheckResult; +import io.goobi.vocabulary.maintenance.selfcheck.ValidationResult; import io.goobi.vocabulary.model.jpa.FieldTypeEntity; import io.goobi.vocabulary.model.jpa.VocabularyEntity; import io.goobi.vocabulary.model.jpa.VocabularyRecordEntity; @@ -10,14 +19,23 @@ import io.goobi.vocabulary.repositories.VocabularyRepository; import io.goobi.vocabulary.repositories.VocabularySchemaRepository; import io.goobi.vocabulary.validation.Validator; +import lombok.extern.slf4j.Slf4j; +import org.flywaydb.core.Flyway; import org.springframework.data.repository.ListCrudRepository; import org.springframework.stereotype.Service; +import java.io.File; +import java.io.IOException; +import java.util.Date; import java.util.LinkedList; import java.util.List; +import java.util.Optional; +@Slf4j @Service public class MaintenanceManager { + public static final File SELF_CHECK_CACHE_FILE = new File("/tmp/vocabulary-server-self-check.json"); + private final FieldTypeRepository fieldTypeRepository; private final VocabularyRepository vocabularyRepository; private final VocabularySchemaRepository vocabularySchemaRepository; @@ -28,7 +46,12 @@ public class MaintenanceManager { private final Validator vocabularySchemaValidator; private final Validator vocabularyRecordValidator; - public MaintenanceManager(FieldTypeRepository fieldTypeRepository, VocabularyRepository vocabularyRepository, VocabularySchemaRepository vocabularySchemaRepository, VocabularyRecordRepository vocabularyRecordRepository, Validator fieldTypeValidator, Validator vocabularyValidator, Validator vocabularySchemaValidator, Validator vocabularyRecordValidator) { + private final ManifestReader manifestReader; + private final Flyway flyway; + + private final ObjectMapper objectMapper; + + public MaintenanceManager(FieldTypeRepository fieldTypeRepository, VocabularyRepository vocabularyRepository, VocabularySchemaRepository vocabularySchemaRepository, VocabularyRecordRepository vocabularyRecordRepository, Validator fieldTypeValidator, Validator vocabularyValidator, Validator vocabularySchemaValidator, Validator vocabularyRecordValidator, ManifestReader manifestReader, Flyway flyway, ObjectMapper objectMapper) { this.fieldTypeRepository = fieldTypeRepository; this.vocabularyRepository = vocabularyRepository; this.vocabularyValidator = vocabularyValidator; @@ -37,47 +60,90 @@ public MaintenanceManager(FieldTypeRepository fieldTypeRepository, VocabularyRep this.fieldTypeValidator = fieldTypeValidator; this.vocabularySchemaValidator = vocabularySchemaValidator; this.vocabularyRecordValidator = vocabularyRecordValidator; + this.manifestReader = manifestReader; + this.flyway = flyway; + this.objectMapper = objectMapper; } - public String testAllData() { - List selfCheckResults = new LinkedList<>(); + public SelfCheckResult performFullSelfCheck() { + ValidationResult types = new ValidationResult(selfTest(fieldTypeRepository, fieldTypeValidator)); + ValidationResult vocabularies = new ValidationResult(selfTest(vocabularyRepository, vocabularyValidator)); + ValidationResult vocabularySchemas = new ValidationResult(selfTest(vocabularySchemaRepository, vocabularySchemaValidator)); + ValidationResult vocabularyRecords = new ValidationResult(selfTest(vocabularyRecordRepository, vocabularyRecordValidator)); - selfCheckResults.add(selfTest("Types", fieldTypeRepository, fieldTypeValidator)); - selfCheckResults.add(selfTest("Vocabularies", vocabularyRepository, vocabularyValidator)); - selfCheckResults.add(selfTest("Vocabulary Schemas", vocabularySchemaRepository, vocabularySchemaValidator)); - selfCheckResults.add(selfTest("Vocabulary Records", vocabularyRecordRepository, vocabularyRecordValidator)); - - return String.join("\n", selfCheckResults); + SelfCheckResult result = new SelfCheckResult(new Date(), types, vocabularies, vocabularySchemas, vocabularyRecords); + saveSelfCheckResult(result); + return result; } - - private String selfTest(String entityName, ListCrudRepository repo, Validator validator) { + private List selfTest(ListCrudRepository repo, Validator validator) { List errors = new LinkedList<>(); for (Entity entity : repo.findAll()) { try { validator.validate(entity); } catch (VocabularyException e) { - errors.add(dumpException(e, 1)); + StringBuilder s = new StringBuilder(); + dumpException(s, e); + errors.add(s.toString()); } } - if (errors.isEmpty()) { - return entityName + ": OK"; - } else { - return entityName + ": FAIL" - + String.join("", errors); - } + return errors; } - private String dumpException(VocabularyException ex, int level) { - StringBuilder s = new StringBuilder(); - s.append('\n') - .append("\t".repeat(level)) - .append(ex.getMessage()); + private void dumpException(StringBuilder s, VocabularyException ex) { + s.append(ex.getMessage()); if (ex.getCauses() != null) { for (VocabularyException c : ex.getCauses()) { - s.append(dumpException(c, level + 1)); + s.append("; "); + dumpException(s, c); } } - return s.toString(); + } + + public MonitoringResult getMonitoringResult() { + VersionInformation coreVersion = new VersionInformation(manifestReader.getVersion(), manifestReader.getRevision()); + VersionsCollection versions = new VersionsCollection(coreVersion); + FlywayInformation flywayInformation = getFlywayInformation(); + Optional selfCheckResult = loadSelfCheckResult(); + MonitoringState monitoringState = getMonitoringState(flywayInformation, selfCheckResult); + return new MonitoringResult(monitoringState, versions, flywayInformation, selfCheckResult.orElse(null)); + } + + private MonitoringState getMonitoringState(FlywayInformation flywayInformation, Optional selfCheckStatus) { + String database = flywayInformation.success() ? "ok" : "error"; + String selfCheck = "unknown"; + if (selfCheckStatus.isPresent()) { + selfCheck = selfCheckStatus.get().success() ? "ok" : "error"; + } + return new MonitoringState(database, selfCheck); + } + + private FlywayInformation getFlywayInformation() { + String version = flyway.info().current().getVersion().getVersion(); + String description = flyway.info().current().getDescription(); + Date date = flyway.info().current().getInstalledOn(); + int executionTime = flyway.info().current().getExecutionTime(); + boolean success = flyway.info().current().getState().isApplied(); + return new FlywayInformation(version, description, date, executionTime, success); + } + + private void saveSelfCheckResult(SelfCheckResult result) { + try { + objectMapper.writeValue(SELF_CHECK_CACHE_FILE, result); + } catch (IOException e) { + log.error("Error saving self-check result", e); + } + } + + private Optional loadSelfCheckResult() { + if (!MaintenanceManager.SELF_CHECK_CACHE_FILE.exists()) { + return Optional.empty(); + } + try { + return Optional.of(objectMapper.readValue(MaintenanceManager.SELF_CHECK_CACHE_FILE, SelfCheckResult.class)); + } catch (IOException e) { + log.error("Error reading cached self-check results", e); + return Optional.empty(); + } } }