/*
 * Copyright 2014 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback;

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.arpnetworking.logback.jackson.RedactionFilter;
import com.arpnetworking.logback.jackson.StenoAnnotationIntrospector;
import com.arpnetworking.logback.jackson.StenoBeanSerializerModifier;
import com.arpnetworking.logback.jackson.ThrowableMixIn;
import com.arpnetworking.logback.serialization.steno.ArrayOfJsonSerialziationStrategy;
import com.arpnetworking.logback.serialization.steno.ArraySerialziationStrategy;
import com.arpnetworking.logback.serialization.steno.ListsSerialziationStrategy;
import com.arpnetworking.logback.serialization.steno.MapOfJsonSerialziationStrategy;
import com.arpnetworking.logback.serialization.steno.MapSerialziationStrategy;
import com.arpnetworking.logback.serialization.steno.ObjectAsJsonSerialziationStrategy;
import com.arpnetworking.logback.serialization.steno.ObjectSerialziationStrategy;
import com.arpnetworking.logback.serialization.steno.SafeSerializationHelper;
import com.arpnetworking.logback.serialization.steno.StandardSerializationStrategy;
import com.arpnetworking.logback.serialization.steno.StenoSerializationHelper;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import com.fasterxml.jackson.module.afterburner.AfterburnerModule;

import java.io.Serializable;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Encoder that builds Steno formatted log messages.
 *

 * This encoder accepts logging events in various formats and keys off the existence of an slf4j Marker added to the log
 * event to determine the format of the arguments.
 *

 * For logging events that don't have an associated Marker a standard Steno wrapper will be created with the string
 * logging message added as a 'message' field in the 'data' object. In such a case the name defaults to "log" unless a
 * name is specifically configured on the encoder.
 *

 * Example Output:
 *

 * {
 *   "time":"2011-11-11T00:00:00.000Z",
 *   "name":"log",
 *   "level":"info",
 *   "data":{
 *     "message":"log message"
 *   },
 *   "context":{
 *     "host":"h",
 *     "processId":"p",
 *     "threadId":"t"
 *   },
 *   "id":"oRw59PrARvatGNC7fiWw4A"
 * }
 *

 * For logging events that have the {@link com.arpnetworking.logback.StenoMarker#ARRAY_MARKER} Marker the logging event is
 * expected to contain the name of the event as the 'message', with the first argument being a String array (String[])
 * containing the keys to be added to the 'data' object and the second argument being an Object array (Object[])
 * containing the values for each key.
 *

 * Example logger call:
 *

 * log.info(StenoMarker.ARRAY_MARKER, "log", new String[] {"key1","key2"}, new Object[] {1234, "foo"});
 *

 * Example Output:
 *

 * {
 *   "time":"2011-11-11T00:00:00.000Z",
 *   "name":"log",
 *   "level":"info",
 *   "data":{
 *     "key1":1234,
 *     "key2":"foo"
 *   },
 *   "context":{
 *     "host":"h",
 *     "processId":"p",
 *     "threadId":"t"
 *   },
 *   "id":"oRw59PrARvatGNC7fiWw4A"
 * }
 *

 * For logging events that have the {@link com.arpnetworking.logback.StenoMarker#MAP_MARKER} Marker the logging event is
 * expected to contain the name of the event as the 'message', with the first argument being a map of key to value to be
 * included inside the 'data' object.
 *

 * Example logger call:
 *

 * log.info(StenoMarker.MAP_MARKER, "log", ImmutableMap.of("key1", 1234, "key2", "foo"));
 *

 * Example Output:
 *

 * {
 *   "time":"2011-11-11T00:00:00.000Z",
 *   "name":"log",
 *   "level":"info",
 *   "data":{
 *     "key1":1234,
 *     "key2":"foo"
 *   },
 *   "context":{
 *     "host":"h",
 *     "processId":"p",
 *     "threadId":"t"
 *   },
 *   "id":"oRw59PrARvatGNC7fiWw4A"
 * }
 *

 *
 * @author Gil Markham (gil at groupon dot com)
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @since 1.0.0
 */
public class StenoEncoder extends BaseLoggingEncoder implements Serializable {

    /**
     * Public constructor.
     */
    public StenoEncoder() {
        // Each instance of StenoEncoder requires its own ObjectMapper instance
        // in order to apply the correct filtering settings to support redaction.
        this(new ObjectMapper());
    }

    /* package private */ StenoEncoder(final ObjectMapper objectMapper) {
        // Each instance of StenoEncoder requires its own ObjectMapper instance
        // in order to apply the correct filtering settings to support redaction.
        this(objectMapper.getFactory(), objectMapper);
    }

    /* package private */ StenoEncoder(final JsonFactory jsonFactory, final ObjectMapper objectMapper) {

        // Initialize object mapper;
        _objectMapper = objectMapper;
        _objectMapper.setAnnotationIntrospector(new StenoAnnotationIntrospector(_objectMapper));
        final SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider();
        simpleFilterProvider.addFilter(RedactionFilter.REDACTION_FILTER_ID, new RedactionFilter(!DEFAULT_REDACT_NULL));
        // Initialize this here based on the above code, if it was initialized at the declaration site then things
        // could get out of sync
        _redactEnabled = true;
        _objectMapper.setFilterProvider(simpleFilterProvider);

        // Setup writing of Date/DateTime values
        _objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        _objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
        _objectMapper.setDateFormat(new StdDateFormat());

        // Simple module with customizations
        final SimpleModule module = new SimpleModule();
        module.setSerializerModifier(new StenoBeanSerializerModifier(this));
        _objectMapper.registerModule(module);

        // Throwable mix-in
        _objectMapper.setMixIns(Collections.singletonMap(Throwable.class, ThrowableMixIn.class));

        // After burner to improve data-bind performance
        _objectMapper.registerModule(new AfterburnerModule());

        // Serialization strategies
        _listsSerialziationStrategy = new ListsSerialziationStrategy(this, jsonFactory, _objectMapper);
        _objectAsJsonSerialziationStrategy = new ObjectAsJsonSerialziationStrategy(this, jsonFactory, _objectMapper);
        _objectSerialziationStrategy = new ObjectSerialziationStrategy(this, jsonFactory, _objectMapper);
        _mapOfJsonSerialziationStrategy = new MapOfJsonSerialziationStrategy(this, jsonFactory, _objectMapper);
        _mapSerialziationStrategy = new MapSerialziationStrategy(this, jsonFactory, _objectMapper);
        _arrayOfJsonSerialziationStrategy = new ArrayOfJsonSerialziationStrategy(this, jsonFactory, _objectMapper);
        _arraySerialziationStrategy = new ArraySerialziationStrategy(this, jsonFactory, _objectMapper);
        _standardSerializationStrategy = new StandardSerializationStrategy(this, jsonFactory, _objectMapper);
    }

    @Override
    public void start() {
        // Add configured Jackson modules
        _objectMapper.registerModules(_jacksonModules);
    }

    /**
     * Enables/Disables redaction support when serializing complex objects.  Redacted fields/properties marked
     * with the @LogRedact annotation will be output as a string with the value "{@code <REDACTED>}".
     *
     * @param redactEnabled - true to filter out redacted fields
     *
     * @since 1.1.0
     */
    public void setRedactEnabled(final boolean redactEnabled) {
        final SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider();
        if (redactEnabled) {
            simpleFilterProvider.addFilter(RedactionFilter.REDACTION_FILTER_ID, new RedactionFilter(!_redactNull));
        } else {
            simpleFilterProvider.addFilter(RedactionFilter.REDACTION_FILTER_ID,
                    SimpleBeanPropertyFilter.serializeAllExcept(Collections.<String>emptySet()));
        }
        _objectMapper.setFilterProvider(simpleFilterProvider);
        _redactEnabled = redactEnabled;
    }

    /**
     * Determines if fields marked with the annotation @LogRedact will be redacted in log output when serializing
     * complex objects. If true then values for annotated fields/properties will be output as a string with the value
     * "{@code <REDACTED>}", otherwise the value will be output as serialized json using a Jackson object mapper.
     * Default: true
     *
     * @return whether redacted fields or filtered
     *
     * @since 1.1.0
     */
    public boolean isRedactEnabled() {
        return _redactEnabled;
    }

    /**
     * Enables/Disables output of null for redacted fields when serializing complex objects.
     *
     * @param redactNull - true to redact null values, assuming redactEnabled is true
     *
     * @since 1.1.0
     */
    public void setRedactNull(final boolean redactNull) {
        if (_redactEnabled) {
            final SimpleFilterProvider simpleFilterProvider = new SimpleFilterProvider();
            simpleFilterProvider.addFilter(RedactionFilter.REDACTION_FILTER_ID, new RedactionFilter(!redactNull));
            _objectMapper.setFilterProvider(simpleFilterProvider);
        }
        _redactNull = redactNull;
    }

