Since JBoss Portal 2.6 it is possible to provide an easy integration of content within the portal. Up to the 2.4 version content integration had to be done by configuring a portlet to show some content from an URI and then place that portlet on a page. The new content integration capabilities allows to directly configure a page window with the content URI removing the need to configure a portlet for that purpose.
The content of a window is defined by
At runtime when the portal needs to render a window it delegates the production of markup to a content provider. The portal comes with a preconfigured set of providers which handles the portlet and the cms content types. The most natural way to plug a content provider in the portal is to use a JSR 168 Portlet. Based on a few carefully chosen conventions it is possible to provide an efficient content integration with the benefit of using standards and without requiring the usage of a proprietary API.
Content providers must be able to allow the user or administrator to chose content from the external resource it integrates in the portal in order to properly configure a portal window. A few interactions between the portal, the content provider and the portal user are necessary to achieve that goal. Here again it is possible to provide content customization using a JSR 168 Portlet. For that purpose two special portlet modes called edit_content and select_content has been introduced. It signals to the portlet that it is selecting or editing the content portion of the state of a portlet. select_content is used to select a new content to put in a window while edit_content is used to modify the previously defined content, often the two modes will display the same thing. The traditional edit mode is not used because the edit mode is more targetted to configure how the portlet show content to the end user rather than what content it shows.
Portlet components are used to integrate content into the portal. It relies on a few conventions which allow the portal and the portlet to communicate.
At runtime the portal will call the portlet with the view mode when it displays content. It will send to the portlet the information about the content to display using the render parameters. Therefore the portlet has just to read the render parameters and use them to properly display the content in the portlet. The render parameters values are the key/value pairs that forms the content properties and the resource URI is found under the uri parameter name.
As explained before, the portal will call the portlet using the edit_content mode. In that mode the portlet and the portal will communicate using either action or render parameters. We have two use cases which are:
Here is the base skeleton of the content portlet. The FSContentDrivenPortlet shows the files which are in the war file in which the portlet is deployed. The arbitrary name filesystem will be the content type interpreted by the portlet.
public class FSContentDrivenPortlet extends GenericPortlet { /** The edit_content mode. */ public static final PortletMode EDIT_CONTENT_MODE = new PortletMode("edit_content"); /** The select_content mode. */ public static final PortletMode SELECT_CONTENT_MODE = new PortletMode("select_content"); ... }
First the doDispatch(RenderRequest req, RenderResponse resp) is overriden in order to branch the requeset flow to a method that will take care of displaying the editor.
protected void doDispatch(RenderRequest req, RenderResponse resp) throws PortletException, PortletSecurityException, IOException { if (EDIT_CONTENT_MODE.equals(req.getPortletMode()) || SELECT_CONTENT_MODE.equals(req.getPortletMode())) { doEditContent(req, resp); } else { super.doDispatch(req, resp); } }
The portlet also needs a few utilities methods which take care of converting content URI to a file back and forth. There is also an implementation of a file filter that keep only text files and avoid the WEB-INF directory of the war file for security reasons.
protected File getFile(String contentURI) throws IOException { String realPath = getPortletContext().getRealPath(contentURI); if (realPath == null) { throw new IOException("Cannot access war file content"); } File file = new File(realPath); if (!file.exists()) { throw new IOException("File " + contentURI + " does not exist"); } return file; }
protected String getContentURI(File file) throws IOException { String rootPath = getPortletContext().getRealPath("/"); if (rootPath == null) { throw new IOException("Cannot access war file content"); } // Make it canonical rootPath = new File(rootPath).getCanonicalPath(); // Get the portion of the path that is significant for us String filePath = file.getCanonicalPath(); return filePath.length() >= rootPath.length() ? filePath.substring(rootPath.length()) : null; }
private final FileFilter filter = new FileFilter() { public boolean accept(File file) { String name = file.getName(); if (file.isDirectory()) { return !"WEB-INF".equals(name); } else if (file.isFile()) { return name.endsWith(".txt"); } else { return false; } } };
The editor is probably the longest part of the portlet. It tries to stay simple though and goes directly to the point.
protected void doEditContent(RenderRequest req, RenderResponse resp) throws PortletException, PortletSecurityException, IOException { // Get the uri value optionally provided by the portal String uri = req.getParameter("content.uri"); // Get the working directory directory File workingDir; if (uri != null) { workingDir = getFile(uri).getParentFile(); } else { // Otherwise try to get the current directory we are browsing, // if no current dir exist we use the root String currentDir = req.getParameter("current_dir"); if (currentDir == null) { currentDir = "/"; } workingDir = getFile(currentDir); } // Get the parent path String parentPath = getContentURI(workingDir.getParentFile()); // Get the children of the selected file, we use a filter // to retain only text files and avoid WEB-INF dir File[] children = workingDir.listFiles(filter); // Configure the response resp.setContentType("text/html"); PrintWriter writer = resp.getWriter(); // writer.print("Directories:<br/>"); writer.print("<ul>"); PortletURL choseDirURL = resp.createRenderURL(); if (parentPath != null) { choseDirURL.setParameter("current_dir", parentPath); writer.print("<li><a href=\"" + choseDirURL + "\">..</a></li>"); } for (int i = 0;i < children.length;i++) { File child = children[i]; if (child.isDirectory()) { choseDirURL.setParameter("current_dir", getContentURI(child)); writer.print("<li><a href=\"" + choseDirURL + "\">" + child.getName() + "</a></li>"); } } writer.print("</ul><br/>"); // writer.print("Files:<br/>"); writer.print("<ul>"); PortletURL selectFileURL = resp.createActionURL(); selectFileURL.setParameter("content.action.select", "select"); for (int i = 0;i < children.length;i++) { File child = children[i]; if (child.isFile()) { selectFileURL.setParameter("content.uri", getContentURI(child)); writer.print("<li><a href=\"" + selectFileURL + "\">" + child.getName() + "</a></li>"); } } writer.print("</ul><br/>"); // writer.close(); }
Last but not least the portlet needs to implement the doView(RenderRequest req, RenderResponse resp) method in order to display the file that the portal window wants to show.
protected void doView(RenderRequest req, RenderResponse resp) throws PortletException, PortletSecurityException, IOException { // Get the URI provided by the portal String uri = req.getParameter("uri"); // Configure the response resp.setContentType("text/html"); PrintWriter writer = resp.getWriter(); // if (uri == null) { writer.print("No selected file"); } else { File file = getFile(uri); FileInputStream in = null; try { in = new FileInputStream(file); FileChannel channel = in.getChannel(); byte[] bytes = new byte[(int)channel.size()]; ByteBuffer buffer = ByteBuffer.wrap(bytes); channel.read(buffer); writer.write(new String(bytes, 0, bytes.length, "UTF8")); } catch (FileNotFoundException e) { writer.print("No such file " + uri); getPortletContext().log("Cannot find file " + uri, e); } finally { if (in != null) { in.close(); } } } // writer.close(); }
Finally we need to make the portal aware of the fact that the portlet can edit and interpret content. For that we need a few descriptors. The portlet.xml descriptor will define our portlet, the portlet-instances.xml will create a single instance of our portlet. The web.xml descriptor will contain a servlet context listener that will hook the content type in the portal content type registry.
<?xml version="1.0" encoding="UTF-8"?> <portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_1_0.xsd" version="1.0"> ... <portlet> <description>File System Content Driven Portlet</description> <portlet-name>FSContentDrivenPortlet</portlet-name> <display-name>File System Content Driven Portlet</display-name> <portlet-class>org.jboss.portal.core.portlet.test.FSContentDrivenPortlet</portlet-class> <supports> <mime-type>text/html</mime-type> </supports> <portlet-info> <title>File Portlet</title> <keywords>sample,test</keywords> </portlet-info> </portlet> ... </portlet-app>
The portlet.xml descriptor
<deployments> ... <deployment> <instance> <instance-id>FSContentDrivenPortletInstance</instance-id> <portlet-ref>FSContentDrivenPortlet</portlet-ref> </instance> </deployment> ... </deployments
The portlet-instances.xml descriptor
<web-app> ... <context-param> <param-name>org.jboss.portal.content_type</param-name> <param-value>filesystem</param-value> </context-param> <context-param> <param-name>org.jboss.portal.portlet_instance</param-name> <param-value>FSContentDrivenPortletInstance</param-value> </context-param> <listener> <listener-class>org.jboss.content.ContentTypeRegistration</listener-class> </listener> ... </web-app>
The web.xml descriptor
How to create a portlet that will enable configuration of content at runtime has been covered above, however it is also possible to configure content in deployment descriptors. With our previous example it would give the following snippet placed in a *-portal.xml file:
<window> <window-name>MyWindow</window-name> <content> <content-type>filesystem</content-type> <content-uri>/dir1/foo.txt</content-uri> </content> <region>center</region> <height>1</height> </window>