package com.linkedin.parseq.retry.backoff;

import com.linkedin.parseq.internal.ArgumentUtil;

import java.util.concurrent.ThreadLocalRandom;

/**
 * A policy that randomizes the result of another policy by adding a random duration in the specified range.
 *
 * @author Oleg Anashkin (oleg.anashkin@gmail.com)
 */
public class RandomizedBackoff implements BackoffPolicy {
  protected final BackoffPolicy _policy;
  protected final long _minRange;
  protected final long _maxRange;

  /**
   * A policy that randomizes the result of another policy by adding a random duration in the specified range.
   *
   * @param policy The base policy to randomize the result of.
   * @param minRange The minimum range of values that may be used to modify the result of the base policy.
   * @param maxRange The maximum range of values that may be used to modify the result of the base policy.
   */
  public RandomizedBackoff(BackoffPolicy policy, long minRange, long maxRange) {
    ArgumentUtil.requireNotNull(policy, "policy");
    if (maxRange <= minRange) {
      throw new IllegalArgumentException(String.format("minRange %s must be strictly less than maxRange %s", minRange, maxRange));
    }

    _policy = policy;
    _minRange = minRange;
    _maxRange = maxRange;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public long nextBackoff(int attempts, Throwable error) {
    return _policy.nextBackoff(attempts, error) + _minRange + Math.round((_maxRange - _minRange) * ThreadLocalRandom.current().nextDouble());
  }
}


package com.linkedin.parseq.retry.termination;

/**
 * A termination policy that signals for termination after either of the specified policies terminate.
 *
 * @author Oleg Anashkin (oleg.anashkin@gmail.com)
 */
public class RequireEither implements TerminationPolicy {
  protected final TerminationPolicy _first;
  protected final TerminationPolicy _second;

  /**
   * A termination policy that signals for termination after either of the specified policies terminate.
   *
   * @param first The first of the two policies that may signal for termination.
   * @param second The second of the two policies that may signal for termination.
   */
  public RequireEither(TerminationPolicy first, TerminationPolicy second) {
    _first = first;
    _second = second;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean shouldTerminate(int attempts, long nextAttemptAt) {
    return _first.shouldTerminate(attempts, nextAttemptAt) || _second.shouldTerminate(attempts, nextAttemptAt);
  }
}


package com.linkedin.parseq.retry;

import com.linkedin.parseq.TaskType;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.linkedin.parseq.Context;
import com.linkedin.parseq.Priority;
import com.linkedin.parseq.Task;
import com.linkedin.parseq.function.Function1;
import com.linkedin.parseq.internal.ArgumentUtil;
import com.linkedin.parseq.promise.Promise;
import com.linkedin.parseq.promise.Promises;
import com.linkedin.parseq.promise.SettablePromise;

/**
 * A parseq task wrapper that supports arbitrary retry policies.
 *
 * @author Oleg Anashkin (oleg.anashkin@gmail.com)
 */
public final class RetriableTask<T> {

  private static final Logger LOGGER = LoggerFactory.getLogger(RetriableTask.class);

  /** A name of the task that needs to be retried. */
  private final String _name;

  /** A task generator function. It will receive a zero-based attempt number as a parameter. */
  private final Function1<Integer, Task<T>> _taskFunction;

  /** Retry policy which will control this task's behavior. */
  private final RetryPolicy _policy;

  /** Start time for the very first attempt. */
  private long _startedAt;

  /**
   * A parseq task wrapper that supports arbitrary retry policies.
   *
   * @param name A name of the task that needs to be retried.
   * @param policy Retry policy that will control this task's behavior.
   * @param taskFunction A task generator function. It will receive a zero-based attempt number as a parameter.
   */
  private RetriableTask(String name, RetryPolicy policy, Function1<Integer, Task<T>> taskFunction)
  {
    ArgumentUtil.requireNotNull(name, "name");
    ArgumentUtil.requireNotNull(policy, "policy");
    ArgumentUtil.requireNotNull(taskFunction, "taskFunction");

    _name = name;
    _policy = policy;
    _taskFunction = taskFunction;
  }

  /**
   * A helper for creating task wrapper with associated retry policy.
   *
   * @param name A name of the task that needs to be retried.
   * @param policy Retry policy that will control this task's behavior.
   * @param taskFunction A task generator function. It will receive a zero-based attempt number as a parameter.
   * @param <U> Type of a task result.
   */
  public static <U> Task<U> withRetryPolicy(String name, RetryPolicy policy, Function1<Integer, Task<U>> taskFunction) {
    RetriableTask<U> retriableTask = new RetriableTask<>(name, policy, taskFunction);
    Task<U> retryTaskWrapper = Task.async(name + " retriableTask", retriableTask::run);
    retryTaskWrapper.getShallowTraceBuilder().setTaskType(TaskType.WITH_RETRY.getName());
    return retryTaskWrapper;
  }

  /** Create a wrapped task with associated recovery task that will retry if necessary. */
  private Task<T> wrap(int attempt) {
    Task<T> retryTask = Task.async(_policy.getName() + ", attempt " + attempt, context -> {
      final SettablePromise<T> result = Promises.settable();

      Task<T> task = _taskFunction.apply(attempt);

      final Task<T> recovery = Task.async(_name + " recovery", recoveryContext -> {
        final SettablePromise<T> recoveryResult = Promises.settable();

        if (task.isFailed()) {
          // Failed task will cause retry to be scheduled.
          ErrorClassification errorClassification = _policy.getErrorClassifier().apply(task.getError());
          retry(attempt + 1, task.getError(), errorClassification, recoveryContext, recoveryResult);
        } else {
          recoveryResult.done(task.get());
        }

        return recoveryResult;
      });

      // Recovery task should run immediately after the original task to process its error.
      recovery.setPriority(Priority.MAX_PRIORITY);
      recovery.getShallowTraceBuilder().setSystemHidden(true);
      Promises.propagateResult(recovery, result);
      context.after(task).run(recovery);
      context.run(task);

      return result;
    });
    retryTask.getShallowTraceBuilder().setTaskType(TaskType.RETRY.getName());
    return retryTask;
  }

  /** Invoke event monitors and schedule a retry if policy allows. */
  private void retry(int attempt, Throwable error, ErrorClassification errorClassification, Context recoveryContext, SettablePromise<T> recoveryResult) {
    long backoffTime = _policy.getBackoffPolicy().nextBackoff(attempt, error);

    if (errorClassification == ErrorClassification.UNRECOVERABLE) {
      // For fatal errors there are no retries.
      LOGGER.debug(String.format("Attempt %s of %s interrupted: %s", attempt, _name, error.getMessage()));
      recoveryResult.fail(error);
    } else if (_policy.getTerminationPolicy().shouldTerminate(attempt, System.currentTimeMillis() - _startedAt + backoffTime)) {
      // Retry policy commands that no more retries should be done.
      LOGGER.debug(String.format("Too many exceptions after attempt %s of %s, aborting: %s", attempt, _name, error.getMessage()));
      recoveryResult.fail(error);
    } else {
      // Schedule a new retry task after a computed backoff timeout.
      LOGGER.debug(String.format("Attempt %s of %s failed and will be retried after %s millis: %s", attempt, _name, backoffTime, error.getMessage()));
      Task<T> retryTask = wrap(attempt);
      Promises.propagateResult(retryTask, recoveryResult);
      recoveryContext.createTimer(backoffTime, TimeUnit.MILLISECONDS, retryTask);
    }
  }

  /** Starts a retriable task */
  private Promise<? extends T> run(Context context) {
    _startedAt = System.currentTimeMillis();
    Task<T> task = wrap(0);
    context.run(task);
    return task;
  }
}


package com.linkedin.restli.client.config;

import java.util.HashSet;
import java.util.Set;

import com.linkedin.parseq.function.Function1;

class ConfigValueCoercers {

  public static final Function1<Object, Long> LONG = val -> {
    if (val instanceof Long)
    {
      return (Long)val;
    }
    else if (val instanceof Integer)
    {
      return (long)(Integer)val;
    }
    else if (val instanceof Short)
    {
      return (long)(Short)val;
    }
    else if (val instanceof String)
    {
      try
      {
        String trimmed = ((String)val).trim();
        return isHexNumber(trimmed) ? Long.decode(trimmed) : Long.valueOf(trimmed);
      }
      catch (NumberFormatException e)
      {
        throw new Exception("Caught error parsing String to Long, String value: " + val, e);
      }
    }
    throw failCoercion(val, Long.class);
  };

  public static Function1<Object, Integer> INTEGER = val -> {
    if (val instanceof Integer)
    {
      return (Integer) val;
    }
    else if (val instanceof Short)
    {
      return (int)(Short)val;
    }
    if (val instanceof String)
    {
      try
      {
        String trimmed = ((String)val).trim();
        return isHexNumber(trimmed) ? Integer.decode(trimmed) : Integer.valueOf(trimmed);
      }
      catch (NumberFormatException e)
      {
        throw new Exception("Caught error parsing String to Integer, String value: " + val, e);
      }
    }
    throw failCoercion(val, Integer.class);
  };

  private static final Set<String> TRUE_VALUES = new HashSet<>(4);
  private static final Set<String> FALSE_VALUES = new HashSet<>(4);
  static {
    TRUE_VALUES.add("true");
    FALSE_VALUES.add("false");

    TRUE_VALUES.add("on");
    FALSE_VALUES.add("off");

    TRUE_VALUES.add("yes");
    FALSE_VALUES.add("no");

    TRUE_VALUES.add("1");
    FALSE_VALUES.add("0");
  }

  public static Function1<Object, Boolean> BOOLEAN = val -> {
    if (val instanceof Boolean)
    {
      return (Boolean) val;
    }
    if (val instanceof String)
    {
      String value = ((String)val).trim();
      if (value.length() == 0)
      {
        return null;
      }
      else if (TRUE_VALUES.contains(value))
      {
        return Boolean.TRUE;
      }
      else if (FALSE_VALUES.contains(value))
      {
        return Boolean.FALSE;
      }
    }
    throw failCoercion(val, Boolean.class);
  };

  /**
   * Determine whether the given value String indicates a hex number, i.e. needs to be
   * passed into Integer.decode instead of Integer.valueOf (etc).
   */
  private static boolean isHexNumber(String value) {
      int index = (value.startsWith("-") ? 1 : 0);
      return (value.startsWith("0x", index) || value.startsWith("0X", index) || value.startsWith("#", index));
  }

