Latest Posts

Archives [+]

File Uploads in Sling

In the JCR Cup support forum there was an interesting question regarding user-generated content. In particular it was asked how users could upload content in Apache Sling. Here's some information about that:

Basically, just using a form with the POST method and the proper encoding will already do the job:

<form action="/content/firststeps/uploads/*" method="POST" enctype="multipart/form-data"><h2>Name</h2><p><input type="text" name="title" /></p><p><input type="file" name="myfile" /></p><p><input type="submit"></p></form>

Posting this form will create a new node below /content/firststeps/uploads that contains the included file "myfile" as a child.

Further options that can be used are available and documented in the SlingFileUploadHandler.java's comments:

* Simple example: * <xmp> *   <form action="/home/admin" method="POST" enctype="multipart/form-data"> *     <input type="file" name="./portrait" /> *   </form> * </xmp> * * this will create a nt:file node below "/home/admin" if the node type of * "admin" is (derived from) nt:folder, a nt:resource node otherwise. * <p/> * * Filename example: * <xmp> *   <form action="/home/admin" method="POST" enctype="multipart/form-data"> *     <input type="file" name="./*" /> *   </form> * </xmp> * * same as above, but uses the filename of the uploaded file as name for the * new node. * <p/> * * Type hint example: * <xmp> *   <form action="/home/admin" method="POST" enctype="multipart/form-data"> *     <input type="file" name="./portrait" /> *     <input type="hidden" name="./portrait@TypeHint" value="my:file" /> *   </form> * </xmp> * * this will create a new node with the type my:file below admin. if the hinted * type extends from nt:file an intermediate file node is created otherwise * directly a resource node.

Case closed? Not quite. What if you would want to do some fancy processing of the incoming request, say, check if it contains a virus. In that case you need to implement the file /apps/firststeps/POST.jsp (note the capitalized file name) yourself in order to overwrite Sling's default handling of POST requests. It could look like this:

<%@page import="javax.jcr.Session"%><%@page import="javax.jcr.Node"%><%@page import="java.io.ByteArrayInputStream"%><%@page import="java.util.Calendar"%><%@taglib prefix="sling" uri="http://sling.apache.org/taglibs/sling/1.0"%><sling:defineObjects/><%// get the root nodeSession mySession = slingRequest.getResourceResolver().adaptTo(Session.class);Node root = mySession.getRootNode();// create a new node of type nt:fileNode myNewNode = root.getNode("content").addNode("mynewnode", "nt:file");Node contentNode = myNewNode.addNode("jcr:content", "nt:resource");// set the mandatory propertiescontentNode.setProperty("jcr:data", new ByteArrayInputStream(request.getParameter("myfile").getBytes()));contentNode.setProperty("jcr:lastModified", Calendar.getInstance());contentNode.setProperty("jcr:mimeType", "mymimetype");mySession.save();%>

This jsp creates a new node of type nt:file at /content/mynewnode and stores the incoming file in the property jcr:data of the child node jcr:content. It also needs to write the required properties jcr:lastModified and jcr:mimeType. In order to do this a session is required which is retrieved from the request object. Also note the necessary <sling:defineObjects/> tag.

The jsp essentially does the same as the default handler does. However, you could also do something completely different as well. For example, you could store incoming files below a different folder or add other custom properties or child nodes.

JCR EventListeners

There is also another way of dealing with incoming files in a custom manner: one can use the first simple HTML form and have a JCR EventListener listen for incoming files. Once the listener gets an event it can do whatever custom logic it requires.

If you are new to JCR Observations you might first want to look at O'Reilly's tutorial on advanced JCR where JCR Versioning and Observations are introduced.

EventListeners usually run in a separate JVM, so the first thing it must do is connect to the repository. If you want to do this via RMI and use CRX make sure that RMI connections are enabled. In CRX Quickstart RMI is configured in crx-quickstart\server\runtime\0\_crx\WEB-INF.web.xml. The default is switched off (I believe). Uncomment the RMI section below the part

    <!--        RMI configuration    -->

and restart your server. Afterwards register an event listener like this by running the code in a seperate JVM (the full code is attached):

