001package jmri.web.servlet.panel;
002
003import static jmri.web.servlet.ServletUtil.IMAGE_PNG;
004import static jmri.web.servlet.ServletUtil.UTF8;
005import static jmri.web.servlet.ServletUtil.UTF8_APPLICATION_JSON;
006import static jmri.web.servlet.ServletUtil.UTF8_TEXT_HTML;
007
008import com.fasterxml.jackson.databind.ObjectMapper;
009import com.fasterxml.jackson.databind.SerializationFeature;
010
011import java.awt.image.BufferedImage;
012import java.io.ByteArrayOutputStream;
013import java.io.IOException;
014import java.net.URLDecoder;
015import java.net.URLEncoder;
016import java.util.List;
017
018import javax.annotation.CheckForNull;
019import javax.annotation.Nonnull;
020import javax.imageio.ImageIO;
021import javax.servlet.ServletException;
022import javax.servlet.http.HttpServlet;
023import javax.servlet.http.HttpServletRequest;
024import javax.servlet.http.HttpServletResponse;
025import javax.swing.JComponent;
026import javax.swing.JFrame;
027
028import jmri.*;
029import jmri.configurexml.ConfigXmlManager;
030import jmri.jmrit.display.*;
031import jmri.server.json.JSON;
032import jmri.server.json.util.JsonUtilHttpService;
033import jmri.util.FileUtil;
034import jmri.util.ColorUtil;
035import jmri.web.server.WebServer;
036import jmri.web.servlet.ServletUtil;
037import jmri.web.servlet.permission.PermissionServlet;
038
039import org.jdom2.Element;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * Abstract servlet for using panels in browser.
045 * <p>
046 * See JMRI Web Server - Panel Servlet Help in help/en/html/web/PanelServlet.shtml for an example description of
047 * the interaction between the Web Servlets, the Web Browser and the JMRI application.
048 *
049 * @author Randall Wood
050 */
051public abstract class AbstractPanelServlet extends HttpServlet {
052
053    protected ObjectMapper mapper;
054    private static final Logger log = LoggerFactory.getLogger(AbstractPanelServlet.class);
055
056    abstract protected String getPanelType();
057
058    @Override
059    public void init() throws ServletException {
060        if (!this.getServletContext().getContextPath().equals("/web/showPanel.html")) {
061            this.mapper = new ObjectMapper();
062            this.mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
063        }
064    }
065
066    /**
067     * Handle a GET request for a panel.
068     * <p>
069     * The request is processed in this order:
070     * <ol>
071     * <li>If the request contains a parameter {@code name=someValue}, redirect
072     * to {@code /panel/someValue} if {@code someValue} is an open panel,
073     * otherwise redirect to {@code /panel/}.</li>
074     * <li>If the request ends in {@code /}, return an HTML page listing all
075     * open panels.</li>
076     * <li>Return the panel named in the last element in the path in the
077     * following formats based on the {@code format=someFormat} parameter:
078     * <dl>
079     * <dt>html</dt>
080     * <dd>An HTML page rendering the panel.</dd>
081     * <dt>png</dt>
082     * <dd>A PNG image of the panel.</dd>
083     * <dt>json</dt>
084     * <dd>A JSON document of the panel (currently incomplete).</dd>
085     * <dt>xml</dt>
086     * <dd>An XML document of the panel ready to render within a browser.</dd>
087     * </dl>
088     * If {@code format} is not specified, it is treated as {@code html}. All
089     * other formats not listed are treated as {@code xml}.
090     * </li>
091     * </ol>
092     */
093    @Override
094    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
095        log.debug("Handling GET request for {}", request.getRequestURI());
096
097        String sessionId = PermissionServlet.getSessionId(request);
098        if (! InstanceManager.getDefault(PermissionManager.class)
099                .hasAtLeastRemotePermission(sessionId, EditorPermissions.EDITOR_PERMISSION,
100                        EditorPermissions.EditorPermissionEnum.View)) {
101            PermissionServlet.permissionDenied(request, response);
102            return;
103        }
104        if (request.getRequestURI().equals("/web/showPanel.html")) { // NOI18N
105            response.sendRedirect("/panel/"); // NOI18N
106            return;
107        }
108        if (request.getParameter(JSON.NAME) != null) {
109            String panelName = URLDecoder.decode(request.getParameter(JSON.NAME), UTF8);
110            if (getEditor(panelName) != null) {
111                response.sendRedirect("/panel/" + URLEncoder.encode(panelName, UTF8)); // NOI18N
112            } else {
113                response.sendRedirect("/panel/"); // NOI18N
114            }
115        } else if (request.getRequestURI().endsWith("/")) { // NOI18N
116            listPanels(request, response);
117        } else {
118            String[] path = request.getRequestURI().split("/"); // NOI18N
119            String panelName = URLDecoder.decode(path[path.length - 1], UTF8);
120            String format = request.getParameter("format");
121            if (format == null) {
122                this.listPanels(request, response);
123            } else {
124                switch (format) {
125                    case "png":
126                        BufferedImage image = getPanelImage(panelName);
127                        if (image == null) {
128                            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "See the JMRI console for details.");
129                        } else {
130                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
131                            ImageIO.write(image, "png", baos);
132                            baos.close();
133                            response.setContentType(IMAGE_PNG);
134                            response.setStatus(HttpServletResponse.SC_OK);
135                            response.setContentLength(baos.size());
136                            response.getOutputStream().write(baos.toByteArray());
137                            response.getOutputStream().close();
138                        }
139                        break;
140                    case "html":
141                        this.listPanels(request, response);
142                        break;
143                    default: {
144                        boolean useXML = (!JSON.JSON.equals(request.getParameter("format")));
145                        response.setContentType(UTF8_APPLICATION_JSON);
146                        String panel = getPanelText(panelName, useXML);
147                        if (panel == null) {
148                            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "See the JMRI console for details.");
149                        } else if (panel.startsWith("ERROR")) {
150                            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, panel.substring(5).trim());
151                        } else {
152                            response.setStatus(HttpServletResponse.SC_OK);
153                            response.setContentLength(panel.getBytes(UTF8).length);
154                            response.getOutputStream().print(panel);
155                        }
156                        break;
157                    }
158                }
159            }
160        }
161    }
162
163    protected void listPanels(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
164        if (JSON.JSON.equals(request.getParameter("format"))) {
165            response.setContentType(UTF8_APPLICATION_JSON);
166            InstanceManager.getDefault(ServletUtil.class).setNonCachingHeaders(response);
167            JsonUtilHttpService service = new JsonUtilHttpService(new ObjectMapper());
168            response.getWriter().print(service.getPanels(JSON.XML, 0));
169        } else {
170            response.setContentType(UTF8_TEXT_HTML);
171            response.getWriter().print(String.format(request.getLocale(),
172                    FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(request.getLocale(), "Panel.html"))),
173                    String.format(request.getLocale(),
174                            Bundle.getMessage(request.getLocale(), "HtmlTitle"),
175                            InstanceManager.getDefault(ServletUtil.class).getRailroadName(false),
176                            Bundle.getMessage(request.getLocale(), "PanelsTitle")
177                    ),
178                    InstanceManager.getDefault(ServletUtil.class).getNavBar(request.getLocale(), "/panel"),
179                    InstanceManager.getDefault(ServletUtil.class).getRailroadName(false),
180                    InstanceManager.getDefault(ServletUtil.class).getFooter(request.getLocale(), "/panel")
181            ));
182        }
183    }
184
185    protected BufferedImage getPanelImage(String name) {
186        JComponent panel = getPanel(name);
187        if (panel == null) {
188            return null;
189        }
190        BufferedImage bi = new BufferedImage(panel.getWidth(), panel.getHeight(), BufferedImage.TYPE_INT_ARGB);
191        panel.paint(bi.getGraphics());
192        return bi;
193    }
194
195    @CheckForNull
196    protected JComponent getPanel(String name) {
197        Editor editor = getEditor(name);
198        if (editor != null) {
199            return editor.getTargetPanel();
200        }
201        return null;
202    }
203
204    protected String getPanelText(String name, boolean useXML) {
205        if (useXML) {
206            return getXmlPanel(name);
207        } else {
208            return getJsonPanel(name);
209        }
210    }
211
212    abstract protected String getJsonPanel(String name);
213
214    abstract protected String getXmlPanel(String name);
215
216    @CheckForNull
217    protected Editor getEditor(String name) {
218        for (Editor editor : InstanceManager.getDefault(EditorManager.class).getAll()) {
219            JFrame frame = editor.getTargetFrame();
220            if (frame.getTitle().equals(name)) {
221                return editor;
222            }
223        }
224        return null;
225    }
226
227    protected void parsePortableURIs(Element element) {
228        if (element != null) {
229            //loop thru and update attributes of this element if value is a portable filename
230            element.getAttributes().forEach((attr) -> {
231                String value = attr.getValue();
232                if (FileUtil.isPortableFilename(value)) {
233                    String url = WebServer.portablePathToURI(value);
234                    if (url != null) {
235                        // if portable path conversion fails, don't change the value
236                        attr.setValue(url);
237                    }
238                }
239            });
240            //recursively call for each child
241            element.getChildren().forEach((child) -> {
242                parsePortableURIs(child);
243            });
244
245        }
246    }
247
248    /**
249     * Build and return an "icons" element containing icon URLs for all
250     * SignalMast states. Element names are cleaned-up aspect names, aspect
251     * attribute is actual name of aspect.
252     *
253     * @param name user/system name of the signalMast using the icons
254     * @param imageset imageset name or "default"
255     * @return an icons element containing icon URLs for SignalMast states
256     */
257    protected Element getSignalMastIconsElement(String name, String imageset) {
258        Element icons = new Element("icons");
259        SignalMast signalMast = InstanceManager.getDefault(SignalMastManager.class).getSignalMast(name);
260        if (signalMast != null) {
261            final String imgset ;
262            if (imageset == null) {
263                imgset = "default" ;
264            } else {
265                imgset = imageset ;
266            }
267            signalMast.getValidAspects().forEach((aspect) -> {
268                Element ea = new Element(aspect.replaceAll("[ ()]", "")); //create element for aspect after removing invalid chars
269                String url = signalMast.getAppearanceMap().getImageLink(aspect, imgset);  // use correct imageset
270                if (!url.contains("preference:")) {
271                    url = "/" + url.substring(url.indexOf("resources"));
272                }
273                ea.setAttribute(JSON.ASPECT, aspect);
274                ea.setAttribute("url", url);
275                icons.addContent(ea);
276            });
277            String url = signalMast.getAppearanceMap().getImageLink("$held", imgset);  //add "Held" aspect if defined
278            if (!url.isEmpty()) {
279                if (!url.contains("preference:")) {
280                    url = "/" + url.substring(url.indexOf("resources"));
281                }
282                Element ea = new Element(JSON.ASPECT_HELD);
283                ea.setAttribute(JSON.ASPECT, JSON.ASPECT_HELD);
284                ea.setAttribute("url", url);
285                icons.addContent(ea);
286            }
287            url = signalMast.getAppearanceMap().getImageLink("$dark", imgset);  //add "Dark" aspect if defined
288            if (!url.isEmpty()) {
289                if (!url.contains("preference:")) {
290                    url = "/" + url.substring(url.indexOf("resources"));
291                }
292                Element ea = new Element(JSON.ASPECT_DARK);
293                ea.setAttribute(JSON.ASPECT, JSON.ASPECT_DARK);
294                ea.setAttribute("url", url);
295                icons.addContent(ea);
296            }
297            Element ea = new Element(JSON.ASPECT_UNKNOWN);
298            ea.setAttribute(JSON.ASPECT, JSON.ASPECT_UNKNOWN);
299            ea.setAttribute("url", "/resources/icons/misc/X-red.gif");  //add icon for unknown state
300            icons.addContent(ea);
301        }
302        return icons;
303    }
304
305    /**
306     * Build and return a panel state display element containing icon URLs for all states.
307     *
308     * @param sub Positional containing additional icons for display (in MultiSensorIcon)
309     * @return a display element based on element name
310     */
311    protected Element positionableElement(@Nonnull Positionable sub) {
312        Element e = ConfigXmlManager.elementFromObject(sub);
313        if (e != null) {
314            switch (e.getName()) {
315                case "signalmasticon":
316                    e.addContent(getSignalMastIconsElement(e.getAttributeValue("signalmast"),
317                            e.getAttributeValue("imageset")));
318                    break;
319                case "multisensoricon":
320                    if (sub instanceof MultiSensorIcon) {
321                        List<Sensor> sensors = ((MultiSensorIcon) sub).getSensors();
322                        for (Element a : e.getChildren()) {
323                            String s = a.getAttributeValue("sensor");
324                            if (s != null) {
325                                for (Sensor sensor : sensors) {
326                                    if (s.equals(sensor.getUserName())) {
327                                        a.setAttribute("sensor", sensor.getSystemName());
328                                    }
329                                }
330                            }
331                        }
332                    }
333                    break;
334                default:
335                    // nothing to do
336            }
337            if (sub.getNamedBean() != null) {
338                try {
339                    e.setAttribute(JSON.ID, sub.getNamedBean().getSystemName());
340                } catch (NullPointerException ex) {
341                    if (sub.getNamedBean() == null) {
342                        log.debug("{} {} does not have an associated NamedBean", e.getName(), e.getAttribute(JSON.NAME));
343                    } else {
344                        log.debug("{} {} does not have a SystemName", e.getName(), e.getAttribute(JSON.NAME));
345                    }
346                }
347            }
348            parsePortableURIs(e);
349        }
350        return e;
351    }
352
353    /** Convert named colors in a panel response to CSS-compatible RGB hex values. */
354    protected void convertNamedColors(@Nonnull Element element) {
355        for (org.jdom2.Attribute attribute : element.getAttributes()) {
356            if (attribute.getName().toLowerCase(java.util.Locale.ROOT).contains("color")) {
357                String value = attribute.getValue();
358                if (value != null && !value.startsWith("#")) {
359                    try {
360                        java.awt.Color color = ColorUtil.stringToColor(value);
361                        if (color != null) {
362                            attribute.setValue(ColorUtil.colorToHexString(color));
363                        }
364                    } catch (IllegalArgumentException ex) {
365                        // Preserve non-color values such as the special "track" value.
366                    }
367                }
368            }
369        }
370        for (Element child : element.getChildren()) {
371            convertNamedColors(child);
372        }
373    }
374
375}