Home

by Bhagya Nirmaan Silva at Redpill Linpro, Karlstad, Sweden.

This customization allows users to publish documents externally and to acquire a link which enables non-alfresco users to view the documents which exist in the system. At the stage of publishing the document, the document public link can get an an expiry date set. It will also allow transformations of the content available within the document by simply specifying the file extension at the end of the external link.

This blog post covers the steps from a slide deck that was done as a part of the training for Alfresco Share and Repository Customization work. The final source code can be found on github at the following link , and you are welcome to suggest and improve the code on your own.

https://github.com/bhagyas/alfresco-external-link

Special thanks goes to Jeff Potts and Share-extras project authors. The content available from their sites served as the base for creating the project. Each step covers an iteration which was  during the development of the final result, hence there will be certain steps which modify actions taken in earlier steps.

You will find screen-shots along the way. :)

Note: You should have a basic understanding on Alfresco Share and Alfresco Repository development in order to continue with this post.


Step 1: Create repository behaviour

  • Added an aspect for a short link (used sc:webable aspect which was available from the J. Potts tutorial)
  • Added the behavior to create a short link and set it as the property whenever asc:webable aspect is added
  • Short link is a GUID that is generated at the time the aspect is added.

Step 1 : Howto

  • Create a Java class which attaches to the onAddAspect behavior.
    • PublishWithShortUrlBehavior.java
      • onAddAspect was overridden and modified to set a a new property namedsc:shortLink – the value will be a GUID generated at the time the action is invoked.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) {
    if (!nodeService.exists(nodeRef)) {
        LOG.error("Executed behaviour on non existing node: " + nodeRef);
        return;
    }
    LOG.debug("OnAddAsepct for " + nodeRef + " with aspect " +aspectTypeQName);
    createNewShortLinkItem(nodeRef);
}
private void createNewShortLinkItem(NodeRef nodeRef) {
    LOG.debug("Creating new shortlink item....");
    Map nodeProps= new HashMap();
    String generatedShortLink = (String)GUID.generate();
    Date today = new Date();
    nodeProps.put(SomeCoModel.PROP_PUBLISHED, today);
    nodeProps.put(SomeCoModel.PROP_IS_ACTIVE, true);
    nodeProps.put(SomeCoModel.PROP_SHORT_LINK, generatedShortLink);
    LOG.debug("Shortlink URL [" + generatedShortLink + "] generated, and added to the node properties.");
    nodeService.addProperties(nodeRef, nodeProps);
}
  • share-config-custom.xml was modified in order to display the shortlink in the Share user interface.
  • The additional properties were defined in the model – scModel.xml and the property labels in scModel.properties

Step 2: Add an action to create the short link

  • Created a UI Action which enables one click publishing from the Share UI.
  • This would add a sc:webable aspect behind the works, and will trigger the publishing behavior.

Step 2 : Howto

  • Create a repository webscript backing the action. The webscript consits of the following parts:
  • publish.post.desc.xml
    • Contains the description for the publish action webscript
  • publish.post.json.ftl
    • Returns a JSON result indicating the status of the publish action call.
  • publish.post.json.js
    • The action logic. This will be adding the sc:webable aspect to a selected node. The file is based on backup.post.json.js which was available on share-extras on Google Code. When the aspect is added the PublishWithShortUrlBehavior will be triggered.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * Publish multiple files action
 * @method POST
 */
/**
 * Entrypoint required by action.lib.js
 *
 * @method runAction
 * @param p_params {object} Object literal containing files array
 * @return {object|null} object representation of action results
 */
