001 /* 002 * Copyright 2009-2015 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005 /* 006 * Copyright (C) 2015 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 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 Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021 package com.unboundid.ldap.sdk.unboundidds.logs; 022 023 024 025 import java.io.Serializable; 026 import java.text.SimpleDateFormat; 027 import java.util.Collections; 028 import java.util.Date; 029 import java.util.LinkedHashMap; 030 import java.util.LinkedHashSet; 031 import java.util.Set; 032 import java.util.Map; 033 034 import com.unboundid.util.ByteStringBuffer; 035 import com.unboundid.util.NotExtensible; 036 import com.unboundid.util.NotMutable; 037 import com.unboundid.util.ThreadSafety; 038 import com.unboundid.util.ThreadSafetyLevel; 039 040 import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*; 041 import static com.unboundid.util.Debug.*; 042 import static com.unboundid.util.StaticUtils.*; 043 044 045 046 /** 047 * <BLOCKQUOTE> 048 * <B>NOTE:</B> This class is part of the Commercial Edition of the UnboundID 049 * LDAP SDK for Java. It is not available for use in applications that 050 * include only the Standard Edition of the LDAP SDK, and is not supported for 051 * use in conjunction with non-UnboundID products. 052 * </BLOCKQUOTE> 053 * This class provides a data structure that holds information about a log 054 * message contained in a Directory Server access or error log file. 055 */ 056 @NotExtensible() 057 @NotMutable() 058 @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 059 public class LogMessage 060 implements Serializable 061 { 062 /** 063 * The format string that will be used for log message timestamps 064 * with seconds-level precision enabled. 065 */ 066 private static final String TIMESTAMP_SEC_FORMAT = 067 "'['dd/MMM/yyyy:HH:mm:ss Z']'"; 068 069 070 071 /** 072 * The format string that will be used for log message timestamps 073 * with seconds-level precision enabled. 074 */ 075 private static final String TIMESTAMP_MS_FORMAT = 076 "'['dd/MMM/yyyy:HH:mm:ss.SSS Z']'"; 077 078 079 080 /** 081 * The thread-local date formatter. 082 */ 083 private static final ThreadLocal<SimpleDateFormat> dateSecFormat = 084 new ThreadLocal<SimpleDateFormat>(); 085 086 087 088 /** 089 * The thread-local date formatter. 090 */ 091 private static final ThreadLocal<SimpleDateFormat> dateMsFormat = 092 new ThreadLocal<SimpleDateFormat>(); 093 094 095 096 /** 097 * The serial version UID for this serializable class. 098 */ 099 private static final long serialVersionUID = -1210050773534504972L; 100 101 102 103 // The timestamp for this log message. 104 private final Date timestamp; 105 106 // The map of named fields contained in this log message. 107 private final Map<String,String> namedValues; 108 109 // The set of unnamed values contained in this log message. 110 private final Set<String> unnamedValues; 111 112 // The string representation of this log message. 113 private final String messageString; 114 115 116 117 /** 118 * Creates a log message from the provided log message. 119 * 120 * @param m The log message to use to create this log message. 121 */ 122 protected LogMessage(final LogMessage m) 123 { 124 timestamp = m.timestamp; 125 unnamedValues = m.unnamedValues; 126 namedValues = m.namedValues; 127 messageString = m.messageString; 128 } 129 130 131 132 /** 133 * Parses the provided string as a log message. 134 * 135 * @param s The string to be parsed as a log message. 136 * 137 * @throws LogException If the provided string cannot be parsed as a valid 138 * log message. 139 */ 140 protected LogMessage(final String s) 141 throws LogException 142 { 143 messageString = s; 144 145 146 // The first element should be the timestamp, which should end with a 147 // closing bracket. 148 final int bracketPos = s.indexOf(']'); 149 if (bracketPos < 0) 150 { 151 throw new LogException(s, ERR_LOG_MESSAGE_NO_TIMESTAMP.get()); 152 } 153 154 final String timestampString = s.substring(0, bracketPos+1); 155 156 SimpleDateFormat f; 157 if (timestampIncludesMilliseconds(timestampString)) 158 { 159 f = dateMsFormat.get(); 160 if (f == null) 161 { 162 f = new SimpleDateFormat(TIMESTAMP_MS_FORMAT); 163 f.setLenient(false); 164 dateMsFormat.set(f); 165 } 166 } 167 else 168 { 169 f = dateSecFormat.get(); 170 if (f == null) 171 { 172 f = new SimpleDateFormat(TIMESTAMP_SEC_FORMAT); 173 f.setLenient(false); 174 dateSecFormat.set(f); 175 } 176 } 177 178 try 179 { 180 timestamp = f.parse(timestampString); 181 } 182 catch (Exception e) 183 { 184 debugException(e); 185 throw new LogException(s, 186 ERR_LOG_MESSAGE_INVALID_TIMESTAMP.get(getExceptionMessage(e)), e); 187 } 188 189 190 // The remainder of the message should consist of named and unnamed values. 191 final LinkedHashMap<String,String> named = 192 new LinkedHashMap<String,String>(); 193 final LinkedHashSet<String> unnamed = new LinkedHashSet<String>(); 194 parseTokens(s, bracketPos+1, named, unnamed); 195 196 namedValues = Collections.unmodifiableMap(named); 197 unnamedValues = Collections.unmodifiableSet(unnamed); 198 } 199 200 201 202 /** 203 * Parses the set of named and unnamed tokens from the provided message 204 * string. 205 * 206 * @param s The complete message string being parsed. 207 * @param startPos The position at which to start parsing. 208 * @param named The map in which to place the named tokens. 209 * @param unnamed The set in which to place the unnamed tokens. 210 * 211 * @throws LogException If a problem occurs while processing the tokens. 212 */ 213 private static void parseTokens(final String s, final int startPos, 214 final Map<String,String> named, 215 final Set<String> unnamed) 216 throws LogException 217 { 218 boolean inQuotes = false; 219 final StringBuilder buffer = new StringBuilder(); 220 for (int p=startPos; p < s.length(); p++) 221 { 222 final char c = s.charAt(p); 223 if ((c == ' ') && (! inQuotes)) 224 { 225 if (buffer.length() > 0) 226 { 227 processToken(s, buffer.toString(), named, unnamed); 228 buffer.delete(0, buffer.length()); 229 } 230 } 231 else if (c == '"') 232 { 233 inQuotes = (! inQuotes); 234 } 235 else 236 { 237 buffer.append(c); 238 } 239 } 240 241 if (buffer.length() > 0) 242 { 243 processToken(s, buffer.toString(), named, unnamed); 244 } 245 } 246 247 248 249 /** 250 * Processes the provided token and adds it to the appropriate collection. 251 * 252 * @param s The complete message string being parsed. 253 * @param token The token to be processed. 254 * @param named The map in which to place named tokens. 255 * @param unnamed The set in which to place unnamed tokens. 256 * 257 * @throws LogException If a problem occurs while processing the token. 258 */ 259 private static void processToken(final String s, final String token, 260 final Map<String,String> named, 261 final Set<String> unnamed) 262 throws LogException 263 { 264 // If the token contains an equal sign, then it's a named token. Otherwise, 265 // it's unnamed. 266 final int equalPos = token.indexOf('='); 267 if (equalPos < 0) 268 { 269 // Unnamed tokens should never need any additional processing. 270 unnamed.add(token); 271 } 272 else 273 { 274 // The name of named tokens should never need any additional processing. 275 // The value may need to be processed to remove surrounding quotes and/or 276 // to un-escape any special characters. 277 final String name = token.substring(0, equalPos); 278 final String value = processValue(s, token.substring(equalPos+1)); 279 named.put(name, value); 280 } 281 } 282 283 284 285 /** 286 * Performs any processing needed on the provided value to obtain the original 287 * text. This may include removing surrounding quotes and/or un-escaping any 288 * special characters. 289 * 290 * @param s The complete message string being parsed. 291 * @param v The value to be processed. 292 * 293 * @return The processed version of the provided string. 294 * 295 * @throws LogException If a problem occurs while processing the value. 296 */ 297 private static String processValue(final String s, final String v) 298 throws LogException 299 { 300 final ByteStringBuffer b = new ByteStringBuffer(); 301 302 for (int i=0; i < v.length(); i++) 303 { 304 final char c = v.charAt(i); 305 if (c == '"') 306 { 307 // This should only happen at the beginning or end of the string, in 308 // which case it should be stripped out so we don't need to do anything. 309 } 310 else if (c == '#') 311 { 312 // Every octothorpe should be followed by exactly two hex digits, which 313 // represent a byte of a UTF-8 character. 314 if (i > (v.length() - 3)) 315 { 316 throw new LogException(s, 317 ERR_LOG_MESSAGE_INVALID_ESCAPED_CHARACTER.get(v)); 318 } 319 320 byte rawByte = 0x00; 321 for (int j=0; j < 2; j++) 322 { 323 rawByte <<= 4; 324 switch (v.charAt(++i)) 325 { 326 case '0': 327 break; 328 case '1': 329 rawByte |= 0x01; 330 break; 331 case '2': 332 rawByte |= 0x02; 333 break; 334 case '3': 335 rawByte |= 0x03; 336 break; 337 case '4': 338 rawByte |= 0x04; 339 break; 340 case '5': 341 rawByte |= 0x05; 342 break; 343 case '6': 344 rawByte |= 0x06; 345 break; 346 case '7': 347 rawByte |= 0x07; 348 break; 349 case '8': 350 rawByte |= 0x08; 351 break; 352 case '9': 353 rawByte |= 0x09; 354 break; 355 case 'a': 356 case 'A': 357 rawByte |= 0x0A; 358 break; 359 case 'b': 360 case 'B': 361 rawByte |= 0x0B; 362 break; 363 case 'c': 364 case 'C': 365 rawByte |= 0x0C; 366 break; 367 case 'd': 368 case 'D': 369 rawByte |= 0x0D; 370 break; 371 case 'e': 372 case 'E': 373 rawByte |= 0x0E; 374 break; 375 case 'f': 376 case 'F': 377 rawByte |= 0x0F; 378 break; 379 default: 380 throw new LogException(s, 381 ERR_LOG_MESSAGE_INVALID_ESCAPED_CHARACTER.get(v)); 382 } 383 } 384 385 b.append(rawByte); 386 } 387 else 388 { 389 b.append(c); 390 } 391 } 392 393 return b.toString(); 394 } 395 396 397 /** 398 * Determines whether a string that represents a timestamp includes a 399 * millisecond component. 400 * 401 * @param timestamp The timestamp string to examine. 402 * 403 * @return {@code true} if the given string includes a millisecond component, 404 * or {@code false} if not. 405 */ 406 private static boolean timestampIncludesMilliseconds(final String timestamp) 407 { 408 // The sec and ms format strings differ at the 22nd character. 409 return ((timestamp.length() > 21) && (timestamp.charAt(21) == '.')); 410 } 411 412 413 414 /** 415 * Retrieves the timestamp for this log message. 416 * 417 * @return The timestamp for this log message. 418 */ 419 public final Date getTimestamp() 420 { 421 return timestamp; 422 } 423 424 425 426 /** 427 * Retrieves the set of named tokens for this log message, mapped from the 428 * name to the corresponding value. 429 * 430 * @return The set of named tokens for this log message. 431 */ 432 public final Map<String,String> getNamedValues() 433 { 434 return namedValues; 435 } 436 437 438 439 /** 440 * Retrieves the value of the token with the specified name. 441 * 442 * @param name The name of the token to retrieve. 443 * 444 * @return The value of the token with the specified name, or {@code null} if 445 * there is no value with the specified name. 446 */ 447 public final String getNamedValue(final String name) 448 { 449 return namedValues.get(name); 450 } 451 452 453 454 /** 455 * Retrieves the value of the token with the specified name as a 456 * {@code Boolean}. 457 * 458 * @param name The name of the token to retrieve. 459 * 460 * @return The value of the token with the specified name as a 461 * {@code Boolean}, or {@code null} if there is no value with the 462 * specified name or the value cannot be parsed as a {@code Boolean}. 463 */ 464 public final Boolean getNamedValueAsBoolean(final String name) 465 { 466 final String s = namedValues.get(name); 467 if (s == null) 468 { 469 return null; 470 } 471 472 final String lowerValue = toLowerCase(s); 473 if (lowerValue.equals("true") || lowerValue.equals("t") || 474 lowerValue.equals("yes") || lowerValue.equals("y") || 475 lowerValue.equals("on") || lowerValue.equals("1")) 476 { 477 return Boolean.TRUE; 478 } 479 else if (lowerValue.equals("false") || lowerValue.equals("f") || 480 lowerValue.equals("no") || lowerValue.equals("n") || 481 lowerValue.equals("off") || lowerValue.equals("0")) 482 { 483 return Boolean.FALSE; 484 } 485 else 486 { 487 return null; 488 } 489 } 490 491 492 493 /** 494 * Retrieves the value of the token with the specified name as a 495 * {@code Double}. 496 * 497 * @param name The name of the token to retrieve. 498 * 499 * @return The value of the token with the specified name as a 500 * {@code Double}, or {@code null} if there is no value with the 501 * specified name or the value cannot be parsed as a {@code Double}. 502 */ 503 public final Double getNamedValueAsDouble(final String name) 504 { 505 final String s = namedValues.get(name); 506 if (s == null) 507 { 508 return null; 509 } 510 511 try 512 { 513 return Double.valueOf(s); 514 } 515 catch (Exception e) 516 { 517 debugException(e); 518 return null; 519 } 520 } 521 522 523 524 /** 525 * Retrieves the value of the token with the specified name as an 526 * {@code Integer}. 527 * 528 * @param name The name of the token to retrieve. 529 * 530 * @return The value of the token with the specified name as an 531 * {@code Integer}, or {@code null} if there is no value with the 532 * specified name or the value cannot be parsed as an 533 * {@code Integer}. 534 */ 535 public final Integer getNamedValueAsInteger(final String name) 536 { 537 final String s = namedValues.get(name); 538 if (s == null) 539 { 540 return null; 541 } 542 543 try 544 { 545 return Integer.valueOf(s); 546 } 547 catch (Exception e) 548 { 549 debugException(e); 550 return null; 551 } 552 } 553 554 555 556 /** 557 * Retrieves the value of the token with the specified name as a {@code Long}. 558 * 559 * @param name The name of the token to retrieve. 560 * 561 * @return The value of the token with the specified name as a {@code Long}, 562 * or {@code null} if there is no value with the specified name or 563 * the value cannot be parsed as a {@code Long}. 564 */ 565 public final Long getNamedValueAsLong(final String name) 566 { 567 final String s = namedValues.get(name); 568 if (s == null) 569 { 570 return null; 571 } 572 573 try 574 { 575 return Long.valueOf(s); 576 } 577 catch (Exception e) 578 { 579 debugException(e); 580 return null; 581 } 582 } 583 584 585 586 /** 587 * Retrieves the set of unnamed tokens for this log message. 588 * 589 * @return The set of unnamed tokens for this log message. 590 */ 591 public final Set<String> getUnnamedValues() 592 { 593 return unnamedValues; 594 } 595 596 597 598 /** 599 * Indicates whether this log message has the specified unnamed value. 600 * 601 * @param value The value for which to make the determination. 602 * 603 * @return {@code true} if this log message has the specified unnamed value, 604 * or {@code false} if not. 605 */ 606 public final boolean hasUnnamedValue(final String value) 607 { 608 return unnamedValues.contains(value); 609 } 610 611 612 613 /** 614 * Retrieves a string representation of this log message. 615 * 616 * @return A string representation of this log message. 617 */ 618 @Override() 619 public final String toString() 620 { 621 return messageString; 622 } 623 }