DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations

Ajax Upload for Tapestry

Taha Siddiqi user avatar by
Taha Siddiqi
·
Jun. 27, 11 · Interview
Like (0)
Save
Tweet
Share
7.16K Views

Join the DZone community and get the full member experience.

Join For Free

Tapestry has an excellent support for JavaScript and Ajax. It strikes a perfect balance between how much a framework should assist and how much the developer should do. Most component-based frameworks, rather than assisting, supervise and most action-based frameworks leave even the integration to the developer. Tapestry provides you with events which you can easily connect to your JavaScript events/functions and all the rest is, as I keep on saying, magic.

In this post, I will talk about integrating an Ajax based upload library, file-uploader with Tapestry.

Ajax Upload

Ajax based upload is not fully supported by all browsers. So this library falls back to iframe based form submission in case the browser does not support Ajax upload. If you want to know more about Ajax-upload you can read the very well written source code of this library along with this.

Although this library is very good but it lacks a few features which I had to add myself.

  1. Once an attachment is uploaded, there is no way to remove that attachment.
  2. If the form is to be resubmitted, the previous uploaded files should be displayed as well
  3. The text displayed on the buttons is not configurable

With such a nicely written script, it was quite easy to accomplish this. I am not going to discuss the modifications except for those which are directly related to the integration.

Decoding the request

The upload request can come in two forms, either as an Ajax-request with input stream as the uploaded file or as a Multipart request. The latter is already taken care of by tapestry-upload module’s MultipartServletRequestFilter which checks if the request is multipart, and in case it is, then decodes it using MultipartDecoder. The decoder uses the Apache commons upload library to decode the request and stores the uploaded files in the form of UploadedFile. The only thing that compelled me to override MultipartDecoder service is the cleanup it does at the end of the request. As Ajax upload will be using multiple requests(one for each upload and one of form submission), the cleanup has to be prevented, so in the Module class we override the service

@Scope(ScopeConstants.PERTHREAD)
public static MultipartDecoder buildMultipartDecoder2(RegistryShutdownHub shutdownHub,
      @Autobuild MultipartDecoderImpl multipartDecoder)
{

   if (needToAddShutdownListener.getAndSet(false))
   {
      shutdownHub.addRegistryShutdownListener(new RegistryShutdownListener()
      {
         @Override
         public void registryDidShutdown()
         {
            FileCleaner.exitWhenFinished();
         }
      });
   }

   return multipartDecoder;
}

public static void contributeServiceOverride(@InjectService("MultipartDecoder2") MultipartDecoder multipartDecoder,
      @SuppressWarnings("rawtypes") MappedConfiguration<Class, Object> overrides)
{
   overrides.add(MultipartDecoder.class, multipartDecoder);
}

The Ajax upload request is decoded in the same way. There is a AjaxUploadServletRequestFilter which checks if the request is an Ajax upload request and in case it is then passes the request to AjaxUploadDecoder for extracting the uploaded file.

public class AjaxUploadServletRequestFilter implements HttpServletRequestFilter
{

   private AjaxUploadDecoder decoder;

   public AjaxUploadServletRequestFilter(AjaxUploadDecoder decoder)
   {
      this.decoder = decoder;
   }

   @Override
   public boolean service(HttpServletRequest request,
          HttpServletResponse response, HttpServletRequestHandler handler)
         throws IOException
   {
      if (decoder.isAjaxUploadRequest(request))
      {
         decoder.setupUploadedFile(request);
      }

      return handler.service(request, response);
   }

}

The interface and implementation of Ajax upload decoder is below

public interface AjaxUploadDecoder
{

   boolean isAjaxUploadRequest(HttpServletRequest request);

   boolean isAjaxUploadRequest(Request request);

   UploadedFile getFileUpload();

   void setupUploadedFile(HttpServletRequest request);

}

public class AjaxUploadDecoderImpl implements AjaxUploadDecoder
{
   private UploadedFileItem uploadedFile;

   public static final String AJAX_UPLOAD_HEADER = "X-File-Name"; 

   private FileItemFactory fileItemFactory;

   public AjaxUploadDecoderImpl(FileItemFactory fileItemFactory)
   {
      this.fileItemFactory = fileItemFactory;
   }

   @Override
   public boolean isAjaxUploadRequest(HttpServletRequest request)
   {
      return request.getHeader(AJAX_UPLOAD_HEADER) != null;
   }

   @Override
   public boolean isAjaxUploadRequest(Request request)
   {
      return request.isXHR() && request.getHeader(AJAX_UPLOAD_HEADER) != null;
   }

