AdoptOS

Assistance with Open Source adoption

ECM

Audience Targeting 2.0 and LCS Will not be Compatible with Liferay Portal 7 Community

Liferay - Mon, 07/18/2016 - 18:11
Today I’m writing to announce that we are discontinuing support for Liferay Connected Services and Audience Targeting for Liferay Portal 7 Community. The main factor behind the decision was the lack of community adoption for both offerings.    Regarding Audience Targeting, there was low adoption by community members compared with most other Marketplace Plug-Ins. Surprisingly, Audience Targeting actually had more interest from Enterprise Subscribers, as you can see by the number of downloads here:      While our Community is an order of magnitude larger than our Subscriber base, the enterprise version of Audience Targeting as of today has 2567 downloads compared with 1951 downloads for the community version. We take that as a sign that most community members aren’t as interested in the functionality provided by the plug-in, which is usually used for Digital Marketing scenarios.    A similar train of thought holds true for Liferay Connected Services, a cloud-based set of tools for monitoring Liferay systems in production and applying patches and updates. After holding a public beta for over a year, we saw almost 3x uptake by enterprise users vs. community users:      Focusing on a single version of both products will also allow us to evolve Audience Targeting and LCS more quickly into mature products. When targeting and LCS become more feature-rich, we may re-visit the offerings and packaging them in a way that’s attractive to the community.    I’ve made two announcements in a row now that are negative from the Community's perspective and it probably looks like Liferay is moving away from our commitment to open source. But our focused approach to targeting and LCS will also free up resources for us to invest in other things that are more valuable to the community in the short term. We have been thinking about Community a lot here at Liferay and I’ll soon be announcing some of the ways we want to invest more in all of you and improve your overall experience with Liferay, so stay tuned.    Bryan Cheung 2016-07-18T23:11:27Z
Categories: CMS, ECM

OSGi Module Patching Guide

Liferay - Thu, 07/14/2016 - 14:43

I wanted to share some testing that I have been doing in terms of patching OSGi modules and adding the patched modules to your own Liferay installations.  One of the many benefits to modularity is that it allows for these kinds of changes to be made without having to recompile the whole platform from scratch.

Disclamer: This procedure is only meant for use by Liferay 7 CE.  Customers using Liferay DXP should continue to using the patching instructions provided in the offical documentation.

Overview

The example I would like to use is patching the JSP compiler module within Liferay 7 GA2 to fix an issue with using JSP Fragment Bundles on Windows:  https://issues.liferay.com/browse/LPS-66814

There is a few steps that we need to take in order to compile a patched version of the module and then add it to your system.  These steps can be used for patching pretty much any module in Liferay.  But keep in mind if you run into any issues with your installation its advisable to remove all patched modules and try to recreate the issue using a clean installation of Liferay before you report the issue.

The process for patching a module is really broke down into two main areas: Compiling the custom module and adding the custom module to your Liferay installation.

Compiling the custom module

In this section we will clone the repository where the module resides and compile the module.  In the case of this example, the fix has already been committed to the Github repository so no code changes are necessary.  We do need to modify the version of the module to create a snapshot version of the module we are replacing in Liferay 7.   Clone and Compile the module:
  1. Clone the com-liferay-portal-osgi-web repository from github: git clone https://github.com/liferay/com-liferay-portal-osgi-web.
  2. From within the bnd.bnd file in portal-osgi-web-servlet-jsp-compiler set the version to 2.0.5.IDENTIFIER where IDENTIFIER  is whatever you want it to be for example: 2.0.5.PATCH-1.
  3. Compile the module by running the following from within the portal-osgi-web-servlet-jsp-compiler directory: gradle assemble.
  4. This will create a new version of the bundle in: portal-osgi-web-servlet-jsp-compiler/build/libs
Deploying the custom module to Liferay With Liferay 7 all of the out of the box modules are provided inside Liferay LPKG files which are zip files.  In order to replace an out of the box module with a custom patched module the original module will need to be removed from the LPKG file and the patched module will need to be added to the LPKG file.   The LPKG file will have to modified in place.  Extracting the file, making the modifications and re-zipping the file didn't work for me.  The osgi/state directory will also have to be removed so that the system will pick up the new module inside the LPKG file.  Liferay should not be running when performing these steps.   Replacing the original module:
  1. The original com.liferay.portal.osgi.web.servlet.jsp.compiler.jar resides inside of the Liferay CE Static.lpkg file inside of osgi/marketplace.  Remove the old module:
    • Windows: Use a graphical zip file manager like 7zip and locate com.liferay.portal.osgi.web.servlet.jsp.compiler-2.0.5.jar and delete it
    • Mac and Linux: Use the zip command line and delete the file:  zip -d Liferay\ CE\ Static.lpkg com.liferay.portal.osgi.web.servlet.jsp.compiler-2.0.5.jar
  2. Add the new patched module to Liferay CE Static.lpkg:
    • Windows: Use 7zip to add the file to Liferay CE Static.lpkg by dragging and dropping the patched module into the open LPKG file
    • Mac and Linux: Use the zip command line to add the new file:  zip -j -r Liferay\ CE\ Static.lpkg /path/to/com-liferay-portal-osgi-web/portal-osgi-web-servlet-jsp-compiler/build/libs/com.liferay.portal.osgi.web.servlet.jsp.compiler-2.0.5.PATCH-1.jar
  3. Delete the osgi/state directory if it exists to force Liferay to read in the newly modified bundle.
  4. Start Liferay
  5. Once Liferay is started login to the Gogo shell with telnet localhost 11311 and type: lb | grep 2.0.5.PATCH-1.  This should show that your patched module is deployed and running.
Jamie Sammons 2016-07-14T19:43:00Z
Categories: CMS, ECM

Liferay 7, Service Builder and External Databases

Liferay - Wed, 07/13/2016 - 22:51

So I'm a long-time supporter of ServiceBuilder.  I saw its purpose way back on Liferay 4 and 5 and have championed it in the forums and here in my blog.

With the release of Liferay 7, ServiceBuilder has undergone a few changes mostly related to the OSGi modularization.  ServiceBuilder will now create two modules, one API module (comparable to the old service jar w/ the interfaces) and a service module (comparable to the service implementation that used to be part of a portlet).

But at it's core, it still does a lot of the same things.  The service.xml file defines all of the entities, you "buildService" (in gradle speak) to rebuild the generated code, consumers still use the API module and your implementation is encapsualted in the service module.  The generated code and the Liferay ServiceBuilder framework are built on top of Hibernate so all of the same Spring and Hibernate facets still apply.  All of the features used in the past are also supported, including custom SQL, DynamicQuery, custom Finders and even External Database support.

External Database support is still included for ServiceBuilder, but there are some restrictions and setup requirements that are necessary to make them work under Liferay 7.

Examples are a good way to work through the process, so I'm going to present a simple ServiceBuilder component that will be tracking logins in an HSQL database separate from the Liferay database.  That last part is obviously contrived since one would not want to go to HSQL for anything real, but you're free to substitute any supported DB for the platform you're targeting.

The Project

So I'll be using Gradle, JDK 1.8 and Liferay CE 7 GA2 for the project.  Here's the command to create the project:

blade create -t servicebuilder -p com.liferay.example.servicebuilder.extdb sb-extdb

This will create a ServiceBuilder project with two modules:

  • sb-extdb-api: The API module that consumers will depend on.
  • sb-extdb-service: The service implementation module.
The Entity

So the first thing we need to define is our entity.  The service.xml file is in the sb-extdb-service module, and here's what we'll start with:

<?xml version="1.0"?> <!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd"> <service-builder package-path="com.liferay.example.servicebuilder.extdb"> <!-- Define a namespace for our example --> <namespace>ExtDB</namespace> <!-- Define an entity for tracking login information. --> <entity name="UserLogin" uuid="false" local-service="true" remote-service="false" data-source="extDataSource" > <!-- session-factory="extSessionFactory" tx-manager="extTransactionManager" --> <!-- userId is our primary key. --> <column name="userId" type="long" primary="true" /> <!-- We'll track the date of last login --> <column name="lastLogin" type="Date" /> <!-- We'll track the total number of individual logins for the user --> <column name="totalLogins" type="long" /> <!-- Let's also track the longest time between logins --> <column name="longestTimeBetweenLogins" type="long" /> <!-- And we'll also track the shortest time between logins --> <column name="shortestTimeBetweenLogins" type="long" /> </entity> </service-builder>

This is a pretty simple entity for tracking user logins.  The user id will be the primary key and we'll track dates, times between logins as well as the user's total logins.

Just as in previous versions of Liferay, we must specify the external data source for our entity/entities.

ServiceBuilder will create and manage tables only for the Liferay DataBase.  ServiceBuilder will not manage the tables, indexes, etc. for any external databases.

In our particular example we're going to be wiring up to HSQL, so I've taken the steps to create the HSQL script file with the table definition as:

CREATE MEMORY TABLE PUBLIC.EXTDB_USERLOGIN( USERID BIGINT NOT NULL PRIMARY KEY, LASTLOGIN TIMESTAMP, TOTALLOGINS BIGINT, LONGESTTIMEBETWEENLOGINS BIGINT, SHORTESTTIMEBETWEENLOGINS BIGINT); The Service

The next thing we need to do is build the services.  In the sb-extdb-service directory, we'll need to build the services:

gradle buildService

Eventually we're going to build out our post login hook to manage this tracking, so we can guess that we could use a method to simplify the login tracking.  Here's the method that we'll add to UserLoginLocalServiceImpl.java:

public class UserLoginLocalServiceImpl extends UserLoginLocalServiceBaseImpl { private static final Log logger = LogFactoryUtil.getLog(UserLoginLocalServiceImpl.class); /** * updateUserLogin: Updates the user login record with the given info. * @param userId User who logged in. * @param loginDate Date when the user logged in. */ public void updateUserLogin(final long userId, final Date loginDate) { UserLogin login; // first try to get the existing record for the user login = fetchUserLogin(userId); if (login == null) { // user has never logged in before, need a new record if (logger.isDebugEnabled()) logger.debug("User " + userId + " has never logged in before."); // create a new record login = createUserLogin(userId); // update the login date login.setLastLogin(loginDate); // initialize the values login.setTotalLogins(1); login.setShortestTimeBetweenLogins(Long.MAX_VALUE); login.setLongestTimeBetweenLogins(0); // add the login addUserLogin(login); } else { // user has logged in before, just need to update record. // increment the logins count login.setTotalLogins(login.getTotalLogins() + 1); // determine the duration time between the current and last login long duration = loginDate.getTime() - login.getLastLogin().getTime(); // if this duration is longer than last, update the longest duration. if (duration > login.getLongestTimeBetweenLogins()) { login.setLongestTimeBetweenLogins(duration); } // if this duration is shorter than last, update the shortest duration. if (duration < login.getShortestTimeBetweenLogins()) { login.setShortestTimeBetweenLogins(duration); } // update the last login timestamp login.setLastLogin(loginDate); // update the record updateUserLogin(login); } } }

After adding the method, we'll need to build services again for the method to get into the API.

Defining The Data Source Beans

So we now need to define our data source beans for the external data source.  We'll create an XML file, ext-db-spring.xml, in the sb-extdb-service/src/main/resources/META-INF/spring directory.  When our module is loaded, the Spring files in this directory will get processed automatically into the module's Spring context.

<?xml version="1.0"?> <beans default-destroy-method="destroy" default-init-method="afterPropertiesSet" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd" > <!-- NOTE: Current restriction in LR7's handling of external data sources requires us to redefine the liferayDataSource bean in our spring configuration. The following beans define a new liferayDataSource based on the jdbc.ext. prefix in portal-ext.properties. --> <bean class="com.liferay.portal.dao.jdbc.spring.DataSourceFactoryBean" id="liferayDataSourceImpl"> <property name="propertyPrefix" value="jdbc.ext." /> </bean> <bean class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy" id="liferayDataSource"> <property name="targetDataSource" ref="liferayDataSourceImpl" /> </bean> <!-- So our entities are all appropriately tagged with the extDataSource, we'll alias the above liferayDataSource so it matches the entities. --> <alias alias="extDataSource" name="liferayDataSource" /> </beans>

These bean definitions are a big departure from the classic way of using an external data source.  Previously we would define separate data source beans from the Liferay Data Source beans, but under Liferay 7 we must redefine the Liferay Data Source to point at our external data source.

This has a couple of important side effects:

Only one data source can be used in a single ServiceBuilder module.  If you have three different external data sources, you must create three different ServiceBuilder modules, one for each data source. The normal Liferay transaction management limits the scope of transactions to the current module.  To manage transactions that cross ServiceBuilder modules, you must define and use XA transactions.

The last line, the alias line, this line defines a Spring alias for the liferayDataSource as your named data source in your service.xml file.

So, back to our example.  We're planning on writing our records into HSQL, so we need to add the properties to the portal-ext.properties for our external datasource connection:

# Connection details for the HSQL database jdbc.ext.driverClassName=org.hsqldb.jdbc.JDBCDriver jdbc.ext.url=jdbc:hsqldb:${liferay.home}/data/hypersonic/logins;hsqldb.write_delay=false jdbc.ext.username=sa jdbc.ext.password= The Post Login Hook

So we'll use blade to create the post login hook.  In the sb-extdb main directory, run blade to create the module:

blade create -p com.liferay.example.servicebuilder.extdb.event -t service -s com.liferay.portal.kernel.events.LifecycleAction sb-extdb-postlogin

Since blade doesn't know we're really adding a sub module, it has created a full standalone gradle project.  While not shown here, I modified a number of the gradle project files to make the postlogin module a submodule of the project.

We'll create the com.liferay.example.servicebuilder.extdb.event.UserLoginTrackerAction with the following details:

/** * class UserLoginTrackerAction: This is the post login hook to track user logins. * * @author dnebinger */ @Component( immediate = true, property = {"key=login.events.post"}, service = LifecycleAction.class ) public class UserLoginTrackerAction implements LifecycleAction { private static final Log logger = LogFactoryUtil.getLog(UserLoginTrackerAction.class); /** * processLifecycleEvent: Invoked when the registered event is triggered. * @param lifecycleEvent * @throws ActionException */ @Override public void processLifecycleEvent(LifecycleEvent lifecycleEvent) throws ActionException { // okay, we need the user login for the event User user = null; try { user = PortalUtil.getUser(lifecycleEvent.getRequest()); } catch (PortalException e) { logger.error("Error accessing login user: " + e.getMessage(), e); } if (user == null) { logger.warn("Could not find the logged in user, nothing to track."); return; } // we have the user, let's invoke the service getService().updateUserLogin(user.getUserId(), new Date()); // alternatively we could just use the local service util: // UserLoginLocalServiceUtil.updateUserLogin(user.getUserId(), new Date()); } /** * getService: Returns the user tracker service instance. * @return UserLoginLocalService The instance to use. */ public UserLoginLocalService getService() { return _serviceTracker.getService(); } // use the OSGi service tracker to get an instance of the service when available. private ServiceTracker<UserLoginLocalService, UserLoginLocalService> _serviceTracker = ServiceTrackerFactory.open(UserLoginLocalService.class); } Checkpoint: Testing

At this point we should be able to build and deploy the api module, the service module and the post login hook module.  We'll use the gradle command:

gradle build

In each of the submodules you'll find a build/libs directory where the bundle jars are.  Fire up your version of LR7CEGA2 (make sure the jdbc.ext properties are in portal-ext.properties file before starting) and put the jars in the $LIFERAY_HOME/deploy folder.  Liferay will pick them up and deploy them.

Drop into the gogo shell and check your modules to ensure they are started.

Log into the portal a few times and you should be able to find the database in the data directory and browse the records to see what it contains.

Conclusion

Using external data sources with Liferay 7's ServiceBuilder is still supported.  It's still a great tool for building a db-based OSGi module, still allows you to generate a bulk of the DB access code while encapsulating behind an API in a controlled manner.

We reviewed the new constraints on ServiceBuilder imposed by Liferay 7:

  • Only one (external) data source per Service Builder module.
  • The external data source objects, the tables, indexes, etc., must be manually managed.
  • For a transaction to span multiple Service Builder modules, XA transactions must be used.

You can find the GitHub project code for this blog here: https://github.com/dnebing/sb-extdb

David H Nebinger 2016-07-14T03:51:15Z
Categories: CMS, ECM

Mobile First with Liferay Screens

Liferay - Wed, 07/13/2016 - 15:26

 

Mobile First with Liferay Screens

 

Mobile has changed the world and will go on doing so for the foreseeable future. A stream of new mobile devices and operating systems is putting pressure on organizations to meet the high expectations of users, who expect a mobile digital experience that also matches their personal behavior, history and profile. Keeping up is not enough now mobile has become the dominant communication platform. With Liferay Screens, organizations can give themselves a head start. And keep it.

 

Digital experience
Open a site and view static content? This is not what modern users expect at all. Users are accustomed to literally being able to shape their digital world themselves. If they show interest in a subject, or their profile matches a particular group, then they expect content and functionality to be geared to this. Users count on the right access at the right moment, for example to relevant information, events and interesting downloads. In short: they want a personalized and consistent digital experience – on every device and in every application.

 

 

Native mobile app development tool
The Liferay Digital Experience Platform has been designed to achieve this digital experience. One component is the Liferay Screens mobile app development tool, which makes it possible to present functionality and data from the Liferay platform in a native mobile app. This means that we can directly make use of standard Liferay facilities, such as login or search, from an iOS app or an Android app. And that a form filled in and sent via the mobile app can also be found on the portal and vice versa.

Smartphone, laptop or PC – in this way, the content for all devices and applications is provided by the same Liferay platform. The result is the coherent and uniform user experience that today’s users demand.

 

Why native?
Can we also create a digital experience of this sort by means of a responsive site, for example? The answer is: yes. But native development is the only way to optimally satisfy the requirements of app users. This is a question of speed, and also, primarily, of user-friendliness. A mobile site or HTML5 or hybrid app just has a different 'feel' than a native app.

Unlike a hybrid or HTML5 app, a native app also gives full access to all components of the device. Interaction with other apps on the device is possible as well.

On a responsive site, navigation can be a tricky issue. The most relevant information is not always presented primarily. Liferay Screens works with a single source and a single administrator, just as a responsive site does, but the presentation can be geared for 100% to the device and the wishes of the user.

 

How Liferay Screens works
Liferay Screens works with reusable components, screenlets, which use the Liferay content and web services for native mobile apps. Screenlets can be compared with the plug-ins of the Liferay platform; they contain the functionality necessary for, for example, a list or detailed display of content from the CMS. This functionality is formed by four elements: View, Theme, Connector and Mobile SDK. By combining screenlets, we can bring together different functionalities in one flow, for example displaying content and entering comments or ratings.

 

 

The theming is reusable, too, and also simple to modify. This makes the development of follow-up native mobile apps based on Liferay Screens considerably faster and easier.

With Liferay Screens, we can provide access to information both from the Liferay platform and from other enterprise systems, and combine this in the app. The tool is open source and fully compatible with Android Studio and X-code. Naturally, the connection with Liferay gives a development advantage to Liferay developers.

 

More advantages of Liferay Screens

  • Optimum security The authentication in the app matches that of the platform. So users who access the data provided from the Liferay environment are always known.
  • Liferay permission system is available. The roles and permissions are the same for app and website.
  • Extensive possibilities for branding The Liferay Screens app gives complete control of the interface and the device. This makes branding easy to implement, both internally and externally.
  • Content available offline Users can obtain access offline to the information they need in their work.
  • Out-of-the-box connection to the Liferay server.
  • Standardized apps architecture.

 

 

Componence gives organizations a head start
Liferay Screens is an indispensable link in every mobile strategy based on Liferay. In combination with the standard functionalities of Liferay and suitable implementation of the platform, this tool makes it possible to provide a consistent inter-device user journey: from the desktop and the portal to the smartphone and the mobile app.

The strength of Liferay Screens is the reusability of components. Componence has already developed large numbers of these screenlets, with which we can completely eliminate the generally higher investment on native development. Our screenlets are at the service of organizations looking for a head start with native mobile apps that satisfy all the requirements and wishes of modern users.

 

View our native mobile apps
Would you like to find out more about Liferay Screens and about the app that Componence recently developed for healthcare provider Philadelphia? Our next blog will appear very soon on Liferay.com. It will focus on Phlink, our native mobile app based on Liferay Screens.

Interested? Then watch our video now and mark the afternoon of 13 October in your diary!

 

Maarten van Heiningen 2016-07-13T20:26:32Z
Categories: CMS, ECM

OSGi Module Dependencies

Liferay - Wed, 07/06/2016 - 22:55

It's going to happen.  At some point in your LR7 development, you're going to build a module which has runtime dependencies.  How do you satisfy those dependencies though?

In this brief blog entry I'll cover some of the options available...

So let's say you have a module which depends upon iText (and it's dependencies).  It doesn't really matter what your module is doing, but you have this dependency and now have to figure out how to satisfy it.

Option 1 - Make Them Global

This is probably the easiest option but is also probably the worst.  Any jars that are in the global class loader (Tomcat's lib and lib/ext, for example), are classes that can be accessed anywhere, including within the Liferay OSGi container.

But global jars have the typical global problems.  Not only do they need to be global, but all of their dependencies must also be global.  Also global classes are the only versions available, you can't really vary them to allow different consumers to leverage different versions.

Option 2 - Let OSGi Handle Them

This is the second easiest option, but it's likely to not work.  If you declare a runtime dependency in your module and if OSGi has a bundle that satisfies the dependency, it will be automatically available to your module.

This will work when you know the dependency can be satisfied, either because you're leveraging something the portal provides or you've actually deployed the dependency into the OSGi container (some jars also conveniently include OSGi bundle information and can be deployed directly into the container).

For our example, however, it is unlikely that iText will have already been deployed into OSGi as a module, so relying on OSGi to inject it may not end well.

Declaring the runtime dependency is going to be handled in your build.gradle file.  Here's a snippet for the iText runtime dependency:

runtime group: 'com.iowagie', name: 'itext', version: '1.4.8'

If iText (and it's dependencies) have been successfully deployed as an OSGi bundle, your runtime declaration will ensure it is available to your module.  If iText is not available, your module will not start and will report unsatisfied dependencies.

Option 3 - Make An Uber Module

Just like uber jars, uber modules will have all of the dependent classes exploded out of their original jars and are available within the module jar.

This is actually quite easy to do using Gradle and BND.

In your build.gradle file, you should declare your runtime dependencies just as you did for Option 2.

To make the uber module, you also need to include the resources in your bnd.bnd file:

Include-Resource: @itext-1.4.8.jar

So here you include the name of the dependent jar, usually you can see what it is when Gradle is downloading the dependency or by browsing your maven repository.

Note that you must also include any dependent jars in your include statement.  For example, iText 2.0.8 has dependencies on BouncyCastle mail and prov, so those would need to be added:

Include-Resource: @itext-2.0.8.jar,@bcmail-138.jar,@bcprov-138.jar

You may need to add these as runtime dependencies so Gradle will have them available for inclusion.

If you use a zip tool to crack open your module jar, you'll see that all of the individual jars have been exploded and all classes are in the jar.

Option 4 - Include the Jars in the Module

The last option is to include the jars in the module itself, not as an uber module, but just containing the jar files within the module jar.

Similar to option 2 and 3, you will declare your runtime dependencies in the build.gradle file.

The bulk of the work is going to be done in the bnd.bnd file.

First you need to define the Bundle-ClassPath attribute to include classes in the module jar but also the extra dependency jars.  In the example below, I'm indicating that my iText jar will be in a lib directory within the module jar:

Bundle-ClassPath:\ .,\ lib/itext.jar

Rather than use the Include-Resource header, we're going to use the -includeresource directive to pull the jars into the bundle:

-includeresource:\ lib/itext.jar=itext-1.4.8.jar

In this format we're saying that lib/itext.jar will be pulled in from itext-1.4.8.jar (which is one of our runtime dependencies so Gradle will have it available for the build).

This format also supports the use of wildcards so you can leave version selection to the build.gradle file.  Here's an example for including any version of commons-lang:

-includeresource:\ lib/itext.jar=itext-1.4.8.jar,\ lib/commons-lang.jar=commons-lang=[0-9]*.jar

If you use a zip tool to crack open your module jar, you'll find there are jars now in the bundle under the lib directory.

Conclusion

So which of these options should you choose?  As with all things Liferay, it depends.

The global option is easy as long as you don't need different versions of jars but have a lot of dependencies on the jar.  For example, if you had 20 different modules all dependent upon iText 1.4.8, global may be the best path with regards to runtime resource consumption.

Option 2 can be an easy solution if the dependent jar is also an OSGi bundle.  In this case you can allow for multiple versions and don't have to worry about bnd file editing.

Option 3 and 4 are going to be the most common route to choose however.  In both of these cases your dependencies are included within the module so the OSGi's class loader is not polluted with different versions of dependent jars.  They are also environment-agnostic; since the modules contain all of their dependencies, the environment does not need to be prepared prior to module deployment.

Personally I stick with Option 4 - uber jars will tend to step on each other when expanding the jars that contain a same path/file in each (usually xml or config info).  Option 4 doesn't suffer from these sorts of issues.

Enjoy!

David H Nebinger 2016-07-07T03:55:21Z
Categories: CMS, ECM

New Maven Archetypes for JSF Portlets

Liferay - Wed, 07/06/2016 - 14:45

The Liferay Faces team is working on production support for JSF portlets in Liferay Portal 7.0 and Liferay DXP. As part of this effort, we have developed some archetypes for use with Maven 3.

Note: At this time, the archetypes (and associated dependencies like Liferay Faces Bridge) are in SNAPSHOT status.

In order to utilize the archetypes, create a file in $HOME/.m2/settings.xml that contains the following:

<?xml version="1.0" encoding="UTF-8"?> <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"> <profiles> <profile> <id>liferay-faces-snapshots</id> <activation> <activeByDefault>true</activeByDefault> </activation> <repositories> <repository> <id>liferay-faces-snapshots</id> <url>https://oss.sonatype.org/content/repositories/snapshots</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> </profile> </profiles> </settings> For a plain JSF portlet, type the following at the command line: mvn archetype:generate \ -DgroupId=com.mycompany \ -DartifactId=com.mycompany.my.jsf.portlet \ -DarchetypeGroupId=com.liferay.faces.archetype \ -DarchetypeArtifactId=com.liferay.faces.archetype.jsf.portlet \ -DarchetypeVersion=5.0.0-SNAPSHOT \ -DinteractiveMode=false For a PrimeFaces portlet, type the following: mvn archetype:generate \ -DgroupId=com.mycompany \ -DartifactId=com.mycompany.my.primefaces.portlet \ -DarchetypeGroupId=com.liferay.faces.archetype \ -DarchetypeArtifactId=com.liferay.faces.archetype.primefaces.portlet \ -DarchetypeVersion=5.0.0-SNAPSHOT \ -DinteractiveMode=false For a Liferay Faces Alloy portlet, type the following: mvn archetype:generate \ -DgroupId=com.mycompany \ -DartifactId=com.mycompany.my.alloy.portlet \ -DarchetypeGroupId=com.liferay.faces.archetype \ -DarchetypeArtifactId=com.liferay.faces.archetype.alloy.portlet \ -DarchetypeVersion=5.0.0-SNAPSHOT \ -DinteractiveMode=false For an ICEfaces portlet, type the following: mvn archetype:generate \ -DgroupId=com.mycompany \ -DartifactId=com.mycompany.my.icefaces.portlet \ -DarchetypeGroupId=com.liferay.faces.archetype \ -DarchetypeArtifactId=com.liferay.faces.archetype.icefaces.portlet \ -DarchetypeVersion=5.0.0-SNAPSHOT \ -DinteractiveMode=false For a RichFaces portlet, type the following: mvn archetype:generate \ -DgroupId=com.mycompany \ -DartifactId=com.mycompany.my.richfaces.portlet \ -DarchetypeGroupId=com.liferay.faces.archetype \ -DarchetypeArtifactId=com.liferay.faces.archetype.richfaces.portlet \ -DarchetypeVersion=5.0.0-SNAPSHOT \ -DinteractiveMode=false Liferay IDE / Liferay Developer Studio If you are developing your JSF portlets with Eclipse for Java EE Developers, you can install the Liferay IDE plugins. Otherwise, if you are developing with Liferay Developer Studio, the plugins are installed by default. After your project is created with the mvn archetype:generate command, you can import the project into Eclipse using the following steps: 1. Click File -> Import ... 2. Expand the "Maven" category 3. Click on "Existing Maven Projects" and click Next
4. Enter the full directory path to your newly created project 5. Click Finish   In order to deploy the portlet, simply drag the project to the "Liferay 7" or "Liferay DXP" server in the Servers pane.   Neil Griffin 2016-07-06T19:45:13Z
Categories: CMS, ECM

The State of Maven Development in Liferay

Liferay - Tue, 07/05/2016 - 19:30

There's been some confusion over state of Maven support for Liferay. As the Product Manager for Developer Tools, I feel responsible for the confusion and felt the need to clear up any questions.

Will Liferay support Maven development for 6.1, 6.2, and 7.0?

Short answer, yes, most definitely!

In 6.1 and 6.2, we’ve been provided support through liferay-maven-support plugin. Support for this plugin will continue as long as we have users on 6.1 and 6.2. It is now in maintenence mode and will only receive critical bug fixes. We will be releasing fixes to 6.2.6-SNAPSHOT till the next release.

Maven Support in Liferay 7.0 is currently in a state of transition. I believe this is the cause of much of the confusion. Liferay 7.0 will no longer use liferay-maven-support plugin. In 7.0, different tasks will require different plugins. Service Builder has its own plugin; building SASS has its own plugin; etc.

Why did we decide to go this route?

The reason we decided to go in a different direction is because much of our tooling had previously resided inside portal-impl.jar. In 7.0, many of the tools were pulled out and live on their own. This gave us a new opportunity for a fresh start. We decided to turn each of these tools into its own Maven plugin. This means anytime there is a new release for any tooling, a new plugin will be released as they are bundled together.

It also allows us to decouple the archetypes from the plugin. Previously they were released together and it caused some issues when we wanted to release one without the other.

What are your future plans for Maven?

As I said for the maven-support-plugin is now in bug fix mode. No more new features will be added. We will continue fixing bugs. We also will not be releasing any new archetypes.

In 7.0, there are still some features missing that were available in maven-support-plugin. We are currently working on theme building. Archetypes are also being worked on right now. We are also working on a simple maven developer guide.

What is the current status? Is it possible to use Maven for a new project with Liferay 7?

For 6.1 and 6.2, nothing has changed. Continue to use the liferay-maven-support plugin.

For 7.0, you can absolutely use Maven to start your next project. If you want to see samples of the new plugins in use take a look at Maven Blade Samples. Archetypes aren’t ready yet so use the samples if you need a starting point. The only thing you can not do yet is to create a theme using only maven. We recommend you take a look at the new theme tools that leverage node.js if you can. It provides much more than a purely maven solution could ever provide.

Is there any documentation?

Not yet. As mentioned before, we are working on a Maven development guide. For now, I recommend you use Maven Blade Samples as a guide.

David Truong 2016-07-06T00:30:38Z
Categories: CMS, ECM

Searching for JPA entities in Liferay

Liferay - Thu, 06/30/2016 - 15:32

So you want to search for your custom JPA entities in Liferay? Quite some documentation is already available but it is somewhat scattered over several blogposts and articles. With this blogpost I would like to give you a summary of everything you need to do, or more precisely everything we did, to make custom JPA entities searchable in the standard Liferay Search Portlet.

The requirement

One of our customers wanted the search results to not only contain the default Liferay objects like articles, documents, etc... But also some of his own custom domain objects. We had created a custom lightweight Support Ticket framework with a set of limited functionalities. It allowed a user to create a ticket and post comments to it. The additional requirement was that a user had to be able to also search for these tickets based on title, description and... oh yeah the comments too. In this blog I will use a very simplified version of this domain that shows all the steps necessary to make a JPA entity searchable. The domain only has 1 JPA entity, SupportTicket, together with a repository and service. There is also a simple portlet that can be used to create and show instances of this entity. So let's go on a journey, a quest you might even call it, to enable the search functionality for your custom JPA entities. Eventually you will get to the end point where you will use the Liferay Search Portlet to search and find your own JPA entities. You can also create your own custom search (taking customization to the max!) but that is outside the scope of this blog post. In the resources section however you can find links for more information on this matter. In a vanilla Liferay 6.2 the result should look like this:

Enter the Indexer

Liferay’s search & indexing functionality is provided by Apache Lucene, a Java search framework. It converts searchable entities into documents. Documents are custom objects that correspond to searchable entities. Usually performing full text searches against an index is much faster than searching for entities in a database. You can find more information about Lucene on its own website. To start you will need to create an indexer class. This class will hold the information required to transform your JPA entity into a Lucene Document. Next you will need to register this Indexer in the portlet so that the Liferay framework knows about it and can use it when necessary.

Creating the Indexer

I already made clear that the Indexer class is responsible for creating the Lucene documents for your JPA entity. It is important to know that this is where you can decide what fields of your entity will get indexed in order to be searchable. You can also specify if this needs to be a term or a phrase. And of course there are some other settings you can implement such as the name of the associated portlet, permissions, ... Have a look at the Sources for blog posts with more information. Liferay provides you with a default implementation named BaseIndexer as an easy start. This abstract class should be extended and the required methods implemented. Enough of the theoretical stuff, let's get started. As soon as you extend the BaseIndexer, you will need to overwrite the following methods. I'm not going to go into the details of all the implementations, I refer you to the Github project. Mostly the implementation will depend on your own requirements and domain.

  • doDelete: delete the document that corresponds to the object parameter
  • doGetDocument: specify which fields the Lucene Index will contain
  • doGetSummary: used to show a summary of the document in the search results
  • doReindex (3 times): will be called when the index is updated
  • getPortletId (2 times): to be able to narrow searches for documents created in a certain portlet instance (hence it needs to be the full portlet Id)
  • getClassNames: the FQCN of the entities this indexer can be used for

Something important to notice in this class is the difference between document.addKeyword and document.addText in the doGetDocument method.

  • addKeyword: adds a single-valued field, with only one term
  • addText: adds a multi-valued field, with multiple terms separated by a white space

The doGetDocument method is the most important piece of code in the TicketIndexer. In this method an incoming Object is transformed into an outgoing (Lucene) Document. The object is of type Ticket as the Indexer has been registered to be used for objects of that type. This registration occurs in the getClassNames method. A new Document object holding the necessary searchable fields can be created from this Ticket. Notice that all the Keyword/Text fields are already predefined in Liferay so you can use these constants. This is quite handy but you can always provide your own names.

@Override protected Document doGetDocument(Object obj) throws Exception { Ticket ticket = (Ticket) obj; List<TicketComment> comments = BeanLocator.getBean(TicketCommentService.class).getComments(ticket); Document document = new DocumentImpl(); document.addUID(PORTLET_ID, ticket.getId()); document.addKeyword(Field.COMPANY_ID, PortalUtil.getDefaultCompanyId()); document.addKeyword(Field.ENTRY_CLASS_NAME, TICKET_CLASS_NAME); document.addKeyword(Field.ENTRY_CLASS_PK, ticket.getId()); document.addKeyword(Field.PORTLET_ID, PORTLET_ID); document.addText(Field.TITLE, ticket.getSubject()); document.addText(Field.CONTENT, ticket.getDescription()); document.addText(Field.COMMENTS, Collections2.transform(comments, getComments()).toArray(new String[] {})); return document;  } Registering the Indexer

Once the Indexer is created, you will need to register it in the Liferay platform. This is a very simple action and it is completed by adding the following line into your portlet’s liferay-portlet.xml (check the DTD for the exact location).

<indexer-class>FQCN of your custom Indexer</indexer-class>

Once your portlet is redeployed Liferay will automatically register your indexer into its framework.

Tip Just by registering your indexer, existing entities won’t be indexed. As a Liferay admin however, you can trigger a reindex from the Control Panel. Just go to Configuration > Server Administration > Resources (tab) and locate the Execute button next to the Reindex all search indexes entry. Be advised that if you already have a large index (due to web content, documents, …) this may take a while. Info You can use some index inspection tool like Luke to inspect and search through your index. If your system contains at least one instance of your custom JPA entity you should see those pop up in Luke after doing a reindex, which means your Indexer implementation did its job. Info If you are eager to know whether or not your current progress has had any impact, you can already update the Liferay Search Portlet as described in Executing the Search. Using the indexer programmatically

No doubt it is great that your existing entities are now indexed. But you don’t want to trigger this indexing yourself manually, do you? By all means you don’t, you want it to happen automagically. Or at least programmatically. That is why you are reading this blog post anyway. So how about actually using the indexer yourself in the creation of a support ticket? Actually you want to use your newly created Indexer at the right time. But what is the right time? Well, right after a new entity is created. Or updated. Or even deleted. So you will need to use your Indexer at these moments, using the code below. We used the nullSafeGetIndexer as it returns a dummy indexer when no indexer can be found for the class, contrary to the default getIndexer() which returns null.

Indexer indexer = IndexerRegistryUtil.nullSafeGetIndexer(Ticket.class.getName()); indexer.reindex(ticket);

This code instructs your indexer, which you've registered with the FQCN of your entity, to reindex any added or updated entities. It delegates to the doReindex methods you have implemented in your own Indexer. When an entity is deleted its corresponding document should not show up in any search results anymore. You will need to call the code below when you are deleting your entity.

Indexer indexer = IndexerRegistryUtil.nullSafeGetIndexer(Ticket.class.getName()); indexer.delete(ticket);

Redeploying your portlet should do the trick of having each new entity being registered as a Document in the index as well. Go and try it out (if you have a create option of course).

Executing the Search

In this blog post Liferay's Search Portlet will be used to search and find your custom entities. You can always create a custom search portlet as well, check the Sources section for articles on how to achieve that. But for our customer we decided that integrating the custom JPA entity into Liferay's Search Portlet was the best solution as other content, e.g. Web Content, should also be searchable. By doing this the client had a nice integration with the default Liferay functionality. The Liferay Search Portlet will need to be updated to also take into account your entity by adding the portlet to a page and adjusting its configuration. But first check out the portlets normal behaviour. As an admin, add the portlet to a page of your choice. Search for the value of one of the earlier defined searchable fields in the Indexer and hit Search. Notice that there are no search results found and that this is clearly communicated.

Now go to the Configuration panel of the portlet, select Advanced and add the FQCN of your entity between quotes in the asset_entries list. Keep in mind the comma separation.

{ "displayStyle": "asset_entries", "fieldName": "entryClassName", "static": false, "data": { "frequencyThreshold": 1, "values": [ "com.liferay.portal.model.User", "com.liferay.portlet.bookmarks.model.BookmarksEntry", "com.liferay.portlet.bookmarks.model.BookmarksFolder", "com.liferay.portlet.blogs.model.BlogsEntry", "com.liferay.portlet.documentlibrary.model.DLFileEntry", "com.liferay.portlet.documentlibrary.model.DLFolder", "com.liferay.portlet.journal.model.JournalArticle", "com.liferay.portlet.journal.model.JournalFolder", "com.liferay.portlet.messageboards.model.MBMessage", "com.liferay.portlet.wiki.model.WikiPage", "be.olaertskoen.blog.search.Ticket" ] }, "weight": 1.5, "className": "com.liferay.portal.kernel.search.facet.AssetEntriesFacet", "label": "asset-type", "order": "OrderHitsDesc" },

Hit Save, close the panel and perform the same search again. Notice that the search result page has changed. It looks like the portlet found something but doesn't really know how to show it. Oh me oh my...

Probably this is not exactly what you wanted. Hell, it should be nothing you ever wanted! What use was all of this? Well, it was the preparation for what is to follow. If you followed the hints or tip above, you already know the indexer has done its work. Now it is time to actually use its results.

How about some usable search results?

You now know that your entities can be found but that the search results look nothing like you ever dreamed of. Next up you are going to get that party started and you should come to an end of this search functionality quest. As you probably know, or not if you are new to the game, but Liferay uses their Asset framework extensively. Not only in the Asset Publisher or for all of their web content, but also for their search results. Yes, that is right: the results in the search portlet are rendered using Assets. Therefor you will also need to create an Asset Renderer for your JPA entities. And more or less consistently you will need to register it as well. At last you will need to use it one way or another.

Creating an Asset Renderer

Similar as to the Indexer there is a base class you can easily extend, BaseAssetRenderer, to implement your own version. This class will give you a bunch of methods you will need to overwrite. One if them is to provide a summary for your entity, another is for the title. Two other important methods are render and getUrlViewInContext. The first one, render, will return a String with the url leading to the full content of your entity. It will use an Asset Publisher on a hidden page created for the Search Portlet. In our project we have an entire page reserved for this so we need to return the url of that detail page (here the page to edit the ticket).

@Override public String render(RenderRequest renderRequest, RenderResponse renderResponse, String template) throws Exception { return TicketDetailFriendlyUrl.create(ticket.getId()); }

The second method, getUrlViewInContext, will be used by the Search Portlet to render the asset in context. This is an option you can activate in the Search Portlet. The result is that the Asset Entry is shown in its own context unlike the previous where the entry is shown in an Asset Publisher. This is actually the default setting and according to me the nicest solution.

@Override public String getURLViewInContext(LiferayPortletRequest liferayPortletRequest, LiferayPortletResponse liferayPortletResponse, String noSuchEntryRedirect) throws Exception { ThemeDisplay themeDisplay = (ThemeDisplay) liferayPortletRequest.getAttribute(WebKeys.THEME_DISPLAY); Group group = themeDisplay.getScopeGroup(); boolean isPrivate = themeDisplay.getLayout().isPrivateLayout(); String groupUrl = PortalUtil.getGroupFriendlyURL(group, isPrivate, themeDisplay); return groupUrl + TicketDetailFriendlyUrl.create(ticket.getId()); }

Fabricating the Asset Renderer

For assets there is actually a required Factory to be used for the AssetRenderers. But again this is easy to achieve as Liferay provides a BaseAssetRendererFactory for you to extend. It contains three methods: one for the type, one for the class name (both methods for our entity class of course) and one method that returns an AssetRenderer. In that method you will need to create a new instance of your entity based on the incoming id and type. The latter was not required in our case as the renderer is only registered for Objects of type Ticket. @Override public AssetRenderer getAssetRenderer(long classPK, int type) throws PortalException, SystemException { TicketService ticketService = BeanLocator.getBean(TicketService.class); Ticket ticket = ticketService.getTicket(classPK); return new TicketAssetRenderer(ticket); } Registering the AssetRendererFactory

Unlike the Indexer you are not going to register the AssetRenderer but the AssetRendererFactory in your liferay-portlet.xml. Why else would you have created that class, right? It is easily performed by adding an asset-renderer-factory tag, containing the factory's FQCN, just before the instanceable tag.

<asset-renderer-factory>be.olaertskoen.blog.search.tickets.renderer.TicketAssetRendererFactory</asset-renderer-factory> Warning To actually get the portlet started, you will need to add a non-empty portlet.properties into the resources folder. We just added a property plugin.package.name.

If you redeploy your portlet, you will again see some changes in the search results.

Wait, where is the title? Where is the description? You coded that in the AssetRenderer, didn't you! Why are you not seeing any actual result? Why is the portal footer (that Powered By Liferay statement) suddenly shown in the search results? Well, Liferay would like to render an asset, but actually there is no asset. So it just can't render an Asset. If you would open your servers log, you will see an error message like this:

NoSuchEntryException: No AssetEntry exists with the key {classNameId=10458, classPK=1}

While you already had existing entities there were never any AssetEntries created for them unless you already implemented the Asset framework for some other reason. Hence you will need to use that framework into your entity creation process as well.

Creating Assets

You will need to adjust the add, update and delete code of your custom entities. Whenever a new entity is created, an AssetEntry needs to be created as well. When the entity is updated, the corresponding AssetEntry should be updated as well. And when an entity is deleted the corresponding AssetEntry should be deleted as well. No need for orphaned database entries! As you know, Liferay provides you with lots of LocalServices to use their framework. This is also the case for the Asset framework. When you create an instance of your entity you can use the AssetEntryLocalServiceUtil to create the corresponding AssetEntry using the following code:

String ticketFriendlyUrl = TicketDetailFriendlyUrl.create(ticket.getId()); Long userId = Long.valueOf(getExternalContext().getUserPrincipal().getName()); ThemeDisplay themeDisplay = getThemeDisplay(); Long groupId = themeDisplay.getScopeGroupId(); AssetEntryLocalServiceUtil.updateEntry(userId, groupId, ticket.getCreateDate(), ticket.getCreateDate(), Ticket.class.getName(), ticket.getId(), String.valueOf(ticket.getId()), 0, new long[] {}, new String[] {}, true, null, null, null, "text/html", ticket.getSubject(), ticket.getDescription(), ticket.getDescription(), ticketFriendlyUrl, null, 0, 0, null, false);

It is usually a good idea to do this right before the code you added to create the index entry for your entity. Now after you redeploy, you will need to create a new instance of your custom entity. For the existing ones there still are no AssetEntries present in Liferay. But when you create a new instance an AssetEntry will be created automagically (well at least we magicians now know the programmatics behind it). So go ahead and create a new instance, you will probably already have a portlet for that.

Alert As written earlier you will need to include similar code in your update and delete code for your entities. In this blog post and example portlet these have been left out.

Next try and search for it using one of the properties you declared in the Indexer class and… Tadaa!

Congratulations! You now have a good looking search result. Hm... Wait, it is not really 100%... Why is the FQCN shown as type of the entity? That does not look so nice! And it is even shown twice! Merde, will this journey never end?

Naming your entities

Be sure, our quest will soon come to an end. To provide your entities with a nice name you can again use Liferay's framework (how many rabbits are there in that hat?). All you need to do is add a translation for it, simple as that. If you look closely it is not really the FQCN of your custom entity that is shown in the Search Portlet, it is prepended with model.resource. You can add that entire output into a language hook as the key and provide it with a value (Superentity, Batentity, Spiderentity or whatever you want to name it).

model.resource.be.olaertskoen.blog.search.Ticket=Support Ticket

Deploy this language hook and reload your previous search.

Finally, it’s over

Isn’t that nice! You are using Liferay's Search Portlet to search for your own custom entities! What a journey that has been. But our customer was very satisfied with this functionality and I hope yours will be too.

With this you now know how to enable a full fledged search option for your custom JPA entities in Liferay. Just as a reminder, here is a short overview of the steps you took.

  • You started out with creating, registering and using an Indexer class that transforms your custom entities into Lucene Documents.
  • Next you configured the default Liferay Search Portlet to also take your custom JPA entities into account in the search queries. This was an easy configuration in the Portlet itself.
  • Last but not least you created, registered and used an AssetRenderer to nicely render the AssetEntry for your custom JPA entity in the Search Portlet. In this step you also added code to create and maintain that AssetEntry according to the lifecycle of your own entities.
Those three steps are all it takes to enable the search functionality for your custom JPA entities created in Liferay. Enjoy! You can find all the example code for this post in my Github project. Sources As mentioned at the start of this blog post, there are several articles and blog posts concerning search and what is required. But we needed to combine several of them to full enable all the features as described in this blog post. Below is a list of the Liferay sources we used. They are still worth reading if you want to know more on what lies behind the scenes or if you need to implement some other specific features.   Koen Olaerts 2016-06-30T20:32:02Z
Categories: CMS, ECM

Fun with Generic Content Templates

Liferay - Wed, 06/29/2016 - 01:14
I came across the idea of a generic template recently, and put it to good use.  If you don't know what I mean by generic template, let me clear that up right away.   A generic template is really just a content template that is not tied to a structure. The point of it is that you can sparate your template code as you see fit, including the generic templates in your main one. All you need to do is add this line of (velocity markup) code in your template. #parse ("$templatesPath/12345") where 12345 is the template key of your generic template. That's it. All the code in your generic template gets pulled into your main template and treated as one.   There! You know what I mean by generic templates now. So, let's talk about the fun I had with them. I've come to be used to velocity, so the sample code below is all vm.   Here's my main template. <h3>$txtPoemTitle.getData()</h3> <div> $htmlVerse.getData() </div> <div> $htmlAbout.getData() </div>   At a glance, you can tell I am displaying three fields:
  • a title
  • a poem (rich text)
  • a few comments about the poem (rich text)

 

Contrived requirement #1: Style it After one minute of frenzied googling, I have this: <style> .poem-body {     float: left;     font-size:16px;     color: #989898;     background-color: #B7E2F0;     border: solid 1px #B7E2F0;     border-radius: 5px/5px;     -webkit-box-shadow:0 23px 17px 0 #B7E2F0;     -moz-box-shadow:0 23px 17px 0 #B7E2F0;     box-shadow: 0 23px 17px 0 #B7E2F0;     }   .about-poem {     float: left;     margin:15px;     font-size: 16px;     font-style:italic;     background-color: #efefef;     color: #555555; } </style>

<h3>$txtPoemTitle.getData()</h3> <div class="poem-body"> $htmlVerse.getData() </div> <div class="about-poem"> $htmlAbout.getData() </div>

Calm down! Ugly, inefficient, but as I said, contrived. I'm just trying to make a point here.   All good. Now, let's move the styling into its own template - a generic template - one without a structure association. Now the main template looks like this:   #parse ("$templatesPath/73906") <h3>$txtPoemTitle.getData()</h3> <div class="poem-body"> $htmlVerse.getData() </div> <div class="about-poem"> $htmlAbout.getData() </div>   73906 is indeed the template key of my generic template, as shown below.       Contrived requirement #2: Create an alternate style for our poem template. A fiery red sonnet style. It looks very similar to the cool blue verse style, just some different colors. <style> .poem-body {     float: left;     font-size:16px;     color: #ffffff;     background-color: #CC0033;     border: solid 1px #CC0033;     border-radius: 5px/5px;     -webkit-box-shadow:0 23px 17px 0 #CC0033;     -moz-box-shadow:0 23px 17px 0 #CC0033;     box-shadow: 0 23px 17px 0 #CC0033;     }   .about-poem {     float: left;     margin:15px;     font-size: 16px;     font-style:bold;     background-color: #efefef;     color: #555555; } </style>   Now, I can simply change my generic template include in my main template as below to reference the alternate template. #parse ("$templatesPath/73945") <h3>$txtPoemTitle.getData()</h3> <div class="poem-body"> $htmlVerse.getData() </div> <div class="about-poem"> $htmlAbout.getData() </div>   Contrived requirement #3: Let the user pick which style to apply This is slightly more involved. I modify my poem structure to have, in addition to the original three fields, a select field named Mood with three options, as shown below. Take note of the values of those options.   Alright! Now over to my main template to use the value from this field. #parse ("$templatesPath/$selectMood.getData()") <h3>$txtPoemTitle.getData()</h3> <div class="poem-body"> $htmlVerse.getData() </div> <div class="about-poem"> $htmlAbout.getData() </div>   And we're done. Here's my content with Mood options. The screenshots that follow show the rendered results.   The Content Item:   Selecting Cool Blue Verse:   Selecting Fiery Red Sonnet:   In Conclusion If you don't see this as useful here, you're probably thinking: why go to all this trouble instead of creating an alternate content template altogether and just use that? Well, let me conclude by highlighting what we accomplished.
  1. Separation of code. We separated the template code between a main template and one or more generic templates. Sure, we just did css in the above example, but these are first clas velcity templates; they could have anything a velocity template can have - css, html, javascript, server-side calls into Liferay's universe of APIs. Power!
  2. Reuse. DRY principle and all. Each generic template is now usable in other main templates.
  3. User-empowerment. By adding a select box to the structure, we've now given the user the ability to switch the generic template that gets used. This makes for some useful indirection. 

Quick FYI on template keys: they are maintained through a LAR export/import, just in case you were wondering. 

 

Happy Fourth, America! To the rest of the world, I hear Jeff Goldblum will be peddling USB drives on an alien mothership once again. If you don't get that reference, consider yourself blessed and enjoy your July anyhow. Javeed Chida 2016-06-29T06:14:27Z
Categories: CMS, ECM

Digital Transformation: Evaluating Costs

Liferay - Fri, 06/24/2016 - 19:54

Having completed a few digital transformation projects in federal, state and local government, we would like to share a few insights.

Digital transformation in government is considered to be a major undertaking. We have seen serious moves in this area in the UK and it is brewing here in Australia with the establishment of the Digital Transformation Office.

Existing technology is part of the transformation agenda, and in our experience, is misaligned with the new, technology driven, real-time connected citizen. People nowadays prefer to do everything online, even better, on their mobile phones. They expect high quality digital experiences. What do they often find when they come to deal with the government? … .

The old friend – PDF. They have to download, print out and fill it in. On many occasions they also have to download instructions or a whole guide book to understand how to complete a form. Then of course they have to put a stamp on an envelope and post it or drive to the agency and stand in line to hand it over, wasting time and money. But that is not the end of it. After all the trouble they still might receive a letter, email or call informing them that some important information is missing and, in the best-case scenario, they will have to do it all over again, or, in the worst, that their applications have been rejected and they lost their fees.

In Australia, councils (local government agency) have 120-150 various application forms – registrations, permits, complaints – and in majority of cases only 5-10 of them have been migrated online.

The government costs associated with processing paper forms are significant. The labor costs start piling up from the moment a clerk opens an envelope containing a paper form (mail-processing costs). The form is screened for obvious mistakes and omissions and applicant is notified by mail, phone call or email (more costs). Data from the form has to be entered into an internal system (data-entry costs). The paper form is either stored (filing and storage costs) or scanned and uploaded into digital storage (scanning costs). To answer enquiries, help desks or call centers are created. A significant part of the enquiries consists of people asking questions about how to fill in a form.

Traditional channels by which government agencies communicate with the citizens, such as face-to-face, telephone and mail, are costly as they require time and resources. Digital transaction, on the other hand, is more than 50 times cheaper than face-to-face transaction, 40 times cheaper than communication over the phone and more than 6 times cheaper than using email. It makes sense to replace traditional channels, where possible, with digital options.

There are many benefits of moving paper-based transactions online, such as saving time and money for customers, raising their satisfaction and trust, and improving the ability for government employees to access data remotely. The more transactions completed online the higher the level of productivity of delivering public services, and the costs are significantly reduced.

We have created a simple online calculator which allows to estimate the Payback Period of ICT expenditure based on complexity of existing business processes and associated costs. It is located at http://samples.fifthocean.com.au/payback-calculator. This calculator uses real figures based on our implementation experiences on all levels of government and businesses.

The calculator can help organizations to identify and plan the sequence of services that are to be digitized. Such sequence can be based on payback period, cost of implementation, timing and complexity of change to a selected business process.

Use of open-source platforms such as Liferay, off-the-shelf fit-for-purpose tools and avoiding bespoke software development greatly boosts reduction in implementation and operation costs of digital services.

Digital Transformation in both organizations and government agencies will continue to be a strategic priority for at least another decade, don’t’ miss the train.

FifthOcean Technologies specialises in transitioning of businesses and government organisations from manual, paper and primitive bespoke eForms based processes into a fully integrated and secure online environment. FifthOcean Technologies is a vendor of an enterprise smart eForms platform - TransForm Engine.

TransForm Engine runs on Liferay CE and EE portals.

Copy of blog is also available on this page.

Victor Zorin 2016-06-25T00:54:17Z
Categories: CMS, ECM

API Integration with HaveIBeenPwned w Structure and Template

Liferay - Wed, 06/15/2016 - 18:15

We recently had a request for our users to be able to check if their email address has been a victim of a known data breach per the awesome API and data stores provided at https://haveibeenpwned.com/. Here is the API: https://haveibeenpwned.com/API/v2 and some background story of this project and why it's worthwhile. There are some already-written consumer examples shared there, but I chose to try and fashion a lightweight version of my own that could be quickly deployed to a Liferay page without developing an entire portlet for this goal.

I was able to achieve this utilizing Liferay Templates and Structures, JavaScript, Freemarker (or Velocity), HTML and CSS. I'll provide a few basic steps and enough (Freemarker) code to get you going. This mini-app greets the user and offers him/her to check their email address for known breaches. The code makes an AJAX call to the HaveIBeenPwned API and returns JSON data (if data is found) that is parsed and formatted for display in HTML / CSS. 

1) Create a simple structure. I named mine "PWNED". Add a text field (that we won't use) to it and save / publish it.

2) Create a (Freemarker) template. I named mine "PWNED". Attach this template to the "PWNED" structure from step 1. Uncheck the "Cacheable" option. Add the code below to the template, then Save / Publish it:

<#assign user = permissionChecker.getUser() /> <!DOCTYPE html> <html> <body> <style> .attention { color: red; } .breachIntro { font-size: 16px; } </style> <h2>Hello, ${user.firstName}!</h2> <p class="breachIntro">Check if your email address been victim of a known data breach:</p> <form id="checkBreaches" name="checkBreaches"> <label for="Email">Email address:</label> <input id="checkEmail" type="text" name="Email" value="" placeholder="${user.emailAddress}"> <input type="submit" name="submit" value="Check It!" onclick="loadDoc();return false;"> <p><a href="#" onclick='reloadMe();'>Refresh Page / Search Again</a></p> </form> <div id="breachInfo"></div> <script> function loadDoc() {   var xhttp = new XMLHttpRequest();   var url =  "https://haveibeenpwned.com/api/v2/breachedaccount/";   if (document.getElementById("checkEmail").value == "") {   var email = "${user.emailAddress}";   }   else {     var email = document.getElementById("checkEmail").value;   }   var checkUserURL = url + email;   xhttp.onreadystatechange = function() {     if (xhttp.readyState == 4 && xhttp.status == 200) {       writeData(xhttp.responseText);     }     else if (xhttp.readyState == 4 && xhttp.status == 404){     document.getElementById("breachInfo").innerHTML = "<h4><p>404 - No data returned for  <span class=\"attention\">\"" + email + ".\"</span> This is a good thing! It appears you have no known associated breaches with this account.</p></h4>";     }   };   xhttp.open("GET", checkUserURL, true);   xhttp.send(); } function writeData(response) { var arr = JSON.parse(response); var i; var email = document.getElementById("checkEmail").value; var out = "<h4><span class=\"attention\">Unfortunately, it looks like \"" + email + "\" has been breached. </span>Please see the details below:</h4>" + "<div class=\"breachEntries\">"; for(i = 0; i < arr.length; i++) {         out += "<div class=\"breachEntry\" id=\"breachEntry" + i + "\"><div class=\"breachDomain\"><strong>Domain of Breach:&nbsp;</strong>" + arr[i].Name +         "&nbsp;(<a href=\"http://" + arr[i].Domain + "\" target=\"_blank\"><strong>http://" + arr[i].Domain + "</a></strong>):&nbsp;" +         "</div><div class=\"breachDesc\">" +         arr[i].Description +         "</div><div class=\"breachItems\"><strong>Breached Items:&nbsp;</strong>" +         arr[i].DataClasses +         "</div><div class=\"breachDate\"><strong>Breach Date:&nbsp;</strong>" +         arr[i].BreachDate +         "</div></div><hr>";         }     out += "</div>"; document.getElementById("breachInfo").innerHTML = out; } function reloadMe() { window.location = window.location.pathname; } </script> <div>(Courtesy of "<a href="https://haveibeenpwned.com/" target="_blank">https://haveibeenpwned.com/</a>" and Daniel Tyger for the Liferay steps / code.) </div> </body> </html> 3) Create a web content object, using the newly-created structure / template combination from steps 1-2. Publish / save it and deploy it to a Liferay page. You should see something like the following: If you enter "foo@bar.com" into the "Email address" field, you will see what the data results look like when parsed...:

and - a look at the resulting HTML:

You could easily paint a table, instead, and include other fields from the API I have omitted in this example...

Explanation of the code:

1) Create the Freemarker "user" variable we will utilize to display the user's first name and email address where we need it later in the app...:

<#assign user = permissionChecker.getUser() /> .... <h2>Hello, ${user.firstName}!</h2> ... var email = "${user.emailAddress}";

2) Create the form and the <div> for displaying the results:

<form id="checkBreaches" name="checkBreaches"> <label for="Email">Email address:</label> <input id="checkEmail" type="text" name="Email" value="" placeholder="${user.emailAddress}"> <input type="submit" name="submit" value="Check It!" onclick="loadDoc();return false;"> <p><a href="/group/mycampus/identity-theft">Refresh Page / Search Again</a></p> </form> <div id="breachInfo"></div>

3) Set up the forked AJAX function call  to the API to use either the Liferay user emailAddress or the one entered by the user into the form and to write a success message if no data is returned:

function loadDoc() ...

and upon successful data results, fire the writeData function:

    if (xhttp.readyState == 4 && xhttp.status == 200) {       writeData(xhttp.responseText);     }

4) Parse the results for display in the writeData() function:

function writeData(response) { var arr = JSON.parse(response); var i; var email = document.getElementById("checkEmail").value; var out = "<h4><span class=\"attention\">Unfortunately, it looks like \"" + email + "\" has been breached. </span>Please see the details below:</h4>" + "<div class=\"breachEntries\">"; for(i = 0; i < arr.length; i++) {         out += "<div class=\"breachEntry\" id=\"breachEntry" + i + "\"><div class=\"breachDomain\"><strong>Domain of Breach:&nbsp;</strong>" + arr[i].Name +         "&nbsp;(<a href=\"http://" + arr[i].Domain + "\" target=\"_blank\"><strong>http://" + arr[i].Domain + "</a></strong>):&nbsp;" +         "</div><div class=\"breachDesc\">" +         arr[i].Description +         "</div><div class=\"breachItems\"><strong>Breached Items:&nbsp;</strong>" +         arr[i].DataClasses +         "</div><div class=\"breachDate\"><strong>Breach Date:&nbsp;</strong>" +         arr[i].BreachDate +         "</div></div><hr>";         }     out += "</div>"; document.getElementById("breachInfo").innerHTML = out; }

Hope it works for you. Please respond if it does not. Suggestions welcome. It's brand new for me...

Daniel Tyger 2016-06-15T23:15:41Z
Categories: CMS, ECM

Learn How to Put Customers First at Liferay Symposium NA

Liferay - Mon, 06/13/2016 - 18:57

 

A Conversation with Customers: Liferay Digital Experience Platform

As digital transformation continues to inform the way we live, work, and play, we expect companies and brands to understand what we're looking for. Businesses have a great opportunity to hear their customers, understand the problems they face, and give them the right solution to make their lives easier. But, do most companies have an idea of how to listen to their customers?

In a recent interview with CMS Connected, Bryan Cheung, CEO, shared how listening to customers sparked Liferay's progression into the digital experience market with the launch of Liferay Digital Experience Platform (DXP). In Liferay's case, transformation was not so much disruptive as it was natural, given its portal heritage, which already had laid out integrations with backend systems and the framework to drive personalized customer experience.

A Better Direction for Customer Experience

The unique infrastructure of Liferay DXP can help build a range of customer experience solutions including web, mobile, and hybrid online/offline experiences across the entire customer lifecycle. In addition, having a single view of the customer lets companies create better, more relevant interactions down the line and gets all departments working together to care for customers.

In this retail banking example, a teller displays a single view of the customer. In one page, the teller can see relevant, current information, i.e., the latest conversation between the customer and the bank (wherever it might have been), gathered from multiple systems across the organization. Based on this customer view, the teller can then make intelligent suggestions to the customer and give the bank an opportunity to offer related or complementary services. 
 


Cheung also shared that Liferay DXP isn't just about connecting surface level interactions. The whole point of Liferay DXP is to let businesses work smarter at what they do—without having to start all over. In this way, businesses gain another opportunity to engage, provide self-service, engender loyalty, and repeat purchases through all customer interactions.

A Natural Fit for Digital Experience Management

Over the past 15 years, Cheung notes that Liferay has set out to make technology work better for people: 

"We see Liferay Digital Experience Platform as the next logical step in our evolution as a company, and I hope that we’ve done a good job listening to your needs as we’ve designed our product."

As customers drive how they interact with brands, companies need to stay ahead of the curve with tools that deliver personalized digital experiences wherever, whenever they engage with your business.

# # #

Learn how to support your customers across the entire customer lifecycle. There's no better time or opportunity to get to know the Liferay team, key clients, and strategic partners than in Chicago this September. Don't miss Liferay Symposium North America!


 

#LSNA16                                        

Angela Wu 2016-06-13T23:57:37Z
Categories: CMS, ECM

User Creation for Load Testing

Liferay - Mon, 06/13/2016 - 01:08

In the leading edge digital world comprising of various applications built upon a portal technology, we land up to a situation of performance testing of the application. There-in we need users in bulk to be created for a load run which is a time consuming task with the manual process. In this scenario we can have a query to dump users into the corresponding tables in the Liferay schema instead of manually creating them. The query deals in with auto insertion of data into the Liferay tables like user_, contact_, group_. This makes user creation in Liferay a simpler task.

The bulk user creation process is as follows:

 

 

  • The user details (emailaddress, screenname, firstname etc.) available in external file (.xls or .csv etc.) is loaded into the test table.
  • Execute the query to load the data into corresponding tables in portal schema.

 

The below query to be execute to creat the users

DECLARE   v_accountId       := '10001';   v_companyId       := '10002';   v_userClassNameId := '10003';   v_userId     NUMBER;   v_userName   VARCHAR2(50);   v_userEmail  VARCHAR2(50);   v_screenName VARCHAR2(50);   v_UUID       VARCHAR2(50); BEGIN   FOR(var currentTestUser IN   (SELECT * FROM testUserData   ))   LOOP     dbms_output.put_line(currentTestUser.userid);     v_userId     := currentTestUser.userid;     v_userName   := currentTestUser.username;     v_userEmail  := currentTestUser.useremail;     v_screenName := currentTestUser.screenname;     -- Insert into contact_     INSERT     INTO Contact_       (         v_userId,          --contactId         v_companyId,       --companyId,         v_userId,          --userId,         v_userName,        --userName,         sysdate,           --createDate,         sydate,            --modifiedDate,         v_userClassNameId, --classNameId.         v_userId,          --classPK,         v_accountId,       --accountId,         0,                 --parentContactId,         v_userEmail,       --emailAddress,         v_userFirstName,   --firstName,         NULL,              --middleName,         v_userLastName,    --lastName,         0,                 --prefixId,         0,                 --suffixId,         1,                 --male,         sysdate,           --birthday,         NULL,              --smsSn,         NULL,              --aimSn,         NULL,              --facebookSn,         NULL,              --icqSn,         NULL,              --jabberSn,         NULL,              --msnSn,         NULL,              --mySpaceSn,         NULL,              --skypeSn,         NULL,              --twitterSn,         NULL,              --ymSn,         NULL,              --employeeStatusId,         NULL,              --employeeNumber,         NULL,              --jobTitle,         NULL,              --jobClass,         NULL,              --hoursOfOperation,       );     -- Insert into user_     INSERT user_     INTO VALUES       (         v_UUID,       --uuid_,         v_userId,     --userId,         v_companyId,  --companyId,         sysdate,      --createDate,         sysdate,      --modifiedDate,         0,            --defaultUser,         v_userId,     --contactId,         'Password1',  --password_,         0,            --passwordEncrypted,         0,            --passwordReset,         NULL,         --passwordModifiedDate,         NULL,         --digest,         NULL,         --reminderQueryQuestion,         NULL,         --reminderQueryAnswer,         0,            --graceLoginCount,         v_screenName, --screenName,         v_userEmail,  --emailAddress,         0,            --facebookId,         -1            --ldapServerId,         NULL,         --openId,         0,            --portraitId,         'en_US'       --languageId,         'UTC'         --timeZoneId,         'Welcome'         || v_screenName, --greeting,         NULL,            --comments,         v_userFirstName, --firstName,         NULL,            --middleName,         v_userLastName,  --lastName,         NULL,            --jobTitle,         NULL,            --loginDate,         NULL,            --loginIP,         NULL,            --lastLoginDate,         NULL,            --lastLoginIP,         NULL,            --lastFailedLoginDate,         0,               --failedLoginAttempts,         0,               --lockout,         NULL,            --lockoutDate,         1,               --agreedToTermsOfUse,         0,               --emailAddressVerified,         0                --status       );     INSERT     INTO group_ VALUES       (         v_GroupUUID, --uuid_,         v_userId,    -- groupId         v_companyId, -- companyId,         v_userId,    -- creatorUserId,         '10005',     -- classNameId,         v_userId,    -- classPK,         0,           -- parentGroupId,         0,           -- liveGroupId,         '/'         ||v_userId         ||'/'     -- treePath,         v_userId, -- name,         NULL,     -- description,         0,        -- type_,         NULL,     -- typeSettings,         1,        -- manualMembership,         0,        -- membershipRestriction,         '/'         || v_screenName -- friendlyURL,         0,              -- site,         0,              -- remoteStagingGroupCount,         1               -- active_,       );   END LOOP; END;   NOTE :  The below values should be hardcoded with corresponding values from portal schema :
  • v_accountId       := '10001';
  • v_companyId       := '10002';
  • v_userClassNameId := '10003';

ADVANTAGES:

  • Automated process for bulk user creation.
  • Saves time and manual effort.
  • Supports performance test activities with minimal effort.
  • Process of bulk user creation in Liferay simplifies the performance tests execution. 
Deependra Singh 2016-06-13T06:08:22Z
Categories: CMS, ECM

Liferay Symposium at the Historic Hilton Chicago

Liferay - Thu, 06/09/2016 - 17:58

Liferay Symposium North America will be held at the Hilton Chicago this year on September 26 - 27. Ideally located in Downtown Chicago, the Hilton is known as one of the finest hotels in the area, offering full amenities and high-end services. Its spacious rooms are comfortable and perfect for relaxing after a full conference day and viewing the Chicago cityscape.

Today, the Hilton is a bustling place that accommodates several different conferences throughout the year like our very own. But during the Roaring Twenties when the hotel was first built, it was connected to a series of shocking headlines, which included embezzlements, crime, and a good amount of family drama. Since then, the scandals have certainly died down, but much of the historic architecture has been well-preserved, such as the Grand Ballroom, where our exhibit hall will be located! And to add to its iconic reputation, this Hilton has hosted every POTUS since it opened in the twenties. That means, every president from President Coolidge to President Obama!

For your convenience, we have a discounted room block reserved for symposium attendees.

Availability: September 24 - 27, 2016
Rate: $269/night
Distance from MDW: 11 miles
Distance from ORD: 19 miles

Find out more information to help you with your travel plans.

Melanie Chung 2016-06-09T22:58:41Z
Categories: CMS, ECM

Plugin Customization in Liferay

Liferay - Thu, 06/09/2016 - 05:09

Plugin is the core of Liferay portal development in its various forms. “Plug-in” as the word enunciates is an add-on to perform additional features. It can be blended into an application to enhance primary capabilities. Liferay provides a wide variety of Out Of Box (OOB) features and ways of integration with external parties using its very own Marketplace plugins like:

•          SAML2.0 Provider EE

•          Calendar

•          Wiki             

The above instances of plugins are powerful tools which support multiple ways to extend the functionality of Liferay portal.

Customization and its necessity:

Liferay SDK is assorted with a number of functional portlets, hooks, themes, layout templates and web modules. In order to extend a specific functionality and its integration with business requirement the customization of plugins is essential.

Hence, extending Liferay with hook as a plugin type mitigates the Liferay portal to do almost anything. By extending a plugin, you can use all its features in your new plugin while keeping your changes/extensions separate from the existing plugin’s source code.

The below diagram depicts the flow of customizing plugins in Liferay:

Customizing hook plugin in Liferay:

The plugin customization is an implementation to create the "hook of Liferay plugin".

Here, we state the elaborated steps for customization of a Liferay OOB plugin.

NOTE : We have chosen SAML plugin to demonstrate the customization.

 

  1. Create a SDK hook plugin project using Liferay IDE.
     
  2. Remove all the auto-generated files except build.xml and the docroot folder.
  3. Copy the original WAR file of the plugin to be extended available at marketplace (here :saml-portlet-6.2.10.3.war) to the root folder of your new plugin.
  4. Make the following changes to build.xml inside the <project> tag to reference the original WAR .

    Add the property name and value corresponding to original.war.file and the plugin name to be customized respectively.

  5.  Copy all the files from the original plugin (here : saml-portlet-6.2.10.3.war) to the new plugin project.

  6. Run the “ant-merge” target.

  7. Build services for the plugin which will give a plugin service.jar (SAMLPlugin-hook-service.jar) to be placed in the server lib directory based on the server type distinguished as follows :

  8. Restart the server and deploy the war.

Tomcat installation

Jboss installation

Place the jar docroot\WEB-INF\lib\SAMLPlugin-hook-service.jar to the ext/lib in the tomcat structure.

 

Place the jar docroot\WEB-INF\lib\SAMLPlugin-hook-service.jar to /jboss/jboss-eap-6.1/modules/com/liferay/portal/main/SAMLPlugin-hook-service.jar

 

Add the below entry in module.xml

<resource-root path="SAMLPlugin-hook-service.jar" />

Benefits of plugin customization:

 The plugin customization in Liferay gives us following advantages:

  • Availability of additional features along with the OOB features.
  • API amendment to fulfil the requirements.
  • Supports integration with other application.
  • Flexibility to develop and customize.
  • Forward compatibility.
  • Reduces cost and effort.

Conclusion :

Thus, plugin customization in Liferay makes it possible to embed the additional features while Liferay plugin integration to the application.

deveshree zawar 2016-06-09T10:09:41Z
Categories: CMS, ECM

Styling Search Bar in Liferay Theme

Liferay - Wed, 06/08/2016 - 22:43

Liferay itself provides Out-Of-The-Box (OOTB) themes enabling organizations to build quick time-to-market customer engaging sites. There are times where organizations would like to build their own theme to fit their corporate branding. When building such theme, there is usually a need to style the search bar.

Details in creating a Liferay Theme can be found here

Thanks to the contribution by Marcos Castro on the Manizanita Theme (a Liferay Theme ), the theme provides me an idea on how to style the search bar (i.e. the Search Portlet) provided by Liferay. Below provides the changes required to be done for the mentioned files in the custom Liferay Theme (using Velocity for the templates).

 

_diffs/js/main.js:

AUI().ready(

'liferay-sign-in-modal', 'event-outside', 'transition',

function(A) {

....

var searchIcon = A.one('.open-search');

var rightIconGroup = A.one('.right-icon-group');

if (searchIcon) {

searchIcon.on(

'click',

function() {

if (!BODY.hasClass('opened-search')) {

var closingSearch = rightIconGroup.once(

'clickoutside',

function() {

BODY.removeClass('opened-search');

}

);

}

else {

closingSearch.detach();

}

BODY.toggleClass('opened-search');

var openSearchInput = A.one('.portlet-search input[type="text"]');

openSearchInput.focus();

}

);

}

.. 

);

 

 

_diffs/cs/custom.css:

.portlet-search {

display: inline-block;

margin-right: -2px;

form {

margin: 0;

padding: 0;

input {

margin-bottom: 3px;

padding: 0;

width: 1px;

@include border-radius(5px);

@include opacity(0);

}

select, input[type="image"] {

display: none;

}

}

}

 

.opened-search #banner{

.portlet-search {

form {

input {

padding: 4px;

width: 150px;

@include opacity(1);

}

select{

padding: 4px;

width: 100px;

background: transparent;

border: 1px solid #ccc;

display: inline;

@include opacity(1);

@include border-radius(5px);

}

}

}

 

.open-search {

color: #555;

width: 13px;

 

.icon-search:before {

content: "\f054";

}

}

}

_diffs/templates/portal_normal.vm (or any velocity file you want to insert search in it):

<span class="right-icon-group">

<span class="portlet-search">

$theme.search()

</span>

<a class="open-search" href="#">

<i class="icon-search"></i>

</a>

</span>

End Result:

Before Clicking Search Icon

After Clicking Search Icon

Note: This may not be the only approach. Depending on the design and deliverables (ie. html, css, javascript, images etc.) from the Creative Design or Web Design Team, the files to change and approach may differ from the above.

Kang Wei Soo 2016-06-09T03:43:28Z
Categories: CMS, ECM

Deploying Liferay Plugins in Liferay on Oracle Weblogic 12c running in "Production" mode

Liferay - Tue, 06/07/2016 - 01:12

I am thinking of what to write for my first blog. Since I have just been asked on how to deploy a Liferay plugin in Liferay on Oracle Welogic 12c Application Server, I am thinking I may as well document my findings here so that the information is not lost. However, the steps mentioned here does not apply for some types of plugin especially Liferay Hooks and, of course, Extension.

Based on Liferay Product Documentation (https://dev.liferay.com/develop/tutorials/-/knowledge_base/6-2/deploying-your-plugins-hot-deploy-vs-auto-deploy), there are times when Application servers running in “production” and “domain” modes only support non-exploded WAR deployments. To package your developed Liferay Plugins in a manner which can be deployed using the deployment features of the supported application servers, “ant direct-deploy” can be used.

An extract from the Documentation link above which provides the details:

" The Liferay Plugins SDK allows you to preprocess your archives and inject all the required elements. You therefore bypass the auto deployer at runtime. You simply need to call the following Ant task:

ant direct-deploy

The direct-deploy Ant task creates an exploded WAR from which you can easily create a WAR file. The location of the exploded WAR depends on the deployment directory of the application server you’ve configured in your Plugins SDK environment. See the Developing with the Plugins SDK tutorials for instructions on configuring the Plugins SDK for your app server. The Plugins SDK’s build.properties provides a default deployment directory value for each supported app server. But you can override the default value by specifying your desired value for theapp.server.[type].deploy.dir (replace [type] with your app server type) in your build.[username].propertiesfile.

If you choose not to use the Liferay Plugins SDK to do direct deployment, you can examine the build-common.xml file in the Plugins SDK to see how Liferay invokes the deployer tools."

Based on my recent exploration with Oracle Weblogic 12c (v12.2.1) Application Server, here are the high level steps in configuring and creating a fully deployable Liferay Plugin which can be deployed using the deployment feature of Oracle Weblogic.

Step 1: Configure Liferay SDK build.<user>.properties in your Build (e.g. Development) Machine where Oracle WebLogic (with Liferay installed) is running in “Development” mode.

For shortcut on how to make Oracle WebLogic run in “Development” mode, please refer to (http://www.baigzeeshan.com/2011/06/how-to-change-weblogic-domain-mode-to.html). 

For more details on how to install Liferay in Oracle WebLogic, please refer to (https://dev.liferay.com/discover/deployment/-/knowledge_base/6-2/installing-liferay-on-oracle-weblogic-12c-12-1-2-and-h)

For more details on configuring Liferay SDK, please refer to (https://dev.liferay.com/develop/tutorials/-/knowledge_base/6-2/setting-up-the-plugins-sdk)

For a sample settings, please refer to (https://github.com/liferay/liferay-plugins/blob/master/build.properties)

In this example, I am using the followings which is tuned to my environment:

app.server.type=weblogic

app.server.parent.dir=C:\\Oracle\\Middleware\\Oracle_Home

app.server.weblogic.dir=${app.server.parent.dir}\\user_projects

app.server.weblogic.deploy.dir=${app.server.weblogic.dir}\\domains\\base_domain\\autodeploy

app.server.weblogic.lib.global.dir=${app.server.weblogic.dir}\\domains\\base_domain\\lib

app.server.weblogic.portal.dir=${app.server.weblogic.dir}\\domains\\base_domain\\autodeploy\\ROOT

Step 2: Execute “ant direct-deploy” in your Build Machine

Once executed successfully, you will see your plug-in “deployed” in “exploded” WAR in the “autodeploy” directory under the configured domain (specified in the settings in step1) of Oracle Weblogic Application Server. Once done, jar/zip up your exploded WAR with a name. To make it in sync with the naming convention of Liferay plugin, mine is named “liferay-workshop-layout-layouttpl.war” (Liferay layout plugin) as shown below.

You may also append a version like this “liferay-workshop-layout-layouttpl-6.2.10.1.war”

Step 3: Deploy this WAR file to your Runtime Machine (different from your Build Machine) where Oracle WebLogic (with Liferay installed) is run in “Production” mode.

Once deployed successfully using the deployment feature of Oracle Weblogic 12c, you will see the deployed packaged as show below

Step 4: Validate that the plugin is available in your Runtime Machine after deployment is successful as shown below

Disclaimer: The post here mainly describes my exploration of the topic mentioned. It may not represent the recommended approach.

 

 

 

 

Kang Wei Soo 2016-06-07T06:12:32Z
Categories: CMS, ECM

Liferay Role Association with Users and Permissions Algoritm 6

Liferay - Mon, 06/06/2016 - 01:27

Role is basically a set of permissions. When a Role is assigned to a User, he is capable to do whatever permitted to that Role. Liferay provide well defined roles and permissions architecture that can be managed from control panel. Through this blog I wanted to describe how user are associated to roles directly or indirectly through Site, Organization and User Groups.

1)Regular Role-Regular role is created to define permissions within the portal scope. User can be associated to Regular Role either directly or if he is member of a site, organization, User Group as follows.

2)Site Role- Site  role is created to define permissions within the site scope. Site Role is assigned to site. If user is member of site either directly or indirectly if he is member of organization or User Group which are further member of that site, he has the permissions of Site Role.

3) Organizational Role- Organizational role is created to define permissions within the organization scope. Organizational Role can be assigned to organization users. If a user is member of organization, he can be assigned to organizational Role.

Permissions Algorithm-Permissions are actions that a user is authorized to do. As permissions is the core part of Liferay Architecture. For each entity, it requires a lot entries to be added in database. In Liferay 6.X Advanced Permissions Algorithm 6 is introduced that reduces the database size significantly.

There are following table in which permission entries are added.

1)ResourceAction- In Liferay we can define action for following resources.

    a) PortletResource

     b) Entity Resource

Resource actions might be like VIEW,UPDATE, DELETE. For each resource action a entry is added in the ResourceAction table. Each resource action is assigned a bitwise value. Let assume following are the values.

VIEW-------------1(001)

UPDATE-----------2(010)

DELETE----------4(100)

2)ResourcePermissions-This table contains the permissions that are assigned to a Role in a particular scope. Permissions are grouped together using bitwise OR and added as single entry.

Permissions->VIEW(001) OR UPDATE(010) OR DELETE(100)=111

So if a role as above three permissions then bitwise OR comes as a single value i.e . 111.It reduces the size of database significantly by eliminating multiple entries.

When it is needed to check that a particular Role has particular permission, algorithm 6 checks it by following rule.

IF((Role Permissions)AND( Action)==(Action)){

     HAS PERMISSION;

}ELSE{

    DON’T HAS PERMISSION

}

3)ResourceBlockPermissions-In Resource Block permissions , similar set of permissions are considered as block so as to reduce duplication. To understand this concept you can go through the blog in which resource block permission are explained very much in detail.

Sushil Patidar 2016-06-06T06:27:32Z
Categories: CMS, ECM

有的放矢,Liferay进军数字体验市场

Liferay - Fri, 06/03/2016 - 20:22
业内知名门户(Portal)软件商Liferay已经将新产品重命名为DXP - 数字体验平台,剑指数字体验市场。

 

这种转变其实并不是Liferay特有的,它更像是一种自然演化。“传统portal中的功能,已无法满足当今客户的功能需求,在Liferay平台上我们需要持续开发和整合这些需求功能。所以基于这些转变,我们需要重定义我们在市场中的位置。”Liferay首席执行官Bryan Cheung如是说。

 

Chueng说,当我们在观察分析自己的案例时候,发现我们已经朝着数字体验管理方向转变了。但是从某些角度看,Liferay却有着其他数字体验平台不具备的特性

 

首先,由于portal独特的架构,Liferay可以深度整合多种后台商务系统。正因为Liferay是许多客户使用的骨干门户,我们借此得以深入客户体验。这也使得Liferay,可以在整个客户生命周期中,帮助客户打造更好的客户关系,而不仅仅是停留在客户获取阶段。

什么是Liferay DXP

 

Liferay DXP是由一些不同的组件构成。核心部分是portal平台本身。其次是整合的内置后台系统包括CRM、ERP、技术支持和其他系统。整合平台使用的是微服务架构,使其定制化与整合客户需求功能更加方便。

 

Chueng说,对于企业来说,深度数字化意味着经营方式的转变。Liferay DXP可以深入到业务流程处理,以助力客户系统设计与模块整合,使其可以重新配置和部署必要的商业服务。

 

这样的架构,可以使客户在他们的客户生命周期中提供丰富用户体验方案,包括页面体验、移动设备体验、离线在线混合体验。Liferay甚至支持新型技术的的接口,如React或者Angular。这些体验还包括市场前景站点、客户站点(客户登录接口)、客户支持(在客户门户系统或者技术支持系统中)。

 

基于Liferay DXP平台,可以轻松的创建网页站点、登录页面、移动站点、移动应用和表单等。亦可创建定向受众体验和为市场客户支持和服务定制个性化体验。

 

Liferay DPX 平台还有两个组件年内问世,营销活动管理(Campaign Management)和单一客户视图(Single View of Customer, SVOC)。SVOC组件可以用来定制化体验,它能够将零散的信息整理重构,然后集中显示。营销活动管理组件使市场营销人员通过一些活动,如促销活动、客户支持或服务,以提高目标客户的参与度。

 

下面是一个银行柜员使用的页面中显示的单一用户视图。在一个页面中,柜员可以看见实时的相关信息,比如客户与银行最后一次的对话(也可能是其他任何信息),这些信息是从银行中其他部门的后台系统中整理出来。基于视图中的信息,柜员可以向用户提供更精准的建议,为银行开创更多交叉销售和业绩提升机遇。

 

客户门户新兴方案

 

Chueng论及了Liferay在传统客户门户解决方案,重点不仅是考虑到成本方面,同时也顾及拓展更多机遇、提供自助服务、强化客户忠诚度以及在与客户的交互中增进购买率。

 

Cheung谈论的另外一个领域就是如何让客户更道德的去管理他们的客户数据。Chueng相信,终端用户理应在如何使用他们自己的数据有一定程度的控制,公司也需要工具来控制用户数的的安全性和敏感性。

 

Liferay DXP采用订阅服务以及混合式结局方案(例如,云服务中的营销活动管理和SVOC)的销售方式。在未来Liferay也会寻机提供更多的管理服务。

 

完备的用户体验

 

Liferya正在以一种独创的方式前进。在整个市场营销领域,整体客户生命周期支持的解决方案,是许多其他数字体验商从未提及的。在这个方案,Liferay在整体用户体验扮演者更重要的角色。

 

大多数的厂商关注在客户获取客户服务阶段。过度关注某一方面会,都会使企业在用户全局参数方面时使失焦。其实这么做也不意味着对与不对,但是对于许多企业,有一种基于各种组件的一体化的解决方案来帮助企业优化业务处理了流程,是一种更智慧的选择。

 

原文地址:http://www.cms-connected.com/News-Articles/May-2016/%E2%80%8BLiferay-Takes-Direct-Aim-at-the-Digital-Experienc

原文作者:BARB MOSHER ZINCK

Neil Jin 2016-06-04T01:22:21Z
Categories: CMS, ECM

What is Senna.js and how it affects Liferay DXP

Liferay - Fri, 06/03/2016 - 14:14

If you have been in touch with the Liferay community in the past years, you've probably heard about Senna.js before.

We first introduced this project in late 2014 and since then we've been anxiously waiting for Liferay 7 and DXP to be released so we could talk more about it. Now it's a good time to see what this project brings to the table.

Ok, so what is Senna.js?

Senna.js is a super fast single page application engine that can dramatically optimize any site's performance. It's open source and doesn't require any dependency.

It was created by Eduardo Lundgren with the help of amazing contributors and it's actively maintained by Liferay engineers.

But what is a Single Page Application?

Even though this term may not be familiar with non-technical people, chances are you already interacted with dozens of applications like this. If you ever used Gmail, Facebook, or Google Docs, then you had the experience of using a SPA.

Basically, a Single Page Application (SPA) is a web app that loads all of the resources required to navigate throughout the site on the first page load. As the user clicks links and interacts with the page, subsequent content is loaded dynamically.

The application often updates the URL in the address bar to emulate traditional page navigation, but another full page request is never made. This is possible due to new browser capabilities like the HTML5 History API.

There are many frameworks for single page applications out there. First we had Backbone, then Ember, then Angular, now React. They all had their ups and downs, but we decided to create our own lightweight solution for many reasons:

First, because Senna.js was built for a single purpose, SPA, rather than being a fully-featured framework. Second, so you don't necessarily need to learn a new technology every time the JavaScript community comes with a new library.

How does it affect Liferay DXP? As I told before, Senna.js can improve the performance of any site. By optimizing speed we can make incredible advances in terms of user experience, and that's crucial for Liferay DXP to be successful.   Ok, enough with the blah blah blah, let's see the difference between having Senna.js enabled and disabled when using Liferay DXP with a 3G connection.    

I don't know about you, but we think that a 13-second difference for a single page navigation is HUGE.

Want to learn more?

Below you can find an entire talk about this topic. Also make sure you check sennajs.com for more technical details and examples.

 

And that's it! Any questions? Feel free to leave a comment :)

Zeno Rocha 2016-06-03T19:14:10Z
Categories: CMS, ECM
Syndicate content