AdoptOS

Assistance with Open Source adoption

CMS

How about leveraging Liferay Forms by adding your own form field?

Liferay - Mon, 11/26/2018 - 09:22

Well, if you're reading this post, I can say you're interested, and maybe anxious, to find out how to create your own form field and deploy it to Liferay Forms, am I right? Therefore, keep reading and see how easy is to complete this task.

The first step we need to do is to install blade-cli ( by the way, what a nice tool! It boosts a lot your Liferay development speed! ), then just type the following:

blade create -t form-field -v 7.1 [customFormFieldNameInCamelCase]

Nice! But, I would like to create a custom form field in Liferay DXP 7.0, is this possible?

For sure! Try the command below:

blade create -t form-field -v 7.0 [customFormFieldNameInCamelCase]

Since version 3.3.0.201811151753 of blade-cli, the developer can choose to name his/her form field module using hyphens as a word separator, like in custom-form-field, or keep using the Camel Case format. Just to let you know, Liferay's developers use to name their modules using hyphens as a word separator.  :)

That's all Folks! Have a nice customization experience!

Renato Rêgo 2018-11-26T14:22:00Z
Categories: CMS, ECM

Changing the Behavior of Scheduled Jobs

Liferay - Wed, 11/21/2018 - 22:27

Part of content targeting involves scheduled jobs that periodically sweep through several tables in order to remove older data. From a modeling perspective, this is as if content targeting were to make the assumption that all of those older events have a weight of zero, and therefore it does not need to store them or load them for modeling purposes.

If we wanted to evaluate whether this assumption is valid, we would ask questions like how much accuracy you lose by making that assumption. For example, is it similar to the small amount of accuracy you lose by identifying stop words for your content and removing them from a search index, or is it much more substantial? If you wanted to find out with an experiment, how would you design the A/B test to detect what you anticipate to be a very small effect size?

However, rather than look in detail at the assumption, today we're going to look at some problems with the assumption's implementation as a scheduled job.

Note: If you'd like to take a look at the proof of concept code being outlined in this post, you can check it out under example-content-targeting-override in my blogs code samples repository. The proof of concept has the following bundles:

  • com.liferay.portal.component.blacklist.internal.ComponentBlacklistConfiguration.config: a sample component blacklist configuration which disables the existing Liferay scheduled jobs for removing older data
  • override-scheduled-job: provides an interface ScheduledBulkOperation and a base class ScheduledBulkOperationMessageListener
  • override-scheduled-job-listener: a sample which consumes the configurations of the existing scheduled jobs to pass to ScheduledBulkOperationMessageListener
  • override-scheduled-job-dynamic-query: a sample implementation of ScheduledBulkOperation that provides the fix submitted for WCM-1490
  • override-scheduled-job-sql: a sample implementation of ScheduledBulkOperation that uses regular SQL to avoid one at a time deletes (assumes no model listeners exist on the audience targeting models)
  • override-scheduled-job-service-wrapper: a sample which consumes the ScheduledBulkOperation implementations in a service wrapper
Understanding the Problem

We have four OSGi components responsible for content targeting's scheduled jobs to remove older data.

  • com.liferay.content.targeting.analytics.internal.messaging.CheckEventsMessageListener
  • com.liferay.content.targeting.anonymous.users.internal.messaging.CheckAnonymousUsersMessageListener
  • com.liferay.content.targeting.internal.messaging.CheckAnonymousUserUserSegmentsMessageListener
  • com.liferay.content.targeting.rule.visited.internal.messaging.CheckEventsMessageListener

Each of the scheduled jobs makes a service call (which by default, encapsulates the operation in a single transaction) to a total of five service builder services that perform the work for those scheduled jobs. Each of these service calls is implemented as an ActionableDynamicQuery in order to perform the deletion.

  • com.liferay.content.targeting.analytics.service.AnalyticsEventLocalService
  • com.liferay.content.targeting.anonymous.users.service.AnonymousUserLocalService
  • com.liferay.content.targeting.service.AnonymousUserUserSegmentLocalService
  • com.liferay.content.targeting.rule.visited.service.ContentVisitedLocalService
  • com.liferay.content.targeting.rule.visited.service.PageVisitedLocalService

These service builder services ultimately delete older data from six tables.

  • CT_Analytics_AnalyticsEvent
  • CT_Analytics_AnalyticsReferrer
  • CT_AU_AnonymousUser
  • CT_AnonymousUserUserSegment
  • CT_Visited_ContentVisited
  • CT_Visited_PageVisited

If you have enough older data in any of these tables, the large transaction used for the mass deletion will eventually overwhelm the database transaction log and cause the transaction to be rolled back (in other words, no data will be deleted). Because the rollback occurs due to having too much data, and none of this data was successfully deleted, this rollback will repeat with every execution of the scheduled job, ultimately resulting in a very costly attempt to delete a lot of data, with no data ever being successfully deleted.

(Note: With WCM-1309, content targeting for Liferay 7.1 works around this problem by allowing the check to run more frequently, which theoretically prevents you from getting too much data in these tables, assuming you started using content targeting with Liferay 7.1 rather than in earlier releases.)

Implementing a Solution

When we convert our problem statement into something actionable, we can say that our goal is to update either the OSGi components or the service builder services (or both) so that the scheduled jobs which performs mass deletions do so across multiple smaller transactions. This will allow the transaction to succeed.

Step 0: Installing Dependencies

First, before we can even think about writing an implementation, we need to be able to compile that implementation. To do that, you'll need the API bundles (by convention, Liferay names these as .api bundles) for Audience Targeting.

compileOnly group: "com.liferay.content-targeting", name: "com.liferay.content.targeting.analytics.api" compileOnly group: "com.liferay.content-targeting", name: "com.liferay.content.targeting.anonymous.users.api" compileOnly group: "com.liferay.content-targeting", name: "com.liferay.content.targeting.api" compileOnly group: "com.liferay.content-targeting", name: "com.liferay.content.targeting.rule.visited.api"

With that in mind, our first road block becomes apparent when we check repository.liferay.com for our dependencies: one of the API bundles (com.liferay.content.targeting.rule.visited.api) is not available, because it's considered part of the enterprise release rather than the community release. To work around that problem, you will need to install all of the artifacts from the release .lpkg into a Maven repository and use those artifacts in your build scripts.

This isn't fundamentally difficult to do, as one of my previous blog posts on Using Private Module Binaries as Dependencies describes. However, because Liferay Audience Targeting currently lives outside of the main Liferay repository there are two wrinkles: (1) the modules in the Audience Targeting distribution don't provide the same hints in their manifests about whether they are available in a public repository or not, and (2) looking up the version each time is a pain.

To address both of these problems, I've augmented the script to ignore the manifest headers and to generate (and install) a Maven BOM from the .lpkg. You can find that augmented script here: lpkg2bom. After putting it in the same folder as the .lpkg, you run it as follows:

./lpkg2bom com.liferay.content-targeting "Liferay Audience Targeting.lpkg"

Assuming you're using the Target Platform Gradle Plugin, you'd then add this to the dependencies section in your parent build.gradle:

targetPlatformBoms group: "com.liferay.content-targeting", name: "liferay.audience.targeting.lpkg.bom", version: "2.1.2"

If you're using the Spring dependency management plugin, you'd add these to the imports section of the dependencyManagement section in your parent build.gradle.

mavenBom "com.liferay.content-targeting:liferay.audience.targeting.lpkg.bom:2.1.2"

(Note: Rumor has it that we plan to merge Audience Targeting into the main Liferay repository as part of Liferay 7.2, so it's possible that the marketplace compile time dependencies situation isn't going to be applicable to Audience Targeting in the future. It's still up in the air whether it gets merged into the main public repository or the main private repository, so it's also possible that compiling customizations to existing Liferay plugins will continue to be difficult in the future.)

Step 1: Managing Dependency Frameworks

Knowing that we are dealing with service builder services, your initial plan might be to override the specific methods invoked by the scheduled jobs, because traditional Liferay wisdom is that the services are easy to customize in Liferay.

  • com.liferay.content.targeting.analytics.service.AnalyticsEventLocalService
  • com.liferay.content.targeting.anonymous.users.service.AnonymousUserLocalService
  • com.liferay.content.targeting.service.AnonymousUserUserSegmentLocalService
  • com.liferay.content.targeting.rule.visited.service.ContentVisitedLocalService
  • com.liferay.content.targeting.rule.visited.service.PageVisitedLocalService

If you attempt this, you will be blindslided by a really difficult part of the Liferay DXP learning curve: the intermixing of multiple dependency management approaches (Spring, Apache Felix Dependency Manager, Declarative Services, etc.), and how that leads to race conditions when dealing with code that runs at component activation. More succinctly, you will end up needing to find a way to control which happens first: your new override of the service builder service being consumed by the OSGi component firing the scheduled job, or the scheduled job firing for the first time.

Rather than try to solve the problem, you can work around it by disabling the existing scheduled job via a component blacklist (relying on its status as a static bundle, which means it has a lower start level than standard modules), and then start a new scheduled job that consumes your custom implementation.

blacklistComponentNames=["com.liferay.content.targeting.analytics.internal.messaging.CheckEventsMessageListener","com.liferay.content.targeting.anonymous.users.internal.messaging.CheckAnonymousUsersMessageListener","com.liferay.content.targeting.internal.messaging.CheckAnonymousUserUserSegmentsMessageListener","com.liferay.content.targeting.rule.visited.internal.messaging.CheckEventsMessageListener"]

Let's take a moment to reflect on this solution design. Given that overriding the service builder service brings us back into a world where we're dealing with multiple dependency management frameworks, it makes more sense to separate the implementation from service builder entirely. Namely, we want to move from a world that's a mixture of Spring and OSGi into a world that is pure OSGi.

Step 2: Setting up the New Scheduled Jobs

Like all scheduled jobs, each of these scheduled jobs will register itself to the scheduler, asking the scheduler to call it at some frequency.

protected void registerScheduledJob(int interval, TimeUnit timeUnit) { SchedulerEntryImpl schedulerEntry = new SchedulerEntryImpl(); String className = getClassName(); schedulerEntry.setEventListenerClass(className); Trigger trigger = triggerFactory.createTrigger( className, className, null, null, interval, timeUnit); schedulerEntry.setTrigger(trigger); _log.fatal( String.format( "Registering scheduled job for %s with frequency %d %s", className, interval, timeUnit)); schedulerEngineHelper.register( this, schedulerEntry, DestinationNames.SCHEDULER_DISPATCH); }

If you're familiar only with older versions of Liferay, it's important to note that we don't control the frequency of scheduled jobs via portal properties, but rather with the same steps that are outlined in the tutorial, Making Your Applications Configurable.

In theory, this would make it easy for you to check configuration values; simply get an instance of the Configurable object, and away you go. However, in the case of Audience Targeting, Liferay chose to make the configuration class and the implementation class private to the module. This means that we'll need to parse the configuration directly from the properties rather than be able to use nice configuration objects, and we'll have to manually code-in the default value that's listed in the annotation for the configuration interface class.

protected void registerScheduledJob( Map<String, Object> properties, String intervalPropertyName, int defaultInterval, String timeUnitPropertyName) { int interval = GetterUtil.getInteger( properties.get(intervalPropertyName), defaultInterval); TimeUnit timeUnit = TimeUnit.DAY; String timeUnitString = GetterUtil.getString( properties.get(timeUnitPropertyName)); if (!timeUnitString.isEmpty()) { timeUnit = TimeUnit.valueOf(timeUnitString); } registerScheduledJob(interval, timeUnit); }

With that boilerplate code out of the way, we assume that our listener will be provided with an implementation of the bulk deletion for our model. For simplicity, we'll call this implementation a ScheduledBulkOperation, which has a method to perform the bulk operation, and a method that tells us how many entries it will attempt to delete at a time.

public interface ScheduledBulkOperation { public void execute() throws PortalException, SQLException; public int getBatchSize(); }

To differentiate between different model classes, we'll assume that the ScheduledBulkOperation has a property model.class that tells us which model it's intended to bulk delete. Then, each of the scheduled jobs asks for its specific ScheduledBulkOperation by specifying a target attribute on its @Reference annotation.

@Override @Reference( target = "(model.class=abc.def.XYZ)" ) protected void setScheduledBulkOperation(ScheduledBulkOperation ScheduledBulkOperation) { super.setScheduledBulkOperation(ScheduledBulkOperation); } Step 3: Breaking Up ActionableDynamicQuery

There are a handful of bulk updates in Liferay that don't actually need to be implemented as large transactions, and so as part of LPS-45839, we added an (undocumented) feature to allow you to break those a large transaction wrapped inside an ActionableDynamicQuery into multiple smaller transactions.

