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}