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    private void processLocale(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
174        response.setContentType(UTF8_APPLICATION_JSON);
175        Profile profile = ProfileManager.getDefault().getActiveProfile();
176        // the locale is the file name portion between "locale-" and ".json"
177        Locale locale = new Locale(request.getPathInfo().substring(8, request.getPathInfo().length() - 5));
178        File cache = new File(ProfileUtils.getCacheDirectory(profile, this.getClass()), locale.toString());
179        FileUtil.createDirectory(cache);
180        File file = new File(cache, "locale.json"); // NOI18N
181        if (!file.exists()) {
182            WebAppManager manager = getWebAppManager();
183            ObjectMapper mapper = new ObjectMapper();
184            ObjectNode translation = mapper.createObjectNode();
185            for (URI url : manager.getPreloadedTranslations(profile, locale)) {
186                log.debug("Reading {}", url);
187                JsonNode translations = mapper.readTree(url.toURL());
188                log.debug("Read {}", translations);
189                if (translations.isObject()) {
190                    log.debug("Adding {}", translations);
191                    Iterator<Entry<String, JsonNode>> fields = translations.fields();
192                    fields.forEachRemaining((field) -> {
193                        translation.set(field.getKey(), field.getValue());
194                    });
195                }
196            }
197            log.debug("Writing {}", translation);
198            mapper.writeValue(file, translation);
199        }
200        response.getWriter().print(FileUtil.readFile(file));
201    }
202
203    private WebAppManager getWebAppManager() throws ServletException {
204        return InstanceManager.getOptionalDefault(WebAppManager.class).orElseThrow(ServletException::new);
205    }
206
207    // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
208    /**
209     * Handles the HTTP <code>GET</code> method.
210     *
211     * @param request  servlet request
212     * @param response servlet response
213     * @throws ServletException if a servlet-specific error occurs
214     * @throws IOException      if an I/O error occurs
215     */
216    @Override
217    protected void doGet(HttpServletRequest request, HttpServletResponse response)
218            throws ServletException, IOException {
219        processRequest(request, response);
220    }
221
222    /**
223     * Handles the HTTP <code>POST</code> method.
224     *
225     * @param request  servlet request
226     * @param response servlet response
227     * @throws ServletException if a servlet-specific error occurs
228     * @throws IOException      if an I/O error occurs
229     */
230    @Override
231    protected void doPost(HttpServletRequest request, HttpServletResponse response)
232            throws ServletException, IOException {
233        processRequest(request, response);
234    }
235
236    /**
237     * Returns a short description of the servlet.
238     *
239     * @return a String containing servlet description
240     */
241    @Override
242    public String getServletInfo() {
243        return "JMRI Web App support";
244    }// </editor-fold>
245
246}