    /**
     * Determines how null values will be output for fields marked with @LogRedact.  If this is set
     * to false then null values will be output as null, otherwise a string with the value of "{@code <REDACTED>}" will
     * be used.  This property only takes effect if 'redactEnabled' is true.  Default: true
     *
     * @return whether nulls will be redacted
     *
     * @since 1.1.0
     */
    public boolean isRedactNull() {
        return _redactNull;
    }

    /**
     * Sets the log event name. This is used in place of the default log event name for logging events which do not
     * specify a {@link com.arpnetworking.logback.StenoMarker} Marker.
     *
     * @param logEventName The log event name.
     *
     * @since 1.1.0
     */
    public void setLogEventName(final String logEventName) {
        _logEventName = logEventName;
    }

    /**
     * Retrieve the default log event name. This is used in place of the default log event name for logging events which
     * do not specify a {@link com.arpnetworking.logback.StenoMarker} Marker.
     *
     * @return The default log event name.
     *
     * @since 1.1.0
     */
    public String getLogEventName() {
        return _logEventName;
    }

    /**
     * Inject thread. This controls whether the process id is injected into each message's context. By default this
     * is true.
     *
     * @param value Whether to inject the process id.
     *
     * @since 1.1.0
     */
    public void setInjectContextProcess(final boolean value) {
        _injectContextProcess = value;
    }

    /**
     * Whether process id is injected into the context. By default this is true.
     *
     * @return True if and only if process id is injected into the context.
     *
     * @since 1.1.0
     */
    public boolean isInjectContextProcess() {
        return _injectContextProcess;
    }

    /**
     * Inject thread. This controls whether the host name is injected into each message's context. By default this
     * is true.
     *
     * @param value Whether to inject the host name.
     *
     * @since 1.1.0
     */
    public void setInjectContextHost(final boolean value) {
        _injectContextHost = value;
    }

    /**
     * Whether host name is injected into the context. By default this is true.
     *
     * @return True if and only if host name is injected into the context.
     *
     * @since 1.1.0
     */
    public boolean isInjectContextHost() {
        return _injectContextHost;
    }

    /**
     * Inject thread. This controls whether the thread name is injected into each message's context. By default this
     * is true.
     *
     * @param value Whether to inject the thread name.
     *
     * @since 1.1.0
     */
    public void setInjectContextThread(final boolean value) {
        _injectContextThread = value;
    }

    /**
     * Whether thread name is injected into the context. By default this is true.
     *
     * @return True if and only if thread name is injected into the context.
     *
     * @since 1.1.0
     */
    public boolean isInjectContextThread() {
        return _injectContextThread;
    }

    /**
     * Inject logger. This controls whether the logger name is injected into each message's context. By default this
     * is false.
     *
     * Note: This field does not strictly conform to the current Steno standard. The current Steno standard is
     * documented as a Json Schema in resources/steno.schema.json. For this reason the logger context
     * field is disabled by default.
     *
     * @param value Whether to inject the logger name.
     *
     * @since 1.1.0
     */
    public void setInjectContextLogger(final boolean value) {
        _injectContextLogger = value;
    }

    /**
     * Whether logger name is injected into the context. By default this is false.
     *
     * @return True if and only if logger name is injected into the context.
     *
     * @since 1.1.0
     */
    public boolean isInjectContextLogger() {
        return _injectContextLogger;
    }

    /**
     * Compress logger name. This controls whether the logger name is compressed prior to injection into each message's
     * context. By default this is false. Compression takes each part of name separated by "." (aka period or dot) and
     * reduces all but the last to only the first character. Compression has no effect if logger name injection is
     * disabled.
     *
     * @param value Whether to compress the logger name.
     *
     * @since 1.3.1
     */
    public void setCompressLoggerName(final boolean value) {
        _compressLoggerName = value;
    }

    /**
     * Whether logger name is compressed. By default this is false. Compression takes each part of name separated by
     * "." (aka period or dot) and reduces all but the last to only the first character.
     *
     * @return True if and only if logger name is to be compressed.
     *
     * @since 1.3.1
     */
    public boolean isCompressLoggerName() {
        return _compressLoggerName;
    }

    /**
     * Inject file. This controls whether the source file name is injected into each message's context. By default this
     * is false.
     *
     * @param value Whether to inject the file name.
     *
     * @since 1.1.0
     */
    public void setInjectContextFile(final boolean value) {
        _injectContextFile = value;
    }

    /**
     * Whether source file name is injected into the context. By default this is false.
     *
     * @return True if and only if source file name is injected into the context.
     *
     * @since 1.1.0
     */
    public boolean isInjectContextFile() {
        return _injectContextFile;
    }

    /**
     * Inject class. This controls whether the source class name is injected into each message's context. By default this
     * is false.
     *
     * @param value Whether to inject the class name.
     *
     * @since 1.1.0
     */
    public void setInjectContextClass(final boolean value) {
        _injectContextClass = value;
    }

    /**
     * Whether class name is injected into the context. By default this is false.
     *
     * @return True if and only if class name is injected into the context.
     *
     * @since 1.1.0
     */
    public boolean isInjectContextClass() {
        return _injectContextClass;
    }

    /**
     * Inject method. This controls whether the method name is injected into each message's context. By default this
     * is false.
     *
     * @param value Whether to inject the method name.
     *
     * @since 1.1.0
     */
    public void setInjectContextMethod(final boolean value) {
        _injectContextMethod = value;
    }

    /**
     * Whether method name is injected into the context. By default this is false.
     *
     * @return True if and only if method name is injected into the context.
     *
     * @since 1.1.0
     */
    public boolean isInjectContextMethod() {
        return _injectContextMethod;
    }

    /**
     * Inject line. This controls whether the line number is injected into each message's context. By default this
     * is false.
     *
     * @param value Whether to inject the line number.
     *
     * @since 1.1.0
     */
    public void setInjectContextLine(final boolean value) {
        _injectContextLine = value;
    }

    /**
     * Whether line number is injected into the context. By default this is false.
     *
     * @return True if and only if line number is injected into the context.
     *
     * @since 1.1.0
     */
    public boolean isInjectContextLine() {
        return _injectContextLine;
    }

    /**
     * Add MDC property to inject into the context. This controls which MDC properties are injected into each message's
     * context. By default no properties are injected.
     *
     * @param key The MDC property key to inject into the context.
     *
     * @since 1.4.0
     */
    public void addInjectContextMdc(final String key) {
        _injectMdcProperties.add(key);
    }

    /**
     * Which MDC properties are injected into the context. By default this is none.
     *
     * @return The iterator over MDC properties injected into the context.
     *
     * @since 1.4.0
     */
    public Iterator<String> iteratorForInjectContextMdc() {
        return _injectMdcProperties.iterator();
    }

    /**
     * Determine if the specific MDC property key is injected into the context. By default it is not.
     *
     * @param key The MDC property key to check.
     * @return True if and only if the key is injected into the context.
     *
     * @since 1.4.0
     */
    public boolean isInjectContextMdc(final String key) {
        return _injectMdcProperties.contains(key);
    }

    /**
     * Add Jackson Module.
     *
     * @param module The Jackson Module.
     *
     * @since 1.5.0
     */
    public void addJacksonModule(final Module module) {
        _jacksonModules.add(module);
    }

    /**
     * Which Jackson Module instances are configured.
     *
     * @return The iterator over configured Jackson Module instances.
     *
     * @since 1.5.0
     */
    public Iterator<Module> iteratorForJacksonModule() {
        return _jacksonModules.iterator();
    }

    /**
     * Determine if the specific Jackson Module is configured.
     *
     * @param module The Jackson Module.
     * @return True if and only if the Module is configured.
     *
     * @since 1.5.0
     */
    public boolean isJacksonModule(final Module module) {
        return _jacksonModules.contains(module);
    }

    /**
     * This controls whether the encoder should only encode types when safe to do so. By default this is true. Types are
     * considered safe for serialization if any of the following are true:
     *
     *

     *
The instance is null.

     *
The type is a String, Number or Boolean.

     *
The type is a JsonNode.

     *
The type is associated with a custom serializer (e.g. not BeanSerializer):
     *

     *
Set with the class annotation @JsonSerialize.

     *
Set with by a registered module.

     *
Set with the method annotation @JsonValue.

     *
Set with the method annotation @LogValue.

     *

     *

     *
The class is explicitly marked safe for logging with the annotation @Loggable.

     *

     *
     * When safe is set to true and a type is determined to be unsafe it is serialized as a reference only.
     *
     * @param value True if and only if the encoder should only encode types when safe to do so.
     *
     * @since 1.8.0
     */
    public void setSafe(final boolean value) {
        _safe = value;
    }