public class MyUploadListener {  public static void main(String[] args) throws MalformedURLException, ClassCastException, RemoteException, NotBoundException, LoginException, RepositoryException, InterruptedException {    ClientRepositoryFactory factory = new ClientRepositoryFactory();    Repository repository = factory.getRepository("//localhost:1234/crx");    Session session = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));    ObservationManager observationManager = session.getWorkspace().getObservationManager();    String[] types = {"nt:file"};    observationManager.addEventListener(new EventListener() {      public void onEvent(EventIterator eventIterator) {        while (eventIterator.hasNext()) {          Event event = eventIterator.nextEvent();          if (event.getType() == Event.NODE_ADDED) {       try {              System.out.println("new upload: " + event.getPath());       } catch (RepositoryException e) {         e.printStackTrace();       }     } }      }    },    Event.NODE_ADDED,"/content/firststeps/uploads",true,null,types,false);        while(true) {      System.out.println("I am still here");        Thread.sleep(5000);    }  }}

The code above is a straight forward EventListener that listens for added nodes of type nt:file at path /content/firststeps/uploads. However, note the clunky endless loop in the end. It is required for not losing the connection to the repository.

This ugly endless loop and enabling RMI can be avoided by registering the EventListener within an OSGi bundle that is deployed in Sling's OSGi container. When an OSGi service gets started the container calls the activate method. In there we can register the EventListener like this:

public class ListenerServiceImpl implements ListenerService {  /** @scr.reference */  private SlingRepository repository;  private static final Logger log = LoggerFactory.getLogger(ListenerServiceImpl.class);  public void log(String text) {    log.error(text);  }  protected void activate(ComponentContext context) {    try {      Session session = repository.loginAdministrative(null);...    } catch (Exception e) {      log("cannot start");    }}

Note that in this case the session is retrived through

Session session =  repository.loginAdministrative(null);

so we are logged in as admin without the need for an RMI connection. Apart from that the JCR code is the same as in the example above. A full tutorial on how to write and deploy OSGi bundles with Sling can be found here and here. The Maven pom.xml and the Java code for the complete bundle is also attached to this post.

A crucial difference between the methods involving EventListeners and those that do not is that EventListeners operate on an admin session (or similar technical user). If you use the POST requests the code is executed on behalf of the logged in user. Depending on the circumstances this might be beneficial or not. For example, if you intend to move incoming files to a folder where only admin has write access you need to use the EventListener.

 

COMMENTS

