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