001/*
002 * Copyright 2011-2016 UnboundID Corp.
003 *
004 * This program is free software; you can redistribute it and/or modify
005 * it under the terms of the GNU General Public License (GPLv2 only)
006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
007 * as published by the Free Software Foundation.
008 *
009 * This program is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU General Public License for more details.
013 *
014 * You should have received a copy of the GNU General Public License
015 * along with this program; if not, see <http://www.gnu.org/licenses>.
016 */
017
018package com.unboundid.scim.sdk;
019
020import java.io.Serializable;
021import java.util.EnumSet;
022import java.util.Properties;
023import java.util.Set;
024import java.util.StringTokenizer;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import static com.unboundid.scim.sdk.StaticUtils.*;
029
030
031
032/**
033 * This class provides a means of enabling and configuring debugging in the
034 * SCIM SDK.
035 * <BR><BR>
036 * Access to debug information can be enabled through applications that use the
037 * SDK by calling the {@link Debug#setEnabled} methods, or it can also be
038 * enabled without any code changes through the use of system properties.  In
039 * particular, the {@link Debug#PROPERTY_DEBUG_ENABLED},
040 * {@link Debug#PROPERTY_DEBUG_LEVEL}, and {@link Debug#PROPERTY_DEBUG_TYPE}
041 * properties may be used to control debugging without the need to alter any
042 * code within the application that uses the SDK.
043 * <BR><BR>
044 * The SCIM SDK debugging subsystem uses the Java logging framework available
045 * through the {@code java.util.logging} package with a logger name of
046 * "{@code com.unboundid.scim.sdk}".  The {@link Debug#getLogger} method may
047 * be used to access the logger instance used by the SCIM SDK.
048 * <BR><BR>
049 * <H2>Example</H2>
050 * The following example demonstrates the process that may be used to enable
051 * debugging within the SCIM SDK and write information about all messages with
052 * a {@code WARNING} level or higher to a file named "/tmp/test.log":
053 * <PRE>
054 *   Debug.setEnabled(true);
055 *   Logger logger = Debug.getLogger();
056 *
057 *   FileHandler fileHandler = new FileHandler("/tmp/test.log");
058 *   fileHandler.setLevel(Level.WARNING);
059 *   logger.addHandler(fileHandler);
060 * </PRE>
061 */
062public final class Debug
063       implements Serializable
064{
065  /**
066   * The name of the system property that will be used to enable debugging in
067   * the UnboundID SCIM SDK for Java.  The fully-qualified name for this
068   * property is "{@code com.unboundid.scim.sdk.debug.enabled}".  If it is set,
069   * then it should have a value of either "true" or "false".
070   */
071  public static final String PROPERTY_DEBUG_ENABLED =
072       "com.unboundid.scim.sdk.debug.enabled";
073
074
075
076  /**
077   * The name of the system property that may be used to indicate whether stack
078   * trace information for the thread calling the debug method should be
079   * included in debug log messages.  The fully-qualified name for this property
080   * is "{@code com.unboundid.scim.sdk.debug.includeStackTrace}".  If it is set,
081   * then it should have a value of either "true" or "false".
082   */
083  public static final String PROPERTY_INCLUDE_STACK_TRACE =
084       "com.unboundid.scim.sdk.debug.includeStackTrace";
085
086
087
088  /**
089   * The name of the system property that will be used to set the initial level
090   * for the debug logger.  The fully-qualified name for this property is
091   * "{@code com.unboundid.scim.sdk.debug.level}".  If it is set, then it should
092   * be one of the strings "{@code SEVERE}", "{@code WARNING}", "{@code INFO}",
093   * "{@code CONFIG}", "{@code FINE}", "{@code FINER}", or "{@code FINEST}".
094   */
095  public static final String PROPERTY_DEBUG_LEVEL =
096       "com.unboundid.scim.sdk.debug.level";
097
098
099
100  /**
101   * The name of the system property that will be used to indicate that
102   * debugging should be enabled for specific types of messages.  The
103   * fully-qualified name for this property is
104   * "{@code com.unboundid.scim.sdk.debug.type}". If it is set, then it should
105   * be a comma-delimited list of the names of the desired debug types.  See the
106   * {@link DebugType} enum for the available debug types.
107   */
108  public static final String PROPERTY_DEBUG_TYPE =
109       "com.unboundid.scim.sdk.debug.type";
110
111
112
113  /**
114   * The name that will be used for the Java logger that will actually handle
115   * the debug messages if debugging is enabled.
116   */
117  public static final String LOGGER_NAME = "com.unboundid.scim.sdk";
118
119
120
121  /**
122   * The logger that will be used to handle the debug messages if debugging is
123   * enabled.
124   */
125  private static final Logger logger = Logger.getLogger(LOGGER_NAME);
126
127
128
129  /**
130   * The serial version UID for this serializable class.
131   */
132  private static final long serialVersionUID = 1481879450652966238L;
133
134
135
136  // Indicates whether any debugging is currently enabled for the SDK.
137  private static boolean debugEnabled;
138
139  // Indicates whether to capture a thread stack trace whenever a debug message
140  // is logged.
141  private static boolean includeStackTrace;
142
143  // The set of debug types for which debugging is enabled.
144  private static EnumSet<DebugType> debugTypes;
145
146
147
148  static
149  {
150    initialize(System.getProperties());
151  }
152
153
154
155  /**
156   * Prevent this class from being instantiated.
157   */
158  private Debug()
159  {
160    // No implementation is required.
161  }
162
163
164
165  /**
166   * Initializes this debugger with the default settings.  Debugging will be
167   * disabled, the set of debug types will include all types, and the debug
168   * level will be "ALL".
169   */
170  public static void initialize()
171  {
172    includeStackTrace = false;
173    debugEnabled      = false;
174    debugTypes        = EnumSet.allOf(DebugType.class);
175
176    logger.setLevel(Level.ALL);
177  }
178
179
180
181  /**
182   * Initializes this debugger with settings from the provided set of
183   * properties.  Any debug setting that isn't configured in the provided
184   * properties will be initialized with its default value.
185   *
186   * @param  properties  The set of properties to use to initialize this
187   *                     debugger.
188   */
189  public static void initialize(final Properties properties)
190  {
191    // First, apply the default values for the properties.
192    initialize();
193    if ((properties == null) || properties.isEmpty())
194    {
195      // No properties were provided, so we don't need to do anything.
196      return;
197    }
198
199    final String enabledProp = properties.getProperty(PROPERTY_DEBUG_ENABLED);
200    if ((enabledProp != null) && (enabledProp.length() > 0))
201    {
202      if (enabledProp.equalsIgnoreCase("true"))
203      {
204        debugEnabled = true;
205      }
206      else if (enabledProp.equalsIgnoreCase("false"))
207      {
208        debugEnabled = false;
209      }
210      else
211      {
212        throw new IllegalArgumentException("Invalid value '" + enabledProp +
213                                           "' for property " +
214                                           PROPERTY_DEBUG_ENABLED +
215                                           ".  The value must be either " +
216                                           "'true' or 'false'.");
217      }
218    }
219
220    final String stackProp =
221         properties.getProperty(PROPERTY_INCLUDE_STACK_TRACE);
222    if ((stackProp != null) && (stackProp.length() > 0))
223    {
224      if (stackProp.equalsIgnoreCase("true"))
225      {
226        includeStackTrace = true;
227      }
228      else if (stackProp.equalsIgnoreCase("false"))
229      {
230        includeStackTrace = false;
231      }
232      else
233      {
234        throw new IllegalArgumentException("Invalid value '" + stackProp +
235                                           "' for property " +
236                                           PROPERTY_INCLUDE_STACK_TRACE +
237                                           ".  The value must be either " +
238                                           "'true' or 'false'.");
239      }
240    }
241
242    final String typesProp = properties.getProperty(PROPERTY_DEBUG_TYPE);
243    if ((typesProp != null) && (typesProp.length() > 0))
244    {
245      debugTypes = EnumSet.noneOf(DebugType.class);
246      final StringTokenizer t = new StringTokenizer(typesProp, ", ");
247      while (t.hasMoreTokens())
248      {
249        final String debugTypeName = t.nextToken();
250        final DebugType debugType = DebugType.forName(debugTypeName);
251        if (debugType == null)
252        {
253          // Throw a runtime exception to indicate that the debug type is
254          // invalid.
255          throw new IllegalArgumentException("Invalid value '" + debugTypeName +
256                      "' for property " + PROPERTY_DEBUG_TYPE +
257                      ".  Allowed values include:  " +
258                      DebugType.getTypeNameList() + '.');
259        }
260        else
261        {
262          debugTypes.add(debugType);
263        }
264      }
265    }
266
267    final String levelProp = properties.getProperty(PROPERTY_DEBUG_LEVEL);
268    if ((levelProp != null) && (levelProp.length() > 0))
269    {
270      logger.setLevel(Level.parse(levelProp));
271    }
272  }
273
274
275
276  /**
277   * Retrieves the logger that will be used to write the debug messages.
278   *
279   * @return  The logger that will be used to write the debug messages.
280   */
281  public static Logger getLogger()
282  {
283    return logger;
284  }
285
286
287
288  /**
289   * Indicates whether any form of debugging is enabled.
290   *
291   * @return  {@code true} if debugging is enabled, or {@code false} if not.
292   */
293  public static boolean debugEnabled()
294  {
295    return debugEnabled;
296  }
297
298
299
300  /**
301   * Indicates whether debugging is enabled for messages of the specified debug
302   * type.
303   *
304   * @param  debugType  The debug type for which to make the determination.
305   *
306   * @return  {@code true} if debugging is enabled for messages of the specified
307   *          debug type, or {@code false} if not.
308   */
309  public static boolean debugEnabled(final DebugType debugType)
310  {
311    return (debugEnabled && debugTypes.contains(debugType));
312  }
313
314
315
316  /**
317   * Specifies whether debugging should be enabled.  If it should be, then it
318   * will be enabled for all debug types.
319   *
320   * @param  enabled  Specifies whether debugging should be enabled.
321   */
322  public static void setEnabled(final boolean enabled)
323  {
324    debugTypes   = EnumSet.allOf(DebugType.class);
325    debugEnabled = enabled;
326  }
327
328
329
330  /**
331   * Specifies whether debugging should be enabled.  If it should be, then it
332   * will be enabled for all debug types in the provided set.
333   *
334   * @param  enabled  Specifies whether debugging should be enabled.
335   * @param  types    The set of debug types that should be enabled.  It may be
336   *                  {@code null} or empty to indicate that it should be for
337   *                  all debug types.
338   */
339  public static void setEnabled(final boolean enabled,
340                                final Set<DebugType> types)
341  {
342    if ((types == null) || types.isEmpty())
343    {
344      debugTypes = EnumSet.allOf(DebugType.class);
345    }
346    else
347    {
348      debugTypes = EnumSet.copyOf(types);
349    }
350
351    debugEnabled = enabled;
352  }
353
354
355
356  /**
357   * Indicates whether log messages should include a stack trace of the thread
358   * that invoked the debug method.
359   *
360   * @return  {@code true} if log messages should include a stack trace of the
361   *          thread that invoked the debug method, or {@code false} if not.
362   */
363  public static boolean includeStackTrace()
364  {
365    return includeStackTrace;
366  }
367
368
369
370  /**
371   * Specifies whether log messages should include a stack trace of the thread
372   * that invoked the debug method.
373   *
374   * @param  includeStackTrace  Indicates whether log messages should include a
375   *                            stack trace of the thread that invoked the debug
376   *                            method.
377   */
378  public static void setIncludeStackTrace(final boolean includeStackTrace)
379  {
380    Debug.includeStackTrace = includeStackTrace;
381  }
382
383
384
385  /**
386   * Retrieves the set of debug types that will be used if debugging is enabled.
387   *
388   * @return  The set of debug types that will be used if debugging is enabled.
389   */
390  public static EnumSet<DebugType> getDebugTypes()
391  {
392    return debugTypes;
393  }
394
395
396
397  /**
398   * Writes debug information about the provided exception, if appropriate.  If
399   * it is to be logged, then it will be sent to the underlying logger using the
400   * {@code WARNING} level.
401   *
402   * @param  t  The exception for which debug information should be written.
403   */
404  public static void debugException(final Throwable t)
405  {
406    if (debugEnabled && debugTypes.contains(DebugType.EXCEPTION))
407    {
408      debugException(Level.WARNING, t);
409    }
410  }
411
412
413
414  /**
415   * Writes debug information about the provided exception, if appropriate.
416   *
417   * @param  l  The log level that should be used for the debug information.
418   * @param  t  The exception for which debug information should be written.
419   */
420  public static void debugException(final Level l, final Throwable t)
421  {
422    if (debugEnabled && debugTypes.contains(DebugType.EXCEPTION))
423    {
424      final StringBuilder buffer = new StringBuilder();
425      addCommonHeader(buffer, l);
426      buffer.append("caughtException=\"");
427      getStackTrace(t, buffer);
428      buffer.append('"');
429
430      logger.log(l, buffer.toString(), t);
431    }
432  }
433
434
435
436  /**
437   * Writes debug information about a coding error detected in the use of the
438   * SCIM SDK.  If it is to be logged, then it will be sent to the underlying
439   * logger using the {@code SEVERE} level.
440   *
441   * @param  t  The {@code Throwable} object that was created and will be thrown
442   *            as a result of the coding error.
443   */
444  public static void debugCodingError(final Throwable t)
445  {
446    if (debugEnabled && debugTypes.contains(DebugType.CODING_ERROR))
447    {
448      final StringBuilder buffer = new StringBuilder();
449      addCommonHeader(buffer, Level.SEVERE);
450      buffer.append("codingError=\"");
451      getStackTrace(t, buffer);
452      buffer.append('"');
453
454      logger.log(Level.SEVERE, buffer.toString());
455    }
456  }
457
458
459
460  /**
461   * Writes a generic debug message, if appropriate.
462   *
463   * @param  l  The log level that should be used for the debug information.
464   * @param  t  The debug type to use to determine whether to write the message.
465   * @param  m  The message to be written.
466   */
467  public static void debug(final Level l, final DebugType t, final String m)
468  {
469    if (debugEnabled && debugTypes.contains(t))
470    {
471      final StringBuilder buffer = new StringBuilder();
472      addCommonHeader(buffer, l);
473      buffer.append("message=\"");
474      buffer.append(m);
475      buffer.append('"');
476
477      logger.log(l, buffer.toString());
478    }
479  }
480
481
482
483  /**
484   * Writes a generic debug message, if appropriate.
485   *
486   * @param  l  The log level that should be used for the debug information.
487   * @param  t  The debug type to use to determine whether to write the message.
488   * @param  m  The message to be written.
489   * @param  e  An exception to include with the log message.
490   */
491  public static void debug(final Level l, final DebugType t, final String m,
492                           final Throwable e)
493  {
494    if (debugEnabled && debugTypes.contains(t))
495    {
496      final StringBuilder buffer = new StringBuilder();
497      addCommonHeader(buffer, l);
498      buffer.append("message=\"");
499      buffer.append(m);
500      buffer.append('"');
501      buffer.append(" exception=\"");
502      getStackTrace(e, buffer);
503      buffer.append('"');
504
505      logger.log(l, buffer.toString(), e);
506    }
507  }
508
509
510
511  /**
512   * Writes common header information to the provided buffer.  It will include
513   * the thread ID, name, and caller stack trace (optional), and it will be
514   * followed by a trailing space.
515   *
516   * @param  buffer  The buffer to which the information should be appended.
517   * @param  level   The log level for the message that will be written.
518   */
519  private static void addCommonHeader(final StringBuilder buffer,
520                                      final Level level)
521  {
522    buffer.append("level=\"");
523    buffer.append(level.getName());
524    buffer.append("\" threadID=");
525    buffer.append(Thread.currentThread().getId());
526    buffer.append(" threadName=\"");
527    buffer.append(Thread.currentThread().getName());
528
529    if (includeStackTrace)
530    {
531      buffer.append("\" calledFrom=\"");
532
533      boolean appended   = false;
534      boolean foundDebug = false;
535      for (final StackTraceElement e : Thread.currentThread().getStackTrace())
536      {
537        final String className = e.getClassName();
538        if (className.equals(Debug.class.getName()))
539        {
540          foundDebug = true;
541        }
542        else if (foundDebug)
543        {
544          if (appended)
545          {
546            buffer.append(" / ");
547          }
548          appended = true;
549
550          buffer.append(e.getMethodName());
551          buffer.append('(');
552          buffer.append(e.getFileName());
553
554          final int lineNumber = e.getLineNumber();
555          if (lineNumber > 0)
556          {
557            buffer.append(':');
558            buffer.append(lineNumber);
559          }
560          else if (e.isNativeMethod())
561          {
562            buffer.append(":native");
563          }
564
565          buffer.append(')');
566        }
567      }
568    }
569
570    buffer.append("\" revision=");
571    buffer.append(Version.REVISION_ID);
572    buffer.append(' ');
573  }
574}