/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.html;

import java.util.HashMap;
import java.util.Map;

import ch.qos.logback.core.Context;
import ch.qos.logback.core.CoreConstants;
import static ch.qos.logback.core.CoreConstants.LINE_SEPARATOR;
import ch.qos.logback.core.LayoutBase;
import ch.qos.logback.core.pattern.Converter;
import ch.qos.logback.core.pattern.ConverterUtil;
import ch.qos.logback.core.pattern.parser.Node;
import ch.qos.logback.core.pattern.parser.Parser;
import ch.qos.logback.core.spi.ScanException;

/**
 * This class is a base class for HTMLLayout classes part of
 * other logback modules such as logback-classic and logback-access.
 *
 *
 * @author Sébastien Pennec
 */
public abstract class HTMLLayoutBase<E> extends LayoutBase<E> {

  protected String pattern;

  protected Converter<E> head;

  protected String title = "Logback Log Messages";

  //It is the responsibility of derived classes to set
  //this variable in their constructor to a default value.
  protected CssBuilder cssBuilder;

   // counter keeping track of the rows output
  protected long counter = 0;

  /**
   * Set the ConversionPattern option. This is the string which controls
   * formatting and consists of a mix of literal content and conversion
   * specifiers.
   * @param conversionPattern the desired conversion pattern
   */
  public void setPattern(String conversionPattern) {
    pattern = conversionPattern;
  }

  /**
   * Returns the value of the ConversionPattern  option.
   * @return the conversion pattern
   */
  public String getPattern() {
    return pattern;
  }

  public CssBuilder getCssBuilder() {
    return cssBuilder;
  }

  public void setCssBuilder(CssBuilder cssBuilder) {
    this.cssBuilder = cssBuilder;
  }

  /**
   * Parses the pattern and creates the Converter linked list.
   */
  @Override
  public void start() {
    int errorCount = 0;

    try {
      Parser<E> p = new Parser<E>(pattern);
      p.setContext(getContext());
      Node t = p.parse();
      this.head = p.compile(t, getEffectiveConverterMap());
      ConverterUtil.startConverters(this.head);
    } catch (ScanException ex) {
      addError("Incorrect pattern found", ex);
      errorCount++;
    }

    if (errorCount == 0) {
      super.started = true;
    }
  }

  protected abstract Map<String, String> getDefaultConverterMap();

  /**
   * Returns a map where the default converter map is merged with the map
   * contained in the context.
   * @return the converter map
   */
  public Map<String, String> getEffectiveConverterMap() {
    Map<String, String> effectiveMap = new HashMap<String, String>();

    // add the least specific map fist
    Map<String, String> defaultMap = getDefaultConverterMap();
    if (defaultMap != null) {
      effectiveMap.putAll(defaultMap);
    }

    // contextMap is more specific than the default map
    Context context = getContext();
    if (context != null) {
      @SuppressWarnings("unchecked")
      Map<String, String> contextMap = (Map<String, String>) context
          .getObject(CoreConstants.PATTERN_RULE_REGISTRY);
      if (contextMap != null) {
        effectiveMap.putAll(contextMap);
      }
    }
    return effectiveMap;
  }

  /**
   * The Title option takes a String value. This option sets the
   * document title of the generated HTML document.
   *
   *

   * Defaults to 'Logback Log Messages'.
   * @param title desired document title
   */
  public void setTitle(String title) {
    this.title = title;
  }

  /**
   * Returns the current value of the Title  option.
   * @return the page/document title
   */
  public String getTitle() {
    return title;
  }

  /**
   * Returns the content type output by this layout, i.e "text/html".
   * @return the content type
   */
  @Override
  public String getContentType() {
    return "text/html";
  }

  /**
   * Returns appropriate HTML headers.
   * @return the headers
   */
  @Override
  public String getFileHeader() {
    StringBuilder sbuf = new StringBuilder();
    sbuf.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"");
    sbuf.append(" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");
    sbuf.append(LINE_SEPARATOR);
    sbuf.append("");
    sbuf.append(LINE_SEPARATOR);
    sbuf.append("  ");
    sbuf.append(LINE_SEPARATOR);
    sbuf.append("    <title>");
    sbuf.append(title);
    sbuf.append("</title>");
    sbuf.append(LINE_SEPARATOR);

    cssBuilder.addCss(sbuf);

    sbuf.append(LINE_SEPARATOR);
    sbuf.append("  ");
    sbuf.append(LINE_SEPARATOR);
    sbuf.append("");
    sbuf.append(LINE_SEPARATOR);

