/**
 * 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. See accompanying LICENSE file.
 */
package org.apache.hadoop.security.authentication.util;

import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContext;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An abstract SignerSecretProvider that can be use used as the base for a
 * rolling secret.  The secret will roll over at the same interval as the token
 * validity, so there are only ever a maximum of two valid secrets at any
 * given time.  This class handles storing and returning the secrets, as well
 * as the rolling over.  At a minimum, subclasses simply need to implement the
 * generateNewSecret() method.  More advanced implementations can override
 * other methods to provide more advanced behavior, but should be careful when
 * doing so.
 */
@InterfaceStability.Unstable
@InterfaceAudience.Private
public abstract class RolloverSignerSecretProvider
    extends SignerSecretProvider {

  private static Logger LOG = LoggerFactory.getLogger(
    RolloverSignerSecretProvider.class);
  /**
   * Stores the currently valid secrets.  The current secret is the 0th element
   * in the array.
   */
  private volatile byte[][] secrets;
  private ScheduledExecutorService scheduler;
  private boolean schedulerRunning;
  private boolean isDestroyed;

  public RolloverSignerSecretProvider() {
    schedulerRunning = false;
    isDestroyed = false;
  }

  /**
   * Initialize the SignerSecretProvider.  It initializes the current secret
   * and starts the scheduler for the rollover to run at an interval of
   * tokenValidity.
   * @param config configuration properties
   * @param servletContext servlet context
   * @param tokenValidity The amount of time a token is valid for
   * @throws Exception thrown if an error occurred
   */
  @Override
  public void init(Properties config, ServletContext servletContext,
          long tokenValidity) throws Exception {
    initSecrets(generateNewSecret(), null);
    startScheduler(tokenValidity, tokenValidity);
  }

  /**
   * Initializes the secrets array.  This should typically be called only once,
   * during init but some implementations may wish to call it other times.
   * previousSecret can be null if there isn't a previous secret, but
   * currentSecret should never be null.
   * @param currentSecret The current secret
   * @param previousSecret The previous secret
   */
  protected void initSecrets(byte[] currentSecret, byte[] previousSecret) {
    secrets = new byte[][]{currentSecret, previousSecret};
  }

  /**
   * Starts the scheduler for the rollover to run at an interval.
   * @param initialDelay The initial delay in the rollover in milliseconds
   * @param period The interval for the rollover in milliseconds
   */
  protected synchronized void startScheduler(long initialDelay, long period) {
    if (!schedulerRunning) {
      schedulerRunning = true;
      scheduler = Executors.newSingleThreadScheduledExecutor();
      scheduler.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
          rollSecret();
        }
      }, initialDelay, period, TimeUnit.MILLISECONDS);
    }
  }

  @Override
  public synchronized void destroy() {
    if (!isDestroyed) {
      isDestroyed = true;
      if (scheduler != null) {
        scheduler.shutdown();
      }
      schedulerRunning = false;
      super.destroy();
    }
  }

  /**
   * Rolls the secret.  It is called automatically at the rollover interval.
   */
  protected synchronized void rollSecret() {
    if (!isDestroyed) {
      LOG.debug("rolling secret");
      byte[] newSecret = generateNewSecret();
      secrets = new byte[][]{newSecret, secrets[0]};
    }
  }

  /**
   * Subclasses should implement this to return a new secret.  It will be called
   * automatically at the secret rollover interval. It should never return null.
   * @return a new secret
   */
  protected abstract byte[] generateNewSecret();

  @Override
  public byte[] getCurrentSecret() {
    return secrets[0];
  }

  @Override
  public byte[][] getAllSecrets() {
    return secrets;
  }
}
