diff --git a/config/common.properties b/config/common.properties index 6512137..620b136 100644 --- a/config/common.properties +++ b/config/common.properties @@ -2,4 +2,7 @@ # publication.version = 0.3-SNAPSHOT publication.version=0.2.9.6-SNAPSHOT #publication.version=0.2.9.3 -#publication.version=0.2.8.8 \ No newline at end of file +#publication.version=0.2.8.8 + +# jackson version +deps.jackson.version=2.17.2 \ No newline at end of file diff --git a/jackson/build.gradle b/jackson/build.gradle index 01adf1a..7160c7b 100644 --- a/jackson/build.gradle +++ b/jackson/build.gradle @@ -81,9 +81,9 @@ dependencies { testImplementation 'com.github.victools:jsonschema-generator:4.36.0' // jackson - implementation 'com.fasterxml.jackson.core:jackson-core:2.17.2' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0' - implementation 'com.fasterxml.jackson.core:jackson-annotations:2.17.2' + implementation "com.fasterxml.jackson.core:jackson-core:${commonProps.get('deps.jackson.version')}" + implementation "com.fasterxml.jackson.core:jackson-databind:${commonProps.get('deps.jackson.version')}" + implementation "com.fasterxml.jackson.core:jackson-annotations:${commonProps.get('deps.jackson.version')}" } jar { diff --git a/jackson/src/main/java/eu/mihosoft/vmf/jackson/VMFJsonSchemaGenerator.java b/jackson/src/main/java/eu/mihosoft/vmf/jackson/VMFJsonSchemaGenerator.java index 20d7347..0b85261 100644 --- a/jackson/src/main/java/eu/mihosoft/vmf/jackson/VMFJsonSchemaGenerator.java +++ b/jackson/src/main/java/eu/mihosoft/vmf/jackson/VMFJsonSchemaGenerator.java @@ -584,6 +584,9 @@ private void addPropertyOrder(Property property, Map propertySch if (order != null) { propertySchema.put("propertyOrder", Integer.parseInt(order)); } + } else{ + // TODO reuse property order from vmf (traversal order via @PropertyOrder, vmf should report th order + // as @Annotation("key="...", value = "...") as well } } catch (Exception e) { throw new RuntimeException("Failed to parse property order", e); diff --git a/vmfedit/build.gradle b/vmfedit/build.gradle index 4c1087f..d67465a 100644 --- a/vmfedit/build.gradle +++ b/vmfedit/build.gradle @@ -63,6 +63,11 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}") + + // add jackson dependencies, use jackson as specified in the common.properties file + implementation "com.fasterxml.jackson.core:jackson-core:${commonProps.get('deps.jackson.version')}" + implementation "com.fasterxml.jackson.core:jackson-databind:${commonProps.get('deps.jackson.version')}" + implementation "com.fasterxml.jackson.core:jackson-annotations:${commonProps.get('deps.jackson.version')}" } test { diff --git a/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonEditorAppController.java b/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonEditorAppController.java index 8b226e9..0d8841b 100644 --- a/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonEditorAppController.java +++ b/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonEditorAppController.java @@ -1,16 +1,17 @@ package eu.mihosoft.vmf.vmfedit; import javafx.fxml.FXML; -import javafx.scene.control.Alert; -import javafx.scene.control.Spinner; -import javafx.scene.control.TextField; +import javafx.scene.control.*; import javafx.scene.web.WebView; import javafx.stage.FileChooser; import javafx.stage.Stage; import java.io.File; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; +import java.util.Optional; + public class JsonEditorAppController { @@ -19,11 +20,15 @@ public class JsonEditorAppController { @FXML private TextField schemaField; + @FXML + private Button browseSchemaButton; private JsonEditorController jsonEditorControl; private File currentFile; + private JsonUtils.SchemaInfo schemaInfo; + @FXML public void initialize() { @@ -45,6 +50,45 @@ public void initialize() { } + @FXML + private void handleNewDocument() { + // ask user whether to save current document or not (yes/no/cancel) + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Save Document"); + alert.setHeaderText("Do you want to save the current document?"); + alert.setContentText("Choose your option."); + + // Set up the button types + ButtonType buttonTypeYes = new ButtonType("Yes"); + ButtonType buttonTypeNo = new ButtonType("No"); + ButtonType buttonTypeCancel = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); + alert.getButtonTypes().setAll(buttonTypeYes, buttonTypeNo, buttonTypeCancel); + + // Show the dialog and wait for response + Optional result = alert.showAndWait(); + + if (result.isPresent()) { + if (result.get() == buttonTypeYes) { + // User chose Yes, save the document + handleSaveDocument(); + } else if (result.get() == buttonTypeCancel) { + // User chose Cancel, do nothing and return + return; + } + // If user chose No, continue with creating new document + } + + // clear editor + schemaField.setText(""); + schemaField.setDisable(false); + browseSchemaButton.setDisable(false); + currentFile = null; + jsonEditorControl.reset(); + // get stage and set title + Stage stage = (Stage) webView.getScene().getWindow(); + stage.setTitle("VMF JSON Editor"); + } + @FXML private void handleLoadDocument() { FileChooser fileChooser = new FileChooser(); @@ -61,6 +105,31 @@ private void handleLoadDocument() { if (file != null) { try { String content = new String(Files.readAllBytes(file.toPath())); + + // remove comments + content = content.replaceAll("//.*", ""); + + // baseURI from parent directory + URI baseURI = file.getParentFile().toURI(); + + JsonUtils.SchemaInfo schemaInfo = JsonUtils.extractSchema(content, baseURI); + + this.schemaInfo = schemaInfo; + + if (schemaInfo.schemaUri() != null) { + schemaField.setDisable(true); + browseSchemaButton.setDisable(true); + schemaField.setText(schemaInfo.schemaUri().toString()); + } else if (schemaInfo.schemaContent() != null) { + schemaField.setDisable(true); + browseSchemaButton.setDisable(true); + jsonEditorControl.setSchema(schemaInfo.schemaContent()); + } else { + schemaField.setDisable(false); + browseSchemaButton.setDisable(false); + jsonEditorControl.setSchema(null); + } + jsonEditorControl.setValue(content); currentFile = file; @@ -82,6 +151,12 @@ private void handleSaveDocument() { try { String content = jsonEditorControl.getValue(); System.out.println("Saving document: " + content); + + // add schema as was specified in the loaded file (as URI or content, see JsonUtils.extractSchema) + if (this.schemaInfo!=null) { + content = JsonUtils.injectSchema(content, this.schemaInfo); + } + Files.write(currentFile.toPath(), content.getBytes()); // get stage and set title @@ -133,6 +208,12 @@ private void handleSaveAsDocument() { try { String content = jsonEditorControl.getValue(); System.out.println("Saving document as: " + content); + + // add schema as was specified in the loaded file (as URI or content, see JsonUtils.extractSchema) + if (this.schemaInfo!=null) { + content = JsonUtils.injectSchema(content, this.schemaInfo); + } + Files.write(file.toPath(), content.getBytes()); currentFile = file; @@ -171,6 +252,8 @@ private void handleBrowseSchema() { File file = fileChooser.showOpenDialog(webView.getScene().getWindow()); if (file != null) { schemaField.setText(file.getAbsolutePath()); + // update schemaInfo + this.schemaInfo = new JsonUtils.SchemaInfo(file.toURI(), null, JsonUtils.SchemaEmbeddingType.EXTERNAL); } } diff --git a/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonEditorController.java b/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonEditorController.java index d389d3c..255de29 100644 --- a/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonEditorController.java +++ b/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonEditorController.java @@ -3,6 +3,7 @@ import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; import javafx.concurrent.Worker; import javafx.fxml.FXML; import javafx.scene.control.Alert; @@ -11,7 +12,7 @@ import netscape.javascript.JSObject; import java.net.URL; -import java.util.Objects; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -31,15 +32,17 @@ public class JsonEditorController { private volatile boolean updatingValue = false; - /** Property holding the current JSON schema */ - private final StringProperty schemaProperty = new SimpleStringProperty( - "{\n" + + private final static String defaultSchema = + "{\n" + " \"$schema\" : \"http://json-schema.org/draft-07/schema#\",\n" + " \"title\" : \"value\",\n" + " \"type\" : \"string\",\n" + " \"readOnly\": true,\n" + " \"default\": \"set a schema\"\n" + - "}"); + "}"; + + /** Property holding the current JSON schema */ + private final StringProperty schemaProperty = new SimpleStringProperty(defaultSchema); /** Property holding the current JSON value */ private final StringProperty valueProperty = new SimpleStringProperty(""); @@ -53,6 +56,44 @@ public JsonEditorController(WebView webView) { this.webView = webView; } + /** + * Resets the JSON editor to its initial state. This clears the schema and value, and reloads the editor. + */ + public void reset() { + migrateValueOnSchemaUpdate = false; + try { + valueProperty.set(""); + setSchema(defaultSchema); + handleSchemaUpdate(defaultSchema); + } finally { + migrateValueOnSchemaUpdate = true; + } + } + + /** + * Initializes the JSON editor component. This method is called automatically by FXML. + * It sets up the WebView, establishes JavaScript bridges, and configures property listeners. + */ + @FXML + public void initialize() { + WebEngine engine = webView.getEngine(); + + // Set up the web engine load listener + setupWebEngineLoadListener(engine); + + // Set up schema change listener + setupSchemaChangeListener(); + + // Set up WebView focus handling + setupWebViewFocusHandling(); + + // Set up JavaScript event handlers + setupJavaScriptEventHandlers(engine); + + // Set up value property change listener + setupValueChangeListener(); + } + /** * Gets the current JSON schema. * @@ -201,42 +242,37 @@ public void setLogListener(LogListener logListener) { this.logListener = logListener; } - /** - * Initializes the JSON editor component. This method is called automatically by FXML. - * It sets up the WebView, establishes JavaScript bridges, and configures property listeners. - */ - @FXML - public void initialize() { - WebEngine engine = webView.getEngine(); - - // Set up the web engine load listener - setupWebEngineLoadListener(engine); - - // Set up schema change listener - setupSchemaChangeListener(); - // Set up WebView focus handling - setupWebViewFocusHandling(); + public void configureEditor(Map config) { - // Set up JavaScript event handlers - setupJavaScriptEventHandlers(engine); + setSchema(schemaProperty().get()); + } - // Set up value property change listener - setupValueChangeListener(); + public String getDefaultSchema() { + return defaultSchema; } + private ChangeListener webEngineLoadListener = null; + /** * Sets up the web engine load listener to handle page load events. */ private void setupWebEngineLoadListener(WebEngine engine) { - engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> { + + if(webEngineLoadListener!=null) { + engine.getLoadWorker().stateProperty().removeListener(webEngineLoadListener); + } + + webEngineLoadListener = (observable, oldValue, newValue) -> { if (newValue == Worker.State.SUCCEEDED) { logListener.log(LogLevel.INFO, "Page loaded successfully", null); initializeJavaScriptBridge(engine); } else if (newValue == Worker.State.FAILED) { logListener.log(LogLevel.ERROR, "Page failed to load", null); } - }); + }; + + engine.getLoadWorker().stateProperty().addListener(webEngineLoadListener); // Load the HTML file URL url = getClass().getResource("json-editor.html"); @@ -260,27 +296,48 @@ private void initializeJavaScriptBridge(WebEngine engine) { engine.executeScript("updateSchema('" + escapeJavaScript(schemaProperty.get()) + "')"); } + private ChangeListener schemaChangeListener = null; + /** * Sets up the schema change listener to handle schema updates. */ private void setupSchemaChangeListener() { - schemaProperty().addListener((observable, oldValue, newValue) -> { + + if(schemaChangeListener!=null) { + schemaProperty().removeListener(schemaChangeListener); + } + + schemaChangeListener = (observable, oldValue, newValue) -> { try { handleSchemaUpdate(newValue); } catch (Exception e) { logListener.log(LogLevel.ERROR, "Error loading schema: " + e.getMessage(), e); showError("Error loading schema", e.getMessage()); } - }); + }; + + schemaProperty().addListener(schemaChangeListener); } + private boolean migrateValueOnSchemaUpdate = true; + /** * Handles updating the schema and attempting to migrate existing values. */ private void handleSchemaUpdate(String schema) { - String value = (String) webView.getEngine().executeScript("JSON.stringify(getValue())"); + + String value = ""; + + if(migrateValueOnSchemaUpdate) { + value = (String) webView.getEngine().executeScript("JSON.stringify(getValue())"); + } + logListener.log(LogLevel.INFO, "Schema updated. Value will be reset and migration attempt will be made.", null); + if(schema == null || schema.isEmpty()) { + schema = defaultSchema; + } + webView.getEngine().executeScript("updateSchema('" + escapeJavaScript(schema) + "')"); if (value != null && !value.isEmpty() && !"\"\"".equals(value) && !"\"set a schema\"".equals(value)) { @@ -311,18 +368,25 @@ private void attemptValueMigration(String value) { /** * Attempts to set a value in the editor with proper UI thread handling. + * @param value The value to set + * @return {@code true} if the value was set successfully, {@code false} otherwise */ private boolean trySetValue(String value) { if(updatingValue) { return true; } + var future = new CompletableFuture(); + String finalValue = value; Platform.runLater(() -> { try { var scene = webView.getScene(); scene.getRoot().setDisable(true); - webView.getEngine().executeScript("setValue('" + escapeJavaScript(value) + "')"); + + webView.getEngine().executeScript("setValue('" + escapeJavaScript(finalValue) + "')"); + future.complete(true); + System.out.println("Value set: " + finalValue); } catch (Exception e) { future.complete(false); logListener.log(LogLevel.ERROR, "Error setting value: " + e.getMessage(), e); @@ -334,15 +398,23 @@ private boolean trySetValue(String value) { return future.join(); } + private ChangeListener focusChangedListener = null; + /** * Sets up focus handling for the WebView component. */ private void setupWebViewFocusHandling() { - webView.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> { - if (!isNowFocused) { + if(focusChangedListener!=null) { + webView.focusedProperty().removeListener(focusChangedListener); + } + + focusChangedListener = (observable, oldValue, newValue) -> { + if (!newValue) { webView.getEngine().executeScript("document.activeElement.blur();"); } - }); + }; + + webView.focusedProperty().addListener(focusChangedListener); } /** @@ -356,15 +428,26 @@ private void setupJavaScriptEventHandlers(WebEngine engine) { engine.setOnVisibilityChanged(event -> logListener.log(LogLevel.INFO, "JS Visibility Changed: " + event.getData(), null)); } + private ChangeListener valueChangeListener = null; + /** * Sets up the value change listener to handle value updates. */ private void setupValueChangeListener() { - valueProperty().addListener((observable, oldValue, newValue) -> { + + if(valueChangeListener!=null) { + valueProperty().removeListener(valueChangeListener); + } + + valueChangeListener = (observable, oldValue, newValue) -> { if (newValue != null && !newValue.isEmpty() && !"\"\"".equals(newValue)) { attemptValueMigration(newValue); } - }); +// attemptValueMigration(newValue); + }; + + valueProperty().addListener(valueChangeListener); + } /** diff --git a/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonUtils.java b/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonUtils.java new file mode 100644 index 0000000..e443cd2 --- /dev/null +++ b/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/JsonUtils.java @@ -0,0 +1,171 @@ +package eu.mihosoft.vmf.vmfedit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; + +public final class JsonUtils { + private JsonUtils() { + throw new AssertionError("Don't instantiate me!"); + } + + enum SchemaEmbeddingType { + EXTERNAL, EMBEDDED, EMBEDDED_DEFINITIONS, NONE + } + + public record SchemaInfo(URI schemaUri, String schemaContent, SchemaEmbeddingType embeddingType ) {} + + /** + * Extracts schema information from JSON content. + * + * @param jsonContent The JSON content to analyze + * @param baseUri Optional base URI for resolving relative schema paths (can be null) + * @return A record containing the schema URI (if external) and schema content (if embedded) + * @throws JsonProcessingException if JSON parsing fails + * @throws IOException if schema loading fails + */ + public static SchemaInfo extractSchema(String jsonContent, URI baseUri) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(jsonContent); + + // Case 1: Check for $schema URI reference + if (rootNode.has("$schema") && rootNode.get("$schema").isTextual()) { + String schemaRef = rootNode.get("$schema").asText(); + try { + URI schemaUri; + if (schemaRef.startsWith("http://") || schemaRef.startsWith("https://") + || schemaRef.startsWith("file:/")) { + schemaUri = new URI(schemaRef); + } else if (baseUri != null) { + // Resolve relative path against baseUri + schemaUri = baseUri.resolve(schemaRef); + } else { + // Relative path without baseUri - return as-is + schemaUri = new URI(schemaRef); + } + return new SchemaInfo(schemaUri, null, SchemaEmbeddingType.EXTERNAL); + } catch (URISyntaxException e) { + throw new IOException("Invalid schema URI: " + schemaRef, e); + } + } + + // Case 2: Check for embedded schema object + if (rootNode.has("$schema") && rootNode.get("$schema").isObject()) { + return new SchemaInfo(null, rootNode.get("$schema").toString(), SchemaEmbeddingType.EMBEDDED); + } + + // Case 3: Check for schema in definitions + if (rootNode.has("definitions")) { + JsonNode definitions = rootNode.get("definitions"); + // Look for any $ref usage in the root properties + if (rootNode.has("properties")) { + JsonNode properties = rootNode.get("properties"); + if (properties.isObject()) { + Iterator elements = properties.elements(); + while (elements.hasNext()) { + JsonNode prop = elements.next(); + if (prop.has("$ref") && prop.get("$ref").asText().startsWith("#/definitions/")) { + // Found a reference to definitions, return the whole schema structure + ObjectNode schemaNode = mapper.createObjectNode(); + schemaNode.set("definitions", definitions); + schemaNode.set("type", rootNode.get("type")); + schemaNode.set("properties", properties); + return new SchemaInfo(null, schemaNode.toString(), SchemaEmbeddingType.EMBEDDED_DEFINITIONS); + } + } + } + } + } + + // Case 4: No schema found + return new SchemaInfo(null, null, SchemaEmbeddingType.NONE); + } + + /** + * Injects schema information into JSON content. + * + * @param jsonContent The JSON content to modify + * @param schemaInfo The schema information to inject + * @return The modified JSON content + * @throws IOException if JSON parsing fails + * @throws IllegalArgumentException if schemaInfo is null or invalid + */ + public static String injectSchema(String jsonContent, SchemaInfo schemaInfo) throws IOException { + if (schemaInfo == null) { + throw new IllegalArgumentException("SchemaInfo cannot be null"); + } + + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(jsonContent); + ObjectNode mutableRoot = (ObjectNode) rootNode; + + switch (schemaInfo.embeddingType()) { + case EXTERNAL -> { + if (schemaInfo.schemaUri() == null) { + throw new IllegalArgumentException("Schema URI cannot be null for EXTERNAL embedding type"); + } + System.out.println("!!! Case External: Injecting schema URI: " + schemaInfo.schemaUri()); + mutableRoot.put("$schema", schemaInfo.schemaUri().toString()); + } + case EMBEDDED -> { + if (schemaInfo.schemaContent() == null) { + throw new IllegalArgumentException("Schema content cannot be null for EMBEDDED embedding type"); + } + JsonNode schemaNode = mapper.readTree(schemaInfo.schemaContent()); + mutableRoot.set("$schema", schemaNode); + System.out.println("!!! Case Embedded: Injecting schema content: " + schemaInfo.schemaContent()); + } + case EMBEDDED_DEFINITIONS -> { + if (schemaInfo.schemaContent() == null) { + throw new IllegalArgumentException("Schema content cannot be null for EMBEDDED_DEFINITIONS embedding type"); + } + System.out.println("!!! Case Embedded Definitions: Injecting schema content: " + schemaInfo.schemaContent()); + JsonNode schemaNode = mapper.readTree(schemaInfo.schemaContent()); + + // Extract and set definitions + if (schemaNode.has("definitions")) { + mutableRoot.set("definitions", schemaNode.get("definitions")); + } + + // Extract and set type if present + if (schemaNode.has("type")) { + mutableRoot.set("type", schemaNode.get("type")); + } + + // Extract and set properties if present + if (schemaNode.has("properties")) { + mutableRoot.set("properties", schemaNode.get("properties")); + } + } + case NONE -> { + System.out.println("!!! Case None: Removing schema information"); + // Remove any existing schema-related fields + mutableRoot.remove("$schema"); + mutableRoot.remove("definitions"); + } + } + + return mapper.writeValueAsString(mutableRoot); + } + + + public static void main(String[] args) throws IOException { + String input = "{\"name\": \"test\"}"; + SchemaInfo schemaInfo = new SchemaInfo( + new File("C:/tmp/app-config-schema.json").toURI(), + null, + SchemaEmbeddingType.EXTERNAL + ); + + String result = JsonUtils.injectSchema(input, schemaInfo); + JsonNode resultNode = new ObjectMapper().readTree(result); + + System.out.println(result); + } +} diff --git a/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/SchemaResolver.java b/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/SchemaResolver.java new file mode 100644 index 0000000..f125705 --- /dev/null +++ b/vmfedit/src/main/java/eu/mihosoft/vmf/vmfedit/SchemaResolver.java @@ -0,0 +1,130 @@ +package eu.mihosoft.vmf.vmfedit; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +/** + * Interface for custom schema resolution strategies. + */ +public interface SchemaResolver { + /** + * Resolves a schema URI to its content. + * + * @param uri The URI to resolve + * @return The schema content as a string, or null if the schema cannot be resolved + * @throws IOException if there is an error reading the schema + */ + String resolveSchema(URI uri) throws IOException; + + /** + * Checks if this resolver can handle the given URI. + * + * @param uri The URI to check + * @return true if this resolver can handle the URI, false otherwise + */ + boolean canHandle(URI uri); + + + /** + * Default file-based schema resolver. + */ + public static class FileSchemaResolver implements SchemaResolver { + @Override + public String resolveSchema(URI uri) throws IOException { + return Files.readString(Path.of(uri)); + } + + @Override + public boolean canHandle(URI uri) { + return uri.getScheme() == null || uri.getScheme().equals("file"); + } + } + + /** + * Resource-based schema resolver for embedded schemas. + */ + public static class ResourceSchemaResolver implements SchemaResolver { + private final ClassLoader classLoader; + private final String basePath; + + public ResourceSchemaResolver(ClassLoader classLoader, String basePath) { + this.classLoader = classLoader; + this.basePath = basePath.endsWith("/") ? basePath : basePath + "/"; + } + + @Override + public String resolveSchema(URI uri) throws IOException { + String resourcePath = basePath + uri.getPath(); + var resource = classLoader.getResourceAsStream(resourcePath); + if (resource == null) { + throw new IOException("Schema resource not found: " + resourcePath); + } + return new String(resource.readAllBytes()); + } + + @Override + public boolean canHandle(URI uri) { + return uri.getScheme() != null && uri.getScheme().equals("resource"); + } + } + + /** + * In-memory schema resolver for predefined schemas. + */ + public static class InMemorySchemaResolver implements SchemaResolver { + private final Map schemaMap = new HashMap<>(); + + public void registerSchema(URI uri, String content) { + schemaMap.put(uri, content); + } + + @Override + public String resolveSchema(URI uri) throws IOException { + String content = schemaMap.get(uri); + if (content == null) { + throw new IOException("Schema not found: " + uri); + } + return content; + } + + @Override + public boolean canHandle(URI uri) { + return uri.getScheme() != null && uri.getScheme().equals("memory"); + } + } + + /** + * Composite resolver that tries multiple resolvers in sequence. + */ + public static class CompositeSchemaResolver implements SchemaResolver { + private final SchemaResolver[] resolvers; + + public CompositeSchemaResolver(SchemaResolver... resolvers) { + this.resolvers = resolvers; + } + + @Override + public String resolveSchema(URI uri) throws IOException { + for (SchemaResolver resolver : resolvers) { + if (resolver.canHandle(uri)) { + return resolver.resolveSchema(uri); + } + } + throw new IOException("No resolver found for schema: " + uri); + } + + @Override + public boolean canHandle(URI uri) { + for (SchemaResolver resolver : resolvers) { + if (resolver.canHandle(uri)) { + return true; + } + } + return false; + } + } +} \ No newline at end of file diff --git a/vmfedit/src/main/java/module-info.java b/vmfedit/src/main/java/module-info.java index b070c34..ae85fd0 100644 --- a/vmfedit/src/main/java/module-info.java +++ b/vmfedit/src/main/java/module-info.java @@ -2,8 +2,9 @@ requires javafx.controls; requires javafx.fxml; requires javafx.web; - requires jdk.jsobject; + requires jdk.jsobject; + requires com.fasterxml.jackson.databind; - opens eu.mihosoft.vmf.vmfedit to javafx.fxml; + opens eu.mihosoft.vmf.vmfedit to javafx.fxml; exports eu.mihosoft.vmf.vmfedit; } \ No newline at end of file diff --git a/vmfedit/src/main/resources/eu/mihosoft/vmf/vmfedit/json-editor-view.fxml b/vmfedit/src/main/resources/eu/mihosoft/vmf/vmfedit/json-editor-view.fxml index 66b423e..0395be1 100644 --- a/vmfedit/src/main/resources/eu/mihosoft/vmf/vmfedit/json-editor-view.fxml +++ b/vmfedit/src/main/resources/eu/mihosoft/vmf/vmfedit/json-editor-view.fxml @@ -13,9 +13,10 @@ - + + @@ -45,7 +46,7 @@ - diff --git a/vmfedit/src/main/resources/eu/mihosoft/vmf/vmfedit/json-editor.html b/vmfedit/src/main/resources/eu/mihosoft/vmf/vmfedit/json-editor.html index 1d15b06..8c387a6 100644 --- a/vmfedit/src/main/resources/eu/mihosoft/vmf/vmfedit/json-editor.html +++ b/vmfedit/src/main/resources/eu/mihosoft/vmf/vmfedit/json-editor.html @@ -70,8 +70,9 @@ theme: 'bootstrap3', iconlib: 'fontawesome5', disable_edit_json: true, - no_additional_properties: false, + no_additional_properties: true, prompt_before_delete: false, + required_by_default: true, }); // Add change listener @@ -136,6 +137,10 @@ window.setValue = function(json) { console.log("Setting value"); if (editor) { + // if java "" is passed, set empty object + if(json === "") { + json = "{}"; + } editor.setValue(JSON.parse(json)); } else { console.error("Editor not initialized");