    return sbuf.toString();
  }

  public String getPresentationHeader() {
    StringBuilder sbuf = new StringBuilder();
    sbuf.append("
");
    sbuf.append(LINE_SEPARATOR);
    sbuf.append("
Log session start time ");
    sbuf.append(new java.util.Date());
    sbuf.append("

");
    sbuf.append(LINE_SEPARATOR);
    sbuf.append(LINE_SEPARATOR);
    sbuf.append("
");
    sbuf.append(LINE_SEPARATOR);

    buildHeaderRowForTable(sbuf);

    return sbuf.toString();
  }

  private void buildHeaderRowForTable(StringBuilder sbuf) {
    Converter<E> c = head;
    String name;
    sbuf.append("
");
    sbuf.append(LINE_SEPARATOR);
    while (c != null) {
      name = computeConverterName(c);
      if (name == null) {
        c = c.getNext();
        continue;
      }
      sbuf.append("");
      sbuf.append(computeConverterName(c));
      sbuf.append("	");
      sbuf.append(LINE_SEPARATOR);
      c = c.getNext();
    }
    sbuf.append("
");
    sbuf.append(LINE_SEPARATOR);
  }

  public String getPresentationFooter() {
    StringBuilder sbuf = new StringBuilder();
    sbuf.append("
");
    return sbuf.toString();
  }

  /**
   * Returns the appropriate HTML footers.
   */
  @Override
  public String getFileFooter() {
    StringBuilder sbuf = new StringBuilder();
    sbuf.append(LINE_SEPARATOR);
    sbuf.append("");
    return sbuf.toString();
  }

  protected void startNewTableIfLimitReached(StringBuilder sbuf) {
    if (this.counter >= CoreConstants.TABLE_ROW_LIMIT) {
      counter = 0;
      sbuf.append("
");
      sbuf.append(LINE_SEPARATOR);
      sbuf.append("

");
      sbuf.append("
");
      sbuf.append(LINE_SEPARATOR);
      buildHeaderRowForTable(sbuf);
    }
  }

  protected String computeConverterName(Converter<E> c) {
    String className = c.getClass().getSimpleName();
    int index = className.indexOf("Converter");
    if (index == -1) {
      return className;
    } else {
      return className.substring(0, index);
    }
  }

}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.joran.action;

import java.util.HashMap;

import org.xml.sax.Attributes;

import ch.qos.logback.core.Appender;
import ch.qos.logback.core.joran.spi.ActionException;
import ch.qos.logback.core.joran.spi.InterpretationContext;
import ch.qos.logback.core.spi.LifeCycle;
import ch.qos.logback.core.util.OptionHelper;

public class AppenderAction<E> extends Action {
  Appender<E> appender;
  private boolean inError = false;

  /**
   * Instantiates an appender of the given class and sets its name.
   *
   * The appender thus generated is placed in the {@link InterpretationContext}'s
   * appender bag.
   */
  @SuppressWarnings("unchecked")
  public void begin(InterpretationContext ec, String localName,
      Attributes attributes) throws ActionException {
    // We are just beginning, reset variables
    appender = null;
    inError = false;

    String className = attributes.getValue(CLASS_ATTRIBUTE);
    if (OptionHelper.isEmpty(className)) {
      addError("Missing class name for appender. Near [" + localName
          + "] line " + getLineNumber(ec));
      inError = true;
      return;
    }

    try {
      addInfo("About to instantiate appender of type [" + className + "]");
      warnDeprecated(className);

      appender = (Appender<E>) OptionHelper.instantiateByClassName(className,
          ch.qos.logback.core.Appender.class, context);

      appender.setContext(context);

      String appenderName = ec.subst(attributes.getValue(NAME_ATTRIBUTE));

      if (OptionHelper.isEmpty(appenderName)) {
        addWarn("No appender name given for appender of type " + className
            + "].");
      } else {
        appender.setName(appenderName);
        addInfo("Naming appender as [" + appenderName + "]");
      }

      // The execution context contains a bag which contains the appenders
      // created thus far.
      HashMap<String, Appender<E>> appenderBag = (HashMap<String, Appender<E>>) ec.getObjectMap().get(
          ActionConst.APPENDER_BAG);

      // add the appender just created to the appender bag.
      appenderBag.put(appenderName, appender);

      ec.pushObject(appender);
    } catch (Exception oops) {
      inError = true;
      addError("Could not create an Appender of type [" + className + "].",
          oops);
      throw new ActionException(oops);
    }
  }

  private void warnDeprecated(String className) {
    if (className.equals("ch.qos.logback.core.ConsoleAppender")) {
      addWarn("ConsoleAppender is deprecated for LogcatAppender");
    }
  }

  /**
   * Once the children elements are also parsed, now is the time to activate the
   * appender options.
   */
  public void end(InterpretationContext ec, String name) {
    if (inError) {
      return;
    }

    if (appender instanceof LifeCycle) {
      ((LifeCycle) appender).start();
    }

    Object o = ec.peekObject();

    if (o != appender) {
      addWarn("The object at the of the stack is not the appender named ["
          + appender.getName() + "] pushed earlier.");
    } else {
      ec.popObject();
    }
  }
}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.util;

/**
 * This class serves as a gateway for invocations of a "costly" operation on a critical execution path.
 *
 * @author Ceki Gülcü
 */
public class DefaultInvocationGate implements InvocationGate {

    static final int MASK_DECREASE_RIGHT_SHIFT_COUNT = 2;

    // experiments indicate that even for the most CPU intensive applications with 200 or more threads MASK
    // values in the order of 0xFFFF is appropriate
    private static final int MAX_MASK = 0xFFFF;
    static final int DEFAULT_MASK = 0xF;
    private volatile long mask = DEFAULT_MASK;
    //private volatile long lastMaskCheck = System.currentTimeMillis();

    // IMPORTANT: This field can be updated by multiple threads. It follows that
    // its values may *not* be incremented sequentially. However, we don't care
    // about the actual value of the field except that from time to time the
    // expression (invocationCounter++ & mask) == mask) should be true.
    private long invocationCounter = 0;

    // if less than thresholdForMaskIncrease milliseconds elapse between invocations of updateMaskIfNecessary()
    // method, then the mask should be increased
    private static final long MASK_INCREASE_THRESHOLD = 100;

    // if more than thresholdForMaskDecrease milliseconds elapse between invocations of updateMaskIfNecessary() method,
    // then the mask should be decreased
    private static final long MASK_DECREASE_THRESHOLD = MASK_INCREASE_THRESHOLD * 8;

    public DefaultInvocationGate() {
        this(MASK_INCREASE_THRESHOLD, MASK_DECREASE_THRESHOLD, System.currentTimeMillis());
    }

    public  DefaultInvocationGate(long minDelayThreshold, long maxDelayThreshold, long currentTime) {
        this.minDelayThreshold = minDelayThreshold;
        this.maxDelayThreshold = maxDelayThreshold;
        this.lowerLimitForMaskMatch = currentTime + minDelayThreshold;
        this.upperLimitForNoMaskMatch = currentTime + maxDelayThreshold;
    }

    private long minDelayThreshold;
    private long maxDelayThreshold;

    long lowerLimitForMaskMatch;
    long upperLimitForNoMaskMatch;

    /*
     * (non-Javadoc)
     *
     * @see ch.qos.logback.core.util.InvocationGate#skipFurtherWork()
     */
    @Override
    final public boolean isTooSoon(long currentTime) {
        boolean maskMatch = ((invocationCounter++) & mask) == mask;

        if (maskMatch) {
            if (currentTime < this.lowerLimitForMaskMatch) {
                increaseMask();
            }
            updateLimits(currentTime);
        } else {
            if (currentTime > this.upperLimitForNoMaskMatch) {
                decreaseMask();
                updateLimits(currentTime);
                return false;
            }
        }
        return !maskMatch;
    }

    private void updateLimits(long currentTime) {
        this.lowerLimitForMaskMatch = currentTime + minDelayThreshold;
        this.upperLimitForNoMaskMatch = currentTime + maxDelayThreshold;
    }

    // package private, for testing purposes only
    long getMask() {
        return mask;
    }

    private void increaseMask() {
        if (mask >= MAX_MASK)
            return;
        mask = (mask << 1) | 1;
    }

    private void decreaseMask() {
        mask = mask >>> MASK_DECREASE_RIGHT_SHIFT_COUNT;
    }

    public long getInvocationCounter() {
        return invocationCounter;
    }
}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.net.server;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import ch.qos.logback.core.spi.ContextAwareBase;

/**
 * A concurrent {@link ServerRunner}.
 *

 * An instance of this object is created with a {@link ServerListener} and
 * an {@link Executor}. On invocation of its {@link #run()} method by the
 * {@link Executor} it begins accepting client connections via its {@code ServerListener}.
 * As each new {@link Client} is accepted, the client is configured with the
 * runner's {@code ch.qos.logback.classic.LoggerContext} and is then passed
 * to the {@link Executor} for concurrent execution of the client's service loop.
 *

 * On invocation of the {@link #stop()} method, the runner closes the listener
 * and each of the connected clients (by invoking {@link Client#close()}
 * effectively interrupting any blocked I/O calls and causing these concurrent
 * subtasks to exit gracefully).  This ensures that before the {@link #stop()}
 * method returns (1) all I/O resources have been released and (2) all
 * of the threads of the {@code Executor} are idle.
 *
 * @author Carl Harris
 */
public abstract class ConcurrentServerRunner<T extends Client>
    extends ContextAwareBase
    implements Runnable, ServerRunner<T> {

  private final Lock clientsLock = new ReentrantLock();

  private final Collection<T> clients = new ArrayList<T>();

  private final ServerListener<T> listener;
  private final Executor executor;

  private boolean running;

  /**
   * Constructs a new server runner.
   * @param listener the listener from which the server will accept new
   *    clients
   * @param executor a executor that will facilitate execution of the
   *    listening and client-handling tasks; while any {@link Executor}
   *    is allowed here, outside of unit testing the only reasonable choice
   *    is a bounded thread pool of some kind.
   */
  public ConcurrentServerRunner(ServerListener<T> listener, Executor executor) {
    this.listener = listener;
    this.executor = executor;
  }

  /**
   * {@inheritDoc}
   */
  public boolean isRunning() {
    return running;
  }

  protected void setRunning(boolean running) {
    this.running = running;
  }

  /**
   * {@inheritDoc}
   */
  public void stop() throws IOException {
    listener.close();
    accept(new ClientVisitor<T>() {
      public void visit(T client) {
        client.close();
      }
    });
  }

  /**
   * {@inheritDoc}
   */
  public void accept(ClientVisitor<T> visitor) {
    Collection<T> clients = copyClients();
    for (T client : clients) {
      try {
        visitor.visit(client);
      }
      catch (RuntimeException ex) {
        addError(client + ": " + ex);
      }
    }
  }

  /**
   * Creates a copy of the collection of all clients that are presently
   * being tracked by the server.
   * @return collection of client objects
   */
  private Collection<T> copyClients() {
    clientsLock.lock();
    try {
      Collection<T> copy = new ArrayList<T>(clients);
      return copy;
    }
    finally {
      clientsLock.unlock();
    }
  }

  /**
   * {@inheritDoc}
   */
  public void run() {
    setRunning(true);
    try {
      addInfo("listening on " + listener);
      while (!Thread.currentThread().isInterrupted()) {
        T client = listener.acceptClient();
        if (!configureClient(client)) {
          addError(client + ": connection dropped");
          client.close();
          continue;
        }
        try {
          executor.execute(new ClientWrapper(client));
        }
        catch (RejectedExecutionException ex) {
          addError(client + ": connection dropped");
          client.close();
        }
      }
    }
    catch (InterruptedException ex) {
      assert true;  // ok... we'll shut down
    }
    catch (Exception ex) {
      addError("listener: " + ex);
    }

    setRunning(false);
    addInfo("shutting down");
    listener.close();
  }

  /**
   * Configures a connected client.
   *

   * A subclass implements this method to perform any necessary configuration
   * of the client object before its {@link Client#run()} method is invoked.
   *
   * @param client the subject client
   * @return {@code true} if configuration was successful; if the return
   *    value is {@code false} the client connection will be dropped
   */
  protected abstract boolean configureClient(T client);

  /**
   * Adds a client to the collection of those being tracked by the server.
   * @param client the client to add
   */
  private void addClient(T client) {
    clientsLock.lock();
    try {
      clients.add(client);
    }
    finally {
      clientsLock.unlock();
    }
  }

  /**
   * Removes a client from the collection of those being tracked by the server.
   * @param client the client to remote
   */
  private void removeClient(T client) {
    clientsLock.lock();
    try {
      clients.remove(client);
    }
    finally {
      clientsLock.unlock();
    }
  }

  /**
   * A wrapper for a {@link Client} responsible for ensuring that client
   * tracking is performed properly.
   */
  private class ClientWrapper implements Client {

    private final T delegate;

    public ClientWrapper(T client) {
      this.delegate = client;
    }

    public void run() {
      addClient(delegate);
      try {
        delegate.run();
      }
      finally {
        removeClient(delegate);
      }
    }

    public void close() {
      delegate.close();
    }
  }

}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.classic.util;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.slf4j.spi.MDCAdapter;

/**
 * A Mapped Diagnostic Context, or MDC in short, is an instrument for
 * distinguishing interleaved log output from different sources. Log output is
 * typically interleaved when a server handles multiple clients
 * near-simultaneously.
 *

 * The MDC is managed on a per thread basis. A child thread
 * automatically inherits a copy of the mapped diagnostic context of
 * its parent.
 *

 * For more information about MDC, please refer to the online manual at
 * http://logback.qos.ch/manual/mdc.html
 *
 * @author Ceki Gülcü
 */
public final class LogbackMDCAdapter implements MDCAdapter {

  // We wish to avoid unnecessarily copying of the map. To ensure
  // efficient/timely copying, we have a variable keeping track of the last
  // operation. A copy is necessary on 'put' or 'remove' but only if the last
  // operation was a 'get'. Get operations never necessitate a copy nor
  // successive 'put/remove' operations, only a get followed by a 'put/remove'
  // requires copying the map.
  // See http://jira.qos.ch/browse/LBCLASSIC-254 for the original discussion.

  // We no longer use CopyOnInheritThreadLocal in order to solve LBCLASSIC-183
  // Initially the contents of the thread local in parent and child threads
  // reference the same map. However, as soon as a thread invokes the put()
  // method, the maps diverge as they should.
  final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();

  private static final int WRITE_OPERATION = 1;
  private static final int MAP_COPY_OPERATION = 2;

  // keeps track of the last operation performed
  final ThreadLocal<Integer> lastOperation = new ThreadLocal<Integer>();

  private Integer getAndSetLastOperation(int op) {
    Integer lastOp = lastOperation.get();
    lastOperation.set(op);
    return lastOp;
  }

  private boolean wasLastOpReadOrNull(Integer lastOp) {
    return lastOp == null || lastOp.intValue() == MAP_COPY_OPERATION;
  }

  private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
    Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());
    if (oldMap != null) {
        // we don't want the parent thread modifying oldMap while we are
        // iterating over it
        synchronized (oldMap) {
          newMap.putAll(oldMap);
        }
    }

    copyOnThreadLocal.set(newMap);
    return newMap;
  }

  /**
   * Put a context value (the val parameter) as identified with the
   * key parameter into the current thread's context map. Note that
   * contrary to log4j, the val parameter can be null.
   *

   * If the current thread does not have a context map it is created as a side
   * effect of this call.
   *
   * @throws IllegalArgumentException in case the "key" parameter is null
   */
  public void put(String key, String val) throws IllegalArgumentException {
    if (key == null) {
      throw new IllegalArgumentException("key cannot be null");
    }

    Map<String, String> oldMap = copyOnThreadLocal.get();
    Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

    if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
      Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
      newMap.put(key, val);
    } else {
      oldMap.put(key, val);
    }
  }

  /**
   * Remove the the context identified by the key parameter.
   */
  public void remove(String key) {
    if (key == null) {
      return;
    }
    Map<String, String> oldMap = copyOnThreadLocal.get();
    if (oldMap == null) return;

    Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

    if (wasLastOpReadOrNull(lastOp)) {
      Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
      newMap.remove(key);
    } else {
      oldMap.remove(key);
    }
  }

  /**
   * Clear all entries in the MDC.
   */
  public void clear() {
    lastOperation.set(WRITE_OPERATION);
    copyOnThreadLocal.remove();
  }

  /**
   * Get the context identified by the key parameter.
   */
  public String get(String key) {
    final Map<String, String> map = copyOnThreadLocal.get();
    if ((map != null) && (key != null)) {
      return map.get(key);
    } else {
      return null;
    }
  }

  /**
   * Get the current thread's MDC as a map. This method is intended to be used
   * internally.
   */
  public Map<String, String> getPropertyMap() {
    lastOperation.set(MAP_COPY_OPERATION);
    return copyOnThreadLocal.get();
  }

  /**
   * Returns the keys in the MDC as a {@link Set}. The returned value can be
   * null.
   */
  public Set<String> getKeys() {
    Map<String, String> map = getPropertyMap();

    if (map != null) {
      return map.keySet();
    } else {
      return null;
    }
  }

  /**
   * Return a copy of the current thread's context map. Returned value may be
   * null.
   */
  public Map<String, String> getCopyOfContextMap() {
    Map<String, String> hashMap = copyOnThreadLocal.get();
    if (hashMap == null) {
      return null;
    } else {
      return new HashMap<String, String>(hashMap);
    }
  }

  public void setContextMap(Map<String, String> contextMap) {
    lastOperation.set(WRITE_OPERATION);

    Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());
    newMap.putAll(contextMap);

    // the newMap replaces the old one for serialisation's sake
    copyOnThreadLocal.set(newMap);
  }
}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.joran.util;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;

import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.spi.ContextAware;

/**
 * Utility class which can convert string into objects.
 * @author Ceki Gülcü
 *
 */
public class StringToObjectConverter {

  private static final Class<?>[] STING_CLASS_PARAMETER = new Class[] { String.class };

  static public boolean canBeBuiltFromSimpleString(Class<?> parameterClass) {
    Package p = parameterClass.getPackage();
    if (parameterClass.isPrimitive()) {
      return true;
    } else if (p != null && "java.lang".equals(p.getName())) {
      return true;
    } else if (followsTheValueOfConvention(parameterClass)) {
      return true;
    } else if (parameterClass.isEnum()) {
      return true;
    } else if (isOfTypeCharset(parameterClass)) {
      return true;
    }
    return false;
  }

  /**
   * Convert val a String parameter to an object of a given type.
   * @param ca context
   * @param val string representation of value
   * @param type target class to convert val into
   * @return the object created from the string
   */
  @SuppressWarnings("unchecked")
  public static Object convertArg(ContextAware ca, String val, Class<?> type) {
    if (val == null) {
      return null;
    }
    String v = val.trim();
    if (String.class.isAssignableFrom(type)) {
      return v;
    } else if (Integer.TYPE.isAssignableFrom(type)) {
      return Integer.valueOf(v);
    } else if (Long.TYPE.isAssignableFrom(type)) {
      return Long.valueOf(v);
    } else if (Float.TYPE.isAssignableFrom(type)) {
      return Float.valueOf(v);
    } else if (Double.TYPE.isAssignableFrom(type)) {
      return Double.valueOf(v);
    } else if (Boolean.TYPE.isAssignableFrom(type)) {
      if ("true".equalsIgnoreCase(v)) {
        return Boolean.TRUE;
      } else if ("false".equalsIgnoreCase(v)) {
        return Boolean.FALSE;
      }
    } else if (type.isEnum()) {
      return convertToEnum(ca, v, (Class<? extends Enum<?>>) type);
    } else if (StringToObjectConverter.followsTheValueOfConvention(type)) {
      return convertByValueOfMethod(ca, type, v);
    } else if (isOfTypeCharset(type)) {
      return convertToCharset(ca, val);
    }

    return null;
  }

  static private boolean isOfTypeCharset(Class<?> type) {
    return Charset.class.isAssignableFrom(type);
  }

  static private Charset convertToCharset(ContextAware ca, String val) {
    try {
      return Charset.forName(val);
    } catch (UnsupportedCharsetException e) {
      ca.addError("Failed to get charset [" + val + "]", e);
      return null;
    }
  }

  // returned value may be null and in most cases it is null.
  public static Method getValueOfMethod(Class<?> type) {
    try {
      return type.getMethod(CoreConstants.VALUE_OF, STING_CLASS_PARAMETER);
    } catch (NoSuchMethodException e) {
      return null;
    } catch (SecurityException e) {
      return null;
    }
  }

  static private boolean followsTheValueOfConvention(Class<?> parameterClass) {
    Method valueOfMethod = getValueOfMethod(parameterClass);
    if (valueOfMethod == null) {
      return false;
    }
    int mod = valueOfMethod.getModifiers();
    return Modifier.isStatic(mod);
  }

  private static Object convertByValueOfMethod(ContextAware ca, Class<?> type,
      String val) {
    try {
      Method valueOfMethod = type.getMethod(CoreConstants.VALUE_OF,
          STING_CLASS_PARAMETER);
      return valueOfMethod.invoke(null, val);
    } catch (Exception e) {
      ca.addError("Failed to invoke " + CoreConstants.VALUE_OF
          + "{} method in class [" + type.getName() + "] with value [" + val
          + "]");
      return null;
    }
  }

  @SuppressWarnings("unchecked")
  private static Object convertToEnum(ContextAware ca, String val,
      Class<? extends Enum> enumType) {
    return Enum.valueOf(enumType, val);
  }

  boolean isBuildableFromSimpleString() {
    return false;
  }
}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.classic.spi;

import java.util.concurrent.CopyOnWriteArrayList;

import org.slf4j.Marker;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.turbo.TurboFilter;
import ch.qos.logback.core.spi.FilterReply;

/**
 * Implementation of TurboFilterAttachable.
 *
 * @author Ceki Gülcü
 */
final public class TurboFilterList extends CopyOnWriteArrayList<TurboFilter> {

  private static final long serialVersionUID = 1L;

  /**
   * Loop through the filters in the chain. As soon as a filter decides on
   * ACCEPT or DENY, then that value is returned. If all of the filters return
   * NEUTRAL, then NEUTRAL is returned.
   */
  public FilterReply getTurboFilterChainDecision(final Marker marker,
      final Logger logger, final Level level, final String format,
      final Object[] params, final Throwable t) {

    final int size = size();
//    if (size == 0) {
//      return FilterReply.NEUTRAL;
//    }
    if (size == 1) {
      try {
        TurboFilter tf = get(0);
        return tf.decide(marker, logger, level, format, params, t);
      } catch (IndexOutOfBoundsException iobe) {
        return FilterReply.NEUTRAL;
      }
    }

    Object[] tfa = toArray();
    final int len = tfa.length;
    for (int i = 0; i < len; i++) {
    //for (TurboFilter tf : this) {
      final TurboFilter tf = (TurboFilter) tfa[i];
      final FilterReply r = tf.decide(marker, logger, level, format, params, t);
      if (r == FilterReply.DENY || r == FilterReply.ACCEPT) {
        return r;
      }
    }
    return FilterReply.NEUTRAL;
  }

  // public boolean remove(TurboFilter turboFilter) {
  // return tfList.remove(turboFilter);
  // }
  //
  // public TurboFilter remove(int index) {
  // return tfList.remove(index);
  // }
  //
  // final public int size() {
  // return tfList.size();
  // }

}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.classic.net.server;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import ch.qos.logback.core.net.server.ServerListener;
import ch.qos.logback.core.net.server.ServerSocketListener;

/**
 * A {@link ServerListener} for remote appenders.
 *
 * @author Carl Harris
 */
class RemoteAppenderServerListener
    extends ServerSocketListener<RemoteAppenderClient>  {

  /**
   * Constructs a new listener.
   * @param serverSocket the {@link ServerSocket} from which to accept
   *    new client connections
   */
  public RemoteAppenderServerListener(ServerSocket serverSocket) {
    super(serverSocket);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected RemoteAppenderClient createClient(String id, Socket socket)
      throws IOException {
    return new RemoteAppenderStreamClient(id, socket);
  }

}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.joran.spi;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import ch.qos.logback.core.Context;
import ch.qos.logback.core.joran.action.Action;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.util.OptionHelper;

/**
 * This class implements the {@link RuleStore} interface. It is the rule store
 * implementation used by default in Joran.
 *
 * @author Ceki Gülcü
 *
 */
public class SimpleRuleStore extends ContextAwareBase implements RuleStore {

  static String KLEENE_STAR = "*";

  // key: Pattern instance, value: ArrayList containing actions
  HashMap<ElementSelector, List<Action>> rules = new HashMap<ElementSelector, List<Action>>();

  // public SimpleRuleStore() {
  // }

  public SimpleRuleStore(Context context) {
    setContext(context);
  }

  /**
   * Add a new rule, i.e. a pattern, action pair to the rule store. <p> Note
   * that the added action's LoggerRepository will be set in the process.
   */
  public void addRule(ElementSelector elementSelector, Action action) {
    action.setContext(context);

    List<Action> a4p = rules.get(elementSelector);

    if (a4p == null) {
      a4p = new ArrayList<Action>();
      rules.put(elementSelector, a4p);
    }

    a4p.add(action);
  }

  public void addRule(ElementSelector elementSelector, String actionClassName) {
    Action action = null;

    try {
      action = (Action) OptionHelper.instantiateByClassName(actionClassName,
          Action.class, context);
    } catch (Exception e) {
      addError("Could not instantiate class [" + actionClassName + "]", e);
    }
    if (action != null) {
      addRule(elementSelector, action);
    }
  }

  // exact match has highest priority
  // if no exact match, check for suffix (tail) match, i.e matches
  // of type */x/y. Suffix match for */x/y has higher priority than match for
  // */x
  // if no suffix match, check for prefix match, i.e. matches for x/*
  // match for x/y/* has higher priority than matches for x/*

  public List<Action> matchActions(ElementPath elementPath) {
    List<Action> actionList;

    if ((actionList = fullPathMatch(elementPath)) != null) {
      return actionList;
    } else if ((actionList = suffixMatch(elementPath)) != null) {
      return actionList;
    } else if ((actionList = prefixMatch(elementPath)) != null) {
      return actionList;
    } else if ((actionList = middleMatch(elementPath)) != null) {
      return actionList;
    } else {
      return null;
    }
  }

  List<Action> fullPathMatch(ElementPath elementPath) {
    for (ElementSelector selector : rules.keySet()) {
       if(selector.fullPathMatch(elementPath))
         return rules.get(selector);
    }
    return null;
  }

  // Suffix matches are matches of type */x/y
  List<Action> suffixMatch(ElementPath elementPath) {
    int max = 0;
    ElementSelector longestMatchingElementSelector = null;

    for (ElementSelector selector : rules.keySet()) {
      if (isSuffixPattern(selector)) {
        int r = selector.getTailMatchLength(elementPath);
        if (r > max) {
          max = r;
          longestMatchingElementSelector = selector;
        }
      }
    }

    if (longestMatchingElementSelector != null) {
      return rules.get(longestMatchingElementSelector);
    } else {
      return null;
    }
  }

  private boolean isSuffixPattern(ElementSelector p) {
    return (p.size() > 1) && p.get(0).equals(KLEENE_STAR);
  }

  List<Action> prefixMatch(ElementPath elementPath) {
    int max = 0;
    ElementSelector longestMatchingElementSelector = null;

    for (ElementSelector selector : rules.keySet()) {
      String last = selector.peekLast();
      if (isKleeneStar(last)) {
        int r = selector.getPrefixMatchLength(elementPath);
        // to qualify the match length must equal p's size omitting the '*'
        if ((r == selector.size() - 1) && (r > max)) {
          max = r;
          longestMatchingElementSelector = selector;
        }
      }
    }

    if (longestMatchingElementSelector != null) {
      return rules.get(longestMatchingElementSelector);
    } else {
      return null;
    }
  }

  private boolean isKleeneStar(String last) {
    return KLEENE_STAR.equals(last);
  }

  List<Action> middleMatch(ElementPath path) {

    int max = 0;
    ElementSelector longestMatchingElementSelector = null;

    for (ElementSelector selector : rules.keySet()) {
      String last = selector.peekLast();
      String first = null;
      if(selector.size() > 1) {
        first = selector.get(0);
      }
      if (isKleeneStar(last) && isKleeneStar(first)) {
        List<String> copyOfPartList = selector.getCopyOfPartList();
        if(copyOfPartList.size() > 2) {
          copyOfPartList.remove(0);
          copyOfPartList.remove(copyOfPartList.size()-1);
        }

        int r = 0;
        ElementSelector clone = new ElementSelector(copyOfPartList);
        if(clone.isContainedIn(path)) {
          r = clone.size();
        }
        if (r > max) {
          max = r;
          longestMatchingElementSelector = selector;
        }
      }
    }

    if (longestMatchingElementSelector != null) {
      return rules.get(longestMatchingElementSelector);
    } else {
      return null;
    }
  }

  public String toString() {
    final String TAB = "  ";

    StringBuilder retValue = new StringBuilder();

    retValue.append("SimpleRuleStore ( ").append("rules = ").append(this.rules)
        .append(TAB).append(" )");

    return retValue.toString();
  }

}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.util;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author Ceki Gücü
 */
public class EnvUtil {

  private static final Pattern versionPattern = Pattern.compile("^(1\\.)?([0-9]+)");

  private EnvUtil() {}

  /**
   * Heuristically determines whether the current OS is Android
   */
  static public boolean isAndroidOS() {
    String osname = OptionHelper.getSystemProperty("os.name");
    String root = OptionHelper.getEnv("ANDROID_ROOT");
    String data = OptionHelper.getEnv("ANDROID_DATA");

    return osname != null && osname.contains("Linux") &&
        root != null && root.contains("/system") &&
        data != null && data.contains("/data");
  }

  static private boolean isJDK_N_OrHigher(int n) {
    Matcher matcher = versionPattern.matcher(System.getProperty("java.version", ""));
    if (matcher.find()) {
      return n <= Integer.parseInt(matcher.group(2));
    }
    return false;
  }

  static public boolean isJDK5() {
    return isJDK_N_OrHigher(5);
  }

  static public boolean isJDK7OrHigher() {
    return isJDK_N_OrHigher(7);
  }
}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 org.slf4j.impl;

import ch.qos.logback.core.status.StatusUtil;
import org.slf4j.ILoggerFactory;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.Util;
import org.slf4j.spi.LoggerFactoryBinder;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.util.ContextInitializer;
import ch.qos.logback.classic.util.ContextSelectorStaticBinder;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;

/**
 *
 * The binding of {@link LoggerFactory} class with an actual instance of
 * {@link ILoggerFactory} is performed using information returned by this class.
 *
 * @author Ceki Gülcü
 */
public class StaticLoggerBinder implements LoggerFactoryBinder {

  /**
   * Declare the version of the SLF4J API this implementation is compiled
   * against. The value of this field is usually modified with each release.
   */
  // to avoid constant folding by the compiler, this field must *not* be final
  public static String REQUESTED_API_VERSION = "1.6"; // !final

  final static String NULL_CS_URL = CoreConstants.CODES_URL + "#null_CS";

  /**
   * The unique instance of this class.
   */
  private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

  private static Object KEY = new Object();

  static {
    SINGLETON.init();
  }

  private boolean initialized = false;
  private LoggerContext defaultLoggerContext = new LoggerContext();
  private final ContextSelectorStaticBinder contextSelectorBinder = ContextSelectorStaticBinder
      .getSingleton();

  private StaticLoggerBinder() {
    defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
  }

  public static StaticLoggerBinder getSingleton() {
    return SINGLETON;
  }

  /**
   * Package access for testing purposes.
   */
  static void reset() {
    SINGLETON = new StaticLoggerBinder();
    SINGLETON.init();
  }

  /**
   * Package access for testing purposes.
   */
  void init() {
    try {
      try {
        new ContextInitializer(defaultLoggerContext).autoConfig();
      } catch (JoranException je) {
        Util.report("Failed to auto configure default logger context", je);
      }
      // logback-292
      if(!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
        StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
      }
      contextSelectorBinder.init(defaultLoggerContext, KEY);
      initialized = true;
    } catch (Exception t) { // see LOGBACK-1159
      Util.report("Failed to instantiate [" + LoggerContext.class.getName()
          + "]", t);
    }
  }

  public ILoggerFactory getLoggerFactory() {
    if (!initialized) {
      return defaultLoggerContext;
    }

    if (contextSelectorBinder.getContextSelector() == null) {
      throw new IllegalStateException(
          "contextSelector cannot be null. See also " + NULL_CS_URL);
    }
    return contextSelectorBinder.getContextSelector().getLoggerContext();
  }

  public String getLoggerFactoryClassStr() {
    return contextSelectorBinder.getClass().getName();
  }

}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.rolling.helper;

import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class FileFilterUtil {

  public static String afterLastSlash(String sregex) {
    int i = sregex.lastIndexOf('/');
    if (i == -1) {
      return sregex;
    } else {
      return sregex.substring(i + 1);
    }
  }

  /**
   * Return the set of files matching the stemRegex as found in 'directory'. A
   * stemRegex does not contain any slash characters or any folder separators.
   *
   * @param file folder's file object
   * @param stemRegex regex file pattern to match
   * @return matching files
   */
  public static File[] filesInFolderMatchingStemRegex(File file,
      final String stemRegex) {

    if (file == null) {
      return new File[0];
    }
    if (!file.exists() || !file.isDirectory()) {
      return new File[0];
    }
    return file.listFiles(new FilenameFilter() {
      public boolean accept(File dir, String name) {
        return name.matches(stemRegex);
      }
    });
  }

  static public int findHighestCounter(File[] matchingFileArray, final String stemRegex) {
    int max = Integer.MIN_VALUE;
    for (File aFile : matchingFileArray) {
      int aCounter = FileFilterUtil.extractCounter(aFile, stemRegex);
      if (max < aCounter)
        max = aCounter;
    }
    return max;
  }

  static public int extractCounter(File file, final String stemRegex) {
    Pattern p = Pattern.compile(stemRegex);
    String lastFileName = file.getName();

    Matcher m = p.matcher(lastFileName);
    if (!m.matches()) {
      throw new IllegalStateException("The regex [" + stemRegex
          + "] should match [" + lastFileName + "]");
    }
    String counterAsStr = m.group(1);
    return Integer.valueOf(counterAsStr);
  }

  public static String slashify(String in) {
    return in.replace('\\', '/');
  }
}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.classic.net;

import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.spi.LifeCycle;

/**
 * An abstract base for components that receive logging events from a remote
 * peer and log according to local policy
 *
 * @author Carl Harris
 */
public abstract class ReceiverBase extends ContextAwareBase
    implements LifeCycle {

  private boolean started;

  /**
   * {@inheritDoc}
   */
  public final void start() {
    if (isStarted()) return;
    if (getContext() == null) {
      throw new IllegalStateException("context not set");
    }
    if (shouldStart()) {
      getContext().getScheduledExecutorService().execute(getRunnableTask());
      started = true;
    }
  }

  /**
   * {@inheritDoc}
   */
  public final void stop() {
    if (!isStarted()) return;
    try {
      onStop();
    }
    catch (RuntimeException ex) {
      addError("on stop: " + ex, ex);
    }
    started = false;
  }

  /**
   * {@inheritDoc}
   */
  public final boolean isStarted() {
    return started;
  }

  /**
   * Determines whether this receiver should start.
   * <p>
   * Subclasses will implement this method to do any subclass-specific
   * validation.  The subclass's {@link #getRunnableTask()} method will be
   * invoked (and the task returned will be submitted to the executor)
   * if and only if this method returns {@code true}
   * @return flag indicating whether this receiver should start
   */
  protected abstract boolean shouldStart();

  /**
   * Allows a subclass to participate in receiver shutdown.
   */
  protected abstract void onStop();

  /**
   * Provides the runnable task this receiver will execute.
   * @return runnable task
   */
  protected abstract Runnable getRunnableTask();

}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.net.server;

import javax.net.ServerSocketFactory;
import javax.net.ssl.SSLContext;

import ch.qos.logback.core.net.ssl.ConfigurableSSLServerSocketFactory;
import ch.qos.logback.core.net.ssl.SSLComponent;
import ch.qos.logback.core.net.ssl.SSLConfiguration;
import ch.qos.logback.core.net.ssl.SSLParametersConfiguration;

/**
 *
 * This is the base class for module specific ServerSocketAppender
 * implementations.
 *
 * @author Carl Harris
 */
public abstract class SSLServerSocketAppenderBase<E>
    extends AbstractServerSocketAppender<E> implements SSLComponent {

  private SSLConfiguration ssl;
  private ServerSocketFactory socketFactory;

  @Override
  protected ServerSocketFactory getServerSocketFactory() {
    return socketFactory;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void start() {
    try {
      SSLContext sslContext = getSsl().createContext(this);
      SSLParametersConfiguration parameters = getSsl().getParameters();
      parameters.setContext(getContext());
      socketFactory = new ConfigurableSSLServerSocketFactory(parameters,
          sslContext.getServerSocketFactory());
      super.start();
    }
    catch (Exception ex) {
      addError(ex.getMessage(), ex);
    }
  }

  /**
   * Gets the SSL configuration.
   * @return SSL configuration; if no configuration has been set, a
   *    default configuration is returned
   */
  public SSLConfiguration getSsl() {
    if (ssl == null) {
      ssl = new SSLConfiguration();
    }
    return ssl;
  }

  /**
   * Sets the SSL configuration.
   * @param ssl the SSL configuration to set
   */
  public void setSsl(SSLConfiguration ssl) {
    this.ssl = ssl;
  }
}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.status;

import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.spi.LifeCycle;
import ch.qos.logback.core.util.StatusPrinter;

import java.io.PrintStream;
import java.util.List;

/**
 *  Print all new incoming status messages on the on the designated PrintStream.
 * @author Ceki Gücü
 */
abstract public class OnPrintStreamStatusListenerBase extends ContextAwareBase implements StatusListener, LifeCycle {

  boolean isStarted = false;

  static final long DEFAULT_RETROSPECTIVE = 300;
  long retrospectiveThresold = DEFAULT_RETROSPECTIVE;

  /**
   * The prefix to place before each status message
   * @since 1.1.10
   */
  String prefix;

  /**
   * @return PrintStream used by derived classes
   */
  abstract protected PrintStream getPrintStream();

  private void print(Status status) {
    StringBuilder sb = new StringBuilder();

    if (prefix != null) {
      sb.append(prefix);
    }

    StatusPrinter.buildStr(sb, "", status);
    getPrintStream().print(sb);
  }

  public void addStatusEvent(Status status) {
    if (!isStarted)
      return;
    print(status);
  }

  /**
   * Print status messages retrospectively
   */
  private void retrospectivePrint() {
    if(context == null)
      return;
    long now = System.currentTimeMillis();
    StatusManager sm = context.getStatusManager();
    List<Status> statusList = sm.getCopyOfStatusList();
    for (Status status : statusList) {
      long timestampOfStatusMesage = status.getDate();
      if (isElapsedTimeLongerThanThreshold(now, timestampOfStatusMesage)) {
        print(status);
      }
    }
  }

  private boolean isElapsedTimeLongerThanThreshold(long now, long timestamp) {
    long elapsedTime = now - timestamp;
    return elapsedTime < retrospectiveThresold;
  }

  /**
   * Invoking the start method can cause the instance to print status messages created less than
   * value of retrospectiveThresold.
   */
  public void start() {
    isStarted = true;
    if (retrospectiveThresold > 0) {
      retrospectivePrint();
    }
  }

  public String getPrefix() {
    return prefix;
  }

  public void setPrefix(String prefix) {
    this.prefix = prefix;
  }

  public void setRetrospective(long retrospective) {
    this.retrospectiveThresold = retrospective;
  }

  public long getRetrospective() {
    return retrospectiveThresold;
  }

  public void stop() {
    isStarted = false;
  }

  public boolean isStarted() {
    return isStarted;
  }

}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.net;

import java.io.IOException;
import java.io.OutputStream;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;

import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.Layout;

/**
 * Base class for SyslogAppender.
 *
 * @author Ceki Gülcü
 *
 * @param <E> type of log event object
 */
public abstract class SyslogAppenderBase<E> extends AppenderBase<E> {

  final static String SYSLOG_LAYOUT_URL = CoreConstants.CODES_URL
      + "#syslog_layout";
  final static int MAX_MESSAGE_SIZE_LIMIT = 65000;

  Layout<E> layout;
  String facilityStr;
  String syslogHost;
  protected String suffixPattern;
  protected SyslogOutputStream sos;
  int port = SyslogConstants.SYSLOG_PORT;
  boolean initialized = false;
  private boolean lazyInit = false;
  int maxMessageSize;
  Charset charset;

  public void start() {
    int errorCount = 0;
    if (facilityStr == null) {
      addError("The Facility option is mandatory");
      errorCount++;
    }

    if (charset == null) {
      // Using defaultCharset() preserves the previous behavior when String.getBytes() was
      // called without arguments
      charset = Charset.defaultCharset();
    }

    if (!lazyInit) {
      // this connect() does not retry (unlike the one in SocketAppenderBase)
      // so account for the error if no connection established
      if (!connect()) {
        errorCount++;
      }
    }

    if (layout == null) {
      layout = buildLayout();
    }

    if (errorCount == 0) {
      super.start();
    }
  }

  abstract public SyslogOutputStream createOutputStream() throws UnknownHostException, SocketException;

  abstract public Layout<E> buildLayout();

  abstract public int getSeverityForEvent(Object eventObject);

  private boolean connect() {
    try {
      sos = createOutputStream();

      final int systemDatagramSize = sos.getSendBufferSize();
      if (maxMessageSize == 0) {
        maxMessageSize = Math.min(systemDatagramSize, MAX_MESSAGE_SIZE_LIMIT);
        addInfo("Defaulting maxMessageSize to [" + maxMessageSize + "]");
      } else if (maxMessageSize > systemDatagramSize) {
        addWarn("maxMessageSize of [" + maxMessageSize + "] is larger than the system defined datagram size of [" + systemDatagramSize + "].");
        addWarn("This may result in dropped logs.");
      }
    } catch (UnknownHostException e) {
      addError("Could not create SyslogWriter", e);
    } catch (SocketException e) {
      addWarn(
          "Failed to bind to a random datagram socket. Will try to reconnect later.",
          e);
    }

    // SyslogOutputStream must be non-null to be connected
    return sos != null;
  }

  @Override
  protected void append(E eventObject) {
    if (!isStarted()) {
      return;
    }

    if (!initialized && lazyInit) {
      initialized = true;
      connect();
    }
    if (sos == null) {
      return;
    }

    try {
      String msg = layout.doLayout(eventObject);
      if(msg == null) {
        return;
      }
      if (msg.length() > maxMessageSize) {
        msg = msg.substring(0, maxMessageSize);
      }
      sos.write(msg.getBytes(charset));
      sos.flush();
      postProcess(eventObject, sos);
    } catch (IOException ioe) {
      addError("Failed to send diagram to " + syslogHost, ioe);
    }
  }

  protected void postProcess(Object event, OutputStream sw) {

  }

  /**
   * Returns the integer value corresponding to the named syslog facility.
   *
   * @param facilityStr the syslog facility name
   * @return the syslog facility code
   * @throws IllegalArgumentException
   *           if the facility string is not recognized
   */
  static public int facilityStringToint(String facilityStr) {
    if ("KERN".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_KERN;
    } else if ("USER".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_USER;
    } else if ("MAIL".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_MAIL;
    } else if ("DAEMON".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_DAEMON;
    } else if ("AUTH".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_AUTH;
    } else if ("SYSLOG".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_SYSLOG;
    } else if ("LPR".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_LPR;
    } else if ("NEWS".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_NEWS;
    } else if ("UUCP".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_UUCP;
    } else if ("CRON".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_CRON;
    } else if ("AUTHPRIV".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_AUTHPRIV;
    } else if ("FTP".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_FTP;
    } else if ("NTP".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_NTP;
    } else if ("AUDIT".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_AUDIT;
    } else if ("ALERT".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_ALERT;
    } else if ("CLOCK".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_CLOCK;
    } else if ("LOCAL0".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_LOCAL0;
    } else if ("LOCAL1".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_LOCAL1;
    } else if ("LOCAL2".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_LOCAL2;
    } else if ("LOCAL3".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_LOCAL3;
    } else if ("LOCAL4".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_LOCAL4;
    } else if ("LOCAL5".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_LOCAL5;
    } else if ("LOCAL6".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_LOCAL6;
    } else if ("LOCAL7".equalsIgnoreCase(facilityStr)) {
      return SyslogConstants.LOG_LOCAL7;
    } else {
      throw new IllegalArgumentException(facilityStr
          + " is not a valid syslog facility string");
    }
  }

  /**
   * Returns the value of the SyslogHost option.
   * @return the syslog hostname
   */
  public String getSyslogHost() {
    return syslogHost;
  }

  /**
   * The SyslogHost option is the name of the the syslog host where log
   * output should go.
   *
   * WARNING If the SyslogHost is not set, then this appender will fail.
   * @param syslogHost the syslog hostname
   */
  public void setSyslogHost(String syslogHost) {
    this.syslogHost = syslogHost;
  }

  /**
   * Returns the string value of the Facility option.
   *
   * See {@link #setFacility} for the set of allowed values.
   * @return the current facility name
   */
  public String getFacility() {
    return facilityStr;
  }

  /**
   * The Facility option must be set one of the strings KERN, USER, MAIL,
   * DAEMON, AUTH, SYSLOG, LPR, NEWS, UUCP, CRON, AUTHPRIV, FTP, NTP, AUDIT,
   * ALERT, CLOCK, LOCAL0, LOCAL1, LOCAL2, LOCAL3, LOCAL4, LOCAL5, LOCAL6,
   * LOCAL7. Case is not important.
   *
   *

   * See {@link SyslogConstants} and RFC 3164 for more information about the
   * Facility option.
   * @param facilityStr the name of the desired facility
   */
  public void setFacility(String facilityStr) {
    if (facilityStr != null) {
      facilityStr = facilityStr.trim();
    }
    this.facilityStr = facilityStr;
  }

  /**
   * Gets the syslog port to connect to
   * @return the syslog port
   */
  public int getPort() {
    return port;
  }

  /**
   * The port number on the syslog server to connect to. Normally, you would not
   * want to change the default value, that is 514.
   * @param port the desired syslog port
   */
  public void setPort(int port) {
    this.port = port;
  }

  /**
   * Gets the maximum size of a syslog message
   * @return the max message size (in characters)
   */
  public int getMaxMessageSize() {
    return maxMessageSize;
  }

  /**
   * Maximum size for the syslog message (in characters); messages
   * longer than this are truncated. The default value is 65400 (which
   * is near the maximum for syslog-over-UDP). Note that the value is
   * characters; the number of bytes may vary if non-ASCII characters
   * are present.
   * @param maxMessageSize the max message size (in characters)
   */
  public void setMaxMessageSize(int maxMessageSize) {
    this.maxMessageSize = maxMessageSize;
  }

  public Layout<E> getLayout() {
    return layout;
  }

  public void setLayout(Layout<E> layout) {
    addWarn("The layout of a SyslogAppender cannot be set directly. See also "
        + SYSLOG_LAYOUT_URL);
  }

  /**
   * Gets the enable status of lazy initialization of the Syslog output
   * stream
   *
   * @return true if enabled; false otherwise
   */
  public boolean getLazy() {
    return lazyInit;
  }

  /**
   * Enables/disables lazy initialization of the Syslog output stream.
   * This defers the connection process until the first outgoing message.
   *
   * @param enable true to enable lazy initialization; false otherwise
   */
  public void setLazy(boolean enable) {
    lazyInit = enable;
  }

  @Override
  public void stop() {
    if (sos != null) {
      sos.close();
    }
    super.stop();
  }

  /**
   * See {@link #setSuffixPattern(String)}.
   *
   * @return the suffix pattern
   */
  public String getSuffixPattern() {
    return suffixPattern;
  }

  /**
   * The suffixPattern option specifies the format of the
   * non-standardized part of the message sent to the syslog server.
   *
   * @param suffixPattern the desired suffix pattern
   */
  public void setSuffixPattern(String suffixPattern) {
    this.suffixPattern = suffixPattern;
  }

  /**
   * Returns the Charset used to encode String messages into byte sequences when writing to
   * syslog.
   */
  public Charset getCharset() {
    return charset;
  }

  /**
   * The Charset to use when encoding messages into byte sequences.
   *
   * @param charset
   */
  public void setCharset(Charset charset) {
    this.charset = charset;
  }
}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.subst;

import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.spi.PropertyContainer;
import ch.qos.logback.core.spi.ScanException;
import ch.qos.logback.core.util.OptionHelper;

import java.util.List;
import java.util.Stack;

/**
 * Compiles a previously parsed Node chain into a String.
 *
 * @author Ceki Gücü
 */
public class NodeToStringTransformer {

  final Node node;
  final PropertyContainer propertyContainer0;
  final PropertyContainer propertyContainer1;

  public NodeToStringTransformer(Node node, PropertyContainer propertyContainer0, PropertyContainer propertyContainer1) {
    this.node = node;
    this.propertyContainer0 = propertyContainer0;
    this.propertyContainer1 = propertyContainer1;
  }

  public NodeToStringTransformer(Node node, PropertyContainer propertyContainer0) {
    this(node, propertyContainer0, null);
  }

  public static String substituteVariable(String input, PropertyContainer pc0, PropertyContainer pc1) throws ScanException {
    Node node = tokenizeAndParseString(input);
    NodeToStringTransformer nodeToStringTransformer = new NodeToStringTransformer(node, pc0, pc1);
    return nodeToStringTransformer.transform();
  }

  private static Node tokenizeAndParseString(String value) throws ScanException {
    Tokenizer tokenizer = new Tokenizer(value);
    List<Token> tokens = tokenizer.tokenize();
    Parser parser = new Parser(tokens);
    return parser.parse();
  }

  public String transform() throws ScanException {
    StringBuilder stringBuilder = new StringBuilder();
    compileNode(node, stringBuilder, new Stack<Node>());
    return stringBuilder.toString();
  }

  private void compileNode(Node inputNode, StringBuilder stringBuilder, Stack<Node> cycleCheckStack) throws ScanException {
    Node n = inputNode;
    while (n != null) {
      switch (n.type) {
        case LITERAL:
          handleLiteral(n, stringBuilder);
          break;
        case VARIABLE:
          handleVariable(n, stringBuilder, cycleCheckStack);
          break;
      }
      n = n.next;
    }
  }

  private void handleVariable(Node n, StringBuilder stringBuilder, Stack<Node> cycleCheckStack) throws ScanException {

    // Check for recursion
    if (haveVisitedNodeAlready(n, cycleCheckStack)) {
      cycleCheckStack.push(n);
      String error = constructRecursionErrorMessage(cycleCheckStack);
      throw new IllegalArgumentException(error);
    }
    cycleCheckStack.push(n);

    StringBuilder keyBuffer = new StringBuilder();
    Node payload = (Node) n.payload;
    compileNode(payload, keyBuffer, cycleCheckStack);
    String key = keyBuffer.toString();
    String value = lookupKey(key);

    if (value != null) {
      Node innerNode = tokenizeAndParseString(value);
      compileNode(innerNode, stringBuilder, cycleCheckStack);
      cycleCheckStack.pop();
      return;
    }

    if (n.defaultPart == null) {
      stringBuilder.append(key + CoreConstants.UNDEFINED_PROPERTY_SUFFIX);
      cycleCheckStack.pop();
      return;
    }

    Node defaultPart = (Node) n.defaultPart;
    StringBuilder defaultPartBuffer = new StringBuilder();
    compileNode(defaultPart, defaultPartBuffer, cycleCheckStack);
    cycleCheckStack.pop();
    String defaultVal = defaultPartBuffer.toString();
    stringBuilder.append(defaultVal);
  }

  private String lookupKey(String key) {
    String value = propertyContainer0.getProperty(key);
    if (value != null)
      return value;

    if (propertyContainer1 != null) {
      value = propertyContainer1.getProperty(key);
      if (value != null)
        return value;
    }

    value = OptionHelper.getSystemProperty(key, null);
    if (value != null)
      return value;

    value = OptionHelper.getEnv(key);
    if (value != null) {
      return value;
    }

    return null;
  }

  private void handleLiteral(Node n, StringBuilder stringBuilder) {
    stringBuilder.append((String) n.payload);
  }

  private String variableNodeValue(Node variableNode) {
    Node literalPayload = (Node) variableNode.payload;
    return (String) literalPayload.payload;
  }

  private String constructRecursionErrorMessage(Stack<Node> recursionNodes) {
    StringBuilder errorBuilder = new StringBuilder("Circular variable reference detected while parsing input [");

    for (Node stackNode : recursionNodes) {
      errorBuilder.append("${").append(variableNodeValue(stackNode)).append("}");
      if (recursionNodes.lastElement() != stackNode) {
        errorBuilder.append(" --> ");
      }
    }
    errorBuilder.append("]");
    return errorBuilder.toString();
  }

  /**
   * Determine if a node has already been visited already by checking the cycleDetectionStack
   * for it's existence. This method is used -- rather than Stack.contains() -- because
   * we want to ignore the Node's 'next' attribute when comparing for equality.
   */
  private boolean haveVisitedNodeAlready(Node node, Stack<Node> cycleDetectionStack) {
    for (Node cycleNode : cycleDetectionStack) {
      if (equalNodes(node, cycleNode)) {
        return true;
      }
    }
    return false;
  }
  private boolean equalNodes(Node node1, Node node2) {
    if (node1.type != null && !node1.type.equals(node2.type)) return false;
    if (node1.payload != null && !node1.payload.equals(node2.payload)) return false;
    if (node1.defaultPart != null && !node1.defaultPart.equals(node2.defaultPart)) return false;

    return true;
  }

}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.classic.android;

import java.io.File;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteStatement;
import ch.qos.logback.classic.db.SQLBuilder;
import ch.qos.logback.classic.db.names.DBNameResolver;
import ch.qos.logback.classic.db.names.DefaultDBNameResolver;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import ch.qos.logback.core.android.AndroidContextUtil;
import ch.qos.logback.core.util.Duration;

/**
 * SQLiteAppender is a logback appender optimized for Android SQLite. It requires no JDBC
 * as it uses the built-in Android SQLite API.
 *
 * @author Anthony Trinh
 * @since 1.0.11
 */
public class SQLiteAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {

  private SQLiteDatabase db;
  private String insertPropertiesSQL;
  private String insertExceptionSQL;
  private String insertSQL;
  private String filename;
  private DBNameResolver dbNameResolver;
  private Duration maxHistory;
  private long lastCleanupTime = 0;
  private SQLiteLogCleaner logCleaner;
  private Clock clock = new SystemClock();

  void setClock(Clock clock) {
    this.clock = clock;
  }

  /**
   * Sets the database name resolver, used to customize the names of the table names
   * and columns in the database.
   *
   * @param dbNameResolver the desired database name resolver
   */
  public void setDbNameResolver(DBNameResolver dbNameResolver) {
    this.dbNameResolver = dbNameResolver;
  }

  /**
   * Get the maximum history in time duration of records to keep
   *
   * @return max history in time duration (e.g., "1 day")
   */
  public String getMaxHistory() {
    return maxHistory != null ? maxHistory.toString() : "";
  }

  /**
   * Gets the maximum history in milliseconds
   * @return the max history in milliseconds
   */
  public long getMaxHistoryMs() {
    return maxHistory != null ? maxHistory.getMilliseconds() : 0;
  }

  /**
   * Set the maximum history in time duration of records to keep
   *
   * @param maxHistory
   *                max history in time duration (e.g., "2 days")
   */
  public void setMaxHistory(String maxHistory) {
    this.maxHistory = Duration.valueOf(maxHistory);
  }

  /**
   * @return the absolute path to the SQLite database
     */
  public String getFilename() {
    return this.filename;
  }

  /**
   * Sets the path to the destination SQLite database
   * @param filename absolute path to file
   */
  public void setFilename(String filename) {
    this.filename = filename;
  }

  /**
   * Gets a file object from a file path to a SQLite database
   * @param filename absolute path to database file
   * @return the file object if a valid file found; otherwise, null
   */
  public File getDatabaseFile(String filename) {
    File dbFile = null;
    if (filename != null && filename.trim().length() > 0) {
      dbFile = new File(filename);
    }
    if (dbFile == null || dbFile.isDirectory()) {
      dbFile = new File(new AndroidContextUtil().getDatabasePath("logback.db"));
    }
    return dbFile;
  }

  /*
   * (non-Javadoc)
   * @see ch.qos.logback.core.UnsynchronizedAppenderBase#start()
   */
  @Override
  public void start() {
    this.started = false;

    File dbfile = getDatabaseFile(this.filename);
    if (dbfile == null) {
      addError("Cannot determine database filename");
      return;
    }

    boolean dbOpened = false;
    try {
      dbfile.getParentFile().mkdirs();
      addInfo("db path: " + dbfile.getAbsolutePath());
      this.db = SQLiteDatabase.openOrCreateDatabase(dbfile.getPath(), null);
      dbOpened = true;
    } catch (SQLiteException e) {
      addError("Cannot open database", e);
    }

    if (dbOpened) {
      if (dbNameResolver == null) {
        dbNameResolver = new DefaultDBNameResolver();
      }

      insertExceptionSQL = SQLBuilder.buildInsertExceptionSQL(dbNameResolver);
      insertPropertiesSQL = SQLBuilder.buildInsertPropertiesSQL(dbNameResolver);
      insertSQL = SQLBuilder.buildInsertSQL(dbNameResolver);

      try {
        this.db.execSQL(SQLBuilder.buildCreateLoggingEventTableSQL(dbNameResolver));
        this.db.execSQL(SQLBuilder.buildCreatePropertyTableSQL(dbNameResolver));
        this.db.execSQL(SQLBuilder.buildCreateExceptionTableSQL(dbNameResolver));

        clearExpiredLogs(this.db);

        super.start();

        this.started = true;
      } catch (SQLiteException e) {
        addError("Cannot create database tables", e);
      }
    }
  }

  /**
   * Removes expired logs from the database
   * @param db
   */
  private void clearExpiredLogs(SQLiteDatabase db) {
    if (lastCheckExpired(this.maxHistory, this.lastCleanupTime)) {
      this.lastCleanupTime = this.clock.currentTimeMillis();
      this.getLogCleaner().performLogCleanup(db, this.maxHistory);
    }
  }

  /**
   * Determines whether it's time to clear expired logs
   * @param expiry max time duration between checks
   * @param lastCleanupTime timestamp (ms) of last cleanup
   * @return true if last check has expired
   */
  private boolean lastCheckExpired(Duration expiry, long lastCleanupTime) {
    boolean isExpired = false;
    if (expiry != null && expiry.getMilliseconds() > 0) {
      final long now = this.clock.currentTimeMillis();
      final long timeDiff = now - lastCleanupTime;
      isExpired = (lastCleanupTime <= 0) || (timeDiff >= expiry.getMilliseconds());
    }
    return isExpired;
  }

  /**
   * Gets the {@code SQLiteLogCleaner} in use. Creates default if needed.
   */
  public SQLiteLogCleaner getLogCleaner() {
    if (this.logCleaner == null) {
      final Clock thisClock = this.clock;
      this.logCleaner = new SQLiteLogCleaner() {
        public void performLogCleanup(SQLiteDatabase db, Duration expiry) {
          final long expiryMs = thisClock.currentTimeMillis() - expiry.getMilliseconds();
          final String deleteExpiredLogsSQL = SQLBuilder.buildDeleteExpiredLogsSQL(dbNameResolver, expiryMs);
          db.execSQL(deleteExpiredLogsSQL);
        }
      };
    }
    return this.logCleaner;
  }

  /**
   * Sets the {@code SQLiteLogCleaner}, invoked when {@code maxHistory} is exceeded
   * at startup and in between logging events
   * @param logCleaner
   */
  public void setLogCleaner(SQLiteLogCleaner logCleaner) {
    this.logCleaner = logCleaner;
  }

  /*
   * (non-Javadoc)
   * @see java.lang.Object#finalize()
   */
  @Override
  protected void finalize() throws Throwable {
    this.db.close();
  }

  /*
   * (non-Javadoc)
   * @see ch.qos.logback.core.UnsynchronizedAppenderBase#stop()
   */
  @Override
  public void stop() {
    this.db.close();
    this.lastCleanupTime = 0;
  }

  /*
   * (non-Javadoc)
   * @see ch.qos.logback.core.UnsynchronizedAppenderBase#append(java.lang.Object)
   */
  @Override
  public void append(ILoggingEvent eventObject) {
    if (isStarted()) {
      try {
        clearExpiredLogs(db);
        SQLiteStatement stmt = db.compileStatement(insertSQL);
        try {
          db.beginTransaction();
          long eventId = subAppend(eventObject, stmt);
          if (eventId != -1) {
            secondarySubAppend(eventObject, eventId);
            db.setTransactionSuccessful();
          }
        } finally {
          if (db.inTransaction()) {
            db.endTransaction();
          }
          stmt.close();
        }
      } catch (Throwable e) {
        addError("Cannot append event", e);
      }
    }
  }

  /**
   * Inserts the main details of a log event into the database
   *
   * @param event the event to insert
   * @param insertStatement the SQLite statement used to insert the event
   * @return the row ID of the newly inserted event; -1 if the insertion failed
   * @throws SQLException
   */
  private long subAppend(ILoggingEvent event, SQLiteStatement insertStatement) throws SQLException {

    bindLoggingEvent(insertStatement, event);
    bindLoggingEventArguments(insertStatement, event.getArgumentArray());

    // This is expensive... should we do it every time?
    bindCallerData(insertStatement, event.getCallerData());

    long insertId = -1;
    try {
      insertId = insertStatement.executeInsert();
    } catch (SQLiteException e) {
      addWarn("Failed to insert loggingEvent", e);
    }
    return insertId;
  }

  /**
   * Updates an existing row of an event with the secondary details of the event.
   * This includes MDC properties and any exception information.
   *
   * @param event the event containing the details to insert
   * @param eventId the row ID of the event to modify
   * @throws SQLException
   */
  private void secondarySubAppend(ILoggingEvent event, long eventId) throws SQLException {
    Map<String, String> mergedMap = mergePropertyMaps(event);
    insertProperties(mergedMap, eventId);

    if (event.getThrowableProxy() != null) {
      insertThrowable(event.getThrowableProxy(), eventId);
    }
  }

  private static final int  TIMESTMP_INDEX = 1;
  private static final int  FORMATTED_MESSAGE_INDEX  = 2;
  private static final int  LOGGER_NAME_INDEX = 3;
  private static final int  LEVEL_STRING_INDEX = 4;
  private static final int  THREAD_NAME_INDEX = 5;
  private static final int  REFERENCE_FLAG_INDEX = 6;
  private static final int  ARG0_INDEX = 7;
//  private static final int  ARG1_INDEX = 8;
//  private static final int  ARG2_INDEX = 9;
//  private static final int  ARG3_INDEX = 10;
  private static final int  CALLER_FILENAME_INDEX = 11;
  private static final int  CALLER_CLASS_INDEX = 12;
  private static final int  CALLER_METHOD_INDEX = 13;
  private static final int  CALLER_LINE_INDEX = 14;
//  private static final int  EVENT_ID_INDEX  = 15;

  /**
   * Binds the main details of a log event to a SQLite statement's parameters
   *
   * @param stmt the SQLite statement to modify
   * @param event the event containing the details to bind
   * @throws SQLException
   */
  private void bindLoggingEvent(SQLiteStatement stmt, ILoggingEvent event) throws SQLException {
    stmt.bindLong(TIMESTMP_INDEX, event.getTimeStamp());
    stmt.bindString(FORMATTED_MESSAGE_INDEX, event.getFormattedMessage());
    stmt.bindString(LOGGER_NAME_INDEX, event.getLoggerName());
    stmt.bindString(LEVEL_STRING_INDEX, event.getLevel().toString());
    stmt.bindString(THREAD_NAME_INDEX, event.getThreadName());
    stmt.bindLong(REFERENCE_FLAG_INDEX, computeReferenceMask(event));
  }

  /**
   * Binds a logging event's arguments (e.g., logger.debug("x={} y={}", arg1, arg2))
   * to a SQLite statement's parameters
   *
   * @param stmt the SQLite statement to modify
   * @param argArray the argument array to bind
   * @throws SQLException
   */
  private void bindLoggingEventArguments(SQLiteStatement stmt, Object[] argArray) throws SQLException {
    int arrayLen = argArray != null ? argArray.length : 0;
    for (int i = 0; i < arrayLen && i < 4; i++) {
      stmt.bindString(ARG0_INDEX+i, asStringTruncatedTo254(argArray[i]));
    }
//
//    // set remaining columns to ""
//    for (int i = arrayLen; i < 4; i++) {
//      stmt.bindString(ARG0_INDEX+i, "");
//    }
  }

  /**
   * Gets the first 254 characters of an object's string representation. This is
   * used to truncate a logging event's argument binding if necessary.
   *
   * @param o the object
   * @return up to 254 characters of the object's string representation; or empty
   * string if the object string is itself null
   */
  private String asStringTruncatedTo254(Object o) {
    String s = null;
    if (o != null) {
      s = o.toString();
    }
    if (s != null && s.length() > 254) {
      s = s.substring(0, 254);
    }
    return s == null ? "" : s;
  }

  private static final short PROPERTIES_EXIST = 0x01;
  private static final short EXCEPTION_EXISTS = 0x02;

  /**
   * Computes the reference mask for a logging event, including
   * flags to indicate whether MDC properties or exception info
   * is available for the event.
   *
   * @param event the logging event to evaluate
   * @return the 16-bit reference mask
   */
  private static short computeReferenceMask(ILoggingEvent event) {
    short mask = 0;

    int mdcPropSize = 0;
    if (event.getMDCPropertyMap() != null) {
      mdcPropSize = event.getMDCPropertyMap().keySet().size();
    }
    int contextPropSize = 0;
    if (event.getLoggerContextVO().getPropertyMap() != null) {
      contextPropSize = event.getLoggerContextVO().getPropertyMap().size();
    }

    if (mdcPropSize > 0 || contextPropSize > 0) {
      mask = PROPERTIES_EXIST;
    }
    if (event.getThrowableProxy() != null) {
      mask |= EXCEPTION_EXISTS;
    }
    return mask;
  }

  /**
   * Merges a log event's properties with the properties of the logger context.
   * The context properties are first in the map, and then the event's properties
   * are appended.
   *
   * @param event the logging event to evaluate
   * @return the merged properties map
   */
  private Map<String, String> mergePropertyMaps(ILoggingEvent event) {
    Map<String, String> mergedMap = new HashMap<String, String>();
    // we add the context properties first, then the event properties, since
    // we consider that event-specific properties should have priority over
    // context-wide properties.
    Map<String, String> loggerContextMap = event.getLoggerContextVO().getPropertyMap();
    if (loggerContextMap != null) {
      mergedMap.putAll(loggerContextMap);
    }

    Map<String, String> mdcMap = event.getMDCPropertyMap();
    if (mdcMap != null) {
      mergedMap.putAll(mdcMap);
    }

    return mergedMap;
  }

  /**
   * Updates an existing row with property details (context properties and event's properties).
   *
   * @param mergedMap the properties of the context plus the event's properties
   * @param eventId the row ID of the event
   * @throws SQLException
   */
  private void insertProperties(Map<String, String> mergedMap, long eventId) throws SQLException {
    if (mergedMap.size() > 0) {
      SQLiteStatement stmt = db.compileStatement(insertPropertiesSQL);
      try {
        for (Entry<String,String> entry : mergedMap.entrySet()) {
          stmt.bindLong(1, eventId);
          stmt.bindString(2, entry.getKey());
          stmt.bindString(3, entry.getValue());
          stmt.executeInsert();
        }
      } finally {
        stmt.close();
      }
    }
  }

  /**
   * Binds the calling function's details (filename, line, etc.) to a SQLite statement's arguments
   *
   * @param stmt the SQLite statement to modify
   * @param callerDataArray the caller's stack trace
   * @throws SQLException
   */
  private void bindCallerData(SQLiteStatement stmt, StackTraceElement[] callerDataArray) throws SQLException {
    if (callerDataArray != null && callerDataArray.length > 0) {
      StackTraceElement callerData = callerDataArray[0];
      if (callerData != null) {
        bindString(stmt, CALLER_FILENAME_INDEX, callerData.getFileName());
        bindString(stmt, CALLER_CLASS_INDEX, callerData.getClassName());
        bindString(stmt, CALLER_METHOD_INDEX, callerData.getMethodName());
        bindString(stmt, CALLER_LINE_INDEX, Integer.toString(callerData.getLineNumber()));
      }
    }
  }

  private void bindString(SQLiteStatement stmt, int columnIndex, String value) {
    if (value != null) {
      stmt.bindString(columnIndex, value);
    }
  }

  /**
   * Inserts an exception into the logging_exceptions table
   *
   * @param stmt
   * @param txt
   * @param i
   * @param eventId
   */
  private void insertException(SQLiteStatement stmt, String txt, short i, long eventId) throws SQLException {
    stmt.bindLong(1, eventId);
    stmt.bindLong(2, i);
    stmt.bindString(3, txt);
    stmt.executeInsert();
  }

  private void insertThrowable(IThrowableProxy tp, long eventId) throws SQLException {

    SQLiteStatement stmt = db.compileStatement(insertExceptionSQL);
    try {
      short baseIndex = 0;
      while (tp != null) {
        StringBuilder buf = new StringBuilder();
        ThrowableProxyUtil.subjoinFirstLine(buf, tp);
        insertException(stmt, buf.toString(), baseIndex++, eventId);

        int commonFrames = tp.getCommonFrames();
        StackTraceElementProxy[] stepArray = tp.getStackTraceElementProxyArray();

        for (int i = 0; i < stepArray.length - commonFrames; i++) {
          StringBuilder sb = new StringBuilder();
          sb.append(CoreConstants.TAB);
          ThrowableProxyUtil.subjoinSTEP(sb, stepArray[i]);
          insertException(stmt, sb.toString(), baseIndex++, eventId);
        }

        if (commonFrames > 0) {
          StringBuilder sb = new StringBuilder();
          sb.append(CoreConstants.TAB)
            .append("... ")
            .append(commonFrames)
            .append(" common frames omitted");

          insertException(stmt, sb.toString(), baseIndex++, eventId);
        }

        tp = tp.getCause();
      }
    } finally {
      stmt.close();
    }
  }
}


/**
 * Copyright 2019 Anthony Trinh
 *
 * 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 ch.qos.logback.core.joran.spi;

import java.util.List;
import java.util.Locale;

/**
 * ElementSelector extends {@link ElementPath} with matching operations such as {@link #fullPathMatch(ElementPath)},
 * {@link #getPrefixMatchLength(ElementPath)} and {@link #getTailMatchLength(ElementPath)}.
 *
 * <p>Parts of the path may contain '*' for wildcard matching.
 *
 * @author Ceki Gülcü
 * @since 1.1.0
 */
public class ElementSelector extends ElementPath {

  public ElementSelector() {
    super();
  }

  public ElementSelector(List<String> list) {
    super(list);
  }

  /**
   * Build an elementPath from a string.
   *
   * Note that "/x" is considered equivalent to "x" and to "x/"
   * @param p element path
   */
  public ElementSelector(String p) {
    super(p);
  }

  public boolean fullPathMatch(ElementPath path) {
    if (path.size() != size()) {
      return false;
    }

    int len = size();
    for (int i = 0; i < len; i++) {
      if (!equalityCheck(get(i), path.get(i))) {
        return false;
      }
    }
    // if everything matches, then the two patterns are equal
    return true;
  }

  /**
   * Returns the number of "tail" components that this pattern has in common
   * with the pattern p passed as parameter. By "tail" components we mean the
   * components at the end of the pattern.
   * @param p element path
   * @return the number of "tail" components in common with p
   */
  public int getTailMatchLength(ElementPath p) {
    if (p == null) {
      return 0;
    }

    int lSize = this.partList.size();
    int rSize = p.partList.size();

    // no match possible for empty sets
    if ((lSize == 0) || (rSize == 0)) {
      return 0;
    }

    int minLen = (lSize <= rSize) ? lSize : rSize;
    int match = 0;

    // loop from the end to the front
    for (int i = 1; i <= minLen; i++) {
      String l = this.partList.get(lSize - i);
      String r = p.partList.get(rSize - i);

      if (equalityCheck(l, r)) {
        match++;
      } else {
        break;
      }
    }
    return match;
  }

  public boolean isContainedIn(ElementPath p) {
    if(p == null) {
      return false;
    }
    return p.toStableString().contains(toStableString());
  }

  /**
   * Returns the number of "prefix" components that this pattern has in common
   * with the pattern p passed as parameter. By "prefix" components we mean the
   * components at the beginning of the pattern.
   * @param p element path
   * @return the number of "prefix" components in common with p
   */
  public int getPrefixMatchLength(ElementPath p) {
    if (p == null) {
      return 0;
    }

    int lSize = this.partList.size();
    int rSize = p.partList.size();

    // no match possible for empty sets
    if ((lSize == 0) || (rSize == 0)) {
      return 0;
    }

    int minLen = (lSize <= rSize) ? lSize : rSize;
    int match = 0;

    for (int i = 0; i < minLen; i++) {
      String l = this.partList.get(i);
      String r = p.partList.get(i);

      if (equalityCheck(l, r)) {
        match++;
      } else {
        break;
      }
    }

    return match;
  }

  private boolean equalityCheck(String x, String y) {
    return x.equalsIgnoreCase(y);
  }

  @Override
  public boolean equals(Object o) {
    if ((o == null) || !(o instanceof ElementSelector)) {
      return false;
    }

    ElementSelector r = (ElementSelector) o;

    if (r.size() != size()) {
      return false;
    }

    int len = size();

    for (int i = 0; i < len; i++) {
      if (!equalityCheck(get(i), r.get(i))) {
        return false;
      }
    }

    // if everything matches, then the two patterns are equal
    return true;
  }

  @Override
  public int hashCode() {
    int hc = 0;
    int len = size();

    for (int i = 0; i < len; i++) {
      // make Pattern comparisons case insensitive
      // http://jira.qos.ch/browse/LBCORE-76
      hc ^= get(i).toLowerCase(Locale.US).hashCode();
    }
    return hc;
  }

}