Ordering files in the CKEditor file browser with Liferay

I’ve already written about extending Liferay’s CKEditor with custom Liferay Hooks to implement required functionality.

Today I’ll continue this article with another UseCase where I improved the sorting of the displayed files in the file browser window.

I started to create this article because of this Stack Overflow question.

If you look at the files in the file browser, you can see that there is no ordering. And this can be a bit bothersome if you want upload a bunch of files into a folder and then you want to select one of those files — and you know them only by name.

In this article I’ll show how to extend the already extended frmresouceslist.html file to sort the resulting files by name and by creation date.

Simple sorting by name

This is the simple solution. For this there is only the need for a hook. If you’ve read the previous article, you can see how a hook is created to extend the functionality of CKEditor in Liferay.

I’ll take that hook and add the sorting functionality to it. However it is nothing like rocket science: we already have the data, we only need to sort the objects returned by Liferay.

If you look closely at frmresourceslist.html you can find a GetFoldersAndFilesCallback function which takes a variable as parameter. This variable is a string, returned by the actual GetFoldersAndFiles method.

So this is the function which generates the displayed list of folders and files in your file browser. Currently it displays only the name and the size of files and the name of folders. The order as it is returned in this callback.

As you might think, I will insert the sorting into this method. I have to tell, that I am not a JavaScript guru so there might be better (faster, less memory-consuming) solutions but I cook with what I have.

So for the sorting I create an associative array where the key is the name of the file, the value is the HTML line displaying the file with name (as a link) and the size of the file.

var sortableNodes = {};
...
sortableNodes[sFileName] = oListManager.GetFileRowHtml(sFileName, sFileUrl, sFileSize, sFileErrorMessage);

After all files are set, I simply sort this array by the keys in ascending order:

var keys = [];
for(var key in sortableNodes) {
  if(sortableNodes.hasOwnProperty(key)) {
    keys.push(key);
  }
}

keys.sort(function(a, b){
    if(a < b) return -1;
    if(a > b) return 1;
    return 0;
});

And the last step is to add the HTML code to the site in the sorted order:

for(var i = 0, len = keys.length; i < len; i++) {
    oHtml.Append(sortableNodes[keys[i]]);
}

Now the file entries are sorted by name. You can achieve the same with the size of the files: just use the size as the key in the sortableNodes associative array.

As you can see this wasn’t the big thing.

Extending the sorting with dates

As for the sorting, this part is more interesting and challenging. Or rather it was when I was implementing the sort.

Some words about ext-plugins

You know so far that Liferay hooks are the best was to override configurations in properties files and not-packaged other files such as HTML, JavaScript — like for CKEditor.

However changing functionality in JAR files (for example in the portal-impl.jar file) requires a special feature: a so called ext-plugin. These ext-plugins are Liferay extensions and enable changing the base Liferay Java classes. But you should be careful to implement such features because it can be messed up with new Liferay releases — or you mess up your current Liferay installation.

I won’t go into detail how to develop an ext-plugin. It is described at the Liferay homepage.

However one thing I was missing: they only show how to change configuration and JSP files. So I struggled a bit until I knew where to place my Java file which extends the functionality of the GetFoldersAndFilesCallback function. But finally I’ve found the solution: just place your overriding / extending Java source files in the src file under the ext-impl folder in your ext-plugin’s home. This means you do not have to define a folder structure representing the package where your extension will reside once compiled.

And one more general thing about how to extend Liferay’s functionality: if you plan to overwrite blocks of code in Java classes, use the inheritance pattern where possible. This means if you want to change one method of the class, create a subclass and add the overriding method there. In this case you only have to maintain your method and not the whole class.

Changing the callback

I had to search a bit until I’ve found the source code where the Callback method resides in Liferay. But after found I’ve seen that it won’t be an easy override as described above because the core functionality I needed to change was in a private function. So this meant I had to copy almost half of the class into my extension to add to lines of extra code.

Changes in the class

The changes I’ve made were in the DocumentCommandReceiver.java file. This file prepares the callback result. If you look up this class, you can see that they do some design flaws (under they I mean the developers of Liferay) because they set the name and the desc property to the name of the file — and display the value of desc in the file browser. If someone later changes the desc property to the real description of the file the whole browser gest messed up and everyone will complain. So this is a bad habit, use such things carefully.

My changes weren’t so tragic, I only added the creation date and modified date to the resulting XML as a String:

public class ExtendedDocumentCommandReceiver extends DocumentCommandReceiver {

  public static DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  @Override
  protected void getFoldersAndFiles(
    CommandArgument commandArgument, Document document, Node rootNode) {
      // some code omitted
  }
  private void _getFiles(
      CommandArgument commandArgument, Document document, Node rootNode)
    throws Exception {
      // ...
      fileElement.setAttribute("created", df.format(fileEntry.getCreateDate()));
      fileElement.setAttribute("modified", df.format(fileEntry.getModifiedDate()));
      // ...
  }

  // ...
}

Because the FileEntry class already defines the creation and modification date, I did not have to extend that class to but I could use the provided methods and cast them to a String with a SimpleDateFormat.

Converting to seconds is a good thing because files sorted onyl by date could end up in the same mess as without sorting.

This was only the first part: implementing the changes does not mean that the will be active in Liferay when you deploy your ext-plugin.

Enabling the new class

To enable the new class and let it provide the dates to the callback I had to change the configuring Spring XML file. If you’ve worked with Spring you know that the managed beans are configured with annotations or with “old-school” XML. I like the XML version because you can change the managed beans if you want to — and so does Liferay too.

