Writing Java Plugins

This website contains links to software which is either no longer maintained or will be supported only until the end of 2019 (CKFinder 2). For the latest documentation about current CKSource projects, including software like CKEditor 4/CKEditor 5, CKFinder 3, Cloud Services, Letters, Accessibility Checker, please visit the new documentation website.

If you look for an information about very old versions of CKEditor, FCKeditor and CKFinder check also the CKEditor forum, which was closed in 2015. If not, please head to StackOverflow for support.

(Finding the File Name: Minor clarification added)
(Plugin Command Class: Minor clarification added)
 
(4 intermediate revisions by the same user not shown)
Line 179: Line 179:
 
It is also worth mentioning that the <code>runEventHandler</code> method of a particular plugin does not have to handle the command sent from the client. As an example, it can be used to just insert something into a database or log information about the user activity.  
 
It is also worth mentioning that the <code>runEventHandler</code> method of a particular plugin does not have to handle the command sent from the client. As an example, it can be used to just insert something into a database or log information about the user activity.  
  
The client code expects an answer in the form of XML code (see the <code>xml.selectSingleNode( 'Connector/FileSize/@size' );</code> line from the <code>plugin.js</code> file). This means that FileResizePlugin has to return an XML response. We will use the <code>XMLCommand</code> class to achieve this purpose; extending this class in the plugin makes us implement its two methods:
+
The client code expects an answer in the form of XML code (see the <code>xml.selectSingleNode( 'Connector/FileSize/@size' );</code> line from the <code>plugin.js</code> file). This means that FileResizePlugin has to return an XML response. We will use the <code>XMLCommand</code> class to achieve this purpose; extending this class in the plugin <code>FileSizeCommandClass</code> makes us implement its two methods:
* <code>createXMLChildNodes(int errorCode, Element rootElement)</code> &ndash; this method adds all extra XML nodes to the response that will be read by a plugin on the client side. It has two parameters:
+
* <code>createXMLChildNodes(int errorCode, Element rootElement)</code> &ndash; this method (described in more detail in the [[CKFinder_2.x/Developers_Guide/Java/Plugins/Writing_Java#Returning_the_XML_Response|Returning the XML Response]] section below) adds all extra XML nodes to the response that will be read by a plugin on the client side. It has two parameters:
 
** <code>errorCode</code> &ndash; an error number returned from the <code>getDataForXml</code> method;
 
** <code>errorCode</code> &ndash; an error number returned from the <code>getDataForXml</code> method;
 
** <code>rootElement</code> which represents the main XML node <code>Connector</code>.
 
** <code>rootElement</code> which represents the main XML node <code>Connector</code>.
Line 195: Line 195:
 
<source lang="java">
 
<source lang="java">
 
initParams();
 
initParams();
createXMLResponse(getDataForXml());
+
createXMLResponse(getDataForXml()); // Invokes the createXMLChildNodes method.
 
</source>
 
</source>
  

Latest revision as of 11:16, 23 May 2011

CKFinder functionality can be extended with server-side plugins. Although the full source code of the CKFinder server connector is available and can be modified in any way desired, a much better way of enhancing the CKFinder connector is to create a plugin.

The main advantages of plugins are:

  • Upgrades are much easier.
  • The plugin code is stored in a single place.
  • Plugins can be easily disabled when they are not needed anymore.

Common use cases:

  • Adding a new server-side command (i.e. fileditor and imageresize plugin).
  • Working with uploaded files (i.e. watermark plugin).
  • Extending information returned by the Init command (i.e. imageresize plugin).

Creating CKFinder for Java Plugins

As mentioned in the Using CKFinder Plugins article, CKFinder functionality can be extended with code that is delegated to dedicated plugins. This architecture ensures that in order to change the behavior of the application, you do not need to modify the CKFinder core — using plugins is much more convenient.

This tutorial shows how to create a CKFinder plugin on the example of a FileSizePlugin which, as the name suggests, returns the size of the file.

In order to create a CKFinder plugin, you will need to follow the steps below:

  • Step 1: Create the plugin folder and file.
  • Step 2: Add the plugin definition.
  • Step 3: Make CKFinder aware of the new plugin.

Please note that you can skip the first step for plugins that just add server-side functionality and do not contain any client-side scripts.

Creating Plugin Folder and File

For a start, you need to create a directory for your plugin inside the /CKFinderJava-X.Y/ckfinder/plugins directory. Please note that by default CKFinder is provided with three client-side plugins: dummy, fileeditor, and imagresize.

important note
Remember that for CKFinder the name of the plugin folder is important and has to be the same as the name of the plugin, otherwise the application will not be able to recognize it.


Since our plugin is named filesizeplugin, the same name will be used for its folder. Inside the newly created filesizeplugin folder we are going to place the plugin.js file that will contain the plugin logic. Open the plugin.js file in an editor of your choice and add the following source code:

CKFinder.addPlugin( 'filesizeplugin', function( api ) {		// The name of the plugin.
	api.addFileContextMenuOption( { label : 'File Size', command : "FileSize" } , function( api, file )
	{
		api.connector.sendCommand( 'FileSize', { fileName : api.getSelectedFile().name }, function( xml )
		{
			if ( xml.checkError() )
				return; 
			var size = xml.selectSingleNode( 'Connector/FileSize/@size' );
			api.openMsgDialog( "", "The exact size of the file is: " + size.value + " bytes");
		} );
	});
});

Since the purpose of this tutorial is to show how to integrate plugins with CKFinder for Java, we are not going to go into the details of the JavaScript code. To sum it up, the plugin code above adds a new menu item to the user interface. This item, when selected, sends the FileSize command to the server, reads the XML response, and displays the size of the queried file in a dialog window.

For more information on how to create JavaScript plugins refer to the Writing JavaScript Plugins article.

Making CKFinder Aware of the Plugin

Fast forward one step, this section describes Step 3 listed above and explains how to make CKFinder aware that the plugin exists. Open the config.xml file and add a new plugin node to the plugins section.

<plugin>
	<name>filesizeplugin</name>
	<class>com.ckfinder.connector.plugins.FileSizePlugin</class>
	<params></params>
</plugin>

A plugin entry contains the following elements:

  • <name> – the name of the plugin that should be the same as the plugin folder name on the client side.
  • <class> – the plugin class extended from com.ckfinder.connector.configuration.Plugin.
  • <params> – additional plugin parameters. A parameter is added with a param node:
    <param name="smallThumb" value="90x90" />

Adding the Plugin Definition

When the CKFinder application starts, it reads plugins declared in the XML configuration file and binds their commands to particular event types. In other words, event handlers are registered for plugins.

To make the registration process work, a plugin class has to be created first. This class has to implement the registerEventHandlers method inside which the plugin command is joined with an appropriate event type. This binding is possible thanks to the application of the addEventHandler method:

eventHandler.addEventHandler(EventTypes.BeforeExecuteCommand, FileSizeCommand.class);

This method has two parameters: the event type, which determines when the command should be invoked, and Command.class (FileSizeCommand.class in this case), which serves as an event handler.

Below is the source code of the FileSizePlugin:

package com.ckfinder.connector.plugins;

import com.ckfinder.connector.configuration.Events;
import com.ckfinder.connector.configuration.Events.EventTypes;
import com.ckfinder.connector.configuration.Plugin;

public class FileSizePlugin extends Plugin {
	@Override
	public void registerEventHandlers(Events eventHandler) {
		eventHandler.addEventHandler(EventTypes.BeforeExecuteCommand, FileSizeCommand.class);
	}
}

Event Types

There are three types of events. You might think of them as flags indicating when the event handlers should be invoked. The table below presents CKFinder event types.

CKFinder Event Types
Event Type Description
EventTypes.AfterFileUpload Event handlers registered for this type of event will be executed after a successful file upload.
EventsTypes.BeforeExecuteCommand Event handlers registered for this type of event will be executed before a standard server-side command is executed.
EventTypes.InitCommand Event handlers registered for this type of event will be executed on CKFinder client-side startup, right before sending the result of the Init command.

Plugin Command Class

Another obligatory step in the plugin creation process is writing the plugin command class. This class has to implement the com.ckfinder.connector.data.IEventHandler interface which forces us to implement the runEventHandler method. The runEventHandler method is the place where the plugin command is executed. The class should look like this:

package com.ckfinder.connector.plugins;

import com.ckfinder.connector.configuration.IConfiguration;
import com.ckfinder.connector.data.EventArgs;
import com.ckfinder.connector.data.IEventHandler; 
import com.ckfinder.connector.errors.ConnectorException;

public class FileSizeCommand implements IEventHandler {
	public boolean runEventHandler(EventArgs args, IConfiguration configuration1)
			throws ConnectorException {
		return false;
	}
}

The runEventHandler method has two parameters: EventArgs and IConfiguration. The IConfiguration object contains all CKFinder configuration options. EventArgs is an abstract class representing one of its implementations. There is one EventArgs implementation class for each event type.

The table below lists the EventArgs implementations for each of the event types.

EventArgs Implementations
Event Type EventArgs Implementation
EventTypes.AfterFileUpload com.ckfinder.connector.data.AfterFileUploadEventArgs
EventTypes.BeforeExecuteCommand com.ckfinder.connector.data.BeforeExecuteCommandEventArgs
EventTypes.InitCommand com.ckfinder.connector.data.InitCommandEventArgs

These implementation classes store data gathered from the event caller that are necessary to execute the plugin command.

The FileSizePlugin has to handle a new non-standard server command, which means that the BeforeExecuteCommandEventArgs implementation of the EventArgs class will be needed for this example. This information is crucial since one of the first things we should do in the runEventHandler method is to cast EventArgs to its appropriate implementation. See the code below:

public boolean runEventHandler (EventArgs args, IConfiguration configuration1)
		throws ConnectorException {
	BeforeExecuteCommandEventArgs args1 = (BeforeExecuteCommandEventArgs) args;		
	return false;
}

This method returns a Boolean object which determines whether code execution should be continued after the runEventHandler method was run. For plugins that support the InitCommand and AfterFileUpload events the value that is returned does not matter since no code is executed afterwards.

CKFinder has built-in support for such commands as GetFiles, CreateFolder, or RenameFolder. When adding a new command with a plugin it is necessary to instruct CKFinder that the command has already been handled. You can achieve this by returning false inside the runEventHandler menthod after the non-standard command was executed.

The FileSizePlugin is an example of a BeforeExecuteCommand plugin, so if FileSize is the command that is being sent to the server, the plugin should handle it and then return false to prevent further search.

The code below does just that:

public boolean runEventHandler (EventArgs args, IConfiguration configuration1)
		throws ConnectorException {
	BeforeExecuteCommandEventArgs args1 = (BeforeExecuteCommandEventArgs) args;
	if ("FileSize".equals(args1.getCommand())) {
		return false;
	}
	return true;
}

If FileSize is not the command used inside the runEventHandler method, the method should return true to allow for further search.

It is also worth mentioning that the runEventHandler method of a particular plugin does not have to handle the command sent from the client. As an example, it can be used to just insert something into a database or log information about the user activity.

The client code expects an answer in the form of XML code (see the xml.selectSingleNode( 'Connector/FileSize/@size' ); line from the plugin.js file). This means that FileResizePlugin has to return an XML response. We will use the XMLCommand class to achieve this purpose; extending this class in the plugin FileSizeCommandClass makes us implement its two methods:

  • createXMLChildNodes(int errorCode, Element rootElement) – this method (described in more detail in the Returning the XML Response section below) adds all extra XML nodes to the response that will be read by a plugin on the client side. It has two parameters:
    • errorCode – an error number returned from the getDataForXml method;
    • rootElement which represents the main XML node Connector.
  • getDataForXml() – should perform validation and execute necessary plugin tasks. If all validation checks were performed correctly and the command tasks were executed without raising an exception, this method should return the CKFINDER_CONNECTOR_ERROR_NONE constant.
@Override
protected int getDataForXml() {
	return Constants.Errors.CKFINDER_CONNECTOR_ERROR_NONE ;
}

After a class is registered as an EventHandler, the runEventHandler methods of the registered classes are run when a given event is invoked by CKFinder. Since more plugins can be registered to handle one event type, it is important for the class to check in the method whether it should proceed. The example below shows that we need to check whether the command name equals FileSize. The runCommand method is called inside runEventHandler in order to invoke all other class methods and execute the command. In short, it invokes the following:

initParams();
createXMLResponse(getDataForXml());	// Invokes the createXMLChildNodes method.

With all changes introduced, the full source code of the runEventHandler method can be seen below:

public boolean runEventHandler(EventArgs args, IConfiguration configuration1)
		throws ConnectorException {
	BeforeExecuteCommandEventArgs args1 = (BeforeExecuteCommandEventArgs) args;
	if ("FileSize".equals(args1.getCommand())) {
		runCommand(args1.getRequest(), args1.getResponse(), configuration1);
		return false;
	}
	return true;
}

Finding the File Name

In order to find out for which file the size was requested, we have to know the file name. This information can be obtained from the request parameter, which in turn can be found in the initParams method of the parent Command class. This method has to be overridden in our FileSizeCommand class so that we could get the file name parameter. Since the initParams method of the Command class collects information about basic configuration parameters, it is necessary to call it, too. See below:

@Override
public void initParams(HttpServletRequest request,
		IConfiguration configuration, Object... params)
		throws ConnectorException {
	super.initParams(request, configuration, params);
}

The class should contain the fileName and fileSize fields so that we could store the desired data inside them.

public class FileSizeCommand extends XMLCommand implements IEventHandler {
	private String fileName;
	private long fileSize;
}

In order to get the file name, you should use the getParameter method as it decodes request parameters in accordance with CKFinder encoding.

@Override
public void initParams(HttpServletRequest request,
		IConfiguration configuration, Object... params)
		throws ConnectorException {
	super.initParams(request, configuration, params);
	this.fileName = getParameter(request, "fileName");
}

Getting the File Size

Time to move on to the core functionality of the plugin — getting the file size:

@Override
protected int getDataForXml() {
	File file = new File(configuration.getTypes().get(this.type).getPath()
		 + this.currentFolder,
		   this.fileName);
	this.fileSize = file.length();
	return Constants.Errors.CKFINDER_CONNECTOR_ERROR_NONE ;
}

A few notes with regard to the code above:

  • configuration.getTypes().get(this.type).getPath() – this is the path to the folder representing particular file types (files, images, Flash objects).
  • this.currentFolder – the path to the current subfolder.
  • this.fileName – the name of the requested file.
  • file.length()length is a standard java.io.File method used to return the size of the requested file in bytes.

Introducing Validation

Right now the method for getting the file size works, but it has one major flaw — it does not provide any validation checks. This means that if anything goes wrong, an ugly exception with a stack trace will be thrown and it will most probably not be particularly readable to all users. Below is the same piece of code, but this time with validation check provided:

@Override
protected int getDataForXml() {
	// Check if we have permission to see this file.
	if (!AccessControlUtil.getInstance(this.configuration)
			.checkFolderACL(this.type, this.currentFolder,
			this.userRole, 
			AccessControlUtil.CKFINDER_CONNECTOR_ACL_FILE_VIEW)) {
		return Constants.Errors.CKFINDER_CONNECTOR_ERROR_UNAUTHORIZED;
	}
	// Check if the file name is set in the request.
	if (this.fileName == null || this.fileName.equals("")) {
		return Constants.Errors.CKFINDER_CONNECTOR_ERROR_INVALID_NAME;
	}
	// Check if the file extension is on the allowed list
	// and see if the file name is allowed.
	if (FileUtils.checkFileExtension(this.fileName,
			configuration.getTypes().get(this.type),
			this.configuration, false) == 1
			|| !FileUtils.checkFileName(this.fileName)) {
		return Constants.Errors.CKFINDER_CONNECTOR_ERROR_INVALID_REQUEST;
	}
	// Create a File object from the file path.
	File file = new File(configuration.getTypes().get(this.type).getPath()
			+ this.currentFolder,
			  this.fileName);
	try {
		// Check if the file is a file and if it exists.
		if (!file.exists() || !file.isFile()) {
			return Constants.Errors.CKFINDER_CONNECTOR_ERROR_FILE_NOT_FOUND;
		}
		// Save the file size.
		this.fileSize = file.length();
		// All File objects throw the SecurityException, so we should handle it
		// to avoid getting an exception on the page.
	} catch (SecurityException e) {
		// If we are in the debug mode, we want to show the exception on the page.
		if (configuration.isDebugMode()) {
			this.exception = e;
		}			
		return Constants.Errors.CKFINDER_CONNECTOR_ERROR_ACCESS_DENIED;
	}
	// No errors.
	return Constants.Errors.CKFINDER_CONNECTOR_ERROR_NONE ;
}

Returning the XML Response

Now there is only one thing left — return the XML response. For this purpose we will use the aforementioned createXMLChildNodes method.

The XML response for the plugin should look like this:

<Connector resourceType="Files">
	<Error number="0"/>
	<CurrentFolder path="/" url="/ckfinder/userfiles/files/" acl="255"/>
	<FileSize size="5647"/>
</Connector>

The <Connector>, <Error> and <CurrentFolder> nodes are provided by the XMLCommand class. You can think of them as a template. Our only task here is adding the FileSize node. This can be achieved with the built-in CKFinder classes that provide XML file support (XmlElementData, XmlAttribute, and XMLCreator):

@Override
protected void createXMLChildNodes(int arg0, Element rootElement)
		throws ConnectorException {
	// Create a new XML element.
	XmlElementData elementData = new XmlElementData("FileSize");
	// Create a new XML attribute.
	XmlAttribute attribute = new XmlAttribute("size", String.valueOf(this.fileSize));
	// Add the attribute to the element.
	elementData.getAttributes().add(attribute);
	// Add the element to the root XML node.
	elementData.addToDocument(this.creator.getDocument(), rootElement);		
}

The plugin is ready. In order to use it you have to compile its classes and put them inside the WEB-INF/classes/ folder (the correct package folder path has to be preserved). Another solution is to create a .jar file and put it inside the WEB-INF/lib folder.

To see the full contents of the FileSizeCommand class, .

package com.ckfinder.connector.plugins;

import com.ckfinder.connector.configuration.Constants;
import com.ckfinder.connector.configuration.IConfiguration;
import com.ckfinder.connector.data.BeforeExecuteCommandEventArgs;
import com.ckfinder.connector.data.EventArgs;
import com.ckfinder.connector.data.IEventHandler;
import com.ckfinder.connector.data.XmlAttribute;
import com.ckfinder.connector.data.XmlElementData;
import com.ckfinder.connector.errors.ConnectorException;
import com.ckfinder.connector.handlers.command.XMLCommand;
import com.ckfinder.connector.utils.AccessControlUtil;
import com.ckfinder.connector.utils.FileUtils;
import java.io.File;
import javax.servlet.http.HttpServletRequest;
import org.w3c.dom.Element;

public class FileSizeCommand extends XMLCommand implements IEventHandler {
	private String fileName;
	private long fileSize;

	public boolean runEventHandler(EventArgs args, IConfiguration configuration1)
			throws ConnectorException {

		BeforeExecuteCommandEventArgs args1 = (BeforeExecuteCommandEventArgs) args;
		if ("FileSize".equals(args1.getCommand())) {
			runCommand(args1.getRequest(), args1.getResponse(), configuration1);
			return false;
		}
		return true;
	}

	@Override
	protected void createXMLChildNodes(int arg0, Element rootElement)
			throws ConnectorException {
		// Create a new XML element.
		XmlElementData elementData = new XmlElementData("FileSize");
		// Create a new XML attribute.
		XmlAttribute attribute = new XmlAttribute("size", String.valueOf(this.fileSize));
		// Add the attribute to the element.
		elementData.getAttributes().add(attribute);
		// Add the element to the root XML node.
		elementData.addToDocument(this.creator.getDocument(), rootElement);
	}

	@Override
	protected int getDataForXml() {
		// Check if we have permission to see this file.
		if (!AccessControlUtil.getInstance(this.configuration)
				.checkFolderACL(this.type, this.currentFolder,
				this.userRole,
				AccessControlUtil.CKFINDER_CONNECTOR_ACL_FILE_VIEW)) {
			return Constants.Errors.CKFINDER_CONNECTOR_ERROR_UNAUTHORIZED;
		}
		// Check if the file name is set in the request.
		if (this.fileName == null || this.fileName.equals("")) {
			return Constants.Errors.CKFINDER_CONNECTOR_ERROR_INVALID_NAME;
		}
		// Check if the file extension is on the allowed list
		// and see if the file name is allowed.
		if (FileUtils.checkFileExtension(this.fileName,
				configuration.getTypes().get(this.type),
				this.configuration, false) == 1
				|| !FileUtils.checkFileName(this.fileName)) {
			return Constants.Errors.CKFINDER_CONNECTOR_ERROR_INVALID_REQUEST;
		}
		// Create a File object from the file path.
		File file = new File(configuration.getTypes().get(this.type).getPath()
			+ this.currentFolder,
			  this.fileName);
		try {
			// Check if the file is a file and if it exists.
			if (!file.exists() || !file.isFile()) {
				return Constants.Errors.CKFINDER_CONNECTOR_ERROR_FILE_NOT_FOUND;
			}
			// Save the file size.
			this.fileSize = file.length();
		// All File objects throw the SecurityException, so we should handle it
		// to avoid getting an exception on the page.
		} catch (SecurityException e) {
			// If we are in the debug mode, we want to show the exception on the page.
			if (configuration.isDebugMode()) {
				this.exception = e;
			}
			return Constants.Errors.CKFINDER_CONNECTOR_ERROR_ACCESS_DENIED;
		}
		// No errors.
		return Constants.Errors.CKFINDER_CONNECTOR_ERROR_NONE ;
	}

	@Override
	public void initParams(HttpServletRequest request,
			IConfiguration configuration, Object... params)
			throws ConnectorException {
		super.initParams(request, configuration, params);
		this.fileName = getParameter(request, "fileName");
	}
}

This page was last edited on 23 May 2011, at 11:16.