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}