/* $Id: APIServlet.java 996524 2010-09-13 13:38:01Z kwright $ */

/**
* 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.manifoldcf.apiservlet;

import org.apache.manifoldcf.core.interfaces.*;
import org.apache.manifoldcf.agents.interfaces.*;
import org.apache.manifoldcf.crawler.interfaces.*;
import org.apache.manifoldcf.crawler.system.ManifoldCF;
import org.apache.manifoldcf.crawler.system.Logging;
import org.apache.manifoldcf.core.util.URLDecoder;

import org.apache.manifoldcf.ui.beans.APIProfile;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;


import jakarta.servlet.*;
import jakarta.servlet.http.*;

/** This servlet class provides API services for ManifoldCF.
*/
public class APIServlet extends HttpServlet
{
  public static final String _rcsid = "@(#)$Id: APIServlet.java 996524 2010-09-13 13:38:01Z kwright $";

  /** The init method.
  */
  public void init(ServletConfig config)
    throws ServletException
  {
    super.init(config);
  }

  /** The destroy method.
  */
  public void destroy()
  {
    super.destroy();
  }

  protected APIProfile getAPISession(IThreadContext tc, HttpServletRequest request)
  {
    Object x = request.getSession().getAttribute("apiprofile");
    if (x == null || !(x instanceof APIProfile))
    {
      // Basic login
      APIProfile ap = new APIProfile();
      request.getSession().setAttribute("apiprofile",ap);
      ap.login(tc,"","");
      return ap;
    }
    return (APIProfile)x;
  }
  
  /** The get method.
  */
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException
  {
    try
    {
      // Mint a thread context
      IThreadContext tc = ThreadContextFactory.make();
      
      // Get the path info string.  This will furnish the command.
      String pathInfo = request.getPathInfo();
      // Get query string.  This is used by some GET operations.
      String queryString = request.getQueryString();
      
      if (pathInfo == null)
      {
        response.sendError(response.SC_BAD_REQUEST,"No path info found");
        return;
      }

      // Verify session
      APIProfile ap = getAPISession(tc,request);
      // Perform the get
      executeRead(tc,response,pathInfo,queryString,ap);
    }
    catch (ManifoldCFException e)
    {
      // We should only see this error if there's an API problem, not if there's an actual problem with the method being called.
      Logging.api.debug("API error doing GET: "+e.getMessage(),e);
      response.sendError(response.SC_BAD_REQUEST,e.getMessage());
    }
  }

