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&name=%s\">%s</a> ", 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("&", "&").replaceAll("\\\"", """).replaceAll("<", "<") 420 .replaceAll(">", ">"); 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}