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 }