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