  /**
   * Generates a consistent exception that can be used if coercion fails.
   */
  private static Exception failCoercion(final Object object, final Class<?> targetType)
  {
    return new Exception("Could not convert object to " + targetType.getSimpleName() + ". Object is instance of: "
        + object.getClass().getName() + ", value: " + object.toString());
  }

}


package com.linkedin.restli.client;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.linkedin.parseq.batching.BatchingSupport;
import com.linkedin.parseq.internal.ArgumentUtil;
import com.linkedin.r2.message.RequestContext;
import com.linkedin.restli.client.config.RequestConfigProvider;

/**
 * A builder to construct {@link ParSeqRestClient} based on provided configurations.
 *
 * @author Jaroslaw Odzga (jodzga@linkedin.com)
 * @author Min Chen (mnchen@linkedin.com)
 */
public class ParSeqRestliClientBuilder {

  private static final Logger LOGGER = LoggerFactory.getLogger(ParSeqRestliClientBuilder.class);

  private static final String DEFAULT_CONFIG = "default";

  private Client _client;
  private ParSeqRestliClientConfig _config;
  Map<String, ParSeqRestliClientConfig> _configs;
  ParSeqRestliClientConfigChooser _configChooser;
  private boolean _d2RequestTimeoutEnabled = false;

  private BatchingSupport _batchingSupport;
  private InboundRequestContextFinder _inboundRequestContextFinder;
  private Function<Request<?>, RequestContext> _requestContextProvider;

  /**
   * This method may throw RuntimeException e.g. when there is a problem with configuration.
   *
   * @throws RuntimeException e.g. when there is a problem with configuration
   * @return instance of ParSeqRestClient
   */
  public ParSeqRestClient build() {

    if (_inboundRequestContextFinder == null) {
      LOGGER.debug("InboundRequestContextFinder not specified, using default one");
    }
    InboundRequestContextFinder inboundRequestContextFinder = _inboundRequestContextFinder == null ?
        () -> Optional.empty() : _inboundRequestContextFinder;

    if (_config != null) {
      LOGGER.debug("One config specified");
      _configs = Collections.singletonMap(DEFAULT_CONFIG, _config);
      _configChooser = (irc, r) -> DEFAULT_CONFIG;
    } else if (_configs == null) {
      throw new IllegalStateException("One type of config has to be specified using either setConfig() or setMultipleConfigs().");
    } else {
      LOGGER.debug("Multiple configs specified");
    }

    RequestConfigProvider configProvider = new MultipleRequestConfigProvider(_configs, _configChooser, inboundRequestContextFinder);
    Function<Request<?>, RequestContext> requestContextProvider = (_requestContextProvider == null) ?
        request -> new RequestContext() :
        _requestContextProvider;

    ParSeqRestClient parseqClient = new ParSeqRestClient(_client, configProvider, requestContextProvider, _d2RequestTimeoutEnabled);
    if (_batchingSupport != null) {
      LOGGER.debug("Found batching support");
      _batchingSupport.registerStrategy(parseqClient);
    } else {
      LOGGER.debug("Did not find batching support");
    }
    return parseqClient;
  }

  /**
   * Gets the underlying Rest.li client implementation.
   *
   * @deprecated Calling #get in a builder is an anti-pattern
   * @return The underlying Rest.li client
   */
  @Deprecated
  public Client getRestClient() {
    return _client;
  }

  public ParSeqRestliClientBuilder setBatchingSupport(BatchingSupport batchingSupport) {
    _batchingSupport = batchingSupport;
    return this;
  }

  /**
   * Sets the underlying Rest.li client implementation.
   *
   * @param client The underlying Rest.li client
   * @deprecated Use #setClient instead
   * @return The builder itself
   */
  @Deprecated
  public ParSeqRestliClientBuilder setRestClient(RestClient client) {
    ArgumentUtil.requireNotNull(client, "client");
    _client = client;
    return this;
  }

  public ParSeqRestliClientBuilder setClient(Client client) {
    ArgumentUtil.requireNotNull(client, "client");
    _client = client;
    return this;
  }

  public ParSeqRestliClientBuilder setRequestContextProvider(Function<Request<?>, RequestContext> requestContextProvider) {
    ArgumentUtil.requireNotNull(requestContextProvider, "requestContextProvider");
    _requestContextProvider = requestContextProvider;
    return this;
  }

  public ParSeqRestliClientBuilder setConfig(ParSeqRestliClientConfig config) {
    ArgumentUtil.requireNotNull(config, "config");
    if (_configs != null) {
      throw new IllegalArgumentException("setMultipleConfigs() has already been called. Only one type of config can be specified using either setConfig() or setMultipleConfigs() but not both.");
    }
    _config = config;
    return this;
  }

  public ParSeqRestliClientBuilder setMultipleConfigs(Map<String, ParSeqRestliClientConfig> configs,
      ParSeqRestliClientConfigChooser chooser) {
    ArgumentUtil.requireNotNull(configs, "configs");
    ArgumentUtil.requireNotNull(chooser, "chooser");
    if (_configs != null) {
      throw new IllegalArgumentException("setConfig() has already been called. Only one type of config can be specified using either setConfig() or setMultipleConfigs() but not both.");
    }
    _configs = configs;
    _configChooser = chooser;
    return this;
  }

  public ParSeqRestliClientBuilder setInboundRequestContextFinder(InboundRequestContextFinder inboundRequestContextFinder) {
    ArgumentUtil.requireNotNull(inboundRequestContextFinder, "inboundRequestContextFinder");
    _inboundRequestContextFinder = inboundRequestContextFinder;
    return this;
  }

  /**
   * Enables or disables d2 per-request timeout.
   *
   * Once enabled, the timeout used in Parseq can be passed down into rest.li R2D2 layer
   * and free up memory sooner than currently possible. For example, if the server defined
   * requestTimeout is 10s, but the client only wants to wait 250ms, parseq rest client
   * could tell r2d2 to trigger the user callback after 250 ms instead of 10s, which would
   * help us handle the case of slow downstream services, which today cause memory problems
   * for high qps upstream services because more objects are being held longer in memory.
   *
   * @param enabled true if this feature is enabled.
   */
  public ParSeqRestliClientBuilder setD2RequestTimeoutEnabled(boolean enabled) {
    _d2RequestTimeoutEnabled = enabled;
    return this;
  }
}


package com.linkedin.parseq;

import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import com.linkedin.parseq.function.Tuple7;
import com.linkedin.parseq.promise.PromiseException;
import com.linkedin.parseq.promise.PromiseListener;
import com.linkedin.parseq.promise.PromiseUnresolvedException;
import com.linkedin.parseq.trace.ShallowTrace;
import com.linkedin.parseq.trace.ShallowTraceBuilder;
import com.linkedin.parseq.trace.Trace;
import com.linkedin.parseq.trace.TraceBuilder;

public class Tuple7TaskDelegate<T1, T2, T3, T4, T5, T6, T7> implements Tuple7Task<T1, T2, T3, T4, T5, T6, T7> {

  private final Task<Tuple7<T1, T2, T3, T4, T5, T6, T7>> _task;

