使 Wicket 行为 return JSON 结果与 jQuery FileUpload 集成

Make Wicket behavior return JSON result for integration with jQuery FileUpload

为了将 jQuery FileUpload 集成到我的 Wicket 项目中,我注册了一个 AbstractAjaxBehavior 并将其 URL 传递给文件输入组件,以便它可以传递给 jQuery 文件上传。也就是说,在文件上传面板的构造函数中:

AbstractAjaxBehavior fileUploadBehavior = new AbstractDefaultAjaxBehavior() {
  @Override
  protected synchronized void respond(AjaxRequestTarget target) {
    // TODO Handle incoming file(s)...
  }
};
WebMarkupContainer file = new WebMarkupContainer("file") {
  @Override
  protected void onComponentTag(ComponentTag tag) {
    super.onComponentTag(tag);

    IValueMap attributes = tag.getAttributes();
    attributes.put("data-upload-url", fileUploadBehavior.getCallbackUrl());
  }
};
add(file);
file.add(fileUploadBehavior);

问题是我似乎无法阻止 return 状态 302 重定向到我的 "stale page" 错误页面的行为。

因此问题是:如何阻止此重定向,而不是 return jQuery FileUpload 期望的 JSON 响应?

可以通过调用

来抑制重定向
RequestCycle requestCycle = getRequestCycle();
requestCycle.scheduleRequestHandlerAfterCurrent(null);

然后可以使用

执行自定义响应
WebResponse response = (WebResponse) requestCycle.getResponse();
response.write(...);

上传的文件可以通过以下方式访问:

ServletWebRequest request = (ServletWebRequest) requestCycle.getRequest();
ServletFileUpload servletFileUpload = new ServletFileUpload(YourFileItemImpl::new);
List<FileItem> fileItems = servletFileUpload.parseRequest(request.getContainerRequest());

其中 ServletFileUpload 来自 Apache Commons FileUpload 库,YourFileItemImpl 是来自同一库的 FileItem 接口的一些实现。 class 应该至少包含 getNamegetSizegetOutputStream 方法的适当实现(后者是执行持久化的方法)。

现在可以迭代 fileItems 列表以构建要传递给 response.write(...) 的适当响应。

总的来说,我们最终实现了以下行为:

AbstractAjaxBehavior fileUploadBehavior = new AbstractDefaultAjaxBehavior() {
  @Override
  protected synchronized void respond(AjaxRequestTarget target) {
    RequestCycle requestCycle = getRequestCycle();

    // Prevent default redirection.
    requestCycle.scheduleRequestHandlerAfterCurrent(null);

    ServletWebRequest request = (ServletWebRequest) requestCycle.getRequest();
    ServletFileUpload servletFileUpload = new ServletFileUpload(YourFileItemImpl::new);

    try {
      // Initialize JSON response.
      JSONObject jsonResponse = new JSONObject();
      JSONArray jsonFiles = new JSONArray();
      jsonResponse.put("files", jsonFiles);

      // Parse and persist uploaded file(s).
      List<FileItem> fileItems = servletFileUpload.parseRequest(request.getContainerRequest());
      // Iterate file items to build JSON response.
      for (FileItem item : fileItems) {
        JSONObject jsonFile = new JSONObject();
        jsonFiles.put(jsonFile);

        jsonFile.put("name", item.getName());
        jsonFile.put("size", item.getSize());

        // TODO Perform validation, e.g. using Apache Tika for file type detection.
        //      Add any error using `jsonFile.put("error", "[error_message]")`. Should
        //      of course take care to not persist invalid files...
     }

     // Write JSON response.
     WebResponse response = (WebResponse) requestCycle.getResponse();
     response.setHeader("Content-Type", "text/html; charset=utf8"); // Because IE...
     response.write(jsonResponse.toString());
   } catch (FileUploadException | IOException | JSONException e) {
     // TODO Handle exception.
   }
 }

您可能还想捕获 fileuploaddone 事件,以便您可以执行适当的 UI 更新(您不能再使用 fileUploadBehavior 来执行此操作 - 但您也只想每个文件批次执行一次 UI 更新):

file.add(new AjaxEventBehavior("fileuploaddone") {
  @Override
  protected void onEvent(AjaxRequestTarget target) {
    // Wait until behavior has completed.
    synchronized (fileUploadBehavior) {
      // TODO Add proper components to `target`.
    }
  }
});

同步 respond 方法有两个目的:

  1. 一次只接受一个(可能是多个文件)上传。这可以防止单个面板最终接受多个文件的竞争条件。

  2. 确保在 respond 回调完成之前不处理 fileuploaddone 事件,即使它在此之前被触发。虽然这在实践中不应该发生,jQuery FileUpload 可能存在错误,这确保了对它(以及恶意用户)的鲁棒性。

最后,添加

可以允许上传多个文件
file.add(new AttributeModifier("multiple", "multiple"));

相反,如果 single-file 需要上传,行为应该验证这一点。