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.NotExtensible;
036import com.unboundid.util.NotMutable;
037import com.unboundid.util.ThreadSafety;
038import com.unboundid.util.ThreadSafetyLevel;
039
040import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*;
041import static com.unboundid.util.Debug.*;
042import static com.unboundid.util.StaticUtils.*;
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 Alcatel-Lucent 8661
054 *   server products.  These classes provide support for proprietary
055 *   functionality or for external specifications that are not considered stable
056 *   or mature enough to be guaranteed to work in an interoperable way with
057 *   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<SimpleDateFormat>();
089
090
091
092  /**
093   * The thread-local date formatter.
094   */
095  private static final ThreadLocal<SimpleDateFormat> dateMsFormat =
096       new ThreadLocal<SimpleDateFormat>();
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      debugException(e);
189      throw new LogException(s,
190           ERR_LOG_MESSAGE_INVALID_TIMESTAMP.get(getExceptionMessage(e)), e);
191    }
192
193
194    // The remainder of the message should consist of named and unnamed values.
195    final LinkedHashMap<String,String> named =
196         new LinkedHashMap<String,String>();
197    final LinkedHashSet<String> unnamed = new LinkedHashSet<String>();
198    parseTokens(s, bracketPos+1, named, unnamed);
199
200    namedValues   = Collections.unmodifiableMap(named);
201    unnamedValues = Collections.unmodifiableSet(unnamed);
202  }
203
204
205
206  /**
207   * Parses the set of named and unnamed tokens from the provided message
208   * string.
209   *
210   * @param  s         The complete message string being parsed.
211   * @param  startPos  The position at which to start parsing.
212   * @param  named     The map in which to place the named tokens.
213   * @param  unnamed   The set in which to place the unnamed tokens.
214   *
215   * @throws  LogException  If a problem occurs while processing the tokens.
216   */
217  private static void parseTokens(final String s, final int startPos,
218                                  final Map<String,String> named,
219                                  final Set<String> unnamed)
220          throws LogException
221  {
222    boolean inQuotes = false;
223    final StringBuilder buffer = new StringBuilder();
224    for (int p=startPos; p < s.length(); p++)
225    {
226      final char c = s.charAt(p);
227      if ((c == ' ') && (! inQuotes))
228      {
229        if (buffer.length() > 0)
230        {
231          processToken(s, buffer.toString(), named, unnamed);
232          buffer.delete(0, buffer.length());
233        }
234      }
235      else if (c == '"')
236      {
237        inQuotes = (! inQuotes);
238      }
239      else
240      {
241        buffer.append(c);
242      }
243    }
244
245    if (buffer.length() > 0)
246    {
247      processToken(s, buffer.toString(), named, unnamed);
248    }
249  }
250
251
252
253  /**
254   * Processes the provided token and adds it to the appropriate collection.
255   *
256   * @param  s         The complete message string being parsed.
257   * @param  token     The token to be processed.
258   * @param  named     The map in which to place named tokens.
259   * @param  unnamed   The set in which to place unnamed tokens.
260   *
261   * @throws  LogException  If a problem occurs while processing the token.
262   */
263  private static void processToken(final String s, final String token,
264                                   final Map<String,String> named,
265                                   final Set<String> unnamed)
266          throws LogException
267  {
268    // If the token contains an equal sign, then it's a named token.  Otherwise,
269    // it's unnamed.
270    final int equalPos = token.indexOf('=');
271    if (equalPos < 0)
272    {
273      // Unnamed tokens should never need any additional processing.
274      unnamed.add(token);
275    }
276    else
277    {
278      // The name of named tokens should never need any additional processing.
279      // The value may need to be processed to remove surrounding quotes and/or
280      // to un-escape any special characters.
281      final String name  = token.substring(0, equalPos);
282      final String value = processValue(s, token.substring(equalPos+1));
283      named.put(name, value);
284    }
285  }
286
287
288
289  /**
290   * Performs any processing needed on the provided value to obtain the original
291   * text.  This may include removing surrounding quotes and/or un-escaping any
292   * special characters.
293   *
294   * @param  s  The complete message string being parsed.
295   * @param  v  The value to be processed.
296   *
297   * @return  The processed version of the provided string.
298   *
299   * @throws  LogException  If a problem occurs while processing the value.
300   */
301  private static String processValue(final String s, final String v)
302          throws LogException
303  {
304    final ByteStringBuffer b = new ByteStringBuffer();
305
306    for (int i=0; i < v.length(); i++)
307    {
308      final char c = v.charAt(i);
309      if (c == '"')
310      {
311        // This should only happen at the beginning or end of the string, in
312        // which case it should be stripped out so we don't need to do anything.
313      }
314      else if (c == '#')
315      {
316        // Every octothorpe should be followed by exactly two hex digits, which
317        // represent a byte of a UTF-8 character.
318        if (i > (v.length() - 3))
319        {
320          throw new LogException(s,
321               ERR_LOG_MESSAGE_INVALID_ESCAPED_CHARACTER.get(v));
322        }
323
324        byte rawByte = 0x00;
325        for (int j=0; j < 2; j++)
326        {
327          rawByte <<= 4;
328          switch (v.charAt(++i))
329          {
330            case '0':
331              break;
332            case '1':
333              rawByte |= 0x01;
334              break;
335            case '2':
336              rawByte |= 0x02;
337              break;
338            case '3':
339              rawByte |= 0x03;
340              break;
341            case '4':
342              rawByte |= 0x04;
343              break;
344            case '5':
345              rawByte |= 0x05;
346              break;
347            case '6':
348              rawByte |= 0x06;
349              break;
350            case '7':
351              rawByte |= 0x07;
352              break;
353            case '8':
354              rawByte |= 0x08;
355              break;
356            case '9':
357              rawByte |= 0x09;
358              break;
359            case 'a':
360            case 'A':
361              rawByte |= 0x0A;
362              break;
363            case 'b':
364            case 'B':
365              rawByte |= 0x0B;
366              break;
367            case 'c':
368            case 'C':
369              rawByte |= 0x0C;
370              break;
371            case 'd':
372            case 'D':
373              rawByte |= 0x0D;
374              break;
375            case 'e':
376            case 'E':
377              rawByte |= 0x0E;
378              break;
379            case 'f':
380            case 'F':
381              rawByte |= 0x0F;
382              break;
383            default:
384              throw new LogException(s,
385                   ERR_LOG_MESSAGE_INVALID_ESCAPED_CHARACTER.get(v));
386          }
387        }
388
389        b.append(rawByte);
390      }
391      else
392      {
393        b.append(c);
394      }
395    }
396
397    return b.toString();
398  }
399
400
401  /**
402   * Determines whether a string that represents a timestamp includes a
403   * millisecond component.
404   *
405   * @param  timestamp   The timestamp string to examine.
406   *
407   * @return  {@code true} if the given string includes a millisecond component,
408   *          or {@code false} if not.
409   */
410  private static boolean timestampIncludesMilliseconds(final String timestamp)
411  {
412    // The sec and ms format strings differ at the 22nd character.
413    return ((timestamp.length() > 21) && (timestamp.charAt(21) == '.'));
414  }
415
416
417
418  /**
419   * Retrieves the timestamp for this log message.
420   *
421   * @return  The timestamp for this log message.
422   */
423  public final Date getTimestamp()
424  {
425    return timestamp;
426  }
427
428
429
430  /**
431   * Retrieves the set of named tokens for this log message, mapped from the
432   * name to the corresponding value.
433   *
434   * @return  The set of named tokens for this log message.
435   */
436  public final Map<String,String> getNamedValues()
437  {
438    return namedValues;
439  }
440
441
442
443  /**
444   * Retrieves the value of the token with the specified name.
445   *
446   * @param  name  The name of the token to retrieve.
447   *
448   * @return  The value of the token with the specified name, or {@code null} if
449   *          there is no value with the specified name.
450   */
451  public final String getNamedValue(final String name)
452  {
453    return namedValues.get(name);
454  }
455
456
457
458  /**
459   * Retrieves the value of the token with the specified name as a
460   * {@code Boolean}.
461   *
462   * @param  name  The name of the token to retrieve.
463   *
464   * @return  The value of the token with the specified name as a
465   *          {@code Boolean}, or {@code null} if there is no value with the
466   *          specified name or the value cannot be parsed as a {@code Boolean}.
467   */
468  public final Boolean getNamedValueAsBoolean(final String name)
469  {
470    final String s = namedValues.get(name);
471    if (s == null)
472    {
473      return null;
474    }
475
476    final String lowerValue = toLowerCase(s);
477    if (lowerValue.equals("true") || lowerValue.equals("t") ||
478        lowerValue.equals("yes") || lowerValue.equals("y") ||
479        lowerValue.equals("on") || lowerValue.equals("1"))
480    {
481      return Boolean.TRUE;
482    }
483    else if (lowerValue.equals("false") || lowerValue.equals("f") ||
484             lowerValue.equals("no") || lowerValue.equals("n") ||
485             lowerValue.equals("off") || lowerValue.equals("0"))
486    {
487      return Boolean.FALSE;
488    }
489    else
490    {
491      return null;
492    }
493  }
494
495
496
497  /**
498   * Retrieves the value of the token with the specified name as a
499   * {@code Double}.
500   *
501   * @param  name  The name of the token to retrieve.
502   *
503   * @return  The value of the token with the specified name as a
504   *          {@code Double}, or {@code null} if there is no value with the
505   *          specified name or the value cannot be parsed as a {@code Double}.
506   */
507  public final Double getNamedValueAsDouble(final String name)
508  {
509    final String s = namedValues.get(name);
510    if (s == null)
511    {
512      return null;
513    }
514
515    try
516    {
517      return Double.valueOf(s);
518    }
519    catch (final Exception e)
520    {
521      debugException(e);
522      return null;
523    }
524  }
525
526
527
528  /**
529   * Retrieves the value of the token with the specified name as an
530   * {@code Integer}.
531   *
532   * @param  name  The name of the token to retrieve.
533   *
534   * @return  The value of the token with the specified name as an
535   *          {@code Integer}, or {@code null} if there is no value with the
536   *          specified name or the value cannot be parsed as an
537   *          {@code Integer}.
538   */
539  public final Integer getNamedValueAsInteger(final String name)
540  {
541    final String s = namedValues.get(name);
542    if (s == null)
543    {
544      return null;
545    }
546
547    try
548    {
549      return Integer.valueOf(s);
550    }
551    catch (final Exception e)
552    {
553      debugException(e);
554      return null;
555    }
556  }
557
558
559
560  /**
561   * Retrieves the value of the token with the specified name as a {@code Long}.
562   *
563   * @param  name  The name of the token to retrieve.
564   *
565   * @return  The value of the token with the specified name as a {@code Long},
566   *          or {@code null} if there is no value with the specified name or
567   *          the value cannot be parsed as a {@code Long}.
568   */
569  public final Long getNamedValueAsLong(final String name)
570  {
571    final String s = namedValues.get(name);
572    if (s == null)
573    {
574      return null;
575    }
576
577    try
578    {
579      return Long.valueOf(s);
580    }
581    catch (final Exception e)
582    {
583      debugException(e);
584      return null;
585    }
586  }
587
588
589
590  /**
591   * Retrieves the set of unnamed tokens for this log message.
592   *
593   * @return  The set of unnamed tokens for this log message.
594   */
595  public final Set<String> getUnnamedValues()
596  {
597    return unnamedValues;
598  }
599
600
601
602  /**
603   * Indicates whether this log message has the specified unnamed value.
604   *
605   * @param  value  The value for which to make the determination.
606   *
607   * @return  {@code true} if this log message has the specified unnamed value,
608   *          or {@code false} if not.
609   */
610  public final boolean hasUnnamedValue(final String value)
611  {
612    return unnamedValues.contains(value);
613  }
614
615
616
617  /**
618   * Retrieves a string representation of this log message.
619   *
620   * @return  A string representation of this log message.
621   */
622  @Override()
623  public final String toString()
624  {
625    return messageString;
626  }
627}