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