diff --git a/app/queue-server/pom.xml b/app/queue-server/pom.xml
index 5e97e07249..1310dac5f3 100644
--- a/app/queue-server/pom.xml
+++ b/app/queue-server/pom.xml
@@ -34,6 +34,12 @@
jackson-dataformat-yaml
2.19.1
+
+
+ org.python
+ jython-standalone
+ ${jython.version}
+
org.apache.poi
poi
diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java
index 587a979aa1..79bb119779 100644
--- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java
+++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java
@@ -7,6 +7,7 @@
import org.phoebus.applications.queueserver.view.PlanEditEvent;
import org.phoebus.applications.queueserver.view.TabSwitchEvent;
import org.phoebus.applications.queueserver.view.ItemUpdateEvent;
+import org.phoebus.applications.queueserver.util.PythonParameterConverter;
import com.fasterxml.jackson.databind.ObjectMapper;
import javafx.application.Platform;
import javafx.geometry.Insets;
@@ -63,6 +64,8 @@ public class RePlanEditorController implements Initializable {
private final ObjectMapper objectMapper = new ObjectMapper();
// Store original parameter values for reset functionality
private final Map originalParameterValues = new HashMap<>();
+ // Python-based parameter converter
+ private final PythonParameterConverter pythonConverter = new PythonParameterConverter();
private class EditableTableCell extends TableCell {
private TextField textField;
@@ -88,9 +91,11 @@ protected void updateItem(String item, boolean empty) {
setText(getString());
setGraphic(null);
- // Style based on enabled state and add tooltip
+ // Style based on enabled state and validation
if (!row.isEnabled()) {
setStyle("-fx-text-fill: grey;");
+ } else if (!row.validate(pythonConverter)) {
+ setStyle("-fx-text-fill: red;");
} else {
setStyle("");
}
@@ -132,15 +137,19 @@ public void commitEdit(String newValue) {
ParameterRow row = getTableRow().getItem();
if (row != null) {
row.setValue(newValue);
+
+ // Update cell color based on Python validation
+ updateValidationColor(row);
+
switchToEditingMode();
updateButtonStates();
- // Update cell color based on validation
- updateValidationColor(row);
}
}
private void updateValidationColor(ParameterRow row) {
- if (row.validate()) {
+ if (!row.isEnabled()) {
+ setStyle("-fx-text-fill: grey;");
+ } else if (row.validate(pythonConverter)) {
setStyle("");
} else {
setStyle("-fx-text-fill: red;");
@@ -208,69 +217,7 @@ public ParameterRow(String name, boolean enabled, String value, String descripti
public boolean isOptional() { return isOptional.get(); }
public Object getDefaultValue() { return defaultValue; }
- public Object getParsedValue() {
- if (!enabled.get()) return defaultValue;
-
- String valueStr = value.get();
- if (valueStr == null || valueStr.trim().isEmpty()) {
- return defaultValue;
- }
-
- try {
- return parseLiteralValue(valueStr);
- } catch (Exception e) {
- return valueStr;
- }
- }
-
- private Object parseLiteralValue(String valueStr) throws Exception {
- valueStr = valueStr.trim();
-
- // Handle None/null
- if ("None".equals(valueStr) || "null".equals(valueStr)) {
- return null;
- }
-
- // Handle booleans
- if ("True".equals(valueStr) || "true".equals(valueStr)) {
- return true;
- }
- if ("False".equals(valueStr) || "false".equals(valueStr)) {
- return false;
- }
-
- // Handle strings (quoted)
- if ((valueStr.startsWith("'") && valueStr.endsWith("'")) ||
- (valueStr.startsWith("\"") && valueStr.endsWith("\""))) {
- return valueStr.substring(1, valueStr.length() - 1);
- }
-
- // Handle numbers
- try {
- if (valueStr.contains(".")) {
- return Double.parseDouble(valueStr);
- } else {
- return Long.parseLong(valueStr);
- }
- } catch (NumberFormatException e) {
- // Continue to other parsing attempts
- }
-
- // Handle lists and dicts using JSON parsing (similar to Python's literal_eval)
- if (valueStr.startsWith("[") || valueStr.startsWith("{")) {
- try {
- ObjectMapper mapper = new ObjectMapper();
- return mapper.readValue(valueStr, Object.class);
- } catch (Exception e) {
- // Fall back to string if JSON parsing fails
- }
- }
-
- // Default to string
- return valueStr;
- }
-
- public boolean validate() {
+ public boolean validate(PythonParameterConverter converter) {
if (!enabled.get()) {
return true; // Disabled parameters are always valid
}
@@ -280,8 +227,18 @@ public boolean validate() {
return isOptional.get();
}
+ // Validate using Python converter
try {
- getParsedValue();
+ List testParams = List.of(
+ new PythonParameterConverter.ParameterInfo(
+ getName(),
+ valueStr,
+ true,
+ isOptional.get(),
+ getDefaultValue()
+ )
+ );
+ converter.convertParameters(testParams);
return true;
} catch (Exception e) {
return false;
@@ -545,9 +502,6 @@ private void loadParametersForSelection(String selectedName) {
isEnabled = true;
} else if (defaultValue != null) {
currentValue = String.valueOf(defaultValue);
- if (!isEditMode) {
- currentValue += " (default)";
- }
}
ParameterRow row = new ParameterRow(paramName, isEnabled, currentValue, description, isOptional, defaultValue);
@@ -642,7 +596,7 @@ private void autoResizeColumns() {
private boolean areParametersValid() {
boolean allValid = true;
for (ParameterRow row : parameterRows) {
- boolean rowValid = row.validate();
+ boolean rowValid = row.validate(pythonConverter);
if (!rowValid) {
allValid = false;
}
@@ -681,8 +635,32 @@ private void updateButtonStates() {
cancelBtn.setDisable(!isEditMode);
}
+ /**
+ * Build kwargs map using Python-based type conversion.
+ * All type conversion is handled by Python script using ast.literal_eval.
+ */
+ private Map buildKwargsWithPython() {
+ List paramInfos = new ArrayList<>();
+
+ for (ParameterRow row : parameterRows) {
+ PythonParameterConverter.ParameterInfo paramInfo =
+ new PythonParameterConverter.ParameterInfo(
+ row.getName(),
+ row.getValue(),
+ row.isEnabled(),
+ row.isOptional(),
+ row.getDefaultValue()
+ );
+ paramInfos.add(paramInfo);
+ }
+
+ // Use Python to convert parameters - no Java fallback
+ return pythonConverter.convertParameters(paramInfos);
+ }
+
private void addItemToQueue() {
if (!areParametersValid()) {
+ showValidationError("Some parameters have invalid values. Please check the red fields.");
return;
}
@@ -693,13 +671,9 @@ private void addItemToQueue() {
}
String itemType = planRadBtn.isSelected() ? "plan" : "instruction";
- Map kwargs = new HashMap<>();
- for (ParameterRow row : parameterRows) {
- if (row.isEnabled()) {
- kwargs.put(row.getName(), row.getParsedValue());
- }
- }
+ // Use Python-based parameter conversion
+ Map kwargs = buildKwargsWithPython();
QueueItem item = new QueueItem(
itemType,
@@ -732,24 +706,45 @@ private void addItemToQueue() {
TabSwitchEvent.getInstance().switchToTab("Plan Viewer");
exitEditMode();
showItemPreview();
+ } else {
+ showValidationError("Failed to add item to queue: " + response.msg());
}
});
} catch (Exception e) {
- e.printStackTrace();
+ LOG.log(Level.WARNING, "Failed to add item to queue", e);
+ Platform.runLater(() -> {
+ String errorMsg = e.getMessage();
+ if (errorMsg == null || errorMsg.isEmpty()) {
+ errorMsg = e.getClass().getSimpleName();
+ }
+ showValidationError("Failed to add item to queue: " + errorMsg);
+ });
}
}).start();
} catch (Exception e) {
- e.printStackTrace();
+ LOG.log(Level.SEVERE, "Failed to add item to queue", e);
+ Platform.runLater(() -> {
+ showValidationError("Failed to add item: " + e.getMessage());
+ });
}
}
+ private void showValidationError(String message) {
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setTitle("Validation Error");
+ alert.setHeaderText(null);
+ alert.setContentText(message);
+ alert.showAndWait();
+ }
+
private void saveItem() {
if (!isEditMode || currentItem == null) {
return;
}
if (!areParametersValid()) {
+ showValidationError("Some parameters have invalid values. Please check the red fields.");
return;
}
@@ -760,13 +755,9 @@ private void saveItem() {
}
String itemType = planRadBtn.isSelected() ? "plan" : "instruction";
- Map kwargs = new HashMap<>();
- for (ParameterRow row : parameterRows) {
- if (row.isEnabled()) {
- kwargs.put(row.getName(), row.getParsedValue());
- }
- }
+ // Use Python-based parameter conversion
+ Map kwargs = buildKwargsWithPython();
QueueItem updatedItem = new QueueItem(
itemType,
@@ -795,16 +786,26 @@ private void saveItem() {
exitEditMode();
showItemPreview();
} else {
- LOG.log(Level.WARNING, "Save failed: " + response.msg());
+ showValidationError("Failed to save item: " + response.msg());
}
});
} catch (Exception e) {
- LOG.log(Level.WARNING, "Save error", e);
+ LOG.log(Level.WARNING, "Failed to save item", e);
+ Platform.runLater(() -> {
+ String errorMsg = e.getMessage();
+ if (errorMsg == null || errorMsg.isEmpty()) {
+ errorMsg = e.getClass().getSimpleName();
+ }
+ showValidationError("Failed to save item: " + errorMsg);
+ });
}
}).start();
} catch (Exception e) {
- e.printStackTrace();
+ LOG.log(Level.SEVERE, "Failed to save item", e);
+ Platform.runLater(() -> {
+ showValidationError("Failed to save item: " + e.getMessage());
+ });
}
}
diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/JythonScriptExecutor.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/JythonScriptExecutor.java
new file mode 100644
index 0000000000..d6773efa35
--- /dev/null
+++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/JythonScriptExecutor.java
@@ -0,0 +1,154 @@
+package org.phoebus.applications.queueserver.util;
+
+import org.python.core.*;
+import org.python.util.PythonInterpreter;
+
+import java.io.InputStream;
+import java.util.concurrent.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Utility class for executing Jython scripts in the Queue Server application.
+ * Based on Phoebus display runtime JythonScriptSupport implementation.
+ */
+public class JythonScriptExecutor {
+
+ private static final Logger LOG = Logger.getLogger(JythonScriptExecutor.class.getName());
+ private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(r -> {
+ Thread thread = new Thread(r, "QueueServer-Jython");
+ thread.setDaemon(true);
+ return thread;
+ });
+
+ private final PythonInterpreter python;
+
+ static {
+ // Configure Jython options (similar to Phoebus display runtime)
+ PySystemState.initialize();
+ Options.dont_write_bytecode = true;
+
+ // Set console encoding
+ PySystemState sys = Py.getSystemState();
+ sys.setdefaultencoding("utf-8");
+
+ LOG.log(Level.INFO, "Jython initialized for Queue Server");
+ }
+
+ /**
+ * Create a new Jython script executor with a dedicated interpreter instance.
+ */
+ public JythonScriptExecutor() {
+ // Synchronized to prevent concurrent initialization issues
+ synchronized (JythonScriptExecutor.class) {
+ this.python = new PythonInterpreter();
+ }
+ }
+
+ /**
+ * Execute a Python script with the given context variables.
+ *
+ * @param scriptContent The Python script code to execute
+ * @param contextVars Variables to pass into the Python context (key -> value pairs)
+ * @return The result object returned by the script (or null if none)
+ */
+ public Object execute(String scriptContent, java.util.Map contextVars) {
+ try {
+ // Set context variables in the Python interpreter
+ if (contextVars != null) {
+ for (java.util.Map.Entry entry : contextVars.entrySet()) {
+ python.set(entry.getKey(), entry.getValue());
+ }
+ }
+
+ // Execute the script
+ python.exec(scriptContent);
+
+ // Try to get a result variable if one was set
+ PyObject result = python.get("result");
+
+ // Clear context to prevent memory leaks
+ if (contextVars != null) {
+ for (String key : contextVars.keySet()) {
+ python.set(key, null);
+ }
+ }
+
+ // Convert PyObject to Java object
+ return result != null ? result.__tojava__(Object.class) : null;
+
+ } catch (Exception e) {
+ LOG.log(Level.FINE, "Jython script execution failed: " + e.getMessage());
+ throw new RuntimeException("Script execution failed: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Execute a Python script asynchronously.
+ *
+ * @param scriptContent The Python script code to execute
+ * @param contextVars Variables to pass into the Python context
+ * @return Future that will contain the result
+ */
+ public Future