   @Override
   public void setupUploadedFile(HttpServletRequest request)
   {
      String fieldName = request.getHeader(AJAX_UPLOAD_HEADER);
      FileItem item = fileItemFactory.createItem(fieldName,
            request.getContentType(), false,
            request.getParameter(AjaxUploadConstants.FILE_PARAMETER));
      try
      {
         TapestryInternalUtils.copy(request.getInputStream(), item.getOutputStream());
      }
      catch (IOException e)
      {
         throw new RuntimeException("Could not copy request's input stream to file", e);
      }

      uploadedFile = new UploadedFileItem(item);
   }

   @Override
   public UploadedFile getFileUpload()
   {
      return uploadedFile;
   }

}

The main job of creating the upload is delegated to the FileItemFactory of apache commons upload library.

Integration

The upload script requires the following arguments :-

  1. element: Element which will be used for creating the component
  2. action: URL to which the file will be sent. The script expects a JSON in respose with success = true for successful upload. In case of an error, an error field is expected to contain the error message
  3. cancelLink: URL which will be called when upload is cancelled
  4. removeLink: URL which will be called when an uploaded file is to be removed
  5. initializeUploadsLink: URL which will be called when the component is loaded. The script expects a JSON containing a list of initially uploaded files, in case of resubmission
  6. sizeLimit: Maximum size of file allowed to be uploaded
  7. name: Name of the file field created
  8. uploadText: Text to be displayed on the upload button
  9. dropText: Text to be displayed on the drop area, in case of a drag-n-drop

All this is done by the component AjaxUpload

@SupportsInformalParameters
@Import(library = "ajaxupload.js", stylesheet = "ajaxupload.css")
public class AjaxUpload extends AbstractField
{

   public static final String FILE_PARAMETER = "qqfile";
   private static final String STYLE_TO_HIDE_INPUT_TEXT = "display:inline;"
         + "color:transparent;background:transparent;" + "border:0;height:1px;width:1px;";

   @Inject
   private JavaScriptSupport javaScriptSupport;

   @Inject
   private ComponentResources resources;

   @Parameter(required = true, autoconnect = true, principal = true)
   private List<UploadedFile> value;

   @SuppressWarnings("unused")
   @Parameter
   private boolean uploaded;

   @Symbol(UploadSymbols.REQUESTSIZE_MAX)
   @Inject
   private int maxSize;

   @Parameter(value = "1", defaultPrefix = BindingConstants.LITERAL)
   private int maxFiles;

   @Inject
   private Request request;

   @Inject
   private Messages messages;

   @Inject
   private MultipartDecoder multipartDecoder;

   @Inject
   private AjaxUploadDecoder ajaxDecoder;

   /**
    * The object that will perform input validation. The "validate:" binding
    * prefix is generally used to provide this object in a declarative fashion.
    */
   @Parameter(defaultPrefix = BindingConstants.VALIDATE)
   private FieldValidator<Object> validate;

   @Environmental
   private ValidationTracker tracker;

   @SuppressWarnings("unused")
   @Environmental
   private FormSupport formSupport;

   @Inject
   private ComponentDefaultProvider defaultProvider;

   @Inject
   private FieldValidationSupport fieldValidationSupport;

   @SuppressWarnings("unused")
   @Mixin
   private RenderDisabled renderDisabled;

   /**
    * Computes a default value for the "validate" parameter using
    * {@link FieldValidatorDefaultSource}.
    */
   final Binding defaultValidate()
   {
      return defaultProvider.defaultValidatorBinding("value", resources);
   }

   public AjaxUpload()
   {
   }

   // For testing
   AjaxUpload(List<UploadedFile> value, FieldValidator<Object> validate,
         MultipartDecoder multipartDecoder,
         AjaxUploadDecoder ajaxDecoder,
         ValidationTracker tracker, ComponentResources resources,
         FieldValidationSupport fieldValidationSupport,
         JavaScriptSupport javaScriptSupport)
   {
      this.value = value;
      if (validate != null)
         this.validate = validate;
      this.multipartDecoder = multipartDecoder;
      this.tracker = tracker;
      this.resources = resources;
      this.fieldValidationSupport = fieldValidationSupport;
      this.javaScriptSupport = javaScriptSupport;
      this.ajaxDecoder = ajaxDecoder;
      maxFiles = 1;
   }

   void beginRender(MarkupWriter writer)
   {
      writer.element("input", "type", "text", "id",
          getClientId(), "style", STYLE_TO_HIDE_INPUT_TEXT, "name",
            getControlName());
      validate.render(writer);
      decorateInsideField();
   }