  /** The PUT method.
  */
  protected void doPut(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException
  {
    try
    {
      // Mint a thread context
      IThreadContext tc = ThreadContextFactory.make();
      
      // Get the path info string.  This will furnish the command.
      String pathInfo = request.getPathInfo();
      
      if (pathInfo == null)
      {
        response.sendError(response.SC_BAD_REQUEST,"No path info found");
        return;
      }

      // Verify session
      APIProfile ap = getAPISession(tc,request);
      // Get the content being 'put'
      InputStream content = request.getInputStream();
      try
      {
	// Do the put.
	executeWrite(tc,response,pathInfo,content,ap);
      }
      finally
      {
	content.close();
      }
    }
    catch (ManifoldCFException e)
    {
      // We should only see this error if there's an API problem, not if there's an actual problem with the method being called.
      Logging.api.debug("API error doing PUT: "+e.getMessage(),e);
      response.sendError(response.SC_BAD_REQUEST,e.getMessage());
    }
  }

 
  /** The POST method.
  */
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException
  {
    try
    {
      // Mint a thread context
      IThreadContext tc = ThreadContextFactory.make();
      
      // Get the path info string.  This will furnish the command.
      String pathInfo = request.getPathInfo();
      
      if (pathInfo == null)
      {
        response.sendError(response.SC_BAD_REQUEST,"No path info found");
        return;
      }

      // Verify session
      APIProfile ap = getAPISession(tc,request);
      // Get the content being posted
      InputStream content = request.getInputStream();
      try
      {
	// Do the put.
	executePost(tc,response,pathInfo,content,ap);
      }
      finally
      {
	content.close();
      }
      
    }
    catch (ManifoldCFException e)
    {
      // We should only see this error if there's an API problem, not if there's an actual problem with the method being called.
      Logging.api.debug("API error doing POST: "+e.getMessage(),e);
      response.sendError(response.SC_BAD_REQUEST,e.getMessage());
    }
  }

  /** The DELETE method.
  */
  protected void doDelete(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException
  {
    try
    {
      // Mint a thread context
      IThreadContext tc = ThreadContextFactory.make();
      
      // Get the path info string.  This will furnish the command.
      String pathInfo = request.getPathInfo();
      
      if (pathInfo == null)
      {
        response.sendError(response.SC_BAD_REQUEST,"No path info found");
        return;
      }

      // Verify session
      APIProfile ap = getAPISession(tc,request);
      // Perform the deletion
      executeDelete(tc,response,pathInfo,ap);
      
    }
    catch (ManifoldCFException e)
    {
      // We should only see this error if there's an API problem, not if there's an actual problem with the method being called.
      Logging.api.debug("API error doing DELETE: "+e.getMessage(),e);
      response.sendError(response.SC_BAD_REQUEST,e.getMessage());
    }
  }

  // Protected methods

  protected static void sendUnauthorizedResponse(HttpServletResponse response)
    throws IOException
  {
    response.setStatus(response.SC_UNAUTHORIZED);
    sendNullJSON(response);
  }
  
  protected static void sendNullJSON(HttpServletResponse response)
    throws IOException
  {
    String loutputText = "{}";
    byte[] lresponseValue = loutputText.getBytes(StandardCharsets.UTF_8);

    // Set response mime type
    response.setContentType("text/plain; charset=utf-8");
    response.setIntHeader("Content-Length", (int)lresponseValue.length);
    ServletOutputStream out = response.getOutputStream();
    try
    {
      out.write(lresponseValue,0,lresponseValue.length);
      out.flush();
    }
    finally
    {
      out.close();
    }
  }
  
  /** Perform a general "read" operation.
  */
  protected static void executeRead(IThreadContext tc, HttpServletResponse response, String pathInfo, String queryString, APIProfile ap)
    throws ManifoldCFException, IOException
  {
    if (!ap.getLoggedOn())
    {
      // Login failed
      sendUnauthorizedResponse(response);
      return;
    }

    // Strip off leading "/"
    if (pathInfo.startsWith("/"))
      pathInfo = pathInfo.substring(1);
      
    int index = pathInfo.indexOf("/");
    String protocol;
    String command;
    if (index == -1)
    {
      protocol = pathInfo;
      command = "";
    }
    else
    {
      protocol = pathInfo.substring(0,index);
      command = pathInfo.substring(index+1);
    }

    // If query string exists, parse it
    Map<String,List<String>> queryParameters = parseQueryString(queryString);
    
    // Execute the request.
    // Since there are no input arguments, we can do this before we look at the protocol.
    
    // There the only response distinction we have here is between exception and no exception.
    Configuration output = new Configuration();
    int readResult = ManifoldCF.executeReadCommand(tc,output,command,queryParameters,ap);

    // Output
    
    String outputText = null;

    if (protocol.equals("json"))
    {
      // Format the response
      try
      {
	outputText = output.toJSON();
      }
      catch (ManifoldCFException e)
      {
	// Log it
	Logging.api.error("Error forming JSON response: "+e.getMessage(),e);
	// Internal server error
	response.sendError(response.SC_INTERNAL_SERVER_ERROR);
	return;
      }
    }
    else
    {
      response.sendError(response.SC_BAD_REQUEST,"Unknown API protocol: "+protocol);
      return;
    }

    if (readResult == ManifoldCF.READRESULT_NOTFOUND)
      response.setStatus(response.SC_NOT_FOUND);
    else if (readResult == ManifoldCF.READRESULT_NOTALLOWED)
      response.setStatus(response.SC_UNAUTHORIZED);

    byte[] responseValue = outputText.getBytes(StandardCharsets.UTF_8);

    // Set response mime type
    response.setContentType("text/plain; charset=utf-8");
    response.setIntHeader("Content-Length", (int)responseValue.length);
    ServletOutputStream out = response.getOutputStream();
    try
    {
      out.write(responseValue,0,responseValue.length);
      out.flush();
    }
    finally
    {
      out.close();
    }

  }
  
  /** Perform a general "write" operation.
  */
  protected static void executeWrite(IThreadContext tc, HttpServletResponse response, String pathInfo, InputStream data, APIProfile ap)
    throws ManifoldCFException, IOException
  {
    if (!ap.getLoggedOn())
    {
      // Login failed
      sendUnauthorizedResponse(response);
      return;
    }

    // Strip off leading "/"
    if (pathInfo.startsWith("/"))
      pathInfo = pathInfo.substring(1);
      
    int index = pathInfo.indexOf("/");
    String protocol;
    String command;
    if (index == -1)
    {
      protocol = pathInfo;
      command = "";
    }
    else
    {
      protocol = pathInfo.substring(0,index);
      command = pathInfo.substring(index+1);
    }

    // We presume the data is utf-8
    StringBuilder sb = new StringBuilder();
    char[] buffer = new char[65536];
    Reader r = new InputStreamReader(data,StandardCharsets.UTF_8);
    while (true)
    {
      int amt = r.read(buffer);
      if (amt == -1)
	break;
      sb.append(buffer,0,amt);
    }
    String argument = sb.toString();
    
    // Parse the input
    Configuration input;
    
    if (protocol.equals("json"))
    {
      if (argument.length() != 0)
      {
	input = new Configuration();
	input.fromJSON(argument);
      }
      else
	input = null;
    }
    else
    {
      response.sendError(response.SC_BAD_REQUEST,"Unknown API protocol: "+protocol);
      return;
    }
    
    // Execute the request.
    
    // We need the following distinctions:
    // Exception vs. no exception
    // OK vs CREATE (both with json response packets)
    Configuration output = new Configuration();
    int writeResult = ManifoldCF.executeWriteCommand(tc,output,command,input,ap);
    
    // Output
    
    
    String outputText = null;

    if (protocol.equals("json"))
    {
      // Format the response
      try
      {
	outputText = output.toJSON();
      }
      catch (ManifoldCFException e)
      {
	// Log it
	Logging.api.error("Error forming JSON response: "+e.getMessage(),e);
	// Internal server error
	response.sendError(response.SC_INTERNAL_SERVER_ERROR);
	return;
      }
    }
    else
    {
      response.sendError(response.SC_BAD_REQUEST,"Unknown API protocol: "+protocol);
      return;
    }

    // This should return either 200 or SC_CREATED
    if (writeResult == ManifoldCF.WRITERESULT_CREATED)
      response.setStatus(response.SC_CREATED);
    else if (writeResult == ManifoldCF.WRITERESULT_NOTFOUND)
      response.setStatus(response.SC_NOT_FOUND);
    else if (writeResult == ManifoldCF.WRITERESULT_NOTALLOWED)
      response.setStatus(response.SC_UNAUTHORIZED);
    
    byte[] responseValue = outputText.getBytes(StandardCharsets.UTF_8);

    // Set response mime type
    response.setContentType("text/plain; charset=utf-8");
    response.setIntHeader("Content-Length", (int)responseValue.length);
    ServletOutputStream out = response.getOutputStream();
    try
    {
      out.write(responseValue,0,responseValue.length);
      out.flush();
    }
    finally
    {
      out.close();
    }

  }

  /** Perform a general "post" operation.
  */
  protected static void executePost(IThreadContext tc, HttpServletResponse response, String pathInfo, InputStream data, APIProfile ap)
    throws ManifoldCFException, IOException
  {
    // Strip off leading "/"
    if (pathInfo.startsWith("/"))
      pathInfo = pathInfo.substring(1);
      
    int index = pathInfo.indexOf("/");
    String protocol;
    String command;
    if (index == -1)
    {
      protocol = pathInfo;
      command = "";
    }
    else
    {
      protocol = pathInfo.substring(0,index);
      command = pathInfo.substring(index+1);
    }

    // Security check.  If the protocol is JSON and the command is LOGIN, we do the login now.  But to
    // prevent denial of service attacks, we don't accept more than a limited amount of login JSON.
    if (protocol.equals("json") && command.equals("LOGIN")) {
      // Do the login!
      // Parse the json login packet
      char[] lbuffer = new char[65536];
      StringBuilder lsb = new StringBuilder();
      Reader lr = new InputStreamReader(data,StandardCharsets.UTF_8);
      while (true)
      {
        int amt = lr.read(lbuffer);
        if (amt == -1)
          break;
        if (lsb.length() + amt > 65536)
          break;
        lsb.append(lbuffer,0,amt);
      }
      
      Configuration loginInput = new Configuration();
      loginInput.fromJSON(lsb.toString());

      String userID = "";
      String password = "";
      for (int i = 0; i < loginInput.getChildCount(); i++)
      {
        ConfigurationNode cn = loginInput.findChild(i);
        if (cn.getType().equals("userID"))
          userID = cn.getValue();
        else if (cn.getType().equals("password"))
          password = cn.getValue();
      }
      ap.login(tc,userID,password);
      if (!ap.getLoggedOn())
      {
        sendUnauthorizedResponse(response);
        return;
      }
      else
      {
        sendNullJSON(response);
        return;
      }
    }

    if (!ap.getLoggedOn())
    {
      // Login failed
      sendUnauthorizedResponse(response);
      return;
    }

    // We presume the data is utf-8
    StringBuilder sb = new StringBuilder();
    char[] buffer = new char[65536];
    Reader r = new InputStreamReader(data,StandardCharsets.UTF_8);
    while (true)
    {
      int amt = r.read(buffer);
      if (amt == -1)
	break;
      sb.append(buffer,0,amt);
    }
    String argument = sb.toString();
    
    // Parse the input
    Configuration input;
    
    if (protocol.equals("json"))
    {
      if (argument.length() != 0)
      {
	input = new Configuration();
	input.fromJSON(argument);
      }
      else
	input = null;
    }
    else
    {
      response.sendError(response.SC_BAD_REQUEST,"Unknown API protocol: "+protocol);
      return;
    }
    
    // Execute the request.

    
    Configuration output = new Configuration();
    int writeResult = ManifoldCF.executePostCommand(tc,output,command,input,ap);
    
    // Output
    
    
    String outputText = null;

    if (protocol.equals("json"))
    {
      // Format the response
      try
      {
	outputText = output.toJSON();
      }
      catch (ManifoldCFException e)
      {
	// Log it
	Logging.api.error("Error forming JSON response: "+e.getMessage(),e);
	// Internal server error
	response.sendError(response.SC_INTERNAL_SERVER_ERROR);
	return;
      }
    }
    else
    {
      response.sendError(response.SC_BAD_REQUEST,"Unknown API protocol: "+protocol);
      return;
    }

    // This should return either 200 or SC_CREATED
    if (writeResult == ManifoldCF.POSTRESULT_CREATED)
      response.setStatus(response.SC_CREATED);
    else if (writeResult == ManifoldCF.POSTRESULT_NOTFOUND)
      response.setStatus(response.SC_NOT_FOUND);
    else if (writeResult == ManifoldCF.POSTRESULT_NOTALLOWED)
      response.setStatus(response.SC_UNAUTHORIZED);

    byte[] responseValue = outputText.getBytes(StandardCharsets.UTF_8);

    // Set response mime type
    response.setContentType("text/plain; charset=utf-8");
    response.setIntHeader("Content-Length", (int)responseValue.length);
    ServletOutputStream out = response.getOutputStream();
    try
    {
      out.write(responseValue,0,responseValue.length);
      out.flush();
    }
    finally
    {
      out.close();
    }

  }
  
  /** Perform a general "delete" operation.
  */
  protected static void executeDelete(IThreadContext tc, HttpServletResponse response, String pathInfo, APIProfile ap)
    throws ManifoldCFException, IOException
  {
    if (!ap.getLoggedOn())
    {
      // Login failed
      sendUnauthorizedResponse(response);
      return;
    }

    // Strip off leading "/"
    if (pathInfo.startsWith("/"))
      pathInfo = pathInfo.substring(1);
      
    int index = pathInfo.indexOf("/");
    String protocol;
    String command;
    if (index == -1)
    {
      protocol = pathInfo;
      command = "";
    }
    else
    {
      protocol = pathInfo.substring(0,index);
      command = pathInfo.substring(index+1);
    }

    // Execute the request.
    // Since there are no input arguments, we can do this before we look at the protocol.
    
    // There the only response distinction we have here is between exception and no exception.
    Configuration output = new Configuration();
    int result = ManifoldCF.executeDeleteCommand(tc,output,command,ap);
    
    // Output
    String outputText = null;

    if (protocol.equals("json"))
    {
      // Format the response
      try
      {
	outputText = output.toJSON();
      }
      catch (ManifoldCFException e)
      {
	// Log it
	Logging.api.error("Error forming JSON response: "+e.getMessage(),e);
	// Internal server error
	response.sendError(response.SC_INTERNAL_SERVER_ERROR);
	return;
      }
    }
    else
    {
      response.sendError(response.SC_BAD_REQUEST,"Unknown API protocol: "+protocol);
      return;
    }
    
    if (result == ManifoldCF.DELETERESULT_NOTFOUND)
      response.setStatus(response.SC_NOT_FOUND);
    else if (result == ManifoldCF.DELETERESULT_NOTALLOWED)
      response.setStatus(response.SC_UNAUTHORIZED);

    
    byte[] responseValue = outputText.getBytes(StandardCharsets.UTF_8);

    // Set response mime type
    response.setContentType("text/plain; charset=utf-8");
    response.setIntHeader("Content-Length", (int)responseValue.length);
    ServletOutputStream out = response.getOutputStream();
    try
    {
      out.write(responseValue,0,responseValue.length);
      out.flush();
    }
    finally
    {
      out.close();
    }

    // return code 200 assumed!
  }
  
  protected static Map<String,List<String>> parseQueryString(String queryString)
  {
    if (queryString == null)
      return null;
    Map<String,List<String>> rval = new HashMap<String,List<String>>();
    String[] terms = queryString.split("&");
    for (String term : terms)
    {
      int index = term.indexOf("=");
      if (index == -1)
        addValue(rval,URLDecoder.decode(term),"");
      else
        addValue(rval,URLDecoder.decode(term.substring(0,index)),URLDecoder.decode(term.substring(index+1)));
    }
    return rval;
  }
  
  protected static void addValue(Map<String,List<String>> rval, String name, String value)
  {
    List<String> valueList = rval.get(name);
    if (valueList == null)
    {
      valueList = new ArrayList<String>(1);
      rval.put(name,valueList);
    }
    valueList.add(value);
  }
  
}