function runAction(p_params)
{
   var results = [];
   var files = p_params.files;
   var file, fileNode, result, nodeRef;
   logger.log("fileNode :" + fileNode);
   // Must have array of files
   if (!files || files.length == 0)
   {
      status.setCode(status.STATUS_BAD_REQUEST, "No files.");
      return;
   }
   for (file in files)
   {
      nodeRef = files[file];
      result =
      {
         nodeRef: nodeRef,
         action: "publishFile",
         success: false,
         isContainer : false
      }
      try
      {
         fileNode = search.findNode(nodeRef);
         if (fileNode === null)
         {
            result.id = file;
            result.nodeRef = nodeRef;
            result.success = false;
         }
         else
         {
            result.id = fileNode.name;
            result.type = fileNode.isContainer ? "folder" : "document";
            result.isContainer = fileNode.isContainer;
            if(!fileNode.isContainer){
                fileNode.addAspect("sc:webable");
                result.success = true;
                logger.info("sc:webable aspect added.");
            }else{
                status.setCode(status.STATUS_BAD_REQUEST, "Folders can't be published.");
                result.success = false;
                throw "Folders can't be published.";
            }
         }
      }
      catch (e)
      {
         result.id = file;
         result.nodeRef = nodeRef;
         result.success = false;
      }
      results.push(result);
   }
   return results;
}
/* Bootstrap action script */
main();
  • slingshot-custom-publish-action.properties
    • Contains the messages for displaying the status for publish actions.
  • UI Components were modified to display the Publish action
    • web-extension/site-webscripts/org/alfresco/components
  • Added the action links to the following.
    • document-details/document-actions.get.config.xml
    • document-library/documentlist.get.config.xml
  • Added the stylesheet containing the publish action icon and the publish action javascript to the header of document library: document-library/actions-common.get.head.ftl
  • Web resources were bundled to contain the publish action related messages inslingshot-custom-publish-action-context.xml
  • Once completed, the action is shown in the document library view as in the below screenshot.


Step 3: External access to the document via the short link

Step 3 : Howto

  • Create a repository webscript to access the document using the shortlink id:
  • showDocument.get.desc.xml
    • Contains the service descriptor for the web script. The script runs as ‘system‘ user and is of ‘org.alfresco.repository.content.stream‘ kind in order to stream the document content.
  • showDocument.get.js
    • The webscript looks for the url part with the shortlink and executes a lucene search to find the node containing the shortLink. Then it streams back the content of the node. No Freemarker template is needed as the streaming webscript expects the content to stream into the model.contentNode.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function main(){
    // check that search term has been provided
    var documentShortLink = url.templateArgs['documentShortLink']
    if (documentShortLink == undefined || documentShortLink.length == 0)
    {
       status.code = 400;
       status.message = "Search term has not been provided." + documentShortLink;
       status.redirect = true;
    }
    else
    {
       var searchQuery = "@sc\\:shortLink:\"" + documentShortLink + "\"";
       // perform search
       var nodes = search.luceneSearch(searchQuery);
       var searchResult = nodes[0];
       model.contentNode = nodes[0];
    }
}
main();

Step 4: Add an expiry date

  • The document external access link will now have an expiry date.
  • This will initially be set to 30 days from the time the document short link is created.
  • The expiry date can be changed via the ‘Edit Metadata’ function in Share.
    • This is how it looks like.
  • When the document is requested after the expiry date, the system will show a message indicating the document has expired.
  • A sample screenshot on how it looks is below.

Document Expired Screenshot

