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