    /**
     * Whether the encoder should only encode types when safe to do so. By default this is true.
     *
     * @return True if and only if the encoder should only encode types when safe to do so.
     *
     * @since 1.8.0
     */
    public boolean isSafe() {
        return _safe;
    }

    /**
     * Inject bean identifier attributes. This controls whether the the instance identifier and class name are
     * always injected into each serialized data or context value. The values are always injected for non-loggable
     * types and this setting allows injection for all types. By default this is false.
     *
     * @since 1.9.0
     *
     * @param value Whether to inject the bean identifier attributes for all beans irregardless of loggability.
     */
    public void setInjectBeanIdentifier(final boolean value) {
        _injectBeanIdentifier = value;
    }

    /**
     * Whether bean identifier attributes are injected. By default this is false.
     *
     * @since 1.9.0
     *
     * @return True if and only if bean identifier attributes are injected.
     */
    public boolean isInjectBeanIdentifier() {
        return _injectBeanIdentifier;
    }

    @Override
    protected String encodeAsString(final ILoggingEvent event, final EncodingException ee) {
        final StringBuilder encoder = new StringBuilder()
                .append("{\"time\":\"")
                .append(ISO_DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(event.getTimeStamp())))
                .append("\",\"name\":\"EncodingException\",\"level\":\"warn\",\"data\":{\"originalMessage\":");
        SafeSerializationHelper.safeEncodeValue(encoder, event.getMessage());
        encoder.append("},\"exception\":");
        SafeSerializationHelper.safeEncodeValue(encoder, ee);
        encoder.append(",\"context\":");
        SafeSerializationHelper.safeEncodeValue(encoder, ee.getContext());
        encoder.append(",\"id\":\"")
                .append(StenoSerializationHelper.createId())
                .append("\",\"version\":\"0\"}\n");
        return encoder.toString();
    }

    @Override
    protected String buildStandardMessage(final ILoggingEvent event) throws EncodingException {
        try {
            return _standardSerializationStrategy.serialize(event, _logEventName);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(SafeSerializationHelper.createSafeContext(this, event, _objectMapper), t);
        }
    }

    @Override
    protected String buildArrayMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final String[] keys,
            @Nullable final Object[] values)
            throws EncodingException {

        try {
            return _arraySerialziationStrategy.serialize(
                    event,
                    firstNonNull(eventName, _logEventName),
                    keys,
                    values);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(SafeSerializationHelper.createSafeContext(this, event, _objectMapper), t);
        }
    }

    @Override
    protected String buildArrayJsonMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final String[] keys,
            @Nullable final String[] jsonValues)
            throws EncodingException {

        try {
            return _arrayOfJsonSerialziationStrategy.serialize(
                    event,
                    firstNonNull(eventName, _logEventName),
                    keys,
                    jsonValues);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(SafeSerializationHelper.createSafeContext(this, event, _objectMapper), t);
        }
    }

    @Override
    protected String buildMapMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final Map<String, ?> map)
            throws EncodingException {

        try {
            return _mapSerialziationStrategy.serialize(
                    event,
                    firstNonNull(eventName, _logEventName),
                    map);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(SafeSerializationHelper.createSafeContext(this, event, _objectMapper), t);
        }
    }

    @Override
    protected String buildMapJsonMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final Map<String, String> map)
            throws EncodingException {

        try {
            return _mapOfJsonSerialziationStrategy.serialize(
                    event,
                    firstNonNull(eventName, _logEventName),
                    map);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(SafeSerializationHelper.createSafeContext(this, event, _objectMapper), t);
        }
    }

    @Override
    protected String buildObjectMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final Object data)
            throws EncodingException {

        try {
            return _objectSerialziationStrategy.serialize(
                    event,
                    firstNonNull(eventName, _logEventName),
                    // TODO(ville): This is where the switch for null as-is for object marker should go [issue #4]
                    data == null ? Collections.emptyMap() : data);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(SafeSerializationHelper.createSafeContext(this, event, _objectMapper), t);
        }
    }

    @Override
    protected String buildObjectJsonMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            final String jsonData)
            throws EncodingException {

        try {
            return _objectAsJsonSerialziationStrategy.serialize(
                    event,
                    firstNonNull(eventName, _logEventName),
                    jsonData);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(SafeSerializationHelper.createSafeContext(this, event, _objectMapper), t);
        }
    }

    @Override
    protected String buildListsMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final List<String> dataKeys,
            @Nullable final List<Object> dataValues,
            @Nullable final List<String> contextKeys,
            @Nullable final List<Object> contextValues)
            throws EncodingException {

        try {
            return _listsSerialziationStrategy.serialize(
                    event,
                    firstNonNull(eventName, _logEventName),
                    dataKeys,
                    dataValues,
                    contextKeys,
                    contextValues);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(
                    SafeSerializationHelper.createSafeContext(
                            this,
                            event,
                            _objectMapper,
                            contextKeys,
                            contextValues),
                    t);
        }
    }

    /* package private */ static <T> T firstNonNull(@Nullable final T first, @Nullable final T second) {
        if (first != null) {
            return first;
        } else if (second != null) {
            return second;
        }
        throw new NullPointerException("Both arguments are null");
    }

    private ObjectMapper _objectMapper;
    private final ListsSerialziationStrategy _listsSerialziationStrategy;
    private final ObjectAsJsonSerialziationStrategy _objectAsJsonSerialziationStrategy;
    private final ObjectSerialziationStrategy _objectSerialziationStrategy;
    private final MapOfJsonSerialziationStrategy _mapOfJsonSerialziationStrategy;
    private final MapSerialziationStrategy _mapSerialziationStrategy;
    private final ArrayOfJsonSerialziationStrategy _arrayOfJsonSerialziationStrategy;
    private final ArraySerialziationStrategy _arraySerialziationStrategy;
    private final StandardSerializationStrategy _standardSerializationStrategy;

    private String _logEventName = STANDARD_LOG_EVENT_NAME;
    private boolean _redactEnabled;
    private boolean _redactNull = DEFAULT_REDACT_NULL;
    private boolean _injectContextProcess = true;
    private boolean _injectContextHost = true;
    private boolean _injectContextThread = true;
    private boolean _injectContextLogger = false;
    private boolean _compressLoggerName = false;
    private boolean _injectContextClass = false;
    private boolean _injectContextFile = false;
    private boolean _injectContextMethod = false;
    private boolean _injectContextLine = false;
    private Set<String> _injectMdcProperties = new LinkedHashSet<>();
    private Set<Module> _jacksonModules = new LinkedHashSet<>();
    private boolean _safe = true;
    private boolean _injectBeanIdentifier = false;

    private static final boolean DEFAULT_REDACT_NULL = true;
    private static final String STANDARD_LOG_EVENT_NAME = "log";
    private static final DateTimeFormatter ISO_DATE_TIME_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ").withZone(ZoneId.of("UTC"));
    private static final long serialVersionUID = -1803222342605243667L;
}


/*
 * Copyright 2015 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback.serialization.keyvalue;

import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.pattern.LoggerConverter;
import ch.qos.logback.classic.pattern.ThreadConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import com.arpnetworking.logback.HostConverter;
import com.arpnetworking.logback.KeyValueEncoder;
import com.arpnetworking.logback.ProcessConverter;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Helper functions and functors for Key-Value serialziation.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @since 1.9.0
 */
public final class KeyValueSerializationHelper {

    /**
     * Create a context based on the KeyValueEncoder configuration.
     *
     * @since 1.9.0
     *
     * @param encoder The KeyValueEncoder instance.
     * @param event The ILoggingEvent instance.
     * @param contextKeys The additional user provided context keys.
     * @param contextValues The additional user provided context values matching the keys.
     * @return Map with event context.
     */
    public static Map<String, Object> createContext(
            final KeyValueEncoder encoder,
            final ILoggingEvent event,
            @Nullable final List<String> contextKeys,
            @Nullable final List<Object> contextValues) {
        final Map<String, Object> context = new LinkedHashMap<>();
        context.put("host", KeyValueSerializationHelper.HOST_CONVERTER.convert(event));
        context.put("processId", KeyValueSerializationHelper.PROCESS_CONVERTER.convert(event));
        context.put("threadId", KeyValueSerializationHelper.THREAD_CONVERTER.convert(event));
        if (contextKeys != null) {
            final int contextValuesLength = contextValues == null ? 0 : contextValues.size();
            for (int i = 0; i < contextKeys.size(); ++i) {
                final String key = contextKeys.get(i);
                final Object value = i < contextValuesLength ? contextValues.get(i) : null;
                context.put(
                        key,
                        value);
            }
        }
        return context;
    }

    private static final ClassicConverter HOST_CONVERTER = new HostConverter();
    private static final ClassicConverter PROCESS_CONVERTER = new ProcessConverter();
    private static final ClassicConverter THREAD_CONVERTER = new ThreadConverter();
    private static final ClassicConverter LOGGER_CONVERTER = new LoggerConverter();

