AEM 6.3: Uploading multiple renditions

Uploading multiple renditions is not supported by default in AEM, our content authors really wanted this feature so we build our own custom solution.

buttons

The 'Add Rendition' action can be found at:

/libs/dam/gui/content/assetdetails/jcr:content/actions/fileupload

The first thing we have to do is the multiple propery to true.

properties

We are now able to select multiple files (in this example 2 files). Look at what happens when we try to upload those files:

network

Only one of the renditions was successfully uploaded, the other one returned caused a server error. In the error log I can see the following happening:

POST /content/dam/saxofoon.png/_jcr_content/renditions HTTP/1.1] org.apache.sling.servlets.post.impl.operations.ModifyOperation Exception during response processing.  
org.apache.sling.api.resource.PersistenceException: Unable to commit changes to session.  
    at org.apache.sling.jcr.resource.internal.helper.jcr.JcrResourceProvider.commit(JcrResourceProvider.java:499)

...

Caused by: javax.jcr.InvalidItemStateException: OakState0001: Unresolved conflicts in /content/dam/saxofoon.png/jcr:content/renditions/jacuzzi.png  
    at org.apache.jackrabbit.oak.api.CommitFailedException.asRepositoryException(CommitFailedException.java:237)

...

Caused by: org.apache.jackrabbit.oak.api.CommitFailedException: OakState0001: Unresolved conflicts in /content/dam/saxofoon.png/jcr:content/renditions/jacuzzi.png  
    at org.apache.jackrabbit.oak.plugins.commit.ConflictValidator.failOnMergeConflict(ConflictValidator.java:115)

Looking at the logging it is obvious that the servlet (I did not bother to go looking for it) was not handling the upload correctly. Lets create out custom solution!

AEM is weird

properties

The name property is set to will_be_replaced, and as the name explains it is in fact replaced when a file is being uploaded.

When no file is selected:

element

When a file(s) IS selected:

element replaced

  • Why have they implemented it this way? -> no clue
  • Do we like this, aka do we want this? -> NO!

We want this behaviour to stop from happening, we need to make changes on 2 locations:

1 /libs/dam/gui/coral/components/admin/assetdetails/clientlibs/assetdetails/js/assetdetails.js

This piece of code is causing our name attribute to change

function queueChanged(event) {  
    var fileUpload = $(".cq-damadmin-admin-actions-rendition-upload-activator")[0];
    fileUpload.name  =  fileUpload.value.replace(/^.*[\\\/]/, '');
    fileUpload.upload();
}

Replace it by the following code (you can overlay the file under /apps/...)

function queueChanged(event) {  
    var fileUpload = $(".cq-damadmin-admin-actions-rendition-upload-activator")[0];
    if (fileUpload.name !== "file") {
        fileUpload.name = fileUpload.value.replace(/^.*[\\\/]/, '');
    }
    fileUpload.upload();
}

2 /libs/dam/gui/content/assetdetails/jcr:content/actions/fileupload

  • Set the name property to file.
  • Set the uploadUrl property to /services/private/renditions?path=${granite:encodeURIPath(requestPathInfo.suffix)}

The uploadUrl points to the rest endpoint we will create in the next step.

AssetEndpoint

Lets create an endpoint that captures our file upload:

@POST
@Path("/private/renditions")
@Consumes({MediaType.MULTIPART_FORM_DATA})
public Response saveRendition(  
    @FormDataParam("file") InputStream fileInputStream,
    @FormDataParam("file") FormDataBodyPart bodyPart,
    @FormDataParam("file") FormDataContentDisposition cdh,
    @QueryParam("path") String path) {

    try {
        assetService.saveRendition(path, cdh.getFileName(), fileInputStream, bodyPart.getMediaType().toString());
        return Response.status(Response.Status.OK).build();
    } catch (RenditionException e) {
        LOG.error("[AssetEndpoint]: " + e.getMessage());
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
    }
}

A HTTP multipart request is a HTTP request that HTTP clients construct to send files and data over to a HTTP Server. It is commonly used by browsers and HTTP clients to upload files to the server.

AssetService

  1. Get the AssetManager
  2. Get the referenced asset
  3. Set the new rendition for the asset
  4. Save everything
public void saveRendition(String assetPath, String fileName, InputStream inputStream, String mimeType) throws RenditionException {

    try (AutoClosableResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver()) {

        AssetManager assetManager = resourceResolver.adaptTo(AssetManager.class);
        Session session = resourceResolver.adaptTo(Session.class);

        if (assetManager == null) {
            throw new RenditionException("The AssetManager object is null");
        }

        if (session == null) {
            throw new RenditionException("The Session object is null");
        }

        final Map<String, Object> map = new HashMap<>();
        map.put(RenditionHandler.PROPERTY_RENDITION_MIME_TYPE, mimeType);

        Asset asset = assetManager.getAsset(assetPath);
        asset.setRendition(fileName, inputStream, map);

        session.refresh(true);
        session.save();

    } catch (RepositoryException e) {
        LOG.error(ExceptionUtils.getStackTrace(e));
        throw new RenditionException("Something went wrong while saving the rendition");
    } catch (LoginException e) {
        LOG.error(ExceptionUtils.getStackTrace(e));
        throw new RenditionException("Could not fetch service resource resolver.");
    }
}

RenditionException

In case you are interested in the exception class:

public class RenditionException extends Exception {

    public RenditionException() {}

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

}

This should make the upload of multiple renditions work. If you do have any questions, do not hesitate to contact me or leave a comment below.