001package jmri.server.web.app; 002 003import static jmri.server.json.JSON.NAME; 004import static jmri.server.json.JSON.VALUE; 005import static jmri.web.servlet.ServletUtil.UTF8_APPLICATION_JAVASCRIPT; 006import static jmri.web.servlet.ServletUtil.UTF8_APPLICATION_JSON; 007import static jmri.web.servlet.ServletUtil.UTF8_TEXT_HTML; 008 009import com.fasterxml.jackson.databind.JsonNode; 010import com.fasterxml.jackson.databind.ObjectMapper; 011import com.fasterxml.jackson.databind.node.ArrayNode; 012import com.fasterxml.jackson.databind.node.ObjectNode; 013import java.io.File; 014import java.io.IOException; 015import java.net.URI; 016import java.util.Iterator; 017import java.util.Locale; 018import java.util.Map.Entry; 019import javax.servlet.ServletException; 020import javax.servlet.annotation.WebServlet; 021import javax.servlet.http.HttpServlet; 022import javax.servlet.http.HttpServletRequest; 023import javax.servlet.http.HttpServletResponse; 024import jmri.Application; 025import jmri.InstanceManager; 026import jmri.Version; 027import jmri.jmrix.ConnectionConfig; 028import jmri.jmrix.ConnectionConfigManager; 029import jmri.profile.Profile; 030import jmri.profile.ProfileManager; 031import jmri.profile.ProfileUtils; 032import jmri.util.FileUtil; 033import jmri.web.server.WebServerPreferences; 034import jmri.web.servlet.ServletUtil; 035import org.openide.util.lookup.ServiceProvider; 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039/** 040 * Dynamic content for the Angular JMRI web application. 041 * 042 * @author Randall Wood (C) 2016 043 */ 044@WebServlet(name = "AppDynamicServlet", urlPatterns = { 045 "/app", 046 "/app/script", 047 "/app/about" 048}) 049@ServiceProvider(service = HttpServlet.class) 050public class WebAppServlet extends HttpServlet { 051 052 private final static Logger log = LoggerFactory.getLogger(WebAppServlet.class); 053 054 /** 055 * Processes requests for both HTTP <code>GET</code> and <code>POST</code> 056 * methods. 057 * 058 * @param request servlet request 059 * @param response servlet response 060 * @throws ServletException if a servlet-specific error occurs 061 * @throws IOException if an I/O error occurs 062 */ 063 protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 064 log.debug("App contextPath: {}, pathInfo: {}", request.getContextPath(), request.getPathInfo()); 065 response.setHeader("Connection", "Keep-Alive"); // NOI18N 066 switch (request.getContextPath()) { 067 case "/app": // NOI18N 068 if (request.getPathInfo().startsWith("/locale-")) { // NOI18N 069 this.processLocale(request, response); 070 } else { 071 this.processApp(request, response); 072 } 073 break; 074 case "/app/about": // NOI18N 075 this.processAbout(request, response); 076 break; 077 case "/app/script": // NOI18N 078 this.processScript(request, response); 079 break; 080 default: 081 } 082 } 083 084 private void processApp(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 085 response.setContentType(UTF8_TEXT_HTML); 086 Profile profile = ProfileManager.getDefault().getActiveProfile(); 087 File cache = new File(ProfileUtils.getCacheDirectory(profile, this.getClass()), request.getLocale().toString()); 088 FileUtil.createDirectory(cache); 089 File index = new File(cache, "index.html"); // NOI18N 090 if (!index.exists()) { 091 String inComments = "-->%n%s<!--"; // NOI18N 092 WebAppManager manager = getWebAppManager(); 093 // Format elements for index.html 094 // 1 = railroad name 095 // 2 = scripts (in comments) 096 // 3 = stylesheets (in comments) 097 // 4 = body content (divs) 098 // 5 = help menu contents (in comments) 099 // 6 = personal menu contents (in comments) 100 FileUtil.appendTextToFile(index, String.format(request.getLocale(), 101 FileUtil.readURL(FileUtil.findURL("web/app/app/index.html")), 102 InstanceManager.getDefault(ServletUtil.class).getRailroadName(false), // railroad name 103 String.format(inComments, manager.getScriptTags(profile)), // scripts (in comments) 104 String.format(inComments, manager.getStyleTags(profile)), // stylesheets (in comments) 105 "<!-- -->", // body content (divs) 106 String.format(inComments, manager.getHelpMenuItems(profile, request.getLocale())), // help menu contents (in comments) 107 String.format(inComments, manager.getUserMenuItems(profile, request.getLocale())) // personal menu contents (in comments) 108 )); 109 } 110 response.getWriter().print(FileUtil.readFile(index)); 111 } 112 113 private void processAbout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 114 response.setContentType(UTF8_APPLICATION_JSON); 115 Profile profile = ProfileManager.getDefault().getActiveProfile(); 116 ObjectMapper mapper = new ObjectMapper(); 117 ObjectNode about = mapper.createObjectNode(); 118 about.put("additionalInfo", Bundle.getMessage(request.getLocale(), "AdditionalInfo", Application.getApplicationName())); // NOI18N 119 about.put("copyright", Version.getCopyright()); // NOI18N 120 about.put("title", InstanceManager.getDefault(WebServerPreferences.class).getRailroadName()); // NOI18N 121 about.put("imgAlt", Application.getApplicationName()); // NOI18N 122 // assuming Application.getLogo() is relative to program: 123 about.put("imgSrc", "/" + Application.getLogo()); // NOI18N 124 ArrayNode productInfo = about.putArray("productInfo"); // NOI18N 125 productInfo.add(mapper.createObjectNode().put(NAME, Application.getApplicationName()).put(VALUE, Version.name())); 126 if (profile != null) { 127 productInfo.add(mapper.createObjectNode().put(NAME, Bundle.getMessage(request.getLocale(), "ActiveProfile")).put(VALUE, profile.getName())); // NOI18N 128 } 129 productInfo.add(mapper.createObjectNode() 130 .put(NAME, "Java") // NOI18N 131 .put(VALUE, Bundle.getMessage(request.getLocale(), "JavaVersion", 132 System.getProperty("java.version", Bundle.getMessage(request.getLocale(), "Unknown")), // NOI18N 133 System.getProperty("java.vm.name", Bundle.getMessage(request.getLocale(), "Unknown")), // NOI18N 134 System.getProperty("java.vm.version", ""), // NOI18N 135 System.getProperty("java.vendor", Bundle.getMessage(request.getLocale(), "Unknown")) // NOI18N 136 ))); 137 productInfo.add(mapper.createObjectNode() 138 .put(NAME, Bundle.getMessage(request.getLocale(), "Runtime")) 139 .put(VALUE, Bundle.getMessage(request.getLocale(), "RuntimeVersion", 140 System.getProperty("java.runtime.name", Bundle.getMessage(request.getLocale(), "Unknown")), // NOI18N 141 System.getProperty("java.runtime.version", "") // NOI18N 142 ))); 143 for (ConnectionConfig conn : InstanceManager.getDefault(ConnectionConfigManager.class)) { 144 if (!conn.getDisabled()) { 145 productInfo.add(mapper.createObjectNode() 146 .put(NAME, Bundle.getMessage(request.getLocale(), "ConnectionName", conn.getConnectionName())) 147 .put(VALUE, Bundle.getMessage(request.getLocale(), "ConnectionValue", conn.name(), conn.getInfo()))); 148 } 149 } 150 response.getWriter().print(mapper.writeValueAsString(about)); 151 } 152 153 private void processScript(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 154 response.setContentType(UTF8_APPLICATION_JAVASCRIPT); 155 Profile profile = ProfileManager.getDefault().getActiveProfile(); 156 File cache = new File(ProfileUtils.getCacheDirectory(profile, this.getClass()), request.getLocale().toString()); 157 FileUtil.createDirectory(cache); 158 File script = new File(cache, "script.js"); // NOI18N 159 if (!script.exists()) { 160 WebAppManager manager = getWebAppManager(); 161 FileUtil.appendTextToFile(script, String.format(request.getLocale(), 162 FileUtil.readURL(FileUtil.findURL("web/app/app/script.js")), // NOI18N 163 manager.getAngularDependencies(profile, request.getLocale()), 164 manager.getAngularRoutes(profile, request.getLocale()), 165 String.format("%n $scope.navigationItems = %s;%n", manager.getNavigation(profile, request.getLocale())), // NOI18N 166 manager.getAngularSources(profile, request.getLocale()), 167 InstanceManager.getDefault(WebServerPreferences.class).getRailroadName() 168 )); 169 } 170 response.getWriter().print(FileUtil.readFile(script)); 171 } 172 173 @SuppressWarnings("deprecation") // The constructor Locale(String) is deprecated since version 19 174 // The replacement Locale.of(String) isn't available before version 19 175 private void processLocale(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 176 response.setContentType(UTF8_APPLICATION_JSON); 177 Profile profile = ProfileManager.getDefault().getActiveProfile(); 178 // the locale is the file name portion between "locale-" and ".json" 179 Locale locale = new Locale(request.getPathInfo().substring(8, request.getPathInfo().length() - 5)); 180 File cache = new File(ProfileUtils.getCacheDirectory(profile, this.getClass()), locale.toString()); 181 FileUtil.createDirectory(cache); 182 File file = new File(cache, "locale.json"); // NOI18N 183 if (!file.exists()) { 184 WebAppManager manager = getWebAppManager(); 185 ObjectMapper mapper = new ObjectMapper(); 186 ObjectNode translation = mapper.createObjectNode(); 187 for (URI url : manager.getPreloadedTranslations(profile, locale)) { 188 log.debug("Reading {}", url); 189 JsonNode translations = mapper.readTree(url.toURL()); 190 log.debug("Read {}", translations); 191 if (translations.isObject()) { 192 log.debug("Adding {}", translations); 193 Iterator<Entry<String, JsonNode>> fields = translations.fields(); 194 fields.forEachRemaining((field) -> { 195 translation.set(field.getKey(), field.getValue()); 196 }); 197 } 198 } 199 log.debug("Writing {}", translation); 200 mapper.writeValue(file, translation); 201 } 202 response.getWriter().print(FileUtil.readFile(file)); 203 } 204 205 private WebAppManager getWebAppManager() throws ServletException { 206 return InstanceManager.getOptionalDefault(WebAppManager.class).orElseThrow(ServletException::new); 207 } 208 209 // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code."> 210 /** 211 * Handles the HTTP <code>GET</code> method. 212 * 213 * @param request servlet request 214 * @param response servlet response 215 * @throws ServletException if a servlet-specific error occurs 216 * @throws IOException if an I/O error occurs 217 */ 218 @Override 219 protected void doGet(HttpServletRequest request, HttpServletResponse response) 220 throws ServletException, IOException { 221 processRequest(request, response); 222 } 223 224 /** 225 * Handles the HTTP <code>POST</code> method. 226 * 227 * @param request servlet request 228 * @param response servlet response 229 * @throws ServletException if a servlet-specific error occurs 230 * @throws IOException if an I/O error occurs 231 */ 232 @Override 233 protected void doPost(HttpServletRequest request, HttpServletResponse response) 234 throws ServletException, IOException { 235 processRequest(request, response); 236 } 237 238 /** 239 * Returns a short description of the servlet. 240 * 241 * @return a String containing servlet description 242 */ 243 @Override 244 public String getServletInfo() { 245 return "JMRI Web App support"; 246 }// </editor-fold> 247 248}