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/QueueServerApp.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerApp.java index 5e1ca2034c..12aded73a9 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerApp.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/QueueServerApp.java @@ -16,7 +16,7 @@ @SuppressWarnings("nls") public final class QueueServerApp implements AppResourceDescriptor { - public static final Logger LOGGER = Logger.getLogger(QueueServerApp.class.getPackageName()); + public static final Logger logger = Logger.getLogger(QueueServerApp.class.getPackageName()); public static final String NAME = "queue-server"; private static final String DISPLAY_NAME = "Queue Server"; diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineHttpClient.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineHttpClient.java index b58da4ed1a..185f4e218b 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineHttpClient.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineHttpClient.java @@ -48,7 +48,7 @@ public static RunEngineHttpClient get() { private final String base; private final String apiKey; private final RateLimiter limiter; - private static final Logger LOG = HttpSupport.LOG; + private static final Logger logger = HttpSupport.logger; private RunEngineHttpClient(String baseUrl, String apiKey, double permitsPerSecond) { this.http = HttpClient.newBuilder() @@ -118,14 +118,14 @@ private T executeWithRetry(HttpRequest req, ApiEndpoint ep, limiter.acquire(); long t0 = System.nanoTime(); try { - LOG.log(Level.FINEST, ep + " attempt " + attempt); + logger.log(Level.FINEST, ep + " attempt " + attempt); HttpResponse rsp = http.send(req, HttpResponse.BodyHandlers.ofString()); - LOG.log(Level.FINEST, ep + " " + rsp.statusCode() + " " + HttpSupport.elapsed(t0) + " ms"); + logger.log(Level.FINEST, ep + " " + rsp.statusCode() + " " + HttpSupport.elapsed(t0) + " ms"); check(rsp, ep); return reader.apply(rsp); } catch (java.io.IOException ex) { if (!HttpSupport.isRetryable(req) || attempt >= HttpSupport.MAX_RETRIES) throw ex; - LOG.log(Level.WARNING, ep + " transport error (" + ex.getClass().getSimpleName() + + logger.log(Level.WARNING, ep + " transport error (" + ex.getClass().getSimpleName() + "), retry in " + back + " ms (attempt " + attempt + ")"); Thread.sleep(back); back = Math.round(back * HttpSupport.BACKOFF_MULTIPLIER); diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java index 5e42902b49..37b0572b83 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java @@ -29,13 +29,13 @@ public final class RunEngineService { private final RunEngineHttpClient http = RunEngineHttpClient.get(); - private static final Logger LOG = Logger.getLogger(RunEngineService.class.getName()); + private static final Logger logger = Logger.getLogger(RunEngineService.class.getPackageName()); /* ---- Ping & status --------------------------------------------------- */ public Envelope ping() throws Exception { return http.call(ApiEndpoint.PING, NoBody.INSTANCE); } public StatusResponse status() throws Exception { - LOG.log(Level.FINEST, "Fetching status"); + logger.log(Level.FINEST, "Fetching status"); return http.send(ApiEndpoint.STATUS, NoBody.INSTANCE, StatusResponse.class); } public Envelope configGet() throws Exception { return http.call(ApiEndpoint.CONFIG_GET, NoBody.INSTANCE); } @@ -168,7 +168,7 @@ public Envelope queueItemUpdate(QueueItem item) throws Exception { /* ───────── Console monitor ───────── */ public InputStream streamConsoleOutput() throws Exception { - LOG.info("Opening console output stream"); + logger.log(Level.FINE, "Opening console output stream"); HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(http.getBaseUrl() + ApiEndpoint.STREAM_CONSOLE_OUTPUT.endpoint().path())) .header("Authorization", "ApiKey " + http.getApiKey()) @@ -178,10 +178,10 @@ public InputStream streamConsoleOutput() throws Exception { HttpResponse rsp = http.httpClient().send(req, HttpResponse.BodyHandlers.ofInputStream()); if (rsp.statusCode() < 200 || rsp.statusCode() >= 300) { - LOG.log(Level.WARNING, "Console stream failed with HTTP " + rsp.statusCode()); + logger.log(Level.WARNING, "Console stream failed with HTTP " + rsp.statusCode()); throw new IOException("console stream - HTTP " + rsp.statusCode()); } - LOG.info("Console output stream opened successfully"); + logger.log(Level.FINE, "Console output stream opened successfully"); return rsp.body(); } @@ -251,7 +251,7 @@ public Envelope rePause(String option) throws Exception { public Envelope plansAllowed() throws Exception { return http.call(ApiEndpoint.PLANS_ALLOWED, NoBody.INSTANCE); } public Map plansAllowedRaw() throws Exception { - LOG.log(Level.FINE, "Fetching plans allowed (raw)"); + logger.log(Level.FINE, "Fetching plans allowed (raw)"); return http.send(ApiEndpoint.PLANS_ALLOWED, NoBody.INSTANCE); } public Envelope devicesAllowed() throws Exception { return http.call(ApiEndpoint.DEVICES_ALLOWED, NoBody.INSTANCE); } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ApplicationController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ApplicationController.java index 91e337fbef..3c12a82e0b 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ApplicationController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ApplicationController.java @@ -3,24 +3,55 @@ import org.phoebus.applications.queueserver.view.ViewFactory; import javafx.fxml.FXML; import javafx.fxml.Initializable; +import javafx.scene.Node; +import javafx.scene.Parent; import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.control.TableView; import java.net.URL; import java.util.ResourceBundle; +import java.util.logging.Level; import java.util.logging.Logger; public final class ApplicationController implements Initializable { @FXML private Tab monitorQueueTab; @FXML private Tab editAndControlQueueTab; - - private static final Logger LOG = Logger.getLogger(ApplicationController.class.getName()); + @FXML private TabPane tabPane; + + private static final Logger logger = Logger.getLogger(ApplicationController.class.getPackageName()); @Override public void initialize(URL url, ResourceBundle rb) { - LOG.info("Initializing ApplicationController"); + logger.log(Level.FINE, "Initializing ApplicationController"); monitorQueueTab.setContent(ViewFactory.MONITOR_QUEUE.get()); editAndControlQueueTab.setContent(ViewFactory.EDIT_AND_CONTROL_QUEUE.get()); - LOG.info("ApplicationController initialization complete"); + + // Disable focus traversal on all components + disableFocusTraversal(monitorQueueTab.getContent()); + disableFocusTraversal(editAndControlQueueTab.getContent()); + + logger.log(Level.FINE, "ApplicationController initialization complete"); + } + + /** + * Recursively disables focus traversal on all nodes in the scene graph, + * except for TableView which remains focus traversable for arrow key navigation. + */ + private void disableFocusTraversal(Node node) { + if (node == null) return; + + // Allow TableView to remain focus traversable for arrow key navigation + if (!(node instanceof TableView)) { + node.setFocusTraversable(false); + } + + if (node instanceof Parent) { + Parent parent = (Parent) node; + for (Node child : parent.getChildrenUnmodifiable()) { + disableFocusTraversal(child); + } + } } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/EditAndControlQueueController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/EditAndControlQueueController.java index e0d5b09707..8fb4e4342a 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/EditAndControlQueueController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/EditAndControlQueueController.java @@ -18,11 +18,11 @@ public class EditAndControlQueueController implements Initializable { @FXML private AnchorPane planQueueContainer; @FXML private AnchorPane planHistoryContainer; - private static final Logger LOG = Logger.getLogger(EditAndControlQueueController.class.getName()); + private static final Logger logger = Logger.getLogger(EditAndControlQueueController.class.getPackageName()); @Override public void initialize(URL url, ResourceBundle resourceBundle) { - LOG.info("Initializing EditAndControlQueueController"); + logger.log(Level.FINE, "Initializing EditAndControlQueueController"); loadInto(runningPlanContainer, "/org/phoebus/applications/queueserver/view/ReRunningPlan.fxml", new ReRunningPlanController(false)); loadInto(planQueueContainer, "/org/phoebus/applications/queueserver/view/RePlanQueue.fxml", new RePlanQueueController(false)); loadInto(planHistoryContainer, "/org/phoebus/applications/queueserver/view/RePlanHistory.fxml", new RePlanHistoryController(false)); @@ -40,7 +40,7 @@ private void loadInto(AnchorPane container, String fxml, Object controller) { AnchorPane.setLeftAnchor(view, 0.0); AnchorPane.setRightAnchor(view, 0.0); } catch (IOException e) { - LOG.log(Level.SEVERE, "Failed to load FXML: " + fxml, e); + logger.log(Level.SEVERE, "Failed to load FXML: " + fxml, e); } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/MonitorQueueController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/MonitorQueueController.java index de3d6ccef2..c6f04ca33e 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/MonitorQueueController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/MonitorQueueController.java @@ -25,7 +25,7 @@ public class MonitorQueueController implements Initializable { @FXML private VBox stack; private final Map savedHeights = new HashMap<>(); - private static final Logger LOG = Logger.getLogger(MonitorQueueController.class.getName()); + private static final Logger logger = Logger.getLogger(MonitorQueueController.class.getPackageName()); private static final String BAR_NORMAL = "-fx-background-color: linear-gradient(to bottom, derive(-fx-base,15%) 0%, derive(-fx-base,-5%) 100%);" + @@ -34,7 +34,7 @@ public class MonitorQueueController implements Initializable { @Override public void initialize(URL url, ResourceBundle resourceBundle) { - LOG.info("Initializing MonitorQueueController"); + logger.log(Level.FINE, "Initializing MonitorQueueController"); loadInto(runningPlanContainer, "/org/phoebus/applications/queueserver/view/ReRunningPlan.fxml", new ReRunningPlanController(true)); loadInto(planQueueContainer, "/org/phoebus/applications/queueserver/view/RePlanQueue.fxml", new RePlanQueueController(true)); loadInto(planHistoryContainer, "/org/phoebus/applications/queueserver/view/RePlanHistory.fxml", new RePlanHistoryController(true)); @@ -76,7 +76,7 @@ private void loadInto(AnchorPane container, String fxml, Object controller) { AnchorPane.setLeftAnchor(view, 0.0); AnchorPane.setRightAnchor(view, 0.0); } catch (IOException e) { - LOG.log(Level.SEVERE, "Failed to load FXML: " + fxml, e); + logger.log(Level.SEVERE, "Failed to load FXML: " + fxml, e); } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReConsoleMonitorController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReConsoleMonitorController.java index 157bd45617..b826304ab1 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReConsoleMonitorController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReConsoleMonitorController.java @@ -200,13 +200,9 @@ private static final class ConsoleTextBuffer { private int line = 0; private int pos = 0; - private boolean progressActive = false; // overwriting a tqdm bar - private String lastVisualLine = ""; // for duplicate-collapse - private static final String NL = "\n"; private static final String CR = "\r"; - private static final char ESC = 0x1B; - private static final String LOG_PREFIXES = "IWEDE"; // info/warn/error/debug/extra + private static final String UP_ONE_LINE = "\u001b[A"; // ESC[A ConsoleTextBuffer(int hardLimit) { this.hardLimit = Math.max(hardLimit, 0); @@ -215,93 +211,82 @@ private static final class ConsoleTextBuffer { void clear() { buf.clear(); line = pos = 0; - progressActive = false; - lastVisualLine = ""; } void addMessage(String msg) { while (!msg.isEmpty()) { + // Find next control sequence + int nlIdx = msg.indexOf(NL); + int crIdx = msg.indexOf(CR); + int upIdx = msg.indexOf(UP_ONE_LINE); - /* next control-char position */ - int next = minPos(msg.indexOf(NL), msg.indexOf(CR), msg.indexOf(ESC)); + int next = minPos(nlIdx, crIdx, upIdx); if (next < 0) next = msg.length(); - /* ---------------------------------------------------- * - * FRAGMENT: plain text until control char * - * ---------------------------------------------------- */ + // ---------------------------------------------------- + // FRAGMENT: plain text until control char + // ---------------------------------------------------- if (next != 0) { String frag = msg.substring(0, next); msg = msg.substring(next); - /* are we switching from progress bar → normal log? */ - if (progressActive && - (frag.startsWith("[") || frag.startsWith("TEST") || - frag.startsWith("Returning") || frag.startsWith("history"))) - { - newline(); // finish bar line - progressActive = false; + // Ensure we have at least one line + if (buf.isEmpty()) { + buf.add(""); } ensureLineExists(line); - /* pad if cursor past EOL */ - if (pos > buf.get(line).length()) { - buf.set(line, buf.get(line) + " ".repeat(pos - buf.get(line).length())); + // Extend current line with spaces if cursor is past EOL + int lineLen = buf.get(line).length(); + if (lineLen < pos) { + buf.set(line, buf.get(line) + " ".repeat(pos - lineLen)); } - /* overwrite / extend */ - StringBuilder sb = new StringBuilder(buf.get(line)); - if (pos + frag.length() > sb.length()) { - sb.setLength(pos); - sb.append(frag); - } else { - sb.replace(pos, pos + frag.length(), frag); - } - buf.set(line, sb.toString()); - pos += frag.length(); - - /* flag if this fragment looks like a tqdm bar * - * (contains “%|” and the typical frame pattern) */ - if (frag.contains("%|")) progressActive = true; + // Insert/overwrite fragment at current position + String currentLine = buf.get(line); + String before = currentLine.substring(0, Math.min(pos, currentLine.length())); + String after = currentLine.substring(Math.min(pos + frag.length(), currentLine.length())); + buf.set(line, before + frag + after); + pos += frag.length(); continue; } - /* ---------------------------------------------------- * - * CONTROL CHAR * - * ---------------------------------------------------- */ - char c = msg.charAt(0); + // ---------------------------------------------------- + // CONTROL SEQUENCES + // ---------------------------------------------------- - if (c == '\n') { // LF: finish logical line - newline(); - progressActive = false; // tqdm ends with a real LF - msg = msg.substring(1); + if (nlIdx == 0) { + // Newline: move to next line + line++; + if (line >= buf.size()) { + buf.add(""); + } + pos = 0; + msg = msg.substring(NL.length()); } - else if (c == '\r') { // CR: return to start of SAME line + else if (crIdx == 0) { + // Carriage return: move to beginning of current line pos = 0; - msg = msg.substring(1); + msg = msg.substring(CR.length()); } - else if (c == ESC) { // ESC [ n A (cursor-up) - int idx = 1; - if (idx < msg.length() && msg.charAt(idx) == '[') { - idx++; - int startDigits = idx; - while (idx < msg.length() && Character.isDigit(msg.charAt(idx))) idx++; - if (idx < msg.length() && msg.charAt(idx) == 'A') { - int nUp = (idx == startDigits) ? 1 - : Math.max(1, Integer.parseInt(msg.substring(startDigits, idx))); - line = Math.max(0, line - nUp); - pos = 0; - ensureLineExists(line); - msg = msg.substring(idx + 1); - continue; - } + else if (upIdx == 0) { + // Move up one line + if (line > 0) { + line--; + } + pos = 0; + msg = msg.substring(UP_ONE_LINE.length()); + } + else { + // Shouldn't happen, but handle gracefully + if (!msg.isEmpty()) { + ensureLineExists(line); + buf.set(line, buf.get(line) + msg.charAt(0)); + pos++; + msg = msg.substring(1); } - /* unknown sequence → treat ESC literally */ - ensureLineExists(line); - buf.set(line, buf.get(line) + c); - pos++; - msg = msg.substring(1); } } trim(); @@ -309,8 +294,12 @@ else if (c == ESC) { // ESC [ n A (cursor-up) String tail(int n) { if (n <= 0) return ""; - boolean sentinel = !buf.isEmpty() && buf.get(buf.size() - 1).isEmpty(); - int visible = buf.size() - (sentinel ? 1 : 0); + + // Remove trailing empty line if present + int visible = buf.size(); + if (visible > 0 && buf.get(visible - 1).isEmpty()) { + visible--; + } int start = Math.max(0, visible - n); StringBuilder out = new StringBuilder(); @@ -322,33 +311,25 @@ String tail(int n) { } private void ensureLineExists(int idx) { - while (buf.size() <= idx) buf.add(""); - } - - private void newline() { - /* avoid storing duplicate visual lines (stream + poll) */ - String justFinished = buf.get(line); - if (!justFinished.equals(lastVisualLine)) { - lastVisualLine = justFinished; - line++; - pos = 0; - ensureLineExists(line); - } else { - /* discard duplicate; stay on existing last line */ - pos = buf.get(line).length(); + while (buf.size() <= idx) { + buf.add(""); } } private void trim() { - boolean sentinel = !buf.isEmpty() && buf.get(buf.size() - 1).isEmpty(); - int quota = hardLimit + (sentinel ? 1 : 0); - while (buf.size() > quota) buf.remove(0); - if (line >= buf.size()) line = buf.size() - 1; + // Keep some buffer beyond hardLimit to avoid constant trimming + int maxAllowed = hardLimit + 100; + while (buf.size() > maxAllowed) { + buf.remove(0); + line = Math.max(0, line - 1); + } } private static int minPos(int... p) { int m = Integer.MAX_VALUE; - for (int v : p) if (v >= 0 && v < m) m = v; + for (int v : p) { + if (v >= 0 && v < m) m = v; + } return m == Integer.MAX_VALUE ? -1 : m; } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReEnvironmentControlsController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReEnvironmentControlsController.java index be72dfceb5..3a3bd3e10a 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReEnvironmentControlsController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReEnvironmentControlsController.java @@ -14,6 +14,7 @@ import java.net.URL; import java.util.Map; import java.util.ResourceBundle; +import java.util.logging.Level; import java.util.logging.Logger; public final class ReEnvironmentControlsController implements Initializable { @@ -23,7 +24,7 @@ public final class ReEnvironmentControlsController implements Initializable { @FXML private Button destroyBtn; private final RunEngineService svc = new RunEngineService(); - private final Logger LOG = Logger.getLogger(getClass().getName()); + private static final Logger logger = Logger.getLogger(ReEnvironmentControlsController.class.getPackageName()); @Override public void initialize(URL url, ResourceBundle rb) { StatusBus.latest().addListener(this::onStatus); @@ -57,12 +58,12 @@ private void refreshButtons(Object statusObj) { @FXML private void open() { try { svc.environmentOpen(); } - catch (Exception ex) { LOG.warning("environmentOpen: " + ex); } + catch (Exception ex) { logger.log(Level.WARNING, "environmentOpen: " + ex); } } @FXML private void close() { try { svc.environmentClose(); } - catch (Exception ex) { LOG.warning("environmentClose: " + ex); } + catch (Exception ex) { logger.log(Level.WARNING, "environmentClose: " + ex); } } @FXML private void destroy() { @@ -75,9 +76,9 @@ private void refreshButtons(Object statusObj) { if (response == ButtonType.OK) { try { svc.environmentDestroy(); - LOG.info("Environment destroyed successfully"); + logger.log(Level.FINE, "Environment destroyed successfully"); } catch (Exception ex) { - LOG.warning("environmentDestroy: " + ex); + logger.log(Level.WARNING, "environmentDestroy: " + ex); } } }); diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReExecutionControlsController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReExecutionControlsController.java index b8ab0f162a..a78bd636cd 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReExecutionControlsController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReExecutionControlsController.java @@ -12,6 +12,7 @@ import java.net.URL; import java.util.Map; import java.util.ResourceBundle; +import java.util.logging.Level; import java.util.logging.Logger; public final class ReExecutionControlsController implements Initializable { @@ -20,7 +21,7 @@ public final class ReExecutionControlsController implements Initializable { stopBtn, abortBtn, haltBtn; private final RunEngineService svc = new RunEngineService(); - private final Logger LOG = Logger.getLogger(getClass().getName()); + private static final Logger logger = Logger.getLogger(ReExecutionControlsController.class.getPackageName()); @Override public void initialize(URL url, ResourceBundle rb) { @@ -109,6 +110,6 @@ else if (statusObj instanceof Map m) { private void call(String label, Runnable r) { try { r.run(); } - catch (Exception ex) { LOG.warning(label + ": " + ex); } + catch (Exception ex) { logger.log(Level.WARNING, label + ": " + ex); } } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReManagerConnectionController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReManagerConnectionController.java index 8da791110d..4c66018b5e 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReManagerConnectionController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReManagerConnectionController.java @@ -21,14 +21,14 @@ public final class ReManagerConnectionController { private final RunEngineService svc = new RunEngineService(); private ScheduledFuture pollTask; private static final int PERIOD_SEC = 1; - private static final Logger LOG = Logger.getLogger(ReManagerConnectionController.class.getName()); + private static final Logger logger = Logger.getLogger(ReManagerConnectionController.class.getPackageName()); @FXML private void connect() { startPolling(); } @FXML private void disconnect() { stopPolling(); } private void startPolling() { if (pollTask != null && !pollTask.isDone()) return; // already running - LOG.info("Starting connection polling every " + PERIOD_SEC + " seconds"); + logger.log(Level.FINE, "Starting connection polling every " + PERIOD_SEC + " seconds"); showPending(); // UI while waiting updateWidgets(queryStatusOnce()); @@ -42,13 +42,13 @@ private StatusResponse queryStatusOnce() { try { return svc.status(); } catch (Exception ex) { - LOG.log(Level.FINE, "Status query failed: " + ex.getMessage()); + logger.log(Level.FINE, "Status query failed: " + ex.getMessage()); return null; } } private void stopPolling() { - LOG.info("Stopping connection polling"); + logger.log(Level.FINE, "Stopping connection polling"); if (pollTask != null) pollTask.cancel(true); pollTask = null; StatusBus.push(null); @@ -74,7 +74,7 @@ private void showOnline() { private void updateWidgets(StatusResponse s) { StatusBus.push((s)); if (s != null) { - LOG.log(Level.FINEST, "Status update: manager_state=" + s.managerState()); + logger.log(Level.FINEST, "Status update: manager_state=" + s.managerState()); showOnline(); } else { showPending(); // keep polling; user may Disconnect 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..74fa5450ab 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; @@ -50,7 +51,7 @@ public class RePlanEditorController implements Initializable { @FXML private TableColumn valueCol; private final RunEngineService svc = new RunEngineService(); - private static final Logger LOG = Logger.getLogger(RePlanEditorController.class.getName()); + private static final Logger logger = Logger.getLogger(RePlanEditorController.class.getPackageName()); private final ObservableList parameterRows = FXCollections.observableArrayList(); private final Map> allowedPlans = new HashMap<>(); private final Map> allowedInstructions = new HashMap<>(); @@ -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; @@ -432,10 +389,10 @@ private void loadAllowedPlansAndInstructions() { allowedPlans.put(planName, planInfo); } } else { - LOG.log(Level.WARNING, "No 'plans_allowed' key in response. Keys: " + responseMap.keySet()); + logger.log(Level.WARNING, "No 'plans_allowed' key in response. Keys: " + responseMap.keySet()); } } else { - LOG.log(Level.WARNING, "Plans response failed. Response: " + responseMap); + logger.log(Level.WARNING, "Plans response failed. Response: " + responseMap); } allowedInstructions.clear(); @@ -450,7 +407,7 @@ private void loadAllowedPlansAndInstructions() { }); } catch (Exception e) { - LOG.log(Level.WARNING, "Failed to load plans", e); + logger.log(Level.WARNING, "Failed to load plans", e); } }).start(); } @@ -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(); + logger.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(); + logger.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); + logger.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(); + logger.log(Level.SEVERE, "Failed to save item", e); + Platform.runLater(() -> { + showValidationError("Failed to save item: " + e.getMessage()); + }); } } @@ -965,7 +966,7 @@ private void processBatchFile(String filePath, String fileType) { } } catch (Exception e) { - LOG.log(Level.WARNING, "Batch file processing error", e); + logger.log(Level.WARNING, "Batch file processing error", e); Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("Error"); diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanHistoryController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanHistoryController.java index c2a1e161c9..395c55e1a0 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanHistoryController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanHistoryController.java @@ -29,6 +29,7 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -49,8 +50,8 @@ public final class RePlanHistoryController implements Initializable { private List stickySel = List.of(); private boolean ignoreSel = false; - private static final Logger LOG = - Logger.getLogger(RePlanHistoryController.class.getName()); + private static final Logger logger = + Logger.getLogger(RePlanHistoryController.class.getPackageName()); private final boolean viewOnly; @@ -123,7 +124,7 @@ private void refresh(StatusResponse st) { ignoreSel = false; restoreSelection(stickySel); } catch (Exception ex) { - LOG.warning("History refresh failed: "+ex.getMessage()); + logger.log(Level.WARNING, "History refresh failed", ex); } } @@ -188,13 +189,13 @@ private void copySelectedToQueue() { new QueueItemAddBatch(clones, "GUI Client", "primary"); svc.queueItemAddBatch(req); // service takes DTO, not Map } catch (Exception ex) { - LOG.warning("Copy-to-Queue failed: "+ex.getMessage()); + logger.log(Level.WARNING, "Copy-to-Queue failed", ex); } } private void clearHistory() { try { svc.historyClear(); } - catch (Exception ex) { LOG.warning("Clear-history failed: "+ex.getMessage()); } + catch (Exception ex) { logger.log(Level.WARNING, "Clear-history failed", ex); } } private void exportHistory(PlanHistorySaver.Format fmt, String ext) { @@ -209,9 +210,9 @@ private void exportHistory(PlanHistorySaver.Format fmt, String ext) { HistoryGetPayload hp = svc.historyGetTyped(); List items = hp.items(); PlanHistorySaver.save(items, f, fmt); - LOG.info(() -> "Exported plan history → " + f); + logger.log(Level.FINE, () -> "Exported plan history → " + f); } catch (Exception e) { - LOG.warning("Export history failed: " + e.getMessage()); + logger.log(Level.WARNING, "Export history failed", e); } }); } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java index b1c2183264..d1c98a205e 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java @@ -22,6 +22,7 @@ import java.net.URL; import java.util.*; +import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -42,8 +43,8 @@ public final class RePlanQueueController implements Initializable { private List stickySel = List.of(); // last user selection private boolean ignoreSticky= false; // guard while we rebuild - private static final Logger LOG = - Logger.getLogger(RePlanQueueController.class.getName()); + private static final Logger logger = + Logger.getLogger(RePlanQueueController.class.getPackageName()); private final boolean viewOnly; @@ -136,7 +137,7 @@ private void refresh(StatusResponse st, Collection explicitFocus) { .map(StatusResponse.PlanQueueMode::loop) .orElse(false)); } catch (Exception ex) { - LOG.warning("Queue refresh failed: " + ex.getMessage()); + logger.log(Level.WARNING, "Queue refresh failed: " + ex.getMessage()); } } @@ -224,7 +225,7 @@ private void moveRelative(int delta) { List focus = selectedUids(); try { sendMove(focus, rows.get(ref).uid(), delta<0); refreshLater(focus); } - catch (Exception ex) { LOG.warning("Move failed: "+ex.getMessage()); } + catch (Exception ex) { logger.log(Level.WARNING, "Move failed: "+ex.getMessage()); } } private void moveAbsolute(int targetRow) { if (rows.isEmpty()) return; @@ -236,7 +237,7 @@ private void moveAbsolute(int targetRow) { boolean before = targetRow < selRows.get(0); List focus = selectedUids(); try { sendMove(focus, rows.get(targetRow).uid(), before); refreshLater(focus); } - catch (Exception ex) { LOG.warning("Move-abs failed: "+ex.getMessage()); } + catch (Exception ex) { logger.log(Level.WARNING, "Move-abs failed: "+ex.getMessage()); } } private void deleteSelected() { @@ -249,7 +250,7 @@ private void deleteSelected() { try { svc.queueItemRemoveBatch(Map.of("uids", selectedUids())); refreshLater(nextFocus==null? List.of() : List.of(nextFocus)); - } catch (Exception ex) { LOG.warning("Delete failed: "+ex.getMessage()); } + } catch (Exception ex) { logger.log(Level.WARNING, "Delete failed: "+ex.getMessage()); } } private void duplicateSelected() { @@ -265,7 +266,7 @@ private void duplicateSelected() { if (orig == null) return; try { svc.addAfter(orig, orig.itemUid()); } // server returns nothing we need catch (Exception ex) { // log but continue - LOG.warning("Duplicate RPC failed: "+ex.getMessage()); + logger.log(Level.WARNING, "Duplicate RPC failed: "+ex.getMessage()); } }); @@ -285,14 +286,14 @@ private void duplicateSelected() { stickySel = added.isEmpty() ? stickySel : List.of(added.get(0)); } catch (Exception ex) { - LOG.warning("Refresh after duplicate failed: "+ex.getMessage()); + logger.log(Level.WARNING, "Refresh after duplicate failed: "+ex.getMessage()); } } private void clearQueue() { try { svc.queueClear(); } - catch (Exception ex) { LOG.warning("Clear failed: "+ex.getMessage()); } } + catch (Exception ex) { logger.log(Level.WARNING, "Clear failed: "+ex.getMessage()); } } private void setLoopMode(boolean loop){ try { svc.queueModeSet(Map.of("loop",loop)); } - catch (Exception ex){ LOG.warning("Loop-set failed: "+ex.getMessage()); } } + catch (Exception ex){ logger.log(Level.WARNING, "Loop-set failed: "+ex.getMessage()); } } private void updateButtonStates() { boolean connected = StatusBus.latest().get()!=null; diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java index 1d738c0727..749e7f5bf7 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java @@ -34,7 +34,7 @@ public class RePlanViewerController implements Initializable { @FXML private Button copyBtn, editBtn; private final RunEngineService svc = new RunEngineService(); - private static final Logger LOG = Logger.getLogger(RePlanViewerController.class.getName()); + private static final Logger logger = Logger.getLogger(RePlanViewerController.class.getPackageName()); private final ObservableList parameterRows = FXCollections.observableArrayList(); private final Map> allowedPlans = new HashMap<>(); private final Map> allowedInstructions = new HashMap<>(); @@ -203,7 +203,7 @@ private void loadAllowedPlansAndInstructions() { Platform.runLater(() -> updateWidgetState()); } catch (Exception e) { - LOG.log(Level.WARNING, "Failed to load plans", e); + logger.log(Level.WARNING, "Failed to load plans", e); } }).start(); } @@ -288,7 +288,7 @@ private void loadParametersForItem(QueueItem item) { currentValue = value != null ? String.valueOf(value) : ""; isEnabled = true; } else if (defaultValue != null) { - currentValue = String.valueOf(defaultValue) + " (default)"; + currentValue = String.valueOf(defaultValue); isEnabled = false; } @@ -425,16 +425,16 @@ private void copyToQueue() { var response = svc.queueItemAdd(request); Platform.runLater(() -> { if (!response.success()) { - LOG.log(Level.WARNING, "Copy to queue failed", response.msg()); + logger.log(Level.WARNING, "Copy to queue failed", response.msg()); } }); } catch (Exception e) { - LOG.log(Level.WARNING, "Copy to queue error", e); + logger.log(Level.WARNING, "Copy to queue error", e); } }).start(); } catch (Exception e) { - LOG.log(Level.WARNING, "Copy to queue error", e); + logger.log(Level.WARNING, "Copy to queue error", e); } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReQueueControlsController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReQueueControlsController.java index 4b0e931b89..977c92dddc 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReQueueControlsController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReQueueControlsController.java @@ -14,6 +14,7 @@ import java.net.URL; import java.util.Map; import java.util.ResourceBundle; +import java.util.logging.Level; import java.util.logging.Logger; public final class ReQueueControlsController implements Initializable { @@ -24,7 +25,7 @@ public final class ReQueueControlsController implements Initializable { @FXML private Button stopBtn; // text toggles “Stop” / “Cancel Stop” private final RunEngineService svc = new RunEngineService(); - private final Logger LOG = Logger.getLogger(getClass().getName()); + private static final Logger logger = Logger.getLogger(ReQueueControlsController.class.getPackageName()); private volatile boolean autoEnabledSrv = false; // what server says now private volatile boolean stopPendingSrv = false; // ditto @@ -124,6 +125,6 @@ else if (statusObj instanceof Map m) { private void runSafely(String what, Runnable r) { try { r.run(); } - catch (Exception ex) { LOG.warning(what + ": " + ex); } + catch (Exception ex) { logger.log(Level.WARNING, what + ": " + ex); } } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReRunningPlanController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReRunningPlanController.java index 1abc67183d..e1e5c3ed3a 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReRunningPlanController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/ReRunningPlanController.java @@ -25,9 +25,10 @@ public final class ReRunningPlanController implements Initializable { @FXML private TextArea planTextArea; private final RunEngineService svc = new RunEngineService(); - private static final Logger LOG = Logger.getLogger(ReRunningPlanController.class.getName()); + private static final Logger logger = Logger.getLogger(ReRunningPlanController.class.getPackageName()); private String lastRunningUid = ""; + private QueueItem cachedRunningItem = null; private final boolean viewOnly; @@ -58,6 +59,7 @@ private void render(StatusResponse st) { if (st == null) { planTextArea.clear(); lastRunningUid = ""; + cachedRunningItem = null; copyBtn.setDisable(true); updateBtn.setDisable(true); return; @@ -82,19 +84,21 @@ private void render(StatusResponse st) { if (uid == null) { // nothing running planTextArea.clear(); lastRunningUid = ""; + cachedRunningItem = null; return; } - QueueItem runningItem; - if (!uid.equals(lastRunningUid)) { // new plan started - runningItem = fetchRunningItem(); + // Fetch running item only if it's a new plan + if (!uid.equals(lastRunningUid)) { + cachedRunningItem = fetchRunningItem(); lastRunningUid = uid; - } else { - runningItem = null; // keep previous text, only update run-list } + + // Always fetch the latest run list List> runList = fetchRunList(); - planTextArea.setText(format(runningItem, runList)); + // Use cached running item to keep displaying plan details + planTextArea.setText(format(cachedRunningItem, runList)); planTextArea.positionCaret(0); } @@ -103,7 +107,7 @@ private QueueItem fetchRunningItem() { QueueGetPayload p = svc.queueGetTyped(); return p.runningItem(); // may be null } catch (Exception ex) { - LOG.log(Level.FINE, "Failed to fetch running item: " + ex.getMessage()); + logger.log(Level.FINE, "Failed to fetch running item: " + ex.getMessage()); return null; } } @@ -116,7 +120,7 @@ private List> fetchRunList() { if (p instanceof Map m && m.containsKey("run_list")) return (List>) m.get("run_list"); } catch (Exception ex) { - LOG.log(Level.FINE, "Failed to fetch run list: " + ex.getMessage()); + logger.log(Level.FINE, "Failed to fetch run list: " + ex.getMessage()); } return List.of(); } @@ -158,14 +162,13 @@ private static String format(QueueItem item, List> runs) { @FXML private void copyToQueue() { - QueueItem running = fetchRunningItem(); - if (running == null) return; + if (cachedRunningItem == null) return; try { - svc.queueItemAdd(running); - LOG.info("Copied running plan to queue: " + running.name()); + svc.queueItemAdd(cachedRunningItem); + logger.log(Level.FINE, "Copied running plan to queue: " + cachedRunningItem.name()); } catch (Exception ex) { - LOG.log(Level.WARNING, "Failed to copy running plan to queue", ex); + logger.log(Level.WARNING, "Failed to copy running plan to queue", ex); } } @@ -174,9 +177,9 @@ private void copyToQueue() { private void updateEnvironment() { try { svc.environmentUpdate(Map.of()); - LOG.info("Environment update requested"); + logger.log(Level.FINE, "Environment update requested"); } catch (Exception ex) { - LOG.log(Level.WARNING, "Failed to update environment", ex); + logger.log(Level.WARNING, "Failed to update environment", ex); } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/HttpSupport.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/HttpSupport.java index 14d64f54fa..051206483d 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/HttpSupport.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/HttpSupport.java @@ -10,7 +10,7 @@ public final class HttpSupport { private HttpSupport() {} - public static final Logger LOG = Logger.getLogger("com.jbi.bluesky"); + public static final Logger logger = Logger.getLogger(HttpSupport.class.getPackageName()); /* ---------------- retry policy ----------- */ public static final int MAX_RETRIES = 3; @@ -29,6 +29,6 @@ public static long elapsed(long startNano) { } public static void fine(String msg) { - LOG.log(Level.FINE, msg); + logger.log(Level.FINE, msg); } } 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..d6fc6b4e6f --- /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 logger = Logger.getLogger(JythonScriptExecutor.class.getPackageName()); + 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"); + + logger.log(Level.FINE, "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) { + logger.log(Level.WARNING, "Jython script execution failed", e); + 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 executeAsync(String scriptContent, java.util.Map contextVars) { + return EXECUTOR.submit(() -> execute(scriptContent, contextVars)); + } + + /** + * Execute a Python script from a resource file. + * + * @param resourcePath Path to the Python script resource + * @param contextVars Variables to pass into the Python context + * @return The result object returned by the script + */ + public Object executeResource(String resourcePath, java.util.Map contextVars) throws Exception { + try (InputStream stream = getClass().getResourceAsStream(resourcePath)) { + if (stream == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + + String scriptContent = new String(stream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); + return execute(scriptContent, contextVars); + } + } + + /** + * Execute a simple Python expression and return the result. + * + * @param expression Python expression to evaluate + * @return The evaluated result + */ + public Object eval(String expression) { + try { + PyObject result = python.eval(expression); + return result.__tojava__(Object.class); + } catch (Exception e) { + logger.log(Level.WARNING, "Jython eval failed for: " + expression, e); + throw new RuntimeException("Evaluation failed: " + e.getMessage(), e); + } + } + + /** + * Close this executor and release resources. + */ + public void close() { + if (python != null) { + python.close(); + } + } + + /** + * Shutdown the shared executor service (call during application shutdown). + */ + public static void shutdown() { + EXECUTOR.shutdown(); + try { + if (!EXECUTOR.awaitTermination(5, TimeUnit.SECONDS)) { + EXECUTOR.shutdownNow(); + } + } catch (InterruptedException e) { + EXECUTOR.shutdownNow(); + Thread.currentThread().interrupt(); + } + } +} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PollCenter.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PollCenter.java index 4c8413cc23..b20fd60629 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PollCenter.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PollCenter.java @@ -8,7 +8,7 @@ // Single ScheduledExecutor shared by all widgets that need periodic polling. public final class PollCenter { - private static final Logger LOG = Logger.getLogger(PollCenter.class.getName()); + private static final Logger logger = Logger.getLogger(PollCenter.class.getPackageName()); private static final ScheduledExecutorService EXEC = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "JBI-poller")); @@ -16,7 +16,7 @@ public final class PollCenter { private PollCenter() {} public static ScheduledFuture every(long periodSec, Runnable task) { - LOG.log(Level.FINE, "Scheduling task with period: " + periodSec + " seconds"); + logger.log(Level.FINE, "Scheduling task with period: " + periodSec + " seconds"); return EXEC.scheduleAtFixedRate(task, 0, periodSec, TimeUnit.SECONDS); } @@ -30,7 +30,7 @@ public static ScheduledFuture every( T t = supplier.get(); javafx.application.Platform.runLater(() -> fxConsumer.accept(t)); } catch (Exception ex) { - LOG.log(Level.WARNING, "Polling task failed", ex); + logger.log(Level.WARNING, "Polling task failed", ex); } }); } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PythonParameterConverter.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PythonParameterConverter.java new file mode 100644 index 0000000000..770581311e --- /dev/null +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/PythonParameterConverter.java @@ -0,0 +1,157 @@ +package org.phoebus.applications.queueserver.util; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Python-based parameter type converter for Queue Server. + * + * This class delegates type conversion to a Python script (using Jython), + * allowing us to use Python's ast.literal_eval for parsing parameter values + * instead of implementing complex type conversions in Java. + */ +public class PythonParameterConverter { + + private static final Logger logger = Logger.getLogger(PythonParameterConverter.class.getPackageName()); + private static final String SCRIPT_RESOURCE = "/org/phoebus/applications/queueserver/scripts/type_converter.py"; + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final JythonScriptExecutor executor; + private final String scriptContent; + + /** + * Create a new Python parameter converter. + */ + public PythonParameterConverter() { + this.executor = new JythonScriptExecutor(); + + // Load the Python script from resources + try (var stream = getClass().getResourceAsStream(SCRIPT_RESOURCE)) { + if (stream == null) { + throw new IllegalStateException("Python converter script not found: " + SCRIPT_RESOURCE); + } + this.scriptContent = new String(stream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); + logger.log(Level.FINE, "Python type converter script loaded successfully"); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to load Python converter script", e); + throw new RuntimeException("Failed to initialize Python converter", e); + } + } + + /** + * Parameter information for conversion. + */ + public static class ParameterInfo { + private String name; + private String value; + private boolean enabled; + private boolean isOptional; + private Object defaultValue; + + public ParameterInfo(String name, String value, boolean enabled, boolean isOptional, Object defaultValue) { + this.name = name; + this.value = value; + this.enabled = enabled; + this.isOptional = isOptional; + this.defaultValue = defaultValue; + } + + // Getters and setters + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public boolean isOptional() { return isOptional; } + public void setOptional(boolean optional) { isOptional = optional; } + + public Object getDefaultValue() { return defaultValue; } + public void setDefaultValue(Object defaultValue) { this.defaultValue = defaultValue; } + } + + /** + * Convert a list of parameters from string values to typed objects using Python. + * + * @param parameters List of parameter information + * @return Map of parameter names to their typed values + */ + public Map convertParameters(List parameters) { + try { + // Serialize parameters to JSON + String parametersJson = objectMapper.writeValueAsString(parameters); + + // Prepare context for Python script + Map context = new HashMap<>(); + context.put("parameters_json", parametersJson); + + // Execute the Python script + Object result = executor.execute(scriptContent, context); + + // Parse result JSON back to Map + if (result != null) { + String resultJson = result.toString(); + + // Check if result contains an error + Map resultMap = objectMapper.readValue(resultJson, + new TypeReference>() {}); + + if (resultMap.containsKey("error")) { + String errorMsg = "Python type conversion failed: " + resultMap.get("error"); + logger.log(Level.SEVERE, errorMsg); + throw new RuntimeException(errorMsg); + } + + return resultMap; + } + + return new HashMap<>(); + + } catch (Exception e) { + logger.log(Level.WARNING, "Python parameter conversion failed", e); + throw new RuntimeException("Parameter conversion failed: " + e.getMessage(), e); + } + } + + + /** + * Validate a parameter value using Python. + * + * @param value String value to validate + * @return true if the value can be parsed, false otherwise + */ + public boolean validateValue(String value) { + if (value == null || value.trim().isEmpty()) { + return true; + } + + try { + List testParam = List.of( + new ParameterInfo("test", value, true, true, null) + ); + convertParameters(testParam); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Close the converter and release resources. + */ + public void close() { + if (executor != null) { + executor.close(); + } + } +} diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/StatusBus.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/StatusBus.java index 44ad2335e7..4cb51d072d 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/StatusBus.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/StatusBus.java @@ -17,7 +17,7 @@ */ public final class StatusBus { - private static final Logger LOG = Logger.getLogger(StatusBus.class.getName()); + private static final Logger logger = Logger.getLogger(StatusBus.class.getPackageName()); private static final ObjectProperty LATEST = new SimpleObjectProperty<>(null); private StatusBus() {} @@ -28,9 +28,9 @@ public static ObjectProperty latest() { public static void push(StatusResponse s) { if (s != null) { - LOG.log(Level.FINEST, "Status update: " + s.managerState()); + logger.log(Level.FINEST, "Status update: " + s.managerState()); } else { - LOG.log(Level.FINE, "Status cleared (server offline)"); + logger.log(Level.FINE, "Status cleared (server offline)"); } if (Platform.isFxApplicationThread()) { diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java index f0700c5581..7e9d816e80 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java @@ -10,7 +10,7 @@ public class ItemUpdateEvent { private static final ItemUpdateEvent instance = new ItemUpdateEvent(); private final List> listeners = new ArrayList<>(); - private static final Logger LOG = Logger.getLogger(ItemUpdateEvent.class.getName()); + private static final Logger logger = Logger.getLogger(ItemUpdateEvent.class.getPackageName()); private ItemUpdateEvent() {} @@ -27,7 +27,7 @@ public void notifyItemUpdated(QueueItem updatedItem) { try { listener.accept(updatedItem); } catch (Exception e) { - LOG.log(Level.WARNING, "Item update listener error", e); + logger.log(Level.WARNING, "Item update listener error", e); } } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java index fa31cb08c2..b414ed3350 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java @@ -13,7 +13,7 @@ public class PlanEditEvent { private static final PlanEditEvent INSTANCE = new PlanEditEvent(); private final List> listeners = new CopyOnWriteArrayList<>(); - private static final Logger LOG = Logger.getLogger(PlanEditEvent.class.getName()); + private static final Logger logger = Logger.getLogger(PlanEditEvent.class.getPackageName()); private PlanEditEvent() {} @@ -34,7 +34,7 @@ public void notifyEditRequested(QueueItem itemToEdit) { try { listener.accept(itemToEdit); } catch (Exception e) { - LOG.log(Level.WARNING, "Error in plan edit listener", e); + logger.log(Level.WARNING, "Error in plan edit listener", e); } } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/QueueItemSelectionEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/QueueItemSelectionEvent.java index 0ce51e3286..0927f5cfcd 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/QueueItemSelectionEvent.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/QueueItemSelectionEvent.java @@ -14,7 +14,7 @@ public class QueueItemSelectionEvent { private static final QueueItemSelectionEvent INSTANCE = new QueueItemSelectionEvent(); private final List> listeners = new CopyOnWriteArrayList<>(); - private static final Logger LOG = Logger.getLogger(TabSwitchEvent.class.getName()); + private static final Logger logger = Logger.getLogger(QueueItemSelectionEvent.class.getPackageName()); private QueueItemSelectionEvent() {} @@ -35,7 +35,7 @@ public void notifySelectionChanged(QueueItem selectedItem) { try { listener.accept(selectedItem); } catch (Exception e) { - LOG.log(Level.WARNING, "Error in queue selection listener", e); + logger.log(Level.WARNING, "Error in queue selection listener", e); } } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java index 61069a74d2..9e78da402a 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java @@ -9,7 +9,7 @@ public class TabSwitchEvent { private static final TabSwitchEvent instance = new TabSwitchEvent(); private final List> listeners = new ArrayList<>(); - private static final Logger LOG = Logger.getLogger(TabSwitchEvent.class.getName()); + private static final Logger logger = Logger.getLogger(TabSwitchEvent.class.getPackageName()); private TabSwitchEvent() {} @@ -26,7 +26,7 @@ public void switchToTab(String tabName) { try { listener.accept(tabName); } catch (Exception e) { - LOG.log(Level.WARNING, "Tab switch listener error", e); + logger.log(Level.WARNING, "Tab switch listener error", e); } } } diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ViewFactory.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ViewFactory.java index a3c3f6291c..2c197c34bd 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ViewFactory.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ViewFactory.java @@ -21,7 +21,7 @@ public enum ViewFactory { MONITOR_QUEUE ("/org/phoebus/applications/queueserver/view/MonitorQueue.fxml"), EDIT_AND_CONTROL_QUEUE ("/org/phoebus/applications/queueserver/view/EditAndControlQueue.fxml"); - private static final Logger LOG = Logger.getLogger(ViewFactory.class.getName()); + private static final Logger logger = Logger.getLogger(ViewFactory.class.getPackageName()); private final String path; @@ -31,7 +31,7 @@ public Parent get() { try { return FXMLLoader.load(ViewFactory.class.getResource(path)); } catch (IOException ex) { - LOG.log(Level.SEVERE, "FXML load failed: " + path, ex); + logger.log(Level.SEVERE, "FXML load failed: " + path, ex); return new StackPane(new Label("⚠ Unable to load " + path)); } } diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/css/style.css b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/css/style.css index f0364d6b55..6e020b588b 100644 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/css/style.css +++ b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/css/style.css @@ -7,4 +7,40 @@ -fx-border-width: 1; -fx-background-color: #ffffff; -fx-padding: 8; +} + +/* Completely hide all focus indicators without changing appearance */ +* { + -fx-focus-color: transparent; + -fx-faint-focus-color: transparent; +} + +/* Keep TextArea border same as unfocused when focused */ +.text-area:focused { + -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), + derive(-fx-base,-1%); +} + +/* Override TextArea content focus styling to match unfocused state */ +.text-area:focused .content { + -fx-background-color: + linear-gradient(from 0px 0px to 0px 4px, derive(-fx-control-inner-background, -8%), -fx-control-inner-background); + -fx-background-insets: 0; + -fx-background-radius: 2; +} + +/* Override TableView focus styling */ +.table-view:focused { + -fx-background-color: -fx-box-border, -fx-control-inner-background; + -fx-background-insets: 0, 1; +} + +/* Make table row selection blue instead of gray */ +.table-row-cell:selected { + -fx-background-color: #0096C9; + -fx-table-cell-border-color: derive(#0096C9, 20%); +} + +.table-row-cell:selected .table-cell { + -fx-text-fill: white; } \ No newline at end of file diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/scripts/type_converter.py b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/scripts/type_converter.py new file mode 100644 index 0000000000..c0d2718290 --- /dev/null +++ b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/scripts/type_converter.py @@ -0,0 +1,183 @@ +""" +Type converter script for Queue Server parameter parsing. + +This script uses Python's ast.literal_eval to safely parse parameter values +from string representations, matching the behavior of the Python Qt implementation. + +Usage from Java: + - Pass 'parameters_json' as a JSON string containing parameter definitions + - Returns 'result' as a JSON string with parsed values +""" + +import ast +import json +import sys + +# Sentinel value for empty/unset parameters (Jython 2.7 doesn't have inspect.Parameter.empty) +EMPTY = object() + +def parse_literal_value(value_str): + """ + Parse a string representation of a value into its proper Python type. + Mimics ast.literal_eval behavior used in the PyQt implementation. + + Args: + value_str: String representation of the value + + Returns: + Parsed value with appropriate Python type + """ + if value_str is None or value_str == '': + return EMPTY + + value_str = value_str.strip() + + # Handle None/null + if value_str in ('None', 'null'): + return None + + # Handle booleans + if value_str in ('True', 'true'): + return True + if value_str in ('False', 'false'): + return False + + # Handle quoted strings + if ((value_str.startswith("'") and value_str.endswith("'")) or + (value_str.startswith('"') and value_str.endswith('"'))): + return value_str[1:-1] + + # Try numeric parsing + try: + if '.' in value_str: + return float(value_str) + else: + return int(value_str) + except ValueError: + pass + + # Try ast.literal_eval for lists, dicts, tuples, etc. + try: + return ast.literal_eval(value_str) + except (ValueError, SyntaxError): + # If all else fails, return as string + return value_str + + +def convert_parameters(parameters_data): + """ + Convert parameter values from string representations to typed objects. + + Args: + parameters_data: List of parameter dictionaries with 'name', 'value', 'enabled' fields + + Returns: + Dictionary mapping parameter names to their parsed values + """ + result = {} + + for param in parameters_data: + param_name = param.get('name') + param_value = param.get('value') + is_enabled = param.get('enabled', True) + default_value = param.get('defaultValue') + + # Skip parameters with no value (empty and no default) + if param_value is None or param_value == '': + continue + + try: + # Parse the value string to proper type (works for both enabled and disabled) + # When disabled, the value contains the stringified default value + parsed_value = parse_literal_value(param_value) + + # Only include if it's not empty + if parsed_value != EMPTY: + result[param_name] = parsed_value + + except Exception as e: + # Log error but continue processing other parameters + sys.stderr.write("Warning: Failed to parse parameter '%s' with value '%s': %s\n" % (param_name, param_value, e)) + # Fall back to string value + result[param_name] = param_value + + return result + + +def validate_parameters(parameters_data): + """ + Validate parameter values and return validation results. + + Args: + parameters_data: List of parameter dictionaries + + Returns: + Dictionary with validation results for each parameter + """ + validation_results = {} + + for param in parameters_data: + param_name = param.get('name') + param_value = param.get('value') + is_enabled = param.get('enabled', True) + is_optional = param.get('isOptional', False) + + if not is_enabled: + validation_results[param_name] = {'valid': True, 'message': 'Disabled'} + continue + + if param_value is None or param_value == '': + is_valid = is_optional + validation_results[param_name] = { + 'valid': is_valid, + 'message': 'Required parameter missing' if not is_valid else 'OK' + } + continue + + try: + # Try to parse the value + parse_literal_value(param_value) + validation_results[param_name] = {'valid': True, 'message': 'OK'} + except Exception as e: + validation_results[param_name] = { + 'valid': False, + 'message': 'Parse error: %s' % str(e) + } + + return validation_results + + +# Main execution +if __name__ == '__main__': + # When run directly (for testing) + test_params = [ + {'name': 'detector', 'value': "'det1'", 'enabled': True, 'isOptional': False}, + {'name': 'num_points', 'value': '10', 'enabled': True, 'isOptional': False}, + {'name': 'exposure', 'value': '0.5', 'enabled': True, 'isOptional': False}, + {'name': 'metadata', 'value': "{'key': 'value'}", 'enabled': True, 'isOptional': True}, + ] + + result = convert_parameters(test_params) + # Don't print - just for testing + pass + +# Script entry point for Jython execution +# Expects: parameters_json (input), sets: result (output) +try: + if 'parameters_json' in dir(): + # Parse input JSON + params_data = json.loads(parameters_json) + + # Convert parameters + converted = convert_parameters(params_data) + + # Set result as JSON string + result = json.dumps(converted) + +except Exception as e: + # Return error as result + result = json.dumps({ + 'error': str(e), + 'type': type(e).__name__ + }) + sys.stderr.write("Error in type_converter.py: %s\n" % e) diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/Application.fxml b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/Application.fxml index fa3d9d9c77..609601d802 100644 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/Application.fxml +++ b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/Application.fxml @@ -8,7 +8,7 @@ - +