   private String getWrapperClientId()
   {
      return getClientId() + "_wrapper";
   }

   private String getFileControlName()
   {
      return getControlName() + "_file";
   }

   void afterRender(final MarkupWriter writer)
   {
      writer.end();
      writer.element("span", "id", getWrapperClientId(), "style", "display:inline-block");
      writer.end();
   }

   @AfterRender
   void addJavaScript()
   {
      JSONObject arguments = fillArguments();
      javaScriptSupport.addScript("new qq.FileUploader(%s);", arguments);
   }

   JSONObject fillArguments()
   {
      JSONObject spec = new JSONObject();
      for (String informalParameter : resources.getInformalParameterNames())
      {
         spec.put(informalParameter,
           resources.getInformalParameter(informalParameter, String.class));
      }

      spec.put("element", getElementId());
      spec.put("sizeField", getControlName());
      spec.put("uploadText", messages.get("ajaxupload.upload-text"));
      spec.put("dropText", messages.get("ajaxupload.drop-text"));
      spec.put("action", getUploadLink());
      spec.put("cancelLink", getCancelLink());
      spec.put("removeLink", getRemoveLink());
      spec.put("initializeUploadsLink", getInitializeUploadsLink());
      spec.put("sizeLimit", maxSize < 0 ? 0 : maxSize);
      spec.put("name", getFileControlName());
      spec.put("id", getFileControlName());
      return spec;
   }

   private Object getElementId()
   {
      return new JSONLiteral("document.getElementById('" + getWrapperClientId() + "')");
   }

   String getUploadLink()
   {
      final Link link = resources.createEventLink("upload");
      return link.toAbsoluteURI();
   }

   String getCancelLink()
   {
      final Link cancelLink = resources.createEventLink("cancelUpload");
      return cancelLink.toAbsoluteURI();
   }

   String getRemoveLink()
   {
      final Link removeLink = resources.createEventLink("removeUpload");
      return removeLink.toAbsoluteURI();
   }

   String getInitializeUploadsLink()
   {
      final Link initializeUploadsLink = resources.createEventLink("initializeUploads");
      return initializeUploadsLink.toAbsoluteURI();
   }

   /**
    * Converts the current list of uploaded files to a JSON object containing a json array
    *  with each element containing the name of the file and a unique key for
    * identification. The unique key is index of the uploaded file in parameter
    * <code>value</code>
    *
    * @return
    */
   JSONObject onInitializeUploads()
   {
      JSONArray array = new JSONArray();
      if (value != null)
      {
         for (int i = 0; i < value.size(); ++i)
         {
            if (value.get(i) == null)
            {
               continue;
            }
            JSONObject indexWithFileName = new JSONObject();
            indexWithFileName.put("serverIndex", i);
            indexWithFileName.put("fileName", value.get(i).getFilePath());
            array.put(indexWithFileName);
         }
      }
      return new JSONObject().put("uploads", array);
   }

   Object onUpload()
   {
      if (hasMaximumFileUploadCountReached())
      {
         return createFailureResponse(messages.format("ajaxupload.maxfiles", maxFiles));
      }

      final UploadedFile uploadedFile;
      if (isAjaxUpload())
      {
         uploadedFile = createUploadedFileFromRequestInputStream();
      }
      else
      {
         uploadedFile = createUploadedFileFromMultipartForm();
      }

      if (value == null)
      {
         value = new ArrayList<UploadedFile>();
      }

      value.add(uploadedFile);

      return createSuccessResponse(value.size() - 1); // Last index
   }

   private boolean hasMaximumFileUploadCountReached()
   {
      if (value == null)
      {
         return maxFiles <= 0;
      }

      // Can't rely on value's size as some of the values can be null
      int size = 0;
      for (UploadedFile uploadedFile : value)
      {
         if (uploadedFile != null)
         {
            size++;
         }
      }
      return this.maxFiles <= size;
   }

   private boolean isAjaxUpload()
   {
      return ajaxDecoder.isAjaxUploadRequest(request);
   }

   private UploadedFile createUploadedFileFromMultipartForm()
   {
      return multipartDecoder.getFileUpload(FILE_PARAMETER);
   }

   private UploadedFile createUploadedFileFromRequestInputStream()
   {
      return ajaxDecoder.getFileUpload();
   }

