001/*
002 * #%L
003 * Netarchivesuite - common
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023
024package dk.netarkivet.common.webinterface;
025
026import java.io.IOException;
027import java.io.UnsupportedEncodingException;
028import java.net.URLDecoder;
029import java.net.URLEncoder;
030import java.text.NumberFormat;
031import java.text.ParseException;
032import java.text.SimpleDateFormat;
033import java.util.Date;
034import java.util.Locale;
035
036import javax.servlet.RequestDispatcher;
037import javax.servlet.ServletException;
038import javax.servlet.ServletRequest;
039import javax.servlet.http.Cookie;
040import javax.servlet.http.HttpServletRequest;
041import javax.servlet.jsp.JspWriter;
042import javax.servlet.jsp.PageContext;
043
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047import dk.netarkivet.common.CommonSettings;
048import dk.netarkivet.common.Constants;
049import dk.netarkivet.common.exceptions.ArgumentNotValid;
050import dk.netarkivet.common.exceptions.ForwardedToErrorPage;
051import dk.netarkivet.common.exceptions.IOFailure;
052import dk.netarkivet.common.utils.I18n;
053import dk.netarkivet.common.utils.Settings;
054import dk.netarkivet.common.utils.StringTree;
055
056/**
057 * This is a utility class containing methods for use in the GUI for netarkivet.
058 */
059public class HTMLUtils {
060
061    /** Web page title placeholder. */
062    private static String TITLE_PLACEHOLDER = "STRING_1";
063
064    /** External JavaScript files placeholder. */
065    private static String JS_PLACEHOLDER = "JS_TO_INCLUDE";
066
067    private static String WEBPAGE_HEADER_TEMPLATE_TOP = "<!DOCTYPE html "
068            + "PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \n "
069            + "  \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"
070            + "<html xmlns=\"http://www.w3.org/1999/xhtml\"" + " xml:lang=\"en\" lang=\"en\">\n" + "<head>\n"
071            + "<meta content=\"text/html; charset=UTF-8\" " + "http-equiv= \"content-type\" />"
072            + "<meta http-equiv=\"Expires\" content=\"0\"/>\n"
073            + "<meta http-equiv=\"Cache-Control\" content=\"no-cache\"/>\n"
074            + "<meta http-equiv=\"Pragma\" content=\"no-cache\"/> \n";
075
076    private static String WEBPAGE_HEADER_AUTOREFRESH = "<meta http-equiv=\"refresh\" content=\"" + TITLE_PLACEHOLDER
077            + "\"/> \n";
078
079    private static String WEBPAGE_HEADER_TEMPLATE_BOTTOM = "<title>" + TITLE_PLACEHOLDER + "</title>\n"
080            + "<script type=\"text/javascript\">\n" + "<!--\n" + "function giveFocus() {\n"
081            + "    var e = document.getElementById('focusElement');\n" + "    if (e != null) {\n"
082            + "        var elms = e.getElementsByTagName('*');\n" + "        if (elms != null && elms.length != null "
083            + "            && elms.item != null && elms.length > 0) {\n" + "            var e2 = elms.item(0);\n"
084            + "                if (e2 != null && e2.focus != null) {\n" + "            }\n"
085            + "            e2.focus();\n" + "        }\n" + "    }\n" + "}\n" + "-->\n" + "</script>\n"
086            + JS_PLACEHOLDER + "\n" + "<link rel=\"stylesheet\" href=\"./netarkivet.css\" " + "type=\"text/css\" />\n"
087            + "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" "
088            + "href=\"./jscalendar/calendar-win2k-cold-1.css\" " + "title=\"./jscalendar/win2k-cold-1\" />\n"
089            + "</head> <body onload=\"giveFocus()\">\n";
090
091    /** Logger for this class. */
092    //private static Log log = LogFactory.getLog(HTMLUtils.class.getName());
093    private static final Logger log = LoggerFactory.getLogger(HTMLUtils.class);
094    /** Translations for this module. */
095    private static final I18n I18N = new I18n(Constants.TRANSLATIONS_BUNDLE);
096
097    /**
098     * Private constructor. There is no reason to instantiate this class.
099     */
100    private HTMLUtils() {
101        // Nothing to initialize
102    }
103
104    /**
105     * Url encodes a string in UTF-8. This encodes _all_ non-letter non-number characters except '-', '_' and '.'. The
106     * characters '/' and ':' are encoded.
107     *
108     * @param s the string to encode
109     * @return the encoded string
110     */
111    public static String encode(String s) {
112        ArgumentNotValid.checkNotNull(s, "s");
113        try {
114            return URLEncoder.encode(s, "UTF-8");
115        } catch (UnsupportedEncodingException e) {
116            throw new ArgumentNotValid(URLEncoder.class.getName() + " does not support UTF-8", e);
117        }
118    }
119
120    /**
121     * Url decodes a string encoded in UTF-8.
122     *
123     * @param s the string to decode
124     * @return the decoded string
125     */
126    public static String decode(String s) {
127        ArgumentNotValid.checkNotNull(s, "s");
128        try {
129            return URLDecoder.decode(s, "UTF-8");
130        } catch (UnsupportedEncodingException e) {
131            throw new ArgumentNotValid(URLDecoder.class.getName() + " does not support UTF-8", e);
132        }
133    }
134
135    /**
136     * Prints the header information for the webpages in the GUI. This includes the navigation menu, and links for
137     * changing the language. The title of the page is generated internationalised from sitesections. If you want to
138     * specify it, use the overloaded method.
139     *
140     * @param context The context of the web page request.
141     * @throws IOException if an error occurs during writing of output.
142     */
143    public static void generateHeader(PageContext context) throws IOException {
144        ArgumentNotValid.checkNotNull(context, "context");
145        String url = ((HttpServletRequest) context.getRequest()).getRequestURL().toString();
146        Locale locale = context.getResponse().getLocale();
147        String title = getTitle(url, locale);
148        generateHeader(title, context);
149    }
150
151    /**
152     * Prints the header information for the webpages in the GUI. This includes the navigation menu, and links for
153     * changing the language. The title of the page is generated internationalised from sitesections. If you want to
154     * specify it, use the overloaded method.
155     *
156     * @param context The context of the web page request.
157     * @throws IOException if an error occurs during writing of output.
158     */
159    public static void generateHeader(PageContext context, String... jsToInclude) throws IOException {
160        ArgumentNotValid.checkNotNull(context, "context");
161        String url = ((HttpServletRequest) context.getRequest()).getRequestURL().toString();
162        Locale locale = context.getResponse().getLocale();
163        String title = getTitle(url, locale);
164        generateHeader(title, context, jsToInclude);
165    }
166
167    /**
168     * Prints the header information for the webpages in the GUI. This includes the navigation menu, and links for
169     * changing the language. The title of the page is generated internationalised from sitesections. If you want to
170     * specify it, use the overloaded method.
171     *
172     * @param context The context of the web page request.
173     * @param refreshInSeconds auto-refresh time in seconds
174     * @throws IOException if an error occurs during writing of output.
175     */
176    public static void generateHeader(PageContext context, long refreshInSeconds) throws IOException {
177        ArgumentNotValid.checkNotNull(context, "context");
178        String url = ((HttpServletRequest) context.getRequest()).getRequestURL().toString();
179        Locale locale = context.getResponse().getLocale();
180        String title = getTitle(url, locale);
181        generateHeader(title, refreshInSeconds, context);
182    }
183
184    /**
185     * Prints the header information for the webpages in the GUI. This includes the navigation menu, and links for
186     * changing the language.
187     *
188     * @param title An internationalised title of the page.
189     * @param context The context of the web page request.
190     * @param jsToInclude path(s) to external .js files to include in header.
191     * @throws IOException if an error occurs during writing to output.
192     */
193    public static void generateHeader(String title, PageContext context, String... jsToInclude) throws IOException {
194        ArgumentNotValid.checkNotNull(title, "title");
195        ArgumentNotValid.checkNotNull(context, "context");
196
197        JspWriter out = context.getOut();
198        String url = ((HttpServletRequest) context.getRequest()).getRequestURL().toString();
199        Locale locale = context.getResponse().getLocale();
200        title = escapeHtmlValues(title);
201        log.debug("Loaded URL '" + url + "' with title '" + title + "'");
202        out.print(WEBPAGE_HEADER_TEMPLATE_TOP);
203
204        String includeJs = "";
205        if (jsToInclude != null && jsToInclude.length > 0) {
206            for (String js : jsToInclude) {
207                includeJs += "<script type=\"text/javascript\" src=\"" + js + "\"></script>\n";
208            }
209        }
210
211        out.print(WEBPAGE_HEADER_TEMPLATE_BOTTOM.replace(TITLE_PLACEHOLDER, title).replace(JS_PLACEHOLDER, includeJs));
212        // Start the two column / one row table which fills the page
213        out.print("<table id =\"main_table\"><tr>\n");
214        // fill in data in the left column
215        generateNavigationTree(out, url, locale);
216        // The right column contains the active form content for this page
217        out.print("<td valign = \"top\" >\n");
218        // Language links
219        generateLanguageLinks(out);
220    }
221
222    /**
223     * Prints the header information for the webpages in the GUI. This includes the navigation menu, and links for
224     * changing the language.
225     *
226     * @param title An internationalised title of the page.
227     * @param context The context of the web page request.
228     * @param refreshInSeconds auto-refresh time in seconds
229     * @throws IOException if an error occurs during writing to output.
230     */
231    public static void generateHeader(String title, long refreshInSeconds, PageContext context) throws IOException {
232        ArgumentNotValid.checkNotNull(title, "title");
233        ArgumentNotValid.checkNotNull(context, "context");
234
235        JspWriter out = context.getOut();
236        String url = ((HttpServletRequest) context.getRequest()).getRequestURL().toString();
237        Locale locale = context.getResponse().getLocale();
238        title = escapeHtmlValues(title);
239        log.debug("Loaded URL '" + url + "' with title '" + title + "'");
240        out.print(WEBPAGE_HEADER_TEMPLATE_TOP);
241        if (refreshInSeconds > 0) {
242            out.print(WEBPAGE_HEADER_AUTOREFRESH.replace(TITLE_PLACEHOLDER, Long.toString(refreshInSeconds)));
243        }
244        out.print(WEBPAGE_HEADER_TEMPLATE_BOTTOM.replace(TITLE_PLACEHOLDER, title).replace(JS_PLACEHOLDER, ""));
245        // Start the two column / one row table which fills the page
246        out.print("<table id =\"main_table\"><tr>\n");
247        // fill in data in the left column
248        generateNavigationTree(out, url, locale);
249        // The right column contains the active form content for this page
250        out.print("<td valign = \"top\" >\n");
251        // Language links
252        generateLanguageLinks(out);
253    }
254
255    /**
256     * Get the locale according to header context information.
257     *
258     * @param context The context of the web page request.
259     * @return The locale given in the the page response.
260     */
261    public static Locale getLocaleObject(PageContext context) {
262        ArgumentNotValid.checkNotNull(context, "context");
263        return context.getResponse().getLocale();
264    }
265
266    /**
267     * Prints out links to change languages. Will read locales and names of languages from settings, and write them as
268     * links to the page "lang.jsp". The locale will be given to this page as a parameter, the name will be shown as the
269     * text of the link
270     *
271     * @param out the writer to which the links are written.
272     * @throws IOException if an error occurs during writing of output.
273     */
274    private static void generateLanguageLinks(JspWriter out) throws IOException {
275        out.print("<div class=\"languagelinks\">");
276        StringTree<String> webinterfaceSettings = Settings.getTree(CommonSettings.WEBINTERFACE_SETTINGS);
277
278        for (StringTree<String> language : webinterfaceSettings.getSubTrees(CommonSettings.WEBINTERFACE_LANGUAGE)) {
279            out.print(String.format("<a href=\"lang.jsp?locale=%s&amp;name=%s\">%s</a>&nbsp;",
280                    escapeHtmlValues(encode(language.getValue(CommonSettings.WEBINTERFACE_LANGUAGE_LOCALE))),
281                    escapeHtmlValues(encode(language.getValue(CommonSettings.WEBINTERFACE_LANGUAGE_NAME))),
282                    escapeHtmlValues(language.getValue(CommonSettings.WEBINTERFACE_LANGUAGE_NAME))));
283        }
284        out.print("</div>");
285    }
286
287    /**
288     * Prints out the navigation tree appearing as a <td>in the left column of the "main_table" table. Subpages are
289     * shown only for the currently-active main-heading of the sections defined in settings.
290     *
291     * @param out the writer to which the output must be written.
292     * @param url the url of the page.
293     * @param locale The locale selecting the language.
294     * @throws IOException if the output cannot be written.
295     */
296    private static void generateNavigationTree(JspWriter out, String url, Locale locale) throws IOException {
297        out.print("<td valign=\"top\" id=\"menu\">\n");
298        // The list of menu items is presented as a 1-column table
299        out.print("<table id=\"menu_table\">\n");
300        String s = I18N.getString(locale, "sidebar.title.menu");
301        out.print("<tr><td><a class=\"sidebarHeader\" href=\"index.jsp\">"
302                + "<img src=\"transparent_menu_logo.png\" alt=\"" + s + "\"/> " + s + "</a></td></tr>\n");
303
304        for (SiteSection section : SiteSection.getSections()) {
305            section.generateNavigationTree(out, url, locale);
306        }
307        out.print("</table>\n");
308        out.print("</td>\n");
309    }
310
311    /**
312     * Writes out footer information to close the page.
313     *
314     * @param out the writer to which the information is written
315     * @throws IOException if the output cannot be written
316     */
317    public static void generateFooter(JspWriter out) throws IOException {
318        ArgumentNotValid.checkNotNull(out, "out");
319        // Close the element containing the page content
320        out.print("</td>\n");
321        // Close the single row in the table
322        out.print("</tr>\n");
323        // Close the table
324        out.print("</table>\n");
325        // Add information about the running system
326        out.print("<div class='systeminfo'>");
327        out.print("NetarchiveSuite " + Constants.getVersionString() + ", "
328                + Settings.get(CommonSettings.ENVIRONMENT_NAME));
329        out.print("</div>");
330        // Close the page
331        out.print("</body></html>");
332
333    }
334
335    /**
336     * Create a table element containing the given string, escaping HTML values in the process.
337     *
338     * @param s An unescaped string. Any HTML tags in this string will end up escaped away.
339     * @return The same string escaped and enclosed in td tags.
340     */
341    public static String makeTableElement(String s) {
342        ArgumentNotValid.checkNotNull(s, "s");
343        return "<td>" + escapeHtmlValues(s) + "</td>";
344    }
345
346    /**
347     * Create a table header element containing the given string, escaping HTML values in the process.
348     *
349     * @param contents An unescaped string. Any HTML tags in this string will end up escaped away.
350     * @return The same string escaped and enclosed in th tags.
351     */
352    public static String makeTableHeader(String contents) {
353        ArgumentNotValid.checkNotNull(contents, "contents");
354        return "<th>" + escapeHtmlValues(contents) + "</th>";
355    }
356
357    /**
358     * Create a table row. Note that in contrast to createTableElement and createTableHeader, the contents are not
359     * escaped. They are expected to contain table elements.
360     *
361     * @param contents The contents to put into the table row. The entries will be delimited by newline characters.
362     * @return The same string escaped and enclosed in td tags.
363     */
364    public static String makeTableRow(String... contents) {
365        ArgumentNotValid.checkNotNull(contents, "contents");
366        StringBuilder sb = new StringBuilder("<tr>");
367        for (String element : contents) {
368            sb.append(element);
369            sb.append("\n");
370        }
371        sb.append("</tr>\n");
372        return sb.toString();
373    }
374
375    /**
376     * Get an HTML representation of the date given.
377     *
378     * @param d A date
379     * @return A representation of the date that can be directly inserted into an HTML document, or the empty string if
380     * d is null.
381     * @deprecated Please use <fmt:date> from taglib instead.
382     */
383    public static String makeDate(Date d) {
384        if (d == null) {
385            return "";
386        } else {
387            return escapeHtmlValues(d.toString());
388        }
389    }
390
391    /**
392     * Returns the toString() value of an object or a hyphen if the argument is null.
393     *
394     * @param o the given object
395     * @return o.toString() or "-" if o is null
396     */
397    public static String nullToHyphen(Object o) {
398        if (o == null) {
399            return "-";
400        } else {
401            return o.toString();
402        }
403    }
404
405    /**
406     * Escapes HTML special characters ", &, < and > (but not ').
407     *
408     * @param input a string
409     * @return The string with values escaped. If input is null, the empty string is returned.
410     */
411    public static String escapeHtmlValues(String input) {
412        if (input == null) {
413            return "";
414        }
415        return input.replaceAll("&", "&amp;").replaceAll("\\\"", "&quot;").replaceAll("<", "&lt;")
416                .replaceAll(">", "&gt;");
417    }
418
419    /**
420     * Encode a string for use in a URL, then escape characters that must be escaped in HTML. This must be used whenever
421     * unknown strings are used in URLs that are placed in HTML.
422     *
423     * @param input A string
424     * @return The same string, encoded to be safely placed in a URL in HTML.
425     */
426    public static String encodeAndEscapeHTML(String input) {
427        ArgumentNotValid.checkNotNull(input, "input");
428        return escapeHtmlValues(encode(input));
429    }
430
431    /**
432     * Escapes a string for use in javascript. Replaces " with \" and ' with \', so e.g.
433     * escapeJavascriptQuotes("\"").equals("\\\"") Also, \ and any non-printable character is escaped for use in
434     * javascript
435     *
436     * @param input a string
437     * @return The string with values escaped. If input is null, the empty string is returned.
438     */
439    public static String escapeJavascriptQuotes(String input) {
440        if (input == null) {
441            return "";
442        }
443        input = input.replaceAll("\\\\", "\\\\\\\\");
444        input = input.replaceAll("\\\"", "\\\\\\\"");
445        input = input.replaceAll("\\\'", "\\\\\\\'");
446        input = input.replaceAll("\\\u0000", "\\\\u0000");
447        input = input.replaceAll("\\\u0001", "\\\\u0001");
448        input = input.replaceAll("\\\u0002", "\\\\u0002");
449        input = input.replaceAll("\\\u0003", "\\\\u0003");
450        input = input.replaceAll("\\\u0004", "\\\\u0004");
451        input = input.replaceAll("\\\u0005", "\\\\u0005");
452        input = input.replaceAll("\\\u0006", "\\\\u0006");
453        input = input.replaceAll("\\\u0007", "\\\\u0007");
454        input = input.replaceAll("\\\b", "\\\\b");
455        input = input.replaceAll("\\\t", "\\\\t");
456        input = input.replaceAll("\\\n", "\\\\n");
457        // Note: \v is an escape for vertical tab that exists
458        // in javascript but not in java
459        input = input.replaceAll("\\\u000B", "\\\\v");
460        input = input.replaceAll("\\\f", "\\\\f");
461        input = input.replaceAll("\\\r", "\\\\r");
462        input = input.replaceAll("\\\u000E", "\\\\u000E");
463        input = input.replaceAll("\\\u000F", "\\\\u000F");
464        input = input.replaceAll("\\\u0010", "\\\\u0010");
465        input = input.replaceAll("\\\u0011", "\\\\u0011");
466        input = input.replaceAll("\\\u0012", "\\\\u0012");
467        input = input.replaceAll("\\\u0013", "\\\\u0013");
468        input = input.replaceAll("\\\u0014", "\\\\u0014");
469        input = input.replaceAll("\\\u0015", "\\\\u0015");
470        input = input.replaceAll("\\\u0016", "\\\\u0016");
471        input = input.replaceAll("\\\u0017", "\\\\u0017");
472        input = input.replaceAll("\\\u0018", "\\\\u0018");
473        input = input.replaceAll("\\\u0019", "\\\\u0019");
474        input = input.replaceAll("\\\u001A", "\\\\u001A");
475        input = input.replaceAll("\\\u001B", "\\\\u001B");
476        input = input.replaceAll("\\\u001C", "\\\\u001C");
477        input = input.replaceAll("\\\u001D", "\\\\u001D");
478        input = input.replaceAll("\\\u001E", "\\\\u001E");
479        input = input.replaceAll("\\\u001F", "\\\\u001F");
480        return input;
481    }
482
483    /**
484     * Sets the character encoding for reading parameters and content from a request in a JSP page.
485     *
486     * @param request The servlet request object
487     */
488    public static void setUTF8(HttpServletRequest request) {
489        ArgumentNotValid.checkNotNull(request, "request");
490        // Why is this in an if block? Suppose we forward from a page where
491        // we read file input from the request. Trying to set the character
492        // encoding again here will throw an exception!
493        // This is a bit of a hack - we know that _if_ we have set it,
494        // we have set it to UTF-8, so this way we won't set it twice...
495        if (request.getCharacterEncoding() == null || !request.getCharacterEncoding().equals("UTF-8")) {
496            try {
497                request.setCharacterEncoding("UTF-8");
498            } catch (UnsupportedEncodingException e) {
499                throw new ArgumentNotValid("Should never happen! UTF-8 not supported", e);
500            }
501        }
502    }
503
504    /**
505     * Given a URL in the sitesection hierarchy, returns the corresponding page title.
506     *
507     * @param url a given URL
508     * @param locale the current locale
509     * @return the corresponding page title, or string about "(no title)" if no title can be found
510     * @throws ArgumentNotValid if the given url or locale is null or url is empty.
511     */
512    public static String getTitle(String url, Locale locale) {
513        ArgumentNotValid.checkNotNull(locale, "Locale locale");
514        ArgumentNotValid.checkNotNullOrEmpty(url, "String url");
515        for (SiteSection section : SiteSection.getSections()) {
516            String title = section.getTitle(url, locale);
517            if (title != null) {
518                return title;
519            }
520        }
521        log.warn("Could not find page title for page '" + url + "'");
522        return I18N.getString(locale, "pagetitle.unknown");
523    }
524
525    /**
526     * Get the (CSS) class name for a row in a table. The row count should start at 0.
527     *
528     * @param rowCount The number of the row
529     * @return A CSS class name that should be the class of the TR element.
530     */
531    public static String getRowClass(int rowCount) {
532        if (rowCount % 6 < 3) {
533            return "row0";
534        } else {
535            return "row1";
536        }
537    }
538
539    /**
540     * Get a locale from cookie, if present. The default request locale otherwise.
541     *
542     * @param request The request to get the locale for.
543     * @return The cookie locale, if present. The default request locale otherwise.
544     */
545    public static String getLocale(HttpServletRequest request) {
546        ArgumentNotValid.checkNotNull(request, "request");
547        Cookie[] cookies = request.getCookies();
548        if (cookies != null) {
549            for (Cookie c : cookies) {
550                if (c.getName().equals("locale")) {
551                    return c.getValue();
552                }
553            }
554        }
555        return request.getLocale().toString();
556    }
557
558    /**
559     * Forward to our standard error message page with an internationalized message. Note that this <em>doesn't</em>
560     * throw ForwardedToErrorPage, it is the job of whoever calls this to do that if not within a JSP page (a JSP page
561     * can just return immediately). All text involved will be HTML-escaped.
562     *
563     * @param context The context that the error happened in (the JSP-defined pageContext, typically)
564     * @param I18N The i18n information
565     * @param label An i18n label for the error. This label should begin with "errormsg;".
566     * @param args Any extra args for i18n
567     * @throws IOFailure If the forward fails
568     */
569    public static void forwardWithErrorMessage(PageContext context, I18n I18N, String label, Object... args) {
570        // Note that we may not want to be to strict here
571        // as otherwise information could be lost.
572        ArgumentNotValid.checkNotNull(context, "context");
573        ArgumentNotValid.checkNotNull(I18N, "I18N");
574        ArgumentNotValid.checkNotNull(label, "label");
575        ArgumentNotValid.checkNotNull(args, "args");
576
577        String msg = HTMLUtils.escapeHtmlValues(I18N.getString(context.getResponse().getLocale(), label, args));
578        context.getRequest().setAttribute("message", msg);
579        RequestDispatcher rd = context.getServletContext().getRequestDispatcher("/message.jsp");
580        final String errormsg = "Failed to forward on error " + msg;
581        try {
582            rd.forward(context.getRequest(), context.getResponse());
583        } catch (IOException e) {
584            log.warn(errormsg, e);
585            throw new IOFailure(errormsg, e);
586        } catch (ServletException e) {
587            log.warn(errormsg, e);
588            throw new IOFailure(errormsg, e);
589        }
590    }
591
592    /**
593     * Forward to our standard error message page with an internationalized message. Note that this <em>doesn't</em>
594     * throw ForwardedToErrorPage, it is the job of whoever calls this to do that if not within a JSP page (a JSP page
595     * can just return immediately). The text involved must be HTML-escaped before passing to this method.
596     *
597     * @param context The context that the error happened in (the JSP-defined pageContext, typically)
598     * @param i18n The i18n information
599     * @param label An i18n label for the error. This label should begin with "errormsg;".
600     * @param args Any extra args for i18n. These must be valid HTML.
601     * @throws IOFailure If the forward fails.
602     */
603    public static void forwardWithRawErrorMessage(PageContext context, I18n i18n, String label, Object... args) {
604        // Note that we may not want to be to strict here
605        // as otherwise information could be lost.
606        ArgumentNotValid.checkNotNull(context, "context");
607        ArgumentNotValid.checkNotNull(I18N, "I18N");
608        ArgumentNotValid.checkNotNull(label, "label");
609        ArgumentNotValid.checkNotNull(args, "args");
610
611        String msg = i18n.getString(context.getResponse().getLocale(), label, args);
612        context.getRequest().setAttribute("message", msg);
613        RequestDispatcher rd = context.getServletContext().getRequestDispatcher("/message.jsp");
614        try {
615            rd.forward(context.getRequest(), context.getResponse());
616        } catch (IOException e) {
617            final String errormsg = "Failed to forward on error " + msg;
618            log.warn(errormsg, e);
619            throw new IOFailure(errormsg, e);
620        } catch (ServletException e) {
621            final String errormsg = "Failed to forward on error " + msg;
622            log.warn(errormsg, e);
623            throw new IOFailure(errormsg, e);
624        }
625    }
626
627    /**
628     * Forward to our standard error message page with an internationalized message, in case of exception. Note that
629     * this <em>doesn't</em> throw ForwardedToErrorPage, it is the job of whoever calls this to do that if not within a
630     * JSP page (a JSP page can just return immediately). All text involved will be HTML-escaped.
631     *
632     * @param context The context that the error happened in (the JSP-defined pageContext, typically)
633     * @param i18n The i18n information
634     * @param e The exception that is being handled.
635     * @param label An i18n label for the error. This label should begin with "errormsg;".
636     * @param args Any extra args for i18n
637     * @throws IOFailure If the forward fails
638     */
639    public static void forwardWithErrorMessage(PageContext context, I18n i18n, Throwable e, String label,
640            Object... args) {
641        // Note that we may not want to be to strict here
642        // as otherwise information could be lost.
643        ArgumentNotValid.checkNotNull(context, "context");
644        ArgumentNotValid.checkNotNull(I18N, "I18N");
645        ArgumentNotValid.checkNotNull(label, "label");
646        ArgumentNotValid.checkNotNull(args, "args");
647
648        String msg = HTMLUtils.escapeHtmlValues(i18n.getString(context.getResponse().getLocale(), label, args));
649        context.getRequest().setAttribute("message", msg + "\n" + e.getLocalizedMessage());
650        RequestDispatcher rd = context.getServletContext().getRequestDispatcher("/message.jsp");
651        final String errormsg = "Failed to forward on error " + msg;
652        try {
653            rd.forward(context.getRequest(), context.getResponse());
654        } catch (IOException e1) {
655            log.warn(errormsg, e1);
656            throw new IOFailure(errormsg, e1);
657        } catch (ServletException e1) {
658            log.warn(errormsg, e1);
659            throw new IOFailure(errormsg, e1);
660        }
661    }
662
663    /**
664     * Checks that the given parameters exist. If any of them do not exist, forwards to the error page and throws
665     * ForwardedToErrorPage.
666     *
667     * @param context The context of the current JSP page
668     * @param parameters List of parameters that must exist
669     * @throws IOFailure If the forward fails
670     * @throws ForwardedToErrorPage If a parameter is missing
671     */
672    public static void forwardOnMissingParameter(PageContext context, String... parameters) throws ForwardedToErrorPage {
673        // Note that we may not want to be to strict here
674        // as otherwise information could be lost.
675        ArgumentNotValid.checkNotNull(context, "context");
676        ArgumentNotValid.checkNotNull(parameters, "parameters");
677
678        ServletRequest request = context.getRequest();
679        for (String parameter : parameters) {
680            String value = request.getParameter(parameter);
681            if (value == null) {
682                forwardWithErrorMessage(context, I18N, "errormsg;missing.parameter.0", parameter);
683                throw new ForwardedToErrorPage("Missing parameter '" + parameter + "'");
684            }
685        }
686
687    }
688
689    /**
690     * Checks that the given parameters exist and are not empty. If any of them are missing or empty, forwards to the
691     * error page and throws ForwardedToErrorPage. A parameter with only whitespace is considered empty.
692     *
693     * @param context The context of the current JSP page
694     * @param parameters List of parameters that must exist and be non-empty
695     * @throws IOFailure If the forward fails
696     * @throws ForwardedToErrorPage if a parameter was missing or empty
697     */
698    public static void forwardOnEmptyParameter(PageContext context, String... parameters) {
699        // Note that we may not want to be to strict here
700        // as otherwise information could be lost.
701        ArgumentNotValid.checkNotNull(context, "context");
702        ArgumentNotValid.checkNotNull(parameters, "parameters");
703
704        forwardOnMissingParameter(context, parameters);
705        ServletRequest request = context.getRequest();
706        for (String parameter : parameters) {
707            String value = request.getParameter(parameter);
708            if (value.trim().length() == 0) {
709                forwardWithErrorMessage(context, I18N, "errormsg;empty.parameter.0", parameter);
710                throw new ForwardedToErrorPage("Empty parameter '" + parameter + "'");
711            }
712        }
713    }
714
715    /**
716     * Checks that the given parameter exists and is one of a set of values. If is is missing or doesn't equal one of
717     * the given values, forwards to the error page and throws ForwardedToErrorPage.
718     *
719     * @param context The context of the current JSP page
720     * @param parameter parameter that must exist
721     * @param legalValues legal values for the parameter
722     * @throws IOFailure If the forward fails
723     * @throws ForwardedToErrorPage if the parameter is none of the given values
724     */
725    public static void forwardOnIllegalParameter(PageContext context, String parameter, String... legalValues)
726            throws ForwardedToErrorPage {
727        // Note that we may not want to be to strict here
728        // as otherwise information could be lost.
729        ArgumentNotValid.checkNotNull(context, "context");
730        ArgumentNotValid.checkNotNull(parameter, "parameter");
731        ArgumentNotValid.checkNotNull(legalValues, "legalValues");
732
733        forwardOnMissingParameter(context, parameter);
734        String value = context.getRequest().getParameter(parameter);
735        for (String legalValue : legalValues) {
736            if (value.equals(legalValue)) {
737                return;
738            }
739        }
740        forwardWithErrorMessage(context, I18N, "errormsg;illegal.value.0.for.parameter.1", value, parameter);
741        throw new ForwardedToErrorPage("Illegal value '" + value + "' for parameter '" + parameter + "'");
742    }
743
744    /**
745     * Parses a integer request parameter and checks that it lies within a given interval. If it doesn't, forwards to an
746     * error page and throws ForwardedToErrorPage.
747     *
748     * @param context The context this call happens in
749     * @param param A parameter to parse.
750     * @param minValue The minimum allowed value
751     * @param maxValue The maximum allowed value
752     * @return The value x parsed from the string, if minValue <= x <= maxValue
753     * @throws ForwardedToErrorPage if the parameter doesn't exist, is not a parseable integer, or doesn't lie within
754     * the limits.
755     */
756    public static int parseAndCheckInteger(PageContext context, String param, int minValue, int maxValue)
757            throws ForwardedToErrorPage {
758        // Note that we may not want to be to strict here
759        // as otherwise information could be lost.
760        ArgumentNotValid.checkNotNull(context, "context");
761        ArgumentNotValid.checkNotNull(param, "param");
762
763        Locale loc = HTMLUtils.getLocaleObject(context);
764        forwardOnEmptyParameter(context, param);
765        int value;
766        String paramValue = context.getRequest().getParameter(param);
767        try {
768            value = NumberFormat.getInstance(loc).parse(paramValue).intValue();
769            if (value < minValue || value > maxValue) {
770                forwardWithErrorMessage(context, I18N, "errormsg;parameter.0.outside.range.1.to.2.3", param,
771                        paramValue, minValue, maxValue);
772                throw new ForwardedToErrorPage("Parameter '" + param + "' should be between " + minValue + " and "
773                        + maxValue + " but is " + paramValue);
774            }
775            return value;
776        } catch (ParseException e) {
777            forwardWithErrorMessage(context, I18N, "errormsg;parameter.0.not.an.integer.1", param, paramValue);
778            throw new ForwardedToErrorPage("Invalid value " + paramValue + " for integer parameter '" + param + "'", e);
779        }
780    }
781
782    /**
783     * Parse an optionally present long-value from a request parameter.
784     *
785     * @param context The context of the web request.
786     * @param param The name of the parameter to parse.
787     * @param defaultValue A value to return if the parameter is not present (may be null).
788     * @return Parsed value or default value if the parameter is missing or empty. Null will only be returned if passed
789     * as the default value.
790     * @throws ForwardedToErrorPage if the parameter is present but not parseable as a long value.
791     */
792    public static Long parseOptionalLong(PageContext context, String param, Long defaultValue) {
793        // Note that we may not want to be to strict here
794        // as otherwise information could be lost.
795        ArgumentNotValid.checkNotNull(context, "context");
796        ArgumentNotValid.checkNotNullOrEmpty(param, "String param");
797
798        Locale loc = HTMLUtils.getLocaleObject(context);
799        String paramValue = context.getRequest().getParameter(param);
800        return parseLong(loc, paramValue, param, defaultValue);
801    }
802
803    /**
804     * Parse an optionally present date-value from a request parameter.
805     *
806     * @param context The context of the web request.
807     * @param param The name of the parameter to parse
808     * @param format The format of the date, in the format defined by SimpleDateFormat
809     * @param defaultValue A value to return if the parameter is not present (may be null)
810     * @return Parsed value or default value if the parameter is missing or empty. Null will only be returned if passed
811     * as the default value.
812     * @throws ForwardedToErrorPage if the parameter is present but not parseable as a date
813     */
814    public static Date parseOptionalDate(PageContext context, String param, String format, Date defaultValue) {
815        ArgumentNotValid.checkNotNullOrEmpty(param, "String param");
816        ArgumentNotValid.checkNotNullOrEmpty(format, "String format");
817        String paramValue = context.getRequest().getParameter(param);
818        if (paramValue != null && paramValue.trim().length() > 0) {
819            paramValue = paramValue.trim();
820            try {
821                return new SimpleDateFormat(format).parse(paramValue);
822            } catch (ParseException e) {
823                forwardWithErrorMessage(context, I18N, "errormsg;parameter.0.not.a.date.with.format.1.2", param,
824                        format, paramValue);
825                throw new ForwardedToErrorPage("Invalid value " + paramValue + " for date parameter '" + param
826                        + "' with format '" + format + "'", e);
827            }
828        } else {
829            return defaultValue;
830        }
831    }
832
833    /**
834     * Parse an optionally present boolean from a request parameter.
835     *
836     * @param context The context of the web request.
837     * @param param The name of the parameter to parse
838     * @param defaultValue A value to return if the parameter is not present (may be null)
839     * @return Parsed value or default value if the parameter is missing or empty. Null will only be returned if passed
840     * as the default value.
841     */
842    public static boolean parseOptionalBoolean(PageContext context, String param, boolean defaultValue) {
843        ArgumentNotValid.checkNotNullOrEmpty(param, "String param");
844        String paramValue = context.getRequest().getParameter(param);
845        if (paramValue != null && paramValue.trim().length() > 0) {
846            paramValue = paramValue.trim();
847            return Boolean.parseBoolean(paramValue);
848        } else {
849            return defaultValue;
850        }
851    }
852
853    /**
854     * Create a localized string representation of the given long.
855     *
856     * @param i A long integer
857     * @param context The given JSP context
858     * @return a localized string representation of the given long TODO Should this method throw ArgumentNotValid if the
859     * context is null
860     */
861    public static String localiseLong(long i, PageContext context) {
862        NumberFormat nf = NumberFormat.getInstance(HTMLUtils.getLocaleObject(context));
863        return nf.format(i);
864    }
865
866    /**
867     * Create a localized string representation of the given long.
868     *
869     * @param i A long integer
870     * @param locale The given locale.
871     * @return a localized string representation of the given long TODO Should this method throw ArgumentNotValid if the
872     * locale is null
873     */
874    public static String localiseLong(long i, Locale locale) {
875        NumberFormat nf = NumberFormat.getInstance(locale);
876        return nf.format(i);
877    }
878
879    /**
880     * Parse a given String for a long value.
881     *
882     * @param loc The given Locale.
883     * @param paramValue The given parameter value
884     * @param parameterName The given parameter name (used for debugging)
885     * @param defaultValue The default value for the parameter, in case the string cannot be parsed
886     * @return the long value found in the paramValue
887     */
888    public static Long parseLong(Locale loc, String paramValue, String parameterName, Long defaultValue) {
889        ArgumentNotValid.checkNotNull(loc, "Locale loc");
890        ArgumentNotValid.checkNotNullOrEmpty(parameterName, "String parameterName");
891
892        if (paramValue != null && paramValue.trim().length() > 0) {
893            paramValue = paramValue.trim();
894            try {
895                return NumberFormat.getInstance(loc).parse(paramValue).longValue();
896            } catch (ParseException e) {
897                throw new ForwardedToErrorPage("Invalid value " + paramValue + " for integer parameter '"
898                        + parameterName + "'", e);
899            }
900        } else {
901            return defaultValue;
902        }
903    }
904}