
eu.mihosoft.vmf.vmfedit.JsonEditorController Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vmf-edit Show documentation
Show all versions of vmf-edit Show documentation
VMF JSon Schema Editor Support (VRL Modeling Framework) adds editor support via json schema.
The newest version!
package eu.mihosoft.vmf.vmfedit;
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;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import netscape.javascript.JSObject;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* Controller class for a JSON editor component that uses a WebView to render and interact with
* a JSON editor interface. This class provides functionality for editing JSON data with schema
* validation and real-time updates.
*
* The controller manages bidirectional communication between JavaFX and JavaScript, handling
* schema updates, value changes, and error conditions.
*/
public class JsonEditorController {
/** The WebView component used to display the JSON editor */
private final WebView webView;
private volatile boolean updatingValue = false;
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("");
/**
* Constructs a new JsonEditorController with the specified WebView.
*
* @param webView The WebView component to use for the JSON editor
*/
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.
*
* @return The current schema as a string
*/
public String getSchema() {
return schemaProperty.get();
}
/**
* Sets a new JSON schema.
*
* @param schema The new schema to set
*/
public void setSchema(String schema) {
schemaProperty.set(schema);
}
/**
* Gets the schema property for binding.
*
* @return The StringProperty representing the schema
*/
public StringProperty schemaProperty() {
return schemaProperty;
}
/**
* Gets the current JSON value.
*
* @return The current value as a string
*/
public String getValue() {
return valueProperty.get();
}
/**
* Sets a new JSON value.
*
* @param value The new value to set
*/
public void setValue(String value) {
valueProperty.set(value);
}
/**
* Commits the last edited value. This ensures that the value is up-to-date with the editor. Do this if you save
* the value without a user interaction, e.g., without losing focus.
*/
public void commitValue() {
String value = (String) webView.getEngine().executeScript("document.activeElement.blur(); JSON.stringify(getValue())");
if(Platform.isFxApplicationThread()) {
valueProperty.set(value);
} else {
CompletableFuture future = new CompletableFuture<>();
Platform.runLater(() -> {
valueProperty.set(value);
future.complete(null);
});
future.join();
}
}
/**
* Gets the value property for binding.
*
* @return The StringProperty representing the value
*/
public StringProperty valueProperty() {
return valueProperty;
}
/**
* Callback implementation for handling editor changes from JavaScript.
*/
public static class MyEditorCallback implements Consumer {
private final JsonEditorController control;
/**
* Constructs a new callback for the specified controller.
*
* @param control The JsonEditorController instance
*/
public MyEditorCallback(JsonEditorController control) {
this.control = control;
}
@Override
public void accept(String newValue) {
new Thread(() -> {
Platform.runLater(() -> {
control.updatingValue = true;
try {
control.valueProperty().set(newValue);
} finally {
control.updatingValue = false;
}
});
}).start();
}
}
/**
* Enumeration of log levels for the editor's logging system.
*/
public enum LogLevel {
DBG, INFO, WARN, ERROR
}
/**
* Interface for logging events from the JSON editor.
*/
@FunctionalInterface
public interface LogListener {
/**
* Called when a log event occurs.
*
* @param level The severity level of the log
* @param message The log message
* @param ex Any associated exception (may be null)
*/
void log(LogLevel level, String message, Exception ex);
}
/** Default log listener implementation */
private LogListener logListener = (level, message, ex) -> {
if(level == LogLevel.ERROR) {
System.err.println("[" + level + "] " + message);
if(ex!=null) ex.printStackTrace(System.err);
} else {
System.out.println("[" + level + "] " + message);
if(ex!=null) ex.printStackTrace(System.out);
}
};
/**
* Sets a custom log listener for the editor.
*
* @param logListener The new log listener to use
*/
public void setLogListener(LogListener logListener) {
if (logListener == null) {
logListener = this.logListener;
}
this.logListener = logListener;
}
public void configureEditor(Map config) {
setSchema(schemaProperty().get());
}
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) {
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");
if (url != null) {
logListener.log(LogLevel.INFO, "Loading URL: " + url, null);
engine.load(url.toExternalForm());
} else {
logListener.log(LogLevel.ERROR, "Could not find json-editor.html", null);
}
}
private MyEditorCallback myEditorCallback = new MyEditorCallback(this);
/**
* Initializes the JavaScript bridge for communication between Java and JavaScript.
*/
private void initializeJavaScriptBridge(WebEngine engine) {
// Get our namespace object
JSObject editorCallbacks = (JSObject) engine.executeScript("EditorCallbacks");
// Set the callback using our namespace's method
editorCallbacks.call("setHostCallback", myEditorCallback);
engine.executeScript("updateSchema('" + escapeJavaScript(schemaProperty.get()) + "')");
}
private ChangeListener schemaChangeListener = null;
/**
* Sets up the schema change listener to handle schema updates.
*/
private void setupSchemaChangeListener() {
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 = "";
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)) {
attemptValueMigration(value);
}
}
/**
* Attempts to migrate an existing value to a new schema.
*/
private void attemptValueMigration(String value) {
if(updatingValue) {
return;
}
Thread.ofVirtual().start(() -> {
for (int i = 0; i < 10; i++) {
if (trySetValue(value)) break;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
/**
* 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(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);
} finally {
var scene = webView.getScene();
scene.getRoot().setDisable(false);
}
});
return future.join();
}
private ChangeListener focusChangedListener = null;
/**
* Sets up focus handling for the WebView component.
*/
private void setupWebViewFocusHandling() {
if(focusChangedListener!=null) {
webView.focusedProperty().removeListener(focusChangedListener);
}
focusChangedListener = (observable, oldValue, newValue) -> {
if (!newValue) {
webView.getEngine().executeScript("document.activeElement.blur();");
}
};
webView.focusedProperty().addListener(focusChangedListener);
}
/**
* Sets up JavaScript event handlers for the web engine.
*/
private void setupJavaScriptEventHandlers(WebEngine engine) {
engine.setOnAlert(event -> logListener.log(LogLevel.INFO, "JS Alert: " + event.getData(), null));
engine.setOnError(event -> logListener.log(LogLevel.ERROR, "JS Error: " + event.getMessage(), null));
engine.setOnStatusChanged(event -> logListener.log(LogLevel.INFO, "JS Status: " + event.getData(), null));
engine.setOnResized(event -> logListener.log(LogLevel.INFO, "JS Resized: " + event.getData(), null));
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() {
if(valueChangeListener!=null) {
valueProperty().removeListener(valueChangeListener);
}
valueChangeListener = (observable, oldValue, newValue) -> {
if (newValue != null && !newValue.isEmpty() && !"\"\"".equals(newValue)) {
attemptValueMigration(newValue);
}
// attemptValueMigration(newValue);
};
valueProperty().addListener(valueChangeListener);
}
/**
* Escapes special characters in JavaScript strings.
*
* @param str The string to escape
* @return The escaped string
*/
private String escapeJavaScript(String str) {
return str.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
/**
* Default error handler for the editor.
*/
private BiConsumer onErrorConsumer = (title, message) -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText(title);
alert.setContentText(message);
alert.showAndWait();
};
/**
* Sets a custom error handler for the editor.
*
* @param onErrorConsumer The new error handler to use
*/
public void setOnError(BiConsumer onErrorConsumer) {
if (onErrorConsumer != null) {
this.onErrorConsumer = onErrorConsumer;
}
}
/**
* Shows an error dialog to the user.
*
* @param title The error dialog title
* @param message The error message to display
*/
private void showError(String title, String message) {
onErrorConsumer.accept(title, message);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy