Developing Applications¶
Portlets and gadgets are the two main types of eXo applications. Portlets are user interface components that provide fragments of markup code from the server side, while gadgets generate dynamic web content on the client side.
This chapter includes the following main topics:
Developing a portlet Steps to create, build and deploy a portlet in eXo Platform. CSS and JavaScript in portlets. Portlet development using frameworks like JSF, Spring MVC, Juzu.
Developing a gadget Steps to create a gadget, create and apply resources in a gadget, and many methods of customizing a gadget.
Extending eXo applications Concept and mechanism of UI Extension framework which allows the customization and extensibility of eXo applications through simple plugins.
Developing a portlet¶
In this part, you learn some portlet development techniques, including:
HelloWorld portlet Writing a very basic JSR-286 portlet.
Portlet deployment Deploying a portlet in eXo Platform by UI and by configuration.
Portlet localization Notice the resource path in eXo portlet is specific.
Portlet CSS Using portal-managed CSS with gatein-resources.xml.
Adding JavaScript to a portlet Using modularized JavaScript in eXo Platform.
Portlet preferences Portlet preferences in eXo are not user-specific.
JSF2 portlet example Writing a JSF2 portlet using JBoss Portlet Bridge.
JSF2 portlet with CDI Writing a JSF2 portlet that utilizes CDI.
Public render parameters Example of the JSR-286 feature.
Contextual properties eXo’s leverage of JSR-286 public render parameter to obtain contextual information, such as navigation URI, site and page name.
Juzu portlet Introduction to Juzu framework that makes portlet development much easier.
Spring MVC portlet it is officially supported as of Platform 4.2.
Vue.js portlet Complete sample for how to develop a Vue.js Portlet in eXo Platform.
Vuetify and Vue.js portlet Complete sample for how to develop a Vue.js and Vuetify Portlet in eXo Platform.
You should also read:
HelloWorld portlet¶
In this part, you will create a very basic portlet which contains a simple JSP page. The source code is available here.
Create a new Maven project as follows:
Edit
pom.xml
:<project> <modelVersion>4.0.0</modelVersion> <groupId>com.acme.samples</groupId> <artifactId>hello-portlet</artifactId> <version>1.0</version> <packaging>war</packaging> <build> <finalName>hello-portlet</finalName> </build> <dependencies> <dependency> <groupId>javax.portlet</groupId> <artifactId>portlet-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> </dependencies> </project>
Edit
WEB-INF/web.xml
:<web-app> <display-name>hello-portlet</display-name> </web-app>
Edit
WEB-INF/portlet.xml
:<portlet-app version="2.0" xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"> <portlet> <portlet-name>Hello</portlet-name> <portlet-class>com.acme.samples.HelloPortlet</portlet-class> <supports> <mime-type>text/html</mime-type> </supports> <portlet-info> <title>Hello</title> </portlet-info> </portlet> </portlet-app>
Edit
HelloPortlet.java
:package com.acme.samples; import java.io.IOException; import javax.portlet.GenericPortlet; import javax.portlet.PortletRequestDispatcher; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import javax.portlet.PortletException; import javax.portlet.RenderMode; public class HelloPortlet extends GenericPortlet { @RenderMode(name = "view") public void Hello(RenderRequest request, RenderResponse response) throws IOException, PortletException { PortletRequestDispatcher prDispatcher = getPortletContext().getRequestDispatcher("/jsp/hello.jsp"); prDispatcher.include(request, response); } }
Edit
jsp/hello.jsp
:<h2>Hello</h2> <h6>Welcome to Hello portlet!</h6> <p><i>Powered by eXo Platform.</i><p>
After being built, the package should be target/hello-portlet.war
.
Go to next section
to deploy it in eXo Platform.
Portlet deployment¶
The portlet war file should be installed into
$PLATFORM_TOMCAT_HOME/webapps
.
eXo Platform server supports hot deployment.
To test your portlet in action, you need to add it to a page. This task can be done in two ways:
Activating a portlet through UI¶
First, you need to register your portlet as a portal-managed application:
Log in as an administrator.
Click AdministrationApplications.
Click Portlet on the right of the screen. Scroll down to find your portlet in the list and click it.
Scroll up to see the screen below, then click Click here to add into categories.
Select one category (or more), such as Development, then save.
Once a portlet has been added to a category, you can change it as follows:
Click Manage Applications.
Find the category that has your portlet. Here you can unregister it from the category by clicking the
icon, or click Hello to edit the default permission.
The default permission takes effect when you add the portlet to a page and do not edit the permission by yourself.
Next, create a page and add the portlet to it. If you need instructions to create a page, see Adding a new page, User guide <ManagingPages.AddingNewPage>. The portlet in view mode will be as follows:
Registering a portlet by configuration¶
In the previous section, you registered the hello-portlet to an application category and add it to a page through UI. In this section, you learn how to register this portlet by configuration.
The registration of portal-managed applications is performed by
configuring the ApplicationRegistryService
service, so you need a
portal extension. In the following example, you are going to make the
hello-portlet as an extension containing service configuration. The
source code is available
here.
Make your hello-portlet a portal extension by adding
META-INF/exo-conf/configuration.xml
file:<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <target-component>org.exoplatform.container.definition.PortalContainerConfig</target-component> <component-plugin> <name>Add PortalContainer Definitions</name> <set-method>registerChangePlugin</set-method> <type>org.exoplatform.container.definition.PortalContainerDefinitionChangePlugin</type> <priority>101</priority> <init-params> <values-param> <name>apply.specific</name> <value>portal</value> </values-param> <object-param> <name>addDependencies</name> <object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependencies"> <field name="dependencies"> <collection type="java.util.ArrayList"> <value><string>hello-portlet</string></value> </collection> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
Add a new configuration file named
WEB-INF/conf/application-registry.xml
:<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <target-component>org.exoplatform.application.registry.ApplicationRegistryService</target-component> <component-plugin> <name>acme.apps</name> <set-method>initListener</set-method> <type>org.exoplatform.application.registry.ApplicationCategoriesPlugins</type> <description></description> <init-params> <object-param> <name>ACME Apps</name> <description></description> <object type="org.exoplatform.application.registry.ApplicationCategory"> <field name="name"><string>ACMEApps</string></field> <field name="displayName"><string>ACME applications</string></field> <field name="description"><string>ACME applications</string></field> <field name="accessPermissions"> <collection type="java.util.ArrayList" item-type="java.lang.String"> <value><string>*:/platform/users</string></value> </collection> </field> <field name="applications"> <collection type="java.util.ArrayList"> <value> <object type="org.exoplatform.application.registry.Application"> <field name="applicationName"><string>Hello</string></field> <field name="categoryName"><string>ACMEApps</string></field> <field name="displayName"><string>Hello</string></field> <field name="type"><string>portlet</string></field> <field name="description"><string>Hello Portlet</string></field> <field name="contentId"><string>hello-portlet/Hello</string></field> <field name="accessPermissions"> <collection type="java.util.ArrayList" item-type="java.lang.String"> <value><string>*:/platform/administrators</string></value> </collection> </field> </object> </value> </collection> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration> - ``accessPermissions``: Set this to *Everyone* if you want to make the category/portlet public. - ``contentId``: The *hello-portlet/Hello* pattern is the package name (declared in ``web.xml``) and the portlet name (declared in ``portlet.xml``).
Add the
WEB-INF/conf/configuration.xml
file to import the new configuration file:<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <import>war:/conf/application-registry.xml</import> </configuration>
After deploying the hello-portlet.war
, you can test that the portlet
is registered under the ACME applications category:
Adding portlet to page by configuration¶
Through UI, you have to register a portlet to portal-managed applications prior to adding it to a page. By configuration, it is not required.
You can download the source code used in this section here.
Assume that you have already configured a site and some pages by site extension.
To add your hello-portlet to a page, you just need to modify pages.xml
to add the following configuration:
<portlet-application>
<portlet>
<application-ref>hello-portlet</application-ref>
<portlet-ref>Hello</portlet-ref>
</portlet>
<title>Hello</title>
<access-permissions>*:/platform/users</access-permissions>
<show-info-bar>false</show-info-bar>
<show-application-state>false</show-application-state>
<show-application-mode>false</show-application-mode>
</portlet-application>
So the whole file looks like this:
<page-set xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.gatein.org/xml/ns/gatein_objects_1_2 http://www.gatein.org/xml/ns/gatein_objects_1_2"
xmlns="http://www.gatein.org/xml/ns/gatein_objects_1_2">
<page>
<name>homepage</name>
<title>Home Page</title>
<access-permissions>*:/platform/users</access-permissions>
<edit-permission>*:/platform/administrators</edit-permission>
<portlet-application>
<portlet>
<application-ref>hello-portlet</application-ref>
<portlet-ref>Hello</portlet-ref>
</portlet>
<title>Hello</title>
<access-permissions>*:/platform/users</access-permissions>
<show-info-bar>false</show-info-bar>
<show-application-state>false</show-application-state>
<show-application-mode>false</show-application-mode>
</portlet-application>
</page>
</page-set>
application-ref
: The web context that you declare inweb.xml
of the portlet package.portlet-ref
: The portlet name declared inportlet.xml
.accessPermissions
: Set it to Everyone if you want to make the portlet public.
Undeploying a portlet¶
The removal of a portlet war will not lead to auto-removal of its application registries and instances in pages. The registered application is still visible and available to be added to a page; however, it does not work anymore and the page will display a message like “This portlet encountered an error and could not be displayed“.
So you should find and remove all the instances of the portlet from every page.
Redeploying a portlet
eXo Platform server supports hot redeployment, so you can just replace the old war with the new one and it should work. However, depending on the technology the portlet uses, the hot redeployment might not work properly. In this case, a server restart is required.
Injecting a portlet using Dynamic Container¶
The mechanism
If you want to inject a portlet to every page in a site, you might add
it directly to the shared layout (sharedlayout-<SITENAME>.xml
). However, in
case you have more than one extension that overrides
sharedlayout-<SITENAME>.xml
, only the last loaded one takes effect. This leads
to trouble that portlets injection cannot be solved in packaging, it
will require extra tasks in deployment (like merging several layouts
from different projects).
As of 4.1, the trouble is solved by the Dynamic Container feature. A shared layout and an extension get involved in how it works:
The shared layout must contain some Dynamic Container instances
To make a site ready to inject portlets, there should be some Dynamic Containers added to the shared layout. This is done by
sharedlayout-<SITENAME>.xml
, like this:<container id="top-dynamic-container" template="system:/groovy/portal/webui/container/UIAddOnContainer.gtmpl"> <name>top-dynamic-container</name> <factory-id>addonContainer</factory-id> </container>
The extension project must configure a component plugin to inject portlets to a container.
So it is important that the extension project is aware of the container name. The configuration will be described later.
In the heart of the feature is the component plugin
org.exoplatform.commons.addons.AddOnPluginImpl
that takes care of injecting specified portlets to a specified Dynamic Container. This makes Dynamic Container a special kind of container, because the portlets that it will contain are pre-defined. In other words, the portlet drag-and-drop is not in the Dynamic Container designation.
So in this way, whenever the named container instance is put into a
page, or all pages via sharedlayout-<SITENAME>.xml
, the portlet injection is
done automatically. An extension does not have to override the layout.
Example
In the following example, you inject the “Help” portlet (a built-in, for simplification) into all pages of the Intranet site. The Help portlet is already featured at the top right of the homepage by default, so you will add another one to the left. The source code of this example is here.
Make a custom extension as described in Portal extension section.
Edit WEB-INF/conf/configuration.xml
:
<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"
xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd">
<external-component-plugins>
<target-component>org.exoplatform.commons.addons.AddOnService</target-component>
<component-plugin>
<name>addPlugin</name>
<set-method>addPlugin</set-method>
<type>org.exoplatform.commons.addons.AddOnPluginImpl</type>
<description></description>
<init-params>
<value-param>
<name>priority</name>
<value>5</value>
</value-param>
<value-param>
<name>containerName</name>
<value>left-topNavigation-container</value>
</value-param>
<object-param>
<name>help-portlet</name>
<description></description>
<object type="org.exoplatform.portal.config.serialize.PortletApplication">
<field name="state">
<object type="org.exoplatform.portal.config.model.TransientApplicationState">
<field name="contentId">
<string>platformNavigation/UIHelpPlatformToolbarPortlet</string>
</field>
</object>
</field>
</object>
</object-param>
</init-params>
</component-plugin>
</external-component-plugins>
</configuration>
In which:
The container instance is identified by
containerName
. Find below a picture that depicts the default layout of the Intranet site.The portlets are identified by
contentId
. In the example,platformNavigation
is the webapp name (declared inweb.xml
), andUIHelpPlatformToolbarPortlet
is the portlet name (declared inportlet.xml
).To inject more than one portlet, add more object-param with different names.
Default Dynamic Container instances
Here are the Dynamic Container instances in the Intranet site:
For a customized site, you can manage Dynamic Containers by customizing shared layout. The configuration sample is given above. There are two templates of Dynamic Container:
system:/groovy/portal/webui/container/UIAddOnContainer.gtmpl
system:/groovy/portal/webui/container/UIAddOnColumnContainer.gtmpl
Portlet localization¶
In this example you add some language resources and CSS to be used in the JSP of the HelloWorld portlet. You can download the portlet’s source code here.
The example is plain JSR-286, except one thing: eXo expects that the
resource bundle should be found in the locale/portlet
folder. The
path is fixed and you need to pack your .properties
files in a
sub-folder of this path.
Create a new Maven project as follows:
Edit
pom.xml
:<project> <modelVersion>4.0.0</modelVersion> <groupId>com.acme.samples</groupId> <artifactId>hello-portlet</artifactId> <version>1.0</version> <packaging>war</packaging> <build> <finalName>hello-portlet</finalName> </build> <dependencies> <dependency> <groupId>javax.portlet</groupId> <artifactId>portlet-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> </dependencies> </project>
Edit
web.xml
:<web-app> <display-name>hello-portlet</display-name> </web-app>
Edit
HelloPortlet.java
:package com.acme.samples; import java.io.IOException; import javax.portlet.GenericPortlet; import javax.portlet.PortletRequestDispatcher; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import javax.portlet.PortletException; import javax.portlet.RenderMode; public class HelloPortlet extends GenericPortlet { @RenderMode(name = "view") public void Hello(RenderRequest request, RenderResponse response) throws IOException, PortletException { PortletRequestDispatcher prDispatcher = getPortletContext().getRequestDispatcher("/jsp/hello.jsp"); prDispatcher.include(request, response); } }
Edit
HelloPortlet_en.properties
to add language properties:com.acme.samples.HelloPortlet.Hello=Hello! com.acme.samples.HelloPortlet.Msg1=This is a portlet example. com.acme.samples.HelloPortlet.Msg2=Written by a baker.
Edit
HelloPortlet_fr.properties
to add language properties:com.acme.samples.HelloPortlet.Hello=Bonjour! com.acme.samples.HelloPortlet.Msg1=C'est un example de portlet. com.acme.samples.HelloPortlet.Msg2=Ecrit par un boulanger.
Edit
portlet.xml
to register supported locale and the resource-bundle:<portlet-app version="2.0" xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"> <portlet> <portlet-name>Hello</portlet-name> <portlet-class>com.acme.samples.HelloPortlet</portlet-class> <supports> <mime-type>text/html</mime-type> </supports> <supported-locale>en</supported-locale> <resource-bundle>locale.portlet.HelloPortlet.HelloPortlet</resource-bundle> <portlet-info> <title>Hello</title> </portlet-info> </portlet> </portlet-app>
Edit
Stylesheet.css
:.HelloPortlet1, .HelloPortlet2, .HelloPortlet3 { padding: 10px; font-style: italic; font-size: 18px; width: 400px; } .HelloPortlet1 { background-color: antiquewhite; } .HelloPortlet2 { background-color: lemonchiffon; } .HelloPortlet3 { background-color: wheat; }
Edit
hello.jsp
to add language properties:<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %> <%@ page import="java.util.ResourceBundle"%> <portlet:defineObjects /> <% String contextPath = request.getContextPath(); ResourceBundle resource = portletConfig.getResourceBundle(request.getLocale()); %> <link rel="stylesheet" type="text/css" href="<%=contextPath%>/skin/Stylesheet.css"/> <div class="HelloPortlet1"> <span><%=resource.getString("com.acme.samples.HelloPortlet.Hello")%></span> </div> <div class="HelloPortlet2"> <span><%=resource.getString("com.acme.samples.HelloPortlet.Msg1")%></span> </div> <div class="HelloPortlet3"> <span><%=resource.getString("com.acme.samples.HelloPortlet.Msg2")%></span> </div> - Notice the *taglib* and *portlet:defineObjects* is added to be able to use the ``portletConfig`` object. - To simplify this example, a CSS link is added to the body (JSP) but this is not recommended. Please see the :ref:`Portlet CSS <PLFDevGuide.DevelopingApplications.DevelopingPortlet.CSS>` section for a better way.
After deployment, add the portlet to a page and test:
Note
The locale resource bundle needs to be packed in a sub folder under WEB-INF/classes/locale/portlet/
.
Portlet CSS¶
In the example of Portlet localization, the CSS resource is added into the JSP. It might make the page slow and ugly.
<link rel="stylesheet" type="text/css" href="<%=contextPath%>/skin/Stylesheet.css"/>
<div>..</div>
In this section you improve it by letting the portal manage your CSS resource. You can download all source code used in this section here.
The registration of a CSS resource to the portal is done via
WEB-INF/gatein-resources.xml
in your .war. For this purpose you
will make your webapp a portal
extension, by adding
META-INF/exo-conf/configuration.xml
file:
<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"
xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd">
<external-component-plugins>
<target-component>org.exoplatform.container.definition.PortalContainerConfig</target-component>
<component-plugin>
<name>Add PortalContainer Definitions</name>
<set-method>registerChangePlugin</set-method>
<type>org.exoplatform.container.definition.PortalContainerDefinitionChangePlugin</type>
<priority>200</priority>
<init-params>
<values-param>
<name>apply.specific</name>
<value>portal</value>
</values-param>
<object-param>
<name>addDependencies</name>
<object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependencies">
<field name="dependencies">
<collection type="java.util.ArrayList">
<value>
<string>hello-portlet</string>
</value>
</collection>
</field>
</object>
</object-param>
</init-params>
</component-plugin>
</external-component-plugins>
</configuration>
The CSS resource is registered like below:
Add the
WEB-INF/gatein-resources.xml
file so that you have:
Edit
gatein-resources.xml
:<gatein-resources xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gatein.org/xml/ns/gatein_resources_1_3 http://www.gatein.org/xml/ns/gatein_resources_1_3" xmlns="http://www.gatein.org/xml/ns/gatein_resources_1_3"> <portlet-skin> <application-name>hello-portlet</application-name> <portlet-name>Hello</portlet-name> <skin-name>Enterprise</skin-name> <css-path>/skin/Stylesheet.css</css-path> </portlet-skin> </gatein-resources> - The application-name is the name of war file and needs to be configured as the same value in ``web.xml``. - The portlet-name is configured in ``portlet.xml``. - Do not miss the :ref:`note <Note-Using-Shared-CSS-Resource>` at the end of this section.
Modify the jsp/hello.jsp
file (to remove the link tag):
<%@ taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
<%@ page import="java.util.ResourceBundle"%>
<%@ page import="org.exoplatform.services.resources.ResourceBundleService"%>
<%@ page import="org.exoplatform.container.PortalContainer"%>
<portlet:defineObjects />
<%
String contextPath = request.getContextPath();
ResourceBundle resource = portletConfig.getResourceBundle(request.getLocale());
%>
<div class="HelloPortlet1">
<span><%=resource.getString("com.acme.samples.HelloPortlet.Hello")%></span>
</div>
<div class="HelloPortlet2">
<span><%=resource.getString("com.acme.samples.HelloPortlet.Msg1")%></span>
</div>
<div class="HelloPortlet3">
<span><%=resource.getString("com.acme.samples.HelloPortlet.Msg2")%></span>
</div>
The result will be:
Adding JavaScript to a portlet¶
In this example, you add a button to the Hello portlet and use jQuery to register an event for the button. When you click the “here” button, a popup will appear. The source code used in this section is here.
Note
This is a quick tutorial. You are strongly recommended to read Developing JavaScript chapter to write your JavaScript safely in eXo Platform.
The registration of a JavaScript module is done via
WEB-INF/gatein-resources.xml
in your .war. For this purpose you
will make your webapp a portal extension,
by adding META-INF/exo-conf/configuration.xml
file:
<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"
xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd">
<external-component-plugins>
<target-component>org.exoplatform.container.definition.PortalContainerConfig</target-component>
<component-plugin>
<name>Add PortalContainer Definitions</name>
<set-method>registerChangePlugin</set-method>
<type>org.exoplatform.container.definition.PortalContainerDefinitionChangePlugin</type>
<priority>101</priority>
<init-params>
<values-param>
<name>apply.specific</name>
<value>portal</value>
</values-param>
<object-param>
<name>addDependencies</name>
<object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependencies">
<field name="dependencies">
<collection type="java.util.ArrayList">
<value>
<string>hello-portlet</string>
</value>
</collection>
</field>
</object>
</object-param>
</init-params>
</component-plugin>
</external-component-plugins>
</configuration>
The JavaScript is added like below:
Add the
WEB-INF/gatein-resources.xml
file:<gatein-resources xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gatein.org/xml/ns/gatein_resources_1_3 http://www.gatein.org/xml/ns/gatein_resources_1_3" xmlns="http://www.gatein.org/xml/ns/gatein_resources_1_3"> <portlet> <name>Hello</name> <module> <script> <path>/js/foo.js</path> </script> <depends> <module>jquery</module> <as>jq</as> </depends> </module> </portlet> </gatein-resources>
Add the
/js/foo.js
file tosrc/main/webapp
:(function($) { $("body").on("click", ".hello .btn", function() { alert("Hello World!"); }); })(jq);
Modify the
jsp/hello.jsp
file:<div class='hello'> <h2>Hello</h2> <h6>Welcome to Hello portlet!</h6> <p>Click <a class='btn'>here</a> to display the popup window.</p> <p><i>Powered by eXo Platform.</i></p> </div>
The result when you click the “here” button:
Portlet preferences¶
JSR-168 lets the implementations decide whether portlet preferences are user-specific or not.
In this example you will learn that portlet preferences in eXo are not user-specific. The source code of this example is here.
Create a new Maven project as follows:
Edit
pom.xml
:<project> <modelVersion>4.0.0</modelVersion> <groupId>com.acme.samples</groupId> <artifactId>hello-portlet</artifactId> <version>1.0</version> <packaging>war</packaging> <build> <finalName>hello-portlet</finalName> </build> <dependencies> <dependency> <groupId>javax.portlet</groupId> <artifactId>portlet-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> </dependencies> </project>
Edit
web.xml
:<web-app> <display-name>hello-portlet</display-name> </web-app>
Edit
HelloPortlet.java
:package com.acme.samples; import java.io.IOException; import javax.portlet.GenericPortlet; import javax.portlet.PortletRequestDispatcher; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import javax.portlet.PortletException; import javax.portlet.ActionRequest; import javax.portlet.ActionResponse; import javax.portlet.PortletMode; import javax.portlet.PortletPreferences; public class HelloPortlet extends GenericPortlet { @Override protected void doView(RenderRequest request, RenderResponse response) throws IOException, PortletException { PortletRequestDispatcher dispatcher = getPortletContext().getRequestDispatcher("/jsp/view.jsp"); dispatcher.forward(request, response); } @Override protected void doEdit(RenderRequest request, RenderResponse response) throws IOException, PortletException { PortletRequestDispatcher dispatcher = getPortletContext().getRequestDispatcher("/jsp/edit.jsp"); dispatcher.forward(request, response); } @Override public void processAction(ActionRequest request, ActionResponse response) throws IOException, PortletException { String borderColor = request.getParameter("border_color"); PortletPreferences preferences = request.getPreferences(); preferences.setValue("border_color", borderColor); preferences.store(); response.setPortletMode(PortletMode.VIEW); } }
Edit
portlet.xml
to support VIEW and EDIT modes:<portlet-app version="2.0" xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"> <portlet> <portlet-name>Hello</portlet-name> <portlet-class>com.acme.samples.HelloPortlet</portlet-class> <supports> <mime-type>text/html</mime-type> <portlet-mode>VIEW</portlet-mode> <portlet-mode>EDIT</portlet-mode> </supports> <portlet-info> <title>Portlet preferences</title> </portlet-info> </portlet> </portlet-app>
Edit
view.jsp
:<%@ page import="javax.portlet.PortletURL" %> <%@ page import="javax.portlet.PortletMode" %> <%@ page import="javax.portlet.PortletPreferences" %> <%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%> <portlet:defineObjects/> <% PortletURL editURL = renderResponse.createRenderURL(); editURL.setPortletMode(PortletMode.EDIT); PortletPreferences preferences = renderRequest.getPreferences(); String borderColor = preferences.getValue("border_color", "transparent"); %> <div style="border: solid 1px <%=borderColor%>"> <a href="<%=editURL%>">Click here to switch to edit mode!</a> </div>
Edit
edit.jsp
:<%@ page import="javax.portlet.PortletURL" %> <%@ page import="javax.portlet.PortletMode" %> <%@ page import="javax.portlet.PortletPreferences" %> <%@ taglib uri="http://java.sun.com/portlet_2_0" prefix="portlet"%> <portlet:defineObjects/> <% PortletURL viewURL = renderResponse.createRenderURL(); viewURL.setPortletMode(PortletMode.VIEW); PortletURL actionURL = renderResponse.createActionURL(); PortletPreferences preferences = renderRequest.getPreferences(); String borderColor = preferences.getValue("border_color", "transparent"); %> <div style="border: solid 1px <%=borderColor%>"> <a href="<%=viewURL%>">Click here to switch to view mode!</a> <p></p> <form action="<%=actionURL%>" method="POST"> <label>Select border color:</label> <select name="border_color"> <option value="transparent" <%=(borderColor == "transparent" ? "selected=\"selected\"" : "")%>>None</option> <option value="red" <%=(borderColor == "red" ? "selected=\"selected\"" : "")%>>Red</option> <option value="blue" <%=(borderColor == "blue" ? "selected=\"selected\"" : "")%>>Blue</option> </select> <input type="submit" value="Save"/> </form> </div>
After deployment, add the portlet to a page and test:
Note
Again, the portlet preferences are not user-specific. In this example, when a user changes the value of border color, it affects other users.
JSF2 portlet example¶
In this example, you write a JSF2 portlet using JBoss Portlet Bridge (JPB).
The code sample originates from JPB project, and is modified in this example so that you can build it independently.
Create a new Maven project as follows:
Edit
pom.xml
:<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.jboss.portletbridge.examples</groupId> <artifactId>jsf2portlet-example</artifactId> <packaging>war</packaging> <name>JSF 2 Portlet Example</name> <version>1.0</version> <dependencies> <dependency> <groupId>com.sun.faces</groupId> <artifactId>jsf-api</artifactId> <version>2.1.14</version> </dependency> <dependency> <groupId>com.sun.faces</groupId> <artifactId>jsf-impl</artifactId> <scope>runtime</scope> <version>2.1.14</version> </dependency> <dependency> <groupId>org.jboss.portletbridge</groupId> <artifactId>portletbridge-api</artifactId> <version>3.1.2.Final</version> </dependency> <dependency> <groupId>org.jboss.portletbridge</groupId> <artifactId>portletbridge-impl</artifactId> <version>3.1.2.Final</version> <scope>runtime</scope> </dependency> </dependencies> <build> <finalName>jsf2portlet-example</finalName> </build> </project>
Pay attention to the runtime
scope. This tells Maven to include the
dependencies to WEB-INF/lib
.
Note
The portlet bridge libraries must be available and are usually bundled with the WEB-INF/lib
directory of the web archive.
Write the managed bean
Echo.java
:package com.acme.samples.jsf2portlet; import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; import javax.faces.event.ActionEvent; @ManagedBean(name = "echo") @SessionScoped public class Echo { String str = "hello"; public String getStr() { return str; } public void setStr(String str) { this.str = str; } public void reset(ActionEvent ae) { str = ""; } }
Edit
web.xml
:<?xml version="1.0"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <display-name>JSF2 Portlet Example</display-name> <context-param> <param-name>javax.portlet.faces.RENDER_POLICY</param-name> <param-value>ALWAYS_DELEGATE</param-value> </context-param> <context-param> <param-name>javax.faces.FACELETS_VIEW_MAPPINGS</param-name> <param-value>*.xhtml</param-value> </context-param> <context-param> <param-name>javax.faces.DEFAULT_SUFFIX</param-name> <param-value>.xhtml</param-value> </context-param> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.faces</url-pattern> </servlet-mapping> </web-app>
The context-params are explained at http://myfaces.apache.org/core21/myfaces-impl/webconfig.html.
Edit
*.xhtml
files:main.xhtml
:<f:view id="ajaxEcho" xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <h:head /> <h:body> <h2>JSF 2 portlet</h2> <p>this is simple JSF 2.0 portlet with AJAX echo.</p> <h:form id="form1"> Output: <h:outputText id="out1" value="#{echo.str}" /> <br /> Input: <h:inputText id="in1" autocomplete="off" value="#{echo.str}"> <f:ajax render="out1" /> </h:inputText> <br /> <!-- A no-op button, just to lose the focus from "in1" --> <h:commandButton id="button1" value="Echo" type="button" /> <br /> <!-- Resets the string, refreshes the form, but not the page --> <h:commandButton id="reset" value="reset" actionListener="#{echo.reset}"> <f:ajax render="@form" /> </h:commandButton> <!-- Reloads the page, doesn't reset the string --> <h:commandButton id="reload" value="reload" /> <h:messages /> </h:form> </h:body> </f:view>
Here the tag f:ajax is used.
edit.xhtml
:<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html"> Edit Mode </ui:composition>
help.xhtml
:<ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html"> Help Mode </ui:composition>
Edit
portlet.xml
:<?xml version="1.0" encoding="UTF-8"?> <portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"> <portlet> <portlet-name>jsf2portlet</portlet-name> <portlet-class>javax.portlet.faces.GenericFacesPortlet</portlet-class> <init-param> <name>javax.portlet.faces.defaultViewId.view</name> <value>/pages/main.xhtml</value> </init-param> <init-param> <name>javax.portlet.faces.defaultViewId.edit</name> <value>/pages/edit.xhtml</value> </init-param> <init-param> <name>javax.portlet.faces.defaultViewId.help</name> <value>/pages/help.xhtml</value> </init-param> <init-param> <name>javax.portlet.faces.preserveActionParams</name> <value>true</value> </init-param> <expiration-cache>0</expiration-cache> <supports> <mime-type>text/html</mime-type> <portlet-mode>VIEW</portlet-mode> <portlet-mode>EDIT</portlet-mode> <portlet-mode>HELP</portlet-mode> </supports> <portlet-info> <title>JSF 2.0 AJAX Portlet</title> </portlet-info> </portlet> </portlet-app>
This last step makes the JSF2 application a portlet.
Deploy the portlet, add it to a page as instructed in previous sections, and test it:
Some references:
JSF2 portlet with CDI¶
In previous section, you have learnt to write JSF2 portlet. In this section, your JSF2 portlet will utilize CDI (Contexts and Dependency Injection).
This section will not explain JBoss Portlet Brigde again, so get back to the previous section if necessary.
The code sample can be found here.
So why CDI?
If you want to get a quick understanding about CDI, and current Dependency Injection frameworks, this introduction may help. As CDI is a part of Java EE specification, Oracle’s Documentation is always recommended.
Note this tutorial sticks with Weld, CDI implementation of JBoss.
In this tutorial, you learn the basic CDI via an example, in which you use @Inject annotation, with some Scopes and Qualifiers.
In the example, your JSF2 portlet is a form in which users input email subject/body and press buttons to send emails. There are two kinds of recipients - “customers” and “partners” - so you have two buttons. See the screenshot below:
The To
mail lists are different in the two cases. So “customers”
addresses are provided by a Bean, and “partners” are provided by a
modified one of that Bean. Both are at ApplicationScoped
.
The From
field will be the email of the logged-in user. It is
provided by another Bean at SessionScoped
.
The base idea of CDI is: your application (the portlet in this example) does not create the Mail list providers, but let CDI create and manage the lifecycle of them, so the application always gets the same object for the same context. As important as that, it is CDI which knows the chain of the dependencies, not the application.
The three Beans are declared by annotations, but need to be packaged
together with a beans.xml
file.
Now let’s start your project. Again, see the full code sample at GitHub.
Create a Maven project with the following structure:
In
pom.xml
, add the dependencies of JSF, JPB and CDI, and also some eXo dependencies to work with eXo Mail and Social services.<!-- CDI (Contexts and Dependency Injection) --> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.enterprise</groupId> <artifactId>cdi-api</artifactId> <version>1.0-SP4</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.gatein</groupId> <artifactId>cdi-portlet-integration</artifactId> <version>1.0.3.Final</version> <scope>runtime</scope> </dependency> <!-- eXo --> <dependency> <groupId>org.exoplatform.core</groupId> <artifactId>exo.core.component.security.core</artifactId> <version>2.5.13-GA</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.exoplatform.social</groupId> <artifactId>social-component-core</artifactId> <version>4.2.0</version> <scope>provided</scope> </dependency>
Edit the
WEB-INF/beans.xml
file:<?xml version="1.0"?> <beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://jboss.org/schema/cdi/beans_1_0.xsd"> <!-- This file is required to enable CDI for this web-app. There is nothing here because the beans will be declared using annotations. In case your beans are packaged in jar, this file should be placed under META-INF/ folder. --> </beans>
Edit the
WEB-INF/portlet.xml
file. You need to configure the portlet filter to theorg.gatein.cdi.PortletCDIFilter
class.<?xml version="1.0" encoding="UTF-8"?> <portlet-app version="2.0" xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"> <portlet> <portlet-name>jsf2portlet-cdi-example</portlet-name> <portlet-class>javax.portlet.faces.GenericFacesPortlet</portlet-class> <init-param> <name>javax.portlet.faces.defaultViewId.view</name> <value>/pages/main.xhtml</value> </init-param> <init-param> <name>javax.portlet.faces.preserveActionParams</name> <value>true</value> </init-param> <expiration-cache>0</expiration-cache> <supports> <mime-type>text/html</mime-type> <portlet-mode>VIEW</portlet-mode> </supports> <portlet-info> <title>JSF2 Portlet CDI</title> </portlet-info> </portlet> <filter> <filter-name>PortletCDIFilter</filter-name> <filter-class>org.gatein.cdi.PortletCDIFilter</filter-class> <lifecycle>ACTION_PHASE</lifecycle> <lifecycle>EVENT_PHASE</lifecycle> <lifecycle>RENDER_PHASE</lifecycle> <lifecycle>RESOURCE_PHASE</lifecycle> </filter> <filter-mapping> <filter-name>PortletCDIFilter</filter-name> <portlet-name>jsf2portlet-cdi-example</portlet-name> </filter-mapping> </portlet-app>
Edit the
WEB-INF/web.xml
file. It is the same as the basic JSF2 portlet, so not repeated here.Edit the
pages/main.xhtml
file.<f:view id="ajaxEcho" xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <h:head /> <h:body> <h2>JSF 2 portlet</h2> <h:form id="form1"> Subject: <h:inputText id="subject" autocomplete="off" value="#{mailSender.subject}"></h:inputText> <br/> Message: <h:inputTextarea id="body" value="#{mailSender.body}"></h:inputTextarea> <br/> <h:commandButton id="sendCustomer" value="Send Customers" actionListener="#{mailSender.sendCustomers}"></h:commandButton> <br/> <h:commandButton id="sendPartners" value="Send Partners" actionListener="#{mailSender.sendPartners}"></h:commandButton> </h:form> </h:body> </f:view>
Create the
MailList.java
interface:package org.exoplatform.samples.jsf2portlet.cdi; public interface MailList { public String getMailList(); }
There will be two implementations of this interface. In companion with CDI, you annotate the two with Qualifiers. For that, you will create two qualifiers, Customer and Partner.
Edit the two qualifiers. In
Customer.java
:package org.exoplatform.samples.jsf2portlet.cdi; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE; import java.lang.annotation.Retention; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Target; import javax.inject.Qualifier; @Qualifier @Retention(RUNTIME) @Target({TYPE, METHOD, FIELD, PARAMETER}) public @interface Customer {}
And do the same with
Partner.java
.Implement the MailList interface. Use the qualifier Customer in
CustomerMailList.java
:package org.exoplatform.samples.jsf2portlet.cdi; import javax.faces.bean.ApplicationScoped; import javax.faces.bean.ManagedBean; @ManagedBean @ApplicationScoped @Customer public class CustomerMailList implements MailList{ public String getMailList() { return "[email protected], [email protected]"; } }
Do it similarly in
PartnerMailList.java
, use the qualifier Partner.Edit
UserBean.java
. This bean provides the current user email, so its scope should be SessionScoped.package org.exoplatform.samples.jsf2portlet.cdi; import javax.faces.bean.ManagedBean; import javax.faces.bean.SessionScoped; import org.exoplatform.container.ExoContainerContext; import org.exoplatform.services.security.ConversationState; import org.exoplatform.social.core.manager.IdentityManager; import org.exoplatform.social.core.identity.model.*; import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider; @ManagedBean @SessionScoped public class UserBean { private String userEmail; public UserBean() { IdentityManager identityManager = (IdentityManager) ExoContainerContext.getCurrentContainer().getComponentInstanceOfType(IdentityManager.class); String currentUserId = ConversationState.getCurrent().getIdentity().getUserId(); Identity currentIdentity = identityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, currentUserId, false); Profile profile = currentIdentity.getProfile(); userEmail = profile.getEmail(); } public String getUserEmail() { return userEmail; } }
Now you have all dependencies that your JSF portlet will use. So let’s finish the portlet.
Edit the
MailSender.java
file:package org.exoplatform.samples.jsf2portlet.cdi; import javax.inject.*; import javax.faces.bean.*; import org.exoplatform.services.mail.MailService; import org.exoplatform.services.mail.Message; import org.exoplatform.container.ExoContainerContext; @ManagedBean public class MailSender { private String subject, body; @Inject @Customer MailList customerMailList; @Inject @Partner MailList partnerMailList; @Inject UserBean userBean; public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public void sendCustomers() { Message message = new Message(); message.setSubject(subject); message.setBody(body); message.setFrom(userBean.getUserEmail()); message.setTo(customerMailList.getMailList()); try { ExoContainerContext.getService(MailService.class).sendMessage(message); } catch (Exception e) { e.printStackTrace(); } } public void sendPartners() { Message message = new Message(); message.setSubject(subject); message.setBody(body); message.setFrom(userBean.getUserEmail()); message.setTo(partnerMailList.getMailList()); try { ExoContainerContext.getService(MailService.class).sendMessage(message); } catch (Exception e) { e.printStackTrace(); } } }
Public render parameters¶
In this example you write two portlets: one sets a value of a public parameter, and the other consumes the value.
The source code of this example is here.
Create a new Maven project as follows:
Edit
pom.xml
:<project> <modelVersion>4.0.0</modelVersion> <groupId>com.acme.samples</groupId> <artifactId>hello-portlet</artifactId> <version>1.0</version> <packaging>war</packaging> <build> <finalName>prp-portlet</finalName> </build> <dependencies> <dependency> <groupId>javax.portlet</groupId> <artifactId>portlet-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> </dependencies> </project>
Edit
web.xml
:<web-app> <display-name>prp-portlet</display-name> </web-app>
Edit
portlet.xml
:<portlet-app version="2.0" xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"> <portlet> <portlet-name>Sharing-PRP-Portlet</portlet-name> <portlet-class>com.acme.samples.SharingPRPPortlet</portlet-class> <supports> <mime-type>text/html</mime-type> </supports> <portlet-info> <title>Sharing-PRP-Portlet</title> </portlet-info> <supported-public-render-parameter>current_time</supported-public-render-parameter> </portlet> <portlet> <portlet-name>Consuming-PRP-Portlet</portlet-name> <portlet-class>com.acme.samples.ConsumingPRPPortlet</portlet-class> <supports> <mime-type>text/html</mime-type> </supports> <portlet-info> <title>Consuming-PRP-Portlet</title> </portlet-info> <supported-public-render-parameter>current_time</supported-public-render-parameter> </portlet> <public-render-parameter> <identifier>current_time</identifier> <name>current_time</name> </public-render-parameter> </portlet-app> - In case you pack the two portlets separately, the two ``portlet.xml`` files must repeat the same *public-render-parameter* and *supported-public-render-parameter* elements. In other words, there is no difference between the sharing portlet and the consuming one's configuration.
Edit
SharingPRPPortlet.java
:package com.acme.samples; import java.io.IOException; import java.io.PrintWriter; import java.util.Date; import javax.portlet.GenericPortlet; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import javax.portlet.PortletException; import javax.portlet.ActionRequest; import javax.portlet.ActionResponse; import javax.portlet.PortletURL; public class SharingPRPPortlet extends GenericPortlet { @Override public void processAction(ActionRequest request, ActionResponse response) throws IOException, PortletException { response.setRenderParameter("current_time", new Date(System.currentTimeMillis()).toString()); } @Override public void doView(RenderRequest request, RenderResponse response) throws IOException, PortletException { PortletURL actionURL = response.createActionURL(); PrintWriter w = response.getWriter(); w.write("<p>Click <a href=\"" + actionURL.toString() + "\">here</a> to execute processAction()</p>"); w.write("<span>" + request.getParameter("current_time") + "</span>"); w.close(); } }
Edit
ConsumingPRPPortlet.java
:package com.acme.samples; import java.io.IOException; import java.io.PrintWriter; import java.util.Map; import javax.portlet.GenericPortlet; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import javax.portlet.PortletException; import javax.portlet.ActionRequest; import javax.portlet.ActionResponse; public class ConsumingPRPPortlet extends GenericPortlet { @Override public void doView(RenderRequest request, RenderResponse response) throws IOException, PortletException { Map<String, String[]> paramNames = request.getPublicParameterMap(); PrintWriter w = response.getWriter(); for (String name : paramNames.keySet()) { String value = request.getParameter(name); w.write("<p>" + "*<b>" + name + "</b>: " + value + "</p>"); } w.close(); } } - In ``SharingPRPPortlet.java``, the ``current_time`` parameter is set by the ``processAction()`` method, so the ``doView()`` method provides a link to trigger ``processAction()``. - While both the portlets prints ``current_time``, the ``ConsumingPRPPortlet`` portlet gets and prints all the public parameters that it supports. Add the two portlets to a page and test them:
Contextual properties¶
ContextualPropertyManager service and plugins give you a way to access information of portal context, like site type, page name and node URI. You can also inject a property as you want.
Note
Such properties are accessed in the same way as public render parameters, but unlike public render parameters, contextual properties values cannot and should not be changed by the portlet.
In this example, you write a ContextualPropertyManager plugin that adds a parameter (called current_time), and a portlet that gets all the public contextual properties, including your one and the built-in ones.
The source code of this example is here.
The ContextualPropertyManager plugin project
Create a new Maven project as follows:
Edit
pom.xml
:<project> <modelVersion>4.0.0</modelVersion> <groupId>com.acme.samples</groupId> <artifactId>cp-plugin</artifactId> <version>1.0</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.gatein.portal</groupId> <artifactId>exo.portal.webui.portal</artifactId> <version>3.5.10.Final</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.exoplatform.kernel</groupId> <artifactId>exo.kernel.container</artifactId> <version>2.4.9-GA</version> <scope>provided</scope> </dependency> </dependencies> </project>
Edit
CPPlugin.java
:package com.acme.samples; import java.util.Map; import java.util.Date; import org.exoplatform.portal.application.state.AbstractContextualPropertyProviderPlugin; import javax.xml.namespace.QName; import org.exoplatform.container.xml.InitParams; import org.exoplatform.portal.webui.application.UIPortlet; public class CPPlugin extends AbstractContextualPropertyProviderPlugin { private QName myQName; public CPPlugin (InitParams params) { super(params); this.myQName = new QName(namespaceURI, "current_time"); } @Override public void getProperties(UIPortlet portletWindow, Map<QName, String[]> properties) { addProperty(properties, myQName, new Date(System.currentTimeMillis()).toString()); } }
Edit
conf/portal/configuration.xml
:<configuration> <external-component-plugins> <target-component>org.exoplatform.portal.application.state.ContextualPropertyManager</target-component> <component-plugin> <name>CPPlugin</name> <set-method>addPlugin</set-method> <type>com.acme.samples.CPPlugin</type> <priority>1</priority> <init-params> <value-param> <name>namespaceURI</name> <description>Namespace URI</description> <value>http://www.gatein.org/xml/ns/prp_1_0</value> </value-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
Build the project and install
target/cp-plugin-1.0.jar
to thelib
folder of the server.
The portlet project
Create a Maven project as follows:
Edit
pom.xml
:<project> <modelVersion>4.0.0</modelVersion> <groupId>com.acme.samples</groupId> <artifactId>hello-portlet</artifactId> <version>1.0</version> <packaging>war</packaging> <build> <finalName>hello-portlet</finalName> </build> <dependencies> <dependency> <groupId>javax.portlet</groupId> <artifactId>portlet-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> </dependencies> </project>
Edit
web.xml
:<web-app> <display-name>hello-portlet</display-name> </web-app>
Edit
portlet.xml
:<portlet-app version="2.0" xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"> <portlet> <portlet-name>Hello</portlet-name> <portlet-class>com.acme.samples.HelloPortlet</portlet-class> <supports> <mime-type>text/html</mime-type> </supports> <portlet-info> <title>Contextual properties</title> </portlet-info> <supported-public-render-parameter>navigation_uri</supported-public-render-parameter> <supported-public-render-parameter>page_name</supported-public-render-parameter> <supported-public-render-parameter>site_type</supported-public-render-parameter> <supported-public-render-parameter>site_name</supported-public-render-parameter> <supported-public-render-parameter>window_width</supported-public-render-parameter> <supported-public-render-parameter>window_height</supported-public-render-parameter> <supported-public-render-parameter>window_show_info_bar</supported-public-render-parameter> <supported-public-render-parameter>current_time</supported-public-render-parameter> </portlet> <public-render-parameter> <identifier>navigation_uri</identifier> <qname xmlns:prp='http://www.gatein.org/xml/ns/prp_1_0'>prp:navigation_uri</qname> </public-render-parameter> <public-render-parameter> <identifier>page_name</identifier> <qname xmlns:prp='http://www.gatein.org/xml/ns/prp_1_0'>prp:page_name</qname> </public-render-parameter> <public-render-parameter> <identifier>site_type</identifier> <qname xmlns:prp='http://www.gatein.org/xml/ns/prp_1_0'>prp:site_type</qname> </public-render-parameter> <public-render-parameter> <identifier>site_name</identifier> <qname xmlns:prp='http://www.gatein.org/xml/ns/prp_1_0'>prp:site_name</qname> </public-render-parameter> <public-render-parameter> <identifier>window_width</identifier> <qname xmlns:prp='http://www.gatein.org/xml/ns/prp_1_0'>prp:window_width</qname> </public-render-parameter> <public-render-parameter> <identifier>window_height</identifier> <qname xmlns:prp='http://www.gatein.org/xml/ns/prp_1_0'>prp:window_height</qname> </public-render-parameter> <public-render-parameter> <identifier>window_show_info_bar</identifier> <qname xmlns:prp='http://www.gatein.org/xml/ns/prp_1_0'>prp:window_show_info_bar</qname> </public-render-parameter> <public-render-parameter> <identifier>current_time</identifier> <qname xmlns:prp='http://www.gatein.org/xml/ns/prp_1_0'>prp:current_time</qname> </public-render-parameter> </portlet-app>
Edit
HelloPortlet.java
by simply dispatching requests toview.jsp
:package com.acme.samples; import java.io.IOException; import java.util.Date; import java.io.PrintWriter; import javax.portlet.GenericPortlet; import javax.portlet.PortletRequestDispatcher; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import javax.portlet.PortletException; import javax.portlet.ActionRequest; import javax.portlet.ActionResponse; public class HelloPortlet extends GenericPortlet { @Override public void doView(RenderRequest request, RenderResponse response) throws IOException, PortletException { PortletRequestDispatcher dispatcher = getPortletContext().getRequestDispatcher("/jsp/view.jsp"); dispatcher.include(request, response); } }
Edit
view.jsp
:<% String navigation_uri = request.getParameter("navigation_uri"); String page_name = request.getParameter("page_name"); String site_type = request.getParameter("site_type"); String site_name = request.getParameter("site_name"); String window_width = request.getParameter("window_width"); String window_height = request.getParameter("window_height"); String window_show_info_bar = request.getParameter("window_show_info_bar"); String current_time = request.getParameter("current_time"); %> <style> #contextual_properties td:last-child {font-style: italic} #contextual_properties tr, td {padding: 5px} </style> <table border="1" id="contextual_properties" style="width: auto; border-spacing: 5px"> <tr><td>navigation_uri</td><td><%=navigation_uri%></td></tr> <tr><td>page_name</td><td><%=page_name%></td></tr> <tr><td>site_type</td><td><%=site_type%></td></tr> <tr><td>site_name</td><td><%=site_name%></td></tr> <tr><td>window_width</td><td><%=window_width%></td></tr> <tr><td>window_height</td><td><%=window_height%></td></tr> <tr><td>window_show_info_bar</td><td><%=window_show_info_bar%></td></tr> <tr><td>current_time</td><td><%=current_time%></td></tr> </table>
After deployment, add the portlet to a page and test:
The properties window_width
and window_height
are the size of
the portlet instance. You can change these parameters, and
window_show_info_bar
as well, in Portlet Setting menu (by clicking
Edit –> PageEdit –> Layout).
Juzu portlet¶
The source code used in this tutorial is here.
Juzu framework offers the following features to ease portlet development:
Be able to develop your portlet like a standalone application, and simply use the @Portlet annotation to make it a portlet.
Live mode: no need to re-deploy your application, because changes are applied when you save your files.
Templating: use Groovy, type safe parameters, template validation at compilation.
Dependency injection - JSR-330 (CDI, Spring, Guice).
Modular architecture with plugins.
References
Juzu documentation: http://juzuweb.org - learn directly from this site where Juzu team will update tutorials, references and Javadocs.
Juzu source code: https://github.com/juzu/juzu.
This tutorial focuses on Juzu portlet deployment in eXo Platform.
The dependencies are different for each server and each dependency injection implementation, so the project will use different Maven build profiles for packaging in each case:
Use
mvn clean package -Pplf-tomcat-guice
to build a package for Tomcat using Guice.Use
mvn clean package -Pplf-jboss-guice
to build a package forUse
mvn clean package -Pplf-tomcat-spring
to build a package for Tomcat using Spring.
Note
Currently, only Guice and Spring are covered in this tutorial. The other implementation, Weld, will be documented later.
Create a Maven project as follows:
Note
You can use the following archetype to generate project but make sure you will modify every single file in accordance with this tutorial.
*mvn archetype:generate -DarchetypeGroupId=org.juzu
-DarchetypeArtifactId=juzu-archetype -DarchetypeVersion=1.0.0
-DgroupId=org.exoplatform.samples -DartifactId=hellojz
-Dversion=5.1.x*
Edit
Controller.java
:package org.exoplatform.samples; import juzu.Path; import juzu.View; import juzu.Response; import juzu.template.Template; import javax.inject.Inject; import java.io.IOException; public class Controller { @Inject @Path("index.gtmpl") Template index; @View public Response.Content index() throws IOException { return index.ok(); } }
Edit
package-info.java
:@juzu.Application @juzu.plugin.servlet.Servlet(value = "/") package org.exoplatform.samples;
Edit
index.gtmpl
:Hello World
Edit
jboss-deployment-structure.xml
:<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.2"> <deployment> <dependencies> <module name="deployment.platform.ear" export="true"/> </dependencies> </deployment> </jboss-deployment-structure>
Edit
portlet.xml
:<?xml version="1.0" encoding="UTF-8"?> <portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" version="2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd"> <portlet> <portlet-name>SampleApplication</portlet-name> <display-name xml:lang="EN">Juzu Sample Application</display-name> <portlet-class>juzu.bridge.portlet.JuzuPortlet</portlet-class> <init-param> <name>juzu.app_name</name> <value>org.exoplatform.samples</value> </init-param> <supports> <mime-type>text/html</mime-type> </supports> <portlet-info> <title>Sample Application</title> </portlet-info> </portlet> </portlet-app>
Edit
web-guice.xml
:<?xml version="1.0" encoding="ISO-8859-1" ?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <!-- Run mode: prod, dev or live --> <context-param> <param-name>juzu.run_mode</param-name> <param-value>${juzu.run_mode:dev}</param-value> </context-param> <!-- Injection container to use: guice, spring, cdi or weld --> <context-param> <param-name>juzu.inject</param-name> <param-value>guice</param-value> </context-param> </web-app>
Edit
web-spring.xml
:<?xml version="1.0" encoding="ISO-8859-1" ?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <!-- Run mode: prod, dev or live --> <context-param> <param-name>juzu.run_mode</param-name> <param-value>${juzu.run_mode:dev}</param-value> </context-param> <!-- Injection container to use: guice, spring, cdi or weld --> <context-param> <param-name>juzu.inject</param-name> <param-value>spring</param-value> </context-param> </web-app>
Edit
pom.xml
:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.exoplatform.samples</groupId> <artifactId>hellojz</artifactId> <version>4.2.x</version> <packaging>war</packaging> <name>Juzu Application</name> <properties> <maven.compiler.target>1.6</maven.compiler.target> <maven.compiler.source>1.6</maven.compiler.source> </properties> <dependencies> <dependency> <groupId>org.juzu</groupId> <artifactId>juzu-core</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.juzu</groupId> <artifactId>juzu-plugins-servlet</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> </dependency> </dependencies> <build> <finalName>hellojz</finalName> </build> <profiles> <profile> <id>plf-tomcat-guice</id> <activation> <activeByDefault>true</activeByDefault> <property> <name>target</name> <value>plf-tomcat-guice</value> </property> </activation> <dependencies> <dependency> <groupId>com.google.inject</groupId> <artifactId>guice</artifactId> <version>3.0</version> </dependency> </dependencies> <properties> <maven.war.webxml>src/main/web-guice.xml</maven.war.webxml> </properties> <build> <plugins> <plugin> <artifactId>maven-war-plugin</artifactId> <version>2.6</version> <configuration> <packagingExcludes> WEB-INF/jboss-deployment-structure.xml, WEB-INF/lib/*.jar </packagingExcludes> </configuration> </plugin> </plugins> </build> </profile> <profile> <id>plf-jboss-guice</id> <activation> <property> <name>target</name> <value>plf-jboss-guice</value> </property> </activation> <dependencies> <dependency> <groupId>com.google.inject</groupId> <artifactId>guice</artifactId> <version>3.0</version> </dependency> </dependencies> <properties> <maven.war.webxml>src/main/web-guice.xml</maven.war.webxml> </properties> </profile> <profile> <id>plf-tomcat-spring</id> <activation> <property> <name>target</name> <value>plf-tomcat-spring</value> </property> </activation> <dependencies> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <scope>runtime</scope> <version>2.5.5</version> </dependency> </dependencies> <properties> <maven.war.webxml>src/main/web-spring.xml</maven.war.webxml> </properties> <build> <plugins> <plugin> <artifactId>maven-war-plugin</artifactId> <version>2.6</version> <configuration> <packagingExcludes> WEB-INF/jboss-deployment-structure.xml </packagingExcludes> </configuration> </plugin> </plugins> </build> </profile> <profile> <id>plf-jboss-spring</id> <activation> <property> <name>target</name> <value>plf-jboss-spring</value> </property> </activation> <dependencies> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <scope>runtime</scope> <version>2.5.5</version> </dependency> </dependencies> <properties> <maven.war.webxml>src/main/web-spring.xml</maven.war.webxml> </properties> </profile> </profiles> </project>
Here are some remarks:
Dependencies
Juzu:
<dependency>
<groupId>org.juzu</groupId>
<artifactId>juzu-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.juzu</groupId>
<artifactId>juzu-plugins-servlet</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
</dependency>
Guice:
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
</dependency>
Spring:
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>2.5.5</version>
</dependency>
Note that you can deploy this portlet using Guice in Tomcat or using Spring in Tomcat as usual.
Spring MVC portlet¶
Spring MVC portlet is officially supported as of eXo Platform 4.2.
This tutorial shows you how to write a basic Spring portlet. Please visit chapter Portlet, Spring documentation for your further reading. Besides, you can download all source code used in this tutorial here.
If you are already familiar with Spring portlet and just want to know how to deploy it in eXo Platform, skip this section and go to Portlet deployment section.
Create a Maven project as follows:
Edit
pom.xml
:<project> <modelVersion>4.0.0</modelVersion> <groupId>org.exoplatform.samples</groupId> <artifactId>sample-spring-mvc-portlet</artifactId> <version>4.2.x</version> <packaging>war</packaging> <dependencies> <dependency> <groupId>javax.portlet</groupId> <artifactId>portlet-api</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc-portlet</artifactId> <version>4.0.4.RELEASE</version> <!-- <version>2.5.5</version> --> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies> <build> <finalName>spring-mvc-portlet</finalName> </build> </project> - Though the Spring version you see here is 4.0.4.RELEASE, it should work in older versions too. This example was tested against Spring 2.5.5 and Spring 4.0.4.RELEASE.
Edit
Contact.java
. This class is the data model.package org.exoplatform.samples.spring; public class Contact { private String firstName; private String lastName; private String displayName; private String email; public Contact(String firstName, String lastName, String displayName, String email) { this.firstName = firstName; this.lastName = lastName; this.displayName = displayName; this.email = email; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
Edit
ContactService.java
. This interface has only one method to get a list of contacts:package org.exoplatform.samples.spring; import java.util.Set; public interface ContactService { public Set getContacts(); }
Edit
ContactServiceImpl.java
. This class implements ContactService and provides a method to create some data for testing. For simplicity, the data is in-memory.package org.exoplatform.samples.spring; import java.util.Set; import java.util.LinkedHashSet; import org.exoplatform.samples.spring.Contact; public class ContactServiceImpl implements ContactService { private static Set contactList = new LinkedHashSet(); public Set getContacts() { if (contactList.size() == 0) { initContacts(); } return contactList; } public void initContacts() { contactList.add(new Contact("John", "Smith", "John Smith", "[email protected]")); contactList.add(new Contact("Mary", "Williams", "Mary Williams", "[email protected]")); contactList.add(new Contact("Jack", "Miller", "Jack Miller", "[email protected]")); contactList.add(new Contact("James", "Davis", "James Davis", "[email protected]")); } }
Edit
ContactController.java
.package org.exoplatform.samples.spring; import org.springframework.web.portlet.mvc.AbstractController; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import org.springframework.web.portlet.ModelAndView; import java.util.Set; public class ContactController extends AbstractController { private ContactService contactService; public void setContactService(ContactService contactService) { this.contactService = contactService; } @Override public ModelAndView handleRenderRequestInternal(RenderRequest request, RenderResponse response) { Set contacts = contactService.getContacts(); ModelAndView modelAndView = new ModelAndView("contactsView", "contacts", contacts); return modelAndView; } } - Here you extend Spring's AbstractController and override the method handleRenderRequestInternal. - This tutorial is limited in render phase. The super class has also the method handleActionRequestInternal that will be called in action phase.
Edit
portlet.xml
.<portlet-app xmlns="http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/portlet http://java.sun.com/xml/ns/portlet/portlet-app_2_0.xsd" version="2.0"> <portlet> <portlet-name>contact</portlet-name> <display-name>Contact</display-name> <portlet-class>org.springframework.web.portlet.DispatcherPortlet</portlet-class> <supports> <mime-type>text/html</mime-type> <portlet-mode>view</portlet-mode> </supports> <portlet-info> <title>Contact</title> </portlet-info> </portlet> </portlet-app>
All Spring portlets have portlet-class DispatcherPortlet that dispatches requests to controllers.
Each instance of DispatcherPortlet has its own WebApplicationContext that inherits all the beans already defined in the Root WebApplicationContext.
Each one also has its portlet-scope beans which are created during its initialization. Those beans are defined in a file named
{portlet-name}-portlet.xml
(that is,contact-portlet.xml
in next step).
Edit
contact-portlet.xml
.<beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" 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-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"> <bean id="contactController" class="org.exoplatform.samples.spring.ContactController"> <property name="contactService" ref="contactService" /> </bean> <bean id="portletModeHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeHandlerMapping"> <property name="portletModeMap"> <map> <entry key="view" value-ref="contactController" /> </map> </property> </bean> </beans> Here you define some portlet-scoped beans: a controller and a handler mapping. The portlet-scoped bean definition overrides any bean with the same name defined at global scope. - The class ContactController you wrote is declared as a bean and is responsible for handling the view mode. - Such beans as view resolver or services should be defined at the application context, so you do not have to define them for each portlet.
Edit
web.xml
.<web-app version="2.5" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <display-name>spring-mvc-portlet</display-name> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>ViewRendererServlet</servlet-name> <servlet-class>org.springframework.web.servlet.ViewRendererServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ViewRendererServlet</servlet-name> <url-pattern>/WEB-INF/servlet/view</url-pattern> </servlet-mapping> </web-app> - The ViewRendererServlet brings all the view rendering capabilities that exist in the Spring servlet framework to the portlet. - Here you add a parameter, ``contextConfigLocation``, to customize the initialization of *DispatcherPortlet*. The goal is to define some beans at the application scope.
Edit
applicationContext.xml
.<beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" 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-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"> <bean id="contactService" class="org.exoplatform.samples.spring.ContactServiceImpl" /> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> </beans> Here you define the service bean that will be consumed by the controller, and a view resolver. - The JstlView is configured to resolve ``/WEB-INF/jsp/*.jsp`` files.
Edit
contactsView.jsp
.<%@taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %> <table border = "1"> <tr> <th style="text-align:left">Name</th> <th style="text-align:left">Email</th> </tr> <c:forEach items = "${contacts}" var ="contact"> <tr> <td>${contact.displayName}</td> <td>${contact.email}</td> </tr> </c:forEach> </table>
Now, you can deploy the portlet in eXo Platform and test:
Vue.js Portlet¶
eXo Platform 6.0 uses Vue.js version 2.6 in multiple Portlet applications. This guide describes how to build a Vue.js application.
Download the complete example from here.
Change project artifacts defined in
pom.xml
<groupId>com.acme.samples</groupId>
<artifactId>vue-webpack-sample</artifactId>
Change WAR name
vue-webpack-sample
defined inpom.xml
,src/main/webapp/META-INF/exo-conf/configuration.xml
,src/main/webapp/WEB-INF/web.xml
,webpack.dev.js
,webpack.prod.js
andsrc/main/webapp/WEB-INF/conf/custom-extension/portal/portal/intranet/pages.xml
<finalName>vue-webpack-sample</finalName>
<object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependencies">
<field name="dependencies">
<collection type="java.util.ArrayList">
<value>
<string>vue-webpack-sample</string>
</value>
</collection>
</field>
</object>
<display-name>vue-webpack-sample</display-name>
const app = 'vue-webpack-sample';
const app = 'vue-webpack-sample';
<portlet-application>
<portlet>
<application-ref>vue-webpack-sample</application-ref>
<portlet-ref>vueWebpackSample</portlet-ref>
</portlet>
<title>Vue Webpack Sample</title>
<access-permissions>*:/platform/users</access-permissions>
<show-info-bar>false</show-info-bar>
</portlet-application>
Change id of application
vue_webpack_sample
defined insrc/main/webapp/index.html
,src/main/webapp/css/main.less
,src/main/webapp/vue-app/components/app.vue
andsrc/main/webapp/vue-app/main.js
:
<div id="vue_webpack_sample"></div>
<template>
<div id="vue_webpack_sample">
<span>{{ $t('sample.i18n.label') }}</span>
</div>
</template>
$mount('#vue_webpack_sample')
#vue_webpack_sample {
display: flex;
background: white;
span {
color: red;
margin: auto;
}
}
Change portlet application name defined in
src/main/webapp/portlet.xml
,src/main/webapp/gatein-resources.xml
andsrc/main/webapp/WEB-INF/conf/custom-extension/portal/portal/intranet/pages.xml
<portlet>
<portlet-name>vueWebpackSample</portlet-name>
<portlet-class>org.exoplatform.commons.api.portlet.GenericDispatchedViewPortlet</portlet-class>
<init-param>
<name>portlet-view-dispatched-file-path</name>
<value>/index.html</value>
</init-param>
<supports>
<mime-type>text/html</mime-type>
</supports>
<portlet-info>
<title>Vue Webpack Sample</title>
</portlet-info>
</portlet>
<portlet>
<name>vueWebpackSample</name>
<module>
<script>
<minify>false</minify>
<path>/js/sample.bundle.js</path>
</script>
<depends>
<module>vue</module>
</depends>
<depends>
<module>eXoVueI18n</module>
</depends>
</module>
</portlet>
<portlet-application>
<portlet>
<application-ref>vue-webpack-sample</application-ref>
<portlet-ref>vueWebpackSample</portlet-ref>
</portlet>
<title>Vue Webpack Sample</title>
<access-permissions>*:/platform/users</access-permissions>
<show-info-bar>false</show-info-bar>
</portlet-application>
Modify npm module name
sample
to use your custom project module name, defined inpackage.json
,webpack.common.js
andsrc/main/webapp/gatein-resources.xml
:
"name": "sample"
entry: {
sample: './src/main/webapp/vue-app/main.js'
},
<portal-skin>
<skin-name>Enterprise</skin-name>
<skin-module>customModuleSampleVuePortlet</skin-module>
<css-path>/css/sample.css</css-path>
<css-priority>11</css-priority>
</portal-skin>
<portlet>
<name>vueWebpackSample</name>
<module>
<script>
<minify>false</minify>
<path>/js/sample.bundle.js</path>
</script>
<depends>
<module>vue</module>
</depends>
<depends>
<module>eXoVueI18n</module>
</depends>
</module>
</portlet>
Rename Resource bundle file
src/main/resources/locale/addon/Sample_en.properties
and change its configuration insrc/main/webapp/WEB-INF/conf/custom-extension/bundle-configuration.xml
<external-component-plugins>
<target-component>org.exoplatform.services.resources.ResourceBundleService</target-component>
<component-plugin>
<name>Vue Sample Portlet Resource Bundle</name>
<set-method>addResourceBundle</set-method>
<type>org.exoplatform.services.resources.impl.BaseResourceBundlePlugin</type>
<init-params>
<values-param>
<name>classpath.resources</name>
<value>locale.addon.Sample</value>
</values-param>
<values-param>
<name>portal.resource.names</name>
<value>locale.addon.Sample</value>
</values-param>
</init-params>
</component-plugin>
</external-component-plugins>
Change name of page where you want to put your application in
src/main/webapp/WEB-INF/conf/custom-extension/portal/portal/intranet/navigation.xml
andsrc/main/webapp/WEB-INF/conf/custom-extension/portal/portal/intranet/pages.xml
<page>
<name>vueSampleAppPage</name>
<title>Sample App</title>
<access-permissions>*:/platform/users</access-permissions>
<edit-permission>*:/platform/administrators</edit-permission>
<container template="system:/groovy/portal/webui/container/UIContainer.gtmpl">
Run
mvn clean install
Stop server and copy WAR file from
target/
folder intowebapps
folder inside eXo Platform installation.Run server
Open page
http://localhost:8080/portal/intranet/PAGE_NAME
Note
PAGE_NAME
= vueSampleApp
by default. The page name is renamed in step 7.
inside navigation.xml
file.
Vuetify and Vue.js Portlet¶
eXo Platform 6.0 uses Vuetify version 2.2 in multiple Portlet applications. This guide describes how to build an application based on Vuetify 2.2 and Vue.js 2.6.
You can download the complete example from here and apply the same steps described in Develop Vue.js Portlet.
In addition to requirements of Vue.js application, inside a Vuetify application some additional configurations are added:
Add
vuetify
JS module as dependency of your custom JS module insrc/main/webapp/WEB-INF/gatein-resources.xml
:
<portlet>
<name>PORTLET_NAME</name> <!-- Portlet name, defined in portlet.xml -->
<module>
<script>
<minify>false</minify>
<path>PORTLET_MODULE_FILE_PATH</path> <!-- For example: /js/sample.bundle.js -->
</script>
<depends>
<module>vue</module>
</depends>
<depends>
<module>vuetify</module> <!-- Vuetify dependency injected in this definition file instead of using 'npm import' -->
</depends>
<depends>
<module>eXoVueI18n</module>
</depends>
</module>
</portlet>
Add a parent DOM element with class
VuetifyApp
to definesrc/main/webapp/index.html
:
<div class="VuetifyApp">
<div id="vuetify_webpack_sample"></div>
</div>
Developing a gadget¶
Gadgets are basically simple applications written in JavaScript and can be imported as windows. They are also considered as independent HTML content, so their UI, including layout, font or color, may be different. That is why you need to make consistent in the look and feel of all gadgets in eXo Platform after creating any new gadgets. One of significant advantages when developing gadgets is the fact that Google provides a standard OpenSocial, which is an API for gadgets to interact with Social network platforms. This section will instruct you to:
Create a very simple gadget in eXo Platform.
Create resources and apply them into a gadget.
Use AJAX and HTML DOM Object for a gadget.
Customize a gadget, including: changing a gadget’s category, resizing, changing thumbnail and setting preferences for a gadget.
To get more information on how to develop gadgets, see Managing Google Gadget, eXo Add-ons guide.
Creating a gadget¶
Creating a gadget is very simple. To create a gadget through a Webapp, you need to create a sample bundle where you will add and deploy your gadget. This procedure walks you through steps to create a very simple gadget called Hello World.
The source code is provided here.
Note
Unlike portlet, the gadget webapp is always required to be a portal extension.
As of 4.3, this can be done simply by adding a META-INF/exo-conf/configuration.xml
file.
The META-INF/exo-conf/configuration.xml
file should has the content
below. Unless you use a traditional custom-extension-config.jar
,
always add this file to your gadget webapp, though it might not be
repeated in later tutorials.
<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"
xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd">
<external-component-plugins>
<target-component>org.exoplatform.container.definition.PortalContainerConfig</target-component>
<component-plugin>
<name>Add PortalContainer Definitions</name>
<set-method>registerChangePlugin</set-method>
<type>org.exoplatform.container.definition.PortalContainerDefinitionChangePlugin</type>
<priority>101</priority>
<init-params>
<values-param>
<name>apply.specific</name>
<value>portal</value>
</values-param>
<object-param>
<name>addDependencies</name>
<object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependencies">
<field name="dependencies">
<collection type="java.util.ArrayList">
<value>
<!-- CHANGE THIS ACCORDINGLY TO YOUR WEBAPPS -->
<string>hello-gadget</string>
</value>
</collection>
</field>
</object>
</object-param>
</init-params>
</component-plugin>
</external-component-plugins>
</configuration>
Here are the steps to create your first gadget:
Create a Maven project with the following
pom.xml
file. See the project structure in the source link given above.<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>sample</groupId> <artifactId>gadget</artifactId> <packaging>war</packaging> <version>1.0</version> <name>Hello Gadget sample</name> <build> <finalName>hello-gadget</finalName> </build> </project>
Edit
web.xml
file:<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" metadata-complete="true" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <display-name>hello-gadget</display-name> </web-app>
Edit
webapp/gadgets/HelloGadget/HelloGadget.xml
file:<?xml version="1.0" encoding="UTF-8"?> <Module> <ModulePrefs author="eXoPlatform" title="Hello World" directory_title="Hello World" description="The simplest gadget"> </ModulePrefs> <Content type="html"> <![CDATA[ <div class='hello'> <h2>Hello</h2> <h6>Welcome to Hello World gadget!</h6> <p><i>Powered by eXo Platform.</i></p> </div> ]]> </Content> </Module>
Edit
webapp/WEB-INF/gadget.xml
file:<gadgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gatein.org/xml/ns/gatein_objects_1_0 http://www.gatein.org/xml/ns/gadgets_1_0" xmlns="http://www.gatein.org/xml/ns/gadgets_1_0"> <gadget name="HelloGadget"> <path>/gadgets/HelloGadget/HelloGadget.xml</path> </gadget> </gadgets>
Include
WEB-INF/jboss-deployment-structure.xml
file if the gadget will be deployed in JBoss:<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.2"> <deployment> <dependencies> <module name="deployment.platform.ear" export="true"/> </dependencies> </deployment> </jboss-deployment-structure>
Build the project with the command:
mvn clean install
.Install
hello-gadget.war
to:$PLATFORM_TOMCAT_HOME/webapps/
.
Note
See details about deployment in portal extension section, especially if you use the traditional extension with jar to be backward compatible.
Start the server and go to UI to do the next steps.
Select Administration –> Applications, then add this gadget into a
category:
Click My Dashboard (the menu drops down from the user name in the Topbar) to add the gadget. The result:
Note
You can also create and edit a gadget completely via UI.
Creating resources¶
To achieve the consistent look and feel, you have to collect the common
features of all gadgets as much as possible and put in a place where it
can be shared for all gadgets. You will use exo-gadget-resources for
this purpose. It is a .war
file that contains commonly used static
resources (stylesheets, images, JavaScript libraries, and more)
available for all gadgets in eXo Platform at runtime:
/exo-gadget-resources
|__skin
| |__exo-gadget
| | |__images
| | |__gadget-common.css
| |__...(3rd-party components' CSS)
|__script
|__jquery
| |__3.2.1
| |__...(other jQuery versions)
| |__plugins
|__utils
The resources are divided into 2 categories: skin and script.
Skin
This is the place for the shared stylesheets of the gadgets
(exo-gadget/gadget-common.css
) and other third-party components
styles adapted to the eXo Platform skin (jqPlot, Flexigrid, and more).
This is a copy of the component’s original CSS with some modifications
to match the eXo Platform’s skin. You can find this original file at the
component’s folder under exo-gadget-resources/script
then link to it
or make your own copy (put it in your gadget’s folder and refer to it in
gadget’s .xml file) to suit your need.
The gadget-common.css
file is the main place for the global
stylesheets. When the eXo Platformskin is changed, updating stylesheets
in this file will affect all gadgets skins accordingly. In this file,
you will define stylesheets applied for all gadgets, such as gadget
title, gadget body, fonts, colors, tables, and some commonly used icons,
such as drop-down arrow, list bullet, setting button, and more.
Script
This is the place for commonly used third-party JavaScript libraries (e.g: jQuery and its plugins) and a library of useful utility scripts (the utils folder).
jQuery and plugins:
jquery/<version>: jQuery JavaScript library.
jquery/plugins/jqplot: Charts and Graphs for jQuery.
jquery/plugins/flexigrid: Lightweight but rich data grid.
jquery/plugins/date.js: JavaScript date library.
jquery/plugins/jquery.timers: JavaScript timer.
Note
Here you should keep the latest and several versions of jQuery because some plugins may not work with the latest version. Several versions of a plugin are also kept.
The utilities scripts:
utils/pretty.date.js
: Calculate the difference from a point of time in the past to the present and display “4 months 3 weeks ago”, for example.
Applying resources in a gadget¶
A gadget should use static resources available in
exo-gadget-resources
instead of including them in their own package.
This helps reduce packaging size, avoid duplication (considering every
gadget uses the same jQuery version, instead of adding each jQuery in
its own package) and take advantages of automatic skin changing/updating
when exo-gadget-resources
is updated.
A sample of applying resources into a gadget is to use a JavaScript. In this example, you will add a button to the Hello World gadget and use jQuery to register an event for the button. When you click the “here” button, the color of welcome message will be changed. The source code of this example is here.
Edit
HelloGadget.xml
file:
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs author="eXoPlatform"
title="Hello World"
directory_title="Hello World"
description="The simplest gadget">
</ModulePrefs>
<Content type="html">
<![CDATA[
<script src="/exo-gadget-resources/script/jquery/3.2.1/jquery.min.js"></script>
<script type="text/javascript">
$("body").live("click", ".hello .btn", function() {
$(".hello h6").css("color", "green");
});
</script>
<div class='hello'>
<h2>Hello</h2>
<h6>Welcome to Hello World gadget!</h6>
<p>Click <a class='btn'>here</a> to change the default color of the welcome message.
<p><i>Powered by eXo Platform.</i></p>
</div>
]]>
</Content>
</Module>
The **Hello World** gadget is now displayed:
|image28|
Add the CSS resources of eXo Platform to make consistent between Hello World gadget and look and feel of eXo Platform which is based on Twitter Bootstrap. To do that, edit
HelloGadget.xml
to add this:<link rel="stylesheet" type="text/css" href="/eXoResources/skin/bootstrap/css/bootstrap.css" />
The look and feel of Hello World gadget is now changed:
Using AJAX and HTML DOM Object¶
AJAX allows your gadget to be updated asynchronously by exchanging small amounts of data with the server behind the scenes. Besides, with the support of HTML DOM Object, the gadget’s content can be created dynamically based on AJAX’s response data. This section instructs you how to leverage these utilities to customize your gadget.
You can get the source code of this example here.
Create a new Maven project with the following structure:
Edit
pom.xml
file:<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchemainstance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>sample</groupId> <artifactId>gadget</artifactId> <packaging>war</packaging> <version>1.0</version> <name>Auto slideshow gadget sample</name> <build> <finalName>auto-slideshow</finalName> </build> </project>
Edit
web.xml
file:<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" metadata-complete="true" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <display-name>auto-slideshow</display-name> </web-app>
Edit
AutoSlideshowGadget.xml
file:<?xml version="1.0" encoding="UTF-8" ?> <Module> <ModulePrefs title="Memories!" width="240" height="200"/> <Content type="html"> <![CDATA[ <!--Gadget's main body which will be added by HTML DOM Object later--> <div id="slideshow"> </div> ]]> </Content> </Module> in which you name the gadget as **Memories!**. The main body consists of a div tag only. This file will be updated later.
Download http://code.jquery.com/jquery-3.2.1.js and save it in
webapp/gadgets/AutoSlideshowGadget/
.Edit
style.css
file:img {width: 240px; } #slideshow { margin: 10px auto 10px; position: relative; width: 240px; height: 200px; padding: 10px; box-shadow: 0 0 20px rgba(0,0,0,0.4); } #slideshow > div { position: absolute; top: 10px; left: 10px; right: 10px; bottom: 10px; }
Edit
myscript.js
file. This script contains a function using AJAX to call DriverConnector API (see here <rest-api/content/DriverConnector.getFoldersAndFiles> for more details) to get all public images of root user. Another function, the traverseXMLDoc function will be added in next steps.function getImages() { //This function uses AJAX to send GET request to server's DriverConnector API and receive XML response jQuery.ajax({ type: "GET", url: "/portal/rest/wcmDriver/getFoldersAndFiles?driverName=Collaboration¤tFolder=Users/r___/ro___/roo___/root/Public¤tPortal=intranet&repositoryName=repository&workspaceName=collaboration&filterBy=Image", contentType: "application/xml; charset=utf-8", dataType: "xml", success: function (data, status, jqXHR) { var strResults=new XMLSerializer().serializeToString(data.documentElement); //build dynamic html content for "slideshow" div tag traverseXMLDoc(strResults, "slideshow"); }, //error report error: function (jqXHR, status) { //error handler alert("Cannot retrieve data!"); } }); }
Note
Notice the url
parameter here is pointing to Public folder
of root user to retrieve image files. Therefore, in later steps,
it requires logging in as root user to upload images before anyone
can use this gadget.
Add the traverseXMLDoc function with 2 input parameters to this script as follows:
function traverseXMLDoc(xmlDoc, idOfContainerDomElement){ //This function traverses through the whole XML response returned from server var $xmlDocObjChildren, $contentDiv; $contentDiv = $('#' + idOfContainerDomElement); if ($contentDiv.length === 0) { throw new Error('There are no DOM elements with this id: "' + idOfContainerDomElement + '"'); } //Information of each image object is contained in "File" tag $xmlDocObjChildren = $(xmlDoc).find("File"); $xmlDocObjChildren.each(function(index, Element) { var $currentObject = $(this), childElementCount = Element.childElementCount, //Image's url is contained in "url" attribute currentNodeType = $currentObject.attr('url'); //Adding dynamic content into gadget's body $contentDiv.append('<div><img src="'+currentNodeType+'"></div>'); }); }
Add some image effects by JavaScript and include all your created resources to the
AutoSlideshowGadget.xml
file:<?xml version="1.0" encoding="UTF-8" ?> <Module> <ModulePrefs title="Memories!" width="240" height="200"/> <Content type="html"> <![CDATA[ <script src="jquery-3.2.1.js"></script> <script src="myscript.js"></script> <link rel="stylesheet" type="text/css" href="style.css" /> <!--Gadget's main body which will be added by HTML DOM Object later--> <div id="slideshow"> </div> <!--Start calling js function--> <script type="text/javascript"> getImages(); //Creating gagdet's effects $("#slideshow > div:gt(0)").hide(); setInterval(function() { $('#slideshow > div:first') .fadeOut(1000) .next() .fadeIn(1000) .end() .appendTo('#slideshow'); }, 3000); </script> ]]> </Content> </Module> Notice that you can follow :ref:`this section <PLFDevGuide.DevelopingApplications.DevelopingGadget.CreatingResources>` to create CSS and Javascript resources in separate files to share with other gadgets. In this guide, we make it simple by including these resources right inside the war package.
Edit
gadget.xml
file:
<gadgets xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gatein.org/xml/ns/gatein_objects_1_0 http://www.gatein.org/xml/ns/gadgets_1_0" xmlns="http://www.gatein.org/xml/ns/gadgets_1_0"> <gadget name="AutoSlideshowGadget"> <path>/gadgets/AutoSlideshowGadget/AutoSlideshowGadget.xml</path> </gadget> </gadgets>
Deploy the gadget and add it to Root’s user dashboard. If necessary,
see Creating a gadget <PLFDevGuide.DevelopingApplications.DevelopingGadget.CreatingGadget> for how to.
Log in as Root, upload several images into Documents –> Public.
The result is as below:
Customizing a gadget¶
You can customize gadgets in some aspects:
Changing the category of a gadget¶
There are 2 ways to change the category of a gadget:
Via UI as described in Adding a portlet/gadget to the Application list. However, this works only one time for a new gadget. After you have added it to a category, you could not change its category via UI.
By configuring
ApplicationCategoriesPlugins
, as detailed below.
Here is the configuration example to add Hello World gadget to the My Gadgets category.
Add the following configuration to WEB-INF/conf/configuration.xml
file:
<external-component-plugins>
<target-component>org.exoplatform.application.registry.ApplicationRegistryService</target-component>
<component-plugin>
<name>new.portal.portlets.registry</name>
<set-method>initListener</set-method>
<type>org.exoplatform.application.registry.ApplicationCategoriesPlugins</type>
<description>this listener init the portlets are registered in PortletRegister</description>
<init-params>
<object-param>
<name>MyGadgets</name>
<description>description</description>
<object type="org.exoplatform.application.registry.ApplicationCategory">
<field name="name"><string>MyGadgets</string></field>
<field name="displayName"><string>My Gadgets</string></field>
<field name="description"><string>List of personal gadgets for development.</string></field>
<field name="accessPermissions">
<collection type="java.util.ArrayList" item-type="java.lang.String">
<value><string>*:/developers</string></value>
</collection>
</field>
<field name="applications">
<collection type="java.util.ArrayList">
<value>
<object type="org.exoplatform.application.registry.Application">
<field name="applicationName"><string>HelloGadget</string></field>
<field name="categoryName"><string>MyGadgets</string></field>
<field name="displayName"><string>Hello Gadget</string></field>
<field name="description"><string>The simplest gadget</string></field>
<field name="type"><string>gadget</string></field>
<field name="contentId"><string>HelloGadget</string></field>
<field name="accessPermissions">
<collection type="java.util.ArrayList" item-type="java.lang.String">
<value><string>*:/developers</string></value>
</collection>
</field>
</object>
</value>
</collection>
</field>
</object>
</object-param>
</init-params>
</component-plugin>
</external-component-plugins>
Deploy it to a fresh server. You will see the category is automatically added:
Note
This method only works for a fresh (empty data) server because the categories defined in the ApplicationRegistryService configuration are created all in once when you start PRODUCT for the first time, and when the database is empty. In case the server is started already, you may write a portlet for this job, as described below.
Create the category by using the following code:
PortalContainer container = PortalContainer.getInstance(); ApplicationRegistryService appService = (ApplicationRegistryService)container.getComponentInstanceOfType(ApplicationRegistryService.class); try { if (appService.getApplication("MyGadgets/HelloGadget") == null) { ApplicationCategory cat = new ApplicationCategory(); cat.setName("MyGadgets"); cat.setDisplayName("My Gadgets"); cat.setDescription("List of personal gadgets for development"); cat.setAccessPermissions(Arrays.asList("*:/developers")); Application app = appService.getApplication("Gadgets/HelloGadget"); appService.save(cat, app); } } catch (Exception e) { throw new RuntimeException(e); }
Add the above code block to one portlet action, for example:
@Override public void processAction(ActionRequest actionRquest, ActionResponse actionResponse) { PortalContainer container = PortalContainer.getInstance(); ApplicationRegistryService appService = (ApplicationRegistryService)container.getComponentInstanceOfType(ApplicationRegistryService.class); try { if (appService.getApplication("MyGadgets/HelloGadget") == null) { ApplicationCategory cat = new ApplicationCategory(); cat.setName("MyGadgets"); cat.setDisplayName("My Gadgets"); cat.setDescription("List of personal gadgets for development"); cat.setAccessPermissions(Arrays.asList("*:/developers")); Application app = appService.getApplication("Gadgets/HelloGadget"); appService.save(cat, app); } } catch (Exception e) { throw new RuntimeException(e); } } @RenderMode(name = "view") public void hello(RenderRequest request, RenderResponse response) throws IOException, PortletException { PrintWriter writer = response.getWriter(); PortletURL actionURL = response.createActionURL(); writer.append("<p>Click <a href='" + actionURL.toString() + "'>here</a> to create MyGadgets category</p>"); }
Deploy the portlet to which you have added the above code block, then do the portlet action.
Resizing a gadget¶
This part instructs you how to use the configuration to resize the
gadget by adding the height attribute (for example, height=”300”) to the
<ModulePrefs> tag in the HelloGadget.xml
file:
<ModulePrefs author="eXoPlatform"
title="Hello World"
directory_title="Hello World"
description="The simplest gadget"
height="300">
</ModulePrefs>
Note
You can also resize a gadget through the web console as described in Editing a gadget.
Changing a gadget thumbnail¶
The gadget thumbnails are displayed in the Page Editor window when you edit a page. The thumbnail image size needs to be consistent for all gadgets in the list. The current size (in eXo Platform) is 80 x 80 px, so you should select an image of this size in PNG (preferred), JPG or GIF format for your gadget thumbnail.
The image can also be one from a public website (absolute URL), for example,
thumbnail="http://www.example.com/images/HelloWorld-icon.jpg"/
or from an internal image (relative URL):thumbnail="image/HelloWorld-icon ``. Add that attribute to the <ModulePrefs> tag in the ``HelloGadget.xml
file.
Setting preferences¶
The Google OpenSocial specifications describe different features that can be used, but eXo Platform implements only some of them. See the code of the following files:
$PLATFORM_TOMCAT_HOME/lib/exo.portal.gadgets-core-{version}.jar!/gatein-features/gatein-container/Gadgets.js
.$PLATFORM_TOMCAT_HOME/webapps/eXoResources/javascript/eXo/gadget/UIGadget.js
.$PLATFORM_TOMCAT_HOME/lib/exo.portal.gadgets-core-{version}.jar!/gatein-features/gatein-container/ExoBasedUserPrefStore.js
.
The following is a simple procedure to set preferences for Hello World gadget. You can get the source here.
In HelloGadget.xml
file:
Enable the setprefs feature:
<ModulePrefs ... <Require feature="setprefs"/> </ModulePrefs>
Register one preference:
<UserPref name="welcome" display_name="Welcome message" default_value="Welcome to Hello World gadget!" required="true"/>
Use JavaScript to get the registered preference that changes Welcome message.
var getUserPrefs = function() { var prefs = new _IG_Prefs(__MODULE_ID__); var welcome_message = prefs.getString("welcome"); $(".hello .alert-info h6").text(welcome_message); }; gadgets.util.registerOnLoadHandler(getUserPrefs); In summary, the complete content of the ``HelloGadget.xml`` file will be: .. code:: xml <?xml version="1.0" encoding="UTF-8" ?> <Module> <ModulePrefs author="eXoPlatform" title="Hello World" directory_title="Hello World" description="The simplest gadget" height="300"> <Require feature="setprefs"/> </ModulePrefs> <UserPref name="welcome" display_name="Welcome message" default_value="Welcome to Hello World gadget!" required="true"/> <Content type="html"> <![CDATA[ <link rel="stylesheet" type="text/css" href="/eXoResources/skin/bootstrap/css/bootstrap.css" /> <script src="/exo-gadget-resources/script/jquery/1.6.2/jquery.min.js"></script> <script type="text/javascript"> $("body").live("click", ".hello .btn", function() { $(".hello h6").css("color", "green"); }); var getUserPrefs = function() { var prefs = new _IG_Prefs(__MODULE_ID__); var welcome_message = prefs.getString("welcome"); $(".hello .alert-info h6").text(welcome_message); }; gadgets.util.registerOnLoadHandler(getUserPrefs); </script> <div class='hello well'> <h2>Hello</h2> <div class='alert alert-info'> <h6></h6> </div> <p>Click <a class='btn btn-primary'>here</a> to change the default color of the welcome message. <p><i>Powered by eXo Platform.</i></p> </div> ]]> </Content> </Module>
The Hello World gadget now appears with a pencil icon that allows changing gadget preferences.
Extending eXo applications¶
Overriding application templates Steps to override the default template of a portlet in eXo Platform.
Extending HTML header element of pages Steps to extend http header elements of pages.
Applications Plugins Tutorials to add plugin to eXo applications, such as Activity extensions or action in Wiki using UI Extension framework.
Notification Tutorials to extend or customize the notification system in eXo Platform.
Overriding user profile design Steps to override your profile page in eXo Platform.
Wiki macro A tutorial to write new macros in eXo Wiki.
ExtensibleFilter mechanism A tutorial to create and insert your own filter into eXo Platform.
Overriding application templates¶
Groovy templates can be overriden thanks to extension mechanism. Here are steps to override a template of Organization portlet. The source code used in this section is provided here for downloading.
Take a look at
$PLATFORM_TOMCAT_HOME/webapps/eXoResources/groovy/organization/webui/component/UIOrganizationPortlet.gtmpl
. This file contains the template definitions of the Organization portlet.
Create a
UIOrganizationPortlet.gtmpl
file and put it incustom-extension.war!/groovy/organization/webui/component
.Copy the existing content from
UIOrganizationPortlet.gtmpl
of Step 1 into yourcustom-extension.war!/groovy/organization/webui/component/UIOrganizationPortlet.gtmpl
, then modify your file, for example change the color of text and background on the toolbar.Refresh the browser if you are running eXo Platform at the developer mode. You will see your modification take effect on the Organization portlet.
Note
If you are not running eXo Platform in the developer mode, you will have to restart the server.
Extending HTML header element of pages¶
In case, you need to define new elements in HTML <head>
element of pages, you can define a service plugin that will allow you to inject a groovy template content in <head>
element of the page.
Note
This assumes that you have defined your custom-extension.war
file using Extension mechanism.
Note
If you need to add:
A stylesheet file, please refer to :ref:`Skin service <#sect-Reference_Guide-Skinning_Portal-Skin_Service>`__
A javascript file, please refer to :ref:`JavaScript development <#sect-Reference_Guide-Javascript_Development>`__
Add a new file under
custom-extension.war/groovy/portal/webui/UICustomPortalApplicationHead.gtmpl
Add a plugin configuration that will inject your file inside
custom-extension.war/WEB-INF/conf/configuration.xml
:
<external-component-plugins> <target-component>org.exoplatform.groovyscript.text.TemplateService</target-component> <component-plugin> <name>UIPortalApplication-head</name> <set-method>addTemplateExtension</set-method> <type>org.exoplatform.groovyscript.text.TemplateExtensionPlugin</type> <init-params> <values-param> <name>templates</name> <description>The list of templates to include in HTML Page Header with UIPortalApplication.gtmpl</description> <value>war:/groovy/portal/webui/UICustomPortalApplicationHead.gtmpl</value> </values-param> </init-params> </component-plugin> </external-component-plugins>
Note
You can also add an html content at the end of body page using the same definition by using plugin name UIPortalApplication-End-Body
instead of UIPortalApplication-head
.
Applications Plugins¶
Here after are some tutorials to write eXo add-ons using the UI Extension framework:
An explanation of the base framework can be found at Platform Reference Guide - UI Extensions.
In general, writing a complete UI Extension involves filter (business logic or access permission), localization and CSS customization. You might read more about the subjects:
Extending Activity UI¶
The Activity Stream is extensible and allows to add features into it without overriding the Stream Application. In fact, even the default Stream uses the same mechanisms to inject additional behaviors, depending on installed addons. The extensible parts of the Activity are :
Activity UI: the activity body is extensible in two ways:
Activity Type
: this will allow you to define a specific UI for the whole Body content of the Activity By example, the news detail that is displayed in standalone mode (not integrated in the stream) redefine the whole content BodyActivity Body
: this type of extensions will allow you to append additional content to the Current Activity Body
Activity Menu items
: as made for existing menu items (Edit
,Delete
,Copy link
…), this type of extension allows to append an additional action in the Activity MenuActivity Footer Action buttons
: as made for all existing buttons (Like
,Share
,Comment
…), this type of extension allow to append an additional button in the Activity Footer part near the Activity ReactionsComment Body
: knowing that a comment is the same Entity as the Activity, the same extension mechanism, as forActivity Body extensions
, is used to extend comment body the same way that it’s done for the activity body.Comment Menu items
: The sameActivity Menu items
extension mechanism is used with a differentextensionRegistry
name and typeComment Footer Action buttons
: The sameActivity Footer Action buttons
extension mechanism is used with a differentextensionRegistry
name and type
How to define a UI for a specific Activity Type ?¶
You may need to add a ‘Poll’ Activity, a ‘Day Humor’ Activity type… By using this extension mechanism you will be able to define complex and feature complete Activity extensions.
A sample of an Activity type extension can be found here.
In this example, a new Activity Type (aCustomType
) is defined to display the activity content inside a colored box: green for activity and red for comments:
To register a new Activity Type extension, the extensionRegistry
is used with parameters activity
and type
:
extensionRegistry.registerExtension('activity', 'type', {
// Activity.type. 'default' corresponds to all activities which doesn't have a corresponding extension
type: 'aCustomType',
options: {
// Redefine the activity display by a custom one
getExtendedComponent: (activity, isActivityDetail) => ({ // The method receives the current activity and isActivityDetail to let you choose component to display switch activity properties
component: SampleActivityType, // Vue component to use for the display of component
overrideHeader: false, // Whether to hide the default header of the activity or not
overrideFooter: false, // Whether to hide the default footer of the activity that contains actions and reactions or not
overrideComments: false, // Whether to hide the list of comments preview or not
}),
// Whether to display edit button or not when user have permission
canEdit: (activity) => true,
// The content to display in Activity Editor (AKA Composer)
getBodyToEdit: (activity) => {
const templateParams = activity.templateParams;
return Vue.prototype.$utils.trim(window.decodeURIComponent(templateParams
&& templateParams.default_title
&& templateParams.default_title
|| activity.title
|| activity.body
|| ''));
},
// Whether the activity is shareable or not
canShare: (activity) => true,
},
});
Inside the Vue Component, you will automatically receive in the props
, the current activity to display.
Possible props
objects are :
props: {
activity: { // Activity or comment to display
type: Object,
default: null,
},
isActivityDetail: { // Activity is displayed in standalone mode or not (Single activity display or display of the activity in the stream)
type: Boolean,
default: null,
},
activityTypeExtension: { // The specific or generic registered Activity Type Extension (See adequate section defining Activity Types)
type: Boolean,
default: null,
},
},
How to extend Activity/Comment Body by new content ?¶
You may need to add an extra content in all or specific activities, like User Activity Rating UI, User Favorite, Number of User Views…
Activity Extension
A sample of an Activity body extension can be found here.
In this example, it will display an icon when the content of the activity contains the word favorite
.
To register a new Activity Body extension, the extensionRegistry
is used with parameters ActivityContent
and activity-content-extensions
:
extensionRegistry.registerComponent('ActivityContent', 'activity-content-extensions', {
id: 'hasFavoriteWordActivity', // Unique Identifier of the extension
isEnabled: (params) => { // Check whether the extension should be enabled or not
const activity = params && params.activity;
return activity
&& !activity.activityId // The activity isn't a comment
&& activity.title // The activity has a title
&& activity.title.toLowerCase().includes('favorite'); // The activity content has at least one occurrence of 'favorite' word
},
vueComponent: SampleActivityBodyExtension, // Vue component to use to display the extension
rank: 50, // Display order comparing to other body extensions
});
Inside the Vue Component, you will automatically receive in the props
, the current activity to display.
Possible props
objects are :
props: {
activity: { // Activity or comment to display
type: Object,
default: null,
},
isActivityDetail: { // Activity is displayed in standalone mode or not (Single activity display or display of the activity in the stream)
type: Boolean,
default: null,
},
activityTypeExtension: { // The specific or generic registered Activity Type Extension (See adequate section defining Activity Types)
type: Boolean,
default: null,
},
},
Comment Extension
A sample of a body extension can be found here.
In this example, it will display an icon when the content of the comment contains the word favorite
.
By default, the same body extensions of Activity is used for Comments.
In fact, the Data model retrieved through REST APIs is exactly the same except a property activityId
that can be found in a Comment and not in Activity.
So by testing on existence of this property, you will be able to distinguish an Activity from a Comment.
Thus, to register a new Comment Body extension, the extensionRegistry
is used with the same Activity Extension name ActivityContent
and activity-content-extensions
:
extensionRegistry.registerComponent('ActivityContent', 'activity-content-extensions', {
id: 'hasFavoriteWordComment', // Unique Identifier of the extension
isEnabled: (params) => { // Check whether the extension should be enabled or not
const activity = params && params.activity;
return activity
&& activity.activityId // The activity is a comment
&& activity.title // The comment has a title
&& activity.title.toLowerCase().includes('favorite'); // The comment content has at least one occurrence of 'favorite' word
},
vueComponent: SampleCommentBodyExtension, // Vue component to use to display the extension
rank: 50, // Display order comparing to other body extensions
});
Inside the Vue Component, you will automatically receive in the props
, the current activity to display.
Possible props
objects are :
props: {
activity: { // Activity or comment to display
type: Object,
default: null,
},
activityTypeExtension: { // The specific or generic registered Activity Type Extension (See adequate section defining Activity Types)
type: Boolean,
default: null,
},
},
How to redefine the UI of a predefined Activity Type ?¶
Each Activity has an associated type. By example:
A news activity is of type
news
(Activity type definition)A kudos activity is of type
exokudos:activity
(Activity type definition)A notes activity is of type
ks-wiki:spaces
(Activity type definition)A default activity (that could have attachments, mentions, link or youtube embedded video) doesn’t define a type (Default activity type definition)
To override, the notes Activity, by example:
Copy the JS content from here (link of file corresponding to Notes version 1.0.x = eXo Platform 6.2.x)
Inject the JS to the page using AMD
You can play with its content and see how it’s behaving.
Adding your own Content UI Extensions¶
There are many UI Components in Content Explorer and Administration that allows plugins:
Action bar
File viewer
Sidebar
Admin control panel
Context menu in the main working area
Creating an action extension¶
This section shows you how to write an action in PRODUCT. Specifically, a “ShowNodePath” button will be displayed in Sites Explorer. When clicking on it, the node path of the current node will be shown. You can download the source code used in this section here.
Create a Maven project which has the following directory structure:
pom.xml
: The project’s POM file.ShowNodePathActionComponent.java
: The simple action to view the node path.configuration.xml
: The configuration file to register your action with the org.exoplatform.webui.ext.UIExtensionManager service.
Here is content of the pom.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme</groupId>
<artifactId>action-example</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.gatein.portal</groupId>
<artifactId>exo.portal.webui.core</artifactId>
<version>3.5.2.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.exoplatform.commons</groupId>
<artifactId>commons-webui-ext</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.exoplatform.ecms</groupId>
<artifactId>ecms-core-webui-explorer</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
</project>
Create a new action and its corresponding listener by editing the
ShowNodePathActionComponent
class as below:package com.acme; import javax.jcr.Node; import org.exoplatform.ecm.webui.component.explorer.UIJCRExplorer; import org.exoplatform.ecm.webui.component.explorer.control.listener.UIActionBarActionListener; import org.exoplatform.web.application.ApplicationMessage; import org.exoplatform.webui.config.annotation.ComponentConfig; import org.exoplatform.webui.config.annotation.EventConfig; import org.exoplatform.webui.core.UIComponent; import org.exoplatform.webui.event.Event; @ComponentConfig( events = { @EventConfig(listeners = ShowNodePathActionComponent.ShowNodePathActionListener.class) }) public class ShowNodePathActionComponent extends UIComponent { public static class ShowNodePathActionListener extends UIActionBarActionListener<ShowNodePathActionComponent> { @Override protected void processEvent(Event<ShowNodePathActionComponent> event) throws Exception { UIJCRExplorer uiJCRExplorer = event.getSource().getAncestorOfType(UIJCRExplorer.class); Node node = uiJCRExplorer.getCurrentNode(); event.getRequestContext() .getUIApplication() .addMessage(new ApplicationMessage("Node path:" + node.getPath(), null, ApplicationMessage.INFO)); } } }
Register the new action with UIExtensionManager in the
configuration.xml
file as below:<configuration xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <target-component>org.exoplatform.webui.ext.UIExtensionManager</target-component> <component-plugin> <name>add.action</name> <set-method>registerUIExtensionPlugin</set-method> <type>org.exoplatform.webui.ext.UIExtensionPlugin</type> <init-params> <object-param> <name>ShowNodePath</name> <object type="org.exoplatform.webui.ext.UIExtension"> <field name="type"> <string>org.exoplatform.ecm.dms.UIActionBar</string> </field> <field name="name"> <string>ShowNodePath</string> </field> <field name="component"> <string>com.acme.ShowNodePathActionComponent</string> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
Some remarks about the Java code and the configuration:
ShowNodePath
will be used to label the action, until you configure the label in resource bundle that will be explained later.ShowNodePathActionComponent
is the class name of your action.There is a matching rule between the action name (
ShowNodePath
) and the listener class name (ShowNodePathActionListener
): the listener class name = the action name + ActionListener.
Build your project:
mvn clean install
Copy the
.jar
file (target/action-example-1.0.jar
) to thelib
folder of PRODUCT.Restart the server.
Testing
Log in as an administrator and go to Content Administration.
Edit a view to add the action to one of tabs of the view. At this step, you will see the ShowNodePath action as below:
Make sure there is a drive that applies the view. For example, you can choose the Admin view and the Collaboration drive.
Go to Sites Explorer and select the drive, then switch to the edited view.
Select any node. The “ShowNodePath” button now displays in Action bar as below:
Next, you can perform the followings for your action extension:
Customizing label and icon¶
Customizing labels
As you can see in the screenshots in previous section, your action displays in UI as “showNodePath” or “ShowNodePath”. You can change this label to something in more friendly way, like “Show Node Path”, by adding and registering your resource bundle to ResourceBundle service:
Add the
src/main/resources/locale/com/acme
folder to your project.Add the
ShowNodePath_en.xml
file to this folder, with the following content:<bundle> <UITabForm> <label> <showNodePath>Show Node Path</showNodePath> </label> </UITabForm> <UIActionBar> <tooltip> <ShowNodePath>Show Node Path</ShowNodePath> </tooltip> </UIActionBar> </bundle>
Note
Notice the “showNodePath” tag (lowercase for first letter) in UITabForm. What you configure in UITabForm element will be displayed in Content Administration portlet. The other, UIActionBar, is for
Sites Explorer portlet.
Add the following configuration to
src/main/resources/conf/portal/configuration.xml
:<external-component-plugins> <target-component>org.exoplatform.services.resources.ResourceBundleService</target-component> <component-plugin> <name>UI Extension</name> <set-method>addResourceBundle</set-method> <type>org.exoplatform.services.resources.impl.BaseResourceBundlePlugin</type> <init-params> <values-param> <name>init.resources</name> <value>locale.com.acme.ShowNodePath</value> </values-param> <values-param> <name>portal.resource.names</name> <value>locale.com.acme.ShowNodePath</value> </values-param> </init-params> </component-plugin> </external-component-plugins>
The locale.com.acme.ShowNodePath
value expresses that your resource
files should be located in the locale/com/acme/
folder and have the
“ShowNodePath” prefix in name. ShowNodePath_en.xml
is resource for
English of which “en” is the locale code. You can add other
resources for many languagues.
Now re-build your project and deploy. Restart server and test, you will see the labels change into “Show Node Path”.
See more details about ResourceBundle service and locale codes.
If you want more samples of such configuration, see:
webapps/ecmexplorer.war!/WEB-INF/classes/locale/portlet/explorer/JCRExplorerPortlet_en.xml
webapps/ecmadmin.war!/WEB-INF/classes/locale/portlet/administration/ECMAdminPortlet_en.xml
Customizing icons
Edit the
webapps/ecmexplorer.war!/skin/icons/24x24/DefaultStylesheet.css
file
and add the icon definition as below (in this case,
the “ManageUnLock” icon is re-used but you could add your own
picture into the
webapps/ecmexplorer.war!/skin/icons/24x24/DefaultSkin
directory):
.ShowNodePathIcon{
width: 24px; height: 24px;
background: url('DefaultSkin/ManageUnLock.gif') no-repeat left center; /* orientation=lt */
background: url('DefaultSkin/ManageUnLock.gif') no-repeat right center; /* orientation=rt */
}
Filtering your action¶
¹. Write your filter class (com/acme/MyUIFilter.java
):
package com.acme; import java.util.Map; import javax.jcr.Node; import org.exoplatform.webui.ext.filter.UIExtensionFilter; import org.exoplatform.webui.ext.filter.UIExtensionFilterType; public class MyUIFilter implements UIExtensionFilter { /* * This method checks if the current node is a file. */ public boolean accept(Map<String, Object> context) throws Exception { //Retrieve the current node from the context Node currentNode = (Node) context.get(Node.class.getName()); return currentNode.isNodeType("nt:file"); } /* * This is the type of the filter. */ public UIExtensionFilterType getType() { return UIExtensionFilterType.MANDATORY; } /* * This is called when the filter has failed. */ public void onDeny(Map<String, Object> context) throws Exception { System.out.println("This node is not a file!"); } }
This filter checks if the current node is a file. Because the filter type is MANDATORY, the action will hide if the current node is a folder. (Thus, with MANDATORY you cannot test onDeny method. Change the type into OPTIONAL if you want to test the method.)
Apply the filter in your action class (
com/acme/ShowNodePathActionComponent.java
):... import java.util.List; import java.util.Arrays; import org.exoplatform.webui.ext.filter.UIExtensionFilter; import org.exoplatform.webui.ext.filter.UIExtensionFilters; import com.acme.MyUIFilter; ... public class ShowNodePathActionComponent extends UIComponent { ... /* * Add filters (MyUIFilter in this example) */ private static final List<UIExtensionFilter> FILTERS = Arrays.asList(new UIExtensionFilter[] {new MyUIFilter()}); @UIExtensionFilters public List<UIExtensionFilter> getFilters() { return FILTERS; } }
Now build, deploy and test that your action displays only for nodes of type “nt:file”.
You have added a filter by Java code. Another way is by configuration, that is extremely good when the filter itself allows flexible configuration. For example, you continue to add UserACLFilter (built-in) that allows you to configure who can use the action:
Add the following configuration to conf/portal/configuration.xml
:
<external-component-plugins>
<target-component>org.exoplatform.webui.ext.UIExtensionManager</target-component>
...
<field name="extendedFilters">
<collection type="java.util.ArrayList">
<value>
<object type="org.exoplatform.webui.ext.filter.impl.UserACLFilter">
<field name="permissions">
<collection type="java.util.ArrayList">
<value>
<string>manager:/platform/administrators</string>
</value>
</collection>
</field>
</object>
</value>
</collection>
</field>
...
</external-component-plugins>
Then test that the action displays only for the users who have manager:/platform/administrators membership.
There are many useful built-in filters in Content. In your real project, you should see if some of them meet your business logic before writing a new one:
org.exoplatform.webui.ext.filter.impl.UserACLFilter
: Filters all nodes that do not have any permission on the current context.org.exoplatform.webui.ext.filter.impl.FileFilter
: Filters all nodes that do not exist in the given MIME type list.org.exoplatform.ecm.webui.component.explorer.control.filter
: This package includes many filters, see in the table.
Filters |
Description |
---|---|
|
Filters nodes to which it is impossible to add categories. |
|
Filters nodes which cannot be cut. |
|
Filters nodes to which it is impossible to add nodes. |
|
Filters nodes that cannot be deleted. |
|
Filters nodes that cannot be removed. |
|
Filters nodes which do not allow versioning. |
|
Filters nodes that cannot be modified. |
``HasMetadataTemplatesFilter` ` |
Filters nodes that do not have metadata templates. |
|
Filters all nodes that do not have the publication plugins. |
|
Filters nodes that do not have the Removepermission. |
|
Filters nodes that are not favorite. |
|
Filters nodes that are favorite. |
|
Filters nodes that are of nt:file. |
|
Filters nodes which do not hold lock. |
|
Filters nodes which are holding lock. |
|
Filters the root node. |
|
Filters nodes that are not in the trash node. |
|
Filters nodes that are in the trash node. |
``IsNotSameNameSiblingFilter` ` |
Filters nodes that allow the same name siblings. |
|
Filters nodes that do not allow commenting. |
|
Filters nodes that do not allow voting. |
|
Filters nodes that are locked. |
|
Filters nodes that are symlinks. |
|
Filters nodes that are of the category type. |
``IsNotSystemWorkspaceFilter` ` |
Filters actions of the system-typed workspace. |
|
Filters nodes that are checked out. |
|
Filters nodes that are not trash ones. |
|
Filters a node that is the trash one. |
``IsNotEditingDocumentFilter` ` |
Filters nodes that are being edited. |
|
Filters nodes where the paste action is not allowed. |
|
Filters nodes that do not allow adding references. |
|
Filters nodes that are folders. |
|
Filters nodes that are not checked out. |
|
Filters nodes which do not allow versioning. |
|
Filters nodes and ancestor nodes which do not allow versioning. |
|
Filters nodes that are not documents. |
|
Filters nodes that are not editable. |
Creating a file viewer¶
eXo Platform supports the inline visualization for many file formats. For example, let’s see the display of PDF file:
For those not yet available, one message will be displayed that requires you to download it. Here is the view of a ZIP file:
However, eXo Platform allows you to create a new file viewer to read one file format, for example, ZIP files. Assuming that you want to display the list of files contained in the ZIP file, follow the steps below. The source code of this project is available here for downloading.
Create a Maven project, for example, named zip-viewer, with the below structure:
Create the view template by editing the
resources/templates/ZipViewer.gtmpl
. Here, you only need to iterate all the ZIP files to display their names:<style> ul.zip-file-list { padding: 0 20px; } ul.zip-file-list li { list-style-position: inside; list-style-type: circle; } </style> <% import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import org.exoplatform.webui.core.UIComponent def uiParent = uicomponent.getParent() def originalNode = uiParent.getOriginalNode() def contentNode = originalNode.getNode("jcr:content") def zis; try { zis = new ZipInputStream(contentNode.getProperty("jcr:data").getStream()) ZipEntry ze out.println("<ul class=\"zip-file-list\">") while ((ze = zis.getNextEntry()) != null) { out.println("<li>" + ze.getName() + "</li>") } out.println("</ul>") } finally { zis?.close() } %>
Open the
java/org/exoplatform/ecm/dms/ZipViewer.java
file. Once the view template is ready, it has to be registered and linked to the ZIP file type. The first step for registering the template is to create a simple class which extendsUIComponent
and to define the view template’s path. Note that this class defines the template’s path, that is,templates/ZipViewer.gtmpl
in this case.package org.exoplatform.ecm.dms; import org.exoplatform.webui.config.annotation.ComponentConfig; import org.exoplatform.webui.core.UIComponent; @ComponentConfig( template = "classpath:templates/ZipViewer.gtmpl" ) public class ZipViewer extends UIComponent { }
Edit the
resources/conf/portal/configuration.xml
file where the class is declared by theorg.exoplatform.webui.ext.UIExtensionManager
component.<?xml version="1.0" encoding="ISO-8859-1"?> <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <target-component>org.exoplatform.webui.ext.UIExtensionManager</target-component> <component-plugin> <name>Zip File dynamic viewer</name> <set-method>registerUIExtensionPlugin</set-method> <type>org.exoplatform.webui.ext.UIExtensionPlugin</type> <init-params> <object-param> <name>Zip</name> <object type="org.exoplatform.webui.ext.UIExtension"> <field name="type"> <string>org.exoplatform.ecm.dms.FileViewer</string> </field> <field name="rank"> <int>110</int> </field> <field name="name"> <string>Zip</string> </field> <field name="category"> <string>FileViewer</string> </field> <field name="component"> <string>org.exoplatform.ecm.dms.ZipViewer</string> </field> <field name="extendedFilters"> <collection type="java.util.ArrayList"> <value> <object type="org.exoplatform.webui.ext.filter.impl.FileFilter"> <field name="mimeTypes"> <collection type="java.util.ArrayList"> <value> <string>application/zip</string> </value> </collection> </field> </object> </value> </collection> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
This configuration links the org.exoplatform.ecm.dms.ZipViewer
component to the application/zip
mimetype.
Update the
pom.xml
file that declares dependencies of the classes imported in theZipViewer.java
file.<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>exo.file.viewer</groupId> <artifactId>zip-viewer</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>zip-viewer</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <groupId>org.gatein.portal</groupId> <artifactId>exo.portal.webui.framework</artifactId> <version>3.5.9-PLF</version> <scope>provided</scope> </dependency> </dependencies> </project>
Build the zip-viewer project using the command:
``mvn clean install``. Your JAR (``zip-viewer/target/zip-viewer-1.0-SNAPSHOT.jar``) should now contain 3 files: - ``templates/ZipViewer.gtmpl`` - ``org/exoplatform/ecm/dms/ZipViewer.class`` - ``conf/portal/configuration.xml``
Put this
.jar
file into the eXo Platform package.$PLATFORM_TOMCAT_HOME/lib
.
Restart the server. The content of a ZIP file is now displayed as below:
Other components¶
Working with other toolbars is quite similar to UIActionbar, except configurations and resources.
Sidebar
Sample configuration
<object-param>
<name>Example</name>
<object type="org.exoplatform.webui.ext.UIExtension">
<field name="type"><string>org.exoplatform.ecm.dms.UISideBar</string></field>
<field name="name"><string>Example</string></field>
<field name="rank"><int>110</int></field>
<field name="component"><string>com.acme.ExampleActionComponent</string></field>
</object>
</object-param>
Resources are located at
$PLATFORM_TOMCAT-HOME/webapps/ecmexplorer/WEB-INF/classes/locale/portlet/explorer/JCRExplorerPortlet_en.xml
(for English which is also the default language):
...
<UISideBar>
...
<label>
<example>Example action</example>
...
</label>
...
</UISideBar>
...
Admin control panel
Sample configuration
<object-param>
<name>Example</name>
<object type="org.exoplatform.webui.ext.UIExtension">
<field name="type">
<string>org.exoplatform.ecm.dms.UIECMAdminControlPanel</string>
</field>
<field name="rank">
<int>110</int>
</field>
<field name="name">
<string>Example</string>
</field>
<field name="category">
<string>Templates</string>
</field>
<field name="component">
<string>org.exoplatform.ecm.webui.component.admin.manager.UITemplatesManagerComponent</string>
</field>
</object>
</object-param>
The “category” field specifies the category where your extension action is performed. There are 4 options:
Templates
Explorer
Repository
Advanced
Resources are located at
$PLATFORM_TOMCAT-HOME/webapps/ecmadmin/WEB-INF/classes/locale/portlet/administration/ECMAdminPortlet_en.xml
(for English which is also the default language):
...
<UIECMAdminControlPanel>
...
<label>
<example>Example panel</example>
...
</label>
...
</UIECMAdminControlPanel>
...
Context menu
Sample configuration
<object-param>
<name>Example</name>
<object type="org.exoplatform.webui.ext.UIExtension">
<field name="type"><string>org.exoplatform.ecm.dms.UIWorkingArea</string></field>
<field name="rank"><int>105</int></field>
<field name="name"><string>Example</string></field>
<field name="category"><string>ItemContextMenu_SingleSelection</string></field>
<field name="component"><string>com.acme.ExampleActionComponent</string></field>
</object>
</object-param>
The “category” field specifies the category where your extension action is performed. There are many options:
ItemContextMenu_SingleSelection
: This menu has only one item when Trash Folder is right-clicked.ItemContextMenu
: The menu appears when the user selects one or many items.GroundContextMenu
&ItemGroundContextMenu
: The menu appears when the user right-clicks the ground of node.
- Resources are located at ``
$TOMCAT-HOME/webapps/ecmexplorer/WEB-INF/classes/locale/portlet/explorer/JCRExplorerPortlet_en.xml
`` (for English which is also the default language):
<UIWorkingArea>
...
<label>
<example>Example action</example>
...
</label>
...
</UIWorkingArea>
Writing an action extension in Wiki¶
This tutorial instructs you to plug an action to the Wiki page via the following main steps:
Note that all source code used in this section is provided here <https://github.com/exo-samples/docs-samples/tree/4.3.x/write-action-extension> for downloading.
Creating your new project¶
Create a Maven project which has the following directory structure:
example
|__ pom.xml
|__ src
|__ main
|__ java
| |__ com
| |__ acme
| |__ ViewSourceActionComponent.java
|__ resources
|__ conf
| |__ portal
| |__ configuration.xml
|__ locale
|__ com
|__ acme
|__ ViewSource_en.properties
Here is content of the pom.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme</groupId>
<artifactId>example</artifactId>
<version>1.0</version>
<name>eXo Wiki action - Example</name>
<description>eXo Wiki action - Example</description>
<dependencies>
<dependency>
<groupId>org.gatein.portal</groupId>
<artifactId>exo.portal.webui.framework</artifactId>
<version>3.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.exoplatform.commons</groupId>
<artifactId>commons-webui-ext</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.exoplatform.wiki</groupId>
<artifactId>wiki-service</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.exoplatform.wiki</groupId>
<artifactId>wiki-webui</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
</project>
Creating new action and the corresponding listener¶
Edit the ViewSourceActionComponent class as below:
package com.acme;
import java.util.Arrays;
import java.util.List;
import org.exoplatform.webui.config.annotation.ComponentConfig;
import org.exoplatform.webui.config.annotation.EventConfig;
import org.exoplatform.webui.event.Event;
import org.exoplatform.webui.ext.filter.UIExtensionFilter;
import org.exoplatform.webui.ext.filter.UIExtensionFilters;
import org.exoplatform.wiki.commons.Utils;
import org.exoplatform.wiki.mow.core.api.wiki.PageImpl;
import org.exoplatform.wiki.webui.UIWikiContentDisplay;
import org.exoplatform.wiki.webui.UIWikiPageContentArea;
import org.exoplatform.wiki.webui.UIWikiPortlet;
import org.exoplatform.wiki.webui.control.action.core.AbstractEventActionComponent;
import org.exoplatform.wiki.webui.control.filter.IsViewModeFilter;
import org.exoplatform.wiki.webui.control.listener.MoreContainerActionListener;
@ComponentConfig (
template = "app:/templates/wiki/webui/control/action/AbstractActionComponent.gtmpl",
events = {
@EventConfig(listeners = ViewSourceActionComponent.ViewSourceActionListener.class)
}
)
public class ViewSourceActionComponent extends AbstractEventActionComponent {
public static final String ACTION = "ViewSource";
private static final List<UIExtensionFilter> FILTERS = Arrays.asList(new UIExtensionFilter[] { new IsViewModeFilter() });
@UIExtensionFilters
public List<UIExtensionFilter> getFilters() {
return FILTERS;
}
@Override
public String getActionName() {
return ACTION;
}
@Override
public boolean isAnchor() {
return false;
}
public static class ViewSourceActionListener extends MoreContainerActionListener<ViewSourceActionComponent> {
@Override
protected void processEvent(Event<ViewSourceActionComponent> event) throws Exception {
UIWikiPortlet wikiPortlet = event.getSource().getAncestorOfType(UIWikiPortlet.class);
UIWikiContentDisplay contentDisplay = wikiPortlet.findFirstComponentOfType(UIWikiPageContentArea.class)
.getChildById(UIWikiPageContentArea.VIEW_DISPLAY);
PageImpl wikipage = (PageImpl) Utils.getCurrentWikiPage();
contentDisplay.setHtmlOutput(wikipage.getContent().getText());
event.getRequestContext().addUIComponentToUpdateByAjax(contentDisplay);
}
}
}
Some remarks:
The action name is ViewSource.
The listener class name = the action name + “ActionListener” (so it is ViewSourceActionListener).
In this example, the listener extends the
MoreContainerActionListener
class. As a result, the action will be added to the More menu in the Wiki portlet. There are some choices that will be introduced later.At the ComponentConfig annotation, you see a gtmpl file is given. Here you re-use the
templates/wiki/webui/control/action/AbstractActionComponent.gtmpl
file that is already packaged inwiki.war
.
Registering new action with UIExtensionManager service¶
Edit the configuration.xml
file as below:
<configuration xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd">
<external-component-plugins>
<target-component>org.exoplatform.webui.ext.UIExtensionManager</target-component>
<component-plugin>
<name>add.action</name>
<set-method>registerUIExtensionPlugin</set-method>
<type>org.exoplatform.webui.ext.UIExtensionPlugin</type>
<init-params>
<object-param>
<name>ViewSource</name>
<object type="org.exoplatform.webui.ext.UIExtension">
<field name="type"><string>org.exoplatform.wiki.webui.control.MoreExtensionContainer</string></field>
<field name="rank"><int>1000</int></field>
<field name="name"><string>ViewSource</string></field>
<field name="component"><string>com.acme.ViewSourceActionComponent</string></field>
</object>
</object-param>
</init-params>
</component-plugin>
</external-component-plugins>
</configuration>
Pay attention to the action name (ViewSource) and the component name (com.acme.ViewSourceActionComponent).
The configuration for UIExtension is explained here.
As noticed before, your action listener extends the
MoreExtensionContainer
class. Here you see it is passed to the
type field. You can decide which menu your action is plugged in, by
choosing one of types below:
Type |
Description |
---|---|
|
Actions will be placed in Toolbar in the View mode. |
|
Actions will be plugged in the Add Page menu in the View mode. |
|
Actions will be plugged in the More menu in the View mode. |
|
Actions will be placed in Toolbar in the Edit mode. |
|
Actions will be placed in the Editor tabs. |
|
Actions will be plugged in the Browse menu in the View mode. |
|
Actions will be placed in the Setting tabs. |
Registering localized resources with ResourceBundle service¶
In this example, you have a resource file, that is
ViewSource_en.properties
. The _en suffix means English. You can
write many resources for other languages.
Edit the
ViewSource_en.properties
file as below:MoreExtensionContainer.action.ViewSource=View Source
This indicates that the label of your action will be View Source.
Name of the MoreExtensionContainer.action.ViewSource
property must
be changed if you use another type. It is dependent on the gtmpl
file you use in your Java class. See this code in
wiki.war!/templates/wiki/webui/control/action/AbstractActionComponent.gtmpl
:
String labelName = _ctx.appRes(uicomponent.getParent().getName() + ".action." + actionName);
Configure the ResourceBundle service in the
configuration.xml
file as below:<external-component-plugins> <target-component>org.exoplatform.services.resources.ResourceBundleService</target-component> <component-plugin> <name>UI Extension</name> <set-method>addResourceBundle</set-method> <type>org.exoplatform.services.resources.impl.BaseResourceBundlePlugin</type> <init-params> <values-param> <name>init.resources</name> <value>locale.com.acme.ViewSource</value> </values-param> <values-param> <name>portal.resource.names</name> <value>locale.com.acme.ViewSource</value> </values-param> </init-params> </component-plugin> </external-component-plugins>
Pay attention to the resource name: locale.com.acme.ViewSource
. It
is a translation of the locale/com/acme/ViewSource_en.properties
file path (relative to the Jar archive), with the _en suffix and the
.properties extension is eliminated.
See here for the ResourceBundle configuration.
Deploying new action extension¶
Follow these steps to deploy and test your new action extension:
Build the project by the command:
mvn clean install
Copy the
target/example-1.0.jar
file into the$PLATFORM_TOMCAT_HOME/lib
directory.Start eXo Platform and go to the Wiki portlet. You will see your action in the More menu as below:
Notification¶
Extending notification system Steps to create an extension which plugs a new notification type and channel into current notification system in eXo Platform.
Overriding email notification Steps to create an extension which overrides the email notification templates following your own style.
Extending notification system¶
eXo Platform provides you with a notification system that allows you to extend in 2 mechanisms:
The extensibility of notification channels, such as by email, directly on-site or through pushing.
The extensibility of notification types, such as connection invitation, space activities.
This section will walk you through a complete sample extension that instructs you how to:
create a new notification channel that pushes notification information to the console panel.
create a new notification type that informs when one user in your network changes her/his profile.
First you need to create a new Maven project with the overall structure:
And now, continue with the detailed steps:
Under pom.xml
Add the following dependencies to the pom.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acme.samples</groupId>
<artifactId>console-notification</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>lib</module>
<module>config</module>
<module>webapp</module>
</modules>
<properties>
<!--Platform project's dependencies (REPLACE 6.0.x-SNAPSHOT by the corresponding version)-->
<org.exoplatform.social.version>6.0.x-SNAPSHOT</org.exoplatform.social.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Import versions from platform project -->
<dependency>
<groupId>org.exoplatform.social</groupId>
<artifactId>social</artifactId>
<version>${org.exoplatform.social.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Under config folder
Create a
pom.xml
file and twoconfiguration.xml
files underconfig
folder as below:Add the following information to
config/pom.xml
:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.acme.samples</groupId> <artifactId>console-notification</artifactId> <version>1.0.0</version> </parent> <artifactId>console-notification-config</artifactId> <packaging>jar</packaging> <build> <finalName>console-notification-config</finalName> </build> </project>
Add the below configuration to
conf/configuration.xml
:<?xml version="1.0" encoding="UTF-8"?> <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <!-- The full qualified name of the PortalContainerConfig --> <target-component>org.exoplatform.container.definition.PortalContainerConfig</target-component> <component-plugin> <!-- The name of the plugin --> <name>Add PortalContainer Definitions</name> <!-- The name of the method to call on the PortalContainerConfig in order to register the PortalContainerDefinitions --> <set-method>registerChangePlugin</set-method> <!-- The full qualified name of the PortalContainerDefinitionPlugin --> <type>org.exoplatform.container.definition.PortalContainerDefinitionChangePlugin</type> <priority>102</priority> <init-params> <values-param> <name>apply.specific</name> <value>portal</value> </values-param> <object-param> <name>addDependencies</name> <object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependencies"> <!-- The name of the portal container --> <field name="dependencies"> <collection type="java.util.ArrayList"> <value> <!--The context name of the portal extension--> <string>console-notification-webapp</string> </value> </collection> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
Add the following configuration to
portal/configuration.xml
:<?xml version="1.0" encoding="UTF-8"?> <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <target-component>org.exoplatform.social.core.manager.IdentityManager</target-component> <component-plugin> <name>SocialProfileListener</name> <set-method>registerProfileListener</set-method> <type>com.acme.samples.notification.SocialProfileListener</type> </component-plugin> </external-component-plugins> <external-component-plugins> <target-component>org.exoplatform.commons.api.notification.channel.ChannelManager</target-component> <component-plugin profiles="all"> <name>console.channel</name> <set-method>register</set-method> <type>com.acme.samples.notification.ConsoleChannel</type> <description>Register the console channel to manager.</description> </component-plugin> </external-component-plugins> <external-component-plugins> <target-component>org.exoplatform.commons.api.notification.service.setting.PluginContainer</target-component> <component-plugin> <name>notification.plugins</name> <set-method>addPlugin</set-method> <type>com.acme.samples.notification.plugin.UpdateProfilePlugin</type> <description>Initial information for plugin.</description> <init-params> <object-param> <name>template.UpdateProfilePlugin</name> <description>The template of UpdateProfilePlugin</description> <object type="org.exoplatform.commons.api.notification.plugin.config.PluginConfig"> <field name="pluginId"> <string>UpdateProfilePlugin</string> </field> <field name="resourceBundleKey"> <string>UINotification.label.UpdateProfilePlugin</string> </field> <field name="order"> <string>11</string> </field> <field name="defaultConfig"> <collection type="java.util.ArrayList"> <value> <string>Instantly</string> </value> </collection> </field> <field name="groupId"> <string>general</string> </field> <field name="bundlePath"> <string>locale.notification.template.Notification</string> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
Register
SocialProfileListener
as a profile listener plugin to theIdentityManager
component. This plugin listens to user profile updating events.Register new plugin
console.channel
to theChannelManager
component. This plugin pushes notifications to console panel.Register new plugin
UpdateProfilePlugin
to thePluginContainer
component. This plugin declares and initializes parameters for the new notification type. The initial parameters include:
template.UpdateProfilePlugin
- the template ofUpdateProfilePlugin
.pluginId
- the Id of plugin which was defined in the classUpdateProfilePlugin
.resourceBundleKey
- the key which will be provided in resource bundle files of each locale.order
- the order to display the new type in notification group.groupId
- the Id of group that this notification type belongs to.bundlePath
- the path to the locale resource.defaultConfig
- the default settings for this notification type at first startup.
Under lib folder
Create another project under
lib
folder with thepom.xml
file as below:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.acme.samples</groupId> <artifactId>console-notification</artifactId> <version>1.0.0</version> </parent> <artifactId>console-notification-lib</artifactId> <packaging>jar</packaging> <dependencies> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.exoplatform.social</groupId> <artifactId>social-component-core</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.exoplatform.core</groupId> <artifactId>exo.core.component.organization.api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.exoplatform.social</groupId> <artifactId>social-component-common</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>console-notification-lib</finalName> </build> </project>
Implement the class
UpdateProfilePlugin.java
as follows:package com.acme.samples.notification.plugin; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import org.exoplatform.commons.api.notification.NotificationContext; import org.exoplatform.commons.api.notification.model.ArgumentLiteral; import org.exoplatform.commons.api.notification.model.NotificationInfo; import org.exoplatform.commons.api.notification.plugin.BaseNotificationPlugin; import org.exoplatform.commons.utils.ListAccess;
- import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.xml.InitParams; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.social.core.identity.model.Identity; import org.exoplatform.social.core.identity.model.Profile; import org.exoplatform.social.core.manager.RelationshipManager;
- public class UpdateProfilePlugin extends BaseNotificationPlugin {
public final static ArgumentLiteral<Profile> PROFILE = new ArgumentLiteral<Profile>(Profile.class, “profile”); private static final Log LOG = ExoLogger.getLogger(UpdateProfilePlugin.class); public final static String ID = “UpdateProfilePlugin”;
- public UpdateProfilePlugin(InitParams initParams) {
super(initParams);
}
@Override public String getId() {
return ID;
}
@Override public boolean isValid(NotificationContext ctx) {
return true;
}
@Override protected NotificationInfo makeNotification(NotificationContext ctx) {
Profile profile = ctx.value(PROFILE); Set<String> receivers = new HashSet<String>();
RelationshipManager relationshipManager = ExoContainerContext.getService(RelationshipManager.class); Identity updatedIdentity = profile.getIdentity(); ListAccess<Identity> listAccess = relationshipManager.getConnections(updatedIdentity); try {
Identity[] relationships = relationshipManager.getConnections(updatedIdentity).load(0, listAccess.getSize()); for(Identity i : relationships) {
receivers.add(i.getRemoteId());
} } catch (Exception ex) { LOG.error(ex.getMessage(), ex);
}
return NotificationInfo.instance() .setFrom(updatedIdentity.getRemoteId()) .to(new ArrayList<String>(receivers)) .setTitle(updatedIdentity.getProfile().getFullName() + ” updated his/her profile.<br/>”) .key(getId());
}
}
This class extends BaseNotificationPlugin
that retrieves information
for new notification type of user profile updating event.
The makeNotification()
method was overriden to generate essential
information for a notification.
Implement the class
SocialProfileListener.java
as below:package com.acme.samples.notification; import org.exoplatform.commons.api.notification.NotificationContext; import org.exoplatform.commons.api.notification.model.PluginKey; import org.exoplatform.commons.notification.impl.NotificationContextImpl; import org.exoplatform.social.core.identity.model.Profile; import org.exoplatform.social.core.profile.ProfileLifeCycleEvent; import org.exoplatform.social.core.profile.ProfileListenerPlugin; import com.acme.samples.notification.plugin.UpdateProfilePlugin; public class SocialProfileListener extends ProfileListenerPlugin { @Override public void avatarUpdated(ProfileLifeCycleEvent event) { Profile profile = event.getProfile(); NotificationContext ctx = NotificationContextImpl.cloneInstance().append(UpdateProfilePlugin.PROFILE, profile); ctx.getNotificationExecutor().with(ctx.makeCommand(PluginKey.key(UpdateProfilePlugin.ID))).execute(ctx); } @Override public void experienceSectionUpdated(ProfileLifeCycleEvent event) { Profile profile = event.getProfile(); NotificationContext ctx = NotificationContextImpl.cloneInstance().append(UpdateProfilePlugin.PROFILE, profile); ctx.getNotificationExecutor().with(ctx.makeCommand(PluginKey.key(UpdateProfilePlugin.ID))).execute(ctx); } @Override public void contactSectionUpdated(ProfileLifeCycleEvent event) { Profile profile = event.getProfile(); NotificationContext ctx = NotificationContextImpl.cloneInstance().append(UpdateProfilePlugin.PROFILE, profile); ctx.getNotificationExecutor().with(ctx.makeCommand(PluginKey.key(UpdateProfilePlugin.ID))).execute(ctx); } @Override public void createProfile(ProfileLifeCycleEvent event) { Profile profile = event.getProfile(); NotificationContext ctx = NotificationContextImpl.cloneInstance().append(UpdateProfilePlugin.PROFILE, profile); ctx.getNotificationExecutor().with(ctx.makeCommand(PluginKey.key(UpdateProfilePlugin.ID))).execute(ctx); } }
This class extends ProfileListenerPlugin
to trigger user profile
updating events and plug them into UpdateProfilePlugin
as
notifications. The instance of UpdateProfilePlugin
will be used to
generate and send messages to all notification channels.
avatarUpdated()
- trigger avatar updating event.experienceSectionUpdated()
- trigger user experience updating event.contactSectionUpdated()
- trigger user contact updating event.createProfile()
- trigger user profile creating event.
Implement the class
ConsoleChannel.java
to have the following code:package com.acme.samples.notification; import java.io.Writer; import org.exoplatform.commons.api.notification.NotificationContext; import org.exoplatform.commons.api.notification.channel.AbstractChannel; import org.exoplatform.commons.api.notification.channel.template.AbstractTemplateBuilder; import org.exoplatform.commons.api.notification.channel.template.TemplateProvider; import org.exoplatform.commons.api.notification.model.ChannelKey; import org.exoplatform.commons.api.notification.model.MessageInfo; import org.exoplatform.commons.api.notification.model.NotificationInfo; import org.exoplatform.commons.api.notification.model.PluginKey; import org.exoplatform.commons.notification.lifecycle.SimpleLifecycle; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; public class ConsoleChannel extends AbstractChannel { private static final Log LOG = ExoLogger.getLogger(ConsoleChannel.class); private final static String ID = "CONSOLE_CHANNEL"; private final ChannelKey key = ChannelKey.key(ID); public ConsoleChannel() { super(new SimpleLifecycle()); } @Override public String getId() { return ID; } @Override public ChannelKey getKey() { return key; } @Override public void dispatch(NotificationContext ctx, String userId) { LOG.info(String.format("CONSOLE:: %s will receive the message from pluginId: %s", userId, ctx.getNotificationInfo().getKey().getId())); } @Override public void registerTemplateProvider(TemplateProvider provider) {} @Override protected AbstractTemplateBuilder getTemplateBuilderInChannel(PluginKey key) { return new AbstractTemplateBuilder() { @Override protected MessageInfo makeMessage(NotificationContext ctx) { return null; } @Override protected boolean makeDigest(NotificationContext ctx, Writer writer) { return false; } }; } }
This concrete class extends AbstractChannel
to define a new
notification channel which sends messages to console panel. Any new
channel must implement this interface and use an
external-component-plugin configuration to be registered in the
ChannelManager
.
The dispatch()
method was overriden to write notification contents
to console panel.
Under webapp folder
Create a new Maven project inside
webapp
folder with the followingpom.xml
file:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.acme.samples</groupId> <artifactId>console-notification</artifactId> <version>1.0.0</version> </parent> <artifactId>console-notification-webapp</artifactId> <packaging>war</packaging> <build> <finalName>console-notification-webapp</finalName> </build> </project>
Add the following configurations to
WEB-INF/web.xml
:<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.0" metadata-complete="true" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <display-name>console-notification-webapp</display-name> <filter> <filter-name>ResourceRequestFilter</filter-name> <filter-class>org.exoplatform.portal.application.ResourceRequestFilter</filter-class> </filter> <filter-mapping> <filter-name>ResourceRequestFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
display-name
- should be the same as the context name of the portal extension.
Open
Notification_en.properties
file to add this text:
#######################################################################
# UpdateProfilePlugin #
#######################################################################
# For UI
UINotification.title.UpdateProfilePlugin= Someone updates profile
UINotification.label.UpdateProfilePlugin= Someone updates profile
This is a resource bundle for English language. The value of
UINotification.title.UpdateProfilePlugin
and
UINotification.label.UpdateProfilePlugin
will be used to display as
English name of the new notification type through user interface.
Testing
Go up to the parent project’s folder and build it with the command: mvn clean install.
Copy the generated jar and war files into the corresponding deployment folders where you unpacked the eXo Platform installation.
Start eXo Platform and you will see your new functions appear in Notification Settings:
Log in as a user and update avatar or experience (remember to enable notification plugins first by an administrator).
Now, a message informing about this activity will be pushed to all notification channels, for instance:
directly on-site:
or on the console, there will be a message for each user who is connecting with the above user, such as:
James will receive the message from pluginId: UpdateProfilePlugin
Overriding email notification¶
In eXo Platform, all email notification templates are defined in the
social-notification-extension.war
package under
WEB-INF/notification/templates/
. Each of these templates corresponds
to a specific notification type. It is obvious that you can change all
of them as your desire.
To do this, there are 2 ways as follows:
Modifying the template layout, such as header, body or footer.
Adding or removing notification properties.
This tutorial selects to customize the ActivityMentionPlugin.gtmpl
file, which is the template for Mention Notification
by email. Note that you can download all the source code used in this
section
here.
First you need to create a new Maven project with the overall structure:
And now, continue with the detailed steps:
Under pom.xml
Add the following dependencies to the pom.xml
file:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.exoplatform</groupId>
<version>1.0.0</version>
<artifactId>email-notification-extension</artifactId>
<name>Email Notification Extension</name>
<packaging>pom</packaging>
<description>Email Notification Extension</description>
<dependencyManagement>
<dependencies>
<!-- Import versions from platform project -->
<dependency>
<groupId>org.exoplatform.platform</groupId>
<artifactId>platform</artifactId>
<version>4.2.x-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>config</module>
<module>webapp</module>
</modules>
</project>
Under config folder
Create a
pom.xml
and aconfiguration.xml
file as below:
Add the following information to
config/pom.xml
:<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <parent> <groupId>org.exoplatform</groupId> <artifactId>email-notification-extension</artifactId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>email-notification-extension-config</artifactId> <name>Email Notification Extension Configuration</name> <packaging>jar</packaging> <description>Email Notification Extension Configuration</description> </project>
Add the below configuration to
conf/configuration.xml
:<?xml version="1.0" encoding="UTF-8"?> <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <!-- The full qualified name of the PortalContainerConfig --> <target-component>org.exoplatform.container.definition.PortalContainerConfig</target-component> <component-plugin> <!-- The name of the plugin --> <name>Change PortalContainer Definitions</name> <!-- The name of the method to call on the PortalContainerConfig in order to register the changes on the PortalContainerDefinitions --> <set-method>registerChangePlugin</set-method> <!-- The full qualified name of the PortalContainerDefinitionChangePlugin --> <type>org.exoplatform.container.definition.PortalContainerDefinitionChangePlugin</type> <priority>102</priority> <init-params> <value-param> <name>apply.default</name> <value>true</value> </value-param> <object-param> <name>change</name> <object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependenciesAfter"> <!-- The list of name of the dependencies to add --> <field name="dependencies"> <collection type="java.util.ArrayList"> <value> <string>email-notification-webapp</string> </value> </collection> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
Under webapp folder
In the below steps, you will modify layout, add and remove several
properties of this ActivityMentionPlugin
template. Note that when
you add a new property to a notification template, it is required that
you declare it in all Notification_xx.properties
files (xx is the
language code, fr for French, for instance). In this tutorial, assume
that there are only 2 languages available which are English (en) and
French (fr).
Create a new Maven project inside
webapp
folder as follows:
In which, the
Notification_en.properties
,Notification_fr.properties
andActivityMentionPlugin.gtmpl
files are copied from$PLATFORM_HOME/webapps/social-notification-extension.war!/WEB-INF/classes/locale/notification/template
and$PLATFORM_HOME/webapps/social-notification-extension.war!/WEB-INF/notification/templates
respectively.
Configure the
pom.xml
file as follows:<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <parent> <groupId>org.exoplatform</groupId> <artifactId>email-notification-extension</artifactId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>email-notification-webapp</artifactId> <packaging>war</packaging> <name>Email Notification Extension Webapp</name> <description>Email Notification Extension Webapp</description> <build> <finalName>email-notification-webapp</finalName> </build> </project>
Add the following configurations to
WEB-INF/web.xml
:<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <display-name>email-notification-webapp</display-name> <!-- Resource filter to cache merged javascript and css --> <filter> <filter-name>ResourceRequestFilter</filter-name> <filter-class>org.exoplatform.portal.application.ResourceRequestFilter</filter-class> </filter> <filter-mapping> <filter-name>ResourceRequestFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- Listener --> <listener> <listener-class>org.exoplatform.container.web.PortalContainerConfigOwner</listener-class> </listener> </web-app>
display-name
- should be the same as the context name of the portal extension.
Modify the
ActivityMentionPlugin.gtmpl
file as below:<table border="0" cellpadding="0" cellspacing="0" width="500" bgcolor="#ffffff" align="center" style="background-color: #ffffff; font-size: 13px;color:#333333;line-height: 18px;font-family: HelveticaNeue, Helvetica, Arial, sans-serif;"> <tr><!--start header area--> <td align="center" valign="middle" bgcolor="#ffffff" style="background-color: #ffffff;"> <table cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffff" align="center" style="border:1px solid #d8d8d8;"> <tr> <!-- insert company logo and link--> <td style="width: 20%;margin:0;height:45px;vertical-align:middle;background-color:#efefef;text-align:center"> <a href="www.exoplatform.com" target="_blank"> <img src="https://www.rosehosting.com/blog/wp-content/uploads/2014/03/exo-platform-vps.png" style="width: 50%"> </a> </td> <!--pass a link through a property--> <td style="margin:0;height:45px;vertical-align:middle;background-color:#efefef;font-family:'HelveticaNeue Bold',Helvetica,Arial,sans-serif;color:grey;font-size:14px;text-align:left" height="45" valign="middle"> <%=_ctx.appRes("Notification.label.header", FOOTER_LINK)%> </td> </tr> </table> </td> </tr><!--end header area--> <tr><!--start content area--> <td bgcolor="#ffffff" style="background-color: #ffffff;"> <table cellpadding="0" cellspacing="0" width="100%" bgcolor="#ffffff" style="background-color: #ffffff; border-left:1px solid #d8d8d8;border-right:1px solid #d8d8d8;"> <tr> <td> <table border="0" cellpadding="0" cellspacing="0" width="92%" bgcolor="#ffffff" align="center" style="background-color: #ffffff; color:#333333;line-height:20px;"> <tr> <td align="left" bgcolor="#ffffff" style="background-color: #ffffff; padding: 10px 0;"> <p style="margin:20, 20;font-weight:bold;vertical-align:middle; font-family: 'HelveticaNeue Bold', Helvetica, Arial, sans-serif;color:#2f5e92;font-size:18px;"> <!--new property--> <%=_ctx.appRes("Notification.label.Type")%> <%=_ctx.appRes("Notification.title.ActivityMentionPlugin")%> </p> <table border="0" cellpadding="0" cellspacing="0" > <tr> <td valign="top" style="margin-top: 0px;"> <p style="margin: 0 0 10px 0; line-height: 22px; color: #333333; font-size:13px; font-family:'HelveticaNeue bold',verdana,arial,tahoma"> <% String profileUrl = "<strong><a target=\"_blank\" style=\"color: #2f5e92; font-size: 13px; text-decoration: none; font-family: 'HelveticaNeue Bold', Helvetica, Arial, sans-serif\" href=\""+ PROFILE_URL + "\">" + USER + "</a></strong>"; %> <%=_ctx.appRes("Notification.message.ActivityMentionPlugin", profileUrl)%>: </p> <!--main content of the mentioned activity--> <table border="0" cellpadding="0" cellspacing="0" width="460" bgcolor="#ffffff" align="center" style="background-color: #ffffff; font-size: 12px;color:#333333;line-height: 18px; margin-bottom: 15px;"> <tbody> <tr> <td align="left" bgcolor="#ffffff" style="background-color: #f9f9f9; padding: 5px 0;"> $ACTIVITY </td> </tr> </tbody> </table> </td> </tr> </table> <!--insert Reply button--> <p style="margin: 0 0 20px;text-align:center"> <a target="_blank" style=" display: inline-block; text-decoration: none; font-size: 11px; font-family: 'HelveticaNeue Bold', Helvetica, Arial, sans-serif; color: #ffffff; text-shadow: 0 -1px 0 rgba(23, 33, 37, .25); background-color: #567ab6; background-image: -moz-linear-gradient(top, #638acd, #426393); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#638acd), to(#426393)); background-image: -webkit-linear-gradient(top, #638acd, #426393); background-image: -o-linear-gradient(top, #638acd, #426393); background-image: linear-gradient(to bottom, #638acd, #426393); background-repeat: repeat-x; border-radius: 4px; -moz-border-radius: 4px; padding: 4px 8px; height: 11px; line-height: 11px; max-height: 11px; text-align: center; border: 1px solid #224886; font-weight: bold; -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); vertical-align: middle; " href="$REPLY_ACTION_URL"><%=_ctx.appRes("Notification.label.Reply")%></a> <!--insert View full discussion button--> <a target="_blank" style=" display: inline-block; text-decoration: none; font-size: 11px; font-family: HelveticaNeue, Helvetica, Arial, sans-serif,serif; color: #333333; background-color: #f1f1f1; background-image: -moz-linear-gradient(top, #ffffff, #f1f1f1); background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f1f1f1)); background-image: -webkit-linear-gradient(top, #ffffff, #f1f1f1); background-image: -o-linear-gradient(top, #ffffff, #f1f1f1); background-image: linear-gradient(to bottom, #ffffff, #f1f1f1); background-repeat: repeat-x; border-radius: 4px; -moz-border-radius: 4px; padding: 4px 8px; height: 11px; line-height: 12px; max-height: 11px; text-align: center; border: 1px solid #c7c7c7; -webkit-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); -moz-box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); box-shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05); vertical-align: middle; margin-left: 3px; " href="$VIEW_FULL_DISCUSSION_ACTION_URL" target="_blank"><%=_ctx.appRes("Notification.label.ViewFullDiscussion")%></a> </p> </td> </tr> </table> </td> </tr> </table> </td> </tr><!--end content area--> </table>
You can replace with your company logo and link here.
A message at header. This property will be declared in the
Notification_xx.properties
files later.A new label before the title of the mentioned activity, which will be declared in the
Notification_xx.properties
files later.A message corresponding to the mentioned activity.
The detailed content of the mentioned activity.
A Reply button, helping user to quickly make answer on the mentioned activity stream.
A View full discussion button, helping user to jump directly to the mentioned activity stream.
In this script, we have added 2 new properties (
Notification.label.header
andNotification.label.Type
) and remove several ones (for example,Notification.label.footer
) in comparison with the old script in thesocial-notification-extension.war
package. The next steps will declare the new ones in two property files.Declare
Notification.label.header
andNotification.label.Type
as 2 new properties as follows:In
Notification_en.properties
:Notification.label.Type=Notification type: Notification.label.header=You has been successfully subscribed to our newsletter. <br/>To unsubscribe, <a target="_blank" style="margin:30px 0 10px 0; color: #2f5e92;text-decoration: none;font-size:13px;font-family:HelveticaNeue,arial,tahoma,serif" href="{0}">click here</a>.
In
Notification_fr.properties
:Notification.label.Type=Type de notification: Notification.label.header=Vous avez \u00E9t\u00E9 abonn\u00E9 \u00E0 notre bulletin avec succès.<br/> Pour d\u00E9sinscription <a target="_blank" style="margin:30px 0 10px 0; color: #2f5e92;text-decoration: none;font-size:13px;font-family:HelveticaNeue,arial,tahoma,serif" href="{0}">cliquez ici</a>.
Testing
Go up to the parent project’s folder and build it with the command: mvn clean install.
Copy the generated jar and war files into the corresponding deployment folders where you unpacked the eXo Platform installation.
Follow this guide to configure email service for eXo Platform.
Start eXo Platform and create 2 new users: john and marry, with real emails. Notice that you need to turn on the email notification, not only on john and marry sides but also on the administrator side as stated here.
Log in as marry user and post an activity that mentions john, for example.
Now, log in john’s email account, you will see a new notification email with layout as follows:
if john user is in English language:
if john user is in French language:
By comparing with the below old template, you will see changes between them:
Overriding user profile design¶
eXo Platform provides you with an extensible user profile design. With this extensibility, you can override portlets on the user profile page with your own templates.
To do this, there are 2 ways as follows:
Overriding existing html content by changing or adding more html elements, such as
div
tags.Overriding existing groovy script by changing or adding more operations, such as
for
loops orif
conditions.
This guide will walk you through both by overriding Profile Portlet, Experience Profile Portlet, Connections User Portlet and Recent Activities Portlet on the user profile page. You can download the source code used in this guide here.
Overriding user profile
Create a webapp
user-profile-extension.war
as follows:
The
user
folder contains your new profile portlet templates.
Add these configurations to
web.xml
:<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <display-name>user-profile-extension</display-name> <!-- Resource filter to cache merged javascript and css --> <filter> <filter-name>ResourceRequestFilter</filter-name> <filter-class>org.exoplatform.portal.application.ResourceRequestFilter</filter-class> </filter> <filter-mapping> <filter-name>ResourceRequestFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- Listener --> <listener> <listener-class>org.exoplatform.container.web.PortalContainerConfigOwner</listener-class> </listener> </web-app>
Override
UIBasicProfilePortlet.gtmpl
with the following code:<% import org.exoplatform.social.user.portlet.UserProfileHelper; import org.exoplatform.social.webui.Utils; //Retrieve the basic information of the user def profile = uicomponent.getProfileInfo(); def keys = profile.keySet(); %> <!-- showing and hiding control buttons --> <button onclick="showFullContact()" id="btn_show_contact">Show full contact information</button> <button onclick="hideFullContact()" id="btn_hide_contact" style="display: none">Hide full contact information</button> <!-- javascript to show and hide full contact information --> <script type="text/javascript"> function showFullContact(){ document.getElementById("$uicomponent.id").style.display = "block"; document.getElementById("btn_show_contact").style.display = "none"; document.getElementById("btn_hide_contact").style.display = "block"; } function hideFullContact(){ document.getElementById("$uicomponent.id").style.display = "none"; document.getElementById("btn_show_contact").style.display = "block"; document.getElementById("btn_hide_contact").style.display = "none"; } </script> <div class="uiSocApplication uiBasicProfilePortlet" id="$uicomponent.id" style="display: none"> <h4 class="head-container"><%=_ctx.appRes("UIBasicProfile.label.ContactInformation")%></h4> <div class="uiBasicInfoSection"> <% //Loop through to print out all information for(key in keys) { def values = profile.get(key); String clzz = key.substring(0, 1).toUpperCase() + key.substring(1); System.out.println(key); //If the user is not the owner, do not print out email if (!Utils.isOwner() && key.toString().equals("email")) continue; %> <div class="group-user-info"> <div class="label-user-info"><strong><%=_ctx.appRes("UIBasicProfile.label." + key)%>:</strong></div> <div class="value-user-info"> <% if(UserProfileHelper.isString(values)) { %> <div class="ui<%=clzz%> ellipsis" rel="tooltip" data-placement="top" title="" data-original-title="<%=values%>"><%=values%></div> <%} else { for(subKey in values.keySet()) { def isIms = UserProfileHelper.isIMs(key); def typeIconClzz = ""; if (isIms) { typeIconClzz = UserProfileHelper.getIconCss(subKey); } def listVal = values.get(subKey); int valueNum = 0; if (UserProfileHelper.isURL(key)) { for (url in listVal) { %> <div class="ui<%=clzz%> ellipsis"><a href="<%=UserProfileHelper.toAbsoluteURL(url)%>" target="_blank" rel="tooltip" data-placement="top" title="" data-original-title="<%=url%>"><%=url%></a></div> <%} } else { if (typeIconClzz.length() > 0) { typeIconClzz = typeIconClzz + " uiIconSocLightGray"; } for (val in listVal) { %> <div class="listContent"> <% if (valueNum == 0) { %> <%if(isIms) {%> <div><i class="<%=typeIconClzz%>"></i> <%=_ctx.appRes("UIBasicProfile.label." + subKey)%>: </div> <%} else { %> <div><%=_ctx.appRes("UIBasicProfile.label." + subKey)%>: </div> <%}%> <%} else { %> <div></div> <%} valueNum++; %> <div class="ellipsis" rel="tooltip" data-placement="top" title="" data-original-title="<%=val%>"><%=val%></div> </div> <% } } } } %> </div> </div> <%}%> <div class="line-bottom"><span></span></div> </div> </div> This template overrides the Profile Portlet by adding: - ``btn_show_contact``: a button to show the portlet's content. - ``btn_hide_contact``: a button to hide the portlet's content. - ``showFullContact()``: a Javascript function to handle when user clicks on the ``btn_show_contact`` button. - ``hideFullContact()``: a Javascript function to handle when user clicks on the ``btn_hide_contact`` button. - the code: :: if (!Utils.isOwner() && key.toString().equals("email")) continue;
to check if the viewer is not the profile page owner, then the email information will not be displayed.
Override
UIMiniConnectionsPortlet.gtmpl
as follows:<% import org.exoplatform.social.core.service.LinkProvider; import org.exoplatform.portal.webui.util.Util; import org.exoplatform.social.webui.Utils; import org.exoplatform.social.user.portlet.UserProfileHelper; //Load current connections of the user List profiles = uicomponent.loadPeoples(); int size = uicomponent.getAllSize(); uicomponent.initProfilePopup(); %> <!-- showing and hiding control buttons --> <button onclick="showConnection()" id="btn_show_connection">Show connections</button> <button onclick="hideConnection()" id="btn_hide_connection" style="display: none">Hide connections</button> <!-- javascript to show and hide user's connections --> <script type="text/javascript"> function showConnection(){ document.getElementById("$uicomponent.id").style.display = "block"; document.getElementById("btn_show_connection").style.display = "none"; document.getElementById("btn_hide_connection").style.display = "block"; } function hideConnection(){ document.getElementById("$uicomponent.id").style.display = "none"; document.getElementById("btn_show_connection").style.display = "block"; document.getElementById("btn_hide_connection").style.display = "none"; } </script> <div class="uiSocApplication uiMiniConnectionsPortlet" id="$uicomponent.id" style="display: none"> <h4 class="head-container"><%=_ctx.appRes("UIBasicProfile.label.Connections")%></h4> <% if(size > 0) { %> <!-- if having connections, loop through to print out --> <div class="borderContainer" id="borderMiniConnectionsPortlet"> <% for(profile in profiles) { %> <a href="<%=profile.getProfileURL()%>" class="avatarXSmall"> <img alt="<%=profile.getDisplayName()%>" src="<%=profile.getAvatarURL()%>"> </a> <% } %> <!-- Provide View all connections feature --> <div class="viewAllConnection"><a href="<%=LinkProvider.getBaseUri(null, null)%>/connections/network/<%=uicomponent.getCurrentRemoteId()%>"><%=_ctx.appRes("UIBasicProfile.label.ViewAll")%> (<%=size%>)</a></div> </div> <% } else { //if no connection and the user is the owner, provide Find new connection feature //if the user is not the owner, just print out the message String keyNoConnection = Utils.isOwner() ? "YouHaveNotConnections" : "UserHaveNotConnections"; String noConnectionCSS = Utils.isOwner() ? "noConnection" : ""; %> <div class="borderContainer $noConnectionCSS center"> <%=_ctx.appRes("UIBasicProfile.info." + keyNoConnection)%> <%if (Utils.isOwner()) { %> <div class="findConnection"><a href="<%=LinkProvider.getBaseUri(null, null)%>/connections/all-people/"><%=_ctx.appRes("UIBasicProfile.label.FindConnections")%></a></div> <%} %> </div> <% } %> </div> This template overrides the Connections User Portlet by adding: - ``btn_show_connection``: a button to show the portlet's content. - ``btn_hide_connection``: a button to hide the portlet's content. - ``showConnection()``: a Javascript function to handle when user clicks on the ``btn_show_connection`` button. - ``hideConnection()``: a Javascript function to handle when user clicks on the ``btn_hide_connection`` button.
Override
UIExperienceProfilePortlet.gtmpl
with:<% import org.exoplatform.social.core.service.LinkProvider; //Retrieve the user's information and check whether the user is the owner or not String aboutMe = uicomponent.getAboutMe(); boolean isOwner = uicomponent.isOwner(); List experienceData = uicomponent.getExperience(); def uiSocApplicationClzz = !isOwner && (experienceData.size() == 0) ? "" : "uiSocApplication"; %> <div class="<%=uiSocApplicationClzz%> uiExperienceProfilePortlet" id="$uicomponent.id"> <% //if the About me information of the user is not empty, print out if(aboutMe.length() > 0) { %> <h4 class="head-container"><%=_ctx.appRes("UIBasicProfile.label.AboutMe")%></h4> <!-- Add more description here --> <p>Quick description of the user</p> <div class="simpleBox aboutMe"><%=aboutMe%></div> <!-- if empty and the user is the owner, print out a message and provide Edit profile feature --> <% } else if(isOwner) { %> <div class="no-content center"> <div><%=_ctx.appRes("UIBasicProfile.info.HaveNotAbout")%></div> <button class="btn btn-primary" onclick="window.location.href=window.location.origin + '<%=LinkProvider.getBaseUri(null, null)%>/edit-profile/'"> <i class="uiIconEdit uiIconLightGray"></i> <%=_ctx.appRes("UIBasicProfile.action.EditProfile")%></button> </div> <% } //if having experience information, loop through to print out if(experienceData.size() > 0) { print("<h4 class=\"head-container\">" + _ctx.appRes("UIBasicProfile.label.Experience") + "</h4>"); print("<div class=\"simpleBox\"> "); for(experience in experienceData) { print("<div class=\"experience-container\"> "); String utilNow = experience.get(uicomponent.EXPERIENCES_IS_CURRENT); for(key in experience.keySet()) { if(uicomponent.EXPERIENCES_IS_CURRENT.equals(key)) { continue; } String label = _ctx.appRes("UIBasicProfile.label." + key); %> <div class="<%=key%> clearfix"><div class="labelName pull-left"><%=label%>:</div> <div rel="tooltip" data-placement="top" title="" data-original-title="<%=experience.get(key)%>" class="pull-left ellipsis"><%=experience.get(key)%> <%=(utilNow != null && "startDate".equals(key)) ? (" "+_ctx.appRes("UIBasicProfile.label.untilNow")) : "" %></div> </div> <% } print("</div>"); } print("</div>"); } %> </div> - This template overrides the Experience Profile Portlet with more description in the code: :: <p>Quick description of the user</p>
Override
UIRecentActivitiesPortlet.gtmpl
as follows:<% import org.exoplatform.social.webui.Utils; import org.exoplatform.portal.webui.util.Util; import org.exoplatform.social.core.service.LinkProvider; import org.exoplatform.social.user.portlet.UserProfileHelper; import org.exoplatform.social.user.portlet.RecentActivitiesHelper; //Retrieve the most recent activities of the user List activities = uicomponent.getRecentActivities(); %> <div class="uiSocApplication uiRecentActivitiesPortlet" id="$uicomponent.id"> <h4 class="head-container"><%=_ctx.appRes("UIBasicProfile.label.RecentActivities")%></h4> <!-- Additional description --> <p>The most recent activities of the user</p> <!-- Main content of the recent activities --> <div class="activityCont"> <% //no activity if(activities.size() == 0) { String keyNoActivities = Utils.isOwner() ? "YouHaveNotActivities" : "UserHaveNotActivities"; %> <div class="simpleBox noActivity center"><%=_ctx.appRes("UIBasicProfile.info." + keyNoActivities)%></div> <% //if having activities, loop through to print out } else { String activityURL = LinkProvider.getBaseUri(null, null) + "/activity?id="; for (activity in activities) { def profile = RecentActivitiesHelper.getOwnerActivityProfile(activity); String avatarURL = profile.getAvatarUrl(); String profileURL = profile.getUrl(); String displayName = profile.getFullName(); String activityTypeIcon = RecentActivitiesHelper.getActivityTypeIcon(activity); String link = RecentActivitiesHelper.getLink(activity); String linkTitle = RecentActivitiesHelper.getLinkTitle(activity); %> <!-- Build an activity stream for each activity--> <div class="activityStream uiDefaultActivity clearfix" id="Activity<%=activity.id%>"> <div class="activityTimeLine pull-left"> <div class="activityAvatar avatarCircle"> <a href="<%=profileURL%>"> <img alt="<%=displayName%>" src=" <%=((avatarURL == null || avatarURL.length() == 0) ? LinkProvider.PROFILE_DEFAULT_AVATAR_URL : avatarURL)%>"> </a> </div> <% if (activityTypeIcon != null && activityTypeIcon.length() > 0) { %> <div class="activityType"><span><i class="<%=activityTypeIcon%> uiIconSocWhite"></i></span></div> <% } %> </div> <!--end activityTimeLine--> <div class="boxContainer" id="boxContainer" onclick="window.open('<%=(activityURL + activity.id)%>', '_self')"> <div id="Content<%=activity.id%>" class="content"> <%if (link != null) { if (linkTitle != null) { %> <div class="status"><%=linkTitle%></div> <div class="link"><a href="javascript:void(0);" onclick="(function(evt){ evt.stopPropagation(); window.open('<%=link%>', '_blank');})(event)"><%=activity.getTitle()%></a></div> <% } else { %> <div><a href="javascript:void(0);" onclick="(function(evt){ evt.stopPropagation(); window.open('<%=link%>', '_self');})(event)"> <%=activity.getTitle()%></a></div> <% } } else {%> <div class="status"><%=activity.getTitle()%></div> <%} %> </div> </div> <!-- end boxContainer--> </div> <!-- end activityStream --> <% } //Provide view all activities feature String activityStreamURL = LinkProvider.getUserActivityUri(Utils.getOwnerIdentity(false).getRemoteId()); print("<div style=\"display: block;\" class=\"boxLoadMore\">"+ "<button class=\"btn\" style=\"width:100%;\" onclick=\"window.location.href='" + activityStreamURL + "'\">" + _ctx.appRes("UIBasicProfile.action.ViewAll") + "</button></div>"); uicomponent.initProfilePopup(); } %> </div> <% if (uicomponent.hasActivityBottomIcon && activities.size() != 0) { %> <div class="activityBottom" style="display: block;"><span></span></div> <% } %> </div> - This template overrides the Recent Activities Portlet with more description in the code: :: <p>The most recent activities of the user</p>
Create a jar file to register this
user-profile-extension.war
to portal container as in Portal extension. Then, edit theconfiguration.xml
file as follows:<?xml version="1.0" encoding="UTF-8"?> <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <!-- The full qualified name of the PortalContainerConfig --> <target-component>org.exoplatform.container.definition.PortalContainerConfig</target-component> <component-plugin> <!-- The name of the plugin --> <name>Change PortalContainer Definitions</name> <!-- The name of the method to call on the PortalContainerConfig in order to register the changes on the PortalContainerDefinitions --> <set-method>registerChangePlugin</set-method> <!-- The full qualified name of the PortalContainerDefinitionChangePlugin --> <type>org.exoplatform.container.definition.PortalContainerDefinitionChangePlugin</type> <priority>102</priority> <init-params> <value-param> <name>apply.default</name> <value>true</value> </value-param> <object-param> <name>change</name> <object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependenciesAfter"> <!-- The list of name of the dependencies to add --> <field name="dependencies"> <collection type="java.util.ArrayList"> <value> <!--The context name of the portal extension--> <string>user-profile-extension</string> </value> </collection> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
Copy these jar and war files into the corresponding deployment folders where you unpacked the eXo Platform installation.
Testing what you have customized
Start eXo Platform and you will see your new profile appear as follows:
Clicking on Show full contact information or Show connections button will expand the corresponding information panel. Note that if you are not this user, the email will not be displayed:
Wiki macro¶
eXo Platform uses XWiki as a Wiki engine, so you can develop and use macros in eXo Wiki completely following the XWiki approach.
If you have never tried using a macro before, you can quickly try eXo video-wiki-macro. Follow the project’s README and it will help you build, deploy and use the macro.
As said, the macro completely follows XWiki approach, so you can refer to their documentation for a start. In this tutorial, you write a new macro called “mailto”. When a user inserts the macro, he inputs a username from that your macro that retrieves an email contact and adds it inline. The source code can be found here.
The project structure
Create a Maven project with the structure like this:
Alternatively, you can generate a project and modify it using the XWiki’s macro-archetype 5.4.2.
The dependencies
In pom.xml
, you need dependencies of XWiki (version 5.4.2 for strong
compatibility) and eXo Social:
<dependency>
<groupId>org.xwiki.rendering</groupId>
<artifactId>xwiki-rendering-syntax-xwiki21</artifactId>
</dependency>
<dependency>
<groupId>org.xwiki.rendering</groupId>
<artifactId>xwiki-rendering-syntax-xhtml</artifactId>
</dependency>
<dependency>
<groupId>org.xwiki.rendering</groupId>
<artifactId>xwiki-rendering-transformation-macro</artifactId>
</dependency>
<dependency>
<groupId>org.exoplatform.social</groupId>
<artifactId>social-component-core</artifactId>
</dependency>
The MailtoMacro class
The macro class should extend
org.xwiki.rendering.macro.AbstractMacro
and implement the
execute
method. It must declare a parameter type so that XWiki takes
care the interface with users to get a username and passes it to your
method.
@Component("mailto")
public class MailtoMacro extends AbstractMacro<MailtoMacroParams> {
public MailtoMacro() {
super("mailto", "Add an email contact inline.", MailtoMacroParams.class);
}
public boolean supportsInlineMode() {
return true;
}
public List<Block> execute(MailtoMacroParams parameters,
String content,
MacroTransformationContext context) throws MacroExecutionException {
IdentityManager identityManager = (IdentityManager) PortalContainer.getInstance().getComponentInstanceOfType(IdentityManager.class);
try {
// Get user info.
Identity identity = identityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, parameters.getUsername(), false);
Profile profile = identity.getProfile();
String displayName = profile.getFullName(); //to be displayed.
String email = profile.getEmail(); //to be linked.
// Build the blocks.
RawBlock rawblock = new RawBlock(displayName, Syntax.XHTML_1_0);
LinkBlock linkblock = new LinkBlock(Arrays.<Block>asList(rawblock), new ResourceReference(email, ResourceType.MAILTO), true);
return Arrays.<Block>asList(linkblock);
} catch (Exception e) {
// In case the parameter is not a valid user id.
RawBlock rawblock = new RawBlock(parameters.getUsername()+"(?)", Syntax.XHTML_1_0);
return Arrays.<Block>asList(rawblock);
}
}
}
You provide the macro name, a description (users will see it) by the annotation and the constructor. You can also categorize your macro, see a code sample in the video-macro’s source code.
The MailtoMacroParams class
You need only one parameter - username that is mandatory. In the execute method, you use it to get a user profile (if it is invalid, the macro simply shows its raw value with a question (?) sign).
Pay attention to the annotations:
public class MailtoMacroParams {
/**
* The MailtoMacro expects a user name. If the user name is not valid, it appends a question sign to the user name.
*/
private String username;
public String getUsername() {
return username;
}
@PropertyDescription("Somebody's ID. His email will be added inline.")
@PropertyMandatory
public void setUsername(String username) {
this.username = username;
}
}
The components file
Finally, you declare the macro’s class name in
src/main/resources/META-INF/components.txt
, just with one line:
org.exoplatform.samples.xwiki.macro.MailtoMacro
Here is the picture of a Wiki page that uses the macros:
ExtensibleFilter mechanism¶
eXo Platform provides you with the ExtensibleFilter mechanism that allows you
to insert more filters from your own extension without touching the
web.xml
file.
In this section, you will be introduced how to create a filter that requires users to change password at the first login and when it is expired. The source code used in this tutorial is available here so that you can clone.
Our general project will be structured as below:
In which, the sub-projects include:
config: creates a jar file that declares the extension (war) as a portal dependency.
services: creates services that check if this is the first login of the current user or their current password has expired, and update their new password.
war: creates a war file that provides filter configuration files, locale resources as well as a form for changing password.
Now, follow the detailed steps:
Under pom.xml
Add the following dependencies to the pom.xml
file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>change-password-extension</artifactId>
<groupId>org.exoplatform.addons.change-password</groupId>
<version>1.1.x-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Change Password Extension</name>
<description>Change Password Extension</description>
<modules>
<module>config</module>
<module>war</module>
<module>services</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.exoplatform.platform</groupId>
<artifactId>platform</artifactId>
<version>4.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Under config folder
Create a
pom.xml
and aconfiguration.xml
file as below:Add the following information to
config/pom.xml
:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>change-password-extension</artifactId> <groupId>org.exoplatform.addons.change-password</groupId> <version>1.1.x-SNAPSHOT</version> </parent> <artifactId>change-password-extension-config</artifactId> <packaging>jar</packaging> <name>Change Password Extension Configuration</name> <description>Change Password Extension Configuration</description> </project>
Add the below configuration to
conf/configuration.xml
:<?xml version="1.0" encoding="UTF-8"?> <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <!-- The full qualified name of the PortalContainerConfig --> <target-component>org.exoplatform.container.definition.PortalContainerConfig</target-component> <component-plugin> <!-- The name of the plugin --> <name>Change PortalContainer Definitions</name> <!-- The name of the method to call on the PortalContainerConfig in order to register the changes on the PortalContainerDefinitions --> <set-method>registerChangePlugin</set-method> <!-- The full qualified name of the PortalContainerDefinitionChangePlugin --> <type>org.exoplatform.container.definition.PortalContainerDefinitionChangePlugin</type> <priority>102</priority> <init-params> <value-param> <name>apply.default</name> <value>true</value> </value-param> <object-param> <name>change</name> <object type="org.exoplatform.container.definition.PortalContainerDefinitionChange$AddDependenciesAfter"> <!-- The list of name of the dependencies to add --> <field name="dependencies"> <collection type="java.util.ArrayList"> <value> <string>change-password-extension</string> </value> </collection> </field> <!-- The name of the target dependency --> <field name="target"> <string>welcome-screens</string> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
Under services folder
This project structure is as follows:
Implement the class
ChangePasswordFilter.java
as follows:package org.exoplatform.changePassword; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import javax.servlet.FilterChain; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.exoplatform.container.ExoContainerContext; import org.exoplatform.container.PortalContainer; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.organization.OrganizationService; import org.exoplatform.services.organization.UserProfile; import org.exoplatform.services.organization.UserProfileHandler; import org.exoplatform.services.security.ConversationState; import org.exoplatform.services.security.Identity; import org.exoplatform.web.filter.Filter; public class ChangePasswordFilter implements Filter { private static Log logger = ExoLogger.getLogger(ChangePasswordFilter.class); private static final String CHANGE_PASSWORD_SERVLET_CTX = "/change-password-extension"; private static final String CHANGE_PASSWORD_SERVLET_URL = "/changePasswordView"; private static final String INITIAL_URI_PARAM_NAME = "initialURI"; private static final String REST_URI = ExoContainerContext.getCurrentContainer().getContext().getRestContextName(); public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest; HttpServletResponse httpServletResponse = (HttpServletResponse)servletResponse; OrganizationService organizationService = (OrganizationService)PortalContainer.getInstance().getComponentInstanceOfType(OrganizationService.class); //get current user Identity identity = ConversationState.getCurrent().getIdentity(); String userId = identity.getUserId(); boolean logged = false; boolean passwordChanged = false; boolean passwordExpired = false; if (!userId.equals("__anonim")) { logged = true; UserProfileHandler userProfileHandler = organizationService.getUserProfileHandler(); try { //get current user profile UserProfile userProfile = userProfileHandler.findUserProfileByName(userId); //get password changing status String changePassword = userProfile.getAttribute("changePassword"); //get expire password date String expirePasswordDate = userProfile.getAttribute("expirePasswordDate"); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MMM/yyyy"); Date today = new Date(); //check if the password has been changed if (changePassword != null && changePassword.equals("true")) { passwordChanged = true; } //check if the password has expired passwordExpired = today.after(simpleDateFormat.parse(expirePasswordDate)); } catch (Exception exception) { logger.error("User profile not found"); } } String requestUri = httpServletRequest.getRequestURI(); boolean isRestUri = requestUri.contains(REST_URI); if (!isRestUri && logged && (!passwordChanged || passwordExpired)) { String requestURI = httpServletRequest.getRequestURI(); String queryString = httpServletRequest.getQueryString(); if (queryString != null) { requestURI += "?" + queryString; } //get context for changing password and forward to password changing view servlet ServletContext servletContext = httpServletRequest.getSession().getServletContext().getContext(CHANGE_PASSWORD_SERVLET_CTX); String targetURI = (new StringBuilder()).append(CHANGE_PASSWORD_SERVLET_URL + "?" + INITIAL_URI_PARAM_NAME + "=").append(requestURI).toString(); servletContext.getRequestDispatcher(targetURI).forward(httpServletRequest, httpServletResponse); return; } filterChain.doFilter(servletRequest, servletResponse); } }
This filter checks the changePassword
attribute from the current
user profile as well as the date when his password will expire. If one
of these conditions is met, this user will be forwarded to the password
changing view servlet in the next step.
Implement the class
ChangePasswordViewServlet.java
as below:package org.exoplatform.changePassword; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class ChangePasswordViewServlet extends HttpServlet { private static final String CHANGE_PASSWORD_JSP_RESOURCE = "/WEB-INF/jsp/changePassword.jsp"; private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { doPost(httpServletRequest, httpServletResponse); } @Override protected void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { getServletContext().getRequestDispatcher(CHANGE_PASSWORD_JSP_RESOURCE).include(httpServletRequest, httpServletResponse); } }
This servlet simply calls the interface for changing password which is
created in this step.
After that, when user sends a post request from that interface, the
ChangePasswordActionServlet
servlet will be initialized. Go to next
step to implement this servlet.
Implement the class
ChangePasswordActionServlet.java
as below:package org.exoplatform.changePassword; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Calendar; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.exoplatform.container.PortalContainer; import org.exoplatform.container.component.RequestLifeCycle; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.organization.OrganizationService; import org.exoplatform.services.organization.User; import org.exoplatform.services.organization.UserProfile; import org.exoplatform.services.organization.UserProfileHandler; public class ChangePasswordActionServlet extends HttpServlet { private static Log logger = ExoLogger.getLogger(ChangePasswordActionServlet.class); private static final long serialVersionUID = 1L; private static final String CHANGE_PASSWORD_JSP_RESOURCE = "/WEB-INF/jsp/changePassword.jsp"; //define the duration (in month) when user password will expire private static final int PASSWORD_EXPIRATION_MONTHS_NUMBER = 6; @Override protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { //get the new password entered String newPassword = httpServletRequest.getParameter("newPassword"); //get the confirmation password String reNewPassword = httpServletRequest.getParameter("reNewPassword"); OrganizationService organizationService = (OrganizationService)PortalContainer.getInstance().getComponentInstanceOfType(OrganizationService.class); String userId = httpServletRequest.getRemoteUser(); try { RequestLifeCycle.begin(PortalContainer.getInstance()); User user = organizationService.getUserHandler().findUserByName(userId); //check if the two passwords entered are the same, if not redirect the current user to the password changing view if (!newPassword.equals(reNewPassword)) { httpServletRequest.setAttribute("notValidNewPassword", "true"); getServletContext().getRequestDispatcher(CHANGE_PASSWORD_JSP_RESOURCE).include(httpServletRequest, httpServletResponse); } else if (newPassword.length() < 6 || newPassword.length() > 30) { //check if the new password does not meet the requirement httpServletRequest.setAttribute("notCorrectNewPassword", "true"); getServletContext().getRequestDispatcher(CHANGE_PASSWORD_JSP_RESOURCE).include(httpServletRequest, httpServletResponse); } else { //do changing the current password into the new one and reset the related attributes UserProfileHandler userProfileHandler = organizationService.getUserProfileHandler(); UserProfile userProfile = userProfileHandler.findUserProfileByName(userId); userProfile.setAttribute("changePassword", "true"); Calendar calendar = Calendar.getInstance(); String passwordExpirationMonthsNumber = System.getProperty("password.expiration.months.number"); calendar.add(Calendar.MONTH, passwordExpirationMonthsNumber != null ? Integer.parseInt(passwordExpirationMonthsNumber) : PASSWORD_EXPIRATION_MONTHS_NUMBER); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MMM/yyyy"); userProfile.setAttribute("expirePasswordDate", simpleDateFormat.format(calendar.getTime())); userProfileHandler.saveUserProfile(userProfile, true); user.setPassword(newPassword); organizationService.getUserHandler().saveUser(user, true); //Redirect to the home page String redirectURI = "/portal/"; httpServletResponse.sendRedirect(redirectURI); } } catch (Exception exception) { logger.error("Password not changed"); } finally { RequestLifeCycle.end(); } } @Override protected void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { doGet(httpServletRequest, httpServletResponse); } }
This servlet verifies the new password whether it meets the minimum and
maximum length or not. If yes, the current password will be updated and
the related attributes including changePassword
and
expirePasswordDate
will also be reset. Note that the
expirePasswordDate
attribute will be calculated based on the
PASSWORD_EXPIRATION_MONTHS_NUMBER
constant.
Under war folder
This war folder will have the following structure:
In which, you will have locale resources in the
resources/locale/portal
folder, css rules for password changing view
in the css/changePassword.css
file and other configuration files. In
this section, you are going to look at the filter-configuration.xml
and changePassword.jsp
files. For the other files, you can check
from the cloned source code.
Add the below configuration to the
filter-configuration.xml
file:<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd http://www.exoplatform.org/xml/ns/kernel_1_2.xsd" xmlns="http://www.exoplatform.org/xml/ns/kernel_1_2.xsd"> <external-component-plugins> <target-component>org.exoplatform.web.filter.ExtensibleFilter</target-component> <component-plugin profiles="all"> <name>ChangePassword Filter</name> <set-method>addFilterDefinitions</set-method> <type>org.exoplatform.web.filter.FilterDefinitionPlugin</type> <init-params> <object-param> <name>Change Password Filter</name> <object type="org.exoplatform.web.filter.FilterDefinition"> <field name="filter"> <object type="org.exoplatform.changePassword.ChangePasswordFilter"/> </field> <field name="patterns"> <collection type="java.util.ArrayList" item-type="java.lang.String"> <value> <string>/*</string> </value> </collection> </field> </object> </object-param> </init-params> </component-plugin> </external-component-plugins> </configuration>
In which, the
patterns
field defines which URLs will be passed through this filter, in this case/*
means that all URLs are counted.
Add the following code to the
changePassword.jsp
file:<%@ page import="org.exoplatform.container.PortalContainer"%> <%@ page import="org.exoplatform.services.resources.ResourceBundleService"%> <%@ page import="java.util.ResourceBundle"%> <%@ page language="java" %> <% String contextPath = request.getContextPath() ; //get locale properties from the locale resource ResourceBundleService service = (ResourceBundleService) PortalContainer.getCurrentInstance(session.getServletContext()) .getComponentInstanceOfType(ResourceBundleService.class); ResourceBundle resourceBundle = service.getResourceBundle(service.getSharedResourceBundleNames(), request.getLocale()) ; String changePassword = resourceBundle.getString("changePassword.title"); String newPassword = resourceBundle.getString("changePassword.newPassword"); String reNewPassword = resourceBundle.getString("changePassword.reNewPassword"); String send = resourceBundle.getString("changePassword.send"); String notValidNewPasswordError = resourceBundle.getString("changePassword.notValidNewPasswordError"); String notCorrectNewPasswordError = resourceBundle.getString("changePassword.notCorrectNewPasswordError"); //get the password validation status String notValidNewPassword = (String) request.getAttribute("notValidNewPassword"); String notCorrectNewPassword = (String) request.getAttribute("notCorrectNewPassword"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html; charset=UTF-8"); %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Change password</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <link href="<%=contextPath%>/css/changePassword.css" rel="stylesheet" type="text/css"/> </head> <body class="change-password"> <div class="bg-light"><span></span></div> <div class="ui-change-password"> <div class="change-password-container"> <div class="change-password-header intro-box"> <div class="change-password-icon"><%=changePassword%></div> </div> <div class="change-password-content"> <div class="change-password-title"> <% //check if the new password is not valid if(notValidNewPassword == "true") { %> <div class="new-password-error"><i class="change-password-icon-error"></i><%=notValidNewPasswordError%></div> <% } //check if the password confirmation is not successful else if(notCorrectNewPassword == "true") { %> <div class="new-password-error"><i class="change-password-icon-error"></i><%=notCorrectNewPasswordError%></div> <% } %> </div> <div class="center-change-password-content"> <form id="changePasswordForm" name="changePasswordForm" action="<%=contextPath%>/changePassword" method="post"> <input id="newPassword" name="newPassword" type="password" placeholder="<%=newPassword%>" onblur="this.placeholder = <%=newPassword%>" onfocus="this.placeholder = ''"/> <input id="reNewPassword" name="reNewPassword" type="password" placeholder="<%=reNewPassword%>" onblur="this.placeholder = <%=reNewPassword%>" onfocus="this.placeholder = ''"/> <div id="changePasswordFormAction" class="change-password-button" onclick="submit();"> <button class="button" href="#"><%=send%></button> </div> </form> </div> </div> </div> </div> </body> </html>
Testing
Build your project with the
mvn clean install
command.Copy the generated jar and war files into the corresponding deployment folders and start eXo Platform.
Sign in with the root account and create a new user such as john.
Sign in with the john account, you will be required to change password at the first login.
Enter a new password and validate it, then click Send. If your new password is valid, you will be automatically redirected to the homepage, otherwise a message that says “The new password is not valid” will appear.