This was further simplified with the refactoring for LPS-46123, so that you could use a pre-defined constant in DefaultActionableDynamicQuery and one method call to get that behavior:

actionableDynamicQuery.setTransactionConfig( DefaultActionableDynamicQuery.REQUIRES_NEW_TRANSACTION_CONFIG);

You can probably guess that as a result of the feature being undocumented, when we implemented the fix for WCM-1388 to use an ActionableDynamicQuery to fix an OutOfMemoryError, we didn't make use of it. So even though we addressed the memory issue, if the transaction was large enough, the transaction was still doomed to be rolled back.

So now we'll look towards our first implementation of a ScheduledBulkOperation: simply taking the existing code that leverages an ActionableDynamicQuery, and make it use a new transaction for each interval of deletions.

For the most part, every implementation of our bulk deletion looks like the following, with a different service being called to get an ActionableDynamicQuery different name for the date column, and a different implementation of ActionableDynamicQuery.PerformAction for the individual delete methods.

ActionableDynamicQuery actionableDynamicQuery = xyzLocalService.getActionableDynamicQuery(); actionableDynamicQuery.setAddCriteriaMethod( (DynamicQuery dynamicQuery) -> { Property companyIdProperty = PropertyFactoryUtil.forName( "companyId"); Property createDateProperty = PropertyFactoryUtil.forName( dateColumnName); dynamicQuery.add(companyIdProperty.eq(companyId)); dynamicQuery.add(createDateProperty.lt(maxDate)); }); actionableDynamicQuery.setPerformActionMethod(xyzDeleteMethod); actionableDynamicQuery.setTransactionConfig( DefaultActionableDynamicQuery.REQUIRES_NEW_TRANSACTION_CONFIG); actionableDynamicQuery.setInterval(batchSize); actionableDynamicQuery.performActions();

With that base boilerplate code, we can implement several bulk deletions for each model that accounts for each of those differences.

@Component( properties = "model.class=abc.def.XYZ", service = ScheduledBulkOperation.class ) public class XYZScheduledBulkOperationByActionableDynamicQuery extends ScheduledBulkOperationByActionableDynamicQuery<XYZ> { } Step 4: Bypassing the Persistence Layer

If you've worked with Liferay service builder, you know that almost all non-upgrade code that lives in Liferay's code base operates on entities one at a time. Naturally, anything implemented with ActionableDynamicQuery has the same limitation.