    static {
        HOST_CONVERTER.start();
        PROCESS_CONVERTER.start();
        THREAD_CONVERTER.start();
        LOGGER_CONVERTER.start();
    }

    private KeyValueSerializationHelper() {}
}


/*
 * Copyright 2014 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
import org.slf4j.Marker;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Base encoder class containing methods to determine if a Steno marker is present.
 *
 * @author Gil Markham (gil at groupon dot com)
 * @since 1.0.0
 */
public abstract class BaseLoggingEncoder extends LayoutWrappingEncoder<ILoggingEvent> {

    @Override
    public Charset getCharset() {
        return StandardCharsets.UTF_8;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public byte[] encode(final ILoggingEvent event) {
        final Marker marker = event.getMarker();
        final String name = event.getMessage();
        final Object[] argumentArray = event.getArgumentArray();

        String output;
        try {
            output = encodeAsString(event, marker, name, argumentArray);
        } catch (final EncodingException ee) {
            output = encodeAsString(event, ee);
        }
        return encodeString(output);
    }

    byte[] encodeString(final String s) {
            return s.getBytes(getCharset());
    }

    /**
     * Safely encode an instance of EncodingException.
     *
     * @since 1.9.0
     *
     * @param event Instance of ILoggingEvent.
     * @param ee Instance of EncodingException to encode.
     * @return Encoded version of EncodingException.
     */
    protected abstract String encodeAsString(ILoggingEvent event, EncodingException ee);

    /**
     * Encode an array based message into a String.
     *
     * @since 1.7.0
     *
     * @param event Instance of ILoggingEvent.
     * @param eventName The name of the event.
     * @param keys Array of keys; indices must match value indices.
     * @param values Array of values; indices must match key indices.
     * @return Message encoded as a String.
     * @throws EncodingException If encoding fails for any reason.
     */
    protected abstract String buildArrayMessage(
            ILoggingEvent event,
            @Nullable String eventName,
            @Nullable String[] keys,
            @Nullable Object[] values)
            throws EncodingException;

    /**
     * Encode a JSON array based message into a String.
     *
     * @since 1.7.0
     *
     * @param event Instance of ILoggingEvent.
     * @param eventName The name of the event.
     * @param keys Array of keys; indices must match value indices.
     * @param jsonValues Array of values encoded in JSON; indices must match key indices.
     * @return Message encoded as a String.
     * @throws EncodingException If encoding fails for any reason.
     */
    protected abstract String buildArrayJsonMessage(
            ILoggingEvent event,
            @Nullable String eventName,
            @Nullable String[] keys,
            @Nullable String[] jsonValues)
            throws EncodingException;

    /**
     * Encode a map based message into a String.
     *
     * @since 1.7.0
     *
     * @param event Instance of ILoggingEvent.
     * @param eventName The name of the event.
     * @param map Map of key to value pairs.
     * @return Message encoded as a String.
     * @throws EncodingException If encoding fails for any reason.
     */
    protected abstract String buildMapMessage(
            ILoggingEvent event,
            @Nullable String eventName,
            @Nullable Map<String, ? extends Object> map)
            throws EncodingException;

    /**
     * Encode a map of encoded JSON values based message into a String.
     *
     * @since 1.7.0
     *
     * @param event Instance of ILoggingEvent.
     * @param eventName The name of the event.
     * @param map Map of key to json encoded value pairs.
     * @return Message encoded as a String.
     * @throws EncodingException If encoding fails for any reason.
     */
    protected abstract String buildMapJsonMessage(
            ILoggingEvent event,
            @Nullable String eventName,
            @Nullable Map<String, String> map)
            throws EncodingException;

    /**
     * Encode an object value based message into a String.
     *
     * @since 1.7.0
     *
     * @param event Instance of ILoggingEvent.
     * @param eventName The name of the event.
     * @param data Object to be serialized as the data.
     * @return Message encoded as a String.
     * @throws EncodingException If encoding fails for any reason.
     */
    protected abstract String buildObjectMessage(
            ILoggingEvent event,
            @Nullable String eventName,
            @Nullable Object data)
            throws EncodingException;

    /**
     * Encode a JSON encoded object value based message into a String.
     *
     * @since 1.7.0
     *
     * @param event Instance of ILoggingEvent.
     * @param eventName The name of the event.
     * @param jsonData JSON encoded object to be serialized as the data.
     * @return Message encoded as a String.
     * @throws EncodingException If encoding fails for any reason.
     */
    protected abstract String buildObjectJsonMessage(
            ILoggingEvent event,
            @Nullable String eventName,
            String jsonData)
            throws EncodingException;

    /**
     * Encode a two pairs of lists representing data and context key-value pairs.
     *
     * @since 1.7.0
     *
     * @param event Instance of ILoggingEvent.
     * @param eventName The name of the event.
     * @param dataKeys List of data keys.
     * @param dataValues List of data values.
     * @param contextKeys List of context keys.
     * @param contextValues List of context values.
     * @return Message encoded as a String.
     * @throws EncodingException If encoding fails for any reason.
     */
    protected abstract String buildListsMessage(
            ILoggingEvent event,
            @Nullable String eventName,
            @Nullable List<String> dataKeys,
            @Nullable List<Object> dataValues,
            @Nullable List<String> contextKeys,
            @Nullable List<Object> contextValues)
            throws EncodingException;

    /**
     * Encode a standard message into a String.
     *
     * @since 1.7.0
     *
     * @param event Instance of ILoggingEvent.
     * @return Message encoded as a String.
     * @throws EncodingException If encoding fails for any reason.
     */
    protected abstract String buildStandardMessage(ILoggingEvent event) throws EncodingException;

    /**
     * Determine whether the marker represents an array event.
     *
     * @since 1.0.0
     *
     * @param marker The Marker instance to evaluate.
     * @return True if and only if marker represents an array event.
     */
    protected boolean isArrayStenoEvent(@Nullable final Marker marker) {
        return marker != null && marker.contains(StenoMarker.ARRAY_MARKER);
    }

    /**
     * Determine whether the marker represents a JSON array event.
     *
     * @since 1.0.4
     *
     * @param marker The Marker instance to evaluate.
     * @return True if and only if marker represents a JSON array event.
     */
    protected boolean isArrayJsonStenoEvent(@Nullable final Marker marker) {
        return marker != null && marker.contains(StenoMarker.ARRAY_JSON_MARKER);
    }

    /**
     * Determine whether the marker represents a map event.
     *
     * @since 1.0.4
     *
     * @param marker The Marker instance to evaluate.
     * @return True if and only if marker represents an array event.
     */
    protected boolean isMapStenoEvent(@Nullable final Marker marker) {
        return marker != null && marker.contains(StenoMarker.MAP_MARKER);
    }

    /**
     * Determine whether the marker represents a JSON map event.
     *
     * @since 1.0.4
     *
     * @param marker The Marker instance to evaluate.
     * @return True if and only if marker represents a JSON map event.
     */
    protected boolean isMapJsonStenoEvent(@Nullable final Marker marker) {
        return marker != null && marker.contains(StenoMarker.MAP_JSON_MARKER);
    }

    /**
     * Determine whether the marker represents an object event.
     *
     * @since 1.1.0
     *
     * @param marker The Marker instance to evaluate.
     * @return True if and only if marker represents an object event.
     */
    protected boolean isObjectStenoEvent(@Nullable final Marker marker) {
        return marker != null && marker.contains(StenoMarker.OBJECT_MARKER);
    }

    /**
     * Determine whether the marker represents a JSON object event.
     *
     * @since 1.1.0
     *
     * @param marker The Marker instance to evaluate.
     * @return True if and only if marker represents a JSON object event.
     */
    protected boolean isObjectJsonStenoEvent(@Nullable final Marker marker) {
        return marker != null && marker.contains(StenoMarker.OBJECT_JSON_MARKER);
    }

    /**
     * Determine whether the marker represents a lists event.
     *
     * @since 1.3.0
     *
     * @param marker The Marker instance to evaluate.
     * @return True if and only if marker represents a lists event.
     */
    protected boolean isListsStenoEvent(@Nullable final Marker marker) {
        return marker != null && marker.contains(StenoMarker.LISTS_MARKER);
    }

    @SuppressWarnings("unchecked")
    private String encodeAsString(
            final ILoggingEvent event,
            final Marker marker,
            final String name,
            final Object[] argumentArray)
            throws EncodingException {
        final String output;
        if (isListsStenoEvent(marker)) {
            output = buildListsMessage(
                    event,
                    name,
                    (List<String>) argumentArray[0],  // data keys
                    (List<Object>) argumentArray[1],  // data object values
                    (List<String>) argumentArray[2],  // context keys
                    (List<Object>) argumentArray[3]); // context object values
        } else if (isArrayStenoEvent(marker)) {
            output = buildArrayMessage(
                    event,
                    name,
                    (String[]) argumentArray[0],  // keys
                    (Object[]) argumentArray[1]); // object values
        } else if (isArrayJsonStenoEvent(marker)) {
            output = buildArrayJsonMessage(
                    event,
                    name,
                    (String[]) argumentArray[0],  // keys
                    (String[]) argumentArray[1]); // json values
        } else if (isMapStenoEvent(marker)) {
            output = buildMapMessage(
                    event,
                    name,
                    (Map<String, Object>) argumentArray[0]); // key to object value map
        } else if (isMapJsonStenoEvent(marker)) {
            output = buildMapJsonMessage(
                    event,
                    name,
                    (Map<String, String>) argumentArray[0]); // key to json value map
        } else if (isObjectStenoEvent(marker)) {
            output = buildObjectMessage(
                    event,
                    name,
                    argumentArray[0]); // data object value
        } else if (isObjectJsonStenoEvent(marker)) {
            output = buildObjectJsonMessage(
                    event,
                    name,
                    (String) argumentArray[0]); // data json value
        } else {
            output = buildStandardMessage(event);
        }
        return output;
    }
}


/*
 * Copyright 2015 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.steno;

import com.arpnetworking.logback.annotations.LogValue;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import javax.annotation.Nullable;

/**
 * Limits actual log output to at most once per specified Period.
 * The implementation will add two data attributes _skipped and
 * _lastLogTime to the wrapped LogBuilder instance.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @since 1.9.0
 * @deprecated This class is not thread safe and easily leads to misuse. Use RateLimitLogger.
 */
@Deprecated
public class RateLimitLogBuilder implements LogBuilder {