Step 4 : Howto

  • The original PublishWithShortUrlBehavior.java was modified to set a new property named sc:expiresOn which belongs under the sc:webable aspect. The sc:expiresOnproperty will be using the d:datetime datatype.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName) {
    if (!nodeService.exists(nodeRef)) {
        LOG.error("Executed behaviour on non existing node: " + nodeRef);
        return;
    }
    LOG.debug("OnAddAsepct for " + nodeRef + " with aspect " +aspectTypeQName);
    createNewShortLinkItem(nodeRef);
}
private void createNewShortLinkItem(NodeRef nodeRef) {
    LOG.debug("Creating new shortlink item....");
    Map nodeProps= new HashMap();
    String generatedShortLink = (String)GUID.generate();
    Date today = new Date();
    nodeProps.put(SomeCoModel.PROP_PUBLISHED, today);
    nodeProps.put(SomeCoModel.PROP_IS_ACTIVE, true);
    Calendar expiryDate = Calendar.getInstance();
    //initial expiry date is after 30 days from the published date
    expiryDate.add(Calendar.DATE, 30);
    nodeProps.put(SomeCoModel.PROP_EXPIRES_ON, expiryDate.getTime());
    nodeProps.put(SomeCoModel.PROP_SHORT_LINK, generatedShortLink);
    LOG.debug("Shortlink URL [" + generatedShortLink + "] generated, and added to the node properties.");
    nodeService.addProperties(nodeRef, nodeProps);
}
  • web-client-config.xml and share-config-custom.xml was modified in order to make the shortlink read only, and to make the expiry date editable by the user.
  • When the document is requested through the short link, the showDocument.get.js will compare the expiry date against the system date and will redirect the request to a page showing a message that indicates the document has expired. It will use a status template available in webscripts to show the message.
    • showDocument.get.js
    • showDocument.get.html.410.ftl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function main(){
    // check that search term has been provided
    var documentShortLink = url.templateArgs['documentShortLink']
    if (documentShortLink == undefined || documentShortLink.length == 0)
    {
       status.code = 400;
       status.message = "Search term has not been provided." + documentShortLink;
       status.redirect = true;
    }
    else
    {
       var searchQuery = "@sc\\:shortLink:\"" + documentShortLink + "\"";
       // perform search
       var nodes = search.luceneSearch(searchQuery);
       var searchResult = nodes[0];
       var expiryDate = searchResult.properties["sc:expiresOn"];
       var today = new Date();
       //do not show if the document has expired. show an expired page instead.
       if(expiryDate < today){
           status.code = 410;
           status.message = "Document expired on " + expiryDate;
           status.redirect = true;
       }
    }
}
main();
1
2
<h2>Sorry, this document has expired.</h2>
<h3>Please contact the source in order to request a new link or to extend the validity.</h3>

Step 5: Improve the Share interface to display the short link URL

  • The shortlink property for published documents will now display as a hyperlink.
  • The user can now click on it to view the document as it will be displayed to an external user who accesses using the shortlink.
  • That’s cool. It helps to make sure the short link works.

Step 5 : Howto

  • Created a new form contoller named shortLink.ftl inside web-extension/site-webscripts/org/alfresco/components/form/controls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="form-field">
   <#if form.mode == "view">
<div class="viewmode-field" style="color: red; background-color: white;">
         <span class="viewmode-label">${field.label?html}:</span>
         <span class="viewmode-value"><#if field.value == "">${msg("form.control.novalue")}<#else>
            <a href="${url.server}${url.context}/proxy/alfresco/labs/show/document/${field.value?html}">
                ${url.server}${url.context}/proxy/alfresco/labs/show/document/${field.value?html}
            </a>
         <!--#if--></span></div>
<#else>
      <label for="${fieldHtmlId}">${field.label?html}:</label>
      <input id="${fieldHtmlId}" disabled="disabled" type="text" value="${field.value?html}" />             title="${msg("form.field.not.editable")}"
             <#if field.control.params.styleClass??>class="${field.control.params.styleClass}"<!--#if-->
             <#if field.control.params.style??>style="${field.control.params.style}"<!--#if--> />
   <!--#if--></div>
  • It is based on readonly.ftl, and was modified to show a clickable URI by using the HTML anchor tag.
  • The server address and the context for creating the hyperlink were obtained from the url object available in the freemarker context.
  • Example:
    • ${url.server}${url.context}/proxy/alfresco/labs/show/document/${field.value?html}
  • The new shortLink.ftl was registered in the share-config-custom.xml to display thesc:shortLink property.

Step 6: Allow the external user to request the document in any format

  • Oh wait, there is more.
  • The user can now ask the system to convert the document to his own file format and then return.
  • Just add the filetype extension to the shortlink, and it will return the converted document – if the system can’t convert, it will return the original filetype.

Step 6 : Howto

  • This introduced Renditions. Renditions allow to store content in different formats already transformed in the server.
  • showDocument.get.js
    • The requested filetype is derived by splitting the URI extension part. Then it will be sent to a function which will guess and return a MIME type based on the extension.
    • The script will look for an existing rendition available. If it’s found, it will return the existing rendition instead of transforming the content everytime it is requested.
    • The mimetype along with the original file content will be sent to the rendition service to create a rendition definition and will be used to create a new rendition.
    • The newly created rendition will be written to the request content, and will be returned to the user.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
