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