001package jmri.server.json.panel;
002
003import com.fasterxml.jackson.databind.JsonNode;
004import java.beans.PropertyChangeEvent;
005import java.beans.PropertyChangeListener;
006import java.io.IOException;
007import java.util.concurrent.Executors;
008import java.util.concurrent.ScheduledExecutorService;
009import java.util.concurrent.ScheduledFuture;
010import java.util.concurrent.ThreadFactory;
011import java.util.concurrent.TimeUnit;
012import jmri.InstanceManager;
013import jmri.JmriException;
014import jmri.jmrit.display.Editor;
015import jmri.jmrit.display.EditorManager;
016import jmri.jmrit.display.controlPanelEditor.ControlPanelEditor;
017import jmri.jmrit.display.panelEditor.PanelEditor;
018import jmri.server.json.JsonConnection;
019import jmri.server.json.JsonException;
020import jmri.server.json.JsonRequest;
021import jmri.server.json.JsonSocketService;
022
023/** WebSocket notifications for panel display-option changes. */
024public class JsonPanelSocketService extends JsonSocketService<JsonPanelHttpService> {
025
026    private static final long DISPLAY_UPDATE_DELAY_MS = 200;
027    private static final ScheduledExecutorService UPDATE_SCHEDULER = Executors.newSingleThreadScheduledExecutor(
028            new ThreadFactory() {
029                @Override
030                public Thread newThread(Runnable task) {
031                    Thread thread = new Thread(task, "JMRI web panel updates");
032                    thread.setDaemon(true);
033                    return thread;
034                }
035            });
036
037    private Editor editor;
038    private String panelName;
039    private ScheduledFuture<?> pendingUpdate;
040    private boolean closed;
041    private final PropertyChangeListener listener = this::propertyChange;
042
043    public JsonPanelSocketService(JsonConnection connection) {
044        super(connection, new JsonPanelHttpService(connection.getObjectMapper()));
045    }
046
047    @Override
048    public void onMessage(String type, JsonNode data, JsonRequest request) throws IOException, JmriException, JsonException {
049        String name = data.path("name").asText();
050        Editor requested = InstanceManager.getDefault(EditorManager.class).getAll().stream()
051                .filter(candidate -> candidate instanceof PanelEditor || candidate instanceof ControlPanelEditor)
052                .filter(candidate -> candidate.getTargetFrame() != null
053                        && candidate.getTargetFrame().getTitle().equals(name))
054                .findFirst().orElse(null);
055        if (requested != editor) {
056            removeEditorListeners();
057            editor = requested;
058            if (editor != null) {
059                editor.addPropertyChangeListener("webPanelDisplayOptions", listener);
060            }
061        }
062        panelName = name;
063    }
064
065    private synchronized void propertyChange(PropertyChangeEvent event) {
066        String property = event.getPropertyName();
067        if (closed || editor == null || !"webPanelDisplayOptions".equals(property)) {
068            return;
069        }
070        if (pendingUpdate != null) {
071            pendingUpdate.cancel(false);
072        }
073        pendingUpdate = UPDATE_SCHEDULER.schedule(this::sendDisplayOptionsChanged,
074                DISPLAY_UPDATE_DELAY_MS, TimeUnit.MILLISECONDS);
075    }
076
077    private void sendDisplayOptionsChanged() {
078        synchronized (this) {
079            if (closed || editor == null || panelName == null) {
080                return;
081            }
082            pendingUpdate = null;
083        }
084        try {
085            com.fasterxml.jackson.databind.node.ObjectNode message = connection.getObjectMapper().createObjectNode()
086                    .put("type", JsonPanelServiceFactory.PANEL_DISPLAY_OPTIONS);
087            message.putObject("data").put("name", panelName).put("value", "displayOptionsChanged");
088            connection.sendMessage(message, 0);
089        } catch (IOException ignored) {
090            // The connection is closing; no further update can be delivered.
091        }
092    }
093
094    @Override
095    public void onList(String type, JsonNode data, JsonRequest request) {
096        // Panel notifications are subscriptions, not list requests.
097    }
098
099    @Override
100    public synchronized void onClose() {
101        closed = true;
102        if (pendingUpdate != null) {
103            pendingUpdate.cancel(false);
104            pendingUpdate = null;
105        }
106        removeEditorListeners();
107        panelName = null;
108    }
109
110    private void removeEditorListeners() {
111        if (editor != null) {
112            editor.removePropertyChangeListener("webPanelDisplayOptions", listener);
113            editor = null;
114        }
115    }
116}