Package org.persvr.remote

Source Code of org.persvr.remote.PersevereFilter

package org.persvr.remote;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.NativeObject;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;
import org.persvr.Persevere;
import org.persvr.data.BinaryData;
import org.persvr.data.DataSourceManager;
import org.persvr.data.FunctionUtils;
import org.persvr.data.GlobalData;
import org.persvr.data.Identification;
import org.persvr.data.JsonPath;
import org.persvr.data.Method;
import org.persvr.data.ObjectNotFoundException;
import org.persvr.data.ObjectNotFoundId;
import org.persvr.data.ObjectPath;
import org.persvr.data.ObservablePersistable;
import org.persvr.data.Persistable;
import org.persvr.data.PersistableClass;
import org.persvr.data.PersistableObject;
import org.persvr.data.Status;
import org.persvr.data.Transaction;
import org.persvr.datasource.DataSource;
import org.persvr.datasource.DynaFileDBSource;
import org.persvr.datasource.LocalJsonFileSource;
import org.persvr.javascript.PersevereContextFactory;
import org.persvr.javascript.PersevereNativeFunction;
import org.persvr.javascript.TestRunner;
import org.persvr.job.Job;
import org.persvr.job.Upgrade;
import org.persvr.remote.Client.IndividualRequest;
import org.persvr.security.SystemPermission;
import org.persvr.security.UserSecurity;
import org.persvr.util.Console;
import org.persvr.util.JSON;
import org.persvr.util.JSONParser;
import org.persvr.util.JSONParser.JSONException;

/**
* This class is the main filter through which all requests should be funneled.
* It examines each request to determine if there is a corresponding data source
* to with data for the handling of the request. This filter handles the HTTP
* interaction including HTTP methods including GET,PUT,POST, and DELETE and
* status codes
*
* @author Kris Zyp
*
*/
@SuppressWarnings("serial")
public class PersevereFilter extends PersevereServlet implements Filter {
  private static Log log = LogFactory.getLog(PersevereFilter.class);
  public static boolean startConsole = true;
  Scriptable global = GlobalData.getGlobalScope();
  /**
   * Initializes the Persevere server
   */
  public void init(FilterConfig config) throws ServletException {
    String webappRoot = config.getServletContext().getRealPath("");
    if (GlobalData.webInfLocation == null) {
      String persvrHome = System.getProperty("persevere.home");
      if (persvrHome != null) {
        GlobalData.webInfLocation = new File(persvrHome).toURI() + "/WEB-INF";
      } else {
        GlobalData.webInfLocation = new File(config.getServletContext().getRealPath("/WEB-INF")).toURI().toString();
      }
    }
    log.debug(GlobalData.webInfLocation.toString());
    global.put("coreApp", global, new DefaultHandler());
    LocalJsonFileSource.setLocalJsonPath(webappRoot);

    Job upgrade = new Upgrade();
    upgrade.execute();
    log.info("Persevere v" + Persevere.getPersevereVersion() + " Started");
    if(startConsole){
      new Console().start();
    }
    config.getServletContext().setAttribute("testrunner", new TestRunner());
    Runtime.getRuntime().addShutdownHook(new Thread() {
      @Override
      public void run() {
        log.info("Persevere shutting down");
      }
    });
  }

  public static interface RequestListener {
    public void request(HttpServletRequest request);
  }

  static DateFormat formatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz");
  public static DateFormat preciseFormatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss.SSS zzz");

  /**
   * Adds a header, allowing the header to be assigned in different ways
   * depending on the response (JSONP in parameters vs real headers in direct
   * requests)
   *
   * @param headerTarget
   */
  private void addHeaders(Object headerTarget) {
    // TODO: This should go away and be replaced with a JSONPResponseWrapper and WindowNameResponseWrapper
    Map<String, String> headers = Client.getCurrentObjectResponse().getHeaders();
    for (Map.Entry<String, String> entry : headers.entrySet()) {
      if (entry.getKey().equals("status") && headerTarget instanceof HttpServletResponse) {
        ((HttpServletResponse) headerTarget).setStatus(Integer.parseInt(entry.getValue()));
      }
      setHeader(entry.getKey(), entry.getValue(), headerTarget);
    }
  }

  /**
   * Gets the parameters from a URL
   */
  private static String getParameterFromUrlEncoded(String urlEncoded, String name) {
    if (urlEncoded.indexOf(name) > -1)//performance guard
      for (String nameValueStr : urlEncoded.split("&")) { // parse the query string
        String[] nameValue = nameValueStr.split("=", 2);
        try {
          if (name.equals(URLDecoder.decode(nameValue[0], "UTF-8")))
            return URLDecoder.decode(nameValue[1], "UTF-8");
        } catch (UnsupportedEncodingException e) {
          e.printStackTrace();
        }
      }
    return null;
  }

  /**
   * Gets the parameters from the URL in the request
   */
  public static String getParameterFromQueryString(HttpServletRequest request, String name) {
    if (request.getQueryString() != null) {
      // _MUST NOT_ read the body (getInputStream) or call getParameter, because that destroys it for later use
      String value = getParameterFromUrlEncoded(request.getQueryString(), name);
      return value;
    }
    return null;

  }

  /**
   * Get a header, using parameters as a backup
   */
  public static String getHeader(HttpServletRequest request, String name) {
    if (request == null)
      return null;
    String value = getParameterFromQueryString(request, "http-" + name);
    if (value == null)
      return request.getHeader(name);

    return value;

  }

  /**
   * Gets a custom (non-standard) parameter using headers as a backup
   */
  public static String getParameter(HttpServletRequest request, String name) {
    if (request == null)
      return null;
    String value = getParameterFromQueryString(request, name.replace('-', '_').toLowerCase());
    if (value == null)
      value = getParameterFromQueryString(request, "http-" + name);
    if (value == null)
      value = request.getHeader(name);
    if (value == null)
      value = request.getHeader("X-" + name);

    return value;
  }