function main(){
    // check that search term has been provided
    var documentShortLink = url.templateArgs['documentShortLink']
    if (documentShortLink == undefined || documentShortLink.length == 0)
    {
       status.code = 400;
       status.message = "Search term has not been provided." + documentShortLink;
       status.redirect = true;
    }
    else
    {
       var searchQuery = "@sc\\:shortLink:\"" + documentShortLink + "\"";
       // perform search
       var nodes = search.luceneSearch(searchQuery);
       var searchResult = nodes[0];
       var expiryDate = searchResult.properties["sc:expiresOn"];
       var today = new Date();
       //do not show if the document has expired. show an expired page instead.
       if(expiryDate < today){          status.code = 410;          status.message = "Document expired on " + expiryDate;           status.redirect = true;     }               //look for an extension within the url      var requestedExtension = url.extension.split("\\.")[1];             if(requestedExtension != null){         var renditionName = "rendition"+ requestedExtension;                        //1. Investigate if the rendition exists...         var existingRenditions = renditionService.getRenditions(nodes[0]);          var existingRenditionFound = null;                      if(existingRenditions != null && existingRenditions.length > 0) {
               //do nothing for now. todo : implement a search and return rendition instead of rendering it again.
               for(var x = 0; x < existingRenditions.length; x++){
                   logger.log(existingRenditions[x]);
                   if(null != existingRenditions[x] ){
                       var node = search.findNode(existingRenditions[x].nodeRef);
                       logger.log(node.properties["cm:name"]);
                       if(node.properties["cm:name"] != null){
                           logger.log(renditionName);
                           if(node.properties["cm:name"] == renditionName){
                               existingRenditionFound = node; //thats it.
                           }
                       }
                   }
               }
           }
           if(existingRenditionFound == null){
             //2. If not create definition
               var renditionEngineName = "reformat";
               var renditionDef = renditionService.createRenditionDefinition(renditionName, renditionEngineName)
               //setting the mime type
               var requestedMimeType = guessMimeType(requestedExtension);
               if(requestedMimeType != null){
                   renditionDef.parameters['mime-type'] = requestedMimeType;
                   //3. Render...
                   var renditionNodeRef = renditionService.render(nodes[0], renditionDef);
                   model.contentNode = renditionNodeRef;
               }else{
                   model.contentNode = nodes[0];
               }
           }else{
               model.contentNode  =  existingRenditionFound; //show the existing rendition
           }
       }else{
           model.contentNode = nodes[0];
       }
    }
}
/**
 * Guesses and returns the mime type based on the file extension provided.
 *
 * */
function guessMimeType(fileExtension){
    for(var x = 0; x < extensions.length; x++){
        if(extensions[x][0] == fileExtension){
            return extensions[x][1];
        }
    }
    return null;
}
/**
 * A manually implemented list of file extensions against the mime types.
 * */
var extensions = [
                  ["pdf","application/pdf"],
                  ["txt","text/plain"],
                  ["gif","image/gif"],
                  ["jpg","image/jpeg"],
                  ["html","text/html"],
                  ["doc","application/msword"]
                ];
main();
  • showDocument.get.html.501.ftl
    • This was created to display an error message in case a rendition being unavailable. However, now the system will return the original file instead of an error if a transformation is not available for a particular filetype.

So that’s it, and when you build and deploy your project you will see a custom action within Alfresco Share document details and document library to publish a document. It will generate a link which will be expired on a set date which is available as a meta data property within the document.

About Bhagya Nirmaan Silva

simple and open hearted 🙂 about.me/bhagyas

This entry was posted in Alfresco and tagged , , , , . Bookmark the permalink.

3 Responses to Publishing an external link to Alfresco content with Expiry Date and Transformation Support

  1. youhammi says:

    good post thanks

  2. Pretty! This was an extremely wonderful post.
    Thank you for supplying these details.

  3. Andrew Nel says:

    Thanks for this post! It looks like a fantastic addition. Has this ever been tested on the Community version?

Publicités

Laisser un commentaire

Choisissez une méthode de connexion pour poster votre commentaire:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s