Latest Posts

Archives [+]

Categories [+]

Authors [+]

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.

 
(optional)
1 comment
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 ;-)

Here is the code for the deactivate method - a session.logout() is ok inside Sling, it will handle the unregistering of event listeners automatically.

protected void deactivate(ComponentContext componentContext) {
if (session != null) {
session.logout();
session = null;
}
}


0 Replies » Reply