  public static class ConditionFailedException extends RuntimeException {

    public ConditionFailedException(String message) {
      super(message);
    }

  }

  static Pattern slashPattern = Pattern.compile("[^\\[\\.]*/");

  /**
   * The main entry point for all requests
   */
  public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain)
      throws IOException, ServletException {
    final HttpServletRequest request = (HttpServletRequest) servletRequest;

    if (Identification.getAbsolutePathPrefix() == null) {
      // if the absolute path prefix has not been initialized, do it now. This
      // will allow the id resolver to understand absolute paths correctly
      String pathPrefix = request.getContextPath();
      Identification.setAbsolutePathPrefix(pathPrefix.length() == 1 ? "/" : (pathPrefix + "/"));
    }
    final Thread thisThread = Thread.currentThread();
    final HttpServletResponse response = (HttpServletResponse) servletResponse;
    //request.setCharacterEncoding("UTF-8");
    //response.setCharacterEncoding("UTF-8");
    final RequestHelper rh;
       
    try {
      rh = new RequestHelper((HttpServletRequest) request, (HttpServletResponse) response);
    } catch (RhinoException e) {
      String reason;
      reason = e.getMessage();
      if (reason != null && reason.startsWith("AccessError")) {
        response.setHeader("WWW-Authenticate", "JSON-RPC, Basic");
        response.setStatus(401);
      }
      response.getOutputStream().print(reason);
      return;
    }
    Client client = rh.getClientConnection();
    String seqIdString = getParameter(request, "Seq-Id");
    String transIdString = getParameter(request, "Transaction-Id");
    long seqId = -1;
    long transId = -1;
    if (seqIdString != null){
      seqId = Long.parseLong(seqIdString);
    }
    if (transIdString != null){
      transId = Long.parseLong(transIdString);
    }
    try{
      synchronized(client){
        if(seqId>=0 && transId>=0 && getParameter(request, "Transaction")!=null){
          //start or enter the specified transaction and track sequence numbers
          client.startOrEnterTransaction(seqId, transId);
        }else{
          //just use a new tranaction for this request
          Transaction.startTransaction();
        }
        if(seqId>=0){
          client.addSequenceId(seqId);
        }
      }
      NativeObject env = new PersistableObject() {
        public Object get(String key, Scriptable start) {
          Object storedValue = super.get(key, start);
          if(storedValue != ScriptableObject.NOT_FOUND)
            return storedValue;
          if ("REQUEST_METHOD".equals(key)) {
            return request.getMethod();
          }
          if ("SERVLET_REQUEST".equals(key)) {
            return request;
          }
          if ("SERVLET_RESPONSE".equals(key)) {
            return response;
          }
          if ("PERSEVERE_REQUEST_HELPER".equals(key)) {
            return rh;
          }
          if ("SERVLET_FILTER_CHAIN".equals(key)) {
            return filterChain;
          }
          if ("SCRIPT_NAME".equals(key)) {
            return request.getServletPath();
          }
          if ("PATH_INFO".equals(key)) {
            String path = request.getRequestURI();
            return path.substring(request.getContextPath().length());
          }
          if ("CONTENT_TYPE".equals(key)) {
            return request.getContentType();
          }
          if ("CONTENT_LENGTH".equals(key)) {
            return request.getContentLength();
          }
          if ("QUERY_STRING".equals(key)) {
            return request.getQueryString();
          }
          if ("SERVER_NAME".equals(key)) {
            return request.getServerName();
          }
          if ("SERVER_PORT".equals(key)) {
            return request.getServerPort();
          }
          if ("SERVER_PROTOCOL".equals(key)) {
            return request.getProtocol();
          }
          if ("jsgi.version".equals(key)) {
            List array = Persevere.newArray();
            array.add(0);
            array.add(1);
            return array;
          }
          if ("jsgi.url_scheme".equals(key)) {
            return request.getScheme();
          }
          if ("jsgi.input".equals(key)) {
            try {
              return request.getInputStream();
            } catch (IOException e) {
              throw new RuntimeException(e);
            }
          }
          if ("jsgi.error".equals(key)) {
            return System.err;
          }
          if ("jsgi.multithread".equals(key)) {
            return true;
          }
          if ("jsgi.multiprocess".equals(key)) {
            return false;
          }
          if ("jsgi.run_once".equals(key)) {
            return false;
          }
          return Undefined.instance;
        }
        public Object getCoreValue(String name){
          return get(name, this);
        }
        @Override
        public Object[] getIds() {
          List list = new ArrayList();
          list.addAll(Arrays.asList(super.getIds()));
          list.addAll(Arrays.asList(new String[]{"REQUEST_METHOD","SCRIPT_NAME","PATH_INFO","CONTENT_TYPE","CONTENT_LENGTH","QUERY_STRING","SERVER_NAME","SERVER_PORT","SERVER_PROTOCOL","jsgi.version","jsgi.url_scheme","jsgi.input","jsgi.error","jsgi.multithread","jsgi.multiprocess","jsgi.run_once"}));
          return list.toArray();
        }

      };
      try {
        ScriptRuntime.setObjectProtoAndParent(env, global);
      } catch (Exception e1) {
      }
      Enumeration headerNames = request.getHeaderNames();
      while(headerNames.hasMoreElements()){
        String headerName = (String) headerNames.nextElement();
        if(!(headerName.equals("Content-Type") || headerName.equals("Content-Length")))
          env.put("HTTP_" + headerName.toUpperCase(), env, request.getHeader(headerName));
      }
      Object result = ((Function) global.get("coreApp", global)).call(PersevereContextFactory.getContext(), global, global,
          new Object[] { env });
      if (result instanceof Scriptable) {
        Object status = ((Scriptable) result).get("status", (Scriptable) result);
        if(status instanceof Number)
          response.setStatus(((Number)status).intValue());
        else if (status instanceof String){
          String[] statusParts = ((String)status).split(" ", 2);
          response.setStatus(Integer.parseInt(statusParts[0]), statusParts[1]);
        }
        Object headers = ((Scriptable) result).get("headers", (Scriptable) result);
        if(headers instanceof Scriptable){
          for (Object key : ((Scriptable)headers).getIds()) {
            response.setHeader(key.toString(), ((Scriptable)headers).get(key.toString(), (Scriptable) headers).toString());
          }
        }
        Object body = ((Scriptable) result).get("body", (Scriptable) result);
        if(body instanceof String)
          response.getOutputStream().write(((String)body).getBytes("UTF-8"));
        else if (body instanceof Scriptable){
          Function forEach = (Function) ScriptableObject.getProperty((Scriptable) body, "forEach");
          Scriptable global = GlobalData.getGlobalScope();
          final ServletOutputStream outputStream = response.getOutputStream();
          forEach.call(PersevereContextFactory.getContext(), global, (Scriptable) body, new Object[]{
            new PersevereNativeFunction(){
              @Override
              public Object profilableCall(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
                try {
                  outputStream.write(args[0].toString().getBytes("UTF-8"));
                } catch (UnsupportedEncodingException e) {
                  throw ScriptRuntime.constructError("Error", e.getMessage());
                } catch (IOException e) {
                  throw ScriptRuntime.constructError("Error", e.getMessage());
                }
                return null;
              }

            }
          });
        }
        else
          throw new RuntimeException("The body must be a string or an object with a forEach");
      }
    }
    finally{
      synchronized(client){
        if(transId>=0 && getParameter(request, "Transaction")!=null){
          if(!"open".equals(getParameter(request, "Transaction"))){
            client.commitTransaction(transId);
          }else{
            Transaction.exitTransaction();
          }
          client.runUnblockedTransactions();
        }else{
          Transaction.currentTransaction().commit();
        }
      }
      //TODO: Release the read set monitoring to free those memory references
      //TODO: Release the IndividualRequest object to free those memory references
      IndividualRequest individualRequest = Client.getCurrentObjectResponse();
      if (individualRequest != null)
        individualRequest.finish(); // indicate we are finished

    }
  }

  public static class DefaultHandler extends PersevereNativeFunction {
    private boolean objectExistsForId(Identification targetId){
      try {
        return targetId.getTarget() != ScriptableObject.NOT_FOUND;
      } catch (ObjectNotFoundException e) {
        return false;
      }
    }
    @Override
    public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
      long startTime = System.currentTimeMillis();
      Scriptable env = (Scriptable) args[0];
      HttpServletRequest request = (HttpServletRequest) env.get("SERVLET_REQUEST", env);
      HttpServletResponse response = (HttpServletResponse) env.get("SERVLET_RESPONSE", env);
      RequestHelper requestHelper = (RequestHelper) env.get("PERSEVERE_REQUEST_HELPER", env);
      FilterChain filterChain = (FilterChain) env.get("SERVLET_FILTER_CHAIN", env);
      //request.setCharacterEncoding("UTF-8");
      //response.setCharacterEncoding("UTF-8");
      String contentType = request.getContentType();
      String connectionId = getParameter(request, "Client-Id");
      String reason;
      String username;
      try {
        PersistableObject.startReadSet();
        Writer writer = null;
        Map suffixHeader = null;
        String suffixString = null;
        Object headerTarget;
        String callbackParameter;
        String seqIdString = getParameter(request, "Seq-Id");
        long seqId = -1;
        if (seqIdString != null)
          seqId = Long.parseLong(seqIdString);
        if (request.getHeader("Origin") != null) {
          response.setHeader("Access-Control-Allow-Origin", "*"); // support cross-site XHR
          response.setHeader("Vary", "Origin"); // this is to make sure the cache is handled properly
        }

        String queryString = (String) env.get("QUERY_STRING", env);
        if (queryString != null) {
          // remove parameters with special meaning, the rest can be used for queries
          queryString = queryString
              .replaceAll(
                  "\\&?(jsonp|transaction|client_id|subscribe_since|server_methods|seq_id|subscribe|windowname|callback|http[-_][^=]*)=[^&]*",
                  "")
          if (queryString.equals(""))
            queryString = null;
        }
        String path = env.get("PATH_INFO", env) + (queryString == null ? "" : ("?" + queryString));
        path = URLDecoder.decode(path.substring(1), "UTF8");
        if (((callbackParameter = getParameterFromQueryString(request, "jsonp")) != null || (callbackParameter = getParameterFromQueryString(
            request, "callback")) != null)) {
          // handle JSONP
          response.setContentType("application/javascript; charset=UTF-8");
          headerTarget = suffixHeader = new HashMap();
          writer = new OutputStreamWriter(response.getOutputStream(), "UTF-8");
          if (request.getAttribute("org.persvr.suspended") == null) {
            if (path.endsWith(".js")) {
              writer.write("temp=");
              suffixString = "\n" + callbackParameter + "(temp";
            } else
              writer.write(callbackParameter + "(");
            writer.flush();
          }
        } else if ((callbackParameter = getParameterFromQueryString(request, "windowname")) != null) {
          // handle window.name requests
          response.setContentType("text/html; charset=UTF-8");
          Client.getCurrentObjectResponse().httpResponse = requestHelper.response = response = new StringHttpServletResponseWrapper(
              response);
          headerTarget = suffixHeader = new HashMap();
          writer = new OutputStreamWriter(response.getOutputStream(), "UTF-8");

        } else {
          headerTarget = response;
        }

        if (suffixHeader != null) {

          formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
          suffixHeader.put("Date", formatter.format(new Date()));

        }
        Matcher matcher = slashPattern.matcher(path);
        if (matcher.find()) {
          String sourceName = matcher.group();
          requestHelper.setPath(sourceName);
        }

        //slashIndex = slashIndex == -1 ? dotIndex : dotIndex == -1 ? slashIndex : Math.min(slashIndex, pathInfo.indexOf('.'));
        /*
         * source = DataSourceManager.getSource(sourceName); if (source ==
         * null) source = AliasIds.getAliasHandler(sourceName); } else
         * source = null;
         */
        String method = request.getMethod().toUpperCase();
        Identification targetId = Identification.idForString(path);

        if (log.isDebugEnabled()) {
          log.debug("Identification targetId:" + targetId);
          log.debug("Request Method: " + method);
        }

        reason = null;
        if ("GET".equals(method) || "POST".equals(method)) {
          // allow the HTTP method to be defined with a parameter
          String explicitMethod = getHeader(request, "method");
          if (explicitMethod != null)
            method = explicitMethod;
          else if ("GET".equals(method))
            // if there is not explicit method, than we can assume cookie authorization is allowed
            requestHelper.authorizeCookieAuthentication();
        }
        try {
          if (targetId instanceof ObjectNotFoundId ||
            (targetId.getSource() instanceof LocalDataSource && ((LocalDataSource) targetId.getSource()).passThrough()) ||
            ("GET".equals(method) && !objectExistsForId(targetId))) {
            if ("PUT".equals(method)) {
              if (UserSecurity.hasPermission(SystemPermission.javaScriptCoding)) {
                // if the user has permission they can also update files with PUT
                File targetFile = new File(((HttpServletRequest) request).getRealPath(path));
                if (targetFile.exists()) {
                  FileOutputStream fos = new FileOutputStream(targetFile);
                  byte[] b = new byte[4096];
                  InputStream in = request.getInputStream();
                  for (int n; (n = in.read(b)) != -1;) {
                    fos.write(b, 0, n);
                  }
                  fos.close();
                  return null;
                }
              }
            } else {
              // use the default handler
              PersevereResponse redirectResponse = new PersevereResponse(response, writer);
              String realPath = request.getRealPath(request.getRequestURI());
              File targetFile;
              boolean fileExists = realPath != null && (targetFile = new File(realPath)).exists() && targetFile.isFile();

              if(!fileExists){
                Scriptable global = GlobalData.getGlobalScope();
                Object app = global.get("app",global);
                if(app instanceof Function){
                  return ((Function)app).call(PersevereContextFactory.getContext(), global, global, new Object[]{env});
                }
              }
              filterChain.doFilter(new PersevereRequest(request, requestHelper, suffixString != null), redirectResponse);
              redirectResponse.flushBuffer();
              //Client.getCurrentObjectResponse().getConnection().commitTransaction();
              return null;
            }
          }

          response.setHeader("Server", "Persevere");
          if (writer == null)
            writer = new OutputStreamWriter(response.getOutputStream(), "UTF-8");
          if ("GET".equals(method) || "HEAD".equals(method)) {
            setCacheHeader(response, 2000); // need to do this to make GETs to refresh in IE
//            response.setHeader("Vary", "Accept, Referer");
          }
          Object target;

            boolean rollback = false;
            boolean newContent = false;
            boolean force204 = false;
            try {

              //postBody = IOUtils.inputStreamToString(request.getInputStream());

              if (targetId instanceof ObjectNotFoundId && "PUT".equals(method)
                  && (path.indexOf("/") == -1 || path.indexOf("/") == path.length() - 1)) {
                if (path.indexOf("/") == path.length() - 1)
                  path = path.substring(0, path.length() - 1);
                String postBody = IOUtils.toString(request.getReader());

                Object postObject = null;
                postObject = requestHelper.parseJsponString(postBody); // TODO: Shouldn't do this twice
                if (postObject instanceof Map) {
                  String superType = (String) ((Map) postObject).get("extends");
                  if (superType != null) {
                    Persevere.createNewTable(path, superType);
                    return null;
                  }
                }
                Persevere.createNewTable(path, "Object");
                return null;
              }
              Persistable datedTarget = null;
              if ("POST".equals(method)) // This is a hack to get POSTs to work on slice operator
                path = path.replaceAll("\\[.*\\:.*\\]", "");
              target = Client.getCurrentObjectResponse().requestData(path, "PUT".equals(method));
              if (targetId instanceof ObjectPath) {
                datedTarget = ((ObjectPath) targetId).getSecondToLastTarget();
              } else if (target instanceof Persistable)
                datedTarget = (Persistable) target;
              String subscribe = getParameter(request, "Subscribe");
              if (subscribe == null)
                subscribe = getParameterFromQueryString(request, "subscribe");
              // handle subscription requests
              if (null != subscribe) {
                final Identification<? extends Object> id = Identification.idForString(path);
                if (datedTarget != null) {
                  if (target instanceof ObservablePersistable)
                    ((ObservablePersistable) target).subscribe();

                  // TODO: Only do this if the range references "now"
                  request.setAttribute("org.persvr.servletNow", new Date());
                  Map<String, String> headers = new HashMap<String, String>();
                  Enumeration headerEnum = request.getHeaderNames();
                  while (headerEnum.hasMoreElements()) {
                    String headerName = (String) headerEnum.nextElement();
                    String value = request.getHeader(headerName);
                    headerName = headerName.toLowerCase();
                    if ("opera-range".equals(headerName))
                      headerName = "range";
                    headers.put(headerName, value);
                  }
                  headers.put("__now__", new Date().getTime() + "");
                  headers.put("__pathInfo__", path);
                  if (connectionId == null) {

                  } else {
                    if ("none".equals(subscribe)) {// far future, unsubscribe
                      requestHelper.connection.removeSubscription(headers);
                    } else if ("*".equals(subscribe)) {
                      requestHelper.connection.addSubscription(headers);
                      PersistableObject.addListener(requestHelper.connection);
                    }
                  }
                  setHeader("Subscribed", "OK", headerTarget);

                }
              }
              byte[] postBytes = null;
              String embeddedContent = null;
              if ("application/x-www-form-urlencoded".equals(request.getContentType())) {
                // if it was POSTed from cross-domain, it may specify it's content in the http-content parameter in the entity
                if (postBytes == null)
                  postBytes = IOUtils.toByteArray(request.getInputStream());
                String postBody = new String(postBytes, "UTF-8");
                embeddedContent = getParameterFromUrlEncoded(postBody, "http-content");
                if (embeddedContent == null)
                  embeddedContent = getParameterFromQueryString(request, "http_content");
                if (embeddedContent != null)
                  request.setAttribute("cross-site", true);
              } else if (!"GET".equals(method)) {
                // if it was a GET (that specifies a method beside GET with http-method) from cross-domain,
                //  it may specify it's content in the http-content parameter in the URL
                embeddedContent = getParameterFromQueryString(request, "http-content");
                if (embeddedContent == null)
                  embeddedContent = getParameterFromQueryString(request, "http_content");
                if (embeddedContent != null)
                  request.setAttribute("cross-site", true);
              }
              if (embeddedContent != null) {
                postBytes = embeddedContent.getBytes("UTF-8");
              }
              // Restart here
              String precondition = getParameter(request, "If");
              //TODO: Handle relative expressions
              if (precondition != null && !ScriptRuntime.toBoolean(Identification.idForString(precondition).getTarget()))
                throw new ConditionFailedException("Condition " + precondition + " was not satisfied");
              contentType = contentType == null ? null : contentType.split(";")[0];
              if ("POST".equals(method)) {
                Object postObject = null;
                boolean isJson = false;
                boolean safeContentType = false;
                if (couldBeJson(contentType)) {
                  try {
                    // this means that the content may be JSON, we are forgiving if URL encoding is used as it is
                    // the default for Ajax requests
                    if (postBytes == null)
                      postBytes = IOUtils.toByteArray(request.getInputStream());
                    String postBody = new String(postBytes, "UTF-8");
                    postObject = requestHelper.parseJsponString(postBody); // TODO: Shouldn't do this twice
                    // If it parsed we can be assured that it wasn't cross-site browser generated
                    requestHelper.authorizeCookieAuthentication();
                    // detect JSON-RPC calls
                    if (postObject instanceof Map && ((Map) postObject).containsKey("id")
                        && ((Map) postObject).containsKey("method") && ((Map) postObject).containsKey("params")
                        && ((Map) postObject).get("params") instanceof List) {
                      // It looks like a JSON-RPC request, treat it as a method call
                      requestHelper.handleRPC(target, (Map) postObject);
                      // we set the username again because it may have changed during the RPC
                      username = UserSecurity.getUserName(UserSecurity.currentUser());
                      // we set the username so the client side can access it
                      setHeader("Username", "public".equals(username) ? null : username, headerTarget);
                      // write out the response (this could actually be a request from the server)
                      Client.getCurrentObjectResponse().writeWaitingRPCs();
                      return null;
                    }
                    Object bodyData = requestHelper.convertParsedToObject(postObject);
                    String newLocation = request.getHeader("Content-ID");
                    // this indicates that the request has a client-assigned id to use temporarily
                    if (newLocation != null) {
                      // get rid of the brackets and the contextPath
                      newLocation = newLocation.substring(request.getContextPath().length() + 2, newLocation.length() - 1);
                      Client client = Client.getCurrentObjectResponse().getConnection();
                      Persistable createdObject = client.getClientSideObject(newLocation);
                      if (createdObject == null)
                        client.clientSideObject(newLocation, createdObject = Persevere.newObject(((Persistable) target)
                            .getId()));
                      target = createdObject;
                      for (Map.Entry<String, Object> entry : ((Persistable) bodyData).entrySet(0)) {
                        createdObject.set(entry.getKey(), entry.getValue());
                      }
                    } else {
                      target = postObject((Persistable) target, bodyData);
                    }
                    newContent = true;
                    isJson = true;
                  } catch (JSONParser.JSONException e) {
                    // if the content type explicity said it was JSON or JavaScript we will throw an error
                    if (contentType == null || contentType.indexOf("json") > 0 || contentType.indexOf("javascript") > 0) {
                      throw e;
                    }
                  }

                } else
                  safeContentType = true;
                if (!isJson) {
                  // it wasn't JSON (couldn't be parsed) and so we treat it as a file
                  if (ServletFileUpload.isMultipartContent(request)) {
                    if(target instanceof List){
                      target = Persevere.newObject(((Persistable) target).getId());
                    }
                    // Create a factory for disk-based file items
                    FileItemFactory factory = new DiskFileItemFactory();

                    // Create a new file upload handler
                    ServletFileUpload upload = new ServletFileUpload(factory);

                    // Parse the request
                    List<FileItem> items = upload.parseRequest(request);
                    for (FileItem item : items) {
                      if (item.isFormField()) {
                        ((Persistable) target).set(item.getFieldName(), item.getString());
                      } else {
                        Persistable fileTarget = createFile(item.getContentType(), null, item.getInputStream());
                        fileTarget.set("name", item.getName());
                        if (!(target instanceof List)) {
                          ((Persistable) target).set(item.getFieldName(), fileTarget);
                        }
                      }
                    }
                  } else {
                    if (safeContentType)
                      requestHelper.authorizeCookieAuthentication();
                    DataSource source = ((Persistable) target).getId().source;
                    String contentDisposition = request.getHeader("Content-Disposition");
                    if (postBytes == null)
                      postBytes = IOUtils.toByteArray(request.getInputStream());
                    // create a File using the provided file/binary data
                    Persistable fileTarget = createFile(contentType, contentDisposition, postBytes);
                    if (!(source instanceof DynaFileDBSource)) {
                      Persistable resourceTarget = Persevere.newObject(((Persistable) target).getId());
                      resourceTarget.put("representation:" + contentType, resourceTarget, fileTarget);
                      ((PersistableObject) resourceTarget).setAttributes("representation:" + contentType,
                          ScriptableObject.DONTENUM);
                    }
                    target = fileTarget;
                    newContent = true;
                    force204 = true;
                  }
                }

                response.setStatus(201);
              } else if ("PUT".equals(method)) {
                boolean isJson = false;
                Identification<? extends Object> id = Identification.idForString(path);
                try {
                  if (couldBeJson(contentType)) {
                    if (postBytes == null)
                      postBytes = IOUtils.toByteArray(request.getInputStream());

                    String postBody = new String(postBytes, "UTF-8");
                    if (id instanceof ObjectPath) {
                      Object value = id.getTarget();
                      if (value instanceof Persistable) {
                        id = ((Persistable) value).getId();
                      }
                    }
                    target = requestHelper.convertWithKnownId(postBody, id);

                    isJson = true;
                  }
                } catch (JSONParser.JSONException e) {
                  if (contentType == null || contentType.indexOf("json") > 0 || contentType.indexOf("javascript") > 0) {
                    throw e;
                  }
                }
                if (!isJson) {
                  // create an alternate representation for the target object using the provided file/binary data
                  DataSource source = target instanceof Persistable ? ((Persistable) target).getId().source : null;
                  String contentDisposition = request.getHeader("Content-Disposition");
                  if (postBytes == null)
                    postBytes = IOUtils.toByteArray(request.getInputStream());
                  Persistable fileTarget = createFile(contentType, contentDisposition, postBytes);
                  if (!(source instanceof DynaFileDBSource) && !(id instanceof ObjectPath)) {
                    if (target instanceof Persistable) {
                      ((Persistable) target).set("representation:" + contentType, fileTarget);
                      ((PersistableObject) target).setAttributes("representation:" + contentType, ScriptableObject.DONTENUM);
                    }
                  }
                  newContent = true;
                  force204 = true;
                  target = fileTarget;
                }
                if (id instanceof ObjectPath) {
                  // if it is a path we set the value of the property
                  Object key = ((ObjectPath) id).getLastPath();
                  Persistable secondToLastTarget = (Persistable) ((ObjectPath) id).getSecondToLastTarget();
                  if (key instanceof Integer)
                    secondToLastTarget.put((Integer) key, secondToLastTarget, target);
                  else {
                    secondToLastTarget.set((String) key, target);
                  }
                } else if (!(target instanceof Persistable))
                  //TODO: This should be allowed if an ObjectPath is used
                  throw new RuntimeException("Can not replace an identified object with a primitive value");
                if (!isJson) {
                  target = Undefined.instance;// no need to return the file just uploaded
                }

              } else if ("DELETE".equals(method)) {
                // delete the target object or property
                Identification<? extends Object> id = Identification.idForString(path);
                if (id instanceof ObjectPath) {
                  // it is a property
                  Object key = ((ObjectPath) id).getLastPath();
                  Persistable secondToLastTarget = (Persistable) ((ObjectPath) id).getSecondToLastTarget();
                  if (key instanceof Integer)
                    secondToLastTarget.delete((Integer) key);
                  else
                    secondToLastTarget.delete((String) key);
                  return null;
                } else if (id instanceof JsonPath) {
                  //TODO: Needs to be a way to do delete items from JSONPath queries
                  for (int i = ((List) target).size(); i > 0;) {
                    i--;
                    Persistable item = ((Persistable) ((List) target).get(i));
                    item.delete();
                  }
                  return null;
                }
                if (target instanceof Persistable) {
                  ((Persistable) target).delete();
                }
                target = Undefined.instance;
              }
              if (datedTarget != null)
                setHeader("Last-Modified", "" + preciseFormatter.format(datedTarget.getLastModified()), headerTarget);
              else
                setHeader("Last-Modified", preciseFormatter.format(new Date()), headerTarget);
              if (target instanceof Persistable
                  && !("PUT".equals(method) || "POST".equals(method) || "DELETE".equals(method) || "GET".equals(method))) {// PUT and POST are special and a result of the modifications and new objects which naturally result from other actions
                Object httpMethod = ScriptableObject.getProperty((Persistable) target, method.toLowerCase()); // TODO: Need to camelcase the method
                // we can handle alternate HTTP methods by defining methods on objects
                if (httpMethod instanceof Function) {
                  // if there is a method, we should call it

                  if (postBytes == null)
                    postBytes = IOUtils.toByteArray(request.getInputStream());
                  String postBody = new String(postBytes, "UTF-8");

                  Object postObject = requestHelper.parseJsponString(postBody); // TODO: Shouldn't do this twice
                  Object bodyData = requestHelper.convertParsedToObject(postObject);
                  String[] parts = FunctionUtils.getSource((Function) httpMethod).split("\\(|\\)", 3);
                  if (parts.length < 2)
                    throw new RuntimeException("Invalid function toString");
                  String[] paramParts = parts[1].split(",");
                  List params = new ArrayList();
                  for (String param : paramParts) {
                    if ("content".equals(param)) {
                      params.add(bodyData);
                    } else if ("uri".equals(param)) {
                      params.add(path);
                    } else {
                      String value = getParameter(request, param);
                      params.add(value);
                    }
                  }
                  target = ((Method) httpMethod).call(PersevereContextFactory.getContext(), GlobalData.getGlobalScope(),
                      (Scriptable) target, params.toArray(), true);
                } else {
                  // indicate what methods are allowed
                  Set<Object> keys = new HashSet<Object>(Arrays.asList(((Persistable) target).getIds()));
                  //TODO: use PersistableObject.entrySet(target, 1);
                  // these are the non-enumerable default methods
                  keys.add("get");
                  keys.add("post");
                  keys.add("delete");
                  keys.add("put");
                  keys.add("head");
                  String methodList = "";
                  for (Object methodKey : keys) {
                    if (((Persistable) target).get(methodKey.toString()) instanceof Method)
                      methodList += methodKey + ", ";
                  }
                  methodList += "OPTIONS";
                  methodList = methodList.toUpperCase();
                  response.addHeader("Allow", methodList);
                  if (method.equals("OPTIONS"))
                    target = "See Allow header for available methods";
                  else {
                    response.setStatus(405); // Method not allowed
                    target = "Method not allowed, see Allow header for available methods";
                  }

                }
              }
              username = UserSecurity.getUserName();
              // we set the username so the client side can access it
              setHeader("Username", "public".equals(username) ? null : username, headerTarget);
              /*
               * if (target) response.setStatus(201);
               */
              /*
               * String range = getParameter(request,"Range"); if
               * (range == null) { range =
               * getParameter(request,"Opera-Range"); }
               *
               * target = handleRange(range,target, new
               * Date(),response);
               */
            } catch (Throwable e) {
              rollback = true;
              log.debug(e);
              throw e;
            } finally {
              if (rollback) // failed, rollback the transaction
                Transaction.currentTransaction().abort();
              else {
                // success
              }
            }
            String output;
            if (newContent && target instanceof Persistable && ((Persistable) target).getId().isAssignedId()) {
              String url = request.getRequestURL().toString();
              int contextPathStart = url.indexOf(request.getContextPath() + '/', 9);
              response.setHeader("Location", url.substring(0, contextPathStart) + request.getContextPath() + '/'
                  + ((Persistable) target).getId().toString());
            }
            if (target == Scriptable.NOT_FOUND || target == Undefined.instance || force204) {
              if ("GET".equals(method))
                response.setStatus(404); // undefined means it's wasn't found
              else
                response.setStatus(204); // undefined could be returned from a method call
            } else {
              String acceptValue = getParameterFromQueryString(request, "http-Accept");
              if(acceptValue == null){
                acceptValue = ScriptableObject.getProperty(env, "HTTP_ACCEPT").toString();
              }
              DataSerializer.serialize(target, acceptValue);
              // just in case any changes were made
            }
         
        } catch (Throwable e) {
          if ("org.mortbay.jetty.RetryRequest".equals(e.getClass().getName())) { // this should not be stopped
            log.debug(e);
            throw (RuntimeException) e;
          }

          log.warn(e);

          while (e.getCause() != null)
            e = e.getCause();

          reason = e.getMessage();
          log.debug("reason :" + reason);
          // output the correct HTTP status code
          if (e instanceof ObjectNotFoundException)
            response.setStatus(404);
          else if (e instanceof SecurityException) {
            response.setHeader("WWW-Authenticate", "JSON-RPC, Basic");
            response.setStatus(401);
          } else if (e instanceof JSONParser.JSONException || e instanceof BadRequestException)
            response.setStatus(400);
          else if (e instanceof ConditionFailedException)
            response.setStatus(412);
          else if (e instanceof RequestedRangeNotSatisfiable)
            response.setStatus(416);
          else if (reason != null && reason.startsWith("URIError"))
            response.setStatus(400);
          else if (reason != null && reason.startsWith("TypeError"))
            response.setStatus(403);
          else if (reason != null && reason.startsWith("AccessError")) {
            response.setHeader("WWW-Authenticate", "JSON-RPC, Basic");
            response.setStatus(401);
          } else {
            if (e instanceof RhinoException)
              log.warn(((RhinoException) e).details() + '\n' + ((RhinoException) e).getScriptStackTrace());
            else
              log.warn("", e);

            response.setStatus(500);
          }
          response.setContentType("application/json");
          if (reason == null) {
            if (writer != null)
              writer.write(JSON.quote("Failed"));
          } else {
            if (writer == null) {
              writer = new PrintWriter(response.getOutputStream());
            }
            if (reason.startsWith("InternalError:"))
              reason = reason.substring(15);
            writer.write(JSON.quote(reason));
          }
        }

        finally {
          // handle the different types of response types (JSONP or window.name)
          if (writer != null && !Boolean.TRUE.equals(request.getAttribute("org.persvr.suspended"))) {
            //writer = new PrintWriter(response.getOutputStream());
            if (response instanceof StringHttpServletResponseWrapper) { // this means we are doing a frame name transport
              HttpServletResponse realResonse = (HttpServletResponse) ((StringHttpServletResponseWrapper) response).getResponse();
              realResonse.getOutputStream().print("<html>" + "<script type='text/javascript'>" + "var loc = window.name;window.name=");
              realResonse.getOutputStream().print(JSON.quote(response.toString()));
              realResonse.getOutputStream().print(";location=loc;");
              realResonse.getOutputStream().print("</script></html>");

            } else {
              if (suffixString != null)
                writer.write(suffixString);
              if (suffixHeader != null)
                writer.write("," + JSON.serialize(suffixHeader) + ");");
            }
            writer.flush();
            response.setHeader("Accept-Ranges", "bytes,items");

            writer.close();
          }
          Status.addRequestTiming(System.currentTimeMillis() - startTime);

          if (log.isDebugEnabled()) {
            log.debug("Request timing: " + (System.currentTimeMillis() - startTime));
          }
        }
        return null;

      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }

  /**
   * Create a new instance of the File JS class in Persevere with the provided
   * binary data
   *
   * @param contentType
   * @param data
   * @return
   * @throws IOException
   */
  private static Persistable createFile(String contentType, String contentDisposition, Object data) throws IOException {
    Persistable fileTarget = Persevere.newObject("File");
    String charSet = null;

    if (contentType != null) {
      String[] contentTypeParts = contentType.split(";\\s*");
      contentType = contentTypeParts[0];
      for (String part : contentTypeParts) {
        if (part.startsWith("charset")) {
          charSet = part.split("=")[1];
        }
      }
    }
    fileTarget.set("contentType", contentType);
    if(contentDisposition != null)
      fileTarget.set("contentDisposition", contentDisposition);
    contentType = contentType == null ? null : contentType.split(";")[0];
    if (contentType != null && (contentType.startsWith("text/") || charSet != null)) {
      if (data instanceof InputStream)
        fileTarget.set("content", IOUtils.toString(new InputStreamReader((InputStream) data, charSet == null ? "UTF-8" : charSet)));
      else
        fileTarget.set("content", new String((byte[]) data, charSet == null ? "UTF-8" : charSet));
    } else {
      if (data instanceof InputStream)
        fileTarget.set("content", new BinaryData((InputStream) data));
      else
        fileTarget.set("content", new BinaryData((byte[]) data));
    }
    return fileTarget;
  }

  private static boolean couldBeJson(String contentType) {
    return contentType == null || contentType.indexOf("json") > 0 || contentType.indexOf("javascript") > 0
        || contentType.indexOf("www-form-urlencoded") > 0 || contentType.indexOf("application/xml") > -1 || contentType.indexOf("text/plain") > -1;
  }

  private static Object postObject(Persistable target, Object bodyData) {
    if (target instanceof PersistableClass) {
      // if we post to a schema, we can just consider this a post to the table
      target = (Persistable) Identification.idForString(((PersistableClass) target).getId().toString() + "/").getTarget();
    }
    if (bodyData instanceof Persistable) {
      Persistable newObject = (Persistable) bodyData;
      ScriptableObject arrayProto = (ScriptableObject) ScriptableObject.getClassPrototype(GlobalData.getGlobalScope(), "Array");
      ScriptableObject objectProto = (ScriptableObject) ScriptableObject.getClassPrototype(GlobalData.getGlobalScope(), "Object");

      if ("".equals(((Persistable) target).getId().subObjectId)
          && (((Persistable) bodyData).getPrototype() == objectProto || ((Persistable) bodyData).getPrototype() == arrayProto)) {
        // this means it is a generic object or list, we need to make it be the right class
        DataSource thisSource = ((Persistable) target).getId().source;
        Class targetClass = DataSourceManager.getObjectsClass(thisSource).objectsClass;
        if (bodyData instanceof List) {
          // this means that an array was provided for a data source that takes objects; we
          //  will assume this means that the user wants to create multiple objects
          int i = 0;
          for (Object obj : (List) bodyData) {
            if (obj instanceof Persistable) {
              newObject = Persevere.newObject(((Persistable) target).getId());
              for (Map.Entry<String, Object> entry : ((Persistable) obj).entrySet(0)) {
                newObject.set(entry.getKey(), entry.getValue());
              }
              ((List) bodyData).set(i++, newObject);
            } else
              throw new RuntimeException("Bulk update arrays should only include objects");
          }
          return bodyData;
        } else {
          newObject = bodyData instanceof List ? Persevere.newArray(((Persistable) target).getId()) : Persevere
              .newObject(((Persistable) target).getId());
          for (Map.Entry<String, Object> entry : ((Persistable) bodyData).entrySet(0)) {
            newObject.set(entry.getKey(), entry.getValue());
          }
        }
      }
      Client.getCurrentObjectResponse().getConnection().changeClientSideObject((Persistable) bodyData, newObject);
      bodyData = newObject;
      if ("".equals(((Persistable) target).getId().subObjectId))
        return newObject; // all that needs to be done in adding a posting to the root list
    }
    if ("".equals(((Persistable) target).getId().subObjectId))
      throw new RuntimeException("You can only add objects to tables");

    if (target instanceof List)
      ((List) target).add(bodyData);
    else if (bodyData instanceof Persistable){ // TODO: may want this to be synchronized
      for(Map.Entry entry : ((Persistable)bodyData).entrySet(0)){
        target.set(entry.getKey().toString(), entry.getValue());
      }
      bodyData = target;
    }
    else
      throw new BadRequestException("Can only POST an object");
    return bodyData;
  }

  public static void setHeader(String name, String value, Object headerTarget) {
    if (headerTarget instanceof HttpServletResponse)
      ((HttpServletResponse) headerTarget).setHeader(name, value);
    else if (headerTarget instanceof Map) {
      try {
        ((Map) headerTarget).put(name, value);
      } catch (JSONException e) {
        throw new RuntimeException(e);
      }
    } else
      throw new RuntimeException("Unacceptable header target");
  }

  /**
   * The destroy method is called by the web container when the servlet is
   * taken out of service and should not be called directly. In this case, the
   * method calls the DataSourceManager to iterate through the data sources
   * that may require clean-up.
   *
   * @return
   */
  public void destroy() {
    DataSourceManager.destroy();
  }

  private static void setCacheHeader(HttpServletResponse response, long expirationTime) {
    //response.setHeader("Pragma", "");
    //response.setHeader("Cache-Control", "must-revalidate");
    Date expirationDate = new Date(new Date().getTime() + expirationTime);
    DateFormat formatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz", Locale.US);
    formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
    response.setHeader("Expires", "Thu, 01 Jan 1970 01:00:00 GMT");
  }

  @Override
  public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    setCacheHeader(response, 2000);
    RequestHelper requestHelper = new RequestHelper(request, response);
    doPost(requestHelper);
  }

  static class BytesServletOutputStream extends ServletOutputStream {
    ByteArrayOutputStream os = new ByteArrayOutputStream();

    public void write(byte[] b, int off, int len) {
      os.write(b, off, len);
    }

    public void write(byte[] b) throws IOException {
      os.write(b);
    }

    public void write(int b) {
      os.write(b);
    }

    public void writeTo(OutputStream out) throws IOException {
      os.writeTo(out);
    }

    public String toString() {
      try {
        return os.toString("UTF-8");
      } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
      }
    }

  }

  static class StringHttpServletResponseWrapper extends HttpServletResponseWrapper {

    public StringHttpServletResponseWrapper(HttpServletResponse response) {
      super(response);
    }

    BytesServletOutputStream os = new BytesServletOutputStream();;

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
      return os;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
      return new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
    }

    public String toString() {
      return os.toString();
    }

    @Override
    public void setContentType(String type) {
    }
  }

  public static interface LocalDataSource {
    public boolean passThrough();
  }

}
TOP

Related Classes of org.persvr.remote.PersevereFilter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.