    /**
     * Public constructor.
     *
     * @since 1.9.0
     *
     * @param logBuilder Instance of LogBuilder.
     * @param duration Minimum time between log message output.
     */
    public RateLimitLogBuilder(final LogBuilder logBuilder, final Duration duration) {
        this(logBuilder, duration, Clock.systemUTC());
    }

    /**
     * Package private constructor.
     *
     * @since 1.9.0
     *
     * @param logBuilder Instance of LogBuilder.
     * @param duration Minimum time between log message output.
     * @param clock Instance of Clock.
     */
    /* package private */ RateLimitLogBuilder(final LogBuilder logBuilder, final Duration duration, final Clock clock) {
        _logBuilder = logBuilder;
        _duration = duration;
        _clock = clock;
    }

    @Override
    public LogBuilder setEvent(final String value) {
        _logBuilder.setEvent(value);
        return this;
    }

    @Override
    public LogBuilder setMessage(final String value) {
        _logBuilder.setMessage(value);
        return this;
    }

    @Override
    public LogBuilder setThrowable(final Throwable value) {
        _logBuilder.setThrowable(value);
        return this;
    }

    @Override
    public LogBuilder addData(final String name, @Nullable final Object value) {
        _logBuilder.addData(name, value);
        return this;
    }

    @Override
    public LogBuilder addContext(final String name, @Nullable final Object value) {
        _logBuilder.addContext(name, value);
        return this;
    }

    @Override
    public void log() {
        final Instant now = _clock.instant();
        boolean shouldLog = true;
        if (_lastLogTime.isPresent()) {
            if (!_lastLogTime.get().plus(_duration).isBefore(now)) {
                shouldLog = false;
                ++_skipped;
            }
        }
        if (shouldLog) {
            _logBuilder
                    .addData("_skipped", _skipped)
                    .addData("_lastLogTime", _lastLogTime)
                    .log();
            _lastLogTime = Optional.of(now);
            _skipped = 0;
        }
    }

    /**
     * Generate a Steno log compatible representation.
     *
     * @since 1.9.0
     *
     * @return Steno log compatible representation.
     */
    @LogValue
    public Object toLogValue() {
        return LogValueMapFactory.<String, Object>builder()
                .put("logBuilder", _logBuilder)
                .put("duration", _duration)
                .put("lastLogTime", _lastLogTime)
                .put("skipped", _skipped)
                .build();
    }

    @Override
    public String toString() {
        return toLogValue().toString();
    }

    private final LogBuilder _logBuilder;
    private final Duration _duration;
    private final Clock _clock;
    private Optional<Instant> _lastLogTime = Optional.empty();
    private int _skipped = 0;
}


/*
 * Copyright 2015 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback.serialization.steno;

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.arpnetworking.logback.StenoEncoder;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.Serializable;
import javax.annotation.Nullable;

/**
 * Serialization strategy for object based message specifications.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @since 1.3.1
 */
public class ObjectSerialziationStrategy implements Serializable {

    /**
     * Public constructor.
     *
     * @param encoder Instance of StenoEncoder.
     * @param jsonFactory Instance of JsonFactory.
     * @param objectMapper Instance of ObjectMapper.
     */
    public ObjectSerialziationStrategy(
            final StenoEncoder encoder,
            final JsonFactory jsonFactory,
            final ObjectMapper objectMapper) {
        _objectMapper = objectMapper;
        _objectAsJsonStrategy = new ObjectAsJsonSerialziationStrategy(encoder, jsonFactory, objectMapper);
    }

    /**
     * Serialize an event.
     *
     * @param event The event.
     * @param eventName The event name.
     * @param data The message data Object.
     * @return Serialization of message as a String.
     * @throws Exception Serialization may throw any Exception.
     */
    public String serialize(
            final ILoggingEvent event,
            final String eventName,
            @Nullable final Object data)
            throws Exception {
        final String jsonData = _objectMapper.writeValueAsString(data);
        return _objectAsJsonStrategy.serialize(
                event,
                eventName,
                jsonData);
    }

    private final ObjectMapper _objectMapper;
    private final ObjectAsJsonSerialziationStrategy _objectAsJsonStrategy;

    private static final long serialVersionUID = 5931045066524060672L;
}


/*
 * Copyright 2014 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback;

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.arpnetworking.logback.serialization.keyvalue.KeyValueSerializationHelper;

import java.io.StringWriter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Encoder to take a Steno log event {@link com.arpnetworking.logback.StenoMarker} and convert it to an
 * easier to read key=value output format when running locally.
 *
 * @author Gil Markham (gil at groupon dot com)
 * @since 1.0.0
 */
public class KeyValueEncoder extends BaseLoggingEncoder {

    /**
     * Sets the default log event name.
     *
     * @param logEventName The log event name.
     *
     * @since 1.8.1
     */
    public void setLogEventName(final String logEventName) {
        _logEventName = logEventName;
    }

    /**
     * Retrieve the default log event name.
     *
     * @return The default log event name.
     *
     * @since 1.8.1
     */
    public String getLogEventName() {
        return _logEventName;
    }

    @Override
    protected String encodeAsString(final ILoggingEvent event, final EncodingException ee) {
        return ee.toString() + " originalMessage=" + event.getMessage() + "\n";
    }

    @Override
    protected String buildArrayMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final String[] keys,
            @Nullable final Object[] values)
            throws EncodingException {

        try {
            return createMessage(event, eventName, keys, values);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(createSafeContext(event), t);
        }
    }