  public Tuple7TaskDelegate(Task<Tuple7<T1, T2, T3, T4, T5, T6, T7>> task) {
    _task = task;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean cancel(Exception reason) {
    return _task.cancel(reason);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Tuple7<T1, T2, T3, T4, T5, T6, T7> get() throws PromiseException {
    return _task.get();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Throwable getError() throws PromiseUnresolvedException {
    return _task.getError();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Tuple7<T1, T2, T3, T4, T5, T6, T7> getOrDefault(Tuple7<T1, T2, T3, T4, T5, T6, T7> defaultValue) throws PromiseUnresolvedException {
    return _task.getOrDefault(defaultValue);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String getName() {
    return _task.getName();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public int getPriority() {
    return _task.getPriority();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean setPriority(int priority) {
    return _task.setPriority(priority);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void await() throws InterruptedException {
    _task.await();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void contextRun(Context context, Task<?> parent, Collection<Task<?>> predecessors) {
    _task.contextRun(context, parent, predecessors);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean await(long time, TimeUnit unit) throws InterruptedException {
    return _task.await(time, unit);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public ShallowTrace getShallowTrace() {
    return _task.getShallowTrace();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void addListener(PromiseListener<Tuple7<T1, T2, T3, T4, T5, T6, T7>> listener) {
    _task.addListener(listener);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Trace getTrace() {
    return _task.getTrace();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isDone() {
    return _task.isDone();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isFailed() {
    return _task.isFailed();
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Long getId() {
    return _task.getId();
  }

  @Override
  public void setTraceValueSerializer(Function<Tuple7<T1, T2, T3, T4, T5, T6, T7>, String> serializer) {
    _task.setTraceValueSerializer(serializer);
  }

  @Override
  public ShallowTraceBuilder getShallowTraceBuilder() {
    return _task.getShallowTraceBuilder();
  }

  @Override
  public TraceBuilder getTraceBuilder() {
    return _task.getTraceBuilder();
  }

}


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

package com.linkedin.parseq.zk.recipes;

import com.linkedin.parseq.Task;
import com.linkedin.parseq.function.Try;
import com.linkedin.parseq.promise.Promises;
import com.linkedin.parseq.promise.SettablePromise;
import com.linkedin.parseq.zk.client.PlanLocal;
import com.linkedin.parseq.zk.client.ZKClient;
import com.linkedin.parseq.zk.client.ZKUtil;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An inter-process mutual exclusive lock based on Zookeeper recipes:
 * http://zookeeper.apache.org/doc/trunk/recipes.html#sc_recipes_Locks
 *
 * @author Ang Xu
 */
public class ZKLock implements Synchronizable {
  private static final Logger LOG = LoggerFactory.getLogger(ZKLock.class);
  private static final String LOCK_INTERNAL_KEY = "lockInternal";

  private final String _lockPath;
  private final List<ACL> _acls;
  private final ZKClient _zkClient;

  public ZKLock(String lockPath, ZKClient zkClient, List<ACL> acls) {
    _lockPath = lockPath;
    _zkClient = zkClient;
    _acls = acls;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public <T> Task<T> synchronize(Task<T> task, long deadline) {
    return PlanLocal.get(getPlanLocalKey(), LockInternal.class)
        .flatMap(lockInternal -> {
          if (lockInternal != null) {
            // we already acquire the lock, add count only.
            lockInternal._lockCount++;
            return Task.value(lockInternal._lockNode);
          } else {
            // try acquire.
            return acquire(deadline);
          }
        })
        /* run the given task with toTry() */
        .flatMap(unused -> task).toTry()
        /* release the lock and unwind the result */
        .flatMap(result -> release().andThen(unwind(result)));
  }

  /**
   * Unwinds the result from {@link Task#toTry()}.
   *
   * @return task with the raw result.
   */
  private <T> Task<T> unwind(Try<T> result) {
    if (result.isFailed()) {
      return Task.failure(result.getError());
    } else {
      return Task.value(result.get());
    }
  }

  /**
   * Try to acquire the lock within the given deadline.
   */
  private Task<String> acquire(long deadline) {
    final String uuid = UUID.randomUUID().toString();

    return
        /* Step 1: create znode with a pathname of "_locknode_/guid-lock-"
         * and the sequence and ephemeral flags set. */
        safeCreateLockNode(uuid, deadline, false)
            .onFailure(e -> _zkClient.deleteNodeHasUUID(_lockPath, uuid))
            .flatMap(lockNode -> tryAcquire(lockNode, deadline)
                .withTimeout(deadline - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                .onFailure(e -> _zkClient.deleteNode(lockNode)));
  }

  /**
   * Try to release the lock.
   */
  private Task<Void> release() {
    return PlanLocal.get(getPlanLocalKey(), LockInternal.class).flatMap(lockInternal -> {
      // should never be null.
      if (lockInternal == null) {
        LOG.error("LockInternal is null when releasing lock: ", _lockPath);
      } else {
        lockInternal._lockCount--;
        if (lockInternal._lockCount == 0) {

          return PlanLocal.remove(getPlanLocalKey())
              .flatMap(unused ->
                  _zkClient.delete(lockInternal._lockNode, -1)
                      .recover(e -> {
                        _zkClient.deleteNode(lockInternal._lockNode);
                        return null;
                      })
              );
        }
      }
      return Task.value(null);
    });
  }

  private Task<String> tryAcquire(String lockNode, long deadline) {
    return
        /* Step 2: Call getChildren( ) on the lock node without setting the watch flag */
        _zkClient.getChildren(_lockPath)
            .flatMap(children -> {
              children = children.stream().map(child -> ZKUtil.normalizeZKPath(_lockPath, child))
                  .collect(Collectors.toList());
              String node = ZKUtil.findNodeWithNextLowestSN(children, lockNode);
              if (node.equals(lockNode)) {
                /* Step 3: If the pathname created in step 1 has the lowest sequence number suffix,
                 * the client has the lock and the client exits the protocol. */
                return PlanLocal.put(getPlanLocalKey(), new LockInternal(lockNode)).map(unused -> lockNode);
              } else {
                /* Step 4: The client calls exists( ) with the watch flag set on the path
                 * in the lock directory with the next lowest sequence number. */
                Task<Void> wait = Task.async(context -> {
                  AtomicBoolean committed = new AtomicBoolean(false);
                  SettablePromise<Void> promise = Promises.settable();
                  Task<Optional<Stat>> exists = _zkClient.exists(node).withWatcher(watchEvent -> {
                    /* Step 5: Otherwise, wait for a notification for the pathname from the
                     * previous step before going to step 2. */
                    if (committed.compareAndSet(false, true)) {
                      promise.done(null);
                    }
                  }).andThen(stat -> {
                    /* Step 5: if exists( ) returns false, go to step 2. */
                    if (!stat.isPresent() && committed.compareAndSet(false, true)) {
                      promise.done(null);
                    }
                  });
                  context.run(exists);
                  return promise;
                });
                /* got to step 2. */
                return wait.flatMap(unused -> tryAcquire(lockNode, deadline));
              }
            })
            .recoverWith(e -> retry(e, deadline, tryAcquire(lockNode, deadline)));
  }

  private Task<String> safeCreateLockNode(String uuid, long deadline, boolean retry) {
    if (retry) {
      /* If this is a retry, check for a node containing the guid used in the path name. Per:
       * http://zookeeper.apache.org/doc/trunk/recipes.html#sc_recipes_GuidNote */
      return _zkClient.getChildren(_lockPath)
          .map(children -> ZKUtil.findNodeWithUUID(children, uuid))
          .flatMap(node -> {
            if (node == null) {
              return createLockNode(uuid);
            } else {
              return Task.value(node);
            }
          })
          .recoverWith(e -> retry(e, deadline, safeCreateLockNode(uuid, deadline, true)));
    } else {
      return createLockNode(uuid).recoverWith(e -> retry(e, deadline, safeCreateLockNode(uuid, deadline, true)));
    }
  }

  private <T> Task<T> retry(Throwable e, long deadline, Task<T> retryTask) {
    if (System.currentTimeMillis() >= deadline) {
      return Task.failure(
          new TimeoutException("Failed to create lock node under path:" + _lockPath + " before deadline passes"));
    } else if (ZKUtil.isRecoverable(e)) {
      return _zkClient.waitFor(KeeperState.SyncConnected, deadline).flatMap(unused -> retryTask);
    } else {
      return Task.failure(e);
    }
  }

  private Task<String> createLockNode(String uuid) {
    return _zkClient.ensurePathExists(_lockPath)
        .flatMap(path -> _zkClient.create(
            ZKUtil.normalizeZKPath(path, uuid + "-lock-"), /* path to the lock node */
            LockUtil.getLockData(),      /* data */
            _acls, /* ACLs */
            CreateMode.EPHEMERAL_SEQUENTIAL /* CreateMode */));
  }

  private String getPlanLocalKey() {
    return LOCK_INTERNAL_KEY + _lockPath;
  }

  private static class LockInternal {
    private final String _lockNode;
    private long _lockCount;

    public LockInternal(String lockNode) {
      _lockNode = lockNode;
      _lockCount = 1;
    }
  }
}


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

package com.linkedin.parseq;

import com.linkedin.parseq.internal.PlanBasedRateLimiter;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;

import com.linkedin.parseq.internal.ArgumentUtil;
import com.linkedin.parseq.internal.ContextImpl;
import com.linkedin.parseq.internal.ExecutionMonitor;
import com.linkedin.parseq.internal.LIFOBiPriorityQueue;
import com.linkedin.parseq.internal.PlanCompletionListener;
import com.linkedin.parseq.internal.PlanDeactivationListener;
import com.linkedin.parseq.internal.PlatformClock;
import com.linkedin.parseq.internal.SerialExecutor;
import com.linkedin.parseq.internal.SerialExecutor.TaskQueue;
import com.linkedin.parseq.internal.PlanContext;

/**
 * An object that can run a set {@link Task}s. Use {@link EngineBuilder} to
 * create Engine instances.
 *
 * @author Chris Pettitt (cpettitt@linkedin.com)
 * @author Jaroslaw Odzga (jodzga@linkedin.com)
 * @author Min Chen (mnchen@linkedin.com)
 */
public class Engine {
  public static final String LOGGER_BASE = Engine.class.getName();

  // TODO: this constant should be renamed.
  // Need to fix in the next major version release.
  public static final String MAX_RELATIONSHIPS_PER_TRACE = "_MaxRelationshipsPerTrace_";
  private static final int DEFUALT_MAX_RELATIONSHIPS_PER_TRACE = 65536;

  public static final String MAX_CONCURRENT_PLANS = "_MaxConcurrentPlans_";
  private static final int DEFUALT_MAX_CONCURRENT_PLANS = Integer.MAX_VALUE;

  public static final String DRAIN_SERIAL_EXECUTOR_QUEUE = "_DrainSerialExecutorQueue_";
  private static final boolean DEFAULT_DRAIN_SERIAL_EXECUTOR_QUEUE = true;

  public static final String DEFAULT_TASK_QUEUE = "_DefaultTaskQueue_";

  private static final State INIT = new State(StateName.RUN, 0);
  private static final State TERMINATED = new State(StateName.TERMINATED, 0);
  private static final Logger LOG = LoggerFactory.getLogger(LOGGER_BASE);

  public static final String MAX_EXECUTION_MONITORS = "_MaxExecutionMonitors_";
  private static final int DEFAULT_MAX_EXECUTION_MONITORS = 1024;

  public static final String MONITOR_EXECUTION = "_MonitorExecution_";
  private static final boolean DEFAULT_MONITOR_EXECUTION = false;

  public static final String EXECUTION_MONITOR_DURATION_THRESHOLD_NANO = "_ExecutionMonitorDurationThresholdNano_";
  private static final long DEFAULT_EXECUTION_MONITOR_DURATION_THRESHOLD_NANO = TimeUnit.SECONDS.toNanos(1);

  public static final String EXECUTION_MONITOR_CHECK_INTERVAL_NANO = "_ExecutionMonitorCheckIntervaldNano_";
  private static final long DEFAULT_EXECUTION_MONITOR_CHECK_INTERVAL_NANO = TimeUnit.MILLISECONDS.toNanos(10);

  public static final String EXECUTION_MONITOR_IDLE_DURATION_NANO = "_ExecutionMonitorIdleDurationNano_";
  private static final long DEFAULT_EXECUTION_MONITOR_IDLE_DURATION_NANO = TimeUnit.MINUTES.toNanos(1);

  public static final String EXECUTION_MONITOR_LOGGING_INTERVAL_NANO = "_ExecutionMonitorLoggingIntervalNano_";
  private static final long DEFAULT_EXECUTION_MONITOR_LOGGING_INTERVAL_NANO = TimeUnit.MINUTES.toNanos(1);

  public static final String EXECUTION_MONITOR_MIN_STALL_NANO = "_ExecutionMonitorMinStallNano_";
  private static final long DEFAULT_EXECUTION_MONITOR_MIN_STALL_NANO = TimeUnit.MILLISECONDS.toNanos(10);

  public static final String EXECUTION_MONITOR_STALLS_HISTORY_SIZE = "_ExecutionMonitorStallsHistorySize_";
  private static final int DEFAULT_EXECUTION_MONITOR_STALLS_HISTORY_SIZE = 1024;

  public static final String EXECUTION_MONITOR_LOG_LEVEL = "_ExecutionMonitorLogLevel_";
  private static final Level DEFAULT_EXECUTION_MONITOR_LOG_LEVEL = Level.WARN;

  private static enum StateName {
    RUN,
    SHUTDOWN,
    TERMINATED
  }

  private final Executor _taskExecutor;
  private final DelayedExecutor _timerExecutor;
  private final ILoggerFactory _loggerFactory;
  private final TaskQueueFactory _taskQueueFactory;

  private final AtomicReference<State> _stateRef = new AtomicReference<State>(INIT);
  private final CountDownLatch _terminated = new CountDownLatch(1);

  private final Map<String, Object> _properties;

  private final int _maxRelationshipsPerTrace;

  private final int _maxConcurrentPlans;
  private final Semaphore _concurrentPlans;
  private final PlanBasedRateLimiter _planBasedRateLimiter;

  private final boolean _drainSerialExecutorQueue;

  private final ExecutionMonitor _executionMonitor;

  private final PlanDeactivationListener _planDeactivationListener;
  private final PlanCompletionListener _planCompletionListener;

  private final PlanCompletionListener _taskDoneListener;

  // Cache these, since we'll use them frequently and they can be pre-computed.
  private final Logger _allLogger;
  private final Logger _rootLogger;

  /* package private */ Engine(final Executor taskExecutor, final DelayedExecutor timerExecutor,
      final ILoggerFactory loggerFactory, final Map<String, Object> properties,
      final PlanDeactivationListener planActivityListener,
      final PlanCompletionListener planCompletionListener,
      final TaskQueueFactory taskQueueFactory,
      final PlanBasedRateLimiter planClassRateLimiter) {
    _taskExecutor = taskExecutor;
    _timerExecutor = timerExecutor;
    _loggerFactory = loggerFactory;
    _properties = properties;
    _planDeactivationListener = planActivityListener;
    _planBasedRateLimiter = planClassRateLimiter;

    _taskQueueFactory = createTaskQueueFactory(properties, taskQueueFactory);

    _allLogger = loggerFactory.getLogger(LOGGER_BASE + ":all");
    _rootLogger = loggerFactory.getLogger(LOGGER_BASE + ":root");

    if (_properties.containsKey(MAX_RELATIONSHIPS_PER_TRACE)) {
      _maxRelationshipsPerTrace = (Integer) getProperty(MAX_RELATIONSHIPS_PER_TRACE);
    } else {
      _maxRelationshipsPerTrace = DEFUALT_MAX_RELATIONSHIPS_PER_TRACE;
    }

    if (_properties.containsKey(MAX_CONCURRENT_PLANS)) {
      _maxConcurrentPlans = (Integer) getProperty(MAX_CONCURRENT_PLANS);
    } else {
      _maxConcurrentPlans = DEFUALT_MAX_CONCURRENT_PLANS;
    }
    _concurrentPlans = new Semaphore(_maxConcurrentPlans);

    if (_properties.containsKey(DRAIN_SERIAL_EXECUTOR_QUEUE)) {
      _drainSerialExecutorQueue = (Boolean) getProperty(DRAIN_SERIAL_EXECUTOR_QUEUE);
    } else {
      _drainSerialExecutorQueue = DEFAULT_DRAIN_SERIAL_EXECUTOR_QUEUE;
    }

    _taskDoneListener = resolvedPromise -> {
      assert _stateRef.get()._pendingCount > 0;
      assert _stateRef.get()._stateName != StateName.TERMINATED;

      State currState;
      State newState;
      do {
        currState = _stateRef.get();
        newState = new State(currState._stateName, currState._pendingCount - 1);
      } while (!_stateRef.compareAndSet(currState, newState));

      _concurrentPlans.release();
      if (planClassRateLimiter != null) {
        _planBasedRateLimiter.release(resolvedPromise.getPlanClass());
      }

      if (newState._stateName == StateName.SHUTDOWN && newState._pendingCount == 0) {
        tryTransitionTerminate();
      }
    };

    _planCompletionListener = planContext -> {
      try {
        planCompletionListener.onPlanCompleted(planContext);
      } catch (Throwable t) {
        LOG.error("Uncaught throwable from custom PlanCompletionListener.", t);
      } finally {
        _taskDoneListener.onPlanCompleted(planContext);
      }
    };

    int maxMonitors = DEFAULT_MAX_EXECUTION_MONITORS;
    if (_properties.containsKey(MAX_EXECUTION_MONITORS)) {
      maxMonitors = (Integer) getProperty(MAX_EXECUTION_MONITORS);
    }

    long durationThresholdNano = DEFAULT_EXECUTION_MONITOR_DURATION_THRESHOLD_NANO;
    if (_properties.containsKey(EXECUTION_MONITOR_DURATION_THRESHOLD_NANO)) {
      durationThresholdNano = (Long) getProperty(EXECUTION_MONITOR_DURATION_THRESHOLD_NANO);
    }

    long checkIntervalNano = DEFAULT_EXECUTION_MONITOR_CHECK_INTERVAL_NANO;
    if (_properties.containsKey(EXECUTION_MONITOR_CHECK_INTERVAL_NANO)) {
      checkIntervalNano = (Long) getProperty(EXECUTION_MONITOR_CHECK_INTERVAL_NANO);
    }

    long idleDurationNano = DEFAULT_EXECUTION_MONITOR_IDLE_DURATION_NANO;
    if (_properties.containsKey(EXECUTION_MONITOR_IDLE_DURATION_NANO)) {
      idleDurationNano = (Long) getProperty(EXECUTION_MONITOR_IDLE_DURATION_NANO);
    }

    long loggingIntervalNano = DEFAULT_EXECUTION_MONITOR_LOGGING_INTERVAL_NANO;
    if (_properties.containsKey(EXECUTION_MONITOR_LOGGING_INTERVAL_NANO)) {
      loggingIntervalNano = (Long) getProperty(EXECUTION_MONITOR_LOGGING_INTERVAL_NANO);
    }

    long minStallNano = DEFAULT_EXECUTION_MONITOR_MIN_STALL_NANO;
    if (_properties.containsKey(EXECUTION_MONITOR_MIN_STALL_NANO)) {
      minStallNano = (Long) getProperty(EXECUTION_MONITOR_MIN_STALL_NANO);
    }

    int stallsHistorySize = DEFAULT_EXECUTION_MONITOR_STALLS_HISTORY_SIZE;
    if (_properties.containsKey(EXECUTION_MONITOR_STALLS_HISTORY_SIZE)) {
      stallsHistorySize = (Integer) getProperty(EXECUTION_MONITOR_STALLS_HISTORY_SIZE);
    }

    Level level = DEFAULT_EXECUTION_MONITOR_LOG_LEVEL;
    if (_properties.containsKey(EXECUTION_MONITOR_LOG_LEVEL)) {
      level = (Level) getProperty(EXECUTION_MONITOR_LOG_LEVEL);
    }

    boolean monitorExecution = DEFAULT_MONITOR_EXECUTION;
    if (_properties.containsKey(MONITOR_EXECUTION)) {
      monitorExecution = (Boolean) getProperty(MONITOR_EXECUTION);
    }

    _executionMonitor = monitorExecution
        ? new ExecutionMonitor(maxMonitors, durationThresholdNano, checkIntervalNano, idleDurationNano,
            loggingIntervalNano, minStallNano, stallsHistorySize, level, new PlatformClock()) : null;
  }

  @SuppressWarnings({ "rawtypes", "unchecked" })
  private TaskQueueFactory createTaskQueueFactory(final Map<String, Object> properties, final TaskQueueFactory taskQueueFactory) {
    if (taskQueueFactory == null) {
      if (_properties.containsKey(DEFAULT_TASK_QUEUE)) {
        String className = (String) properties.get(DEFAULT_TASK_QUEUE);
        try {
          final Class<? extends SerialExecutor.TaskQueue> clazz =
              (Class<? extends TaskQueue>) Thread.currentThread().getContextClassLoader().loadClass(className);
          return () -> {
            try {
              return clazz.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
              return new LIFOBiPriorityQueue();
            }
          };
        } catch (ClassNotFoundException e) {
          LOG.error("Failed to load TasQueue implementation: " + className + ", will use default implementation", e);
        }
      }
      return LIFOBiPriorityQueue::new;
    } else {
      return taskQueueFactory;
    }
  }

  public Object getProperty(String key) {
    return _properties.get(key);
  }

  private final String defaultPlanClass(final Task<?> task) {
    return task.getClass().getName();
  }

  /**
   * Runs the given task. Task passed in as a parameter becomes a root on a new Plan.
   * All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
   *

   * This method throws {@code IllegalStateException} if Engine does not have capacity to run the task.
   * Engine's capacity is specified by a {@value #MAX_CONCURRENT_PLANS} configuration property. Use
   * {@link EngineBuilder#setEngineProperty(String, Object)} to set this property.
   * For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
   * {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
   *
   * @param task the task to run
   * @throws IllegalStateException
   */
  public void run(final Task<?> task) {
    run(task, defaultPlanClass(task));
  }

  /**
   * Runs the given task. Task passed in as a parameter becomes a root on a new Plan.
   * All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
   *

   * This method throws {@code IllegalStateException} if Engine does not have capacity to run the task.
   * Engine's capacity is specified by a {@value #MAX_CONCURRENT_PLANS} configuration property. Use
   * {@link EngineBuilder#setEngineProperty(String, Object)} to set this property.
   * For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
   * {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
   *
   * @param task the task to run
   * @param planClass string that identifies a "class" of the Plan. Plan class ends up in a ParSeq
   * Trace and can be used to group traces into "classes" when traces are statistically analyzed.
   * @throws IllegalStateException
   */
  public void run(final Task<?> task, final String planClass) {
    if (!tryRun(task, planClass)) {
      throw new IllegalStateException("Starting new plan rejected, exceeded limit of concurrent plans: " + _maxConcurrentPlans);
    }
  }

  /**
   * Runs the given task. Task passed in as a parameter becomes a root on a new Plan.
   * All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
   *

   * This method blocks until Engine has a capacity to run the task. Engine's capacity is
   * specified by a {@value #MAX_CONCURRENT_PLANS} configuration property. Use
   * {@link EngineBuilder#setEngineProperty(String, Object)} to set this property.
   * For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
   * {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
   *
   * @param task the task to run
   */
  public void blockingRun(final Task<?> task) {
    blockingRun(task, defaultPlanClass(task));
  }

  /**
   * Runs the given task. Task passed in as a parameter becomes a root on a new Plan.
   * All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
   *

   * This method blocks until Engine has a capacity to run the task. Engine's capacity is
   * specified by a {@value #MAX_CONCURRENT_PLANS} configuration property. Use
   * {@link EngineBuilder#setEngineProperty(String, Object)} to set this property.
   * For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
   * {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
   *
   * @param task the task to run
   * @param planClass string that identifies a "class" of the Plan. Plan class ends up in a ParSeq
   * Trace and can be used to group traces into "classes" when traces are statistically analyzed.
   */
  public void blockingRun(final Task<?> task, final String planClass) {
    try {
      acquirePermit(planClass);
      runWithPermit(task, planClass);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }

  /**
   * Runs the given task if Engine has a capacity to start new plan as specified by
   * {@value #MAX_CONCURRENT_PLANS} configuration parameter.
   * For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
   * {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
   * Task passed in as a parameter becomes a root on a new Plan.
   * All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
   * This method returns immediately and does not block. It returns {@code true} if Plan was successfully started.
   * @param task the task to run
   * @return true if Plan was started
   */
  public boolean tryRun(final Task<?> task) {
    return tryRun(task,  defaultPlanClass(task));
  }

  /**
   * Runs the given task if Engine has a capacity to start new plan as specified by
   * {@value #MAX_CONCURRENT_PLANS} configuration parameter.
   * For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
   * {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
   * Task passed in as a parameter becomes a root on a new Plan.
   * All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
   * This method returns immediately and does not block. It returns {@code true} if Plan was successfully started.
   * @param task the task to run
   * @param planClass string that identifies a "class" of the Plan
   * @return true if Plan was started
   */
  public boolean tryRun(final Task<?> task, final String planClass) {
    if (tryAcquirePermit(planClass)) {
      runWithPermit(task, planClass);
      return true;
    } else {
      return false;
    }
  }

  /**
   * Runs the given task if Engine has a capacity to start new plan as specified by
   * {@value #MAX_CONCURRENT_PLANS} configuration parameter within specified amount of time.
   * For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
   * {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
   * Task passed in as a parameter becomes a root on a new Plan.
   * All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
   * If Engine does not have capacity to start the task, this method will block up to specified amount of
   * time waiting for other concurrently running Plans to complete. If there is no capacity to start the task
   * within specified amount of time this method will return false. It returns {@code true} if Plan was successfully started.
   * @param task the task to run
   * @param timeout amount of time to wait for Engine's capacity to run the task
   * @param unit
   * @return true if Plan was started within the given waiting time and the current thread has not
   * been {@linkplain Thread#interrupt interrupted}.
   */
  public boolean tryRun(final Task<?> task, final long timeout, final TimeUnit unit) throws InterruptedException {
    return tryRun(task,  defaultPlanClass(task), timeout, unit);
  }

  /**
   * Runs the given task if Engine has a capacity to start new plan as specified by
   * {@value #MAX_CONCURRENT_PLANS} configuration parameter within specified amount of time.
   * For the sake of backwards compatibility default value for a {@value #MAX_CONCURRENT_PLANS} is
   * {@value #DEFUALT_MAX_CONCURRENT_PLANS} which essentially means "unbounded capacity".
   * Task passed in as a parameter becomes a root on a new Plan.
   * All tasks created and started as a consequence of a root task will belong to that plan and will share a Trace.
   * If Engine does not have capacity to start the task, this method will block up to specified amount of
   * time waiting for other concurrently running Plans to complete. If there is no capacity to start the task
   * within specified amount of time this method will return false. It returns {@code true} if Plan was successfully started.
   * @param task the task to run
   * @param planClass string that identifies a "class" of the Plan
   * @param timeout amount of time to wait for Engine's capacity to run the task
   * @param unit
   * @return true if Plan was started within the given waiting time and the current thread has not
   * been {@linkplain Thread#interrupt interrupted}.
  */
  public boolean tryRun(final Task<?> task, final String planClass, final long timeout, final TimeUnit unit) throws InterruptedException {
    if (tryAcquirePermit(planClass, timeout, unit)) {
      runWithPermit(task, planClass);
      return true;
    } else {
      return false;
    }
  }

  /**
   * Runs the given task with its own context. Use {@code Tasks.seq} and
   * {@code Tasks.par} to create and run composite tasks.
   *
   * @param task the task to run
   */
  private void runWithPermit(final Task<?> task, final String planClass) {
    ArgumentUtil.requireNotNull(task, "task");
    ArgumentUtil.requireNotNull(planClass, "planClass");
    State currState, newState;
    do {
      currState = _stateRef.get();
      if (currState._stateName != StateName.RUN) {
        task.cancel(new EngineShutdownException("Task submitted after engine shutdown"));
        return;
      }
      newState = new State(StateName.RUN, currState._pendingCount + 1);
    } while (!_stateRef.compareAndSet(currState, newState));

    PlanContext planContext = new PlanContext(this, _taskExecutor, _timerExecutor, _loggerFactory, _allLogger,
        _rootLogger, planClass, task, _maxRelationshipsPerTrace, _planDeactivationListener, _planCompletionListener,
        _taskQueueFactory.newTaskQueue(), _drainSerialExecutorQueue, _executionMonitor);
    new ContextImpl(planContext, task).runTask();
  }

  // acquire a permit to run the given task plan. always acquire a permit first from engine level
  // rate limiter and then from plan class level rate limit if specified.
  private boolean tryAcquirePermit(String planClass) {
    return _concurrentPlans.tryAcquire() &&
        (_planBasedRateLimiter == null || _planBasedRateLimiter.tryAcquire(planClass));
  }

  private boolean tryAcquirePermit(String planClass, long timeout, TimeUnit unit)
      throws InterruptedException {
    return _concurrentPlans.tryAcquire(timeout, unit) &&
        (_planBasedRateLimiter == null || _planBasedRateLimiter.tryAcquire(planClass, timeout, unit));
  }

  private void acquirePermit(String planClass) throws InterruptedException {
    _concurrentPlans.acquire();
    if (_planBasedRateLimiter != null) {
      _planBasedRateLimiter.acquire(planClass);
    }
  }

  /**
   * If the engine is currently running, this method will initiate an orderly
   * shutdown. No new tasks will be accepted, but already running tasks will be
   * allowed to finish. Use {@link #awaitTermination(int, java.util.concurrent.TimeUnit)}
   * to wait for the engine to shutdown.
   *

   * If the engine is already shutting down or stopped this method will have
   * no effect.
   */
  public void shutdown() {
    if (tryTransitionShutdown()) {
      tryTransitionTerminate();
    }
  }

  /**
   * Returns {@code true} if engine shutdown has been started or if the engine
   * is terminated. Use {@link #isTerminated()} to determine if the engine is
   * actually stopped and {@link #awaitTermination(int, java.util.concurrent.TimeUnit)}
   * to wait for the engine to stop.
   *
   * @return {@code true} if the engine has started shutting down or if it has
   *         finished shutting down.
   */
  public boolean isShutdown() {
    return _stateRef.get()._stateName != StateName.RUN;
  }

  /**
   * Returns {@code true} if the engine has completely stopped. Use
   * {@link #awaitTermination(int, java.util.concurrent.TimeUnit)} to wait for
   * the engine to terminate. Use {@link #shutdown()} to start engine shutdown.
   *
   * @return {@code true} if the engine has completed stopped.
   */
  public boolean isTerminated() {
    return _stateRef.get()._stateName == StateName.TERMINATED;
  }

  /**
   * Waits for the engine to stop. Use {@link #shutdown()} to initiate
   * shutdown.
   *
   * @param time the amount of time to wait
   * @param unit the unit for the time to wait
   * @return {@code true} if shutdown completed within the specified time or
   *         {@code false} if not.
   * @throws InterruptedException if this thread is interrupted while waiting
   *         for the engine to stop.
   */
  public boolean awaitTermination(final int time, final TimeUnit unit) throws InterruptedException {
    return _terminated.await(time, unit);
  }

  private boolean tryTransitionShutdown() {
    State currState, newState;
    do {
      currState = _stateRef.get();
      if (currState._stateName != StateName.RUN) {
        return false;
      }
      newState = new State(StateName.SHUTDOWN, currState._pendingCount);
    } while (!_stateRef.compareAndSet(currState, newState));
    return true;
  }

  private void tryTransitionTerminate() {
    State currState;
    do {
      currState = _stateRef.get();
      if (currState._stateName != StateName.SHUTDOWN || currState._pendingCount != 0) {
        return;
      }
    } while (!_stateRef.compareAndSet(currState, TERMINATED));

    _terminated.countDown();
  }

  private static class State {
    private final StateName _stateName;
    private final long _pendingCount;

    private State(final StateName stateName, final long pendingCount) {
      _pendingCount = pendingCount;
      _stateName = stateName;
    }
  }
}


package com.linkedin.parseq.retry.backoff;

/**
 * A policy that increases the initial backoff duration by repeatedly multiplying by an approximation of the golden
 * ratio (8 / 5, the sixth and fifth fibonacci numbers).
 *
 * @author Oleg Anashkin (oleg.anashkin@gmail.com)
 */
public class FibonacciBackoff implements BackoffPolicy {
  protected final long _backoff;

  /**
   * A policy that increases the initial backoff duration by repeatedly multiplying by an approximation of the golden
   * ratio (8 / 5, the sixth and fifth fibonacci numbers).
   *
   * @param backoff The backoff used for the first retry as well as the base for all subsequent attempts.
   */
  public FibonacciBackoff(final long backoff) {
    _backoff = backoff;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public long nextBackoff(int attempts, Throwable error) {
    return Math.round(Math.pow(1.6, attempts - 1) * _backoff);
  }
}


package com.linkedin.parseq.httpclient;

import java.util.concurrent.atomic.AtomicReference;

import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClientConfig;

public class HttpClient {

  private static final AtomicReference<AsyncHttpClient> _client =
      new AtomicReference<AsyncHttpClient>();

  /**
   * Returns raw http client. If client has not been initialized yet
   * then new client is created with default configuration.
   * @return raw http client
   */
  public static synchronized AsyncHttpClient getNingClient() {
    if (_client.get() == null) {
      initialize(new AsyncHttpClientConfig.Builder().build());
    }
    return _client.get();
  }

  /**
   * Initializes HttpClient with custom configuration.
   * @param cfg client configuration
   * @see AsyncHttpClientConfig
   */
  @SuppressWarnings("resource")
  public static synchronized void initialize(AsyncHttpClientConfig cfg) {
    if (!_client.compareAndSet(null, new AsyncHttpClient(cfg))) {
      throw new RuntimeException("async http client concurrently initialized");
    }
  }

  public static void close() {
    if (_client.get() != null) {
      _client.get().close();
    }
  }

  public static WrappedRequestBuilder get(String url) {
    return new WrappedRequestBuilder(getNingClient().prepareGet(url), "GET " + url);
  }

  public static WrappedRequestBuilder connect(String url) {
    return new WrappedRequestBuilder(getNingClient().prepareConnect(url), "CONNECT " + url);
  }

  public static WrappedRequestBuilder options(String url) {
    return new WrappedRequestBuilder(getNingClient().prepareOptions(url), "OPTIONS " + url);
  }

  public static WrappedRequestBuilder head(String url) {
    return new WrappedRequestBuilder(getNingClient().prepareHead(url), "HEAD " + url);
  }

  public static WrappedRequestBuilder post(String url) {
    return new WrappedRequestBuilder(getNingClient().preparePost(url), "POST " + url);
  }

  public static WrappedRequestBuilder put(String url) {
    return new WrappedRequestBuilder(getNingClient().preparePut(url), "PUT " + url);
  }

  public static WrappedRequestBuilder delete(String url) {
    return new WrappedRequestBuilder(getNingClient().prepareDelete(url), "DELETE " + url);
  }

}


package ru.noties.markwon.ext.tables;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spanned;

import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TableCell;
import org.commonmark.ext.gfm.tables.TableHead;
import org.commonmark.ext.gfm.tables.TableRow;
import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.CustomNode;

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

import ru.noties.markwon.Markwon;

/**
 * A class to parse TableBlock and return a data-structure that is not dependent
 * on commonmark-java table extension. Can be useful when rendering tables require special
 * handling (multiple views, specific table view) for example when used with `markwon-recycler` artifact
 *
 * @see #parse(Markwon, TableBlock)
 * @since 3.0.0
 */
public class Table {

    /**
     * Factory method to obtain an instance of {@link Table}
     *
     * @param markwon    Markwon
     * @param tableBlock TableBlock to parse
     * @return parsed {@link Table} or null
     */
    @Nullable
    public static Table parse(@NonNull Markwon markwon, @NonNull TableBlock tableBlock) {

        final Table table;

        final ParseVisitor visitor = new ParseVisitor(markwon);
        tableBlock.accept(visitor);
        final List<Row> rows = visitor.rows();

        if (rows == null) {
            table = null;
        } else {
            table = new Table(rows);
        }

        return table;
    }

    public static class Row {

        private final boolean isHeader;
        private final List<Column> columns;

        public Row(
                boolean isHeader,
                @NonNull List<Column> columns) {
            this.isHeader = isHeader;
            this.columns = columns;
        }

        public boolean header() {
            return isHeader;
        }

        @NonNull
        public List<Column> columns() {
            return columns;
        }

        @Override
        public String toString() {
            return "Row{" +
                    "isHeader=" + isHeader +
                    ", columns=" + columns +
                    '}';
        }
    }

    public static class Column {

        private final Alignment alignment;
        private final Spanned content;

        public Column(@NonNull Alignment alignment, @NonNull Spanned content) {
            this.alignment = alignment;
            this.content = content;
        }

        @NonNull
        public Alignment alignment() {
            return alignment;
        }

        @NonNull
        public Spanned content() {
            return content;
        }

        @Override
        public String toString() {
            return "Column{" +
                    "alignment=" + alignment +
                    ", content=" + content +
                    '}';
        }
    }

    public enum Alignment {
        LEFT,
        CENTER,
        RIGHT
    }

    private final List<Row> rows;

    public Table(@NonNull List<Row> rows) {
        this.rows = rows;
    }

    @NonNull
    public List<Row> rows() {
        return rows;
    }

    @Override
    public String toString() {
        return "Table{" +
                "rows=" + rows +
                '}';
    }

    static class ParseVisitor extends AbstractVisitor {

        private final Markwon markwon;

        private List<Row> rows;

        private List<Column> pendingRow;
        private boolean pendingRowIsHeader;

        ParseVisitor(@NonNull Markwon markwon) {
            this.markwon = markwon;
        }

        @Nullable
        public List<Row> rows() {
            return rows;
        }

        @Override
        public void visit(CustomNode customNode) {

            if (customNode instanceof TableCell) {

                final TableCell cell = (TableCell) customNode;

                if (pendingRow == null) {
                    pendingRow = new ArrayList<>(2);
                }

                pendingRow.add(new Table.Column(alignment(cell.getAlignment()), markwon.render(cell)));
                pendingRowIsHeader = cell.isHeader();

                return;
            }

            if (customNode instanceof TableHead
                    || customNode instanceof TableRow) {

                visitChildren(customNode);

                // this can happen, ignore such row
                if (pendingRow != null && pendingRow.size() > 0) {

                    if (rows == null) {
                        rows = new ArrayList<>(2);
                    }

                    rows.add(new Table.Row(pendingRowIsHeader, pendingRow));
                }

                pendingRow = null;
                pendingRowIsHeader = false;

                return;
            }

            visitChildren(customNode);
        }

        @NonNull
        private static Table.Alignment alignment(@NonNull TableCell.Alignment alignment) {
            final Table.Alignment out;
            if (TableCell.Alignment.RIGHT == alignment) {
                out = Table.Alignment.RIGHT;
            } else if (TableCell.Alignment.CENTER == alignment) {
                out = Table.Alignment.CENTER;
            } else {
                out = Table.Alignment.LEFT;
            }
            return out;
        }
    }
}


package ru.noties.markwon.sample.customextension;

import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.widget.TextView;

import ru.noties.markwon.Markwon;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.sample.R;

public class CustomExtensionActivity extends Activity {

    // please note that this sample won't work on a device with SDK level < 21
    // as we are using vector drawables for the sake of brevity. Other than resources
    // used, this is fully functional sample on all SDK levels
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_text_view);

        final TextView textView = findViewById(R.id.text_view);

        // note that we haven't registered CorePlugin, as it's the only one that can be
        // implicitly deducted and added automatically. All other plugins require explicit
        // `usePlugin` call
        final Markwon markwon = Markwon.builder(this)
                // try commenting out this line to see runtime dependency resolution
                .usePlugin(ImagesPlugin.create(this))
                .usePlugin(IconPlugin.create(IconSpanProvider.create(this, 0)))
                .build();

        markwon.setMarkdown(textView, getString(R.string.input));
    }
}


package ru.noties.markwon.sample.customextension;

import android.support.annotation.NonNull;
import android.text.TextUtils;

import org.commonmark.node.Node;
import org.commonmark.node.Text;
import org.commonmark.parser.delimiter.DelimiterProcessor;
import org.commonmark.parser.delimiter.DelimiterRun;

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

@SuppressWarnings("WeakerAccess")
public class IconProcessor implements DelimiterProcessor {

    @NonNull
    public static IconProcessor create() {
        return new IconProcessor();
    }

    // ic-home-black-24
    private static final Pattern PATTERN = Pattern.compile("ic-(\\w+)-(\\w+)-(\\d+)");

    private static final String TO_FIND = IconNode.DELIMITER_STRING + "ic-";

    /**
     * Should be used when input string does not wrap icon definition with `@` from both ends.
     * So, `@ic-home-white-24` would become `@ic-home-white-24@`. This way parsing is easier
     * and more predictable (cannot specify multiple ending delimiters, as we would require them:
     * space, newline, end of a document, and a lot of more)
     *
     * @param input to process
     * @return processed string
     * @see #prepare(StringBuilder)
     */
    @NonNull
    public static String prepare(@NonNull String input) {
        final StringBuilder builder = new StringBuilder(input);
        prepare(builder);
        return builder.toString();
    }

    public static void prepare(@NonNull StringBuilder builder) {

        int start = builder.indexOf(TO_FIND);
        int end;

        while (start > -1) {

            end = iconDefinitionEnd(start + TO_FIND.length(), builder);

            // if we match our pattern, append `@` else ignore
            if (iconDefinitionValid(builder.subSequence(start + 1, end))) {
                builder.insert(end, '@');
            }

            // move to next
            start = builder.indexOf(TO_FIND, end);
        }
    }

    @Override
    public char getOpeningCharacter() {
        return IconNode.DELIMITER;
    }

    @Override
    public char getClosingCharacter() {
        return IconNode.DELIMITER;
    }

    @Override
    public int getMinLength() {
        return 1;
    }

    @Override
    public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
        return opener.length() >= 1 && closer.length() >= 1 ? 1 : 0;
    }

    @Override
    public void process(Text opener, Text closer, int delimiterUse) {

        final IconGroupNode iconGroupNode = new IconGroupNode();

        final Node next = opener.getNext();

        boolean handled = false;

        // process only if we have exactly one Text node
        if (next instanceof Text && next.getNext() == closer) {

            final String text = ((Text) next).getLiteral();

            if (!TextUtils.isEmpty(text)) {

                // attempt to match
                final Matcher matcher = PATTERN.matcher(text);
                if (matcher.matches()) {
                    final IconNode iconNode = new IconNode(
                            matcher.group(1),
                            matcher.group(2),
                            matcher.group(3)
                    );
                    iconGroupNode.appendChild(iconNode);
                    next.unlink();
                    handled = true;
                }
            }
        }

        if (!handled) {

            // restore delimiters if we didn't match

            iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING));

            Node node;
            for (Node tmp = opener.getNext(); tmp != null && tmp != closer; tmp = node) {
                node = tmp.getNext();
                // append a child anyway
                iconGroupNode.appendChild(tmp);
            }

            iconGroupNode.appendChild(new Text(IconNode.DELIMITER_STRING));
        }

        opener.insertBefore(iconGroupNode);
    }

    private static int iconDefinitionEnd(int index, @NonNull StringBuilder builder) {

        // all spaces, new lines, non-words or digits,

        char c;

        int end = -1;
        for (int i = index; i < builder.length(); i++) {
            c = builder.charAt(i);
            if (Character.isWhitespace(c)
                    || !(Character.isLetterOrDigit(c) || c == '-' || c == '_')) {
                end = i;
                break;
            }
        }

        if (end == -1) {
            end = builder.length();
        }

        return end;
    }

    private static boolean iconDefinitionValid(@NonNull CharSequence cs) {
        final Matcher matcher = PATTERN.matcher(cs);
        return matcher.matches();
    }
}


package ru.noties.markwon;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.widget.TextView;

import org.commonmark.parser.Parser;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.html.MarkwonHtmlRenderer;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.priority.PriorityProcessor;

/**
 * @since 3.0.0
 */
@SuppressWarnings("WeakerAccess")
class MarkwonBuilderImpl implements Markwon.Builder {

    private final Context context;

    private final List<MarkwonPlugin> plugins = new ArrayList<>(3);

    private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE;

    private PriorityProcessor priorityProcessor;

    MarkwonBuilderImpl(@NonNull Context context) {
        this.context = context;
    }

    @NonNull
    @Override
    public Markwon.Builder bufferType(@NonNull TextView.BufferType bufferType) {
        this.bufferType = bufferType;
        return this;
    }

    @NonNull
    @Override
    public Markwon.Builder usePlugin(@NonNull MarkwonPlugin plugin) {
        plugins.add(plugin);
        return this;
    }

    @NonNull
    @Override
    public Markwon.Builder usePlugins(@NonNull Iterable<? extends MarkwonPlugin> plugins) {

        final Iterator<? extends MarkwonPlugin> iterator = plugins.iterator();

        MarkwonPlugin plugin;

        while (iterator.hasNext()) {
            plugin = iterator.next();
            if (plugin == null) {
                throw new NullPointerException();
            }
            this.plugins.add(plugin);
        }

        return this;
    }

    @SuppressWarnings("UnusedReturnValue")
    @NonNull
    public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) {
        this.priorityProcessor = priorityProcessor;
        return this;
    }

    @NonNull
    @Override
    public Markwon build() {

        if (plugins.isEmpty()) {
            throw new IllegalStateException("No plugins were added to this builder. Use #usePlugin " +
                    "method to add them");
        }

        // this class will sort plugins to match a priority/dependency graph that we have
        PriorityProcessor priorityProcessor = this.priorityProcessor;
        if (priorityProcessor == null) {
            // strictly speaking we do not need updating this field
            // as we are not building this class to be reused between multiple `build` calls
            priorityProcessor = this.priorityProcessor = PriorityProcessor.create();
        }

        // please note that this method must not modify supplied collection
        // if nothing should be done -> the same collection can be returned
        final List<MarkwonPlugin> plugins = preparePlugins(priorityProcessor, this.plugins);

        final Parser.Builder parserBuilder = new Parser.Builder();
        final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
        final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder();
        final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder();
        final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
        final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
        final MarkwonHtmlRenderer.Builder htmlRendererBuilder = MarkwonHtmlRenderer.builder();

        for (MarkwonPlugin plugin : plugins) {
            plugin.configureParser(parserBuilder);
            plugin.configureTheme(themeBuilder);
            plugin.configureImages(asyncDrawableLoaderBuilder);
            plugin.configureConfiguration(configurationBuilder);
            plugin.configureVisitor(visitorBuilder);
            plugin.configureSpansFactory(spanFactoryBuilder);
            plugin.configureHtmlRenderer(htmlRendererBuilder);
        }

        final MarkwonConfiguration configuration = configurationBuilder.build(
                themeBuilder.build(),
                asyncDrawableLoaderBuilder.build(),
                htmlRendererBuilder.build(),
                spanFactoryBuilder.build());

        final RenderProps renderProps = new RenderPropsImpl();

        return new MarkwonImpl(
                bufferType,
                parserBuilder.build(),
                visitorBuilder.build(configuration, renderProps),
                Collections.unmodifiableList(plugins)
        );
    }

    @VisibleForTesting
    @NonNull
    static List<MarkwonPlugin> preparePlugins(
            @NonNull PriorityProcessor priorityProcessor,
            @NonNull List<MarkwonPlugin> plugins) {

        // with this method we will ensure that CorePlugin is added IF and ONLY IF
        // there are plugins that depend on it. If CorePlugin is added, or there are
        // no plugins that require it, CorePlugin won't be added
        final List<MarkwonPlugin> out = ensureImplicitCoreIfHasDependents(plugins);

        return priorityProcessor.process(out);
    }

    // this method will _implicitly_ add CorePlugin if there is at least one plugin
    // that depends on CorePlugin
    @VisibleForTesting
    @NonNull
    static List<MarkwonPlugin> ensureImplicitCoreIfHasDependents(@NonNull List<MarkwonPlugin> plugins) {
        // loop over plugins -> if CorePlugin is found -> break;
        // iterate over all plugins and check if CorePlugin is requested

        boolean hasCore = false;
        boolean hasCoreDependents = false;

        for (MarkwonPlugin plugin : plugins) {

            // here we do not check for exact match (a user could've subclasses CorePlugin
            // and supplied it. In this case we DO NOT implicitly add CorePlugin
            //
            // if core is present already we do not need to iterate anymore -> as nothing
            // will be changed (and we actually do not care if there are any dependents of Core
            // as it's present anyway)
            if (CorePlugin.class.isAssignableFrom(plugin.getClass())) {
                hasCore = true;
                break;
            }

            // if plugin has CorePlugin in dependencies -> mark for addition
            if (!hasCoreDependents) {
                // here we check for direct CorePlugin, if it's not CorePlugin (exact, not a subclass
                // or something -> ignore)
                if (plugin.priority().after().contains(CorePlugin.class)) {
                    hasCoreDependents = true;
                }
            }
        }

        // important thing here is to check if corePlugin is added
        // add it _only_ if it's not present
        if (hasCoreDependents && !hasCore) {
            final List<MarkwonPlugin> out = new ArrayList<>(plugins.size() + 1);
            // add default instance of CorePlugin
            out.add(CorePlugin.create());
            out.addAll(plugins);
            return out;
        }

        return plugins;
    }
}


package ru.noties.markwon.core.spans;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.NonNull;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.LeadingMarginSpan;
import android.widget.TextView;

import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.utils.LeadingMarginUtils;

public class OrderedListItemSpan implements LeadingMarginSpan {

    /**
     * Process supplied `text` argument and supply TextView paint to all OrderedListItemSpans
     * in order for them to measure number.
     *

     * NB, this method must be called before setting text to a TextView (`TextView#setText`
     * internally can trigger new Layout creation which will ask for leading margins right away)
     *
     * @param textView to which markdown will be applied
     * @param text     parsed markdown to process
     * @since 2.0.1
     */
    public static void measure(@NonNull TextView textView, @NonNull CharSequence text) {

        if (!(text instanceof Spanned)) {
            // nothing to do here
            return;
        }

        final OrderedListItemSpan[] spans = ((Spanned) text).getSpans(
                0,
                text.length(),
                OrderedListItemSpan.class);

        if (spans != null) {
            final TextPaint paint = textView.getPaint();
            for (OrderedListItemSpan span : spans) {
                span.margin = (int) (paint.measureText(span.number) + .5F);
            }
        }
    }

    private final MarkwonTheme theme;
    private final String number;
    private final Paint paint = ObjectsPool.paint();

    // we will use this variable to check if our order number text exceeds block margin,
    // so we will use it instead of block margin
    // @since 1.0.3
    private int margin;

    public OrderedListItemSpan(
            @NonNull MarkwonTheme theme,
            @NonNull String number
    ) {
        this.theme = theme;
        this.number = number;
    }

    @Override
    public int getLeadingMargin(boolean first) {
        // @since 2.0.1 we return maximum value of both (now we should measure number before)
        return Math.max(margin, theme.getBlockMargin());
    }

    @Override
    public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {

        // if there was a line break, we don't need to draw anything
        if (!first
                || !LeadingMarginUtils.selfStart(start, text, this)) {
            return;
        }

        paint.set(p);

        theme.applyListItemStyle(paint);

        // if we could force usage of #measure method then we might want skip this measuring here
        // but this won't hold against new values that a TextView can receive (new text size for
        // example...)
        final int numberWidth = (int) (paint.measureText(number) + .5F);

        // @since 1.0.3
        int width = theme.getBlockMargin();
        if (numberWidth > width) {
            // let's keep this logic here in case a user decided not to call #measure and is fine
            // with current implementation
            width = numberWidth;
            margin = numberWidth;
        } else {
            margin = 0;
        }

        final int left;
        if (dir > 0) {
            left = x + (width * dir) - numberWidth;
        } else {
            left = x + (width * dir) + (width - numberWidth);
        }

        // @since 1.1.1 we are using `baseline` argument to position text
        c.drawText(number, left, baseline, paint);
    }
}


package ru.noties.markwon.sample.basicplugins;

import android.app.Activity;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.widget.TextView;

import org.commonmark.node.Heading;
import org.commonmark.node.Paragraph;

import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.Markwon;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonPlugin;
import ru.noties.markwon.MarkwonSpansFactory;
import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.movement.MovementMethodPlugin;
import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.ImageItem;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.SchemeHandler;
import ru.noties.markwon.image.network.NetworkSchemeHandler;

public class BasicPluginsActivity extends Activity {

    private TextView textView;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        textView = new TextView(this);
        setContentView(textView);

        step_1();

        step_2();

        step_3();

        step_4();

        step_5();
    }

    /**
     * In order to apply paragraph spans a custom plugin should be created (CorePlugin will take care
     * of everything else).
     *

     * Please note that when a plugin is registered and it depends on CorePlugin, there is no
     * need to explicitly specify it. By default all plugins that extend AbstractMarkwonPlugin do declare
     * it\'s dependency on CorePlugin ({@link MarkwonPlugin#priority()}).
     *

     * Order in which plugins are specified to the builder is of little importance as long as each
     * plugin clearly states what dependencies it has
     */
    private void step_1() {

        final String markdown = "# Hello!\n\nA paragraph?\n\nIt should be!";

        final Markwon markwon = Markwon.builder(this)
                .usePlugin(new AbstractMarkwonPlugin() {
                    @Override
                    public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
                        builder.setFactory(Paragraph.class, (configuration, props) ->
                                new ForegroundColorSpan(Color.GREEN));
                    }
                })
                .build();

        markwon.setMarkdown(textView, markdown);
    }

    /**
     * To disable some nodes from rendering another custom plugin can be used
     */
    private void step_2() {

        final String markdown = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)";

        final Markwon markwon = Markwon.builder(this)
                .usePlugin(new AbstractMarkwonPlugin() {
                    @Override
                    public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {

                        // for example to disable rendering of heading:
                        // try commenting this out to see that otherwise headings will be rendered
                        builder.on(Heading.class, null);

                        // same method can be used to override existing visitor by specifying
                        // a new NodeVisitor instance
                    }
                })
                .build();

        markwon.setMarkdown(textView, markdown);
    }

    /**
     * To customize core theme plugin can be used again
     */
    private void step_3() {

        final String markdown = "`A code` that is rendered differently\n\n```\nHello!\n```";

        final Markwon markwon = Markwon.builder(this)
                .usePlugin(new AbstractMarkwonPlugin() {
                    @Override
                    public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
                        builder
                                .codeBackgroundColor(Color.BLACK)
                                .codeTextColor(Color.RED);
                    }
                })
                .build();

        markwon.setMarkdown(textView, markdown);
    }

    /**
     * MarkwonConfiguration contains these utilities:
     *

     *
SyntaxHighlight

     *
LinkSpan.Resolver

     *
UrlProcessor

     *
ImageSizeResolver

     *

     *

     * In order to customize them a custom plugin should be used
     */
    private void step_4() {

        final String markdown = "[a link without scheme](github.com)";

        final Markwon markwon = Markwon.builder(this)
                // please note that Markwon does not handle MovementMethod,
                // so if your markdown has links your should apply MovementMethod manually
                // or use MovementMethodPlugin (which uses system LinkMovementMethod by default)
                .usePlugin(MovementMethodPlugin.create())
                .usePlugin(new AbstractMarkwonPlugin() {
                    @Override
                    public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
                        // for example if specified destination has no scheme info, we will
                        // _assume_ that it's network request and append HTTPS scheme
                        builder.urlProcessor(destination -> {
                            final Uri uri = Uri.parse(destination);
                            if (TextUtils.isEmpty(uri.getScheme())) {
                                return "https://" + destination;
                            }
                            return destination;
                        });
                    }
                })
                .build();

        markwon.setMarkdown(textView, markdown);
    }

    /**
     * Images configuration. Can be used with (or without) ImagesPlugin, which does some basic
     * images handling (parsing markdown containing images, obtain an image from network
     * file system or assets). Please note that
     */
    private void step_5() {

        final String markdown = "![image](myownscheme://en.wikipedia.org/static/images/project-logos/enwiki-2x.png)";

        final Markwon markwon = Markwon.builder(this)
                .usePlugin(ImagesPlugin.create(this))
                .usePlugin(new AbstractMarkwonPlugin() {
                    @Override
                    public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) {
                        // we can have a custom SchemeHandler
                        // here we will just use networkSchemeHandler to redirect call
                        builder.addSchemeHandler("myownscheme", new SchemeHandler() {

                            final NetworkSchemeHandler networkSchemeHandler = NetworkSchemeHandler.create();

                            @Nullable
                            @Override
                            public ImageItem handle(@NonNull String raw, @NonNull Uri uri) {
                                raw = raw.replace("myownscheme", "https");
                                return networkSchemeHandler.handle(raw, Uri.parse(raw));
                            }
                        });
                    }
                })
                .build();

        markwon.setMarkdown(textView, markdown);
    }

    // text lifecycle (after/before)
    // rendering lifecycle (before/after)
    // renderProps
    // process
    // priority
}


package ru.noties.markwon.recycler;

import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import org.commonmark.node.Node;

import java.util.List;

import ru.noties.markwon.Markwon;
import ru.noties.markwon.MarkwonReducer;

/**
 * Adapter to display markdown in a RecyclerView. It is done by extracting root blocks from a
 * parsed markdown document (via {@link MarkwonReducer} and rendering each block in a standalone RecyclerView entry. Provides
 * ability to customize rendering of blocks. For example display certain blocks in a horizontal
 * scrolling container or display tables in a specific widget designed for it ({@link Builder#include(Class, Entry)}).
 *
 * @see #builder(int, int)
 * @see #builder(Entry)
 * @see #create(int, int)
 * @see #create(Entry)
 * @see #setMarkdown(Markwon, String)
 * @see #setParsedMarkdown(Markwon, Node)
 * @see #setParsedMarkdown(Markwon, List)
 * @since 3.0.0
 */
public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter.Holder> {

    @NonNull
    public static Builder builderTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) {
        return builder(SimpleEntry.createTextViewIsRoot(defaultEntryLayoutResId));
    }

    /**
     * Factory method to obtain {@link Builder} instance.
     *
     * @see Builder
     */
    @NonNull
    public static Builder builder(
            @LayoutRes int defaultEntryLayoutResId,
            @IdRes int defaultEntryTextViewResId
    ) {
        return builder(SimpleEntry.create(defaultEntryLayoutResId, defaultEntryTextViewResId));
    }

    @NonNull
    public static Builder builder(@NonNull Entry<? extends Node, ? extends Holder> defaultEntry) {
        //noinspection unchecked
        return new MarkwonAdapterImpl.BuilderImpl((Entry<Node, Holder>) defaultEntry);
    }

    @NonNull
    public static MarkwonAdapter createTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) {
        return builderTextViewIsRoot(defaultEntryLayoutResId)
                .build();
    }

    /**
     * Factory method to create a {@link MarkwonAdapter} for evaluation purposes. Resulting
     * adapter will use default layout for all blocks. Default layout has no styling and should
     * be specified explicitly.
     *
     * @see #create(Entry)
     * @see #builder(int, int)
     * @see SimpleEntry
     */
    @NonNull
    public static MarkwonAdapter create(
            @LayoutRes int defaultEntryLayoutResId,
            @IdRes int defaultEntryTextViewResId
    ) {
        return builder(defaultEntryLayoutResId, defaultEntryTextViewResId).build();
    }

    /**
     * Factory method to create a {@link MarkwonAdapter} that uses supplied entry to render all
     * nodes.
     *
     * @param defaultEntry {@link Entry} to be used for node rendering
     * @see #builder(Entry)
     */
    @NonNull
    public static MarkwonAdapter create(@NonNull Entry<? extends Node, ? extends Holder> defaultEntry) {
        return builder(defaultEntry).build();
    }

    /**
     * Builder to create an instance of {@link MarkwonAdapter}
     *
     * @see #include(Class, Entry)
     * @see #reducer(MarkwonReducer)
     * @see #build()
     */
    public interface Builder {

        /**
         * Include a custom {@link Entry} rendering for a Node. Please note that `node` argument
         * must be exact type, as internally there is no validation for inheritance. if multiple
         * nodes should be rendered with the same {@link Entry} they must specify so explicitly.
         * By calling this method for each.
         *
         * @param node  type of the node to register
         * @param entry {@link Entry} to be used for `node` rendering
         * @return self
         */
        @NonNull
        <N extends Node> Builder include(
                @NonNull Class<N> node,
                @NonNull Entry<? super N, ? extends Holder> entry);

        /**
         * Specify how root Node will be reduced to a list of nodes. There is a default
         * {@link MarkwonReducer} that will be used if not provided explicitly (there is no need to
         * register your own unless you require it).
         *
         * @param reducer {@link MarkwonReducer}
         * @return self
         * @see MarkwonReducer
         */
        @NonNull
        Builder reducer(@NonNull MarkwonReducer reducer);

        /**
         * @return {@link MarkwonAdapter}
         */
        @NonNull
        MarkwonAdapter build();
    }

    /**
     * @see SimpleEntry
     */
    public static abstract class Entry<N extends Node, H extends Holder> {

        @NonNull
        public abstract H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent);

        public abstract void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node);

        /**
         * Will be called when new content is available (clear internal cache if any)
         */
        public void clear() {

        }

        public long id(@NonNull N node) {
            return node.hashCode();
        }

        public void onViewRecycled(@NonNull H holder) {

        }
    }

    public abstract void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown);

    public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document);

    public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List<Node> nodes);

    public abstract int getNodeViewType(@NonNull Class<? extends Node> node);

    @SuppressWarnings("WeakerAccess")
    public static class Holder extends RecyclerView.ViewHolder {

        public Holder(@NonNull View itemView) {
            super(itemView);
        }

        // please note that this method should be called after constructor
        @Nullable
        protected <V extends View> V findView(@IdRes int id) {
            return itemView.findViewById(id);
        }

        // please note that this method should be called after constructor
        @NonNull
        protected <V extends View> V requireView(@IdRes int id) {
            final V v = itemView.findViewById(id);
            if (v == null) {
                final String name;
                if (id == 0
                        || id == View.NO_ID) {
                    name = String.valueOf(id);
                } else {
                    name = "R.id." + itemView.getResources().getResourceName(id);
                }
                throw new NullPointerException(String.format("No view with id(R.id.%s) is found " +
                        "in layout: %s", name, itemView));
            }
            return v;
        }
    }
}


package ru.noties.markwon.priority;

import android.support.annotation.NonNull;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ru.noties.markwon.MarkwonPlugin;

import static java.lang.Math.max;

class PriorityProcessorImpl extends PriorityProcessor {

    @NonNull
    @Override
    public List<MarkwonPlugin> process(@NonNull List<MarkwonPlugin> in) {

        // create new collection based on supplied argument
        final List<MarkwonPlugin> plugins = new ArrayList<>(in);

        final int size = plugins.size();

        final Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map =
                new HashMap<>(size);

        for (MarkwonPlugin plugin : plugins) {
            if (map.put(plugin.getClass(), new HashSet<>(plugin.priority().after())) != null) {
                throw new IllegalStateException(String.format("Markwon duplicate plugin " +
                        "found `%s`: %s", plugin.getClass().getName(), plugin));
            }
        }

        final Map<MarkwonPlugin, Integer> cache = new HashMap<>(size);
        for (MarkwonPlugin plugin : plugins) {
            cache.put(plugin, eval(plugin, map));
        }

        Collections.sort(plugins, new PriorityComparator(cache));

        return plugins;
    }

    private static int eval(
            @NonNull MarkwonPlugin plugin,
            @NonNull Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map) {

        final Set<Class<? extends MarkwonPlugin>> set = map.get(plugin.getClass());

        // no dependencies
        if (set.isEmpty()) {
            return 0;
        }

        final Class<? extends MarkwonPlugin> who = plugin.getClass();

        int max = 0;

        for (Class<? extends MarkwonPlugin> dependency : set) {
            max = max(max, eval(who, dependency, map));
        }

        return 1 + max;
    }

    // we need to count the number of steps to a root node (which has no parents)
    private static int eval(
            @NonNull Class<? extends MarkwonPlugin> who,
            @NonNull Class<? extends MarkwonPlugin> plugin,
            @NonNull Map<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> map) {

        // exact match
        Set<Class<? extends MarkwonPlugin>> set = map.get(plugin);

        if (set == null) {

            // let's try to find inexact type (overridden/subclassed)
            for (Map.Entry<Class<? extends MarkwonPlugin>, Set<Class<? extends MarkwonPlugin>>> entry : map.entrySet()) {
                if (plugin.isAssignableFrom(entry.getKey())) {
                    set = entry.getValue();
                    break;
                }
            }

            if (set == null) {
                // unsatisfied dependency
                throw new IllegalStateException(String.format("Markwon unsatisfied dependency found. " +
                                "Plugin `%s` comes after `%s` but it is not added.",
                        who.getName(), plugin.getName()));
            }
        }

        if (set.isEmpty()) {
            return 0;
        }

        int value = 1;

        for (Class<? extends MarkwonPlugin> dependency : set) {

            // a case when a plugin defines `Priority.after(getClass)` or being
            // referenced by own dependency (even indirect)
            if (who.equals(dependency)) {
                throw new IllegalStateException(String.format("Markwon plugin `%s` defined self " +
                        "as a dependency or being referenced by own dependency (cycle)", who.getName()));
            }

            value += eval(who, dependency, map);
        }

        return value;
    }

    private static class PriorityComparator implements Comparator<MarkwonPlugin> {

        private final Map<MarkwonPlugin, Integer> map;

        PriorityComparator(@NonNull Map<MarkwonPlugin, Integer> map) {
            this.map = map;
        }

        @Override
        public int compare(MarkwonPlugin o1, MarkwonPlugin o2) {
            return map.get(o1).compareTo(map.get(o2));
        }
    }
}