001/* 002 * #%L 003 * Netarchivesuite - monitor 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.monitor.webinterface; 025 026import java.io.BufferedReader; 027import java.io.IOException; 028import java.io.StringReader; 029import java.util.List; 030import java.util.Locale; 031import java.util.Random; 032 033import javax.management.MalformedObjectNameException; 034import javax.servlet.http.HttpServletRequest; 035import javax.servlet.http.HttpServletResponse; 036import javax.servlet.jsp.JspWriter; 037import javax.servlet.jsp.PageContext; 038 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042import dk.netarkivet.common.exceptions.ArgumentNotValid; 043import dk.netarkivet.common.exceptions.ForwardedToErrorPage; 044import dk.netarkivet.common.utils.DomainUtils; 045import dk.netarkivet.common.utils.I18n; 046import dk.netarkivet.common.utils.Settings; 047import dk.netarkivet.common.utils.StringUtils; 048import dk.netarkivet.common.webinterface.HTMLUtils; 049import dk.netarkivet.monitor.Constants; 050import dk.netarkivet.monitor.MonitorSettings; 051 052/** 053 * Various utility methods and classes for the JMX Monitor page. and a bunch of JMX properties used by 054 * Monitor-JMXsummary.jsp. 055 */ 056public class JMXSummaryUtils { 057 058 private static final Logger log = LoggerFactory.getLogger(JMXSummaryUtils.class); 059 060 061 /** JMX property for remove application button. */ 062 public static final String JMXRemoveApplication = dk.netarkivet.common.management.Constants.REMOVE_JMX_APPLICATION; 063 /** JMX property for the physical location. */ 064 public static final String JMXPhysLocationProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_LOCATION; 065 /** JMX property for the machine name. */ 066 public static final String JMXMachineNameProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_MACHINE; 067 /** JMX property for the application name. */ 068 public static final String JMXApplicationNameProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_APPLICATIONNAME; 069 /** JMX property for the application instance id. */ 070 public static final String JMXApplicationInstIdProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_APPLICATIONINSTANCEID; 071 /** JMX property for the HTTP port. */ 072 public static final String JMXHttpportProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_HTTP_PORT; 073 /** JMX property for the harvest channel. */ 074 public static final String JMXHarvestChannelProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_CHANNEL; 075 /** JMX property for the replica name. */ 076 public static final String JMXArchiveReplicaNameProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_REPLICANAME; 077 /** JMX property for the index. */ 078 public static final String JMXIndexProperty = dk.netarkivet.common.management.Constants.PRIORITY_KEY_INDEX; 079 080 /** 081 * The preferred length of lines in the jmx log. 082 */ 083 private static final int PREFERRED_LENGTH = Settings.getInt(MonitorSettings.JMX_PREFERRED_MAX_LOG_LENGTH); 084 085 /** 086 * The maximum length of lines in the jmx log. 087 */ 088 private static final int MAX_LENGTH = Settings.getInt(MonitorSettings.JMX_ABSOLUTE_MAX_LOG_LENGTH); 089 090 /** JMX properties, which can set to star. */ 091 public static final String[] STARRABLE_PARAMETERS = new String[] {JMXRemoveApplication, JMXPhysLocationProperty, 092 JMXMachineNameProperty, JMXApplicationNameProperty, JMXApplicationInstIdProperty, JMXHttpportProperty, 093 JMXHarvestChannelProperty, JMXArchiveReplicaNameProperty, JMXIndexProperty}; 094 /** Status/Monitor-JMXsummary.jsp. */ 095 public static final String STATUS_MONITOR_JMXSUMMARY = "Status/Monitor-JMXsummary.jsp"; 096 097 /** The log MBean name prefix. */ 098 private static final String LOGGING_MBEAN_NAME_PREFIX = "dk.netarkivet.common.logging:"; 099 100 /** Internationalisation object. */ 101 private static final I18n I18N = new I18n(dk.netarkivet.monitor.Constants.TRANSLATIONS_BUNDLE); 102 103 /** The character for show all. */ 104 private static final String CHARACTER_SHOW_ALL = "*"; 105 /** The character for don't show column. */ 106 private static final String CHARACTER_NOT_COLUMN = "-"; 107 /** The character for only seeing the first row of the log. */ 108 private static final String CHARACTER_FIRST_ROW = "0"; 109 /** The number of log lines showed in generateMessage. */ 110 private static final int NUMBER_OF_LOG_LINES = 5; 111 112 /** Instance of class Random used to generate a unique id for each div. */ 113 private static final Random random = new Random(); 114 115 /** 116 * Reduce the class name of an application to the essentials. 117 * 118 * @param applicationName The class name of the application, should not be null. 119 * @return A reduced name suitable for user output. 120 * @throws ArgumentNotValid if argument isn't valid. 121 */ 122 public static String reduceApplicationName(String applicationName) throws ArgumentNotValid { 123 ArgumentNotValid.checkNotNull(applicationName, "String applicationName"); 124 String[] split = applicationName.split("\\."); 125 return split[split.length - 1]; 126 } 127 128 /** 129 * Creates the show links for showing columns again. 130 * <p> 131 * Goes through all parameters to check if their column is active. If a column is not active, the link to showing a 132 * specific column again is generated. 133 * 134 * @param starredRequest A request to take parameters from, should be different from null. 135 * @param l For retrieving the correct words form the current language. 136 * @return The link to show the parameter again. 137 * @throws ArgumentNotValid if argument isn't valid. 138 */ 139 public static String generateShowColumn(StarredRequest starredRequest, Locale l) throws ArgumentNotValid { 140 ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest"); 141 142 StringBuilder res = new StringBuilder(); 143 144 for (String parameter : STARRABLE_PARAMETERS) { 145 if (CHARACTER_NOT_COLUMN.equals(starredRequest.getParameter(parameter))) { 146 // generate the link, but use the parameter applied to the 147 // table field value. 148 res.append(generateLink(starredRequest, parameter, CHARACTER_SHOW_ALL, 149 I18N.getString(l, "tablefield;" + parameter))); 150 res.append(","); 151 } 152 } 153 154 // If any content, then remove last ',' and put 'show' in front 155 if (res.length() > 0) { 156 res.deleteCharAt(res.length() - 1); 157 res.insert(0, I18N.getString(l, "show") + ": "); 158 } 159 160 return res.toString(); 161 } 162 163 /** 164 * Generate HTML to show at the top of the table, containing a "show all" link if the parameter is currently 165 * restricted. This function is only used by JMXIndexProperty field, the other properties uses generateShowLing 166 * instead. 167 * 168 * @param starredRequest A request to take parameters from, should not be null. 169 * @param parameter The parameter that, if not already unrestricted, should be unrestricted in the "show all" link, 170 * should not be null. 171 * @param l the current locale. 172 * @return HTML to insert at the top of the JMX monitor table. 173 * @throws ArgumentNotValid if arguments isn't valid. 174 */ 175 public static String generateShowAllLink(StarredRequest starredRequest, String parameter, Locale l) 176 throws ArgumentNotValid { 177 ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest"); 178 ArgumentNotValid.checkNotNull(parameter, "String parameter"); 179 if (CHARACTER_SHOW_ALL.equals(starredRequest.getParameter(parameter))) { 180 return ""; 181 } else { 182 return "(" + generateLink(starredRequest, parameter, CHARACTER_SHOW_ALL, I18N.getString(l, "showall")) 183 + ")"; 184 } 185 } 186 187 /** 188 * Generate HTML to show at the top of the table, containing a "show all" and a "off" links if the parameter is 189 * currently restricted. 190 * 191 * @param starredRequest A request to take parameters from, should not be null. 192 * @param parameter The parameter that, if not already unrestricted, should be unrestricted in the "show all", 193 * should not be null. 194 * @param l the current locale. 195 * @return HTML to insert at the top of the JMX monitor table. 196 * @throws ArgumentNotValid if arguments isn't valid. 197 */ 198 public static String generateShowLink(StarredRequest starredRequest, String parameter, Locale l) 199 throws ArgumentNotValid { 200 ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest"); 201 ArgumentNotValid.checkNotNull(parameter, "String parameter"); 202 if (CHARACTER_SHOW_ALL.equals(starredRequest.getParameter(parameter))) { 203 return "(" + generateLink(starredRequest, parameter, CHARACTER_NOT_COLUMN, I18N.getString(l, "hide")) + ")"; 204 } else { 205 return "(" + generateLink(starredRequest, parameter, CHARACTER_SHOW_ALL, I18N.getString(l, "showall")) 206 + ", " + generateLink(starredRequest, parameter, CHARACTER_NOT_COLUMN, I18N.getString(l, "hide")) 207 + ")"; 208 } 209 } 210 211 /** 212 * Tests if a parameter in the request is "-" (thus off). 213 * 214 * @param starredRequest A request to take parameters from, should not be null. 215 * @param parameter The parameter that should be tested. 216 * @return Whether the parameter is set to "-". 217 * @throws ArgumentNotValid if argument isn't valid. 218 */ 219 public static boolean showColumn(StarredRequest starredRequest, String parameter) throws ArgumentNotValid { 220 ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest"); 221 ArgumentNotValid.checkNotNullOrEmpty(parameter, "String parameter"); 222 if (CHARACTER_NOT_COLUMN.equals(starredRequest.getParameter(parameter))) { 223 return false; 224 } 225 return true; 226 } 227 228 /** 229 * Generate an HTML link to the JMX summary page with one part of the URL parameters set to a specific value. 230 * 231 * @param request A request to draw other parameter values from, should not be null. 232 * @param setPart Which of the parameters to set. 233 * @param setValue The value to set that parameter to. 234 * @param linkText The HTML text that should go inside the link. Remember to escape HTML values if inserting a 235 * normal string. 236 * @return A link to insert in the page, or an unlinked text, if setPart or setValue is null, or an empty string if 237 * linkText is null. 238 * @throws ArgumentNotValid if request is null. 239 */ 240 public static String generateLink(StarredRequest request, String setPart, String setValue, String linkText) 241 throws ArgumentNotValid { 242 ArgumentNotValid.checkNotNull(request, "StarredRequest request"); 243 if (linkText == null) { 244 return ""; 245 } 246 if (setPart == null || setValue == null) { 247 return linkText; 248 } 249 StringBuilder builder = new StringBuilder(); 250 builder.append("<a href=\"/" + STATUS_MONITOR_JMXSUMMARY + "?"); 251 boolean isFirst = true; 252 for (String queryPart : STARRABLE_PARAMETERS) { 253 if (isFirst) { 254 isFirst = false; 255 } else { 256 builder.append("&"); 257 } 258 builder.append(queryPart); 259 builder.append("="); 260 if (queryPart.equals(setPart)) { 261 builder.append(HTMLUtils.encode(setValue)); 262 } else { 263 builder.append(HTMLUtils.encode(request.getParameter(queryPart))); 264 } 265 } 266 builder.append("\">"); 267 builder.append(linkText); 268 builder.append("</a>"); 269 return builder.toString(); 270 } 271 272 /** 273 * Get status entries from JMX based on a request and some parameters. 274 * 275 * @param parameters The parameters to query JMX for, should not be null. 276 * @param request A request possibly containing values for some of the parameters, should not be null. 277 * @param context the current JSP context, should not be null. 278 * @return Status entries for the MBeans that match the parameters. 279 * @throws ArgumentNotValid if the query is invalid (typically caused by invalid parameters). 280 * @throws ForwardedToErrorPage if unable to create JMX-query. 281 */ 282 public static List<StatusEntry> queryJMXFromRequest(String[] parameters, StarredRequest request, PageContext context) 283 throws ArgumentNotValid, ForwardedToErrorPage { 284 ArgumentNotValid.checkNotNull(parameters, "String[] parameters"); 285 ArgumentNotValid.checkNotNull(request, "StarredRequest request"); 286 ArgumentNotValid.checkNotNull(context, "PageContext context"); 287 288 String query = null; 289 try { 290 query = createJMXQuery(parameters, request); 291 log.debug("Executing JMX query: " + query); 292 return JMXStatusEntry.queryJMX(query); 293 } catch (MalformedObjectNameException e) { 294 if (query != null) { 295 HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.in.querying.jmx.with.query.0", 296 query); 297 throw new ForwardedToErrorPage("Error in querying JMX with query '" + query + "'.", e); 298 } else { 299 HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.in.building.jmxquery"); 300 throw new ForwardedToErrorPage("Error building JMX query", e); 301 } 302 } 303 } 304 305 /** 306 * Select zero or more beans from JMX and unregister these. 307 * 308 * @param parameters The parameters to query JMX for, should not be null. 309 * @param request A request possibly containing values for some of the parameters, which select zero or more beans. 310 * @param context the current JSP context, should not be null. 311 * @throws ArgumentNotValid if arguments isn't valid. 312 */ 313 public static void unregisterJMXInstance(String[] parameters, StarredRequest request, PageContext context) 314 throws ArgumentNotValid { 315 ArgumentNotValid.checkNotNull(parameters, "String[] parameters"); 316 ArgumentNotValid.checkNotNull(context, "PageContext context"); 317 String query = null; 318 try { 319 query = createJMXQuery(parameters, request); 320 JMXStatusEntry.unregisterJMXInstance(query); 321 } catch (MalformedObjectNameException e) { 322 if (query != null) { 323 HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.in.querying.jmx.with.query.0", 324 query); 325 throw new ForwardedToErrorPage("Error in querying JMX with query '" + query + "'.", e); 326 } else { 327 HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.in.building.jmxquery"); 328 throw new ForwardedToErrorPage("Error building JMX query", e); 329 } 330 } catch (Exception e) { 331 // Both InstanceNotFoundException and MBeanRegistrationException 332 // are treated equal. 333 HTMLUtils.forwardWithErrorMessage(context, I18N, e, "errormsg;error.when.unregistering.mbean.0", query); 334 throw new ForwardedToErrorPage("Error when unregistering JMX MBean with query '" + query + "'.", e); 335 } 336 } 337 338 /** 339 * Build a JMX query string (ObjectName) from a request and a list of parameters to query for. This string is always 340 * a property pattern (wildcarded), even if all the values we define in the names are specified. 341 * 342 * @param parameters The parameters to query for. These should make up the parts of the unique identification of an 343 * MBean. 344 * @param starredRequest A request containing current values for the given parameters. 345 * @return A query, wildcarded for those parameters that are or missing in starredRequest. 346 * @throws ArgumentNotValid if one or all of the arguements are null. 347 */ 348 public static String createJMXQuery(String[] parameters, StarredRequest starredRequest) throws ArgumentNotValid { 349 ArgumentNotValid.checkNotNull(parameters, "String[] parameters"); 350 ArgumentNotValid.checkNotNull(starredRequest, "StarredRequest starredRequest"); 351 StringBuilder query = new StringBuilder(LOGGING_MBEAN_NAME_PREFIX + "*"); 352 for (String queryPart : parameters) { 353 if (!CHARACTER_SHOW_ALL.equals(starredRequest.getParameter(queryPart)) 354 && !CHARACTER_NOT_COLUMN.equals(starredRequest.getParameter(queryPart))) { 355 query.append(","); 356 query.append(queryPart); 357 query.append("="); 358 String parameter = starredRequest.getParameter(queryPart); 359 query.append(parameter); 360 } 361 } 362 363 return query.toString(); 364 } 365 366 /** 367 * Make an HTML fragment that shows a log message preformatted. If the log message is longer than 368 * NUMBER_OF_LOG_LINES lines, the rest are hidden and replaced with an internationalized link "More..." that will 369 * show the rest. Long loglines are split to length approximately monitor.preferredMaxJMXLogLength and then split 370 * again to ensure that no lines are wider than monitor.absoluteMaxJMXLogLength. However as the wrapping algorithm 371 * is not sophisticated it is better to split the lines in a meaningful way where they are generated. 372 * 373 * @param logMessage The log message to present. 374 * @param l the current Locale. 375 * @return An HTML fragment as defined above. 376 * @throws ArgumentNotValid if argument isn't valid. 377 */ 378 public static String generateMessage(String logMessage, Locale l) throws ArgumentNotValid { 379 ArgumentNotValid.checkNotNull(logMessage, "String logMessage"); 380 StringBuilder msg = new StringBuilder(); 381 logMessage = HTMLUtils.escapeHtmlValues(logMessage); 382 // All strings starting with "http:" or "https:" are replaced with 383 // a proper HTML Anchor 384 logMessage = logMessage.replaceAll("(https?://[^ \\t\\n\"]*)", "<a href=\"$1\">$1</a>"); 385 logMessage = StringUtils.splitStringOnWhitespace(logMessage, PREFERRED_LENGTH); 386 logMessage = StringUtils.splitStringForce(logMessage, MAX_LENGTH); 387 BufferedReader sr = new BufferedReader(new StringReader(logMessage)); 388 msg.append("<pre>"); 389 int lineno = 0; 390 String line; 391 try { 392 while (lineno < NUMBER_OF_LOG_LINES && (line = sr.readLine()) != null) { 393 msg.append(line); 394 msg.append('\n'); 395 ++lineno; 396 } 397 msg.append("</pre>"); 398 if ((line = sr.readLine()) != null) { 399 // We use a random number for generating a unique id for the div. 400 // TODO should change method to take an integer, so no 401 // colidation is happening. 402 int id = random.nextInt(); 403 msg.append("<a id=\"show"); 404 msg.append(id); 405 msg.append("\" onclick=\"document.getElementById('log"); 406 msg.append(id); 407 msg.append("').style.display='block';" + "document.getElementById('show"); 408 msg.append(id); 409 410 msg.append("').style.display='none';\">"); 411 msg.append(I18N.getString(l, "more.dot.dot.dot")); 412 msg.append("</a>"); 413 414 msg.append("<div id=\"log"); 415 msg.append(id); 416 msg.append("\" style=\"display:none\">"); 417 msg.append("<pre>"); 418 // Display the rest of the message in a div, which are not 419 // visible. 420 do { 421 msg.append(HTMLUtils.escapeHtmlValues(line)); 422 msg.append('\n'); 423 } while ((line = sr.readLine()) != null); 424 msg.append("</pre>"); 425 msg.append("</div>"); 426 } 427 } catch (IOException e) { 428 // This should never happen, but if it does, just return the string 429 // without all the fancy stuff. 430 return "<pre>" + HTMLUtils.escapeHtmlValues(logMessage) + "</pre>"; 431 } 432 return msg.toString(); 433 } 434 435 /** 436 * This class encapsulates a HttpServletRequest, making non-existing parameters appear as "*" for wildcard (or "0" 437 * for the index parameter). 438 */ 439 public static class StarredRequest { 440 /** A http request, for a starred request. */ 441 HttpServletRequest req; 442 443 /** 444 * Makes the request reusable for this class. 445 * 446 * @param req A http request, for a starred request, should not be null. 447 * @throws ArgumentNotValid if argument isn't valid. 448 */ 449 public StarredRequest(HttpServletRequest req) throws ArgumentNotValid { 450 ArgumentNotValid.checkNotNull(req, "HttpServletRequest req"); 451 this.req = req; 452 } 453 454 /** 455 * Gets a parameter from the original request, except if the parameter is unset, return the following. "index" = 456 * "0". "applicationInstanceId" = "-". "location" = "-". "http-port" = "-". Default = "*". 457 * 458 * @param paramName The parameter. 459 * @return The parameter or "*", "0" or "-"; never null. 460 * @throws ArgumentNotValid if argument isn't valid. 461 */ 462 public String getParameter(String paramName) throws ArgumentNotValid { 463 ArgumentNotValid.checkNotNull(paramName, "String paramName"); 464 String value = req.getParameter(paramName); 465 if (value == null || value.length() == 0) { 466 if (JMXIndexProperty.equals(paramName)) { 467 return CHARACTER_FIRST_ROW; 468 } else if (JMXPhysLocationProperty.equals(paramName)) { 469 return CHARACTER_NOT_COLUMN; 470 } else if (JMXApplicationInstIdProperty.equals(paramName)) { 471 return CHARACTER_NOT_COLUMN; 472 } else if (JMXHttpportProperty.equals(paramName)) { 473 return CHARACTER_NOT_COLUMN; 474 } else { 475 return CHARACTER_SHOW_ALL; 476 } 477 } else { 478 return value; 479 } 480 } 481 } 482 483 /** 484 * Remove an application. Used by Monitor-JMXsummary.jsp. Code has been moved from the jsp to here to avoid compile errors at 485 * runtime in correlation with the upgrade to java 1.8 and introduction of embedded tomcat to handle jsp pages. This was previously done via jetty 6. 486 * 487 * @param request http request from selvlet 488 * @param response http session for page context Harveststatus-running.jsp 489 * @param pageContext http session for page context Harveststatus-running.jsp 490 */ 491 public static void RemoveApplication(HttpServletRequest request, HttpServletResponse response, PageContext pageContext) 492 throws IOException, ArgumentNotValid { 493 ArgumentNotValid.checkNotNull(request, "ServletRequest request"); 494 ArgumentNotValid.checkNotNull(response, "ServletRequest response"); 495 ArgumentNotValid.checkNotNull(pageContext, "PageContext pageContext"); 496 497 // remove application if parameter remove is set. 498 String remove = request.getParameter(Constants.REMOVE); 499 if(remove != null) { 500 JMXSummaryUtils.StarredRequest starredRequest = 501 new JMXSummaryUtils.StarredRequest(request); 502 try { 503 JMXSummaryUtils.unregisterJMXInstance( 504 JMXSummaryUtils.STARRABLE_PARAMETERS, starredRequest, 505 pageContext); 506 StringBuilder builder = new StringBuilder("/"); 507 builder.append(JMXSummaryUtils.STATUS_MONITOR_JMXSUMMARY); 508 builder.append("?"); 509 /** 510 * oldquery is set in the link to remove an application from the summary view. It is used to 511 * enable us to return to the previous view after removing an application. 512 */ 513 String oldquery = starredRequest.getParameter("oldquery"); 514 if (oldquery != null) { 515 builder.append(java.net.URLDecoder.decode(oldquery)); 516 } 517 response.sendRedirect(builder.toString()); 518 } catch (ForwardedToErrorPage e) { 519 return; 520 } 521 522 } 523 } 524 525}