001package jmri.web.servlet.frameimage;
002
003import static jmri.server.json.JSON.NAME;
004import static jmri.server.json.JSON.URL;
005import static jmri.web.servlet.ServletUtil.UTF8;
006
007import com.fasterxml.jackson.databind.ObjectMapper;
008import com.fasterxml.jackson.databind.node.ArrayNode;
009import com.fasterxml.jackson.databind.node.ObjectNode;
010
011import java.awt.*;
012import java.awt.event.MouseEvent;
013import java.awt.event.MouseListener;
014import java.awt.image.BufferedImage;
015import java.io.ByteArrayOutputStream;
016import java.io.IOException;
017import java.io.UnsupportedEncodingException;
018import java.net.URLDecoder;
019import java.net.URLEncoder;
020import java.text.MessageFormat;
021import java.util.Arrays;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027
028import javax.annotation.CheckForNull;
029import javax.annotation.Nonnull;
030import javax.imageio.ImageIO;
031import javax.servlet.ServletException;
032import javax.servlet.annotation.WebServlet;
033import javax.servlet.http.HttpServlet;
034import javax.servlet.http.HttpServletRequest;
035import javax.servlet.http.HttpServletResponse;
036import javax.swing.AbstractButton;
037import javax.swing.JButton;
038import javax.swing.JCheckBox;
039import javax.swing.JDialog;
040import javax.swing.JFrame;
041import javax.swing.JRadioButton;
042import javax.swing.JTable;
043import javax.swing.JToggleButton;
044import javax.swing.SwingUtilities;
045import javax.swing.table.JTableHeader;
046
047import jmri.InstanceManager;
048import jmri.jmrit.display.Editor;
049import jmri.jmrit.display.Positionable;
050import jmri.server.json.JSON;
051import jmri.server.json.JsonException;
052import jmri.server.json.util.JsonUtilHttpService;
053import jmri.util.JmriJFrame;
054import jmri.util.swing.JDialogListener;
055import jmri.util.swing.JmriMouseEvent;
056import jmri.web.server.WebServerPreferences;
057
058import org.openide.util.lookup.ServiceProvider;
059import org.slf4j.Logger;
060import org.slf4j.LoggerFactory;
061
062/**
063 * A simple servlet that returns a JMRI window as a PNG image or enclosing HTML
064 * file.
065 * <p>
066 * The suffix of the request determines which. <dl>
067 * <dt>.html<dd>Returns a HTML file that displays the frame enabled for clicking
068 * via server side image map; see the .properties file for the content
069 * <dt>.png<dd>Just return the image <dt>no name<dd>Return an HTML page with
070 * links to available images </dl>
071 * <p>
072 * The associated .properties file contains the HTML fragments used to form
073 * replies.
074 * <p>
075 * Parts taken from Core Web Programming from Prentice Hall and Sun Microsystems
076 * Press, http://www.corewebprogramming.com/. &copy; 2001 Marty Hall and Larry
077 * Brown; may be freely used or adapted.
078 *
079 * @author Modifications by Bob Jacobsen Copyright 2005, 2006, 2008
080 */
081@WebServlet(name = "FrameServlet",
082        urlPatterns = {"/frame"})
083@ServiceProvider(service = HttpServlet.class)
084public class JmriJFrameServlet extends HttpServlet {
085
086    void sendClick(String name, @Nonnull Component c, int xg, int yg, Container frameContentPane) {  // global positions
087        int x = xg - c.getLocation().x;
088        int y = yg - c.getLocation().y;
089        // log.debug("component is {}", c);
090        log.debug("Local click at {},{} in {}", x, y, c.getClass());
091
092        if (c.getClass().equals(JButton.class)) {
093            ((AbstractButton) c).doClick();
094        } else if (c.getClass().equals(JToggleButton.class)) {
095            ((AbstractButton) c).doClick();
096        } else if (c.getClass().equals(JCheckBox.class)) {
097            ((AbstractButton) c).doClick();
098        } else if (c.getClass().equals(JRadioButton.class)) {
099            ((AbstractButton) c).doClick();
100        } else if (MouseListener.class.isAssignableFrom(c.getClass())) {
101            log.debug("Invoke directly on MouseListener, at {},{}", x, y);
102            sendClickSequence((MouseListener) c, c, x, y);
103        } else if (c instanceof jmri.jmrit.display.MultiSensorIcon) {
104            log.debug("Invoke Clicked on MultiSensorIcon");
105            JmriMouseEvent e = new JmriMouseEvent(c,
106                    JmriMouseEvent.MOUSE_CLICKED,
107                    0, // time
108                    0, // modifiers
109                    xg, yg, // this component expects global positions for some reason
110                    1, // one click
111                    false // not a popup
112            );
113            ((Positionable) c).doMouseClicked(e);
114        } else if (Positionable.class.isAssignableFrom(c.getClass())) {
115            log.debug("Invoke Pressed, Released and Clicked on Positionable");
116            JmriMouseEvent e = new JmriMouseEvent(c,
117                    JmriMouseEvent.MOUSE_PRESSED,
118                    0, // time
119                    0, // modifiers
120                    x, y, // x, y not in this component?
121                    1, // one click
122                    false // not a popup
123            );
124            ((Positionable) c).doMousePressed(e);
125
126            e = new JmriMouseEvent(c,
127                    JmriMouseEvent.MOUSE_RELEASED,
128                    0, // time
129                    0, // modifiers
130                    x, y, // x, y not in this component?
131                    1, // one click
132                    false // not a popup
133            );
134            ((Positionable) c).doMouseReleased(e);
135
136            e = new JmriMouseEvent(c,
137                    JmriMouseEvent.MOUSE_CLICKED,
138                    0, // time
139                    0, // modifiers
140                    x, y, // x, y not in this component?
141                    1, // one click
142                    false // not a popup
143            );
144            ((Positionable) c).doMouseClicked(e);
145        } else {
146            if ( c instanceof JButton ){
147                ((JButton)c).doClick();
148                return;
149            }
150            MouseListener[] la = c.getMouseListeners();
151            log.debug("Invoke {} contained mouse listeners", la.length);
152            log.debug("component is {}", c);
153            /*
154             * Using c.getLocation() above we adjusted the click position for
155             * the offset of the control relative to the frame. That works fine
156             * in the cases above. In this case getLocation only provides the
157             * offset of the control relative to the Component. So we also need
158             * to adjust the click position for the offset of the Component
159             * relative to the frame.
160             */
161            if (c instanceof JTable || c instanceof JTableHeader) {
162                // need to make clicks on a JTable and JTableHeader all relative
163                Rectangle rT = c.getBounds();
164                Rectangle r = SwingUtilities.convertRectangle(c.getParent(), rT, frameContentPane);
165                // need to adjust table click, note that table can scroll
166                x += (int) rT.getX() - (int) r.getX();
167                y += (int) rT.getY() - (int) r.getY();
168                log.debug("New JTable x: {} and y: {}", x, y);
169            }
170
171            for (MouseListener ml : la) {
172                log.trace("Send click sequence at {},{}", x, y);
173                sendClickSequence(ml, c, x, y);
174            }
175        }
176    }
177
178    private void sendClickSequence(MouseListener m, Component c, int x, int y) {
179        /*
180         * create the sequence of mouse events needed to click on a control:
181         * MOUSE_ENTERED MOUSE_PRESSED MOUSE_RELEASED MOUSE_CLICKED MOUSE_EXITED
182         */
183        MouseEvent e = new MouseEvent(c,
184                MouseEvent.MOUSE_ENTERED,
185                0, // time
186                0, // modifiers
187                x, y, // x, y not in this component?
188                1, // one click
189                false // not a popup
190        );
191        m.mouseEntered(e);
192        e = new MouseEvent(c,
193                MouseEvent.MOUSE_PRESSED,
194                0, // time
195                0, // modifiers
196                x, y, // x, y not in this component?
197                1, // one click
198                false, // not a popup
199                MouseEvent.BUTTON1);
200        m.mousePressed(e);
201        e = new MouseEvent(c,
202                MouseEvent.MOUSE_RELEASED,
203                0, // time
204                0, // modifiers
205                x, y, // x, y not in this component?
206                1, // one click
207                false, // not a popup
208                MouseEvent.BUTTON1);
209        m.mouseReleased(e);
210        e = new MouseEvent(c,
211                MouseEvent.MOUSE_CLICKED,
212                0, // time
213                0, // modifiers
214                x, y, // x, y not in this component?
215                1, // one click
216                false, // not a popup
217                MouseEvent.BUTTON1);
218        m.mouseClicked(e);
219        e = new MouseEvent(c,
220                MouseEvent.MOUSE_EXITED,
221                0, // time
222                0, // modifiers
223                x, y, // x, y not in this component?
224                1, // one click
225                false, // not a popup
226                MouseEvent.BUTTON1);
227        m.mouseExited(e);
228    }
229
230    @Override
231    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
232        // because we work with Swing, we do this on the AWT thread
233
234        if (javax.swing.SwingUtilities.isEventDispatchThread()) {
235            doGetOnSwing(request, response);
236            return;
237        }
238
239        try {
240            javax.swing.SwingUtilities.invokeAndWait(
241                () -> {
242                    try {
243                        doGetOnSwing(request, response);
244                    } catch ( ServletException | IOException ex ) {
245                        throw new RuntimeException(ex);
246                    }
247                }
248            );
249        } catch (InterruptedException ex) {
250            // ignore
251            log.trace("Ignoring InterruptedException");
252        } catch (java.lang.reflect.InvocationTargetException ex) {
253            // exception thrown up, unpack and rethrow?
254            log.trace("top-level caught", ex);
255            if (ex.getCause() != null) {
256                log.trace("1st level caught", ex.getCause());
257                if (ex.getCause().getCause() != null) {
258                    // have to decode within content
259                    Throwable ex2 = ex.getCause().getCause();
260                    if ( ex2 instanceof ServletException) {
261                        throw (ServletException) ex2;
262                    } else if ( ex2 instanceof IOException) {
263                        throw (IOException) ex2;
264                    } else {
265                        // wrap and throw
266                        throw new RuntimeException(ex);
267                    }
268                } else {
269                    // wrap and throw
270                    throw new RuntimeException(ex);
271                }
272            } else {
273                // just wrap and rethrow the InvocationTargetException, but this should never happen
274                throw new RuntimeException(ex);
275            }
276        }
277    }
278
279    protected void doGetOnSwing(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
280        WebServerPreferences preferences = InstanceManager.getDefault(WebServerPreferences.class);
281        if (preferences.isDisableFrames()) {
282            if (preferences.isRedirectFramesToPanels()) {
283                if (JSON.JSON.equals(request.getParameter("format"))) {
284                    response.sendRedirect("/panel?format=json");
285                } else {
286                    response.sendRedirect("/panel");
287                }
288            } else {
289                response.sendError(HttpServletResponse.SC_FORBIDDEN, Bundle.getMessage(request.getLocale(), "FramesAreDisabled"));
290            }
291            return;
292        }
293        JmriJFrame frame = null;
294        String name = getFrameName(request.getRequestURI());
295        if (name != null) {
296            List<String> disallowedFrames = Arrays.asList(preferences.getDisallowedFrames());
297            if (disallowedFrames.contains(name)) {
298                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Frame [" + name + "] not allowed (check Preferences)");
299                return;
300            }
301            frame = JmriJFrame.getFrame(name);
302            if (frame == null) {
303                response.sendError(HttpServletResponse.SC_NOT_FOUND, "Can not find frame [" + name + "]");
304                return;
305            } else if (!frame.isVisible()) {
306                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Frame [" + name + "] hidden");
307            } else if (!frame.getAllowInFrameServlet()) {
308                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Frame [" + name + "] not allowed by design");
309                return;
310            }
311        }
312        Map<String, String[]> parameters = this.populateParameterMap(request.getParameterMap());
313        if (frame != null && parameters.containsKey("coords") &&
314            !(parameters.containsKey("protect") && Boolean.parseBoolean(parameters.get("protect")[0]))) { // NOI18N
315            this.doClick(frame, parameters.get("coords")[0]); // NOI18N
316        }
317        if (frame != null && request.getRequestURI().contains(".html")) { // NOI18N
318            this.doHtml(frame, request, response, parameters);
319        } else if (frame != null && request.getRequestURI().contains(".png")) { // NOI18N
320            this.doImage(frame, request, response);
321        } else {
322            this.doList(request, response);
323        }
324    }
325
326    @Override
327    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
328        this.doGet(request, response);
329    }
330
331    private void doHtml(@Nonnull JmriJFrame frame, HttpServletRequest request,
332        @Nonnull HttpServletResponse response, Map<String, String[]> parameters) throws ServletException, IOException {
333        WebServerPreferences preferences = InstanceManager.getDefault(WebServerPreferences.class);
334        Date now = new Date();
335        boolean click = false;
336        boolean useAjax = preferences.isUseAjax();
337        boolean plain = preferences.isSimple();
338        String clickRetryTime = Integer.toString(preferences.getClickDelay());
339        String noclickRetryTime = Integer.toString(preferences.getRefreshDelay());
340        boolean protect = false;
341        if (parameters.containsKey("coords")) { // NOI18N
342            click = true;
343        }
344        if (parameters.containsKey("retry")) { // NOI18N
345            noclickRetryTime = parameters.get("retry")[0]; // NOI18N
346        }
347        if (parameters.containsKey("ajax")) { // NOI18N
348            useAjax = Boolean.parseBoolean(parameters.get("ajax")[0]); // NOI18N
349        }
350        if (parameters.containsKey("plain")) { // NOI18N
351            plain = Boolean.parseBoolean(parameters.get("plain")[0]); // NOI18N
352        }
353        if (parameters.containsKey("protect")) { // NOI18N
354            protect = Boolean.parseBoolean(parameters.get("protect")[0]); // NOI18N
355        }
356        response.setStatus(HttpServletResponse.SC_OK);
357        response.setContentType("text/html"); // NOI18N
358        response.setHeader("Connection", "Keep-Alive"); // NOI18N
359        response.setDateHeader("Date", now.getTime()); // NOI18N
360        response.setDateHeader("Last-Modified", now.getTime()); // NOI18N
361        response.setDateHeader("Expires", now.getTime()); // NOI18N
362        // 0 is host
363        // 1 is frame name  (after escaping special characters)
364        // 2 is retry in META tag, click or noclick retry
365        // 3 is retry in next URL, future retry
366        // 4 is state of plain
367        // 5 is the CSS stylesteet name addition, based on "plain"
368        // 6 is ajax preference
369        // 7 is protect
370        Object[] args = new String[]{"localhost", // NOI18N
371            URLEncoder.encode(frame.getTitle(), UTF8),
372            (click ? clickRetryTime : noclickRetryTime),
373            noclickRetryTime,
374            Boolean.toString(plain),
375            (plain ? "-plain" : ""), // NOI18N
376            Boolean.toString(useAjax),
377            Boolean.toString(protect)};
378        response.getWriter().write(Bundle.getMessage(request.getLocale(), "FrameDocType")); // NOI18N
379        response.getWriter().write(MessageFormat.format(Bundle.getMessage(request.getLocale(), "FramePart1"), args)); // NOI18N
380        if (useAjax) {
381            response.getWriter().write(MessageFormat.format(Bundle.getMessage(request.getLocale(), "FramePart2Ajax"), args)); // NOI18N
382        } else {
383            response.getWriter().write(MessageFormat.format(Bundle.getMessage(request.getLocale(), "FramePart2NonAjax"), args)); // NOI18N
384        }
385        response.getWriter().write(MessageFormat.format(Bundle.getMessage(request.getLocale(), "FrameFooter"), args)); // NOI18N
386
387        log.debug("Sent jframe html with click={}", (click ? "True" : "False"));
388    }
389
390    private void doImage(@Nonnull JmriJFrame frame, HttpServletRequest request,
391        @Nonnull HttpServletResponse response) throws ServletException, IOException {
392        Date now = new Date();
393        response.setStatus(HttpServletResponse.SC_OK);
394        response.setContentType("image/png"); // NOI18N
395        response.setDateHeader("Date", now.getTime()); // NOI18N
396        response.setDateHeader("Last-Modified", now.getTime()); // NOI18N
397        response.setHeader("Cache-Control", "no-cache"); // NOI18N
398        response.setHeader("Connection", "Keep-Alive"); // NOI18N
399        response.setHeader("Keep-Alive", "timeout=5, max=100"); // NOI18N
400        BufferedImage image = new BufferedImage(frame.getContentPane().getWidth(),
401                frame.getContentPane().getHeight(),
402                BufferedImage.TYPE_INT_RGB);
403        frame.getContentPane().paint(image.createGraphics());
404
405        doDialog(getDialog(frame), image);
406
407        //put it in a temp file to get post-compression size
408        ByteArrayOutputStream tmpFile = new ByteArrayOutputStream();
409        ImageIO.write(image, "png", tmpFile); // NOI18N
410        tmpFile.close();
411        response.setContentLength(tmpFile.size());
412        response.getOutputStream().write(tmpFile.toByteArray());
413        log.debug("Sent [{}] as {} byte png.", frame.getTitle(), tmpFile.size());
414    }
415
416    private void doDialog(@CheckForNull JDialog dialog, @Nonnull BufferedImage image){
417        if ( dialog == null ) {
418            return;
419        }
420        log.debug("dialog {}", dialog);
421
422        BufferedImage dImage = new BufferedImage(dialog.getContentPane().getWidth(),
423        dialog.getContentPane().getHeight(), BufferedImage.TYPE_INT_RGB);
424        dialog.getContentPane().paint(dImage.createGraphics());
425        image.getGraphics().drawImage(dImage, 0, 20, null);
426
427        Graphics2D g = (Graphics2D)image.getGraphics();
428
429        g.setColor(Color.WHITE);
430        g.fillRect(0, 0, dialog.getContentPane().getWidth(), 20);
431
432        g.setColor(Color.DARK_GRAY );
433        g.drawRect(0, 0, dialog.getContentPane().getWidth(), dialog.getContentPane().getHeight()+20);
434
435        RenderingHints hints =new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
436        g.setRenderingHints(hints);
437        g.drawString(dialog.getTitle(), 10, 15);
438    }
439
440    private void doList(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response) throws ServletException, IOException {
441        List<String> disallowedFrames = Arrays.asList(InstanceManager.getDefault(WebServerPreferences.class).getDisallowedFrames());
442        String format = request.getParameter("format"); // NOI18N
443        ObjectMapper mapper = new ObjectMapper();
444        Date now = new Date();
445        boolean usePanels = Boolean.parseBoolean(request.getParameter(JSON.PANELS));
446        response.setStatus(HttpServletResponse.SC_OK);
447        if ("json".equals(format)) { // NOI18N
448            response.setContentType("application/json"); // NOI18N
449        } else {
450            response.setContentType("text/html"); // NOI18N
451        }
452        response.setHeader("Connection", "Keep-Alive"); // NOI18N
453        response.setDateHeader("Date", now.getTime()); // NOI18N
454        response.setDateHeader("Last-Modified", now.getTime()); // NOI18N
455        response.setDateHeader("Expires", now.getTime()); // NOI18N
456
457        if ("json".equals(format)) { // NOI18N
458            ArrayNode root = mapper.createArrayNode();
459            HashSet<JFrame> frames = new HashSet<>();
460            JsonUtilHttpService service = new JsonUtilHttpService(new ObjectMapper());
461            for (JmriJFrame frame : JmriJFrame.getFrameList()) {
462                if (frame == null) {
463                    continue;
464                }
465                if (usePanels && frame instanceof Editor) {
466                    ObjectNode node = service.getPanel((Editor) frame, JSON.XML, 0);
467                    if (node != null) {
468                        root.add(node);
469                        frames.add(((Editor) frame).getTargetFrame());
470                    }
471                } else {
472                    String title = frame.getTitle();
473                    if (!title.isEmpty()
474                            && frame.getAllowInFrameServlet()
475                            && !disallowedFrames.contains(title)
476                            && !frames.contains(frame)
477                            && frame.isVisible()) {
478                        ObjectNode node = mapper.createObjectNode();
479                        try {
480                            node.put(NAME, title);
481                            node.put(URL, "/frame/" + URLEncoder.encode(title, UTF8) + ".html"); // NOI18N
482                            node.put("png", "/frame/" + URLEncoder.encode(title, UTF8) + ".png"); // NOI18N
483                            root.add(node);
484                            frames.add(frame);
485                        } catch (UnsupportedEncodingException ex) {
486                            JsonException je = new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to encode panel title \"" + title + "\"", 0);
487                            response.sendError(je.getCode(), mapper.writeValueAsString(je.getJsonMessage()));
488                            return;
489                        }
490                    }
491                }
492            }
493            response.getWriter().write(mapper.writeValueAsString(root));
494        } else {
495            response.getWriter().append(Bundle.getMessage(request.getLocale(), "FrameDocType")); // NOI18N
496            response.getWriter().append(Bundle.getMessage(request.getLocale(), "ListFront")); // NOI18N
497            response.getWriter().write(Bundle.getMessage(request.getLocale(), "TableHeader")); // NOI18N
498            // list frames, (open JMRI windows)
499            for (JmriJFrame frame : JmriJFrame.getFrameList()) {
500                String title = frame.getTitle();
501                //don't add to list if blank or disallowed
502                if (!title.isEmpty() && frame.getAllowInFrameServlet() && !disallowedFrames.contains(title) && frame.isVisible()) {
503                    String link = "/frame/" + URLEncoder.encode(title, UTF8) + ".html"; // NOI18N
504                    //format a table row for each valid window (frame)
505                    response.getWriter().append("<tr><td><a href='" + link + "'>"); // NOI18N
506                    response.getWriter().append(title);
507                    response.getWriter().append("</a></td>"); // NOI18N
508                    response.getWriter().append("<td><a href='");
509                    response.getWriter().append(link);
510                    response.getWriter().append("'><img src='"); // NOI18N
511                    response.getWriter().append("/frame/" + URLEncoder.encode(title, UTF8) + ".png"); // NOI18N
512                    response.getWriter().append("'></a></td></tr>\n"); // NOI18N
513                }
514            }
515            response.getWriter().append("</table>"); // NOI18N
516            response.getWriter().append(Bundle.getMessage(request.getLocale(), "ListFooter")); // NOI18N
517        }
518    }
519
520    // Requests for frames are always /frame/<name>.html or /frame/<name>.png
521    private String getFrameName(@Nonnull String uri) throws UnsupportedEncodingException {
522        if (!uri.contains(".")) {
523            return null;
524        } else {
525            // if request contains parameters, strip those off
526            int stop = (uri.contains("?")) ? uri.indexOf('?') : uri.length(); // NOI18N
527            String name = uri.substring(uri.lastIndexOf('/'), stop); // NOI18N
528            // URI contains a leading / at this point
529            name = name.substring(1, name.lastIndexOf('.')); // NOI18N
530            name = URLDecoder.decode(name, UTF8); //undo escaped characters
531            log.debug("Frame name is {}", name); // NOI18N
532            return name;
533        }
534    }
535
536    // The HttpServeletRequest does not like image maps, so we need to process
537    // the parameter names to see if an image map was clicked
538    protected Map<String, String[]> populateParameterMap(@Nonnull Map<String, String[]> map) {
539        Map<String, String[]> parameters = new HashMap<>();
540        map.entrySet().stream().forEach((entry) -> {
541            String[] value = entry.getValue();
542            String key = entry.getKey();
543            if (value[0].contains("?")) { // NOI18N
544                // a user's click is in another key's value
545                String[] values = value[0].split("\\?"); // NOI18N
546                if (values[0].contains(",")) {
547                    parameters.put(key, new String[]{values[1]});
548                    parameters.put("coords", new String[]{values[0]}); // NOI18N
549                } else {
550                    parameters.put(key, new String[]{values[0]});
551                    parameters.put("coords", new String[]{values[1]}); // NOI18N
552                }
553            } else if (key.contains(",")) { // NOI18N
554                // we have a user's click
555                String[] coords = new String[1];
556                if (key.contains("?")) { // NOI18N
557                    // the key is combined
558                    coords[0] = key.substring(key.indexOf("?")); // NOI18N
559                    key = key.substring(0, key.indexOf("?") - 1); // NOI18N
560                    parameters.put(key, value);
561                } else {
562                    coords[0] = key;
563                }
564                log.debug("Setting click coords to {}", coords[0]);
565                parameters.put("coords", coords); // NOI18N
566            } else {
567                parameters.put(key, value);
568            }
569        });
570        return parameters;
571    }
572
573    private void doClick(@Nonnull JmriJFrame frame, @Nonnull String coords) {
574        String[] click = coords.split(","); // NOI18N
575        int x = Integer.parseInt(click[0]);
576        int y = Integer.parseInt(click[1]);
577
578        JDialog dialog = getDialog(frame);
579        if ( dialog != null ) {
580            y -= 20; // offset dialog title
581            Component cc = dialog.getContentPane().findComponentAt(x, y);
582            if ( cc != null ){
583                log.debug("click dialog {} at x:{} y:{} component:{}",dialog.getTitle(),x,y, cc);
584                sendClick(frame.getTitle(), cc, x, y, dialog.getContentPane());
585            }
586            return;
587        }
588
589        //send click to topmost component under click spot
590        Component c = frame.getContentPane().findComponentAt(x, y);
591        if ( c == null ) { // click outside of Frame
592            return;
593        }
594        //log.debug("topmost component is class={}", c.getClass().getName());
595        sendClick(frame.getTitle(), c, x, y, frame.getContentPane());
596
597        //if clicked on background, search for layout editor target pane TODO: simplify id'ing background
598        if (!c.getClass().getName().equals("jmri.jmrit.display.Editor$TargetPane") // NOI18N
599                && (c instanceof jmri.jmrit.display.PositionableLabel)
600                && !(c instanceof jmri.jmrit.display.LightIcon)
601                && !(c instanceof jmri.jmrit.display.LocoIcon)
602                && !(c instanceof jmri.jmrit.display.MemoryOrGVIcon)
603                && !(c instanceof jmri.jmrit.display.MultiSensorIcon)
604                && !(c instanceof jmri.jmrit.display.PositionableIcon)
605                && !(c instanceof jmri.jmrit.display.ReporterIcon)
606                && !(c instanceof jmri.jmrit.display.RpsPositionIcon)
607                && !(c instanceof jmri.jmrit.display.SlipTurnoutIcon)
608                && !(c instanceof jmri.jmrit.display.TurnoutIcon)) {
609            clickOnEditorPane(frame.getContentPane(), x, y, frame);
610        }
611    }
612
613    //recursively search components to find editor target pane, where layout editor paints components
614    public void clickOnEditorPane(@Nonnull Component c, int x, int y, JmriJFrame f) {
615
616        if (c.getClass().getName().equals("jmri.jmrit.display.Editor$TargetPane")) { // NOI18N
617            log.debug("Sending additional click to Editor$TargetPane");
618            //then click on it
619            sendClick(f.getTitle(), c, x, y, f);
620
621            //keep looking
622        } else if (c instanceof Container) {
623            //check this component's children
624            for (Component child : ((Container) c).getComponents()) {
625                clickOnEditorPane(child, x, y, f);
626            }
627        }
628    }
629
630    @CheckForNull
631    private static JDialog getDialog(@Nonnull JmriJFrame frame) {
632        for ( var pcl : frame.getPropertyChangeListeners() ) {
633            log.debug("PCL : {}", pcl);
634            if ( pcl instanceof JDialogListener ){
635                return ((JDialogListener) pcl).getDialog();
636            }
637        }
638        return null;
639    }
640
641    private static final Logger log = LoggerFactory.getLogger(JmriJFrameServlet.class);
642}