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