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