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