    @Override
    protected String buildArrayJsonMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final String[] keys,
            @Nullable final String[] jsonValues)
            throws EncodingException {

        try {
            final Object[] escapedJsonValues = jsonValues == null ? null : escapeStringValues(jsonValues);
            return createMessage(event, eventName, keys, escapedJsonValues);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(createSafeContext(event), t);
        }
    }

    @Override
    protected String buildMapMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final Map<String, ? extends Object> map)
            throws EncodingException {

        try {
            final String[] keys = map == null ? null : new String[map.size()];
            final Object[] values = map == null ? null : new Object[map.size()];
            if (map != null) {
                int index = 0;
                for (final Map.Entry<String, ? extends Object> entry : map.entrySet()) {
                    keys[index] = entry.getKey();
                    values[index] = entry.getValue();
                    ++index;
                }
            }

            return createMessage(
                    event,
                    eventName,
                    keys,
                    values);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(createSafeContext(event), t);
        }
    }

    @Override
    protected String buildMapJsonMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final Map<String, String> map)
            throws EncodingException {

        try {
            final String[] keys = map == null ? null : new String[map.size()];
            final Object[] values = map == null ? null : new Object[map.size()];
            if (map != null) {
                int index = 0;
                for (final Map.Entry<String, ? extends Object> entry : map.entrySet()) {
                    keys[index] = entry.getKey();
                    values[index] = entry.getValue();
                    ++index;
                }
            }

            return createMessage(
                    event,
                    eventName,
                    keys,
                    values == null ? null : escapeStringValues(values));
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(createSafeContext(event), t);
        }
    }

    @Override
    protected String buildObjectMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final Object data)
            throws EncodingException {

        try {
            return createMessage(
                    event,
                    eventName,
                    new String[]{"data"},
                    escapeStringValues(new Object[]{data == null ? null : data.toString()}));
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(createSafeContext(event), t);
        }
    }

    @Override
    protected String buildObjectJsonMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            final String jsonData)
            throws EncodingException {

        try {
            return createMessage(
                    event,
                    eventName,
                    new String[]{"data"},
                    escapeStringValues(new Object[]{jsonData}));
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(createSafeContext(event), t);
        }
    }

    @Override
    protected String buildListsMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final List<String> dataKeys,
            @Nullable final List<Object> dataValues,
            @Nullable final List<String> contextKeys,
            @Nullable final List<Object> contextValues)
            throws EncodingException {

        try {
            final int dataKeysSize = dataKeys == null ? 0 : dataKeys.size();
            final int contextKeysSize = contextKeys == null ? 0 : contextKeys.size();
            final int dataValuesSize = dataValues == null ? 0 : dataValues.size();
            final int contextValuesSize = contextValues == null ? 0 : contextValues.size();

            final int size = dataKeysSize + contextKeysSize;

            final String[] keys = new String[size];
            final Object[] values = new Object[size];

            int index = 0;
            if (contextKeys != null) {
                for (final String key : contextKeys) {
                    keys[index++] = key;
                }
            }
            if (dataKeys != null) {
                for (final String key : dataKeys) {
                    keys[index++] = key;
                }
            }
            index = 0;
            int valueCount = 0;
            for (int i = 0; i < contextKeysSize; ++i) {
                if (valueCount < contextValuesSize) {
                    values[index] = contextValues.get(i);
                } else {
                    values[index] = null;
                }
                ++index;
                ++valueCount;
            }
            valueCount = 0;
            for (int i = 0; i < dataKeysSize; ++i) {
                if (valueCount < dataValuesSize) {
                    values[index] = dataValues.get(i);
                } else {
                    values[index] = null;
                }
                ++index;
                ++valueCount;
            }

            return createMessage(
                    event,
                    eventName,
                    keys,
                    escapeStringValues(values));
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(createSafeContext(event, contextKeys, contextValues), t);
        }
    }

    @Override
    protected String buildStandardMessage(final ILoggingEvent event) throws EncodingException {
        try {
            return getLayout().doLayout(event);
            // CHECKSTYLE.OFF: IllegalCatch: Ensure any exception or error is caught to prevent Appender death.
        } catch (final Throwable t) {
            // CHECKSTYLE.ON: IllegalCatch
            throw new EncodingException(createSafeContext(event), t);
        }
    }

    /**
     * Create a format String compatible with MessageFormatter.
     *
     * @param name The name.
     * @param keys The argument keys.
     * @return The format String.
     */
    protected String buildFormatString(@Nullable final String name, @Nullable final String[] keys) {
        final String effectiveName = name == null ? _logEventName : name;
        final StringWriter stringWriter = new StringWriter();
        stringWriter.append("name=\"").append(effectiveName).append("\"");
        if (keys != null && keys.length > 0) {
            for (final String key : keys) {
                stringWriter.append(", ").append(key).append("=\"{}\"");
            }
        }
        return stringWriter.toString();
    }

    /**
     * Escape all String instances.
     *
     * @param values Arguments to encode.
     * @return Encoded arguments.
     */
    protected Object[] escapeStringValues(final Object[] values) {
        final Object[] escapedValues = new Object[values.length];
        for (int i = 0; i < values.length; i++) {
            Object value = values[i];
            // Instance of check implies value is not null
            if (value instanceof String) {
                value = ((String) value).replaceAll("\"", "\\\\\"");
            }
            escapedValues[i] = value;
        }
        return escapedValues;
    }

    /* package private */ String createMessage(
            final ILoggingEvent event,
            @Nullable final String eventName,
            @Nullable final String[] keys,
            @Nullable final Object[] values) {

        final String formatString = buildFormatString(eventName, keys);
        final LoggingEventWrapper eventWrapper = new LoggingEventWrapper(event, formatString, values);
        return layout.doLayout(eventWrapper);
    }

    /* package private */ Map<String, Object> createSafeContext(final ILoggingEvent event) {
        return createSafeContext(event, Collections.emptyList(), Collections.emptyList());
    }

    /* package private */ Map<String, Object> createSafeContext(
            final ILoggingEvent event,
            @Nullable final List<String> contextKeys,
            @Nullable final List<Object> contextValues) {
        return KeyValueSerializationHelper.createContext(this, event, contextKeys, contextValues);
    }

    private String _logEventName = STANDARD_LOG_EVENT_NAME;
    private static final String STANDARD_LOG_EVENT_NAME = "log";
}


/*
 * Copyright 2014 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.LoggerContextVO;
import ch.qos.logback.classic.spi.LoggingEvent;
import org.slf4j.Marker;
import org.slf4j.helpers.MessageFormatter;

import java.util.Arrays;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Logback LoggingEvent wrapper for overriding the message and argumentArray.
 *
 * @author Gil Markham (gil at groupon dot com)
 * @since 1.0.0
 */
public class LoggingEventWrapper extends LoggingEvent {

    /**
     * Public constructor.
     *
     * @param event Instance of ILoggingEvent.
     * @param message The message.
     * @param argumentArray The array of arguments for the message.
     */
    public LoggingEventWrapper(
            final ILoggingEvent event,
            final String message,
            @Nullable final Object[] argumentArray) {
        _wrappedEvent = event;
        _message = message;
        _argumentArray = argumentArray == null ? null : Arrays.copyOf(argumentArray, argumentArray.length);
    }

    @Override
    public String getThreadName() {
        return _wrappedEvent.getThreadName();
    }

    @Override
    public Level getLevel() {
        return _wrappedEvent.getLevel();
    }

    @Override
    public String getMessage() {
        return _message;
    }

    @Override
    public @Nullable Object[] getArgumentArray() {
        return _argumentArray == null ? null : Arrays.copyOf(_argumentArray, _argumentArray.length);
    }

    @Override
    public String getLoggerName() {
        return _wrappedEvent.getLoggerName();
    }

    @Override
    public LoggerContextVO getLoggerContextVO() {
        return _wrappedEvent.getLoggerContextVO();
    }

    @Override
    public IThrowableProxy getThrowableProxy() {
        return _wrappedEvent.getThrowableProxy();
    }

    @Override
    public StackTraceElement[] getCallerData() {
        return _wrappedEvent.getCallerData();
    }

    @Override
    public boolean hasCallerData() {
        return _wrappedEvent.hasCallerData();
    }

    @Override
    public Marker getMarker() {
        return _wrappedEvent.getMarker();
    }

    @Override
    public Map<String, String> getMDCPropertyMap() {
        return _wrappedEvent.getMDCPropertyMap();
    }

    @Override
    @SuppressWarnings("deprecation")
    public Map<String, String> getMdc() {
        return _wrappedEvent.getMdc();
    }

    @Override
    public long getTimeStamp() {
        return _wrappedEvent.getTimeStamp();
    }

    @Override
    public void prepareForDeferredProcessing() {
        _wrappedEvent.prepareForDeferredProcessing();
    }

    /**
     * Return the the message formatted with arguments. Implemented as suggested in:
     *
     * http://jira.qos.ch/browse/LBCLASSIC-47
     *
     * @return The formatted message.
     */
    public String getFormattedMessage() {
        if (_formattedMessage != null) {
            return _formattedMessage;
        }
        if (_argumentArray != null) {
            _formattedMessage = MessageFormatter.arrayFormat(_message, _argumentArray).getMessage();
        } else {
            _formattedMessage = _message;
        }

        return _formattedMessage;
    }

    private final ILoggingEvent _wrappedEvent;
    private final String _message;
    @Nullable private final Object[] _argumentArray;
    private transient String _formattedMessage;
}


/*
 * Copyright 2014 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback.jackson;

import com.arpnetworking.logback.annotations.LogRedact;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;

import java.util.Collections;
import java.util.Set;

/**
 * Jackson property filter that replaces values of fields annotated with @LogRedact with a string value of
 * "<REDACTED>".  If constructed with 'allowNull' as true then fields whose value is null will be output
 * as 'null' rather than a filtered string.
 *
 * @author Gil Markham (gil at groupon dot com)
 * @since 1.1.0
 */
public class RedactionFilter extends SimpleBeanPropertyFilter.SerializeExceptFilter {

    /**
     * Public constructor.
     *
     * @param allowNull Whether to allow null field values to pass as-is even
     * if those fields are annotated as redacted.
     */
    public RedactionFilter(final boolean allowNull) {
        this(allowNull, Collections.<String>emptySet());
    }

    /**
     * Public constructor.
     *
     * @param allowNull Whether to allow null field values to pass as-is even
     * if those fields are annotated as redacted.
     * @param properties Additional properties for SimpleBeanPropertyFilter.
     */
    public RedactionFilter(final boolean allowNull, final Set<String> properties) {
        super(properties);
        _allowNull = allowNull;
    }

