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.Application;
010import sailpoint.object.Bundle;
011import sailpoint.object.Filter;
012import sailpoint.object.Identity;
013import sailpoint.object.Link;
014import sailpoint.object.ManagedAttribute;
015import sailpoint.object.ProvisioningPlan;
016import sailpoint.object.SailPointObject;
017import sailpoint.tools.GeneralException;
018import sailpoint.tools.xml.AbstractXmlObject;
019
020import javax.xml.transform.OutputKeys;
021import javax.xml.transform.Transformer;
022import javax.xml.transform.TransformerFactory;
023import javax.xml.transform.dom.DOMSource;
024import javax.xml.transform.stream.StreamResult;
025import java.io.ByteArrayOutputStream;
026import java.io.IOException;
027import java.io.PrintStream;
028import java.io.PrintWriter;
029import java.io.StringWriter;
030import java.lang.reflect.Array;
031import java.text.DateFormat;
032import java.text.MessageFormat;
033import java.text.SimpleDateFormat;
034import java.util.Calendar;
035import java.util.Collection;
036import java.util.Date;
037import java.util.Map;
038import java.util.TreeMap;
039import java.util.function.Supplier;
040
041/**
042 * A wrapper around the Commons Logging {@link Log} class that supplements its
043 * features with some extras available in other logging libraries. Since this
044 * class itself implements {@link Log}, Beanshell should not care if you simply
045 * overwrite the 'log' variable in your code.
046 *
047 * Log strings are interpreted as Java {@link MessageFormat} objects and have
048 * the various features of that class in your JDK version.
049 *
050 * Values passed as arguments are only evaluated when the appropriate log
051 * level is active. If the log level is not active, the operation becomes a
052 * quick no-op. This prevents a lot of isDebugEnabled() type checks.
053 *
054 * The 'S' stands for Super. Super Logger.
055 */
056public class SLogger implements org.apache.commons.logging.Log {
057        
058        /**
059         * Helper class to format an object for logging. The format is only derived
060         * when the {@link #toString()} is called, meaning that if you log one of these
061         * and the log level is not enabled, a slow string conversion will never occur.
062         *
063         * Null values are transformed into the special string '(null)'.
064         *
065         * Formatted values are cached after the first format operation, even if the
066         * underlying object is modified.
067         *
068         * The following types are handled by the Formatter:
069         *
070         * - null
071         * - Strings
072         * - Arrays of Objects
073         * - Arrays of StackTraceElements
074         * - Collections of Objects
075         * - Maps
076         * - Dates and Calendars
077         * - XML {@link Document}s
078         * - Various SailPointObjects
079         *
080         * Nested objects are also passed through a Formatter.
081         */
082        public static class Formatter implements Supplier<String> {
083
084                /**
085                 * The cached formatted value
086                 */
087                private String formattedValue;
088                /**
089                 * The object to format.
090                 */
091                private final Object item;
092
093                /**
094                 * Creates a new formatter.
095                 *
096                 * @param Item The item to format.
097                 */
098                public Formatter(Object Item) {
099                        this.item = Item;
100                        this.formattedValue = null;
101                }
102
103                @SuppressWarnings("unchecked")
104                private Map<String, Object> createLogMap(SailPointObject value) {
105                        Map<String, Object> map = new ListOrderedMap();
106                        map.put("class", value.getClass().getName());
107                        map.put("id", value.getId());
108                        map.put("name", value.getName());
109                        return map;
110                }
111
112                /**
113                 * Formats an object.
114                 *
115                 * @param valueToFormat The object to format.
116                 * @return The formatted version of that object.
117                 */
118                private String format(Object valueToFormat) {
119                        return format(valueToFormat, false);
120                }
121
122                /**
123                 * Formats an object according to multiple type-specific format rules.
124                 *
125                 * @param valueToFormat The object to format.
126                 * @return The formatted version of that object.
127                 */
128                private String format(Object valueToFormat, boolean indent) {
129                        StringBuilder value = new StringBuilder();
130
131                        if (valueToFormat == null) {
132                                value.append("(null)");
133                        } else if (valueToFormat instanceof String) {
134                                value.append(valueToFormat);
135                        } else if (valueToFormat instanceof bsh.This) {
136                                String namespaceName = "???";
137                                if (((This) valueToFormat).getNameSpace() != null) {
138                                        namespaceName = ((This) valueToFormat).getNameSpace().getName();
139                                }
140                                value.append("bsh.This[namespace=").append(namespaceName).append("]");
141                        } else if (valueToFormat instanceof StackTraceElement[]) {
142                                for (StackTraceElement element : (StackTraceElement[]) valueToFormat) {
143                                        value.append("\n  at ");
144                                        value.append(element);
145                                }
146                        } else if (valueToFormat instanceof Throwable) {
147                                Throwable t = (Throwable)valueToFormat;
148                                try (StringWriter target = new StringWriter()) {
149                                        try (PrintWriter printWriter = new PrintWriter(target)) {
150                                                t.printStackTrace(printWriter);
151                                        }
152                                        value.append(target);
153                                } catch(IOException e) {
154                                        return "Exception printing object of type Throwable: " + e;
155                                }
156                        } else if (valueToFormat.getClass().isArray()) {
157                                value.append("[\n");
158                                boolean first = true;
159                                int length = Array.getLength(valueToFormat);
160                                for (int i = 0; i < length; i++) {
161                                        if (!first) {
162                                                value.append(",\n");
163                                        }
164                                        value.append("  ");
165                                        value.append(format(Array.get(valueToFormat, i), true));
166                                        first = false;
167                                }
168                                value.append("\n]");
169                        } else if (valueToFormat instanceof Collection) {
170                                value.append("[\n");
171                                boolean first = true;
172                                for (Object arg : (Collection<?>) valueToFormat) {
173                                        if (!first) {
174                                                value.append(",\n");
175                                        }
176                                        value.append("  ").append(format(arg, true));
177                                        first = false;
178                                }
179                                value.append("\n]");
180                        } else if (valueToFormat instanceof Map) {
181                                value.append("{\n");
182                                boolean first = true;
183                                for (Map.Entry<?, ?> entry : new TreeMap<Object, Object>((Map<?, ?>) valueToFormat).entrySet()) {
184                                        if (!first) {
185                                                value.append(",\n");
186                                        }
187                                        value.append("  ");
188                                        value.append(format(entry.getKey()));
189                                        value.append("=");
190                                        value.append(format(entry.getValue(), true));
191                                        first = false;
192                                }
193                                value.append("\n}");
194                        } else if (valueToFormat instanceof Date) {
195                                DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
196                                value.append(format.format((Date) valueToFormat));
197                        } else if (valueToFormat instanceof Calendar) {
198                                value.append(format(((Calendar) valueToFormat).getTime()));
199                        } else if (valueToFormat instanceof Document) {
200                                try {
201                                        Transformer transformer = TransformerFactory.newInstance().newTransformer();
202                                        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
203                                        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
204                                        try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
205                                                transformer.transform(new DOMSource((Document) valueToFormat), new StreamResult(output));
206                                                value.append(output);
207                                        }
208                                } catch (Exception e) {
209                                        return "Exception transforming object of type Document " + e;
210                                }
211                        } else if (valueToFormat instanceof Identity) {
212                                value.append("Identity").append(format(toLogMap((Identity) valueToFormat)));
213                        } else if (valueToFormat instanceof Bundle) {
214                                value.append("Bundle").append(format(toLogMap((Bundle)valueToFormat)));
215                        } else if (valueToFormat instanceof ManagedAttribute) {
216                                value.append("ManagedAttribute").append(format(toLogMap((ManagedAttribute) valueToFormat)));
217                        } else if (valueToFormat instanceof Link) {
218                                value.append("Link").append(format(toLogMap((Link) valueToFormat)));
219                        } else if (valueToFormat instanceof Application) {
220                                value.append("Application").append(format(toLogMap((Application) valueToFormat)));
221                        } else if (valueToFormat instanceof SailPointContext) {
222                                value.append("SailPointContext[").append(valueToFormat.hashCode()).append(", username = ").append(((SailPointContext) valueToFormat).getUserName()).append("]");
223                        } else if (valueToFormat instanceof ProvisioningPlan) {
224                                try {
225                                        value.append(ProvisioningPlan.getLoggingPlan((ProvisioningPlan) valueToFormat).toXml());
226                                } catch (GeneralException e) {
227                                        return "Exception transforming object of type " + valueToFormat.getClass().getName() + " to XML: " + e;
228                                }
229                        } else if (valueToFormat instanceof Filter) {
230                                value.append("Filter[").append(((Filter) valueToFormat).getExpression(true)).append("]");
231                        } else if (valueToFormat instanceof AbstractXmlObject) {
232                                try {
233                                        value.append(((AbstractXmlObject)valueToFormat).toXml());
234                                } catch (GeneralException e) {
235                                        return "Exception transforming object of type " + valueToFormat.getClass().getName() + " to XML: " + e;
236                                }
237                        } else {
238                                value.append(valueToFormat);
239                        }
240
241                        String result = value.toString();
242                        if (indent) {
243                                result = result.replace("\n", "\n  ").trim();
244                        }
245
246                        return result;
247                }
248
249                /**
250                 * Returns the formatted string of the item when invoked via the {@link Supplier} interface
251                 * @return The formatted string
252                 * @see Supplier#get()
253                 */
254                @Override
255                public String get() {
256                        return toString();
257                }
258
259                /**
260                 * Converts the Identity to a Map for logging purposes
261                 * @param value The Identity convert
262                 * @return A Map containing some basic identity details
263                 */
264                private Map<String, Object> toLogMap(Identity value) {
265                        Map<String, Object> map = createLogMap(value);
266                        map.put("type", value.getType());
267                        map.put("displayName", value.getDisplayName());
268                        map.put("disabled", value.isDisabled() || value.isInactive());
269                        map.put("attributes", format(value.getAttributes()));
270                        return map;
271                }
272
273                /**
274                 * Converts the Link to a Map for logging purposes
275                 * @param value The Link to convert
276                 * @return A Map containing some basic Link details
277                 */
278                private Map<String, Object> toLogMap(Link value) {
279                        Map<String, Object> map = createLogMap(value);
280                        map.put("application", value.getApplicationName());
281                        map.put("nativeIdentity", value.getNativeIdentity());
282                        map.put("displayName", value.getDisplayName());
283                        map.put("disabled", value.isDisabled());
284                        return map;
285                }
286
287                /**
288                 * Converts the Application to a Map for logging purposes
289                 * @param value The Application to convert
290                 * @return A Map containing some basic Application details
291                 */
292                private Map<String, Object> toLogMap(Application value) {
293                        Map<String, Object> map = createLogMap(value);
294                        map.put("authoritative", value.isAuthoritative());
295                        map.put("connector", value.getConnector());
296                        if (value.isInMaintenance()) {
297                                map.put("maintenance", true);
298                        }
299                        return map;
300                }
301
302                /**
303                 * Converts the Bundle / Role to a Map for logging purposes
304                 * @param value The Bundle to convert
305                 * @return A Map containing some basic Bundle details
306                 */
307                private Map<String, Object> toLogMap(Bundle value) {
308                        Map<String, Object> map = createLogMap(value);
309                        map.put("type", value.getType());
310                        map.put("displayName", value.getDisplayName());
311                        return map;
312                }
313
314                /**
315                 * Converts the ManagedAttribute / Entitlement object to a Map for logging purposes
316                 * @param value The MA to convert
317                 * @return A Map containing some basic MA details
318                 */
319                private Map<String, Object> toLogMap(ManagedAttribute value) {
320                        Map<String, Object> map = createLogMap(value);
321                        map.put("application", value.getApplication().getName());
322                        map.put("attribute", value.getAttribute());
323                        map.put("value", value.getValue());
324                        map.put("displayName", value.getDisplayName());
325                        return map;
326                }
327
328                /**
329                 * If the formatted value exists, the cached version will be returned.
330                 * Otherwise, the format string will be calculated at this time, cached,
331                 * and then returned.
332                 *
333                 * @see java.lang.Object#toString()
334                 */
335                @Override
336                public String toString() {
337                        if (this.formattedValue == null) {
338                                this.formattedValue = format(item);
339                        }
340                        return this.formattedValue;
341                }
342        }
343
344        /**
345         * An enumeration of log levels to replace the one in log4j
346         */
347        public enum Level {
348                TRACE,
349                DEBUG,
350                INFO,
351                WARN,
352                ERROR,
353                FATAL
354        }
355
356        /**
357         * Wraps the arguments for future formatting. The format string is not resolved
358         * at this time.
359         *
360         * NOTE: In newer versions of logging APIs, this would be accomplished
361         * by passing a {@link Supplier} to the API. However, in Commons Logging 1.x,
362         * this is not available. It is also not available in Beanshell, as it requires
363         * lambda syntax. If that ever becomes available, this class will become
364         * obsolete.
365         *
366         * @param args The arguments for any place-holders in the message template.
367         * @return The formatted arguments.
368         */
369        public static SLogger.Formatter[] format(Object[] args) {
370                if (args == null) {
371                        return null;
372                }
373                Formatter[] argsCopy = new Formatter[args.length];
374                for (int i = 0; i < args.length; i++) {
375                        argsCopy[i] = new Formatter(args[i]);
376                }
377                return argsCopy;
378        }
379
380        /**
381         * Renders the MessageTemplate using the given arguments
382         * @param messageTemplate The message template
383         * @param args The arguments
384         * @return The resolved message template
385         */
386        public static String renderMessage(String messageTemplate, Object[] args) {
387                if (args != null && args.length > 0) {
388                        MessageFormat template = new MessageFormat(messageTemplate);
389                        return template.format(args);
390                } else {
391                        return messageTemplate;
392                }
393        }
394
395        /**
396         * The underlying logger to use.
397         */
398        private final Log logger;
399        /**
400         * The underlying output stream to use.
401         */
402        private final PrintStream out;
403
404        /**
405         * Creates a new logger.
406         *
407         * @param Owner The class to log messages for.
408         */
409        public SLogger(Class<?> Owner) {
410                logger = LogFactory.getLog(Owner);
411                out = null;
412        }
413
414        /**
415         * Wraps the given log4j logger with this logger
416         *
417         * @param WrapLog The logger to wrap
418         */
419        public SLogger(Log WrapLog) {
420                logger = WrapLog;
421                out = null;
422        }
423
424        /**
425         * Creates a new logger.
426         *
427         * @param Out The output stream to
428         */
429        public SLogger(PrintStream Out) {
430                logger = null;
431                out = Out;
432        }
433        
434        @Override
435        public void debug(Object arg0) {
436                debug("{0}", arg0);
437        }
438        
439        @Override
440        public void debug(Object arg0, Throwable arg1) {
441                debug("{0} {1}", arg0, arg1);
442        }
443
444        /**
445         * Logs an debugging message.
446         *
447         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
448         * @param Args The arguments for any place-holders in the message template.
449         */
450        public void debug(String MessageTemplate, Object... Args) {
451                log(Level.DEBUG, MessageTemplate, format(Args));
452        }
453
454        /**
455         * @see Log#error(Object)
456         */
457        @Override
458        public void error(Object arg0) {
459                error("{0}", arg0);
460        }
461
462        /**
463         * @see Log#error(Object, Throwable)
464         */
465        @Override
466        public void error(Object arg0, Throwable arg1) {
467                error("{0}", arg0);
468                handleException(arg1);
469        }
470
471        /**
472         * Logs an error message.
473         *
474         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
475         * @param Args The arguments for any place-holders in the message template.
476         */
477        public void error(String MessageTemplate, Object... Args) {
478                log(Level.ERROR, MessageTemplate, format(Args));
479        }
480
481        @Override
482        public void fatal(Object arg0) {
483                fatal("{0}", arg0);
484        }
485
486        @Override
487        public void fatal(Object arg0, Throwable arg1) {
488                fatal("{0}", arg0);
489                handleException(arg1);
490        }
491
492        /**
493         * Logs a fatal error message.
494         *
495         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
496         * @param Args The arguments for any place-holders in the message template.
497         */
498        public void fatal(String MessageTemplate, Object... Args) {
499                log(Level.FATAL, MessageTemplate, format(Args));
500        }
501
502        /**
503         * Gets the internal Log object wrapped by this class
504         * @return The internal log object
505         */
506        /*package*/ Log getLogger() {
507                return logger;
508        }
509
510        /**
511         * Handles an exception.
512         *
513         * @param Error The exception to handle.
514         */
515        public synchronized void handleException(Throwable Error) {
516                save(Error);
517                if (logger != null) {
518                        logger.error(Error.toString(), Error);
519                } else if (out != null) {
520                        Error.printStackTrace(out);
521                }
522        }
523        
524        @Override
525        public void info(Object arg0) {
526                info("{0}", arg0);
527        }
528
529        @Override
530        public void info(Object arg0, Throwable arg1) {
531                info("{0} {1}", arg0, arg1);
532        }
533
534        /**
535         * Logs an informational message.
536         *
537         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
538         * @param Args The arguments for any place-holders in the message template.
539         */
540        public void info(String MessageTemplate, Object... Args) {
541                log(Level.INFO, MessageTemplate, format(Args));
542        }
543
544        /**
545         * @see Log#isDebugEnabled()
546         */
547        @Override
548        public boolean isDebugEnabled() {
549                if (logger != null) {
550                        return logger.isDebugEnabled();
551                } else {
552                        return true;
553                }
554        }
555
556        /**
557         * 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.
558         *
559         * @param log The logger to check
560         * @param logLevel The level to check
561         * @return true if the logger is enabled
562         */
563        private boolean isEnabledFor(Log log, Level logLevel) {
564                switch(logLevel) {
565                case TRACE:
566                        return log.isTraceEnabled();
567                case DEBUG:
568                        return log.isDebugEnabled();
569                case INFO:
570                        return log.isInfoEnabled();
571                case WARN:
572                        return log.isWarnEnabled();
573                case ERROR:
574                        return log.isErrorEnabled();
575                case FATAL:
576                        return log.isFatalEnabled();
577                }
578                return false;
579        }
580
581        /**
582         * @see Log#isErrorEnabled()
583         */
584        @Override
585        public boolean isErrorEnabled() {
586                if (logger != null) {
587                        return logger.isErrorEnabled();
588                } else {
589                        return true;
590                }
591        }
592
593        /**
594         * @see Log#isFatalEnabled()
595         */
596        @Override
597        public boolean isFatalEnabled() {
598                if (logger != null) {
599                        return logger.isFatalEnabled();
600                } else {
601                        return true;
602                }
603        }
604
605        /**
606         * @see Log#isInfoEnabled()
607         */
608        @Override
609        public boolean isInfoEnabled() {
610                if (logger != null) {
611                        return logger.isInfoEnabled();
612                } else {
613                        return true;
614                }
615        }
616
617        /**
618         * @see Log#isTraceEnabled()
619         */
620        @Override
621        public boolean isTraceEnabled() {
622                if (logger != null) {
623                        return logger.isTraceEnabled();
624                } else {
625                        return true;
626                }
627        }
628
629        /**
630         * @see Log#isWarnEnabled()
631         */
632        @Override
633        public boolean isWarnEnabled() {
634                if (logger != null) {
635                        return logger.isWarnEnabled();
636                } else {
637                        return true;
638                }
639        }
640
641        /**
642         * Logs the message at the appropriate level according to the Commons Logging API
643         * @param logLevel The log level to log at
644         * @param message The message to log
645         */
646        public void log(Level logLevel, String message) {
647                switch(logLevel) {
648                case TRACE:
649                        logger.trace(message);
650                        break;
651                case DEBUG:
652                        logger.debug(message);
653                        break;
654                case INFO:
655                        logger.info(message);
656                        break;
657                case WARN:
658                        logger.warn(message);
659                        break;
660                case ERROR:
661                        logger.error(message);
662                        break;
663                case FATAL:
664                        logger.fatal(message);
665                        break;
666                }
667        }
668
669        /**
670         * Logs a message.
671         *
672         * @param logLevel The level to log the message at.
673         * @param messageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
674         * @param args The arguments for any place-holders in the message template.
675         */
676        private void log(Level logLevel, String messageTemplate, Object... args) {
677                save(logLevel, messageTemplate, args);
678                if (logger != null) {
679                        if (isEnabledFor(logger, logLevel)) {
680                                String message = renderMessage(messageTemplate, args);
681                                log(logLevel, message);
682                        }
683                } else if (out != null) {
684                        String message = renderMessage(messageTemplate, args);
685                        out.println(message);
686                }
687        }
688
689        /**
690         * Hook to allow log messages to be intercepted and saved.
691         *
692         * @param LogLevel The level to log the message at.
693         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
694         * @param Args The arguments for any place-holders in the message template.
695         */
696        protected void save(@SuppressWarnings("unused") Level LogLevel, @SuppressWarnings("unused") String MessageTemplate, @SuppressWarnings("unused") Object[] Args) {
697                /* Does Nothing */
698        }
699
700        /**
701         * Hook to allow log messages to be intercepted and saved. In this version of this
702         * class, this is a no-op.
703         *
704         * @param Error The exception to handle.
705         */
706        protected void save(@SuppressWarnings("unused") Throwable Error) {
707                /* Does Nothing */
708        }
709
710        @Override
711        public void trace(Object arg0) {
712                trace("{0}", arg0);
713        }
714
715        @Override
716        public void trace(Object arg0, Throwable arg1) {
717                trace("{0} {1}", arg0, arg1);
718        }
719
720        /**
721         * Logs a trace message.
722         *
723         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
724         * @param Args The arguments for any place-holders in the message template.
725         */
726        public void trace(String MessageTemplate, Object... Args) {
727                log(Level.TRACE, MessageTemplate, format(Args));
728        }
729
730        /**
731         * @see Log#warn(Object)
732         */
733        @Override
734        public void warn(Object arg0) {
735                warn("{0}", arg0);
736        }
737
738        /**
739         * @see Log#warn(Object, Throwable)
740         */
741        @Override
742        public void warn(Object arg0, Throwable arg1) {
743                warn("{0} {1}", arg0, arg1);
744        }
745
746        /**
747         * Logs a warning message.
748         *
749         * @param MessageTemplate A message template, which can either be a plain string or contain place-holders like {0} and {1}.
750         * @param Args The arguments for any place-holders in the message template.
751         */
752        public void warn(String MessageTemplate, Object... Args) {
753                log(Level.WARN, MessageTemplate, format(Args));
754        }
755}