  • By Alexander Klimetschek - 4:24 PM on Jul 08, 2008   Reply
    And if you create a session and register an event listener in the component activate() method, please make sure that you unregister the event listener and close the session in the deactivate() method. Otherwise the bundle cannot be removed, since the repository still has references to the session and event listeners. As a bundle update (which you typically do very often during development) will lead to multiple bundles in different versions inside the OSGi container, you get weird errors... just went through it ;-)<br/><br/>Here is the code for the deactivate method - a session.logout() is ok inside Sling, it will handle the unregistering of event listeners automatically.<br/><br/>protected void deactivate(ComponentContext componentContext) {<br/> if (session != null) {<br/> session.logout();<br/> session = null;<br/> }<br/>}<br/><br/><br/>
    • By Brian Riggs - 2:14 AM on Feb 08, 2011   Reply
      If you need to do post-processing (as in the third code snippet), keep in mind the ambiguities around the encoding of request parameters (as mentioned on the Sling site at http://sling.apache.org/site/request-parameters.html). With the code snippet above, the file we uploaded was saved with the wrong encoding.

      To work around this, we did the following (instead of calling request.getParameter):

      contentNode.setProperty("jcr:data", slingRequest.getRequestParameter("myfile").getInputStream());
    • By Parvez Ahmad Hakim - 12:50 PM on Jul 20, 2011   Reply
      this is not useful code. I wanted to upload a file and set its properties, but no success
      Parvez Ahmad Hakim
      www.abobjects.com
      My code is package org.cord.client.dms.mainapp.dialogs;

      import org.apache.commons.httpclient.Credentials;
      import org.apache.commons.httpclient.Header;
      import org.apache.commons.httpclient.HttpClient;
      import org.apache.commons.httpclient.HttpException;
      import org.apache.commons.httpclient.HttpMethodBase;
      import org.apache.commons.httpclient.HttpStatus;
      import org.apache.commons.httpclient.NameValuePair;
      import org.apache.commons.httpclient.UsernamePasswordCredentials;
      import org.apache.commons.httpclient.auth.AuthScope;
      import org.apache.commons.httpclient.methods.PostMethod;
      //import org.cord.client.shared.fileship.webdav.internal.general.GlobalObjects;

      import java.io.BufferedReader;
      import java.io.InputStreamReader;
      import java.util.List;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.OutputStream;
      import java.util.ArrayList;
      import java.util.HashMap;
      import java.util.List;
      import java.util.Map;
      import java.util.StringTokenizer;


      import org.apache.commons.httpclient.HttpClient;
      import org.apache.commons.httpclient.methods.DeleteMethod;
      import org.apache.commons.httpclient.methods.GetMethod;
      import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
      import org.apache.commons.httpclient.methods.PostMethod;
      import org.apache.commons.httpclient.methods.PutMethod;
      import org.apache.commons.httpclient.methods.multipart.FilePart;
      import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
      import org.apache.commons.httpclient.methods.multipart.Part;
      import org.apache.commons.httpclient.methods.multipart.StringPart;

      import java.io.File;
      import java.io.IOException;
      import java.io.InputStream;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.Map;

      import org.apache.commons.httpclient.HttpClient;
      import org.apache.commons.httpclient.methods.DeleteMethod;
      import org.apache.commons.httpclient.methods.GetMethod;
      import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
      import org.apache.commons.httpclient.methods.PostMethod;
      import org.apache.commons.httpclient.methods.PutMethod;
      import org.apache.commons.httpclient.methods.multipart.FilePart;
      import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
      import org.apache.commons.httpclient.methods.multipart.Part;
      import org.apache.commons.httpclient.methods.multipart.StringPart;
      import org.cord.client.shared.fileship.constants.Message;
      import org.cord.client.shared.fileship.constants.FileshipConstants.DownloadStatus;
      import org.cord.client.shared.fileship.utils.Utility;
      import org.cord.client.shared.fileship.webdav.internal.general.DownloadCache;
      import org.cord.client.shared.fileship.webdav.internal.general.GlobalObjects;
      import org.eclipse.core.runtime.IProgressMonitor;
      import org.eclipse.jface.dialogs.ProgressMonitorDialog;
      import org.eclipse.jface.operation.IRunnableWithProgress;
      import org.eclipse.swt.widgets.Display;
      import org.eclipse.swt.widgets.Shell;
      import org.eclipse.ui.IWorkbench;
      import org.eclipse.ui.IWorkbenchWindow;
      import org.eclipse.ui.PlatformUI;
      public class ADMTest {
      protected static HttpClient httpClient;
      public static void setup(){
      // setup HTTP client, with authentication (using default Jackrabbit credentials)
      httpClient = new HttpClient();
      httpClient.getParams().setAuthenticationPreemptive(true);
      Credentials defaultcreds = new UsernamePasswordCredentials("admin", "admin");
      httpClient.getState().setCredentials(new AuthScope("http://localhost", 8080, AuthScope.ANY_REALM), defaultcreds);

      }

      public static void PlayWithSling(){
      setup();
      // upload();


      final Map<String,String> props = new HashMap<String,String>();
      props.put("saphire","Tata motors");
      //props.put("diamond","StrikingInvitorVal");

      // POST and get URL of created node
      try {
      String urlOfNewNode = createNode("http://localhost:8080/up.txt", props, null, true);
      } catch(IOException ex) {
      // fail("createNode failed: " + ioe);
      ex.printStackTrace();
      }



      //===================
      // standard test by AP
      /// works fine downloadAndOpenFile("http://localhost:8080/content/sproc.txt/file", "germany.txt"); /////////////////////
      ////// test compl

      if (1==1)return;

      // create new node
      final String url = "http://localhost:8080/content/pp.txt";//HTTP_BASE_URL + folderPath;
      File localFile = new File("c:\\pp.txt");



      /*
      String urlOfNewNode = null;
      try {
      urlOfNewNode = createNode(url, null);
      } catch( Exception ioe) {
      //fail("createNode failed: " + ioe);
      ioe.printStackTrace();
      }


      // upload local file
      */
      try {
      // upload();
      uploadToFileNode("http://localhost:8080/content", localFile, "./file", null);
      /// actual one uploadToFileNode(urlOfNewNode, localFile, "./file", null);
      // test by AP
      // checkGet(urlOfNewNode);

      } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      }

      //upload( );// .... WORKING
      // add some properties to the node
      /**
      final Map<String,String> props = new HashMap<String,String>();
      props.put("saphire","Tata motors");
      props.put("swingman","StrikingInvitorVal");

      // POST and get URL of created node
      try {
      String urlOfNewNode = createNode(url, props, null, true);
      } catch(IOException ex) {
      // fail("createNode failed: " + ioe);
      ex.printStackTrace();
      }
      */



      }
      /** Upload to an file node structure, see SLING-168 */
      public static void uploadToFileNode(String url, File localFile, String fieldName, String typeHint)
      throws IOException {

      final Part[] parts = new Part[typeHint == null ? 1 : 2];
      parts[0] = new FilePart(fieldName, localFile);
      if (typeHint != null) {
      parts[1] = new StringPart(fieldName + "@TypeHint", typeHint);
      }
      final PostMethod post = new PostMethod(url);
      post.setFollowRedirects(false);
      post.setRequestEntity(new MultipartRequestEntity(parts, post.getParams()));

      final int status = httpClient.executeMethod(post);
      final int expected = 200;
      if(status!=expected) {
      throw new HttpStatusCodeException(expected, status, "POST", getResponseBodyAsStream(post, 0));
      }
      }
      public static void upload() {
      try {

      final PutMethod put = new PutMethod("http://localhost:8080/content/sqlnet.log");
      FileInputStream fs = new FileInputStream("c:/sqlnet.log");
      put.setRequestEntity(new InputStreamRequestEntity(fs));
      GlobalObjects.webDavEngine. getClient().executeMethod(put);
      } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      }
      }

      /*
      public static void upload(){
      try {
      PutMethod post = new PutMethod("http://localhost:8080/content/up.txt");
      post.setFollowRedirects(false);
      FileInputStream fs = new FileInputStream("c:\\up.txt");
      post.setRequestEntity(new InputStreamRequestEntity(fs));
      final int status = httpClient.executeMethod(post);
      final int expected = 200;
      post.releaseConnection();

      if(status!=expected) {
      throw new HttpStatusCodeException(expected, status, "POST", getResponseBodyAsStream(post, 0));
      }


      } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      }
      }
      */

      /////////////
      /** Call the other createNode method with headers==null */
      public static String createNode(String url, Map<String,String> nodeProperties) throws IOException {
      return createNode(url, nodeProperties, null, false);
      }

      /** Create a node under given path, using a POST to Sling
      * @param url under which node is created
      * @param multiPart if true, does a multipart POST
      * @return the URL that Sling provides to display the node
      */
      public static String createNode(String url, Map<String,String> clientNodeProperties, Map<String,String> requestHeaders,boolean multiPart)
      throws IOException {
      return createNode(url, new NameValuePairList(clientNodeProperties), requestHeaders, multiPart);
      }

      /** Create a node under given path, using a POST to Sling
      * @param url under which node is created
      * @param multiPart if true, does a multipart POST
      * @return the URL that Sling provides to display the node
      */
      public static String createNode(String url, NameValuePairList clientNodeProperties, Map<String,String> requestHeaders, boolean multiPart)
      throws IOException {
      //return createNode(url, clientNodeProperties, requestHeaders, multiPart, null, null, null);
      return createNode(url, clientNodeProperties, requestHeaders, false, null, null, null);
      }

      /** Create a node under given path, using a POST to Sling
      * @param url under which node is created
      * @param multiPart if true, does a multipart POST
      * @param localFile file to upload
      * @param fieldName name of the file field
      * @param typeHint typeHint of the file field
      * @return the URL that Sling provides to display the node
      */
      public static String createNode(String url, NameValuePairList clientNodeProperties, Map<String,String> requestHeaders, boolean multiPart,
      File localFile, String fieldName, String typeHint)
      throws IOException {

      final PostMethod post = new PostMethod(url);
      post.setFollowRedirects(false);

      // create a private copy of the properties to not tamper with
      // the properties of the client
      NameValuePairList nodeProperties = new NameValuePairList(clientNodeProperties);
      /**
      // add sling specific properties
      nodeProperties.prependIfNew(":redirect", "*");
      nodeProperties.prependIfNew(":displayExtension", "");
      nodeProperties.prependIfNew(":status", "browser");

      // add fake property - otherwise the node is not created
      if (clientNodeProperties == null) {
      nodeProperties.add("jcr:created", "");
      }
      */
      // force form encoding to UTF-8, which is what we use to convert the
      // string parts into stream data
      nodeProperties.addOrReplace("_charset_", "UTF-8");

      if( nodeProperties.size() > 0) {
      if(multiPart) {
      final List<Part> partList = new ArrayList<Part>();
      for(org.cord.client.dms.mainapp.dialogs.NameValuePair e : nodeProperties) {
      if (e.getValue() != null) {
      partList.add(new StringPart(e.getName(), e.getValue(), "UTF-8"));
      }
      }
      if (localFile != null) {
      partList.add(new FilePart(fieldName, localFile));
      if (typeHint != null) {
      partList.add(new StringPart(fieldName + "@TypeHint", typeHint));
      }
      }
      final Part [] parts = partList.toArray(new Part[partList.size()]);
      post.setRequestEntity(new MultipartRequestEntity(parts, post.getParams()));
      } else {
      for(org.cord.client.dms.mainapp.dialogs.NameValuePair e : nodeProperties) {
      post.addParameter(e.getName(),e.getValue());
      }
      }
      }

      if(requestHeaders != null) {
      for(Map.Entry<String,String> e : requestHeaders.entrySet()) {
      post.addRequestHeader(e.getKey(), e.getValue());
      }
      }


      final int expected = 302;
      // HttpClient httpClient= GlobalObjects.webDavEngine.getClient();
      //final int status = httpClient.executeMethod(post);
      final int status = GlobalObjects.webDavEngine.getClient().executeMethod(post);

      if(status!=expected) {
      throw new HttpStatusCodeException(expected, status, "POST", url, getResponseBodyAsStream(post, 0));

      }
      String location = post.getResponseHeader("Location").getValue();
      post.releaseConnection();

      // simple check if host is missing
      if (!location.startsWith("http://")) {
      String host = "http://localhost:8080";
      int idx = host.indexOf('/', 8);
      if (idx > 0) {
      host = host.substring(0, idx);
      }
      location = host + location;
      }
      return location;
      }


      /** Return m's response body as a string, optionally limiting the length that we read
      * @param maxLength if 0, no limit
      */
      public static String getResponseBodyAsStream(HttpMethodBase m, int maxLength) throws IOException {
      final InputStream is = m.getResponseBodyAsStream();
      final StringBuilder content = new StringBuilder();
      final String charset = m.getResponseCharSet();
      final byte [] buffer = new byte[16384];
      int n = 0;
      while( (n = is.read(buffer, 0, buffer.length)) > 0) {
      content.append(new String(buffer, 0, n, charset));
      if(maxLength != 0 && content.length() > maxLength) {
      throw new IllegalArgumentException("Response body size is over maxLength (" + maxLength + ")");
      }
      }
      System.out.println(content.toString());
      return content.toString();
      }
      public static void downloadAndOpenFile(final String path, final String localPath) {
      final String remotePath = Utility.encodePath(path);
      {
      GetMethod getMethod = new GetMethod(remotePath);
      try {
      httpClient.executeMethod(getMethod);
      } catch (HttpException e1) {
      // TODO Auto-generated catch block
      e1.printStackTrace();
      } catch (IOException e1) {
      // TODO Auto-generated catch block
      e1.printStackTrace();
      }
      Header[] contlen = getMethod
      .getResponseHeaders("Content-Length");
      long totalBytes = 0;
      if (contlen.length != 0) {
      StringTokenizer stone = new StringTokenizer(
      contlen[0].getValue(), "=");
      totalBytes = new Integer(stone.nextToken()).intValue();

      }

      try {
      InputStream from = getMethod.getResponseBodyAsStream();
      OutputStream to = new FileOutputStream(localPath);
      int ch = -1;
      long kbTransfered = 0;
      for (int count = 0;; count++) {
      ch = from.read();
      if (ch == -1) {
      to.flush();
      break;
      }
      ++kbTransfered;
      to.write(ch);

      }
      to.close();
      getMethod.releaseConnection();
      }catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      }

      }
      }
      public static void checkGet(String urlString){
      final GetMethod get = new GetMethod(urlString);
      try {
      final int status = httpClient.executeMethod(get);
      } catch (HttpException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      } catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      }
      }



      }
      • By Parvez Ahmad Hakim - 9:18 AM on Jul 25, 2011   Reply
        Parvez Ahmad Hakim
        www.abobjects.com
        Any update? I get error using curl also
        : some constraint error

      ADD A COMMENT