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}