001package jmri.web.servlet;
002
003import java.io.File;
004import java.io.FileInputStream;
005import java.io.IOException;
006import java.nio.charset.StandardCharsets;
007import java.util.Date;
008import java.util.Locale;
009import javax.servlet.http.HttpServletResponse;
010import jmri.InstanceManager;
011import jmri.InstanceManagerAutoDefault;
012import jmri.util.FileUtil;
013import jmri.web.server.WebServerPreferences;
014
015/**
016 * Utility methods to reduce code duplication in servlets.
017 *
018 * @author Randall Wood
019 */
020public class ServletUtil implements InstanceManagerAutoDefault {
021
022    public static final String UTF8 = StandardCharsets.UTF_8.toString(); // NOI18N
023    // media types
024    public static final String APPLICATION_JAVASCRIPT = "application/javascript"; // NOI18N
025    public static final String APPLICATION_JSON = "application/json"; // NOI18N
026    public static final String APPLICATION_XML = "application/xml"; // NOI18N
027    public static final String IMAGE_PNG = "image/png"; // NOI18N
028    public static final String TEXT_HTML = "text/html"; // NOI18N
029    public static final String UTF8_CHARSET = "; charset=utf-8"; // NOI18N
030    public static final String UTF8_APPLICATION_JAVASCRIPT = APPLICATION_JAVASCRIPT + UTF8_CHARSET; // NOI18N
031    public static final String UTF8_APPLICATION_JSON = APPLICATION_JSON + UTF8_CHARSET; // NOI18N
032    public static final String UTF8_APPLICATION_XML = APPLICATION_XML + UTF8_CHARSET; // NOI18N
033    public static final String UTF8_TEXT_HTML = TEXT_HTML + UTF8_CHARSET; // NOI18N
034    public static final String HIDDEN = "hidden"; // NOI18N
035
036    /**
037     * Get the railroad name for HTML documents.
038     *
039     * @param inComments Return the railroad name prepended and appended by
040     *                   closing and opening comment markers
041     * @return the Railroad name, possibly with formatting
042     */
043    public String getRailroadName(boolean inComments) {
044        if (inComments) {
045            return "-->" + InstanceManager.getDefault(WebServerPreferences.class).getRailroadName() + "<!--"; // NOI18N
046        }
047        return InstanceManager.getDefault(WebServerPreferences.class).getRailroadName();
048    }
049
050    /**
051     * Create a common footer.
052     *
053     * @param locale  If a template is not available in locale, will return US
054     *                English.
055     * @param context divs included in footer template with class
056     *                {@code context-<context>-only} will be shown.
057     * @return an HTML footer
058     * @throws IOException if template cannot be located
059     */
060    public String getFooter(Locale locale, String context) throws IOException {
061        // Should return a built NavBar with li class for current context set to "active"
062        String footer = String.format(locale, String.format(locale, "-->%s<!--", // NOI18N
063                FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(locale, "Footer.html")))), // NOI18N
064                this.getRailroadName(true));
065        String clazz = "context" + context.replace("/", "-"); // NOI18N
066        // replace class "context-<this-context>-only" with class "show"
067        footer = footer.replace(clazz + "-only", "show"); // NOI18N
068        // replace class "context-<some-other-context>-only" with class "hidden"
069        footer = footer.replaceAll("context-[\\w-]*-only", HIDDEN); // NOI18N
070        // replace class "context-<this-context>" with class "active"
071        footer = footer.replace(clazz, "active"); // NOI18N
072        return footer;
073    }
074
075    /**
076     * Create a common navigation header.
077     *
078     * @param locale  If a template is not available in locale, will return US
079     *                English.
080     * @param context divs included in navigation bar template with class
081     *                {@code context-<context>-only} will be shown.
082     * @return an HTML navigation bar
083     * @throws IOException if template cannot be located
084     */
085    public String getNavBar(Locale locale, String context) throws IOException {
086        // Should return a built NavBar with li class for current context set to "active"
087        String navBar = String.format(locale, String.format(locale, "-->%s<!--", // NOI18N
088                FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(locale, "NavBar.html")))), // NOI18N
089                this.getRailroadName(true));
090        String clazz = "context" + context.replace("/", "-"); // NOI18N
091        // replace class "context-<this-context>-only" with class "show"
092        navBar = navBar.replace(clazz + "-only", "show"); // NOI18N
093        // replace class "context-<some-other-context>-only" with class "hidden"
094        navBar = navBar.replaceAll("context-[\\w-]*-only", HIDDEN); // NOI18N
095        // replace class "context-<this-context>" with class "active"
096        navBar = navBar.replace(clazz, "active"); // NOI18N
097        if (InstanceManager.getDefault(WebServerPreferences.class).allowRemoteConfig()) {
098            navBar = navBar.replace("config-enabled-only", "show"); // NOI18N
099            navBar = navBar.replace("config-disabled-only", HIDDEN); // NOI18N
100        } else {
101            navBar = navBar.replace("config-enabled-only", HIDDEN); // NOI18N
102            navBar = navBar.replace("config-disabled-only", "show"); // NOI18N
103        }
104        if (!InstanceManager.getDefault(WebServerPreferences.class).isReadonlyPower()) {
105            navBar = navBar.replace("data-power=\"readonly\"", "data-power=\"readwrite\""); // NOI18N
106        }
107        return navBar;
108    }
109
110    /**
111     * Set HTTP headers to prevent caching.
112     *
113     * @param response the response to set headers in
114     * @return the date used for headers setting expiration and modification times
115     */
116    public Date setNonCachingHeaders(HttpServletResponse response) {
117        Date now = new Date();
118        response.setDateHeader("Date", now.getTime()); // NOI18N
119        response.setDateHeader("Last-Modified", now.getTime()); // NOI18N
120        response.setDateHeader("Expires", now.getTime()); // NOI18N
121        response.setHeader("Cache-control", "no-cache, no-store"); // NOI18N
122        response.setHeader("Pragma", "no-cache"); // NOI18N
123        return now;
124    }
125
126    /**
127     * Write a file to the given response.
128     *
129     * @param response    the response to write the file into
130     * @param file        file to write
131     * @param contentType file mime content type
132     * @throws java.io.IOException if communications lost with client
133     */
134    public void writeFile(HttpServletResponse response, File file, String contentType) throws IOException {
135        if (file.exists()) {
136            if (file.canRead()) {
137                response.setContentType(contentType);
138                response.setStatus(HttpServletResponse.SC_OK);
139                response.setContentLength((int) file.length());
140                try (FileInputStream fileInputStream = new FileInputStream(file)) {
141                    int bytes = fileInputStream.read();
142                    while (bytes != -1) {
143                        response.getOutputStream().write(bytes);
144                        bytes = fileInputStream.read();
145                    }
146                }
147            } else {
148                response.sendError(HttpServletResponse.SC_FORBIDDEN);
149            }
150        } else {
151            response.sendError(HttpServletResponse.SC_NOT_FOUND);
152        }
153    }
154
155    /**
156     * Return the complete title for an HTML document given the portion of the
157     * title specific to the document.
158     *
159     * @param locale The requested Locale
160     * @param title  Portion of title specific to page
161     * @return The complete title
162     */
163    public String getTitle(Locale locale, String title) {
164        return String.format(Bundle.getMessage(locale, "HtmlTitle"), this.getRailroadName(false), title);
165    }
166}