This happens partly because there are no foreign keys (I don't know why this is the case either), partly because of an old incompatibility between Weblogic and Hibernate 3 (which was later addressed through a combination of LPS-29145 and LPS-41524, though the legacy setting lives on in hibernate.query.factory_class), and partly because we still notify model listeners one at a time.

In theory, you can set the value of the legacy property to org.hibernate.hql.ast.ASTQueryTranslatorFactory to allow for Hibernate queries with bulk updates (among a lot of other nice features that are available in Hibernate by default, but not available in Liferay due to the default value of the portal property), and then use that approach instead of an ActionableDynamicQuery. That's what we're hoping to eventually be able to do with LPS-86407.

However, if you know you don't have model listeners on the models you are working with (not always a safe assumption), a new option becomes available. You can choose to write everything with plain SQL outside of the persistence layer and not have to pay the Hibernate ORM cost, because nothing needs to know about the model.

This brings us to our second implementation of a ScheduledBulkOperation: using plain SQL.

With the exception of the deletions for CT_Analytics_AnalyticsReferrer (which is effectively a cascade delete, emulated with code), the mass deletion of each of the other five tables can be thought of as having the following form:

DELETE FROM CT_TableName WHERE companyId = ? AND dateColumnName < ?

Whether you delete in large batches or you delete in small batches, the query is the same. So let's assume that something provides us with a map where the key is a companyId, and the value is a sorted set of the timestamps you will use for the deletions, where the timestamps are pre-divided into the needed batch size.

String deleteSQL = String.format( "DELETE FROM %s WHERE companyId = ? AND %s < ?", getTableName(), getDateColumnName()); try (Connection connection = dataSource.getConnection(); PreparedStatement ps = connection.prepareStatement(deleteSQL)) { connection.setAutoCommit(true); for (Map.Entry<Long, SortedSet<Timestamp>> entry : timestampsMap.entrySet()) { long companyId = entry.getKey(); SortedSet<Timestamp> timestamps = entry.getValue(); ps.setLong(1, companyId); for (Timestamp timestamp : timestamps) { ps.setTimestamp(2, timestamp); ps.executeUpdate(); clearCache(getTableName()); } } }

So all that's left is to identify the breakpoints. In order to delete in small batches, choose the number of records that you want to delete in each batch (for example, 10000). Then, assuming you're running on a database other than MySQL 5.x, you can fetch the different breakpoints for those batches, though the modulus function will vary from database to database.

SELECT companyId, dateColumnName FROM ( SELECT companyId, dateColumnName, ROW_NUMBER() OVER (PARTITION BY companyId ORDER BY dateColumnName) AS rowNumber FROM CT_TableName WHERE dateColumnName < ? ) WHERE MOD(rowNumber, 10000) = 0 ORDER BY companyId, dateColumnName

If you're running a database like MySQL 5.x, you will need something similar to a stored procedure, or you can pull back all the companyId, dateColumnName records and discard anything that isn't located at a breakpoint. It's wasteful, but it's not that bad.

Finally, you sequentially execute the mass delete query for each of the different breakpoint values (and treat the original value as one extra breakpoint) rather than just the final value by itself. With that, you've effectively broken up one transaction into multiple transactions, and it will happen as fast as the database can manage, without having to pay the ORM penalty.

Expanding the Solution

Now suppose you encounter the argument, "What happens if someone manually calls the method outside of the scheduled in order to clean up the older data?" At that point, overriding the sounds looks like a good idea.

Since we already have a ScheduledBulkOperation implementation, and because service wrappers are implemented as OSGi components, the implementation is trivial.

@Component(service = ServiceWrapper.class) public class CustomXYZEventLocalService extends XYZLocalServiceWrapper { public CustomXYZEventLocalService() { super(null); } @Override public void checkXYZ() throws PortalException { try { _scheduledBulkOperation.execute(); } catch (SQLException sqle) { throw new PortalException(sqle); } } @Reference( target = "(model.class=abc.def.XYZ)" ) private ScheduledBulkOperation _scheduledBulkOperation; } Over-Expanding the Solution

With the code now existing in a service override, we have the following question: should we move the logic to whatever we use to override the service and then have the scheduled job consume the service rather than this extra ScheduledBulkOperation implementation? And if so, should we just leave the original scheduled job enabled?

With the above solution already put together, it's not obvious why you would ask that question. After all, if you have the choice to not mix Spring and OSGi, why are you choosing to mix them together again?

However, if you didn't declare the scheduled bulk update operation as its own component, and you had originally just embedded the logic inside of the listener, this question is perfectly natural to ask when you're refactoring for code reuse. Do you move the code to the service builder override, or do you create something else that both the scheduled job and the service consume? And it's not entirely obvious that you should almost never attempt the former.

Evaluating Service Wrappers

In order to know whether it's possible to consume our new service builder override from a scheduled job, you'll need to know the order of events for how a service wrapper is registered:

  1. OSGi picks up your component, which declares that it provides the ServiceWrapper.class service
  2. The ServiceTracker within ServiceWrapperRegistry is notified that your component exists
  3. The ServiceTracker creates a ServiceBag, passing your service wrapper as an argument
  4. The ServiceBag injects your service wrapper implementation into the Spring proxy object

Notice that when you follow the service wrapper tutorial, your service is not registered to OSGi under the interface it implements, because Liferay relies on the Spring proxy (not the original implementation) being published as an OSGi component. This is deliberate, because Liferay hasn't yet implemented a way to proxy OSGi components (though rumor has it that this is being planned for Liferay 7.2), and without that, you lose all of the benefits of the advices that are attached to services.

However, as a side-effect of this, this means that even though no components are notified of a new implementation of the service, all components are transparently updated to use your new service wrapper once the switch completes. What about your scheduled job? Well, until your service wrapper is injected into the Spring proxy, your scheduled job will still be calling the original service. In other words, we're back to having a race condition between all of the dependency management frameworks.

In order to fight against that race condition, you might consider manually registering the scheduled job after a delay, or duplicating the logic that exists in ServiceWrapperRegistry and ServiceBag and polling the proxy to find out when your service wrapper registered. However, all of that is really just hiding dependencies.

Evaluating Bundle Fragments

If you were still convinced that you could override the service and have your scheduled job invoke the service, you might consider overriding the existing service builder bean using a fragment bundle and ext-spring.xml, as described in a past blog entry by David Nebinger on OSGi Fragment Bundles.

However, there are two key limitations of this approach.

  1. You need a separate bundle fragment for each of the four bundles.
  2. A bundle fragment can't register new OSGi components through Declarative Services.

The second limitation warrants additional discussion, because it's also another part of the OSGi learning curve. Namely, code that would work perfectly in a regular bundle will stop working if you move it to a bundle fragment, because a bundle fragment is never started (it only advances to the RESOLVED state).

Since it's a service builder plugin, one workaround for DXP is to use a Spring bean, where the Spring bean will get registered to OSGi automatically later in the initialization cycle. However, choosing this strategy means you shouldn't add @Component to your scheduled job class (otherwise, it gets instantiated by both Spring and OSGi, and that will get messy), and there are a few things you need to keep in mind as you're trying to manage the fact that you're playing with two dependency management frameworks.

  • In order to get references to other Spring-managed components within the same bundle (for example, the service builder service you overrode), you should do it with ext-spring
  • In order to get references to Spring beans, you should use @BeanReference
  • In order to get references to OSGi components, you need to either (a) use static fields and ServiceProxyFactory, as briefly mentioned in the tutorial on Detecting Unresolved OSGi Components, or (b) use the Liferay registry API exported to the global classloader, as mentioned in the tutorial on Using OSGi Services from EXT Plugins
Evaluating Marketplace Overrides

Of course, if you were to inject the new service using ext-spring.xml using a marketplace override, as described in a past blog entry by David Nebinger on Extending Liferay OSGi Modules, you're able to register new components just fine.

However, there are still three key limitations of this approach.

  1. You need a separate bundle for each of the four marketplace overrides.
  2. Each code change requires a full server restart and a clean osgi/state folder.
  3. You need to be fully aware that the increased flexibility of a marketplace override is similar to the increased flexibility of an EXT plugin.

In theory, the reason the second limitation exists is because marketplace overrides are scanned by the same code that scans .lpkg folders rather than through a regular bundle scanning mechanism, and that scan happens only once and only happens during the module framework initialization. In theory, you might be able to work around it by adding the osgi/marketplace/override folder to the module.framework.auto.deploy.dirs portal property. However, I don't know how this actually works in practice, because I've quietly accepted the documentation that says that restarts are necessary.

Reviewing the Solution

To summarize, overriding Liferay scheduled jobs is fairly straightforward once you have all of the dependencies you need, assuming you're willing to accept the following two steps:

  1. Disable the existing scheduled job
  2. Create a new implementation of the work that scheduled job performs

If you reject these steps and try to play at the boundary of where different dependency management frameworks interact, you need to deal with the race conditions and complications that arise from that interaction.

Minhchau Dang 2018-11-22T03:27:00Z
Categories: CMS, ECM

Changing OSGi References

Liferay - Fri, 11/16/2018 - 12:53

So we've all seen those @Reference annotations scattered throughout the Liferay code, and it can almost seem like those references are not changeable.

In fact, this is not really true at all.

The OSGi Configuration Admin service can be used to change the reference binding without touching the code.

Let's take a look at a contrived example from Liferay's com.liferay.blogs.demo.internal.BlogsDemo class.

This class has a number of @Reference injections for different types of demo content generators. One of those is declared as:

@Reference(target = "(source=lorem-ipsum)", unbind = "-") protected void setLoremIpsumBlogsEntryDemoDataCreator( BlogsEntryDemoDataCreator blogsEntryDemoDataCreator) { _blogsEntryDemoDataCreators.add(blogsEntryDemoDataCreator); }

So in this example, the com.liferay.blogs.demo.data.creator.internal.LoremIpsumBlogsEntryDemoDataCreatorImpl is registered with a property, "source=lorem-ipsum", and it can generate content for a blogs entry demo.

Let's say that we have our own demo data creator, com.example.KlingonBlogsEntryDemoDataCreatorImpl that generates blog entries in Klingon (it has "source=klingon" defined for its property), and we want the blogs demo class to not use the lorem-ipsum version, but instead use our klingon variety.

How can we do this?

Well, BlogsDemo is a component, so we could create a copy of it and change the relevant code to @Reference ours, but this seems kind of like overkill.

A much easier way would be to get OSGi to just bind to our instance rather than the original. This is actually quite easy to do.

First we will need to create a configuration admin override file in osgi/config named after the full class name but with a .config extension.

So we need to create an osgi/config/com.liferay.blogs.demo.internal.BlogsDemo.config file. This file will have our override for the reference to bind to, but we need to get some more details for that.

We need to know the name for the field that we're going to be setting, that will be part of the configuration change. This will actually come from what the @Reference decorates. If @Reference is on a field, the field name will be the name you need; if it is on a setter, the name will be the setter method name without the leading "set" prefix.

So, from above, since we have setLoremIpsumBlogsEntryDemoDataCreator(), our field name will be "LoremIpsumBlogsEntryDemoDataCreator".

To change the target, we'll need to add a line to our config file with the following:

LoremIpsumBlogsEntryDemoDataCreator.target="(source\=klingon)"

This will effectively change the target string from the old (source=lorem-ipsum) to the new (source=klingon).

So this is how we can basically change up the wiring w/o really overriding a line of code.

You can even take this further. With a simple @Reference annotation w/o a target filter, you can add a target filter to bind a different reference. This could be an alternative to relying on a higher service ranking for binding.

For those cases where a service tracker is being used to track a list of entities, you can use this technique to exclude one or more references that you don't want to have the service tracker capture.

So actually I didn't come up with all of this myself.  It's actually an adaptation of https://dev.liferay.com/develop/tutorials/-/knowledge_base/7-0/overriding-service-references#configure-the-component-to-use-the-custom-service to demonstrate just how that can be used to change the wiring.

David H Nebinger 2018-11-16T17:53:00Z
Categories: CMS, ECM

Accessing Services in JSPs

Liferay - Fri, 11/16/2018 - 10:21
Introduction

When developing JSP-based portlets for OSGi deployment, and even when doing JSP fragment bundle overrides, it is often necessary to get service references in the JSP pages. But OSGi @Reference won't work in the JSP files, so we need ways to expose the services so they can be accessed in the JSPs...

Retrieving Services in the JSP

So we're going to work this a little backwards, we're going to cover how to get the service reference in the JSP itself.

In order to get the references, we're going to use a scriptlet to pull the reference from the request attributes, similar to :

<% TrashHelper trashHelper = (TrashHelper) request.getAttribute(TrashHelper.class.getName()); %>

The idea is that we will be pulling the reference directly out of the request attributes. We need to cast the object coming from the attributes to the right type, and we'll be following the Liferay standard of using the full class name as the attribute key.

The challenge is how to set the attribute into the request.

Setting Services in a Portlet You Control

So when you control the portlet code, injecting the service reference is pretty easy.

In your portlet class, you're going to add your @Reference for the service you need to pass. Your portlet class would include something along the lines of:

@Reference(unbind = "-") protected void setTrashHelper(TrashHelper trashHelper) { this._trashHelper = trashHelper; } private TrashHelper _trashHelper;

With the reference available, you'll then override the render() method to set the attribute:

@Override public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException { renderRequest.setAttribute(TrashHelper.class.getName(), _trashHelper); super.render(renderRequest, renderResponse); }

So this sets the service as an attribute in the render request. On the JSP side, it would be able to get the service via the code shared above.

Setting Services in a Portlet You Do Not Control

So you may need to build a JSP fragment bundle to override JSP code, and in your override you need to add a service which was not injected by the core portlet.  It would be kind of overkill to override the portlet just to inject missing services.

So how can you inject the services you need? A portlet filter implementation!

Portlet filters are similar to the old servlet filters, they are used to wrap the invocation of an underlying portlet. And, like servlet filters, can make adjustments to requests/responses on the way into the portlet as well as on the way out.

So we can build a portlet filter component and inject our service reference that way...

@Component( immediate = true, property = "javax.portlet.name=com_liferay_dictionary_web_portlet_DictionaryPortlet", service = PortletFilter.class ) public class TrashHelperPortletFilter implements RenderFilter { @Override public void doFilter(RenderRequest renderRequest, RenderResponse renderResponse, FilterChain filterChain) throws IOException, PortletException { filterChain.doFilter(renderRequest, renderResponse); renderRequest.setAttribute(TrashHelper.class.getName(), _trashHelper); } @Reference(unbind = "-") protected void setTrashHelper(TrashHelper trashHelper) { this._trashHelper = trashHelper; } private TrashHelper _trashHelper; }

So this portlet filter is configured to bind to the Dictionary portlet. It will be invoked at each portlet render since it implements a RenderFilter. The implementation calls through to the filter chain to invoke the portlet, but on the way out it adds the helper service to the request attributes.

Conclusion

So we've seen how we can use OSGi services in the JSP files indirectly via request attribute injection. In portlets we control, we can inject the service directly. For portlets we do not control, we can use a portlet filter to inject the service too.

David H Nebinger 2018-11-16T15:21:00Z
Categories: CMS, ECM

Pro Liferay Deployment

Liferay - Fri, 11/16/2018 - 00:15
Introduction

The official Liferay deployment docs are available here: https://dev.liferay.com/discover/deployment

They make it easy for folks new to Liferay to get the system up and running and work through all of the necessary configuration.

But it is not the process followed by professionals. I wanted to share the process I use that it might provide an alternative set of instructions that you can use to build out your own production deployment process.

The Bundle

Like the Liferay docs, you may want to start from a bundle; always start from the latest bundle you can. It is, for the most part, a working system that may be ready to go. I say for the most part because many of the bundles are older versions of the application servers. This may or may not be a concern for your organization, so consider whether you need to update the application server.

You'll want to explode the bundle so all of the files are ready to go.

If you are using DXP, you'll want to download and apply the latest fixpack. Doing this before the first start will ensure that you won't need to deal with an upgrade later on.

The Database

You will, of course, need a database for Liferay to connect to and set up. I prefer to create the initial database using database specific tools. One key aspect to keep in mind is that the database must be set up for UTF-8 support as Liferay will be storing UTF-8 content.

Here's examples for what I use for MySQL/MariaDB:

create database lportal character set utf8; grant all privileges on lportal.* to 'MyUser’@‘192.168.1.5' identified by 'myS3cr3tP4sswd'; flush privileges;

Here's the example I use for Postgres:

create role MyUser with login password 'myS3cr3tP4sswd'; alter role MyUser createdb; alter role MyUser superuser; create database lportal with owner 'MyUser' encoding 'UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8' template template0; grant all privileges on database lportal to MyUser;

There's other examples available for other databases, but hopefully you get the gist.

From an enterprise perspective, you'll have things to consider such as a backup strategy, possibly a replication strategy, a cluster strategy, ... These things will obviously depend upon enterprise needs and requirements and are beyond the scope of this blog post.

Along with the database, you'll need to connect the appserver to the database. I always want to go for the JNDI database configuration rather than sticking the values in the portal-ext.properties. The passwords are much more secure in the JNDI database configuration.

For tomcat, this means going into the conf/Catalina/localhost directory and editing the ROOT.xml file as such:

<Resource name="jdbc/LiferayPool" auth="Container" type="javax.sql.DataSource" factory="com.zaxxer.hikari.HikariJNDIFactory" minimumIdle="5" maximumPoolSize="10" connectionTimeout="300000" dataSource.user="MyUser" dataSource.password="myS3cr3tP4sswd" driverClassName="org.mariadb.jdbc.Driver" dataSource.implicitCachingEnabled="true" jdbcUrl="jdbc:mariadb://dbserver/lportal?characterEncoding=UTF-8&dontTrackOpenResources=true&holdResultsOpenOverStatementClose=true&useFastDateParsing=false&useUnicode=true" /> Elasticsearch

Elasticsearch is also necessary, so the next step is to stand up your ES solution. Could be one node or a cluster. Get your ES system set up and collect your IP address(es). Verify that firewall rules allow for connectivity from the appserver(s) to the ES node(s).

With the ES servers, create an ES configuration file, com.liferay.portal.search.elasticsearch.configuration.ElasticsearchConfiguration.config in the osgi/config directory and set the contents:

operationMode="REMOTE" clientTransportIgnoreClusterName="false" indexNamePrefix="liferay-" httpCORSConfigurations="" additionalConfigurations="" httpCORSAllowOrigin="/https?://localhost(:[0-9]+)?/" networkBindHost="" transportTcpPort="" bootstrapMlockAll="false" networkPublishHost="" clientTransportSniff="true" additionalIndexConfigurations="" retryOnConflict="5" httpCORSEnabled="true" clientTransportNodesSamplerInterval="5s" additionalTypeMappings="" logExceptionsOnly="true" httpEnabled="true" networkHost="[_eth0_,_local_]" transportAddresses=["lres01:9300","lres02:9300"] clusterName="liferay" discoveryZenPingUnicastHostsPort="9300-9400"

Obviously you'll need to edit the contents to use local IP address(es) and/or name(s). This can and should all be set up before the Liferay first start.

Portal-ext.properties

Next is the portal-ext.properties file. Below is the one that I typically start with as it fits most of the use cases for the portal that I've used. All properties are documented here: https://docs.liferay.com/ce/portal/7.0/propertiesdoc/portal.properties.html

company.default.web.id=example.com company.default.home.url=/web/example default.logout.page.path=/web/example default.landing.page.path=/web/example admin.email.from.name=Example Admin admin.email.from.address=admin@example.com users.reminder.queries.enabled=false session.timeout=5 session.timeout.warning=0 session.timeout.auto.extend=true session.tracker.memory.enabled=false permissions.inline.sql.check.enabled=true layout.user.private.layouts.enabled=false layout.user.private.layouts.auto.create=false layout.user.public.layouts.enabled=false layout.user.public.layouts.auto.create=false layout.show.portlet.access.denied=false redirect.url.security.mode=domain browser.launcher.url= index.search.limit=2000 index.filter.search.limit=2000 index.on.upgrade=false setup.wizard.enabled=false setup.wizard.add.sample.data=off counter.increment=2000 counter.increment.com.liferay.portal.model.Layout=10 direct.servlet.context.reload=false search.container.page.delta.values=20,30,50,75,100,200 com.liferay.portal.servlet.filters.gzip.GZipFilter=false com.liferay.portal.servlet.filters.monitoring.MonitoringFilter=false com.liferay.portal.servlet.filters.sso.ntlm.NtlmFilter=false com.liferay.portal.servlet.filters.sso.opensso.OpenSSOFilter=false com.liferay.portal.sharepoint.SharepointFilter=false com.liferay.portal.servlet.filters.validhtml.ValidHtmlFilter=false blogs.pingback.enabled=false blogs.trackback.enabled=false blogs.ping.google.enabled=false dl.file.rank.check.interval=-1 dl.file.rank.enabled=false message.boards.pingback.enabled=false company.security.send.password=false company.security.send.password.reset.link=false company.security.strangers=false company.security.strangers.with.mx=false company.security.strangers.verify=false #company.security.auth.type=emailAddress company.security.auth.type=screenName #company.security.auth.type=userId field.enable.com.liferay.portal.kernel.model.Contact.male=false field.enable.com.liferay.portal.kernel.model.Contact.birthday=false terms.of.use.required=false # ImageMagick imagemagick.enabled=false #imagemagick.global.search.path[apple]=/opt/local/bin:/opt/local/share/ghostscript/fonts:/opt/local/share/fonts/urw-fonts imagemagick.global.search.path[unix]=/usr/bin:/usr/share/ghostscript/fonts:/usr/share/fonts/urw-fonts #imagemagick.global.search.path[windows]=C:\\Program Files\\gs\\bin;C:\\Program Files\\ImageMagick # OpenOffice # soffice -headless -accept="socket,host=127.0.0.1,port=8100;urp;" openoffice.server.enabled=true # xuggler xuggler.enabled=true #hibernate.jdbc.batch_size=0 hibernate.jdbc.batch_size=200 cluster.link.enabled=true ehcache.cluster.link.replication.enabled=true cluster.link.channel.properties.control=tcpping.xml cluster.link.channel.properties.transport.0=tcpping.xml cluster.link.autodetect.address=dbserver company.security.auth.requires.https=true main.servlet.https.required=true atom.servlet.https.required=true axis.servlet.https.required=true json.servlet.https.required=true jsonws.servlet.https.required=true spring.remoting.servlet.https.required=true tunnel.servlet.https.required=true webdav.servlet.https.required=true rss.feeds.https.required=true dl.store.impl=com.liferay.portal.store.file.system.AdvancedFileSystemStore

Okay, so first of all, don't just copy this into your portal-ext.properties file as-is. You'll need to edit it for names, sites, addresses, etc. It also enables clusterlink and sets up use of https as well as the advanced filesystem store.

I tend to use TCPPING for my ClusterLink configuration as unicast doesn't have some of the connectivity issues. I use a standard configuration (seen below), and use the tomcat setenv.sh file to specify the initial hosts.

<!-- TCP based stack, with flow control and message bundling. This is usually used when IP multicasting cannot be used in a network, e.g. because it is disabled (routers discard multicast). Note that TCP.bind_addr and TCPPING.initial_hosts should be set, possibly via system properties, e.g. -Djgroups.bind_addr=192.168.5.2 and -Djgroups.tcpping.initial_hosts=192.168.5.2[7800] author: Bela Ban --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:org:jgroups" xsi:schemaLocation="urn:org:jgroups http://www.jgroups.org/schema/jgroups.xsd"> <TCP bind_port="7800" recv_buf_size="${tcp.recv_buf_size:5M}" send_buf_size="${tcp.send_buf_size:5M}" max_bundle_size="64K" max_bundle_timeout="30" use_send_queues="true" sock_conn_timeout="300" timer_type="new3" timer.min_threads="4" timer.max_threads="10" timer.keep_alive_time="3000" timer.queue_max_size="500" thread_pool.enabled="true" thread_pool.min_threads="2" thread_pool.max_threads="8" thread_pool.keep_alive_time="5000" thread_pool.queue_enabled="true" thread_pool.queue_max_size="10000" thread_pool.rejection_policy="discard" oob_thread_pool.enabled="true" oob_thread_pool.min_threads="1" oob_thread_pool.max_threads="8" oob_thread_pool.keep_alive_time="5000" oob_thread_pool.queue_enabled="false" oob_thread_pool.queue_max_size="100" oob_thread_pool.rejection_policy="discard"/> <TCPPING async_discovery="true" initial_hosts="${jgroups.tcpping.initial_hosts:localhost[7800],localhost[7801]}" port_range="2"/> <MERGE3 min_interval="10000" max_interval="30000"/> <FD_SOCK/> <FD timeout="3000" max_tries="3" /> <VERIFY_SUSPECT timeout="1500" /> <BARRIER /> <pbcast.NAKACK2 use_mcast_xmit="false" discard_delivered_msgs="true"/> <UNICAST3 /> <pbcast.STABLE stability_delay="1000" desired_avg_gossip="50000" max_bytes="4M"/> <pbcast.GMS print_local_addr="true" join_timeout="2000" view_bundling="true"/> <MFC max_credits="2M" min_threshold="0.4"/> <FRAG2 frag_size="60K" /> <!--RSVP resend_interval="2000" timeout="10000"/--> <pbcast.STATE_TRANSFER/> </config>

Additionally, since I want to use the Advanced Filesystem Store, I need a osgi/config/com.liferay.portal.store.file.system.configuration.AdvancedFileSystemStoreConfiguration.cfg file with the following contents:

## ## To apply the configuration, place this file in the Liferay installation's osgi/modules folder. Make sure it is named ## com.liferay.portal.store.file.system.configuration.AdvancedFileSystemStoreConfiguration.cfg. ## rootDir=/liferay/document_library JVM & App Server Config

So of course I use the Deployment Checklist to configure JVM, GC and memory configuration. I do prefer to use at least an 8G memory partition. Also I add the JGroups initial hosts.

Conclusion

Conclusion? But we haven't really started up the portal yet, how can this be the conclusion?

And that is really the point. All configuration is done before the portal is launched. Any other settings that could be changed in the System Settings control panel, well those I would also create the osgi/config file(s) to hold the settings.

The more that is done in configuration pre-launch, the less likelihood there is of getting unnecessary data loaded, user public/private layouts that might not be needed, proper filesystem store out of the gate, ...

It really is how the pros get their Liferay environments up and running...

David H Nebinger 2018-11-16T05:15:00Z
Categories: CMS, ECM

Boosting Search

Liferay - Thu, 11/15/2018 - 17:43
Introduction

A client recently was moving off of Google Search Appliance (GSA) on to Liferay and Elasticsearch. One key aspect of GSA that they relied on though, was KeyMatch.

What is KeyMatch? Well, in GSA an administrator can define a list of specific keywords and assign content to them. When a user performs a search that includes one of the specific keywords, the associated content is boosted to the top of the search results.

This way an admin can ensure that a specific piece of content can be promoted as a top result.

For example, you run a bakery. During holidays, you have specially decorated cakes and cupcakes. You might do a KeyMatch search for "cupcake" to your specialty cupcakes so when a user searches, they get the specialty cakes over your normal cupcakes.

Elasticsearch Tuning

So Elasticsearch, the heart of the Liferay search facilities, does not have KeyMatch support. In fact, often it may seem that there is little search result tuning capabilities at all. In fact, this is not the case.

There are tuning opportunities for Elasticsearch, but it does take some effort to get the outcomes you're hoping for.

Tag Boosting

So one way to get a result similar to KeyMatch would be to boost the match for tags.

In our bakery example above, all of our contents related to cupcakes will, of course, appear as search results for "cupcake" if only because the keyword is part of our content. Tagging content with "cupcake" would also get it to come up as a search result, but may not make it score high enough to make them stand out as results.

We could, however, use tag boosting so that a keyword match on a tag would push a match to the top of the search results.

So how do you implement a tag boost? Through a custom IndexPostProcessor implementation.

Here's one that I whipped up that will boost tag matches by 100.0:

@Component( immediate = true, property = { "indexer.class.name=com.liferay.journal.model.JournalArticle", "indexer.class.name=com.liferay.document.library.kernel.model.DLFileEntry" }, service = IndexerPostProcessor.class ) public class TagBoostIndexerPostProcessor extends BaseIndexerPostProcessor implements   IndexerPostProcessor { @Override public void postProcessFullQuery(BooleanQuery fullQuery, SearchContext searchContext)   throws Exception { List<BooleanClause<Query>> clauses = fullQuery.clauses(); if ((clauses == null) || (clauses.isEmpty())) { return; } Query query; BooleanQueryImpl queryImpl; for (BooleanClause<Query> clause : clauses) { query = clause.getClause(); updateBoost(query); } } protected void updateBoost(final Query query) { if (query instanceof BooleanClauseImpl) { BooleanClauseImpl<Query> booleanClause = (BooleanClauseImpl<Query>) query; updateBoost(booleanClause.getClause()); } else if (query instanceof BooleanQueryImpl) { BooleanQueryImpl booleanQuery = (BooleanQueryImpl) query; for (BooleanClause<Query> clause : booleanQuery.clauses()) { updateBoost(clause.getClause()); } } else if (query instanceof WildcardQueryImpl) { WildcardQueryImpl wildcardQuery = (WildcardQueryImpl) query; if (wildcardQuery.getQueryTerm().getField().startsWith(Field.ASSET_TAG_NAMES)) { query.setBoost(100.0f); } } else if (query instanceof MatchQuery) { MatchQuery matchQuery = (MatchQuery) query; if (matchQuery.getField().startsWith(Field.ASSET_TAG_NAMES)) { query.setBoost(100.0f); } } } }

So this is an IndexPostProcessor implementation that is bound to all JournalArticles and DLFileEntries. When a search is performed, the postProcessFullQuery() method will be invoked with the full query to be processed and the search context. The above code will be used to identify all tag matches and will increase the boost for them.

This implementation uses recursion because the passed in query is actually a tree; processing via recursion is an easy way to visit each node in the tree looking for matches on tag names.

When a match is found, the boost on the query is set to 100.0.

Using this implementation, if a single article is tagged with "cupcake", a search for "cupcake" will cause those articles with the tag to jump to the top of the search results.

Other Modification Ideas

This is an example of how you can modify the search before it is handed off to Elasticsearch for processing.

It can be used to remove query items, change query items, add query items, etc.

It can also be used to adjust the query filters to exclude items from search results.

Conclusion

So the internals of the postProcessFullQuery() method and arguments are not really documented, at least not anywhere in detail that I could find for adjusting the query results.

Rather than reading through the code for how the query is built, when I was creating this override, I actually used a debugger to check the nodes of the tree to determine types, fields, etc.

I hope this will give you some ideas about how you too might adjust your search queries in ways to manipulate search results to get the ordering you're looking for.

David H Nebinger 2018-11-15T22:43:00Z
Categories: CMS, ECM

Liferay And...  Jackson

Liferay - Tue, 11/13/2018 - 18:27
Introduction

So often when discussing how to deal with dependencies, we're often looking for ways to package our third party jars into our custom modules.

There's good reason to do this. It ensures that our modules get a version of a third party jar that we've tested with. It also excludes ambiguity over where the dependency will come from, whether it is deployed and available or not, etc.

That said, there is another option that we don't really talk about much, even though it is still a viable one. Many third party jars are actually OSGi-ready and can be deployed as modules separately from your own custom modules.

Jackson, for example, is actually module jars on their own and can be deployed to Liferay just by dropping them into the deploy folder.

Dependencies Deployed as Modules

So why deploy a third party dependency jar as an OSGi module instead of just as an embedded jar?

Often it comes down to either a concern about class loaders or, less frequently, a (misguided) attempt to shrink general modules size. I usually say this is misguided because memory and disk consumption is cheap, and the problems (to be discussed below) often are not worth it.

So what about the class loader concern? Well, when you are using a system which uses class loaders to instantiate java classes, such as with Jackson and it marshaling JSON into Java objects with Jackson annotations, class loader hierarchies and the normal boundaries between OSGi modules can make general OSGi usage a challenge when the annotations are used in different bundles.

For example, if you have module A and module B and both have POJOs decorated with Jackson annotations, you could run into issues with the annotations. When performing a package scan for classes decorated with the annotations, if the annotation is loaded by a different class loader it is effectively a different class and may not be visible during annotation processing.

If your package is deployed as a standalone module, though, then all bundles sharing the dependency will pull from the same module and therefore the same class loader.

A downside of this, though, is with versioning. If you deploy Jackson 2.9.3 and 2.9.7, there are two competing versions available and can still lead to class loader issues when the different versions are used at the same time. In the case of just a single version deployed, then you have the typical concern of all modules stuck using an agreed upon version.

Is My Dependency OSGi Ready?

So the first thing you'll need to know is whether your third party dependency jar is an OSGi module or not.

The most complicated way to find out is by opening up your jar with a zip tool to look at the contents. If the jar is a bundle, the META-INF/MANIFEST.MF file will contain the OSGi headers like Bundle-Name, Bundle-SymbolicName, Bundle-Version, etc. Additionally you may have OSGi-specific files in the META-INF folder for declarative services.

An easier way is just to use one of the Maven repo search tools. When looking for jackson-core 2.9.6 in mvnrepository.com, you come across the page like https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core/2.9.6:

Under the Files section, it is shown as a bundle w/ the size. This means it is an OSGi-ready bundle. When not OSGi-ready, the search tools will typically show it as just a jar.

This and the other Jackson jars are all marked as bundles, so I know I can deploy them as modules.

Deploying Jackson as Modules

So for a future "Liferay And..." blog post, I have need of Jackson as a module instead of as just a dependency, so in this post we're going to focus on deploying Jackson as modules. Sure this may not be necessary for all deployments or usage of Jackson, but it is for me.

Okay, so our test is going to be to build a couple of modules w/ some POJOs decorated with Jackson annotations and a module that will be marshaling to/from JSON. In order to do this, we need to have the following Jackson modules deployed:

  • compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.6'
  • compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.6'
  • compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.6'
  • compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: '2.9.6'
  • compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.9.6'
  • compile group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: '2.9.6'

Download these bundle jars and drop them into the Liferay deploy folder while Liferay is running. The bundles will deploy into Liferay and you'll see the messages in the log that the bundles are deployed and started.

Building Test Modules

So in the referenced Github repo, we'll build out a Liferay workspace with three module projects:

  1. Animals - Defines the POJOs with Jackson annotations for defining different pet instances.
  2. Persons - Defines a POJO for a person to define their set of pets.
  3. Mappings - Services based upon using Jackson to marshal to/from JSON.
  4. Gogo-Commands - Provides some simple Gogo commands that we can use to test the modules w/o building out a portlet infrastructure.

The Github repo is: https://github.com/dnebing/liferay-and-jackson

The animals and persons modules are nothing fancy, but they do leverage the Jackson annotations from the deployed OSGi Jackson modules.

The mappings module uses the Jackson ObjectMapper to handle the marshaling. It is capable of processing classes from the other modules.

The gogo module contains some simple gogo shell commands:

Command Description jackson:createCatCreates a Cat instance and outputs the toString representation of it. Args are [name [breed [age [favorite treat]]]]. jackson:createDogCreates a Dog instance and outputs the toString representation of it. Args are [name [breed [age [likes pigs ears]]]]. jackson:catJsonLike createCat, but outputs the JSON representation of the cat. jackson:dogJsonLike createDog, but outputs the JSON representation of the dog. jackson:catParses the given JSON into a Cat object and outputs the toString representation of it. The only argument is the JSON. jackson:dogParses the given JSON into a Dog object and outputs the toString representation of it. The only argument is the JSON.

For the cat and dog commands, to pass JSON as a single argument, enclose it in single quotes:

jackson:cat '{"type":"cat","name":"claire","age":6,"breed":"house","treat":"filets"}'

Conclusion

Seems like an odd place to stop, huh?

I mean, we have identified how to find OSGi-ready modules such as the Jackson modules, we have deployed them to Liferay, and we have built modules that depend upon them.

So why introduce Jackson like this and then stop? Well, it is just a preparatory blog for my next post, Liferay And... MongoDB. We'll be leveraging Jackson as part of that solution, so starting with the Jackson deployment is a good starting point.  See you in the next post!

David H Nebinger 2018-11-13T23:27:00Z
Categories: CMS, ECM

Get that old school page editing touch back again

Liferay - Tue, 11/13/2018 - 04:56

At this years Unconference at DEVCON in Amsterdam, Victor Valle kept a session about changing the look-and-feel of the portal administration. Things like removing the default product menu and creating your own. I missed that discussion because so many other interesting discussions were going on, so I have no idea if someone brought this up. In this blog article, I'm not going to do anything "drastic" like like removing a side menu. I just want to show you how easy it is to get something back which some of us missed since version 6.2.

I've heard some people complain about the way the page editing works since version 7.0, and it seems like themelets are still pretty much an underrated or unknown addition to Liferay theming. I hope this blog will tackle both.

By default you'll have to hover over a portlet or widget to get the configuration options:

Nothing wrong with it. The page is displayed as anyone without editing rights would see the page. No need to toggle that icon which shows/hides the controls. But for those who have to do a lot of page editing or widget configuration, ... every second counts. We just want to click on the ellipsis icon to configure that widget without having to hover over it first.

Some of us want this:

There are different ways to get this toggle controls feature back

After all, the controls icon did not disappear. It only becomes visible when your screensize is small enough. So it's all a matter of tweaking the styles.

Option 1: Portlet decorators

You can easily add your own portlet decorator and make it the default one in your theme. You just need to add the custom decorator in the  look-and-feel.xml of your theme:

<portlet-decorator id="show-controls" name="Show Controls"> <default-portlet-decorator>true</default-portlet-decorator> <portlet-decorator-css-class>portlet-show-controls</portlet-decorator-css-class> </portlet-decorator>

Then you will want to add some css to the theme so the toggle controls icon is displayed at all time. Toglling the icon will add a css class "controls-visible" to the body element. This is easy, just display the "portlet-topper" everytime "controls-visible" is present.

Why this option is bad for this purpose: I believe portlet decorators are meant for styling purposes. How do you want the user to see the widget when you select a certain decorator. When you want to use portlet decorators for this purpose and you want to display widgets in different ways, with(out) borders or titles... This means you'll lose the controls anyway when you assign a different decorator to a widget. I bet you'll say: "Just put the extra styles on every widget". So just forget I even brought this up.

Option 2: Add some css and js

We'll add the custom css to the theme:

/* old school portlet decorators, don't mind the shameless usage of !important */ .control-menu .toggle-controls { display: block !important; } .controls-visible.has-toggle-controls { .portlet-topper { display: -webkit-box !important; display: -moz-box !important; display: box !important; display: -webkit-flex !important; display: -moz-flex !important; display: -ms-flexbox !important; display: flex !important; position: relative; opacity: 1; transform: none !important; } section.portlet { border: 1px solid #8b8b8b; border-radius: 0.5rem; } } .controls-hidden { .portlet-topper { display: none !important; } }

We're not there yet. We'll need to put a "has-toggle-controls" class on the body element because it seems "controls-visible" is there by default, even when you're not logged in. And in this case I want to display a border around the portlet when the controls are active. So I'll be setting my own css class like this inside main.js:

var toggleControls = document.querySelector('.toggle-controls'); if (toggleControls !== null) { document.body.classList.add('has-toggle-controls'); }

But what if we had lots of different themes. Are we really going to add the same code over and over again? And what if business decides to add a box-shadow effect after a few weeks?

Option 3: Themelets

A themelet is an extension of a theme. You can extend all your themes with the same themelet. When some style needs to change, you just edit the themelet and rebuild your themes. This will require you to use the liferay-theme-generator. But I bet everyone does by now, right? More information on how to build and use themelets.

You can find the themelet I wrote on github.

Please be sure your theme's _custom.scss imports the css from themelets:

/* inject:imports */ /* endinject */

And your portal_normal.ftl should contain:

<!-- inject:js --> <!-- endinject --> Conclusion

When changing the behaviour or design of certain aspects which are not design (in the eye of the end users or guest users) related, I think its always better to go with themelets. You'll rarely come across a portal with only one theme. So using themelets will make it so much easier to maintain your themes.

Michael Adamczyk 2018-11-13T09:56:00Z
Categories: CMS, ECM

The reason we moved to 7zip bundles

Liferay - Mon, 11/12/2018 - 14:51

As some of you may have already discovered, 7.1 GA2 was released as a 7zip bundle instead of the typical zip bundle. This probably caused a ton of issues. Even our own Dev Tools are not yet equipped to handle 7z files since all the events took up our time.

I will provide you with the reasons why we had to make this move and hopefully, everyone will come to the conclusion that this was the best decision, albeit, the communication could have been handled significantly better.

The original goal that led to providing 7z bundles was to improve startup times. We discovered that if we prepopulate the OSGi state, we were able to significantly reduce startup times by 2-3 times. As we began our testing, our zip bundles were not preserving timestamps correctly. They were rounding our timestamps to the nearest seconds which invalidated our OSGi state. We also found that our bundles had grown to 1.2 gigabytes!

This improvement imposed 2 requirements:
  • maintain the original timestamp
  • significantly increase the number of duplicate files.

We began to look for solutions. Naturally, tar.gz was the first solution that came to mind. It would easily preserve the timestamps but it did not solve the file size issue. While some people may find a large download acceptable, we did not believe that it would be appropriate for some of our use cases. As a result, someone suggested that we investigate 7zip because 7zip will actually detect for duplicate files and treat them as a single file during compression. This significantly brought down the file size from 1.2 gigabytes to 400 megabytes. It was the perfect solution for us. So this is why we have ultimately decided to use 7zip instead of zips. 

Since our initial development, we have also fixed the duplicate file issue. This means that tar.gz is also viable as a solution (though the bundles are slightly larger at 600 megabytes). From now on, we will be providing 7zip bundles and also tar.gz bundles. Internally we will be using 7zip because ultimately that 200-megabyte difference is still too significant for our use cases, but for everyone else, you guys can decide what works best for you.  

David Truong 2018-11-12T19:51:00Z
Categories: CMS, ECM

Liferay Portal 7.1 CE GA2 Release

Liferay - Mon, 11/05/2018 - 23:00
What's New Downloads

Download the release now at: https://www.liferay.com/downloads-community

New Features Summary

Web Experience:  Fragments allow a content author to create small reusable content pieces. Fragments can be edited in real time or can be exported and managed with the tooling of your choice. Use content pages from within a site to have complete control over the layout of your pages.  Navigation Menus let's you create site navigation in new and interesting ways and have full control over the navigations visual presentation.
 


Forms Experience: Forms can now have complex grid layouts, numeric fields and file uploads. They now include new personalization rules that let you customize the default behavior of the form. Using the new Element Sets, form creators can now create groups of reusable components. Forms fields can now be translated into any language using any locale and can also be easily duplicated.
 


Redesigned System Settings: System Settings have received a complete overhaul. Configuration options have been logically grouped together making it easier than ever before to find what's configurable. Several options that were located under Server Administration have also been moved to System Settings.
 


User Administration: The User account form has been completely redesigned. Each form section can now be saved independently of each other minimizing the chance of losing changes. The new ScreensNavigationEntry let's developers add custom forms under user administration.

Improvements to Blogs and Forums: Blog readers a can now un-subscribe to notifications via email.  Blog authors now have complete control over the friendly URL of the entry.  Estimated reading time can be enabled in System Settings and will be calculated based on time taken to write an entry.

Blogs also have a new cards ADT that can be selected from the application configuration.  Videos can now be added inline while writing a new entry from popular services such as: Youtube, Vimeo, Facebook Video, and Twitch. Message boards users can now attach as many files as they want by dragging and dropping them in a post. Message boards also has had many visual updates.
 


Workflow Improvements: Workflow has received a complete UI overhaul. All workflow configuration is now consolidated under one area in the Control Panel. Workflow definitions are now versioned and previous versions can be restored. Workflow definitions can now be saved in draft form and published live when they are ready.

Infrastructure: Many improvements have been incorporated at the core platform level, including ElasticSearch 6.0 support and the inclusion of Tomcat 9.0. 

Documentation

Official Documentation can be found on Liferay Developer network.  For information on upgrading, see the Upgrade Guide.

Bug Reporting

If you believe you have encountered a bug in the new release you can report your issue on issues.liferay.com, selecting the "7.1.0 CE GA2" release as the value for the "Affects Version/s" field.

Getting Support

Support is provided by our awesome community. Please visit our community website for more details on how you can receive support.

Liferay and its worldwide partner network also provides services, support, training, and consulting around its flagship enterprise offering, Liferay DXP.

Also note that customers on existing releases such as 6.2 and 7.0 continue to be professionally supported, and the documentation, source, and other ancillary data about these releases will remain in place.

Jamie Sammons 2018-11-06T04:00:00Z
Categories: CMS, ECM

Mitigating RichFaces 4.5.17.Final EOL Vulnerabilities

Liferay - Mon, 11/05/2018 - 18:16
Mitigating RichFaces 4.5.17.Final EOL Vulnerabilities

If you are using RichFaces, you should be aware that Code White has discovered some remote code execution vulnerabilities in the component library. Unfortunately, since RichFaces has reached end-of-life status, these vulnerabilities will not be fixed. Thankfully there two easy options to mitigate these vulnerabilities:

  1. Migrate to Alberto Fernandez’s fork of RichFaces.

    Alberto has fixed the known security vulnerabilities and other issues with RichFaces, so you should be able to upgrade to his latest release with little trouble:

    <dependency> <groupId>com.github.albfernandez.richfaces</groupId> <artifactId>richfaces</artifactId> <version>4.6.5.ayg</version> </dependency>
  2. Disable resource serialization.

    RichFaces has a whitelist of classes that it will deserialize. By setting the whitelist to empty you can avoid this remote code execution vulnerability. Just add the following content to a file named src/main/resources/org/richfaces/resource/resource-serialization.properties in your Maven or Gradle project:

    # Disable resource serialization to disallow remote code execution: # CVE-2013-2165, RF-14310, CVE-2015-0279, RF-13977, and RF-14309. # See https://codewhitesec.blogspot.com/2018/05/poor-richfaces.html for more details. whitelist=

The Liferay Faces team has used the second mitigation method to protect our RichFaces demos and archetypes. We have released new versions of our RichFaces archetypes with the mitigation included. Please see the release notes for more details.

Kyle Joseph Stiemann 2018-11-05T23:16:00Z
Categories: CMS, ECM

For a more conspicuous SPA loading indicator

Liferay - Thu, 11/01/2018 - 17:15

// The french version of this article can be found here: Pour un indicateur de chargement SPA plus visible.

Since version 7.0 of Liferay, you surely noticed the apparition of a thin loading bar on top of screen, after most of user actions.

This loading bar is part of the new SPA (Single Page Application) mode of Liferay, supported by the Senna.js framework.

Unfortunately, this bar is so inconspicuous that most users do not see it. In general, without a visual feedback related to their action, they reiterate their action several times, which often lengthens the waiting time.

In the end, users are often unnecessarily frustrated just because this load indicator is not visible enough.

Fortunately, it's quite simple to fix this with a few lines of CSS in a custom theme, because this loading bar is just a single HTML tag on which a CSS class is dynamically applied.

<div class="lfr-spa-loading-bar"></div>

As a starting point, we can consider the superb loaders provided by Luke Haas in his project Single Element CSS Spinners. Just make some adaptations to get a CSS loader compatible with Liferay:

/* Reset properties used by the original loader */ .lfr-spa-loading .lfr-spa-loading-bar, .lfr-spa-loading-bar { -moz-animation: none 0 ease 0 1 normal none running; -webkit-animation: none 0 ease 0 1 normal none running; -o-animation: none 0 ease 0 1 normal none running; -ms-animation: none 0 ease 0 1 normal none running; animation: none 0 ease 0 1 normal none running; display: block; -webkit-transform: none; -moz-transform: none; -ms-transform: none; -o-transform: none; transform: none; background: transparent; right: initial; bottom: initial; } /* Pure CSS loader from https://projects.lukehaas.me/css-loaders */ .lfr-spa-loading .lfr-spa-loading-bar, .lfr-spa-loading .lfr-spa-loading-bar:after { border-radius: 50%; width: 10em; height: 10em; z-index: 1999999; } .lfr-spa-loading .lfr-spa-loading-bar { margin: 60px auto; font-size: 10px; text-indent: -9999em; border-top: 1.1em solid rgba(47, 164, 245, 0.2); border-right: 1.1em solid rgba(47, 164, 245, 0.2); border-bottom: 1.1em solid rgba(47, 164, 245, 0.2); border-left: 1.1em solid #2FA4F5; -webkit-transform: translateZ(0); -ms-transform: translateZ(0); transform: translateZ(0); -webkit-animation: load8 1.1s infinite linear; animation: load8 1.1s infinite linear; } @-webkit-keyframes load8 { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @keyframes load8 { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } /* Positionning */ .lfr-spa-loading .lfr-spa-loading-bar { position: fixed; top: 50%; left: 50%; margin-top: -5em; margin-left: -5em; }

Once the custom theme is applied we get a loader clearly visible that no user can miss:

This snippet support Liferay 7.0 and 7.1 and is also available on gist.

If you also have tips to improve the UX of a portal Liferay, feel free to share them in the comments of this post or in a dedicated blog post.

Sébastien Le Marchand
Freelance Technical Consultant in Paris

Sébastien Le Marchand 2018-11-01T22:15:00Z
Categories: CMS, ECM

Pour un indicateur de chargement SPA plus visible

Liferay - Thu, 11/01/2018 - 17:00

// The english version of this article can be found here: For a more conspicuous SPA loader.

Depuis la version 7.0 de Liferay, vous avez surement remarqué l’apparition d’une fine barre de chargement en haut de l’écran en réponse à la plupart des actions utilisateur.

Cette barre de chargement intervient dans le cadre du nouveau fonctionnement SPA (Single Page Application) de Liferay, assuré par le framework Senna.js.

Malheureusement, cette barre est tellement discrète que la plupart des utilisateurs ne la voient pas. En général, ne constatant pas de retour visuel à leur action, ils réitèrent leur action plusieurs fois, ce qui souvent allonge encore le temps d’attente.

Au final les utilisateurs sont souvent inutilement frustrés juste parce-que cet indicateur de chargement n’est pas assez visible.

Heureusement, il est assez simple de remédier à cela avec quelques lignes de CSS dans un thème personnalisé, car cette barre de chargement est juste une unique balise HTML sur laquelle est appliquée dynamiquement une classe CSS.

<div class="lfr-spa-loading-bar"></div>

Comme point de départ, on pourra considérer les superbes loaders proposé par Luke Haas dans son projet Single Element CSS Spinners. Il suffit alors faire quelques adaptations pour obtenir un loader CSS compatible avec Liferay :

/* Reset properties used by the original loader */ .lfr-spa-loading .lfr-spa-loading-bar, .lfr-spa-loading-bar { -moz-animation: none 0 ease 0 1 normal none running; -webkit-animation: none 0 ease 0 1 normal none running; -o-animation: none 0 ease 0 1 normal none running; -ms-animation: none 0 ease 0 1 normal none running; animation: none 0 ease 0 1 normal none running; display: block; -webkit-transform: none; -moz-transform: none; -ms-transform: none; -o-transform: none; transform: none; background: transparent; right: initial; bottom: initial; } /* Pure CSS loader from https://projects.lukehaas.me/css-loaders */ .lfr-spa-loading .lfr-spa-loading-bar, .lfr-spa-loading .lfr-spa-loading-bar:after { border-radius: 50%; width: 10em; height: 10em; z-index: 1999999; } .lfr-spa-loading .lfr-spa-loading-bar { margin: 60px auto; font-size: 10px; text-indent: -9999em; border-top: 1.1em solid rgba(47, 164, 245, 0.2); border-right: 1.1em solid rgba(47, 164, 245, 0.2); border-bottom: 1.1em solid rgba(47, 164, 245, 0.2); border-left: 1.1em solid #2FA4F5; -webkit-transform: translateZ(0); -ms-transform: translateZ(0); transform: translateZ(0); -webkit-animation: load8 1.1s infinite linear; animation: load8 1.1s infinite linear; } @-webkit-keyframes load8 { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } @keyframes load8 { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } /* Positionning */ .lfr-spa-loading .lfr-spa-loading-bar { position: fixed; top: 50%; left: 50%; margin-top: -5em; margin-left: -5em; }

Une fois le thème personnalisé appliqué on obtient un loader bien visible, qu’aucun utilisateur ne pourra plus manquer :

Ce snippet est compatible avec les versions 7.0 et 7.1 de Liferay et est également disponible sur gist.

Si vous aussi vous avez des astuces pour améliorer l’UX d’un portail Liferay, n’hésitez pas à les partager dans les commentaires de ce billet ou dans un billet dédié.

Sébastien Le Marchand
Consultant Technique indépendant à Paris

Sébastien Le Marchand 2018-11-01T22:00:00Z
Categories: CMS, ECM

Anonymize your custom entities and comply with GDPR the easy way!

Liferay - Wed, 10/31/2018 - 09:54

Helping my colleague Sergio Sanchez with his GDPR talk in the past Spanish Symposium, I came across a hidden gem in Liferay 7.1. It turns out you can integrate custom Service Builder entities with Liferay’s GDPR framework (UAD), to enable anonymization of personal data in your own entities.

I didn’t know anything about this feature, but it’s really easy to use and works like a charm (after solving a couple “gotchas”!). Some of this gotchas have been identified in LPS's and will be mentioned in the article.

Let’s take a look at how it’s done!

Service Builder to the rescue!

The first step is to create a Service Builder project, using the tool you like most (Blade, Liferay IDE...). This will create two projects, as usual, API and service, and you'll find the first hiccup. Blade doesn’t generate the service.xml file using the 7.1 DTD, it still uses 7.0, so the first thing we need to do is update the DTD to 7.1:

<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.1.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_1_0.dtd">

This issue is being tracked in LPS-86544.

The second “gotcha” is that when you update the DTD to version 7.1 Service Builder will generate code that won't compile or run with Liferay CE 7.1 GA1. To make it compile, you need to add a dependency to Petra and update the kernel to at least version 3.23.0 (that's the kernel version for Liferay CE 7.1 GA2, not released yet), but unfortunately won't run if you deploy it to a Liferay CE 7.1 GA1 instance.

Thanks to Minhchau Dang for pointing this out, Minhchau filed this bug in ticket LPS-86835.

I ended up using these in my build.gradle (of the -service project), I'm using Liferay DXP FP2:

dependencies { compileOnly group: "biz.aQute.bnd", name: "biz.aQute.bndlib", version: "3.5.0" compileOnly group: "com.liferay", name: "com.liferay.portal.spring.extender", version: "2.0.0" compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "3.26.0" compileOnly group: "com.liferay", name: "com.liferay.petra.string", version: "2.0.0" }

Also note that all across the example in Github I've used kernel version 3.26.0, not only in the -service project, but everywhere.

What exactly do you want to anonymize?

Now your service and api projects should be ready to compile, so the next step is to include the necessary information in your service.xml to make your entities anonymizable.

The first two things you need to include are two attributes at the entity level, uad-application-name, and uad-package-path. uad-application-name is the name that Liferay will use to show your application in the anonymization UI, and uad-package-path is the package Service Builder will use to create the UAD classes (Pro tip: don’t include “uad” in the package name as I did, SB will include it for you)

In my example I used this entity:

<entity local-service="true" name="Promotion" remote-service="true" uuid="true" uad-application-name="Citytour" uad-package-path="com.liferay.symposium.citytour.uad">

Once you have specified those, you can start telling Service Builder how your entity’s data will be anonymized. For this, you can use two attributes at a field level: uad-anonymize-field-name and uad-nonanonymizable.

Uad-anonymize-field-name, from the 7.1 service.xml DTD:

“The uad-anonymize-field- name value specifies the anonymous user field that
should be used to replace this column's value during auto anonymization. For
example, if "fullName" is specified, the anonymous user's full name will replace
this column's value during auto anonymization. The uad-anonymize-field-name
value should only be used withuser name columns (e.g. " statusByUserName ").”

For example, if we have a field defined like this:

<column name="userName" type="String" uad-anonymize-field-name="fullName"/>

That means that when the auto-anonymization process runs, it will replace the value of that field with the Anonymous User Full Name.

On the other hand, uad-nonanonymizable, again from the 7.1 DTD:

“The uad- nonanonymizable value specifies whether the column represents data
associated with a specific user that should be reviewed by an administrator in
the event of a GDPR compliance request. This implies the data cannot be
anonymized automatically.”

This means exactly that, the field can’t be auto-anonymized and needs manual revision when deleting user data. That’s partially true because even though the anonymization is not automatic, the admin user doesn’t have to actually do anything, just review the entities and click “anonymize” (providing the anonymization process for the entity is implemented, which we’ll do later on).

I used this fields in my “Promotion” entity:

<!-- PK fields --> <column name="promotionId" primary="true" type="long" /> <!-- Group instance --> <column name="groupId" type="long" /> <!-- Audit fields --> <column name="companyId" type="long" /> <column name="userId" type="long" /> <column name="userName" type="String" uad-anonymize-field-name="fullName"/> <column name="createDate" type="Date" /> <column name="modifiedDate" type="Date" /> <!-- Promotion Data --> <column name="description" type="String" uad-nonanonymizable="true"/> <column name="price" type="double" uad-nonanonymizable="true"/> <column name="destinationCity" type="String" uad-nonanonymizable="true"/> <!-- Personal User Data --> <column name="offereeFirstName" type="String"/> <column name="offereeLastName" type="String"/> <column name="offereeIdNumber" type="String" /> <column name="offereeTelephone" type="String" />

In my case, I’m telling the framework that the description, price, and destinationCity fields need
manual review.

So, what does SB do with this two attributes? Actually, if we have an entity with a field marked with uad-anonymize-field-name, when running buildService it will create two new projects to hold the anonymization, display, and data export logic! Isn't Service Builder awesome?

Build Service Builder Services using buildService

Excellent, you’re almost there! Now you’re ready to run the buildService gradle task, and you should see that SB has created two projects for you: <entity>-uad, and <entity>-uad-test. The <entity>-uad project (promotions-uad in my case) contains the custom anonymization and data export logic for your entity, and -uad-test contains well, the test classes.

And now that we have both projects, we must fix another “gotcha”. If you run the build or deploy gradle tasks now on those projects, they will fail spectacularly. Why? Well, if you take a look at your projects, you’ll see that there’s no build.gradle file! That’s Service Builder being a little petty, but don’t worry, you can create a new one and include these dependencies (again, based on my system):

dependencies { compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "3.26.0" compileOnly group: "com.liferay", name: "com.liferay.user.associated.data.api", version: "3.0.1" compileOnly group: "com.liferay", name: "com.liferay.petra.string", version: "2.0.0" compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0" compileOnly project(":modules:promotions:promotions-api") }

This bug is being tracked in LPS-86814

Now your project should compile without issue, let’s add our custom anonymization logic!

Customize to anonymize

In your -uad project you’ll find three packages (actually four if you count constants):

  • Anonymizer: Holds the custom anonymization logic for your entity.
  • Display: Holds the custom display logic for Liferay’s UAD UI.
  • Exporter: Holds the custom personal data export logic.

In each one, you’ll find a Base class and another class that extends the Base class. For the anonymizer package in my example, I have a BasePromotionUADAnonymizer class and a PromotionUADAnonymizer class. What we’ll do is use the concrete class (PromotionUADAnonymizer in my case) and override the autoAnonymize method. In this method, you tell the framework how to anonymize each field of your custom entity. In my example I did this:

@Component(immediate = true, service = UADAnonymizer.class) public class PromotionUADAnonymizer extends BasePromotionUADAnonymizer { @Override public void autoAnonymize(Promotion promotion, long userId, User anonymousUser) throws PortalException { if (promotion.getUserId() == userId) { promotion.setUserId(anonymousUser.getUserId()); promotion.setUserName(anonymousUser.getFullName()); promotion.setOffereeFirstName(anonymousUser.getFirstName()); promotion.setOffereeLastName(anonymousUser.getLastName()); promotion.setOffereeIdNumber("XXXXXXXXX"); promotion.setOffereeTelephone("XXX-XXX-XXX"); } promotionLocalService.updatePromotion(promotion); } }

I’m using the anonymousUser fields for my entities’ first and last name, and setting the ID and telephone fields to anonymous entries, but you can choose what fields you want to anonymize and how.

Done!

Good job, you’re done! Now your entities are integrated with Liferay’s UAD framework, and when deleting a user, you’ll see that it works like a charm. To see how UAD works from a user perspective, you can check out the Managing User Data chapter in the documentation.

I’ve uploaded this example in my GitHub repo, along with a web portlet to create Promotions. You just need to add the promotions-web portlet to the page, create some promotions with a user different from Test Test and then delete the user, you should see that all entries created by that user have now anonymous fields.

Hope you like it: https://github.com/ibairuiz/sympo-demo

Ibai Ruiz 2018-10-31T14:54:00Z
Categories: CMS, ECM

Joomla 3.9 is live!

Joomla! - Tue, 10/30/2018 - 08:45

It’s a good day for the Joomla Project, as today we proudly announce the release of Joomla 3.9 – ‘The Privacy Tool Suite’ - marking the tenth minor release in the 3.x series.

Categories: CMS

LiferayPhotos: a fully functional app in 10 days using Liferay Screens. Final part: Development experience.

Liferay - Mon, 10/29/2018 - 12:06

In the previous posts, we talked about what we were going to do, how we were going to do it, and which tools we were going to use. We introduced you the Liferay Screens’s screenlets and the reason why we were going to use them.

Well, it’s time to explain to you how we developed this application and how a junior developer, with only one year of experience in the mobile world and 2 weeks in Liferay, made this app in 10 days thanks to Liferay Screens and its screenlets. In this post, I will explain what I have done and how have been my experience.

Introduction

I am going to detail as much as possible how we have done the UserProfile screen, where the user can see their photos next to their basic information and the Feed screen, which shows a timeline with the users’s photos. Finally, I will show you how we have implemented the button that allows upload new images, either taking a new photo from the camera or getting it from the gallery. Also, I want to explain what problems encountered when I was doing the development.

First of all, as a starting point, it’s necessary to be familiarized with creating iOS themes. All of this is explained in this tutorial. They are very useful because, as you will see, to do the app we only have to change the screenlet’s UI because the logic is already done.

UserProfile screen

For the UserProfile screen I’ve created a collection of new themes for the screenlets used in this screen. Basically, what I have done is to extend the default theme, which has the name of the screenlet but ending in View_default, e.g., UserPortraitView_default.

The new theme should be given the same name than the default theme but changing the suffix to the name that we want to give them. For example, in UserProfile we modify the UserPortraitView to show the image inside a circle. For this, as I said before, we just need to copy the default theme and change the name, in my case to UserPortraitView_liferayphotos. Thus, the theme name will be liferayphotos. You have to put this name into the screenlet’s themeName attribute.

The associated code to the view, created in an analogous way, is where I have coded how the image will be displayed, in our case and as I said above, will appear inside a circle shape.

import Foundation import LiferayScreens class UserPortraitView_liferayphotos: UserPortraitView_default { override func onShow() { super.onShow() self.portraitImage?.layer.cornerRadius = self.portraitImage!.frame.size.width / 2 self.portraitImage?.clipsToBounds = true self.borderWidth = 0.0 self.editButton?.layer.cornerRadius = self.editButton!.frame.size.width / 2 self.screenlet?.backgroundColor = .clear } }

Just below of the UserPortraitScreenlet I have added the username and the user email. Both are a simple label text.

In the back of the user basic information, I’ve placed an image using an ImageDisplayScreenlet. This will let you render an image from documents and media, pretty straightforward.

Now, I am going to explain the image gallery. Maybe, this is the trickiest part in this screen but, with screenlets, nothing is difficult :)

To do this I’ve used an ImageGalleryScreenlet, but, as I did with the UserPortraitScreenlet, I have created a custom theme to give it a more appropriate style. This style consists of 3 columns with a little small space between columns and rows.

First of all, I had to specify the number of columns because the default number didn’t fit our design. It’s just one variable called columnNumber:

override var columnNumber: Int { get { return 3 } set {} }

The next step is to specify which layout we needed to use in the gallery. I needed to calculate his size and the space between the other items. All of this was done in the function doCreateLayout().

override func doCreateLayout() -> UICollectionViewLayout { let layout = UICollectionViewFlowLayout() let cellWidth = UIScreen.main.bounds.width / CGFloat(columnNumber) - spacing layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) layout.itemSize = CGSize(width: cellWidth, height: cellWidth) layout.minimumInteritemSpacing = spacing layout.minimumLineSpacing = spacing return layout }

And that’s all in this screen. With three screenlets I have made a fully functional user profile.

Feed screen

Honestly, I thought that Feed screen was the most complicated screen. In fact, all screen is just one big screenlet: the imageGalleryScreenlet. But how? Well, I have to show all images, so, it’s an image gallery and the rest information is added to the cell of each item of the gallery. 

As we mentioned in the previous post, in the cell I have two screenlets: asset display screenlet and rating screenlet.

First of all, I have created a theme for the image gallery screenlet. In this theme I’ve specified that I only need one column, as I did in the previous screen; then I give to the screenlet the item size and pass the custom cell name.

Already in the custom cell, I had to build my personal cell with the screenlets mentioned above. For the asset display screenlet, I’ve created a simple view with the user portrait screenlet and a label with the username.

For the rating screenlet, I’ve used another custom view with a new image (a heart) and with the number of likes of the image.

The rest of the cell consists of one image and some labels to give the aspect that we wanted.

As we can see, both screens were made in the same way: creating themes to give a more appropriate style and nothing else. All logic was done in the screenlet and I don’t have to worry about it.

Other screens

The other screens were far more easy than the mentioned here because the view was less complex, and I just had to tweak the style of the screenlets a little bit. For example, the sign up screenlet and the login screenlet follow the same style, as we can see in the pictures below.

                                                     

Final result

In the next video you can see the result of the application.

Conclusion

As we can see along this post, the principal and more complex logic of the application is already provided by the screenlet. I have to focus only on customizing the screenlets creating new themes and nothing else. The new themes depend on the platform to develop, of course, so we’ve needed iOS knowledge in this case.

Screenlets are a powerful tool to create native applications integrating with Liferay and very easy to use.

My humble recommendation is that if you never have used screenlets technology, create a little app as proof of concept to familiarize yourself with it. This task will only take a couple of hours and will help you to gain knowledge about screenlets and to observe how powerful they are.

With this little knowledge and some experience (not much) in the mobile platform, you will be ready to develop an application like the one mentioned in this blog, because all screenlets are used in a similar way.

Related links

Github repository: https://github.com/LuismiBarcos/LiferayPhotos

Luis Miguel Barco 2018-10-29T17:06:00Z
Categories: CMS, ECM

Binding Java and Swift libraries for Xamarin projects

Liferay - Mon, 10/29/2018 - 09:14

Previously, we talked about how market is really fragmented right now because there isn’t any popular hybrid framework. We kept an eye on some of the most popular frameworks like React Native, Nativescript, Ionic2 and Xamarin. We did a prototype with Nativescript and it worked fine but we think that the use of native libraries with this technology isn’t mature enough. If you want to know how these frameworks work underneath and their pros and cons, read this article.

Since I started with hybrid frameworks, I have another point of view of how other developers work with other technology or language and also how I feel working outside my comfort zone. We usually work with Java for Android, Swift for iOS and this year we built a hybrid solution for our framework using JavaScript and CSS. But, we have never tried some cross-compile framework. So, we decided to give Xamarin a chance!

“We want to help people develop their apps easier and faster. Liferay Screens is our solution. Since 2017, you don’t have to choose between native and hybrid, you can use both at the same time!”

When we met Xamarin

The team decided to implement a prototype in Xamarin, but we didn’t know this framework very well. So, first of all, we had to know what language we had to use, how it worked, and how we could use Liferay Screens using the native part, so we didn’t have to code everything again.

Official documentation

Xamarin has a lot of documentation and you have to read it at least twice, if not more, just to connect all the dots and realise what you’re doing. We need to know some things before starting:

When we took our first steps and wrote some C# code, we were prepared to start our binding library but…Surprise, surprise!

Officially, Xamarin does not support binding Swift libraries.

And now, what?

Ok, that’s awful, but it’s not the end of the world. Let’s investigate!

We talked with the community about binding Swift libraries and everybody seemed to be very lost. You would think that no one had done this before (some people in Objective-C, right, but not in Swift). I kept investigating and I found a post in StackOverflow related to what I wanted to do. This was really exciting and I did everything step by step to get my C# library. The post was moved to Medium: https://medium.com/@Flash3001/binding-swift-libraries-xamarin-ios-ff32adbc7c76

First binding draft

When we had our first draft of the library, with only two or three classes, we decided to test it. We created a new Xamarin project, installed the NuGet package and…It failed.

At this point, we read the documentation about error codes and binding troubleshooting. And we had to add this information to the previous one:

Putting everything together

At that moment, we had everything we really needed to implement our library. So, we started the whole implementation:

  1. Create the .aar (for Java binding) and .framework (for Swift binding) files from native code.
  2. Create an empty binding library for Android in Visual Studio and add the .aar. Mono framework will create the C# classes for us (as many as it can).
  3. Create a fat library for iOS. It will contain x86 and arm architectures.
  4. Use Objective Sharpie ( sharpie bind ) to autogenerate C# classes for iOS. We can create them from a Pod or from a .h file. This command will create two files: ApiDefinitions.cs and StrucsAndEnums.cs .
  5. Create an empty binding library for iOS in Visual Studio and add the files we created on step 4.
  6. Rebuild and check everything is ok. If there are some errors, we can check them directly in Visual Studio or in the documentation.

We worked hard and finally we created a binding library from our native one. This library is already in the NuGet website.

This was really amazing because we didn’t know anything about C#, nor Xamarin, nor binding a library and here we are, with our library released and ready to be used. Finally, we reached our goal and we created a library without implementing everything again. It was hard and it took almost 4 months but now everyone can use it and this is what we love most!

Binding troubleshooting

The steps to create a binding library are annoying and it’s very easy to get angry and not understand what’s going on, what’s the meaning of an error, why it’s not working and so on.

  1. Do not despair and take a deep breath.
  2. It’s necessary to know and use the “Xamarin steps” (this is what our team named it!). Those steps are: clean the project several times, open closed project, open closed the simulator, uninstall the app in the simulator, and the most important one, delete the obj and bin folders.
  3. Xamarin.iOS crashes unexpectedly without any error messages in the console: check the log file; on Mac OS, do this via the Console app; on Windows, use the Event Viewer. In the Mac OS app, you must click User Reports and then look for your app’s name. Note that there may be more than one log file.
  4. At the moment, it’s necessary to add  Swift runtime NuGet packages (available for Swift 3 and Swift 4) to our Xamarin project. This will increase the size of the final app.
Sample projects

It may also help to investigate Xamarin.Android and Xamarin.iOS sample apps developed by Liferay. Both are good examples of how to use the Screenlets, Views (Android), and Themes (iOS):

If you want to read more about Liferay Screens for Xamarin, you can read our official documentation: https://dev.liferay.com/es/develop/tutorials/-/knowledge_base/7-0/using-xamarin-with-liferay-screens

If you have any questions about how to install it or how to use it, just post it in our forums. We will gladly help you. And if you already have a binding library, share your experience with others.

Sarai Diaz 2018-10-29T14:14:00Z
Categories: CMS, ECM

First anniversary of the Liferay Spain User Group

Liferay - Sat, 10/27/2018 - 05:54

//The spanish version of this article can be found here: Primer aniversario de la comunidad Liferay en España, otra vez.

One year ago, on October 25, 2017, Liferay Community in Spain started again. During this year, we have made important achievements: we have joined a great group in all the events and we have called meetups regularity (usually last Wednesday every month). Only this year, Community has held 9 meetups, counting presentations and round tables, covering various topics:

When you compare the Liferay Symposium Spain 2018 agenda with the meetup topics, it’s clear that our Community had the chance to see many of the sessions first in exclusive. At this point we should especially thank the work and support of the Liferay team and especially my fellow co-organizers of the Liferay Spain User Group: Eduardo García, Javier Gamarra  and David Gómez have managed to prepare and get experts for each of the areas.

As in October was going to be a year since the User group retook its activity, and taking the chance of the Liferay Symposium in Spain happening in Madrid, we scheduled our monthly meetup at the same date to celebrate that first anniversary. Thus, the meetup was included by the Liferay team in the oficial agenda of the symposium as part of the celebration.

Fig. Screenshot of the Liferay Events App showing the LSUG meetup scheduled as part of the Symposium agenda.

 

In this last meetup, we made a review of what we have achieved and the main objectives for next year, highlighting:

  • Holding meetups in other locations, and even getting to create local groups (some attendees showed interest from Asturias and Seville)
  • Encouraging members to give talks
  • Planning other types of talks, more focused on different user levels, (introductory sessions, workshops, etc).
  • Using Forums again. They are very useful for Community, and they had lost importance
     

To commemorate the anniversary, the Liferay team gave away T-shirts to Community members.

   

We have also gained visibility from Liferay. Here we show two important examples. The first one, in the closing keynotes of Liferay Symposium 2018, and the second one with our honorary member.

    

If you want to join the Liferay Community, just join the Liferay Spain User Group in meetup and sign in the Liferay Community Slack where we have a dedicated #lug-spain channel. If you are reading this from outside Spain, you probably can find a local Liferay User Group near you or, if it does not exists, you can learn how to create a new LUG on your area.
 

Álvaro Saugar 2018-10-27T10:54:00Z
Categories: CMS, ECM

Primer año de la Comunidad Liferay en España, otra vez

Liferay - Sat, 10/27/2018 - 03:36

//The english version of this article can be found here:  First anniversary of the Liferay Spain User Group

El  25 de octubre se cumplió un año del comienzo de la nueva etapa de la Comunidad Liferay en España. En este año se han conseguido logros importantes, como juntarnos un grupo fijo en todos los eventos y tener  una regularidad en los meetups durante el año. En total, entre presentaciones y mesas redondas, se han hecho  9 encuentros, en los que se han llegado a tocar diversos temas:

Si vemos  las conferencias dadas y la agenda del Symposium Liferay 2018, se puede comprobar que, en la Comunidad, hemos tenido muchas primicias sobre los temas que se han expuesto. En este punto hay que agradecer especialmente  el trabajo y apoyo al equipo de Liferay y especialmente a quiénes  están involucrados en la Comunidad. Eduardo García, Javier Gamarra y David Gómez han conseguido preparar y traer a los responsables de cada uno de las áreas en las que se ha hablado.

Como en este mes de octubre se cumplía un año desde que retomamos la actividad del grupo de usuarios, se aprovechó el simposio de Liferay para adelantar la celebración y hacer coincidir la reunión, que incluso fue incluida en la agenda del simposio.

En esta  última reunión, hicimos un repaso de lo hecho y los objetivos principales para el próximo año, entre los que podemos destacar:

  • Meetups en otras localizaciones, incluso facilitar la creación de grupos locales (entre los asistentes se mostró interés en Asturias y Sevilla)
  • Participación en charlas de los miembros
  • Otro tipo de charlas, más enfocadas a usuario, workshop, etc.
  • También se remarcó mucho la necesidad que fomentar los foros, ya que habían perdido importancia.

Para conmemorar el aniversario, desde el equipo de Liferay, regalaron camisetas a los miembros de la comunidad.

      

También hemos conseguido visibilidad por parte de Liferay, como se puede ver en el cierre del simposio y con nuestro miembro de honor.

       

Si quieres unirte a la comunidad de Liferay en España, puedes hacerlo en el grupo de meetup y en el Slack de la comunidad donde tenemos un canal #lug-spain específico. Si no lees esto desde España, quizá puedas encontrar un grupo de usuarios de Liferay que te quede cerca y si no hay, siempre puedes empezar un LUG en tu zona.

Álvaro Saugar 2018-10-27T08:36:00Z
Categories: CMS, ECM

Customizing Liferay Navigation menus

Liferay - Fri, 10/26/2018 - 07:47

In Liferay 7.1 we introduced a new approach to the Liferay Navigation. New navigation doesn't depend on the pages tree, there is a possibility to create multiple navigation menus, to compose them using different types of elements and to assign them different functions. Out of the box, we provide 3 types of menu elements - Pages, Submenus and URLs.  But we don't want to limit the users with these 3 types and any developer can create own navigation menu element type by implementing a simple interface and in this article, I want to explain how to do that. 
As the idea for this example, I want to use a "Call back request" functionality, there are plugins for most of the modern CMS which allow creating a "Call back request" button and process the user's input in a specific way. The idea of the article is to provide more or less the same functionality(of course, simplified) for the Liferay Navigation menus.
Let's create a module with the following structure:

The key parts here are CallbackSiteNavigationMenuItemType class which implements SiteNavigationMenuItemType interface, and the edit_callback. jsp view which provides the UI to add/edit navigation menu items of this type.

CallbackSiteNavigationMenuItemType has 4 main methods(getRegularURL, getTitle, renderAddPage, and renderEditPage), the first of them define the URL the specific menu item leads to. In the getRegularURL method, we create a PortletURL to perform a notification action with the userId from the properties of the SiteNavigationMenuItem instance and a number to callback to from the JS prompt function result:

@Override public String getRegularURL( HttpServletRequest request, SiteNavigationMenuItem siteNavigationMenuItem) { UnicodeProperties properties = new UnicodeProperties(true); properties.fastLoad(siteNavigationMenuItem.getTypeSettings()); PortletURL portletURL = PortletURLFactoryUtil.create( request, CallbackPortletKeys.CALLBACK, PortletRequest.ACTION_PHASE); portletURL.setParameter( ActionRequest.ACTION_NAME, "/callback/notify_user"); portletURL.setParameter("userId", properties.getProperty("userId")); String numberParamName = _portal.getPortletNamespace(CallbackPortletKeys.CALLBACK) + "number"; StringBundler sb = new StringBundler(7); sb.append("javascript: var number = prompt('"); sb.append("Please specify a number to call back','');"); sb.append("var url = Liferay.Util.addParams({"); sb.append(numberParamName); sb.append(": number}, '"); sb.append(portletURL.toString()); sb.append("'); submitForm(document.hrefFm, url);"); return sb.toString(); }

The getTitle method defines the title shown to the user in the menu, by default it uses the current SiteNavigationMenuItem name.
In the renderAddPage and renderEditPage methods, we use JSPRenderer to show the appropriate JSP to the user when adding or editing a navigation menu item of this new type.

Our view is also pretty simple:

<%@ include file="/init.jsp" %> <% SiteNavigationMenuItem siteNavigationMenuItem = (SiteNavigationMenuItem)request.getAttribute( SiteNavigationWebKeys.SITE_NAVIGATION_MENU_ITEM); String name = StringPool.BLANK; User user = null; if (siteNavigationMenuItem != null) { UnicodeProperties typeSettingsProperties = new UnicodeProperties(); typeSettingsProperties.fastLoad(siteNavigationMenuItem.getTypeSettings()); name = typeSettingsProperties.get("name"); long userId = GetterUtil.getLong(typeSettingsProperties.get("userId")); user = UserLocalServiceUtil.getUser(userId); } CallbackDisplayContext callbackDisplayContext = new CallbackDisplayContext( renderRequest, renderResponse); %> <aui:input label="name" maxlength='<%= ModelHintsUtil.getMaxLength( SiteNavigationMenuItem.class.getName(), "name") %>' name="TypeSettingsProperties--name--" placeholder="name" value="<%= name %>"> <aui:validator name="required" /> </aui:input> <aui:input name="TypeSettingsProperties--userId--" type="hidden" value="<%= (user != null) ? user.getUserId() : 0 %>" /> <aui:input disabled="<%= true %>" label="User to notify" name="TypeSettingsProperties--userName--" placeholder="user-name" value="<%= (user != null) ? user.getFullName() : StringPool.BLANK %>" /> <aui:button id="chooseUser" value="choose" /> <aui:script use="aui-base,liferay-item-selector-dialog"> A.one('#<portlet:namespace/>chooseUser').on( 'click', function(event) { var itemSelectorDialog = new A.LiferayItemSelectorDialog( { eventName: '<%= callbackDisplayContext.getEventName() %>', on: { selectedItemChange: function(event) { if (event.newVal && event.newVal.length > 0) { var user = event.newVal[0]; A.one('#<portlet:namespace/>userId').val(user.id); A.one('#<portlet:namespace/>userName').val(user.name); } } }, 'strings.add': 'Done', title: 'Choose User', url: '<%= callbackDisplayContext.getItemSelectorURL() %>' } ); itemSelectorDialog.open(); }); </aui:script>

We use TypeSettingsProperties-prefixed parameters names to save all necessary fields in the SiteNavigationMenuItem instance properties. We need to save two fields - name of the navigation menu item which is used as a title in the menu and the ID of the user we want to notify about a callback request, in a practical case it could be a sales manager/account executive or someone who holds the responsibility for this type of requests. This JSP is using Liferay Item Selector to select the user and the final UI looks like this:

Auxiliary functionality in this module is the notification functionality. We want to send a notification by email and using Liferay internal notification system. In order to do that we need two more components, an MVCActionCommand we used in our getRegularURL method and a notification handler to allow sending this type of notifications. NotifyUserMVCActionCommand has no magic, it accepts userId and number parameters from the request and send a notification to the user using SubscriptionSender component:

@Component( immediate = true, property = { "javax.portlet.name=" + CallbackPortletKeys.CALLBACK, "mvc.command.name=/callback/notify_user" }, service = MVCActionCommand.class ) public class NotifyUserMVCActionCommand extends BaseMVCActionCommand { @Override protected void doProcessAction( ActionRequest actionRequest, ActionResponse actionResponse) throws Exception { String number = ParamUtil.getString(actionRequest, "number"); long userId = ParamUtil.getLong(actionRequest, "userId"); SubscriptionSender subscriptionSender = _getSubscriptionSender( number, userId); subscriptionSender.flushNotificationsAsync(); } private SubscriptionSender _getSubscriptionSender( String number, long userId) throws Exception { User user = _userLocalService.getUser(userId); SubscriptionSender subscriptionSender = new SubscriptionSender(); subscriptionSender.setCompanyId(user.getCompanyId()); subscriptionSender.setSubject("Call back request"); subscriptionSender.setBody("Call back request: " + number); subscriptionSender.setMailId(StringUtil.randomId()); subscriptionSender.setPortletId(CallbackPortletKeys.CALLBACK); subscriptionSender.setEntryTitle("Call bak request: " + number); subscriptionSender.addRuntimeSubscribers( user.getEmailAddress(), user.getFullName()); return subscriptionSender; } @Reference private UserLocalService _userLocalService; }

And to make it possible to send notifications from this particular portlet we need to implement a UserNotificationHandler to allow delivery and to define the body of notifications in this case:

@Component( immediate = true, property = "javax.portlet.name=" + CallbackPortletKeys.CALLBACK, service = UserNotificationHandler.class ) public class CallbackUserNotificationHandler extends BaseModelUserNotificationHandler { public CallbackUserNotificationHandler() { setPortletId(CallbackPortletKeys.CALLBACK); } @Override public boolean isDeliver( long userId, long classNameId, int notificationType, int deliveryType, ServiceContext serviceContext) throws PortalException { return true; } @Override protected String getBody( UserNotificationEvent userNotificationEvent, ServiceContext serviceContext) throws Exception { JSONObject jsonObject = JSONFactoryUtil.createJSONObject( userNotificationEvent.getPayload()); return jsonObject.getString("entryTitle"); } }

After deploying this module we can add the element of our new type to the navigation menu. Unfortunately, the only application display template for the navigation menus that supports Javascript in the URLs is List, so in order to try our Callback request element type, we have to configure Navigation Menu Widget to use List template. Clicking on the new item type we can type a number and request our Call back.

Please keep in mind that user cannot create a notification to himself so it is necessary to log out and perform this action as a Guest user or using any other account. In My Account section we can see the list of notifications:

Full code of this example is available here.

Hope it helps! If you need any help implementing your navigation menu item types  feel free to ask in comments.

Pavel Savinov 2018-10-26T12:47:00Z
Categories: CMS, ECM
Syndicate content