001package com.identityworksllc.iiq.common.logging;
002
003import bsh.This;
004import org.apache.commons.collections.map.ListOrderedMap;
005import org.apache.commons.logging.Log;
006import org.apache.commons.logging.LogFactory;
007import org.w3c.dom.Document;
008import sailpoint.api.SailPointContext;
009import sailpoint.object.*;
010import sailpoint.tools.GeneralException;
011import sailpoint.tools.xml.AbstractXmlObject;
012
013import javax.xml.transform.OutputKeys;
014import javax.xml.transform.Transformer;
015import javax.xml.transform.TransformerFactory;
016import javax.xml.transform.dom.DOMSource;
017import javax.xml.transform.stream.StreamResult;
018import java.io.ByteArrayOutputStream;
019import java.io.IOException;
020import java.io.PrintStream;
021import java.io.PrintWriter;
022import java.io.StringWriter;
023import java.lang.reflect.Array;
024import java.text.DateFormat;
025import java.text.MessageFormat;
026import java.text.SimpleDateFormat;
027import java.time.ZonedDateTime;
028import java.util.*;
029import java.util.concurrent.Callable;
030import java.util.concurrent.atomic.AtomicReference;
031import java.util.function.Supplier;
032
033/**
034 * A wrapper around the Commons Logging {@link Log} class that supplements its
035 * features with some extras available in other logging libraries. Since this
036 * class itself implements {@link Log}, Beanshell should not care if you simply
037 * overwrite the 'log' variable in your code.
038 *
039 * Log strings are interpreted as Java {@link MessageFormat} objects and have
040 * the various features of that class in your JDK version.
041 *
042 * Values passed as arguments are only evaluated when the appropriate log
043 * level is active. If the log level is not active, the operation becomes a
044 * quick no-op. This prevents a lot of isDebugEnabled() type checks.
045 *
046 * You can also pass a {@link Supplier} to any argument, and the supplier's
047 * get() method will be called to derive the value, but only if the log level
048 * is active. This allows you to wrap complex operations in a lambda and
049 * only have them executed if the log level is active.
050 *
051 * If you invoke {@link #capture()}, all log messages will be captured in a
052 * thread-local buffer, which can be retrieved with {@link #getCapturedLogs()}.
053 * You should always invoke {@link #reset()} in a finally block to ensure that
054 * the thread-local buffer is cleared. Capture state is shared at the global
055 * level for a particular thread, so all instances of SLogger, regardless
056 * of classloader, will share the same capture buffer.
057 *
058 * In addition to the usual Log levels, this class supports the following levels:
059 * - HERE: A log message indicating that the code has reached a certain point. This is mostly useful for tracing execution.
060 * - ENTER: A log message indicating that the code is entering a certain segment, such as a method. This is logged at DEBUG level.
061 * - EXIT: A log message indicating that the code is exiting a certain segment, such as a method. This is logged at DEBUG level.
062 *
063 * The 'S' stands for Super. Super Logger.
064 */
065public class SLogger implements org.apache.commons.logging.Log {
066
067        /**
068         * An enumeration of log levels to replace the one in log4j
069         */
070        public enum Level {
071                /**
072                 * Trace level logs
073                 */
074                TRACE,
075                /**
076                 * Debug level logs
077                 */
078                DEBUG,
079                /**
080                 * Info level logs
081                 */
082                INFO,
083                /**
084                 * Warning level logs
085                 */
086                WARN,
087                /**
088                 * Error level logs
089                 */
090                ERROR,
091                /**
092                 * Fatal level logs
093                 */
094                FATAL,
095                /**
096                 * Log messages indicating that the code has reached a certain point
097                 */
098                HERE,
099                /**
100                 * Log messages indicating that the code is entering a certain segment
101                 */
102                ENTER,
103                /**
104                 * Log messages indicating that the code is exiting a certain segment
105                 */
106                EXIT
107        }
108
109    /**
110         * Container class to format an object for logging. The format is only derived
111         * when {@link Formatter#toString()} is called, meaning that if you log one of these
112         * and the log level is not enabled, a slow string conversion will never occur.
113         *
114         * Null values are transformed into the special string '(null)'.
115         *
116         * Formatted values are cached after the first format operation, even if the
117         * underlying object is modified.
118         *
119         * The following types are handled by the Formatter:
120         *
121         * - null
122         * - Strings
123         * - Arrays of Objects
124         * - Arrays of StackTraceElements
125         * - Collections of Objects
126         * - Maps
127         * - Dates and Calendars
128         * - XML {@link Document}s
129         * - Various SailPointObjects
130         *
131         * Nested objects (e.g., the items in a list) are also passed through Formatter.
132         */
133        public static class Formatter implements Supplier<String> {
134        /**
135         * Formats the given object for logging
136         * @param obj The object to format
137         * @return The formatted object
138         */
139        public static String format(Object obj) {
140            return new Formatter(obj).toString();
141        }
142
143                /**
144                 * The formatted value cached after the first call to toString()
145                 */
146                private String formattedValue;
147
148                /**
149                 * The object to format.
150                 */
151                private final Object item;
152
153                /**
154                 * Creates a new formatter.
155                 *
156                 * @param Item The item to format.
157                 */
158                public Formatter(Object Item) {
159                        this.item = Item;
160                        this.formattedValue = null;
161                }
162
163                @SuppressWarnings("unchecked")
164                private Map<String, Object> createLogMap(SailPointObject value) {
165                        Map<String, Object> map = new ListOrderedMap();
166                        map.put("class", value.getClass().getSimpleName());
167                        map.put("id", value.getId());
168            if (!(value instanceof Link)) {
169                map.put("name", value.getName());
170            }
171                        return map;
172                }
173
174                /**
175                 * Formats an object. Equivalent to format(valueToFormat, false).
176                 *
177                 * @param valueToFormat The object to format.
178                 * @return The formatted version of that object.
179                 */
180                private String formatInternal(Object valueToFormat) {
181                        return format(valueToFormat, false);
182                }
183
184                /**
185                 * Formats an object according to multiple type-specific format rules.
186                 *
187                 * @param valueToFormat The object to format.
188                 * @return The formatted version of that object.
189                 */
190                private String format(Object valueToFormat, boolean indent) {
191                        StringBuilder value = new StringBuilder();
192
193                        if (valueToFormat == null) {
194                                value.append("(null)");
195                        } else if (valueToFormat instanceof Supplier) {
196                // Note that the Supplier's code is only invoked at this point
197                value.append(format(((Supplier<?>) valueToFormat).get(), indent));
198                        } else if (valueToFormat instanceof String) {
199                                value.append(valueToFormat);
200                        } else if (valueToFormat instanceof bsh.This) {
201                                String namespaceName = "???";
202                                if (((This) valueToFormat).getNameSpace() != null) {
203                                        namespaceName = ((This) valueToFormat).getNameSpace().getName();
204                                }
205                                value.append("bsh.This[namespace=").append(namespaceName).append("]");
206                        } else if (valueToFormat instanceof StackTraceElement[]) {
207                                for (StackTraceElement element : (StackTraceElement[]) valueToFormat) {
208                                        value.append("\n  at ");
209                                        value.append(element);
210                                }
211                        } else if (valueToFormat instanceof Throwable) {
212                                Throwable t = (Throwable)valueToFormat;
213                                try (StringWriter target = new StringWriter()) {
214                                        try (PrintWriter printWriter = new PrintWriter(target)) {
215                                                t.printStackTrace(printWriter);
216                                        }
217                                        value.append(target);
218                                } catch(IOException e) {
219                                        return "Exception printing object of type Throwable: " + e;
220                                }
221                        } else if (valueToFormat.getClass().isArray()) {
222                                value.append("[\n");
223                                boolean first = true;
224                                int length = Array.getLength(valueToFormat);
225                                for (int i = 0; i < length; i++) {
226                                        if (!first) {
227                                                value.append(",\n");
228                                        }
229                                        value.append("  ");
230                                        value.append(format(Array.get(valueToFormat, i), true));
231                                        first = false;
232                                }
233                                value.append("\n]");
234                        } else if (valueToFormat instanceof Collection) {
235                                value.append("[\n");
236                                boolean first = true;
237                                for (Object arg : (Collection<?>) valueToFormat) {
238                                        if (!first) {
239                                                value.append(",\n");
240                                        }
241                                        value.append("  ").append(format(arg, true));
242                                        first = false;
243                                }
244                                value.append("\n]");
245                        } else if (valueToFormat instanceof Map) {
246                                value.append("{\n");
247                                boolean first = true;
248                                for (Map.Entry<?, ?> entry : new TreeMap<Object, Object>((Map<?, ?>) valueToFormat).entrySet()) {
249                                        if (!first) {
250                                                value.append(",\n");
251                                        }
252                                        value.append("  ");
253                                        value.append(formatInternal(entry.getKey()));
254                                        value.append("=");
255                                        value.append(format(entry.getValue(), true));
256                                        first = false;
257                                }
258                                value.append("\n}");
259                        } else if (valueToFormat instanceof Date) {
260                                DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
261                                value.append(format.format((Date) valueToFormat));
262                        } else if (valueToFormat instanceof Calendar) {
263                                value.append(formatInternal(((Calendar) valueToFormat).getTime()));
264                        } else if (valueToFormat instanceof Document) {
265                                try {
266                                        Transformer transformer = TransformerFactory.newInstance().newTransformer();
267                                        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
268                                        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
269                                        try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
270                                                transformer.transform(new DOMSource((Document) valueToFormat), new StreamResult(output));
271                                                value.append(output);
272                                        }
273                                } catch (Exception e) {
274                                        return "Exception transforming object of type Document " + e;
275                                }
276                        } else if (valueToFormat instanceof Identity) {
277                                value.append("Identity").append(formatInternal(toLogMap((Identity) valueToFormat)));
278                        } else if (valueToFormat instanceof Bundle) {
279                                value.append("Bundle").append(formatInternal(toLogMap((Bundle)valueToFormat)));
280                        } else if (valueToFormat instanceof ManagedAttribute) {
281                                value.append("ManagedAttribute").append(formatInternal(toLogMap((ManagedAttribute) valueToFormat)));
282                        } else if (valueToFormat instanceof Link) {
283                                value.append("Link").append(formatInternal(toLogMap((Link) valueToFormat)));
284                        } else if (valueToFormat instanceof Application) {
285                                value.append("Application").append(formatInternal(toLogMap((Application) valueToFormat)));
286                        } else if (valueToFormat instanceof SailPointContext) {
287                                value.append("SailPointContext[").append(valueToFormat.hashCode()).append(", username = ").append(((SailPointContext) valueToFormat).getUserName()).append("]");
288                        } else if (valueToFormat instanceof ProvisioningPlan) {
289                                try {
290                                        value.append(ProvisioningPlan.getLoggingPlan((ProvisioningPlan) valueToFormat).toXml());
291                                } catch (GeneralException e) {
292                                        return "Exception transforming object of type " + valueToFormat.getClass().getName() + " to XML: " + e;
293                                }
294                        } else if (valueToFormat instanceof Filter) {
295                                value.append("Filter[").append(((Filter) valueToFormat).getExpression(true)).append("]");
296                        } else if (valueToFormat instanceof AbstractXmlObject) {
297                                try {
298                                        value.append(((AbstractXmlObject)valueToFormat).toXml());
299                                } catch (GeneralException e) {
300                                        return "Exception transforming object of type " + valueToFormat.getClass().getName() + " to XML: " + e;
301                                }
302                        } else {
303                                value.append(valueToFormat);
304                        }
305
306                        String result = value.toString();
307                        if (indent) {
308                                result = result.replace("\n", "\n" + TAB_SPACES).trim();
309                        }
310
311                        return result;
312                }
313
314                /**
315                 * Returns the formatted string of the item when invoked via the {@link Supplier} interface.
316         * This output will NOT be cached, unlike a call to toString().
317         *
318                 * @return The formatted string, freshly calculated
319                 * @see Supplier#get()
320                 */
321                @Override
322                public String get() {
323                        return formatInternal(this.item);
324                }
325
326                /**
327                 * Converts the Identity to a Map for logging purposes
328                 * @param value The Identity convert
329                 * @return A Map containing some basic identity details
330                 */
331                private Map<String, Object> toLogMap(Identity value) {
332                        Map<String, Object> map = createLogMap(value);
333                        map.put("type", value.getType());
334                        map.put("displayName", value.getDisplayName());
335                        map.put("disabled", value.isDisabled() || value.isInactive());
336                        map.put("attributes", formatInternal(value.getAttributes()));
337                        return map;
338                }
339
340                /**
341                 * Converts the Link to a Map for logging purposes
342                 * @param value The Link to convert
343                 * @return A Map containing some basic Link details
344                 */
345                private Map<String, Object> toLogMap(Link value) {
346                        Map<String, Object> map = createLogMap(value);
347                        map.put("application", value.getApplicationName());
348                        map.put("nativeIdentity", value.getNativeIdentity());
349                        map.put("displayName", value.getDisplayName());
350                        map.put("disabled", value.isDisabled());
351                        return map;
352                }
353
354                /**
355                 * Converts the Application to a Map for logging purposes
356                 * @param value The Application to convert
357                 * @return A Map containing some basic Application details
358                 */
359                private Map<String, Object> toLogMap(Application value) {
360                        Map<String, Object> map = createLogMap(value);
361                        map.put("authoritative", value.isAuthoritative());
362                        map.put("connector", value.getConnector());
363                        if (value.isInMaintenance()) {
364                                map.put("maintenance", true);
365                        }
366                        return map;
367                }
368
369                /**
370                 * Converts the Bundle / Role to a Map for logging purposes
371                 * @param value The Bundle to convert
372                 * @return A Map containing some basic Bundle details
373                 */
374                private Map<String, Object> toLogMap(Bundle value) {
375                        Map<String, Object> map = createLogMap(value);
376                        map.put("type", value.getType());
377                        map.put("displayName", value.getDisplayName());
378                        return map;
379                }
380
381                /**
382                 * Converts the ManagedAttribute / Entitlement object to a Map for logging purposes
383                 * @param value The MA to convert
384                 * @return A Map containing some basic MA details
385                 */
386                private Map<String, Object> toLogMap(ManagedAttribute value) {
387                        Map<String, Object> map = createLogMap(value);
388                        map.put("application", value.getApplication().getName());
389                        map.put("attribute", value.getAttribute());
390                        map.put("value", value.getValue());
391                        map.put("displayName", value.getDisplayName());
392                        return map;
393                }
394
395                /**
396                 * If the formatted value exists, the cached version will be returned.
397                 * Otherwise, the format string will be calculated at this time, cached,
398                 * and then returned.
399                 *
400                 * @see java.lang.Object#toString()
401                 */
402                @Override
403                public String toString() {
404                        if (this.formattedValue == null) {
405                                this.formattedValue = formatInternal(item);
406                        }
407                        return this.formattedValue;
408                }
409        }
410
411    public static final String CUSTOM_GLOBAL_CAPTURED_LOGS_TOKEN = "IIQCommon.SLogger.CapturedLogs";
412
413    /**
414     * Spaces to use for tabs in stack traces
415     */
416    private static final String TAB_SPACES = "    ";
417    /**
418     * The context name, typically the class name
419     */
420    private String contextName;
421    /**
422     * The context stack, which can be used to track nested contexts,
423     * such as method calls. The context stack will be prepended to
424     * all log messages if it is not empty.
425     */
426        protected final Stack<String> contextStack;
427        /**
428         * The underlying logger to use.
429         */
430        protected final Log logger;
431        /**
432         * The underlying output stream to use.
433         */
434        protected final PrintStream out;
435
436    /**
437     * Creates a new logger with the given logger and print stream.
438     * @param logger the underlying Commons Logging logger to use
439     * @param out the underlying PrintStream to use
440     */
441        protected SLogger(Log logger, PrintStream out) {
442                contextStack = new Stack<>();
443                this.logger = logger;
444                this.out = out;
445        }
446    /**
447     * Creates a new logger.
448     * @param name The name to log messages for. Typically, this is a class name, but may be a rule library, etc
449     */
450    public SLogger(String name) {
451        this(LogFactory.getLog(name), null);
452
453        this.contextName = name;
454    }
455
456        /**
457         * Creates a new logger.
458         *
459         * @param Owner The class to log messages for.
460         */
461        public SLogger(Class<?> Owner) {
462                this(LogFactory.getLog(Owner), null);
463
464        this.contextName = Owner.getName();
465    }
466
467        /**
468         * Wraps the given log4j logger with this logger
469         *
470         * @param WrapLog The logger to wrap
471         */
472        public SLogger(Log WrapLog) {
473                this(WrapLog, null);
474        }
475
476        /**
477         * Creates a new logger.
478         *
479         * @param Out The output stream to
480         */
481        public SLogger(PrintStream Out) {
482                this(null, Out);
483        }
484
485        /**
486         * Wraps the arguments for future formatting. The format string is not resolved
487         * at this time, meaning that the toString() is lazily evaluated.
488         *
489         * @param args The arguments for any place-holders in the message template.
490         * @return The formatted arguments.
491         */
492        public static SLogger.Formatter[] format(Object[] args) {
493                if (args == null) {
494                        return null;
495                }
496                Formatter[] argsCopy = new Formatter[args.length];
497                for (int i = 0; i < args.length; i++) {
498                        argsCopy[i] = new Formatter(args[i]);
499                }
500                return argsCopy;
501        }
502
503    /**
504     * Gets the captured logs ref for the given thread from the CustomGlobal.
505     * If it does not exist, it will be created. Using CustomGlobal and only
506     * core JDK classes for this allows this trace to be shared across plugins
507     * that may contain an obfuscated instance of this class.
508     *
509     * If the ThreadLocal does not already exist, it will be created in a block
510     * synchronized on the CustomGlobal class itself, preventing double creation.
511     *
512     * The contents of the AtomicReference may be null if capture() has not been
513     * invoked yet, or if reset() has been invoked.
514     *
515     * @return The AtomicReference containing the StringBuilder for captured logs for this thread
516     */
517    @SuppressWarnings("unchecked")
518    protected static AtomicReference<StringBuilder> getCapturedLogsRef() {
519        ThreadLocal<AtomicReference<StringBuilder>> threadLocal = (ThreadLocal<AtomicReference<StringBuilder>>) CustomGlobal.get(CUSTOM_GLOBAL_CAPTURED_LOGS_TOKEN);
520        if (threadLocal == null) {
521            synchronized(CustomGlobal.class) {
522                threadLocal = (ThreadLocal<AtomicReference<StringBuilder>>) CustomGlobal.get(CUSTOM_GLOBAL_CAPTURED_LOGS_TOKEN);
523                if (threadLocal == null) {
524                    threadLocal = InheritableThreadLocal.withInitial(AtomicReference::new);
525                    CustomGlobal.put(CUSTOM_GLOBAL_CAPTURED_LOGS_TOKEN, threadLocal);
526                }
527            }
528        }
529        return threadLocal.get();
530    }
531
532        /**
533         * Renders the MessageTemplate using the given arguments
534         * @param messageTemplate The message template
535         * @param args The arguments
536         * @return The resolved message template
537         */
538        public static String renderMessage(String messageTemplate, Object[] args) {
539                if (args != null && args.length > 0) {
540                        MessageFormat template = new MessageFormat(messageTemplate);
541                        return template.format(args);
542                } else {
543                        return messageTemplate;
544                }
545        }
546
547    /**
548     * Appends the standard prefix to the captured logs, including timestamp and context stack
549     */
550    protected void appendStandardPrefix() {
551        String prefix = getTimestamp();
552        builder().append(prefix);
553        builder().append("[").append(contextName).append("] ");
554        if (!contextStack.isEmpty()) {
555            String ctx = "[" + contextStack.stream().reduce((a, b) -> a + " > " + b).orElse("") + "] ";
556            builder().append(ctx);
557        }
558    }
559
560    /**
561     * Returns the StringBuilder used to capture logs, initializing it if necessary
562     * @return The StringBuilder for captured logs
563     */
564    protected StringBuilder builder() {
565        AtomicReference<StringBuilder> ref = getCapturedLogsRef();
566        if (ref.get() == null) {
567            ref.set(new StringBuilder());
568        }
569        return ref.get();
570    }
571
572    /**
573     * Begins capturing logs. This will clear any previously captured logs by
574     * replacing the current StringBuilder with a new one.
575     */
576    public void capture() {
577        builder().append("Starting capture at ").append(getTimestamp()).append("\n\n");
578    }
579
580    /**
581     * @see Log#debug(Object)
582     * @param arg0 The message to log
583     */
584        @Override
585        public void debug(Object arg0) {
586                debug("{0}", arg0);
587        }
588
589    /**
590     * @see Log#debug(Object, Throwable)
591     * @param arg0 The message to log
592     * @param arg1 The exception to log
593     */
594        @Override
595        public void debug(Object arg0, Throwable arg1) {
596                debug("{0} {1}", arg0, arg1);
597        }
598
599        /**
600         * Logs an debugging message.
601         *
602         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
603         * @param Args The arguments for any place-holders in the message template.
604         */
605        public void debug(String MessageTemplate, Object... Args) {
606                log(Level.DEBUG, MessageTemplate, (Object[]) format(Args));
607        }
608
609        /**
610         * Logs an entry message for a segment of code. This will log at DEBUG level if
611         * the system configuration property IIQCommon.SLogger.EnterExitEnabled is set to true.
612         *
613         * @param value The value to log as the 'location', e.g., a method name, a chunk of code, etc
614         */
615        public void enter(String value) {
616                if (isDebugEnabled()) {
617                        log(Level.ENTER, "Entering: {0}", value);
618                }
619        }
620
621        /**
622         * Logs an entry message for a Beanshell function. This will log at DEBUG level if
623         *       * the system configuration property IIQCommon.SLogger.EnterExitEnabled is set to true.
624         *
625         * @param bshThis The Beanshell 'this' object, which contains the namespace name.
626         */
627        public void enter(bsh.This bshThis) {
628                if (bshThis != null && bshThis.getNameSpace() != null) {
629                        enter(bshThis.getNameSpace().getName());
630                } else {
631                        enter("(unknown Beanshell function)");
632                }
633        }
634
635        /**
636         * @see Log#error(Object)
637         */
638        @Override
639        public void error(Object arg0) {
640                error("{0}", arg0);
641        }
642
643        /**
644         * @see Log#error(Object, Throwable)
645         */
646        @Override
647        public void error(Object arg0, Throwable arg1) {
648                error("{0}", arg0);
649                handleException(arg1);
650        }
651
652        /**
653         * Logs an error message.
654         *
655         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
656         * @param Args The arguments for any place-holders in the message template.
657         */
658        public void error(String MessageTemplate, Object... Args) {
659                log(Level.ERROR, MessageTemplate, (Object[]) format(Args));
660        }
661
662        /**
663         * Logs an exit message for a segment of code. This will log at DEBUG level if
664         * the system configuration property IIQCommon.SLogger.EnterExitEnabled is set to true.
665         *
666         * @param value The value to log as the 'location', e.g., a method name, a chunk of code, etc
667         */
668        public void exit(String value) {
669                if (isDebugEnabled()) {
670                        log(Level.EXIT, "Exiting: {0}", value);
671                }
672        }
673
674        /**
675         * Logs an exit message for a Beanshell function. This will log at DEBUG level if
676         * the system configuration property IIQCommon.SLogger.EnterExitEnabled is set to true.
677         *
678         * @param bshThis The Beanshell 'this' object, which contains the namespace name.
679         */
680        public void exit(bsh.This bshThis) {
681                if (bshThis != null && bshThis.getNameSpace() != null) {
682                        exit(bshThis.getNameSpace().getName());
683                } else {
684                        exit("(unknown Beanshell function)");
685                }
686        }
687
688        @Override
689        public void fatal(Object arg0) {
690                fatal("{0}", arg0);
691        }
692
693        @Override
694        public void fatal(Object arg0, Throwable arg1) {
695                fatal("{0}", arg0);
696                handleException(arg1);
697        }
698
699        /**
700         * Logs a fatal error message.
701         *
702         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
703         * @param Args The arguments for any place-holders in the message template.
704         */
705        public void fatal(String MessageTemplate, Object... Args) {
706                log(Level.FATAL, MessageTemplate, format(Args));
707        }
708        
709    /**
710     * Returns the captured logs as a String
711     * @return The captured logs
712     */
713    public String getCapturedLogs() {
714        return builder().toString();
715    }
716
717        /**
718         * Gets the internal Log object wrapped by this class
719         * @return The internal log object
720         */
721        /*package*/ Log getLogger() {
722                return logger;
723        }
724
725    /**
726     * Generates a timestamp string for captured log entries
727     * @return The timestamp string
728     */
729    protected String getTimestamp() {
730        ZonedDateTime now = ZonedDateTime.now();
731        String timestamp = now.toString();
732        return "[" + timestamp + "] ";
733    }
734
735        /**
736         * Handles an exception.
737         *
738         * @param Error The exception to handle.
739         */
740        public synchronized void handleException(Throwable Error) {
741                save(Error);
742                if (logger != null) {
743                        logger.error(Error.toString(), Error);
744                } else if (out != null) {
745                        Error.printStackTrace(out);
746                }
747        }
748
749        /**
750         * Logs a message "Here", along with a custom suffix, indicating that the code
751         * has reached a certain point. This will only be logged (at INFO level) if
752         * the system configuration property IIQCommon.SLogger.HereEnabled is set to true.
753         *
754         * @param value The value to log as the 'location', e.g., a method name, a chunk of code, etc.
755         */
756        public void here(String value) {
757                log(Level.HERE, "Here: {0}", value);
758        }
759
760        @Override
761        public void info(Object arg0) {
762                info("{0}", arg0);
763        }
764
765        @Override
766        public void info(Object arg0, Throwable arg1) {
767                info("{0} {1}", arg0, arg1);
768        }
769
770        /**
771         * Logs an informational message.
772         *
773         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
774         * @param Args The arguments for any place-holders in the message template.
775         */
776        public void info(String MessageTemplate, Object... Args) {
777                log(Level.INFO, MessageTemplate, format(Args));
778        }
779
780    /**
781     * Returns true if log capturing is currently active
782     * @return true if capturing is active
783     */
784    public boolean isCapturing() {
785        AtomicReference<StringBuilder> ref = getCapturedLogsRef();
786        return ref.get() != null;
787    }
788
789        /**
790         * @see Log#isDebugEnabled()
791         */
792        @Override
793        public boolean isDebugEnabled() {
794                if (logger != null) {
795                        return logger.isDebugEnabled();
796                } else {
797                        return true;
798                }
799        }
800
801        /**
802         * Returns true if the logger is enabled for the given level. Unfortunately, Commons Logging doesn't have a friendly isEnabledFor(Level) type API, since some of its downstream loggers may not either.
803         *
804         * @param log The logger to check
805         * @param logLevel The level to check
806         * @return true if the logger is enabled
807         */
808        private boolean isEnabledFor(Log log, Level logLevel) {
809                switch(logLevel) {
810                case TRACE:
811                        return log.isTraceEnabled();
812                case DEBUG:
813                        return log.isDebugEnabled();
814                case ENTER:
815                case EXIT:
816                        Configuration sc1 = Configuration.getSystemConfig();
817                        boolean enterExitEnabled = sc1 != null && sc1.getBoolean("IIQCommon.SLogger.EnterExitEnabled", false);
818                        return enterExitEnabled && log.isDebugEnabled();
819                case INFO:
820                        return log.isInfoEnabled();
821                case WARN:
822                        return log.isWarnEnabled();
823                case ERROR:
824                        return log.isErrorEnabled();
825                case FATAL:
826                        return log.isFatalEnabled();
827                case HERE:
828                        Configuration sc2 = Configuration.getSystemConfig();
829                        boolean hereEnabled = sc2 != null && sc2.getBoolean("IIQCommon.SLogger.HereEnabled", false);
830                        return hereEnabled && log.isInfoEnabled();
831                }
832                return false;
833        }
834
835        /**
836         * @see Log#isErrorEnabled()
837         */
838        @Override
839        public boolean isErrorEnabled() {
840                if (logger != null) {
841                        return logger.isErrorEnabled();
842                } else {
843                        return true;
844                }
845        }
846
847        /**
848         * @see Log#isFatalEnabled()
849         */
850        @Override
851        public boolean isFatalEnabled() {
852                if (logger != null) {
853                        return logger.isFatalEnabled();
854                } else {
855                        return true;
856                }
857        }
858
859        /**
860         * @see Log#isInfoEnabled()
861         */
862        @Override
863        public boolean isInfoEnabled() {
864                if (logger != null) {
865                        return logger.isInfoEnabled();
866                } else {
867                        return true;
868                }
869        }
870
871        /**
872         * @see Log#isTraceEnabled()
873         */
874        @Override
875        public boolean isTraceEnabled() {
876                if (logger != null) {
877                        return logger.isTraceEnabled();
878                } else {
879                        return true;
880                }
881        }
882
883        /**
884         * @see Log#isWarnEnabled()
885         */
886        @Override
887        public boolean isWarnEnabled() {
888                if (logger != null) {
889                        return logger.isWarnEnabled();
890                } else {
891                        return true;
892                }
893        }
894
895        /**
896         * Logs the message at the appropriate level according to the Commons Logging API
897         * @param logLevel The log level to log at
898         * @param message The message to log
899         */
900        public void log(Level logLevel, String message) {
901                switch(logLevel) {
902                case TRACE:
903                        logger.trace(message);
904                        break;
905                case DEBUG:
906                case ENTER:
907                case EXIT:
908                        logger.debug(message);
909                        break;
910                case INFO:
911                case HERE:
912                        logger.info(message);
913                        break;
914                case WARN:
915                        logger.warn(message);
916                        break;
917                case ERROR:
918                        logger.error(message);
919                        break;
920                case FATAL:
921                        logger.fatal(message);
922                        break;
923                }
924        }
925
926        /**
927         * Logs a message.
928         *
929         * @param logLevel The level to log the message at.
930         * @param messageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
931         * @param args The arguments for any place-holders in the message template.
932         */
933        protected void log(Level logLevel, String messageTemplate, Object... args) {
934                save(logLevel, messageTemplate, args);
935                if (logger != null) {
936                        if (isEnabledFor(logger, logLevel)) {
937                                String message = renderMessage(messageTemplate, args);
938                                if (!contextStack.isEmpty()) {
939                                        String ctx = "[" + contextStack.stream().reduce((a, b) -> a + " > " + b).orElse("") + "]";
940                                        message = ctx + " " + message;
941                                }
942                                log(logLevel, message);
943                        }
944                } else if (out != null) {
945                        String message = renderMessage(messageTemplate, args);
946                        if (!contextStack.isEmpty()) {
947                                String ctx = "[" + contextStack.stream().reduce((a, b) -> a + " > " + b).orElse("") + "]";
948                                message = ctx + " " + message;
949                        }
950                        out.println(message);
951                }
952        }
953
954        /**
955         * Pops the top value off the context stack
956         * @return The value popped off the stack, or null if the stack is empty.
957         */
958        public String pop() {
959                if (contextStack.isEmpty()) {
960                        return null;
961                }
962                return contextStack.pop();
963        }
964
965        /**
966         * Pushes a value onto the context stack. This will be logged with the message
967         * @param value The context value to add
968         */
969        public void push(String value) {
970                if (value != null) {
971                        contextStack.push(value);
972                }
973        }
974
975    /**
976     * Resets/clears the captured logs
977     */
978    public void reset() {
979        AtomicReference<StringBuilder> ref = getCapturedLogsRef();
980        ref.set(new StringBuilder());
981    }
982
983    /**
984     * Saves an error message and stack trace to the captured logs. If the
985     * error cannot be rendered for whatever reason, a message will be printed
986     * to standard error as a last resort.
987     *
988     * @param error The exception to handle.
989     */
990    @SuppressWarnings("UseOfSystemOutOrSystemErr")
991    protected void save(Throwable error) {
992        if (!isCapturing()) {
993            return;
994        }
995
996        appendStandardPrefix();
997
998        builder().append("[THROWABLE] ");
999
1000        try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
1001            error.printStackTrace(pw);
1002            pw.flush();
1003            String stackTrace = sw.toString().replace("\t", TAB_SPACES);
1004            builder().append(stackTrace).append("\n");
1005        } catch (Exception e) {
1006            System.err.println("LAST RESORT: Error logging stack trace: " + e.getMessage());
1007            error.printStackTrace();
1008        }
1009    }
1010
1011    /**
1012     * Saves a log message to the captured logs
1013     *
1014     * @param LogLevel The level to log the message at.
1015     * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
1016     * @param Args The arguments for any place-holders in the message template.
1017     */
1018    protected void save(Level LogLevel, String MessageTemplate, Object[] Args) {
1019        if (!isCapturing()) {
1020            return;
1021        }
1022
1023        appendStandardPrefix();
1024
1025        String formattedMessage = String.format(MessageTemplate, Args);
1026        builder().append("[").append(LogLevel.name()).append("] ").append(formattedMessage).append("\n");
1027    }
1028
1029        @Override
1030        public void trace(Object arg0) {
1031                trace("{0}", arg0);
1032        }
1033
1034        @Override
1035        public void trace(Object arg0, Throwable arg1) {
1036                trace("{0} {1}", arg0, arg1);
1037        }
1038
1039        /**
1040         * Logs a trace message.
1041         *
1042         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
1043         * @param Args The arguments for any place-holders in the message template.
1044         */
1045        public void trace(String MessageTemplate, Object... Args) {
1046                log(Level.TRACE, MessageTemplate, format(Args));
1047        }
1048
1049        /**
1050         * @see Log#warn(Object)
1051         */
1052        @Override
1053        public void warn(Object arg0) {
1054                warn("{0}", arg0);
1055        }
1056
1057        /**
1058         * @see Log#warn(Object, Throwable)
1059         */
1060        @Override
1061        public void warn(Object arg0, Throwable arg1) {
1062                warn("{0} {1}", arg0, arg1);
1063        }
1064
1065        /**
1066         * Logs a warning message.
1067         *
1068         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
1069         * @param Args The arguments for any place-holders in the message template.
1070         */
1071        public void warn(String MessageTemplate, Object... Args) {
1072                log(Level.WARN, MessageTemplate, format(Args));
1073        }
1074
1075}