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}