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