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}