001/* 002 * Copyright 2009-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2015-2019 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.Debug; 036import com.unboundid.util.NotExtensible; 037import com.unboundid.util.NotMutable; 038import com.unboundid.util.StaticUtils; 039import com.unboundid.util.ThreadSafety; 040import com.unboundid.util.ThreadSafetyLevel; 041 042import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*; 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 054 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 055 * for proprietary functionality or for external specifications that are not 056 * considered stable or mature enough to be guaranteed to work in an 057 * interoperable way with 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<>(); 089 090 091 092 /** 093 * The thread-local date formatter. 094 */ 095 private static final ThreadLocal<SimpleDateFormat> dateMsFormat = 096 new ThreadLocal<>(); 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 Debug.debugException(e); 189 throw new LogException(s, 190 ERR_LOG_MESSAGE_INVALID_TIMESTAMP.get( 191 StaticUtils.getExceptionMessage(e)), 192 e); 193 } 194 195 196 // The remainder of the message should consist of named and unnamed values. 197 final LinkedHashMap<String,String> named = 198 new LinkedHashMap<>(StaticUtils.computeMapCapacity(10)); 199 final LinkedHashSet<String> unnamed = 200 new LinkedHashSet<>(StaticUtils.computeMapCapacity(10)); 201 parseTokens(s, bracketPos+1, named, unnamed); 202 203 namedValues = Collections.unmodifiableMap(named); 204 unnamedValues = Collections.unmodifiableSet(unnamed); 205 } 206 207 208 209 /** 210 * Parses the set of named and unnamed tokens from the provided message 211 * string. 212 * 213 * @param s The complete message string being parsed. 214 * @param startPos The position at which to start parsing. 215 * @param named The map in which to place the named tokens. 216 * @param unnamed The set in which to place the unnamed tokens. 217 * 218 * @throws LogException If a problem occurs while processing the tokens. 219 */ 220 private static void parseTokens(final String s, final int startPos, 221 final Map<String,String> named, 222 final Set<String> unnamed) 223 throws LogException 224 { 225 boolean inQuotes = false; 226 final StringBuilder buffer = new StringBuilder(); 227 for (int p=startPos; p < s.length(); p++) 228 { 229 final char c = s.charAt(p); 230 if ((c == ' ') && (! inQuotes)) 231 { 232 if (buffer.length() > 0) 233 { 234 processToken(s, buffer.toString(), named, unnamed); 235 buffer.delete(0, buffer.length()); 236 } 237 } 238 else if (c == '"') 239 { 240 inQuotes = (! inQuotes); 241 } 242 else 243 { 244 buffer.append(c); 245 } 246 } 247 248 if (buffer.length() > 0) 249 { 250 processToken(s, buffer.toString(), named, unnamed); 251 } 252 } 253 254 255 256 /** 257 * Processes the provided token and adds it to the appropriate collection. 258 * 259 * @param s The complete message string being parsed. 260 * @param token The token to be processed. 261 * @param named The map in which to place named tokens. 262 * @param unnamed The set in which to place unnamed tokens. 263 * 264 * @throws LogException If a problem occurs while processing the token. 265 */ 266 private static void processToken(final String s, final String token, 267 final Map<String,String> named, 268 final Set<String> unnamed) 269 throws LogException 270 { 271 // If the token contains an equal sign, then it's a named token. Otherwise, 272 // it's unnamed. 273 final int equalPos = token.indexOf('='); 274 if (equalPos < 0) 275 { 276 // Unnamed tokens should never need any additional processing. 277 unnamed.add(token); 278 } 279 else 280 { 281 // The name of named tokens should never need any additional processing. 282 // The value may need to be processed to remove surrounding quotes and/or 283 // to un-escape any special characters. 284 final String name = token.substring(0, equalPos); 285 final String value = processValue(s, token.substring(equalPos+1)); 286 named.put(name, value); 287 } 288 } 289 290 291 292 /** 293 * Performs any processing needed on the provided value to obtain the original 294 * text. This may include removing surrounding quotes and/or un-escaping any 295 * special characters. 296 * 297 * @param s The complete message string being parsed. 298 * @param v The value to be processed. 299 * 300 * @return The processed version of the provided string. 301 * 302 * @throws LogException If a problem occurs while processing the value. 303 */ 304 private static String processValue(final String s, final String v) 305 throws LogException 306 { 307 final ByteStringBuffer b = new ByteStringBuffer(); 308 309 for (int i=0; i < v.length(); i++) 310 { 311 final char c = v.charAt(i); 312 if (c == '"') 313 { 314 // This should only happen at the beginning or end of the string, in 315 // which case it should be stripped out so we don't need to do anything. 316 } 317 else if (c == '#') 318 { 319 // Every octothorpe should be followed by exactly two hex digits, which 320 // represent a byte of a UTF-8 character. 321 if (i > (v.length() - 3)) 322 { 323 throw new LogException(s, 324 ERR_LOG_MESSAGE_INVALID_ESCAPED_CHARACTER.get(v)); 325 } 326 327 byte rawByte = 0x00; 328 for (int j=0; j < 2; j++) 329 { 330 rawByte <<= 4; 331 switch (v.charAt(++i)) 332 { 333 case '0': 334 break; 335 case '1': 336 rawByte |= 0x01; 337 break; 338 case '2': 339 rawByte |= 0x02; 340 break; 341 case '3': 342 rawByte |= 0x03; 343 break; 344 case '4': 345 rawByte |= 0x04; 346 break; 347 case '5': 348 rawByte |= 0x05; 349 break; 350 case '6': 351 rawByte |= 0x06; 352 break; 353 case '7': 354 rawByte |= 0x07; 355 break; 356 case '8': 357 rawByte |= 0x08; 358 break; 359 case '9': 360 rawByte |= 0x09; 361 break; 362 case 'a': 363 case 'A': 364 rawByte |= 0x0A; 365 break; 366 case 'b': 367 case 'B': 368 rawByte |= 0x0B; 369 break; 370 case 'c': 371 case 'C': 372 rawByte |= 0x0C; 373 break; 374 case 'd': 375 case 'D': 376 rawByte |= 0x0D; 377 break; 378 case 'e': 379 case 'E': 380 rawByte |= 0x0E; 381 break; 382 case 'f': 383 case 'F': 384 rawByte |= 0x0F; 385 break; 386 default: 387 throw new LogException(s, 388 ERR_LOG_MESSAGE_INVALID_ESCAPED_CHARACTER.get(v)); 389 } 390 } 391 392 b.append(rawByte); 393 } 394 else 395 { 396 b.append(c); 397 } 398 } 399 400 return b.toString(); 401 } 402 403 404 /** 405 * Determines whether a string that represents a timestamp includes a 406 * millisecond component. 407 * 408 * @param timestamp The timestamp string to examine. 409 * 410 * @return {@code true} if the given string includes a millisecond component, 411 * or {@code false} if not. 412 */ 413 private static boolean timestampIncludesMilliseconds(final String timestamp) 414 { 415 // The sec and ms format strings differ at the 22nd character. 416 return ((timestamp.length() > 21) && (timestamp.charAt(21) == '.')); 417 } 418 419 420 421 /** 422 * Retrieves the timestamp for this log message. 423 * 424 * @return The timestamp for this log message. 425 */ 426 public final Date getTimestamp() 427 { 428 return timestamp; 429 } 430 431 432 433 /** 434 * Retrieves the set of named tokens for this log message, mapped from the 435 * name to the corresponding value. 436 * 437 * @return The set of named tokens for this log message. 438 */ 439 public final Map<String,String> getNamedValues() 440 { 441 return namedValues; 442 } 443 444 445 446 /** 447 * Retrieves the value of the token with the specified name. 448 * 449 * @param name The name of the token to retrieve. 450 * 451 * @return The value of the token with the specified name, or {@code null} if 452 * there is no value with the specified name. 453 */ 454 public final String getNamedValue(final String name) 455 { 456 return namedValues.get(name); 457 } 458 459 460 461 /** 462 * Retrieves the value of the token with the specified name as a 463 * {@code Boolean}. 464 * 465 * @param name The name of the token to retrieve. 466 * 467 * @return The value of the token with the specified name as a 468 * {@code Boolean}, or {@code null} if there is no value with the 469 * specified name or the value cannot be parsed as a {@code Boolean}. 470 */ 471 public final Boolean getNamedValueAsBoolean(final String name) 472 { 473 final String s = namedValues.get(name); 474 if (s == null) 475 { 476 return null; 477 } 478 479 final String lowerValue = StaticUtils.toLowerCase(s); 480 if (lowerValue.equals("true") || lowerValue.equals("t") || 481 lowerValue.equals("yes") || lowerValue.equals("y") || 482 lowerValue.equals("on") || lowerValue.equals("1")) 483 { 484 return Boolean.TRUE; 485 } 486 else if (lowerValue.equals("false") || lowerValue.equals("f") || 487 lowerValue.equals("no") || lowerValue.equals("n") || 488 lowerValue.equals("off") || lowerValue.equals("0")) 489 { 490 return Boolean.FALSE; 491 } 492 else 493 { 494 return null; 495 } 496 } 497 498 499 500 /** 501 * Retrieves the value of the token with the specified name as a 502 * {@code Double}. 503 * 504 * @param name The name of the token to retrieve. 505 * 506 * @return The value of the token with the specified name as a 507 * {@code Double}, or {@code null} if there is no value with the 508 * specified name or the value cannot be parsed as a {@code Double}. 509 */ 510 public final Double getNamedValueAsDouble(final String name) 511 { 512 final String s = namedValues.get(name); 513 if (s == null) 514 { 515 return null; 516 } 517 518 try 519 { 520 return Double.valueOf(s); 521 } 522 catch (final Exception e) 523 { 524 Debug.debugException(e); 525 return null; 526 } 527 } 528 529 530 531 /** 532 * Retrieves the value of the token with the specified name as an 533 * {@code Integer}. 534 * 535 * @param name The name of the token to retrieve. 536 * 537 * @return The value of the token with the specified name as an 538 * {@code Integer}, or {@code null} if there is no value with the 539 * specified name or the value cannot be parsed as an 540 * {@code Integer}. 541 */ 542 public final Integer getNamedValueAsInteger(final String name) 543 { 544 final String s = namedValues.get(name); 545 if (s == null) 546 { 547 return null; 548 } 549 550 try 551 { 552 return Integer.valueOf(s); 553 } 554 catch (final Exception e) 555 { 556 Debug.debugException(e); 557 return null; 558 } 559 } 560 561 562 563 /** 564 * Retrieves the value of the token with the specified name as a {@code Long}. 565 * 566 * @param name The name of the token to retrieve. 567 * 568 * @return The value of the token with the specified name as a {@code Long}, 569 * or {@code null} if there is no value with the specified name or 570 * the value cannot be parsed as a {@code Long}. 571 */ 572 public final Long getNamedValueAsLong(final String name) 573 { 574 final String s = namedValues.get(name); 575 if (s == null) 576 { 577 return null; 578 } 579 580 try 581 { 582 return Long.valueOf(s); 583 } 584 catch (final Exception e) 585 { 586 Debug.debugException(e); 587 return null; 588 } 589 } 590 591 592 593 /** 594 * Retrieves the set of unnamed tokens for this log message. 595 * 596 * @return The set of unnamed tokens for this log message. 597 */ 598 public final Set<String> getUnnamedValues() 599 { 600 return unnamedValues; 601 } 602 603 604 605 /** 606 * Indicates whether this log message has the specified unnamed value. 607 * 608 * @param value The value for which to make the determination. 609 * 610 * @return {@code true} if this log message has the specified unnamed value, 611 * or {@code false} if not. 612 */ 613 public final boolean hasUnnamedValue(final String value) 614 { 615 return unnamedValues.contains(value); 616 } 617 618 619 620 /** 621 * Retrieves a string representation of this log message. 622 * 623 * @return A string representation of this log message. 624 */ 625 @Override() 626 public final String toString() 627 { 628 return messageString; 629 } 630}