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