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}