    /**
     * {@inheritDoc}
     *
     * @deprecated Provided for compatibility with deprecated method in Jackson.
     */
    @Override
    @Deprecated
    public void serializeAsField(
            final Object pojo,
            final JsonGenerator jgen,
            final SerializerProvider prov,
            final BeanPropertyWriter writer) throws Exception {
        if (writer.getAnnotation(LogRedact.class) == null) {
            super.serializeAsField(pojo, jgen, prov, writer);
        } else { // since 2.3
            if (_allowNull && writer.get(pojo) == null) {
                super.serializeAsField(pojo, jgen, prov, writer);
            } else {
                jgen.writeStringField(writer.getSerializedName().getValue(), REDACTION_STRING);
            }
        }
    }

    @Override
    public void serializeAsField(
            final Object pojo,
            final JsonGenerator jgen,
            final SerializerProvider prov,
            final PropertyWriter writer) throws Exception {
        if (writer instanceof BeanPropertyWriter) {
            final BeanPropertyWriter beanPropertyWriter = (BeanPropertyWriter) writer;
            if (beanPropertyWriter.getAnnotation(LogRedact.class) == null) {
                super.serializeAsField(pojo, jgen, prov, writer);
            } else {
                if (_allowNull && beanPropertyWriter.get(pojo) == null) {
                    super.serializeAsField(pojo, jgen, prov, writer);
                } else {
                    jgen.writeStringField(beanPropertyWriter.getSerializedName().getValue(), REDACTION_STRING);
                }
            }
        } else {
            super.serializeAsField(pojo, jgen, prov, writer);
        }
    }

    private final boolean _allowNull;

    private static final long serialVersionUID = -3087655661573890897L;

    /**
     * The identifier of the RedactionFilter filter.
     */
    public static final String REDACTION_FILTER_ID = "com.arpnetworking.logback.jackson.RedactionFilterId";

    /**
     * The String to replace field values annotated as redacted.
     */
    public static final String REDACTION_STRING = "<REDACTED>";
}


/*
 * Copyright 2015 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback.serialization.steno;

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.arpnetworking.logback.StenoEncoder;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.Serializable;
import java.io.StringWriter;

/**
 * Serialization strategy for JSON object based message specifications.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @since 1.3.1
 */
public class ObjectAsJsonSerialziationStrategy implements Serializable {

    /**
     * Public constructor.
     *
     * @param encoder Instance of StenoEncoder.
     * @param jsonFactory Instance of JsonFactory.
     * @param objectMapper Instance of ObjectMapper.
     */
    public ObjectAsJsonSerialziationStrategy(
            final StenoEncoder encoder,
            final JsonFactory jsonFactory,
            final ObjectMapper objectMapper) {
        _encoder = encoder;
        _jsonFactory = jsonFactory;
        _objectMapper = objectMapper;
    }

    /**
     * Serialize an event.
     *
     * @param event The event.
     * @param eventName The event name.
     * @param jsonData The message data as serialized JSON.
     * @return Serialization of message as a String.
     * @throws Exception Serialization may throw any Exception.
     */
    public String serialize(
            final ILoggingEvent event,
            final String eventName,
            final String jsonData)
            throws Exception {

        final StringWriter jsonWriter = new StringWriter();
        final JsonGenerator jsonGenerator = _jsonFactory.createGenerator(jsonWriter);

        // Start wrapper
        StenoSerializationHelper.startStenoWrapper(event, eventName, jsonGenerator, _objectMapper);

        // Write event data
        jsonGenerator.writeFieldName("data");
        if (jsonData == null) {
            jsonGenerator.writeStartObject();
            jsonGenerator.writeEndObject();
        } else {
            jsonGenerator.writeRawValue(jsonData);
        }
        // TODO(vkoskela): Support writing null objects as-is via configuration [ISSUE-4]
        // e.g. "data":null -- although this is not supported by the current Steno specification

        // Output throwable
        StenoSerializationHelper.writeThrowable(event.getThrowableProxy(), jsonGenerator, _objectMapper);

        // End wrapper
        StenoSerializationHelper.endStenoWrapper(event, eventName, jsonGenerator, _objectMapper, _encoder);

        return jsonWriter.toString();
    }

    private final StenoEncoder _encoder;
    private final JsonFactory _jsonFactory;
    private final ObjectMapper _objectMapper;

    private static final long serialVersionUID = -9117316012859601728L;
}


/*
 * Copyright 2016 Ville Koskela
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback;

import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;

/**
 * Abstract Steno caller converter.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @since 1.14.0
 */
public abstract class AbstractStenoCallerConverter extends ClassicConverter {

    /**
     * Retrieve the relevant caller data adjusted for Steno logger wrapping.
     *
     * @param loggingEvent The ILoggingEvent instance.
     * @return The relevant caller data adjusted for Steno logger wrapping.
     */
    protected StackTraceElement getCallerData(final ILoggingEvent loggingEvent) {
        final StackTraceElement[] callerData = loggingEvent.getCallerData();
        if (callerData != null) {
            for (int i = 0; i < callerData.length; ++i) {
                final String callerClassName = callerData[i].getClassName();
                if (!callerClassName.startsWith(STENO_CLASS_NAME_PREFIX)) {
                    return callerData[i];
                }
            }
        }
        return null;
    }

    private static final String STENO_CLASS_NAME_PREFIX = "com.arpnetworking.steno.";
}


/*
 * Copyright 2015 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback.serialization.steno;

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.arpnetworking.logback.StenoEncoder;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.Serializable;
import java.io.StringWriter;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Serialization strategy for lists based message specifications.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @since 1.3.1
 */
public class ListsSerialziationStrategy implements Serializable {

    /**
     * Public constructor.
     *
     * @param encoder Instance of StenoEncoder.
     * @param jsonFactory Instance of JsonFactory.
     * @param objectMapper Instance of ObjectMapper.
     */
    public ListsSerialziationStrategy(
            final StenoEncoder encoder,
            final JsonFactory jsonFactory,
            final ObjectMapper objectMapper) {
        _encoder = encoder;
        _jsonFactory = jsonFactory;
        _objectMapper = objectMapper;
    }

    /**
     * Serialize an event.
     *
     * @param event The event.
     * @param eventName The event name.
     * @param dataKeys The message data keys.
     * @param dataValues The message data values.
     * @param contextKeys The message context keys.
     * @param contextValues The message context values.
     * @return Serialization of message as a String.
     * @throws Exception Serialization may throw any Exception.
     */
    public String serialize(
            final ILoggingEvent event,
            final String eventName,
            @Nullable final List<String> dataKeys,
            @Nullable final List<Object> dataValues,
            @Nullable final List<String> contextKeys,
            @Nullable final List<Object> contextValues)
            throws Exception {

        final StringWriter jsonWriter = new StringWriter();
        final JsonGenerator jsonGenerator = _jsonFactory.createGenerator(jsonWriter);

        // Start wrapper
        StenoSerializationHelper.startStenoWrapper(event, eventName, jsonGenerator, _objectMapper);

        // Write event data
        jsonGenerator.writeObjectFieldStart("data");
        StenoSerializationHelper.writeKeyValuePairs(dataKeys, dataValues, jsonGenerator, _objectMapper, _encoder);
        jsonGenerator.writeEndObject(); // End 'data' field

        // Output throwable
        StenoSerializationHelper.writeThrowable(event.getThrowableProxy(), jsonGenerator, _objectMapper);

        // End wrapper
        StenoSerializationHelper.endStenoWrapper(event, eventName, contextKeys, contextValues, jsonGenerator, _objectMapper, _encoder);

        return jsonWriter.toString();
    }

    private final StenoEncoder _encoder;
    private final JsonFactory _jsonFactory;
    private final ObjectMapper _objectMapper;

    private static final long serialVersionUID = 5800980890975405162L;
}


/*
 * Copyright 2015 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback.serialization.steno;

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.arpnetworking.logback.StenoEncoder;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.Serializable;
import java.io.StringWriter;
import javax.annotation.Nullable;

/**
 * Serialization strategy for array based message specifications.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @since 1.3.1
 */
public class ArraySerialziationStrategy implements Serializable {

    /**
     * Public constructor.
     *
     * @param encoder Instance of StenoEncoder.
     * @param jsonFactory Instance of JsonFactory.
     * @param objectMapper Instance of ObjectMapper.
     */
    public ArraySerialziationStrategy(
            final StenoEncoder encoder,
            final JsonFactory jsonFactory,
            final ObjectMapper objectMapper) {
        _encoder = encoder;
        _jsonFactory = jsonFactory;
        _objectMapper = objectMapper;
    }