   private Object createFailureResponse(String errorMessage)
   {
      JSONObject response = new JSONObject();
      response.put("success", false);
      response.put("error", errorMessage);
      if (!request.isXHR())
      {
         return new StatusResponse(response.toString());
      }
      else
      {
         return response;
      }
   }

   private Object createSuccessResponse(int serverIndex)
   {
      JSONObject response = new JSONObject();
      response.put("success", true);
      response.put("serverIndex", serverIndex);
      if (!request.isXHR())
      {
         return new StatusResponse(response.toString());
      }
      else
      {
         return response;
      }
   }

   void onRemoveUpload(int serverIndex)
   {
      // We use index of an uploadedFile in 'value' as a key at
      // the client side and if the uploaded file is removed we cleanup and set
      // the element at that index to null. As the 'value' may contain null, we
      // need to remove those entries in processSubmission()
      if (value != null && serverIndex >= 0 && serverIndex < value.size())
      {
         UploadedFile item = value.get(serverIndex);
         if (item != null && (item instanceof UploadedFileItem))
         {
            ((UploadedFileItem) item).cleanup();
         }
         value.set(serverIndex, null);
      }
   }

   void onCancelUpload(String fileName)
   {
      // TODO: Some how remove the partially uploaded file
   }

   @Override
   protected void processSubmission(String elementName)
   {
      // Nothing to process from current request as the uploads have already
      // been received and stored in value
      if (value != null)
      {
         // Remove any null values in 'value'
         removeNullsFromValue();
      }

      try
      {
         fieldValidationSupport.validate(value, resources, validate);
      }
      catch (ValidationException ex)
      {
         tracker.recordError(this, ex.getMessage());
      }
   }

   private void removeNullsFromValue()
   {
      List<UploadedFile> uploads = new ArrayList<UploadedFile>();
      for (UploadedFile upload : value)
      {
         if (upload != null)
         {
            uploads.add(upload);
         }
      }
      value = uploads;
   }

   public List<UploadedFile> getValue()
   {
      return value;
   }

   @Override
   public boolean isRequired()
   {
      return value != null && value.size() > 0;
   }

   AjaxUpload injectDecorator(ValidationDecorator decorator)
   {
      setDecorator(decorator);

      return this;
   }

   AjaxUpload injectRequest(Request request)
   {
      this.request = request;

      return this;
   }

   AjaxUpload injectFormSupport(FormSupport formSupport)
   {
      // We have our copy ...
      this.formSupport = formSupport;

      // As does AbstractField
      setFormSupport(formSupport);

      return this;
   }

   AjaxUpload injectFieldValidator(FieldValidator<Object> validator)
   {
      this.validate = validator;

      return this;
   }

   void injectResources(ComponentResources resources)
   {
      this.resources = resources;
   }

   void injectValue(List<UploadedFile> value)
   {
      this.value = value;
   }

   void injectMessages(Messages messages)
   {
      this.messages = messages;
   }

   static class StatusResponse implements StreamResponse
   {

      private String text;

      StatusResponse(String text)
      {
         this.text = text;
      }

      @Override
      public String getContentType()
      {
         return "text/html";
      }

      @Override
      public InputStream getStream() throws IOException
      {
         return new ByteArrayInputStream(text.toString().getBytes());
      }

      @Override
      public void prepareResponse(Response response)
      {

      }

      public JSONObject getJSON()
      {
         return new JSONObject(text);
      }
   }

   public void injectFieldValidationSupport(FieldValidationSupport support)
   {
      this.fieldValidationSupport = support;
   }

}

Note, I have embedded a textfield into the component. The reason for doing this is to facilitate client-side validation support, which requires an input(as it uses the form attribute of the component, hidden can’t be used as it is not visible).

In the upload request, we check to see the type of request and then get the uploaded file from the corresponding decoder. We send the index of the uploaded file to the script which is used for uniquely identifying the uploaded file. When the file is to be removed, this index is sent with the request and based on that index, the file is removed.

Usage

It can be used in the template as

<form t:type='form'>
   <input t:type='tawus/ajaxupload' t:id='uploads'/>
</form>

and in the class file 

@Persist
@Property
private List<UploadedFile> uploads;

void onSuccess(){
   //Use uploads
   uploads.clear();
}

The value has to be persisted at the value has to live multiple requests.

You can find the module here

 

From http://tawus.wordpress.com/2011/06/25/ajax-upload-for-tapestry/

Upload Requests

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • OWASP Kubernetes Top 10
  • Tracking Software Architecture Decisions
  • Rust vs Go: Which Is Better?
  • Use Golang for Data Processing With Amazon Kinesis and AWS Lambda

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: