/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hadoop.registry.client.impl.zk;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.curator.ensemble.EnsembleProvider;
import org.apache.curator.ensemble.fixed.FixedEnsembleProvider;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.api.CreateBuilder;
import org.apache.curator.framework.api.DeleteBuilder;
import org.apache.curator.framework.api.GetChildrenBuilder;
import org.apache.curator.retry.BoundedExponentialBackoffRetry;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileAlreadyExistsException;
import org.apache.hadoop.fs.PathIsNotEmptyDirectoryException;
import org.apache.hadoop.fs.PathNotFoundException;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.service.CompositeService;
import org.apache.hadoop.service.ServiceStateException;
import org.apache.hadoop.registry.client.api.RegistryConstants;
import org.apache.hadoop.registry.client.binding.RegistryPathUtils;
import org.apache.hadoop.registry.client.exceptions.AuthenticationFailedException;
import org.apache.hadoop.registry.client.exceptions.NoChildrenForEphemeralsException;
import org.apache.hadoop.registry.client.exceptions.NoPathPermissionsException;
import org.apache.hadoop.registry.client.exceptions.RegistryIOException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.List;

/**
 * This service binds to Zookeeper via Apache Curator. It is more
 * generic than just the YARN service registry; it does not implement
 * any of the Registry Operations API.
 */
@InterfaceAudience.Private
@InterfaceStability.Evolving
public class CuratorService extends CompositeService
    implements RegistryConstants, RegistryBindingSource {

  private static final Logger LOG =
      LoggerFactory.getLogger(CuratorService.class);

  /**
   * the Curator binding
   */
  private CuratorFramework curator;

  /**
   * Path to the registry root
   */
  private String registryRoot;

  /**
   * Supplied binding source. This defaults to being this
   * service itself.
   */
  private final RegistryBindingSource bindingSource;

  /**
   * Security service
   */
  private RegistrySecurity registrySecurity;

  /**
   * the connection binding text for messages
   */
  private String connectionDescription;

  /**
   * Security connection diagnostics
   */
  private String securityConnectionDiagnostics = "";

  /**
   * Provider of curator "ensemble"; offers a basis for
   * more flexible bonding in future.
   */
  private EnsembleProvider ensembleProvider;

  /**
   * Construct the service.
   * @param name service name
   * @param bindingSource source of binding information.
   * If null: use this instance
   */
  public CuratorService(String name, RegistryBindingSource bindingSource) {
    super(name);
    if (bindingSource != null) {
      this.bindingSource = bindingSource;
    } else {
      this.bindingSource = this;
    }
  }

  /**
   * Create an instance using this service as the binding source (i.e. read
   * configuration options from the registry)
   * @param name service name
   */
  public CuratorService(String name) {
    this(name, null);
  }

  /**
   * Init the service.
   * This is where the security bindings are set up
   * @param conf configuration of the service
   * @throws Exception
   */
  @Override
  protected void serviceInit(Configuration conf) throws Exception {

    registryRoot = conf.getTrimmed(KEY_REGISTRY_ZK_ROOT,
        DEFAULT_ZK_REGISTRY_ROOT);

    // create and add the registy service
    registrySecurity = new RegistrySecurity("registry security");
    addService(registrySecurity);

    if (LOG.isDebugEnabled()) {
      LOG.debug("Creating Registry with root {}", registryRoot);
    }

    super.serviceInit(conf);
  }

  /**
   * Start the service.
   * This is where the curator instance is started.
   * @throws Exception
   */
  @Override
  protected void serviceStart() throws Exception {
    super.serviceStart();

    // create the curator; rely on the registry security code
    // to set up the JVM context and curator
    curator = createCurator();
  }

  /**
   * Close the ZK connection if it is open
   */
  @Override
  protected void serviceStop() throws Exception {
    IOUtils.closeStream(curator);
    super.serviceStop();
  }

  /**
   * Internal check that a service is in the live state
   * @throws ServiceStateException if not
   */
  private void checkServiceLive() throws ServiceStateException {
    if (!isInState(STATE.STARTED)) {
      throw new ServiceStateException(
          "Service " + getName() + " is in wrong state: "
          + getServiceState());
    }
  }

  /**
   * Flag to indicate whether or not the registry is secure.
   * Valid once the service is inited.
   * @return service security policy
   */
  public boolean isSecure() {
    return registrySecurity.isSecureRegistry();
  }

  /**
   * Get the registry security helper
   * @return the registry security helper
   */
  protected RegistrySecurity getRegistrySecurity() {
    return registrySecurity;
  }

  /**
   * Build the security diagnostics string
   * @return a string for diagnostics
   */
  protected String buildSecurityDiagnostics() {
    // build up the security connection diags
    if (!isSecure()) {
      return "security disabled";
    } else {
      StringBuilder builder = new StringBuilder();
      builder.append("secure cluster; ");
      builder.append(registrySecurity.buildSecurityDiagnostics());
      return builder.toString();
    }
  }

  /**
   * Create a new curator instance off the root path; using configuration
   * options provided in the service configuration to set timeouts and
   * retry policy.
   * @return the newly created creator
   */
  private CuratorFramework createCurator() throws IOException {
    Configuration conf = getConfig();
    createEnsembleProvider();
    int sessionTimeout = conf.getInt(KEY_REGISTRY_ZK_SESSION_TIMEOUT,
        DEFAULT_ZK_SESSION_TIMEOUT);
    int connectionTimeout = conf.getInt(KEY_REGISTRY_ZK_CONNECTION_TIMEOUT,
        DEFAULT_ZK_CONNECTION_TIMEOUT);
    int retryTimes = conf.getInt(KEY_REGISTRY_ZK_RETRY_TIMES,
        DEFAULT_ZK_RETRY_TIMES);
    int retryInterval = conf.getInt(KEY_REGISTRY_ZK_RETRY_INTERVAL,
        DEFAULT_ZK_RETRY_INTERVAL);
    int retryCeiling = conf.getInt(KEY_REGISTRY_ZK_RETRY_CEILING,
        DEFAULT_ZK_RETRY_CEILING);

    if (LOG.isDebugEnabled()) {
      LOG.debug("Creating CuratorService with connection {}",
          connectionDescription);
    }
    CuratorFramework framework;

    synchronized (CuratorService.class) {
      // set the security options

      // build up the curator itself
      CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
      builder.ensembleProvider(ensembleProvider)
       .connectionTimeoutMs(connectionTimeout)
       .sessionTimeoutMs(sessionTimeout)

       .retryPolicy(new BoundedExponentialBackoffRetry(retryInterval,
           retryCeiling,
           retryTimes));

      // set up the builder AND any JVM context
      registrySecurity.applySecurityEnvironment(builder);
      //log them
      securityConnectionDiagnostics = buildSecurityDiagnostics();
      framework = builder.build();
      framework.start();
    }

    return framework;
  }

  @Override
  public String toString() {
    return super.toString()
           + " " + bindingDiagnosticDetails();
  }

  /**
   * Get the binding diagnostics
   * @return a diagnostics string valid after the service is started.
   */
  public String bindingDiagnosticDetails() {
    return " Connection=\"" + connectionDescription + "\""
           + " root=\"" + registryRoot + "\""
           + " " + securityConnectionDiagnostics;
  }

  /**
   * Create a full path from the registry root and the supplied subdir
   * @param path path of operation
   * @return an absolute path
   * @throws IllegalArgumentException if the path is invalide
   */
  protected String createFullPath(String path) throws IOException {
    return RegistryPathUtils.createFullPath(registryRoot, path);
  }

  /**
   * Get the registry binding source ... this can be used to
   * create new ensemble providers
   * @return the registry binding source in use
   */
  public RegistryBindingSource getBindingSource() {
    return bindingSource;
  }

  /**
   * Create the ensemble provider for this registry, by invoking
   * {@link RegistryBindingSource#supplyBindingInformation()} on
   * the provider stored in {@link #bindingSource}
   * Sets {@link #ensembleProvider} to that value;
   * sets {@link #connectionDescription} to the binding info
   * for use in toString and logging;
   *
   */
  protected void createEnsembleProvider() {
    BindingInformation binding = bindingSource.supplyBindingInformation();
    connectionDescription = binding.description
                            + " " + securityConnectionDiagnostics;
    ensembleProvider = binding.ensembleProvider;
  }

  /**
   * Supply the binding information.
   * This implementation returns a fixed ensemble bonded to
   * the quorum supplied by {@link #buildConnectionString()}
   * @return the binding information
   */
  @Override
  public BindingInformation supplyBindingInformation() {
    BindingInformation binding = new BindingInformation();
    String connectString = buildConnectionString();
    binding.ensembleProvider = new FixedEnsembleProvider(connectString);
    binding.description =
        "fixed ZK quorum \"" + connectString + "\"";
    return binding;
  }

  /**
   * Override point: get the connection string used to connect to
   * the ZK service
   * @return a registry quorum
   */
  protected String buildConnectionString() {
    return getConfig().getTrimmed(KEY_REGISTRY_ZK_QUORUM,
        DEFAULT_REGISTRY_ZK_QUORUM);
  }

  /**
   * Create an IOE when an operation fails
   * @param path path of operation
   * @param operation operation attempted
   * @param exception caught the exception caught
   * @return an IOE to throw that contains the path and operation details.
   */
  protected IOException operationFailure(String path,
      String operation,
      Exception exception) {
    return operationFailure(path, operation, exception, null);
  }

  /**
   * Create an IOE when an operation fails
   * @param path path of operation
   * @param operation operation attempted
   * @param exception caught the exception caught
   * @return an IOE to throw that contains the path and operation details.
   */
  protected IOException operationFailure(String path,
      String operation,
      Exception exception,
      List<ACL> acls) {
    IOException ioe;
    String aclList = "[" + RegistrySecurity.aclsToString(acls) + "]";
    if (exception instanceof KeeperException.NoNodeException) {
      ioe = new PathNotFoundException(path);
    } else if (exception instanceof KeeperException.NodeExistsException) {
      ioe = new FileAlreadyExistsException(path);
    } else if (exception instanceof KeeperException.NoAuthException) {
      ioe = new NoPathPermissionsException(path,
          "Not authorized to access path; ACLs: " + aclList);
    } else if (exception instanceof KeeperException.NotEmptyException) {
      ioe = new PathIsNotEmptyDirectoryException(path);
    } else if (exception instanceof KeeperException.AuthFailedException) {
      ioe = new AuthenticationFailedException(path,
          "Authentication Failed: " + exception
          + "; " + securityConnectionDiagnostics,
          exception);
    } else if (exception instanceof KeeperException.NoChildrenForEphemeralsException) {
      ioe = new NoChildrenForEphemeralsException(path,
          "Cannot create a path under an ephemeral node: " + exception,
          exception);
    } else if (exception instanceof KeeperException.InvalidACLException) {
      // this is a security exception of a kind
      // include the ACLs to help the diagnostics
      StringBuilder builder = new StringBuilder();
      builder.append("Path access failure ").append(aclList);
      builder.append(" ");
      builder.append(securityConnectionDiagnostics);
      ioe = new NoPathPermissionsException(path, builder.toString());
    } else {
      ioe = new RegistryIOException(path,
          "Failure of " + operation + " on " + path + ": " +
          exception.toString(),
          exception);
    }
    if (ioe.getCause() == null) {
      ioe.initCause(exception);
    }
    return ioe;
  }

  /**
   * Create a path if it does not exist.
   * The check is poll + create; there's a risk that another process
   * may create the same path before the create() operation is executed/
   * propagated to the ZK node polled.
   *
   * @param path path to create
   * @param acl ACL for path -used when creating a new entry
   * @param createParents flag to trigger parent creation
   * @return true iff the path was created
   * @throws IOException
   */
  @VisibleForTesting
  public boolean maybeCreate(String path,
      CreateMode mode,
      List<ACL> acl,
      boolean createParents) throws IOException {
    return zkMkPath(path, mode, createParents, acl);
  }

  /**
   * Stat the file
   * @param path path of operation
   * @return a curator stat entry
   * @throws IOException on a failure
   * @throws PathNotFoundException if the path was not found
   */
  public Stat zkStat(String path) throws IOException {
    checkServiceLive();
    String fullpath = createFullPath(path);
    Stat stat;
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Stat {}", fullpath);
      }
      stat = curator.checkExists().forPath(fullpath);
    } catch (Exception e) {
      throw operationFailure(fullpath, "read()", e);
    }
    if (stat == null) {
      throw new PathNotFoundException(path);
    }
    return stat;
  }

  /**
   * Get the ACLs of a path
   * @param path path of operation
   * @return a possibly empty list of ACLs
   * @throws IOException
   */
  public List<ACL> zkGetACLS(String path) throws IOException {
    checkServiceLive();
    String fullpath = createFullPath(path);
    List<ACL> acls;
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("GetACLS {}", fullpath);
      }
      acls = curator.getACL().forPath(fullpath);
    } catch (Exception e) {
      throw operationFailure(fullpath, "read()", e);
    }
    if (acls == null) {
      throw new PathNotFoundException(path);
    }
    return acls;
  }

  /**
   * Probe for a path existing
   * @param path path of operation
   * @return true if the path was visible from the ZK server
   * queried.
   * @throws IOException on any exception other than
   * {@link PathNotFoundException}
   */
  public boolean zkPathExists(String path) throws IOException {
    checkServiceLive();
    try {
      // if zkStat(path) returns without throwing an exception, the return value
      // is guaranteed to be not null
      zkStat(path);
      return true;
    } catch (PathNotFoundException e) {
      return false;
    } catch (IOException e) {
      throw e;
    }
  }

  /**
   * Verify a path exists
   * @param path path of operation
   * @throws PathNotFoundException if the path is absent
   * @throws IOException
   */
  public String zkPathMustExist(String path) throws IOException {
    zkStat(path);
    return path;
  }

  /**
   * Create a directory. It is not an error if it already exists
   * @param path path to create
   * @param mode mode for path
   * @param createParents flag to trigger parent creation
   * @param acls ACL for path
   * @throws IOException any problem
   */
  public boolean zkMkPath(String path,
      CreateMode mode,
      boolean createParents,
      List<ACL> acls)
      throws IOException {
    checkServiceLive();
    path = createFullPath(path);
    if (acls == null || acls.isEmpty()) {
      throw new NoPathPermissionsException(path, "Empty ACL list");
    }

    try {
      RegistrySecurity.AclListInfo aclInfo =
          new RegistrySecurity.AclListInfo(acls);
      if (LOG.isDebugEnabled()) {
        LOG.debug("Creating path {} with mode {} and ACL {}",
            path, mode, aclInfo);
      }
      CreateBuilder createBuilder = curator.create();
      createBuilder.withMode(mode).withACL(acls);
      if (createParents) {
        createBuilder.creatingParentsIfNeeded();
      }
      createBuilder.forPath(path);

    } catch (KeeperException.NodeExistsException e) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("path already present: {}", path, e);
      }
      return false;
    } catch (Exception e) {
      throw operationFailure(path, "mkdir() ", e, acls);
    }
    return true;
  }

  /**
   * Recursively make a path
   * @param path path to create
   * @param acl ACL for path
   * @throws IOException any problem
   */
  public void zkMkParentPath(String path,
      List<ACL> acl) throws
      IOException {
    // split path into elements

    zkMkPath(RegistryPathUtils.parentOf(path),
        CreateMode.PERSISTENT, true, acl);
  }

  /**
   * Create a path with given data. byte[0] is used for a path
   * without data
   * @param path path of operation
   * @param data initial data
   * @param acls
   * @throws IOException
   */
  public void zkCreate(String path,
      CreateMode mode,
      byte[] data,
      List<ACL> acls) throws IOException {
    Preconditions.checkArgument(data != null, "null data");
    checkServiceLive();
    String fullpath = createFullPath(path);
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Creating {} with {} bytes of data and ACL {}",
            fullpath, data.length,
            new RegistrySecurity.AclListInfo(acls));
      }
      curator.create().withMode(mode).withACL(acls).forPath(fullpath, data);
    } catch (Exception e) {
      throw operationFailure(fullpath, "create()", e, acls);
    }
  }

  /**
   * Update the data for a path
   * @param path path of operation
   * @param data new data
   * @throws IOException
   */
  public void zkUpdate(String path, byte[] data) throws IOException {
    Preconditions.checkArgument(data != null, "null data");
    checkServiceLive();
    path = createFullPath(path);
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Updating {} with {} bytes", path, data.length);
      }
      curator.setData().forPath(path, data);
    } catch (Exception e) {
      throw operationFailure(path, "update()", e);
    }
  }

  /**
   * Create or update an entry
   * @param path path
   * @param data data
   * @param acl ACL for path -used when creating a new entry
   * @param overwrite enable overwrite
   * @throws IOException
   * @return true if the entry was created, false if it was simply updated.
   */
  public boolean zkSet(String path,
      CreateMode mode,
      byte[] data,
      List<ACL> acl, boolean overwrite) throws IOException {
    Preconditions.checkArgument(data != null, "null data");
    checkServiceLive();
    if (!zkPathExists(path)) {
      zkCreate(path, mode, data, acl);
      return true;
    } else {
      if (overwrite) {
        zkUpdate(path, data);
        return false;
      } else {
        throw new FileAlreadyExistsException(path);
      }
    }
  }

  /**
   * Delete a directory/directory tree.
   * It is not an error to delete a path that does not exist
   * @param path path of operation
   * @param recursive flag to trigger recursive deletion
   * @param backgroundCallback callback; this being set converts the operation
   * into an async/background operation.
   * task
   * @throws IOException on problems other than no-such-path
   */
  public void zkDelete(String path,
      boolean recursive,
      BackgroundCallback backgroundCallback) throws IOException {
    checkServiceLive();
    String fullpath = createFullPath(path);
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Deleting {}", fullpath);
      }
      DeleteBuilder delete = curator.delete();
      if (recursive) {
        delete.deletingChildrenIfNeeded();
      }
      if (backgroundCallback != null) {
        delete.inBackground(backgroundCallback);
      }
      delete.forPath(fullpath);
    } catch (KeeperException.NoNodeException e) {
      // not an error
    } catch (Exception e) {
      throw operationFailure(fullpath, "delete()", e);
    }
  }

  /**
   * List all children of a path
   * @param path path of operation
   * @return a possibly empty list of children
   * @throws IOException
   */
  public List<String> zkList(String path) throws IOException {
    checkServiceLive();
    String fullpath = createFullPath(path);
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("ls {}", fullpath);
      }
      GetChildrenBuilder builder = curator.getChildren();
      List<String> children = builder.forPath(fullpath);
      return children;
    } catch (Exception e) {
      throw operationFailure(path, "ls()", e);
    }
  }

  /**
   * Read data on a path
   * @param path path of operation
   * @return the data
   * @throws IOException read failure
   */
  public byte[] zkRead(String path) throws IOException {
    checkServiceLive();
    String fullpath = createFullPath(path);
    try {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Reading {}", fullpath);
      }
      return curator.getData().forPath(fullpath);
    } catch (Exception e) {
      throw operationFailure(fullpath, "read()", e);
    }
  }

  /**
   * Return a path dumper instance which can do a full dump
   * of the registry tree in its <code>toString()</code>
   * operation
   * @return a class to dump the registry
   * @param verbose verbose flag - includes more details (such as ACLs)
   */
  public ZKPathDumper dumpPath(boolean verbose) {
    return new ZKPathDumper(curator, registryRoot, verbose);
  }

  /**
   * Add a new write access entry for all future write operations.
   * @param id ID to use
   * @param pass password
   * @throws IOException on any failure to build the digest
   */
  public boolean addWriteAccessor(String id, String pass) throws IOException {
    RegistrySecurity security = getRegistrySecurity();
    ACL digestACL = new ACL(ZooDefs.Perms.ALL,
        security.toDigestId(security.digest(id, pass)));
    return security.addDigestACL(digestACL);
  }

  /**
   * Clear all write accessors
   */
  public void clearWriteAccessors() {
    getRegistrySecurity().resetDigestACLs();
  }


  /**
   * Diagnostics method to dump a registry robustly.
   * Any exception raised is swallowed
   * @param verbose verbose path dump
   * @return the registry tree
   */
  protected String dumpRegistryRobustly(boolean verbose) {
    try {
      ZKPathDumper pathDumper = dumpPath(verbose);
      return pathDumper.toString();
    } catch (Exception e) {
      // ignore
      LOG.debug("Ignoring exception:  {}", e);
    }
    return "";
  }
}