/*
 * 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.lucene.replicator.http;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpStatus;
import org.apache.lucene.replicator.Replicator;
import org.apache.lucene.replicator.SessionToken;
import org.apache.lucene.util.SuppressForbidden;

/**
 * A server-side service for handling replication requests. The service assumes requests are sent in
 * the format <code>/&lt;context&gt;/&lt;shard&gt;/&lt;action&gt;</code> where
 *
 * <ul>
 *   <li>{@code context} is the servlet context, e.g. {@link #REPLICATION_CONTEXT}
 *   <li>{@code shard} is the ID of the shard, e.g. "s1"
 *   <li>{@code action} is one of {@link ReplicationAction} values
 * </ul>
 *
 * For example, to check whether there are revision updates for shard "s1" you should send the
 * request: <code>http://host:port/replicate/s1/update</code>.
 *
 * <p>This service is written like a servlet, and {@link #perform(HttpServletRequest,
 * HttpServletResponse)} takes servlet request and response accordingly, so it is quite easy to
 * embed in your application's servlet.
 *
 * @lucene.experimental
 * @deprecated use org.apache.lucene.replicator.nrt instead
 */
@Deprecated
public class ReplicationService {

  /** Actions supported by the {@link ReplicationService}. */
  public enum ReplicationAction {
    OBTAIN,
    RELEASE,
    UPDATE
  }

  /** The context path for the servlet. */
  public static final String REPLICATION_CONTEXT = "/replicate";

  /** Request parameter name for providing the revision version. */
  public static final String REPLICATE_VERSION_PARAM = "version";

  /** Request parameter name for providing a session ID. */
  public static final String REPLICATE_SESSION_ID_PARAM = "sessionid";

  /** Request parameter name for providing the file's source. */
  public static final String REPLICATE_SOURCE_PARAM = "source";

  /** Request parameter name for providing the file's name. */
  public static final String REPLICATE_FILENAME_PARAM = "filename";

  private static final int SHARD_IDX = 0, ACTION_IDX = 1;

  private final Map<String, Replicator> replicators;

  public ReplicationService(Map<String, Replicator> replicators) {
    super();
    this.replicators = replicators;
  }

  /**
   * Returns the path elements that were given in the servlet request, excluding the servlet's
   * action context.
   */
  private String[] getPathElements(HttpServletRequest req) {
    String path = req.getServletPath();
    String pathInfo = req.getPathInfo();
    if (pathInfo != null) {
      path += pathInfo;
    }
    int actionLen = REPLICATION_CONTEXT.length();
    int startIdx = actionLen;
    if (path.length() > actionLen && path.charAt(actionLen) == '/') {
      ++startIdx;
    }

    // split the string on '/' and remove any empty elements. This is better
    // than using String.split() since the latter may return empty elements in
    // the array
    StringTokenizer stok = new StringTokenizer(path.substring(startIdx), "/");
    ArrayList<String> elements = new ArrayList<>();
    while (stok.hasMoreTokens()) {
      elements.add(stok.nextToken());
    }
    return elements.toArray(new String[0]);
  }

  private static String extractRequestParam(HttpServletRequest req, String paramName)
      throws ServletException {
    String param = req.getParameter(paramName);
    if (param == null) {
      throw new ServletException("Missing mandatory parameter: " + paramName);
    }
    return param;
  }

  private static void copy(InputStream in, OutputStream out) throws IOException {
    byte[] buf = new byte[16384];
    int numRead;
    while ((numRead = in.read(buf)) != -1) {
      out.write(buf, 0, numRead);
    }
  }

  /** Executes the replication task. */
  @SuppressForbidden(reason = "XXX: security hole")
  public void perform(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    String[] pathElements = getPathElements(req);

    if (pathElements.length != 2) {
      throw new ServletException(
          "invalid path, must contain shard ID and action, e.g. */s1/update");
    }

    final ReplicationAction action;
    try {
      action = ReplicationAction.valueOf(pathElements[ACTION_IDX].toUpperCase(Locale.ENGLISH));
    } catch (IllegalArgumentException e) {
      throw new ServletException("Unsupported action provided: " + pathElements[ACTION_IDX], e);
    }

    final Replicator replicator = replicators.get(pathElements[SHARD_IDX]);
    if (replicator == null) {
      throw new ServletException("unrecognized shard ID " + pathElements[SHARD_IDX]);
    }

    // SOLR-8933 Don't close this stream.
    ServletOutputStream resOut = resp.getOutputStream();
    try {
      switch (action) {
        case OBTAIN:
          final String sessionID = extractRequestParam(req, REPLICATE_SESSION_ID_PARAM);
          final String fileName = extractRequestParam(req, REPLICATE_FILENAME_PARAM);
          final String source = extractRequestParam(req, REPLICATE_SOURCE_PARAM);
          InputStream in = replicator.obtainFile(sessionID, source, fileName);
          try {
            copy(in, resOut);
          } finally {
            in.close();
          }
          break;
        case RELEASE:
          replicator.release(extractRequestParam(req, REPLICATE_SESSION_ID_PARAM));
          break;
        case UPDATE:
          String currVersion = req.getParameter(REPLICATE_VERSION_PARAM);
          SessionToken token = replicator.checkForUpdate(currVersion);
          if (token == null) {
            resOut.write(0); // marker for null token
          } else {
            resOut.write(1);
            token.serialize(new DataOutputStream(resOut));
          }
          break;
      }
    } catch (Exception e) {
      resp.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); // propagate the failure
      try {
        /*
         * Note: it is assumed that "identified exceptions" are thrown before
         * anything was written to the stream.
         */
        ObjectOutputStream oos = new ObjectOutputStream(resOut);
        oos.writeObject(e);
        oos.flush();
      } catch (Exception e2) {
        throw new IOException("Could not serialize", e2);
      }
    } finally {
      resp.flushBuffer();
    }
  }
}
