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