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