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}