    /**
     * Serialize an event.
     *
     * @param event The event.
     * @param eventName The event name.
     * @param keys The message keys.
     * @param values The message values.
     * @return Serialization of message as a String.
     * @throws Exception Serialization may throw any Exception.
     */
    public String serialize(
            final ILoggingEvent event,
            final String eventName,
            @Nullable final String[] keys,
            @Nullable final Object[] values)
            throws Exception {

        final StringWriter jsonWriter = new StringWriter();
        final JsonGenerator jsonGenerator = _jsonFactory.createGenerator(jsonWriter);

        // Start wrapper
        StenoSerializationHelper.startStenoWrapper(event, eventName, jsonGenerator, _objectMapper);

        // Write event data
        jsonGenerator.writeObjectFieldStart("data");
        final int argsLength = values == null ? 0 : values.length;
        if (keys != null) {
            for (int i = 0; i < keys.length; i++) {
                if (i >= argsLength) {
                    jsonGenerator.writeObjectField(keys[i], null);
                } else if (StenoSerializationHelper.isSimpleType(values[i])) {
                    jsonGenerator.writeObjectField(keys[i], values[i]);
                } else {
                    jsonGenerator.writeFieldName(keys[i]);
                    _objectMapper.writeValue(
                            jsonGenerator,
                            values[i]);
                }
            }
        }
        jsonGenerator.writeEndObject(); // End 'data' field

        // Output throwable
        StenoSerializationHelper.writeThrowable(event.getThrowableProxy(), jsonGenerator, _objectMapper);

        // End wrapper
        StenoSerializationHelper.endStenoWrapper(event, eventName, jsonGenerator, _objectMapper, _encoder);

        return jsonWriter.toString();
    }

    private final StenoEncoder _encoder;
    private final JsonFactory _jsonFactory;
    private final ObjectMapper _objectMapper;

    private static final long serialVersionUID = 309784279717760584L;
}


/*
 * Copyright 2015 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.arpnetworking.logback.serialization.steno;

import ch.qos.logback.classic.spi.ILoggingEvent;
import com.arpnetworking.logback.StenoEncoder;
import com.arpnetworking.steno.LogReferenceOnly;
import com.arpnetworking.steno.LogValueMapFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.FloatNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.TextNode;

import java.lang.reflect.Array;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;

/**
 * Helper functions and for safe serialziation.
 *
 * @author Ville Koskela (ville dot koskela at inscopemetrics dot com)
 * @since 1.11.2
 */
public final class SafeSerializationHelper {

    /**
     * Create a serialization safe context based on the StenoEncoder configuration.
     *
     * @since 1.11.2
     * @param encoder The StenoEncoder instance.
     * @param event The ILoggingEvent instance.
     * @param objectMapper ObjectMapper instance.
     * @return Map with event context.
     */
    public static Map<String, Object> createSafeContext(
            final StenoEncoder encoder,
            final ILoggingEvent event,
            final ObjectMapper objectMapper) {
        return createSafeContext(encoder, event, objectMapper, Collections.emptyList(), Collections.emptyList());
    }

    /**
     * Create a serialization safe context based on the StenoEncoder configuration.
     *
     * @since 1.11.2
     * @param encoder The StenoEncoder instance.
     * @param event The ILoggingEvent instance.
     * @param objectMapper ObjectMapper instance.
     * @param contextKeys The additional user provided context keys.
     * @param contextValues The additional user provided context values matching the keys.
     * @return Map with event context.
     */
    public static Map<String, Object> createSafeContext(
            final StenoEncoder encoder,
            final ILoggingEvent event,
            final ObjectMapper objectMapper,
            @Nullable final List<String> contextKeys,
            @Nullable final List<Object> contextValues) {
        return StenoSerializationHelper.createContext(encoder, event, objectMapper, contextKeys, contextValues);
    }

    /**
     * Safely serialize a value.
     *
     * @since 1.11.2
     * @param encoder The StenoEncoder instance.
     * @param value The Object instance to safely serialize.
     */
    public static void safeEncodeValue(final StringBuilder encoder, @Nullable final Object value) {
        if (value == null) {
            encoder.append("null");
        } else if (value instanceof Map) {
            safeEncodeMap(encoder, (Map<?, ?>) value);
        } else if (value instanceof List) {
            safeEncodeList(encoder, (List<?>) value);
        } else if (value.getClass().isArray()) {
            safeEncodeArray(encoder, value);
        } else if (value instanceof LogValueMapFactory.LogValueMap) {
            safeEncodeLogValueMap(encoder, (LogValueMapFactory.LogValueMap) value);
        } else if (value instanceof Throwable) {
            safeEncodeThrowable(encoder, (Throwable) value);
        } else if (StenoSerializationHelper.isSimpleType(value)) {
            if (value instanceof Boolean) {
                encoder.append(BooleanNode.valueOf((Boolean) value).toString());
            } else if (value instanceof Double) {
                encoder.append(DoubleNode.valueOf((Double) value).toString());
            } else if (value instanceof Float) {
                encoder.append(FloatNode.valueOf((Float) value).toString());
            } else if (value instanceof Long) {
                encoder.append(LongNode.valueOf((Long) value).toString());
            } else if (value instanceof Integer) {
                encoder.append(IntNode.valueOf((Integer) value).toString());
            } else {
                encoder.append(new TextNode(value.toString()).toString());
            }
        } else {
            safeEncodeValue(encoder, LogReferenceOnly.of(value).toLogValue());
        }
    }

    /* package private */ static void safeEncodeThrowable(final StringBuilder encoder, final Throwable throwable) {
        encoder.append("{\"type\":\"")
                .append(throwable.getClass().getName())
                .append("\",\"message\":");
        safeEncodeValue(encoder, throwable.getMessage());
        encoder.append(",\"backtrace\":[");
        for (final StackTraceElement ste : throwable.getStackTrace()) {
            safeEncodeValue(encoder, ste.toString());
            encoder.append(",");
        }
        if (throwable.getStackTrace().length == 0) {
            encoder.append("]");
        } else {
            encoder.setCharAt(encoder.length() - 1, ']');
        }
        encoder.append(",\"data\":{");
        if (throwable.getSuppressed().length > 0) {
            encoder.append("\"suppressed\":[");
            for (final Throwable suppressed : throwable.getSuppressed()) {
                safeEncodeThrowable(encoder, suppressed);
                encoder.append(",");
            }
            encoder.setCharAt(encoder.length() - 1, ']');
            encoder.append(",");
        }
        if (throwable.getCause() != null) {
            encoder.append("\"cause\":");
            safeEncodeThrowable(encoder, throwable.getCause());
            encoder.append(",");
        }
        if (encoder.charAt(encoder.length() - 1) == ',') {
            encoder.setCharAt(encoder.length() - 1, '}');
        } else {
            encoder.append("}");
        }
        encoder.append("}");
    }

    /* package private */ static void safeEncodeMap(final StringBuilder encoder, final Map<?, ?> valueAsMap) {
        encoder.append("{");
        for (Map.Entry<?, ?> entry : valueAsMap.entrySet()) {
            encoder.append("\"")
                    .append(entry.getKey().toString())
                    .append("\":");
            safeEncodeValue(encoder, entry.getValue());
            encoder.append(",");
        }
        if (valueAsMap.isEmpty()) {
            encoder.append("}");
        } else {
            encoder.setCharAt(encoder.length() - 1, '}');
        }
    }

    /* package private */ static void safeEncodeList(final StringBuilder encoder, final List<?> valueAsList) {
        encoder.append("[");
        for (Object listValue : valueAsList) {
            safeEncodeValue(encoder, listValue);
            encoder.append(",");
        }
        if (valueAsList.isEmpty()) {
            encoder.append("]");
        } else {
            encoder.setCharAt(encoder.length() - 1, ']');
        }
    }

    /* package private */ static void safeEncodeArray(final StringBuilder encoder, final Object value) {
        encoder.append("[");
        for (int i = 0; i < Array.getLength(value); ++i) {
            safeEncodeValue(encoder, Array.get(value, i));
            encoder.append(",");
        }
        if (Array.getLength(value) == 0) {
            encoder.append("]");
        } else {
            encoder.setCharAt(encoder.length() - 1, ']');
        }
    }

    /* package private */ static void safeEncodeLogValueMap(
            final StringBuilder encoder,
            final LogValueMapFactory.LogValueMap logValueMap) {
        final Map<String, Object> safeLogValueMap = new LinkedHashMap<>();
        final Optional<Object> target = logValueMap.getTarget();
        safeLogValueMap.put("_id", target.isPresent() ? Integer.toHexString(System.identityHashCode(target.get())) : null);
        safeLogValueMap.put("_class", target.isPresent() ? target.get().getClass().getName() : null);
        safeEncodeValue(encoder, safeLogValueMap);
    }

    private SafeSerializationHelper() {}
}