/* $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 javax.servlet.*;
import javax.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);
}
}