To enable the ExtendedDocumentCommandReceiver class I’ve had to change the editor-spring.xml file:

<?xml version="1.0"?>

<beans
  default-destroy-method="destroy"
  default-init-method="afterPropertiesSet"
  xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
>
  <bean id="com.liferay.portal.editor.fckeditor.receiver.CommandReceiverFactory" class="com.liferay.portal.editor.fckeditor.receiver.CommandReceiverFactory">
    <property name="commandReceivers">
      <map key-type="java.lang.String" value-type="com.liferay.portal.editor.fckeditor.receiver.CommandReceiver">
        <entry key="Attachment">
          <bean class="com.liferay.portal.editor.fckeditor.receiver.impl.AttachmentCommandReceiver" />
        </entry>
        <entry key="Document">
          <bean class="com.liferay.portal.editor.fckeditor.receiver.impl.ExtendedDocumentCommandReceiver" />
        </entry>
        <entry key="Page">
          <bean class="com.liferay.portal.editor.fckeditor.receiver.impl.PageCommandReceiver" />
        </entry>
      </map>
    </property>
  </bean>
</beans>

This file belongs in the META-INF folder beside the source file of the ExtendedDocumentCommandReceiver class.

After these changes the ext-plugin is ready to build and deploy.

Changes in the HTML file

Unfortunately this was not the whole thing. The changes in the ExtendedDocumentReceiver provide now the missing date fields in the callback XML, however the entries do not get sorted after the dates out-of-the-box.

Fortunately this one change is not as bad because I have already described how to implement the sorting of the files by name with the use of a hook. The solution for this problem is to extend that hook to display these dates and sort by one of them.

Getting the dates in the callback is simple:

var sCreateDate =
    oNode.attributes.getNamedItem('created') ? oNode.attributes.getNamedItem('created').value : null;
var sModifiedDate =
    oNode.attributes.getNamedItem('modified')? oNode.attributes.getNamedItem('modified').value : null;

I provided a null-check because when the ext-plugin is not deployed we’d end up with a blank browser winddow and a JavaScript error in the console.

I tried to maintain the naming conventions used in the browser window but you can alter it as you wish. I won’t complain.

After the values are there I simply changed the sorting key in the associative array described in the previous section:

var htmlRow = oListManager.GetFileRowHtml(sFileName, sFileUrl, sFileSize, sCreateDate, sModifiedDate, sFileErrorMessage);

if(sModifiedDate) {
    sortableNodes[sModifiedDate] = htmlRow;
    ascending = false;
} else if(sCreateDate) {
    sortableNodes[sCreateDate] = htmlRow;
    ascending = false;
} else {
    sortableNodes[sFileName] = htmlRow;
}

As you can see, a null-check here is needed too. Naturally it is a bit overworking because I check the existence of the dates in the loop every time. So if it is there once it would be there always, if the date is not there once it should not be there any time. However this null-check enables the usage of the hook without the ext-plugin.

And if you look close enough I’ve changed the method wich displays the files as HTML rows in the table and provide the file name as the name (instead of the description) and display the additional dates too:

oListManager.GetFileRowHtml = function (fileName, fileUrl, fileSize, sCreateDate, sModifiedDate, errorMessage) {
  ...
    var htmlRow = '<tr>' +
        '<td width="16">' +
            sLink +
            '<img alt="" src="images/icons/' + sIcon + '.gif" width="16" height="16" border="0"><\/a>' +
        '<\/td><td>&nbsp;' +
            sLink +
            fileName +
            '<\/a>' +
        '<\/td><td align="right" nowrap>&nbsp;' +
            fileSize +
            ' KB';
        if(sCreateDate) {
            htmlRow += '<\/td><td align="right" nowrap>&nbsp;' +
            sCreateDate;
        }
        if(sModifiedDate) {
            htmlRow += '<\/td><td align="right" nowrap>&nbsp;' +
            sModifiedDate;
        }
        htmlRow += '<\/td><\/tr>';
    return htmlRow;
}

Again, the null-checks are essential because I do not want to display additional columns if they’re not filled with any value.

And this was it. Now I have a file browser window which displays the files sorted by modification date.

Sources

I released the sources under the MIT license, you can find the ext plugin in my Bitbucket repository, as well as the hook in the other one from the last article.

Further improvements

I won’t stop right here because I have some plans how to make the file browser more usable. Naturally the most exciting feature would be to enable users to sort the displayed files. My goal is this too — however as I mentioned above, I am no JavaScript guru so it would take its time to implement this.

However until that I’ll add a header to the table to display the names of the columns. This is essential in my eyes — and now that I provide two date fields it is more needed than anything else.

The changes you’ll see in the repositories as soon as I push them.

Update (2015.02.25)

I’ve got an answer from the Liferay Marketplace: I cannot publish an ext-plugin. So the solution is out of the question. I see no added value providing only the ck-editor hook at the marketplace. It is not a big thing to implement and you can only sort by name or file size.

So if you want to sort by date’s you have to make it manually (or use my plugins) — without downloading it from the marketplace.

5 thoughts on “Ordering files in the CKEditor file browser with Liferay

    • Hello Juan,
      thanks for the information, I wasn’t really aware of the Liferay marketplace.
      However it is a good idea so I added the project to the marketplace and I hope I’ll get through the submission process soon.

  1. Pingback: Getting Liferay JournalArticle localized content in Java | JaPy Software
  2. Pingback: Filtering files by name in CKEditor’s file browser dialog in Liferay | JaPy Software
  3. Pingback: Ordering files in the CKEditor file browser with Liferay | Dinesh Ram Kali.

Leave a comment