001    /*
002     * Copyright 2011-2012 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    
018    package com.unboundid.scim.sdk;
019    
020    import java.io.Serializable;
021    import java.util.EnumSet;
022    import java.util.Properties;
023    import java.util.Set;
024    import java.util.StringTokenizer;
025    import java.util.logging.Level;
026    import java.util.logging.Logger;
027    
028    import 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     */
062    public 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_NUMBER);
572        buffer.append(' ');
573      }
574    }