AdoptOS

Assistance with Open Source adoption

CMS

13 Reasons to Love Joomla!

Joomla! - Wed, 08/22/2018 - 06:30

13 years ago, we set out with a vision – to develop a CMS that stood firm on its Open Source values. Instantly, the community was behind us – with more than a thousand people joining our project within just one day. Fast forward 13 years and you could say that we’ve come a long way. 14 versions in (major/minor), Joomla! now powers millions of websites, with more than 7950 extensions lining the digital shelves of our extension directory.

Categories: CMS

Delivering Localised Documents & Assets

Liferay - Mon, 08/20/2018 - 09:33

A customer recently asked me how Liferay can help them deliver documents to their users based on user-preferred language. One of the easiest ways to do this is through Liferay’s out-of-the-box web content localisation feature. In this  blog entry, I will show how this can be easily implemented.

 

1. The first step is to create a Web Content structure with fields for our web content and documents. Make sure the “Documents and Media” field is set to be repeatable so that we can add multiple documents.

 

 

2.  Create a template with this code. Note that the field names from my structure are HtmlField and DocumentsAndMediaArray and that I am using Lexicon Experience language to display the documents as cards.

 

3. Now that we’ve created our structure and template, it’s time to start adding the web content and documents.

 

4. In this example I am going to add a Spanish version of my web content, as well as Spanish versions of my documents. Click on the language selector button to start this process.

 

5. Once the Web Content article has been translated and published, all that remains now is to drop it on a page. It will appear as follows.

 

6. When it comes to delivering content based on a user’s preferred language, there are a number of options, such as picking up the preferred language in the user’s Liferay profile or web browser. Here, I am allowing the user to manually select their preferred language from a Liferay Language Selector widget placed on the page.

 

7. Clicking on the Spanish flag gives us the following.

 

And that’s all there is to it! This example is all you need to allow non-technical users to add localised documentation to a website. It should also serve as a good starting point to address more elaborate business requirements.

 

 

John Feeney 2018-08-20T14:33:00Z
Categories: CMS, ECM

Finding Bundle Dependencies

Liferay - Sun, 08/19/2018 - 15:23
Introduction

So many times I have answered the question, "How do I find out what import packages my bundle needs?" with the tired and unsatisfactory response that uses the following process:

  1. Build your module.
  2. Deploy your module.
  3. Use Gogo to see why your bundle doesn't start, often from an Unresolved Requirement on an Import Package.
  4. Either include the lib in your jar or use the Import-Package bnd.bnd property to exclude the package.
  5. Go to  step 1, repeat until no further Unresolved Requirements are found.

Yeah, so this is really a pain, but it was the only way I knew of how to see what the imports are that your module needs.

Until Today.

Introducing Bnd

The Bnd tool (available https://bnd.bndtools.org/) is the heart of building an OSGi bundle.  Whether you're using Blade, Gradle, Maven, etc it doesn't matter; under the covers you are likely invoking Bnd to build the module.

Most of us don't know about Bnd only because we're lazy developers. Well okay, maybe not lazy, but we're only going to learn new tools if we have to. But if we can do our jobs without knowing every detail of the process, we're generally fine with it.

It is Bnd which is responsible for applying the directives in your bnd.bnd file and generating the bundle full of the OSGi details necessary for your bundle to deploy and run.

As it turns out, the Bnd tool knows a heck of a lot more about our bundles than we do.

To find this out, though, we need the Bnd tool installed.

Follow the instructions from https://bnd.bndtools.org/chapters/120-install.html on 3.1 to install the command line client.

Bnd Printing

The command line tool actually gives you a full basket of tools to play with, you can find the whole list here: https://bnd.bndtools.org/chapters/860-commands.html

Honestly I have not really played with many of them yet.  I surely need to because there are definitely useful nuggets of gold in them there hills.

For example, the one nugget I've found so far is the print command.

I'm talking specifically about using bnd print --impexp to list imports and exports as described https://bnd.bndtools.org/chapters/390-wrapping.html in section 20.3.

Turns out this command will list the imports and exports Bnd has identified for your module.

I turned this on one of my own modules to see what I would get:

bnd print --impexp build/libs/com.example.hero.rules.engine.simple-1.1.0.jar [IMPEXP] Import-Package com.example.data.model {version=[1.0,2)} com.example.hero.rules.engine {version=[1.1,2)} com.example.hero.rules.model {version=[1.0,2)} com.example.hero.rules.service {version=[1.0,2)} com.liferay.portal.kernel.log {version=[7.0,8)} com.liferay.portal.kernel.util {version=[7.3,8)} com.liferay.portal.kernel.uuid {version=[6.2,7)} javax.xml.datatype javax.xml.namespace javax.xml.parsers org.w3c.dom org.w3c.dom.bootstrap org.w3c.dom.ls org.xml.sax

Cool, huh? I can see that my module wants to import stuff that I have defined off in the API module, but I can also see that I'm leveraging portal-kernel as well as XML processing.

POI Portlet

One of the frequent requests is what is necessary to use POI in a portlet. Let's find out together, shall we?

So I created a simple module using Blade and use the following build.gradle file:

dependencies { compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0" compileOnly group: "com.liferay.portal", name: "com.liferay.util.taglib", version: "2.0.0" compileOnly group: "javax.portlet", name: "portlet-api", version: "2.0" compileOnly group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1" compileOnly group: "jstl", name: "jstl", version: "1.2" compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0" compileInclude group: 'org.apache.poi', name: 'poi-ooxml', version: '3.17' }

The only thing I added here was the compileInclude directive. As we all know, this will automagically include the declared dependency and some of the transitive dependencies in the bundle. But what many of us have seen, if you deploy this guy you would still get Unresolved Reference messages.

Well, using Bnd we can now see why that is:

bnd print --impexp build/libs/com.example.poi-1.0.0.jar [IMPEXP] Import-Package com.liferay.portal.kernel.portlet.bridges.mvc {version=[1.0,2)} com.microsoft.schemas.office.powerpoint com.microsoft.schemas.office.word com.sun.javadoc com.sun.tools.javadoc javax.crypto javax.crypto.spec javax.imageio javax.imageio.metadata javax.imageio.stream javax.portlet {version=[2.0,3)} javax.security.auth.x500 javax.servlet {version=[3.0,4)} javax.servlet.http {version=[3.0,4)} javax.swing javax.xml.bind javax.xml.bind.annotation javax.xml.bind.annotation.adapters javax.xml.crypto javax.xml.crypto.dom javax.xml.crypto.dsig javax.xml.crypto.dsig.dom javax.xml.crypto.dsig.keyinfo javax.xml.crypto.dsig.spec javax.xml.parsers javax.xml.transform javax.xml.transform.dom javax.xml.transform.stream javax.xml.validation javax.xml.xpath junit.framework org.apache.commons.logging org.apache.crimson.jaxp org.apache.jcp.xml.dsig.internal.dom org.apache.poi.hsmf org.apache.poi.hsmf.datatypes org.apache.poi.hsmf.extractor org.apache.poi.hwpf.extractor org.apache.tools.ant org.apache.tools.ant.taskdefs org.apache.tools.ant.types org.apache.xml.resolver org.apache.xml.resolver.tools org.apache.xml.security org.apache.xml.security.c14n org.apache.xml.security.signature org.apache.xml.security.utils org.bouncycastle.asn1 org.bouncycastle.asn1.cmp org.bouncycastle.asn1.nist org.bouncycastle.asn1.ocsp org.bouncycastle.asn1.x500 org.bouncycastle.asn1.x509 org.bouncycastle.cert org.bouncycastle.cert.jcajce org.bouncycastle.cert.ocsp org.bouncycastle.cms org.bouncycastle.cms.bc org.bouncycastle.operator org.bouncycastle.operator.bc org.bouncycastle.tsp org.bouncycastle.util org.etsi.uri.x01903.v14 org.junit org.junit.internal org.junit.runner org.junit.runner.notification org.openxmlformats.schemas.officeDocument.x2006.math org.openxmlformats.schemas.schemaLibrary.x2006.main org.w3c.dom org.w3c.dom.events org.w3c.dom.ls org.xml.sax org.xml.sax.ext org.xml.sax.helpers Export-Package com.example.poi.constants {version=1.0.0}

Nuts!

Now we can see just what OSGi is going to want us to deal with. We can add the compileInclude directives for these artifacts if we want to include them, or we could mask them using the ! syntax for the bnd.bnd Import-Package directive, or even mark them as optional by listing them in the Import-Package directive with the resolution:=optional instruction, ala:

Import-Package:\ ...\ org.apache.tools.ant.*;resolution:=optional,\ ... Conclusion

During my testing, I did find that this is not the perfect solution. Not even the Bnd tool will process transitive dependencies correctly.

For example, we can see from above that BouncyCastle is imported, but what we can't see are any transitive dependencies to BouncyCastle that might be lurking if we decide to include BouncyCastle in. We would have to re-run the bnd print --impexp command again, but that will still be better than the old tired answer I used to have to give.

Enjoy!

David H Nebinger 2018-08-19T20:23:00Z
Categories: CMS, ECM

New lessons on Liferay University

Liferay - Fri, 08/17/2018 - 03:00

As promised less than a month ago, we're working on more content for Liferay University. Meet your new professors Charles Cohick and Dimple Koticha.

The new lessons are

As with all lessons on Liferay University, they're completely free and available after logging in with your liferay.com account.

But wait, there's more...

Learn as much as you can

For a limited time, Liferay University Passport, our flat rate for each and every course on Liferay University, is available for an introductory price at almost 30% discount.  And even those courses aren't finished yet: There are more to come. So, get it while you save that much. With the one-time payment for a personal passport, even the paid courses are free and you have a full year time to take them all.

Prefer a live trainer in the room?

Of course, if you prefer to have a live trainer in the room: The regular trainings are still available, and are updated to  contain all of the courses that you find on Liferay University and Passport. And, this way (with a trainer, on- or offline) you can book courses for all of the previous versions of Liferay as well.

And, of course, the fine documentation is still available and updated to contain information about the new version already.

(Photo: CC by 2.0 Hamza Butt)

Olaf Kock 2018-08-17T08:00:00Z
Categories: CMS, ECM

Liferay CE 7.x / Liferay DXP 7.x Java Agents

Liferay - Wed, 08/15/2018 - 10:38

A SysAdmin came up to me and said he was having issues starting Liferay DXP 7.0, a bunch of CNFEs were coming up at startup.

I found that they were set up to use Wily for monitoring their JVMs, and it was those classes that were generating CNFEs.

In general, when you add the -Djavaagent=XXX parameter onto your app server's startup command, you're enabling an agent which will have full access inside of the JVM, but only as long as the class loader hierarchy is available. The classes in the agent are injected into the highest point of the class loader hierarchy so they are normally visible across the entire app server.

Except, of course, for Liferay's OSGi container.

Liferay takes great care when creating the OSGi container to limit the "pollution" of the OSGi container's class loader to prevent classes from the app server leaking in as global classes in the OSGi container.

For monitoring agents, though, the agent packages will not be available within OSGi even though the agent is still going to try to inject itself into object instantiation.

This leads to all of the ClassNotFoundExceptions for missing packages/classes during startup.

Enabling Agent Monitoring in OSGi

We can actually enable the agent inside of the OSGi container, but it takes an additional configuration step.

In our portal-ext.properties file, we need to add the packages from the agent to the module.framework.properties.org.osgi.framework.bootdelegation property.

Note here that I said "add".  You can't just say module.framework.properties.org.osgi.framework.bootdelegation=com.agent.* and think it is going to work out because that strips out all of the other packages Liferay normally passes through the boot delegation.

To find your list, you need your portal.properties file or access to the portlet properties panel in the System Administration control panel (or from Github or from your copy of your DXP source or ...).  Using the existing value as the guide, you'll end up with a value like:

module.framework.properties.org.osgi.framework.bootdelegation=\ __redirected,\ com.liferay.aspectj,\ com.liferay.aspectj.*,\ com.liferay.portal.servlet.delegate,\ com.liferay.portal.servlet.delegate*,\ com.sun.ccpp,\ com.sun.ccpp.*,\ com.sun.crypto.*,\ com.sun.image.*,\ com.sun.jmx.*,\ com.sun.jna,\ com.sun.jndi.*,\ com.sun.mail.*,\ com.sun.management.*,\ com.sun.media.*,\ com.sun.msv.*,\ com.sun.org.*,\ com.sun.syndication,\ com.sun.tools.*,\ com.sun.xml.*,\ com.yourkit.*,\ sun.*,\ com.agent.*

See how I tacked on the "com.agent.*" at the end of the list?

You'll of course change the "com.agent" stuff to match whatever package your particular agent is using, but hopefully you get the idea.

David H Nebinger 2018-08-15T15:38:00Z
Categories: CMS, ECM

Extending Liferay DXP - User Registration (Part 2)

Liferay - Mon, 08/13/2018 - 02:24

This is the second part of the "Extending Liferay DXP - User Registration" blog. In this blog we explore the ways of implementing registration process for a portal with multiple sites

Portal Sites Configuration

Let’s presume we have a portal with the following sites configured:

  • "Liferay", default site, will not be listed
  • "Site 1", site with open membership
  • "Site 2", site with restricted membership
  • "Site 3", site with restricted membership
  • "Site 4", private site, will not be listed

Each of the sites has its own description which we want to display to the user:

 

User Registration Process Flow

The main steps of the user registration process that we are going to implement here are:

  1. Check if a user already has an account 
  2. If user already has an account but is not signed in, ask to sign in
  3. If user is already signed in, show current read-only details of a user
  4. Show the sites which is the user is not a member of with the description of the site when the site is selected
  5. Collect additional information from the user if the user has selected a 'restricted' site
  6. User reviews the request, with the ability to save request as PDF file, and submits the form
  7. On form submission:
    1. Automatically create user account if user does not exist
    2. If the site selected is 'open', user is automatically enrolled into this site
    3. If the site selected is 'restricted', site membership request is created for this user
    4. If the site selected is 'restricted', notification email is sent to site owners with user details attached as PDF

 

For the implementation of this process we use SmartForms.

Here we show only essential screenshots of the User Registration Form, the entire form definition can be downloaded (see link at the end of this blog). Once it is imported you can use Visual Designer to see how the business rules are implemented.

 

User Flow

1. User is asked to enter an email, SmartForms connects to portal webservices to check if such email is already registered (the source code for all webservices is at the end of the blog)

2. If user already has an account, then 'must log in' message is displayed

3. If user is already signed in, the form is automatically populated with user details (user data is automatically brought by SmartForms from Liferay environment).

4. On the next page the user is asked to select a site from the site list obtained via webservice. When user selects a site the description of the site is displayed (webservice again). You can put your own definition of Terms & Conditions together with 'I agree' checkbox.

.

5. If the site the user selected is of 'restricted' type, the user is asked to provide additional information.

SmartForms will automatically handle this rule once you add the following state condition to 'Company/Organisation Details' page. Visual Designer screenshot:

6. The membership request summary is displayed on a final page, allowing the user to save membership request details as PDF file and Submit the form when ready.

7. Processing the submitted form. There are two form submission handlers :

  1. Emailer, which sends a 'membership site request' email to Site Owner(s) if the selected site is 'restricted', attaching PDF that contains all data submitted by the user (implemented using SmartForms workflow, but of course you can use your own).
  2. Webhook, that creates user account if the user is not registered yet and submits membership request of this user to a selected site. (the source code for this webhook  is at the end of the blog).

That's it. That is the user flow, which is probably longer than the implementation notes listed below.

Implementation Notes Data Flow

Below is the generic data flow where the Form Controller could be implemented inside or outside of the portal.

In our implementation we are using SmartForms to build and run User Registration Form. SmartForms Cloud is acting as Form Controller.

 

User Registration Handler Code

User registration handler implements:

  1. SOAP webservices to query portal data and populate form fields
  2. JSON REST endpoint to executes all relevant actions on form submission, creation of portal users and site membership requests

Here we provide source code for essential functions, link to get the full source code is at the end of the blog.

 

Checking if User is Registered

@WebMethod (action=namespace +  "getMemberStatusByEmail" )

public String getMemberStatusByEmail(

         @WebParam (name= "fieldName" )

         String fieldname,

         @WebParam (name= "fields" )

         Field[] fieldList) {

     try {

     

         Map<String, Field> fieldMap = fieldArrayToMap(fieldList);

         Field emailAddressVO = fieldMap.get(FIELDNAME_USER_EMAIL);

         if (emailAddressVO ==  null ) {

             logger.warn( "Call to getMemberStatusByEmail() is misconfigured, cannot find field '" + FIELDNAME_USER_EMAIL +  "'" );

             return "false" ;

         }

         String emailAddress = emailAddressVO.getValue();

         

         if (emailAddress.trim().length() ==  0 ) {

             return "false" ;

         }

             

         try {

             UserLocalServiceUtil.getUserByEmailAddress( this .getCompanyId(fieldMap), emailAddress);

             // no exception, user exists

             return "true" ;

         }  catch (Exception e) {}

         // user is not registered

         return "false" ;

     }  catch (Throwable e) {

         logger.error( "System error " , e);

         return "false" ;

     }

}

 

Getting List of Open and Protected Sites

@WebMethod (action=namespace +  "getSites" )

public Option[] getSites(

         @WebParam (name= "fieldName" )

         String fieldname,

         @WebParam (name= "fields" )

         Field[] fieldList) {

     Option[] blankOptions =  new Option[ 0 ];

     try {      

         Map<String, Field> fieldMap = fieldArrayToMap(fieldList);        

         long companyId =  this .getCompanyId(fieldMap);          

         User user =  null ;

         Field userScreennameField = fieldMap.get(FIELDNAME_USER_SCREENNAME);

         if (userScreennameField !=  null ) {

             if (userScreennameField.getValue().trim().length() >  0 ) {

                 try {

                     user = UserLocalServiceUtil.getUserByScreenName(companyId, userScreennameField.getValue());

                 }  catch (Exception e) {}

             }

         }

         

         List<Option> validGroups =  new ArrayList<Option>();

         LinkedHashMap<String, Object> params =  new LinkedHashMap<String, Object>();

         // limit selection by sites only

         params.put( "site" ,  new Boolean( true ));

         // limit selection by active groups only

         params.put( "active" ,  new Boolean( true ));

         List<Group> allGroupsList = GroupLocalServiceUtil.search(companyId, params , QueryUtil.ALL_POS, QueryUtil.ALL_POS);

         Iterator<Group> allActiveGroups = allGroupsList.iterator();

         while (allActiveGroups.hasNext()) {

             Group group = allActiveGroups.next();

             boolean isAlreadyAMember =  false ;

             // check if user is already a member of it

             if (user !=  null ) {

                 if (group.isGuest()) {

                     // is a member anyway

                     isAlreadyAMember =  true ;

                 }  else {

                     isAlreadyAMember = UserLocalServiceUtil.hasGroupUser(group.getGroupId(), user.getUserId());

                 }

             }

             // add the site to the selection list if this is a regular community site and the user is not already a member of it

             if (group.isRegularSite() && !group.isUser() && !isAlreadyAMember && !group.isGuest()) {

                 // include Open and Restricted sites only

                 if (group.getType() ==  1 || group.getType() ==  2 ) {

                     validGroups.add(  new Option( group.getName(group.getDefaultLanguageId()), String.valueOf(group.getGroupId()) ) );

                 }

             }

         }

         return validGroups.toArray( new Option[validGroups.size()]);

     }  catch (Throwable e) {

         logger.error( "System error " , e);

         return blankOptions;

     }

}

 

Getting Email Addresses of Owners of a Site

@WebMethod (action=namespace +  "getSiteOwnerEmails" )

public String getSiteOwnerEmails(

         @WebParam (name= "fieldName" )

         String fieldname,

         @WebParam (name= "fields" )

         Field[] fieldList) {

     Map<String, Field> fieldMap = fieldArrayToMap(fieldList);

     Group group =  this .getSelectedGroup(fieldMap);

     if (group ==  null ) {

         // no group selected yet

         return "" ;

     }  else if (group.getType() !=  2 ) {

         // this is not a restricted site

         return "" ;

     }  else {

         // check if Terms and Conditions acknowledge is checked, otherwise no point of fetching email addresses

         Field termsAndConditionsField = fieldMap.get(FIELDNAME_TnC_CHECKBOX);

         if (termsAndConditionsField ==  null ) {

             logger.warn( "Call to getSiteOwnerEmails() is misconfigured, cannot find field '" + FIELDNAME_TnC_CHECKBOX +  "'" );

             return "" ;

         }

         if (termsAndConditionsField.getValue().length() ==  0 ) {

             // not checked

             return "" ;

         }

         // make a list of email addresses of site owners for a restricted site

         // this will be used to send 'site membership request' email

         StringBuilder response =  new StringBuilder();

         Role siteOwnerRole;

         try {

             siteOwnerRole = RoleLocalServiceUtil.getRole(group.getCompanyId(), RoleConstants.SITE_OWNER);

         }  catch (PortalException e) {

             logger.error( "Unexpected error" , e);

             return "" ;

         }

         List<User> groupUsers = UserLocalServiceUtil.getGroupUsers(group.getGroupId());

         for ( int i =  0 ; i < groupUsers.size(); i++) {

             User user = groupUsers.get(i);

             if (UserGroupRoleLocalServiceUtil.hasUserGroupRole(user.getUserId(), group.getGroupId(), siteOwnerRole.getRoleId())) {

                 if (response.length() >  0 ) {

                     response.append( ';' );

                 }

                 response.append(user.getEmailAddress());

             }

         }

         logger.info( "compiled site admin emails " + response.toString());

         return response.toString();

     }

}

 

Creating User Account and Site Membership Request

// method to process JSON webhook call

@POST

@Path ( "/user-registration" )

public void newFormSubmittedAsJsonFormat(String input) {

     logger.info( "In /webhook/user-registration" );

     

     /* check authorization */

     String handShakeKey = request.getHeader( "X-SmartForms-Handshake-Key" );

     if (handShakeKey ==  null || !handShakeKey.equals(SMARTFORMS_HADSHAKE_KEY) ) {

         throw new WebApplicationException(Response.Status.UNAUTHORIZED);           

     }

     

     JSONObject data;

     try {

         data = JSONFactoryUtil.createJSONObject(input);

         

         Map<String, String> fields =  this .jsonFormDataToFields(data);

         logger.info( "Have received fields " + fields.size());

         User user =  null ;

         

         ServiceContext serviceContext =  new ServiceContext();

         long groupId = Long.parseLong(fields.get(FIELD_SITE_ID));

         long companyId = Long.parseLong(fields.get(FIELD_COMPANY_ID));

         if (fields.get(FIELD_USER_ID).length() >  0 ) {

             logger.info( "User is already registered" );

             try {

                 user = UserLocalServiceUtil.getUser(Long.parseLong(fields.get(FIELD_USER_ID)));

             }  catch (Exception e) {

                 logger.error( "Unable to fetch user" , e);

                 throw new WebApplicationException(Response.Status.NOT_FOUND);

             }

         }  else {

             // create user

             String firstName = fields.get(FIELD_USER_FIRST_NAME);

             String lastName = fields.get(FIELD_USER_LAST_NAME);

             String email = fields.get(FIELD_USER_EMAIL);

             logger.info( "Creating user " + firstName +  " " + lastName +  " " + email);

             try {

                 // the following data could come from the form, but we just provide some hard-coded value

                 long groups[] =  new long [ 0 ];

                 if (!fields.get(FIELD_SITE_TYPE).equals( "restricted" )) {

                     // this is an open group, add it to the list

                     groups =  new long [ 1 ];

                     groups[ 0 ] = groupId;

                 }

                 

                 long blanks[] =  new long [ 0 ];

                 boolean sendEmail =  false ;

                 Locale locale = PortalUtil.getSiteDefaultLocale(groupId);

                 boolean male =  true ;

                 String jobTitle =  "" ;

                 long suffixId =  0 ;

                 long prefixId =  0 ;

                 String openId =  null ;

                 long facebookId =  0 ;

                 String screenName =  null ;

                 boolean autoScreenName =  true ;

                 boolean autoPassword =  true ;

                 long creatorUserId =  0 ;

                 user = UserLocalServiceUtil.addUser(

                         creatorUserId, companyId,

                         autoPassword,  null ,  null ,

                         autoScreenName, screenName , email,

                         facebookId, openId, locale,

                         firstName,  "" , lastName,

                         prefixId, suffixId, male,

                         1 ,  1 ,  2000 ,

                         jobTitle,

                         groups, blanks, blanks, blanks,

                         sendEmail ,

                         serviceContext);

             }  catch (Exception e) {

                 logger.error( "Unable to create user" , e);

                 throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);

             }

         }

         

         if (fields.get(FIELD_SITE_TYPE).equals( "restricted" )) {

             try {

                 MembershipRequestLocalServiceUtil.addMembershipRequest(user.getUserId(), groupId,

                         "User has requested membership via User Registration Form, you should have received an email" , serviceContext);

             }  catch (PortalException e) {

                 logger.error( "Unable ot add membership request" );;

             }

         }      

     }  catch (JSONException e) {

         logger.error( "Unable to create json object from data" );

         throw new WebApplicationException(Response.Status.BAD_REQUEST);

     }

}  

 

private Map<String, String> jsonFormDataToFields(JSONObject data) {

     Map<String, String> map =  new HashMap<String, String>();

     JSONArray fields = data.getJSONArray( "fields" );

     for ( int i =  0 ; i < fields.length(); i++) {

         JSONObject field = fields.getJSONObject(i);

         logger.info(field.toJSONString());

         map.put(field.getString( "name" ), field.getString( "value" ));

     }

     return map;

}

 

Full source code for Form Handler project can be downloaded from here:

http://extras.repo.smartfor.ms/blogs/Extending-Liferay-User-Registration/userRegistration-source.zip

 

To make it work on your Liferay installation using SmartForms you will need to:

One more thing, in SmartForm webhook configuration please change the localhost to URL of your portal:

Feel free to ask questions if you run into problems ...

Victor Zorin 2018-08-13T07:24:00Z
Categories: CMS, ECM

How to upgrade my sharded environment to Liferay 7.x?

Liferay - Fri, 08/10/2018 - 02:35

Hi Liferay Community,

Before responding this question I would like to explain what's sharding first: to overcome the horizontal scalability concerns of open source databases at the time (circa 2008), Liferay implemented physical partitioning support.  The solution allowed administrators to configure portal instances to be stored in different database instances and database server processes.

This feature was originally named "sharding" although "data partitioning" is more accurate since it requires a small amount of information sharing to coordinate partitions.

Thus, beginning in 7.0, Liferay removed its own physical partitioning implementation in favor of the capabilities provided natively by database vendors. Please, notice that logical partitioning via the "portal instance" concept (logical set of data grouped by the companyId column with data security at portal level) is not affected by this change and it's available in current Liferay versions.

Having explained this, the answer to this question is simple, just the follow the official procedure to do it:
https://dev.liferay.com/discover/deployment/-/knowledge_base/7-0/upgrading-sharded-environment

So Liferay 7.x provides a process which will convert all shards in independent database schemas after the upgrade. This can be suitable for thoses cases where you need to keep information separated for legal reasons. However if you can not afford to maintain one complete environment for every of those independent databases you could try another approach: disable staging by merging all shards into just one database schema before performing the upgrade to Liferay 7.x.

The option of merging all shard schemas into the default one is feasible because sharding generates unique ids per every row among all databases. These are the steps you should follow to achieve this:

  1. Create a backup for the shard database schemas in the production environment.
  2. Copy the content of every table in the non default shards into the default shard. It's recommended to create an SQL script to automate this process.
  3. If a unique index is violated, analyze the data for the two records which cause the issue and remove one of them since it's not necessary anymore (different reasons could cause the creation of data in the incorrect shard in the past such as wrong configuration, a bug, issues with custom developments, etc.)
  4. Resume this process from the last point of failure.
  5. Repeat 3 and 4 until the default shard database contains all data from the other shards.
  6. Clean up the Shard table except for the default shard record.
  7. Startup a Liferay server using this database without the sharding portal.properties:
    1. Remove all database connections except for the default one.
    2. Comment the line META-INF/shard-data-source-spring.xml in the spring.configs property.
  8. Ensure that everything works well and you can access to the different portal instances. 

It is recommended that you keep record of the changes made in the step 3 and 6 since you will need to repeat this process once you decide to go live after merging all databases in the default shard. It is also advisable to do this as a separate project before performing the upgrade to Liferay 7.x. Once you have completed this process you will just need to execute the upgrade as a regular non-shared environment:
https://dev.liferay.com/en/discover/deployment/-/knowledge_base/7-1/upgrading-to-liferay-71

This alternative to upgrade sharded environments is not officially supported but it has been executed succesfully in a couple of installations. For that reason, if you have any question regarding it please write a comment in the this blog entry or open a new thread in the community forums, other members of the community and I will try to assist you during this process.

Alberto Chaparro 2018-08-10T07:35:00Z
Categories: CMS, ECM

Extending Liferay DXP - User Registration (Part 1)

Liferay - Sun, 08/05/2018 - 20:19

I am starting a short series of blogs in which I will show how to customise User Registration / Customer Acquisition process in Liferay.

The inspiration for this blog is one of our DXP portal implementations which had very broad (and frequently changing) user data collection requirements.


There are many scenarios in which you want to customise user registration and account 
creation process:

  • creating a custom user registration form allows delivering a more consistent user experience;
  • when based on information a user provided an approver makes a decision to grant or reject the registration;
  • apart from default fields you can add any user account fields;
  • you can collect any other information related to your business and store it in your custom tables;
  • different registration flows might be required for different groups of users or sites;
  • a wizard-like registration form can deliver more information and help user to make the right choices;
  • users can apply for different roles or permissions at registration;
  • and it could be a place where you might want to display your marketing material.

For now I plan to write three articles:

1. Custom registration process for a portal with multiple sites. 
The main features:

  • Check if a user already has an account
  • Show and allow the user to select which site to join
  • Collect additional information from the user and send email notification to interested parties (approver for restricted site) on form submission
  • Automatically create the user account on form submission if the membership type of the selected site is open, otherwise create the user account but make it inactive (will be activated by approver)


2. Getting more data out of registration form 
The main features:

  •  Add fields to registration form that will be stored in User Account details
  • Collect information to populate user Custom fields
  • Add fields to store in custom tables
  • Allow users to download and print the entries they made to the form for their records


3. Taking payments at registration 
The main features:

  • If payment is required as part of request to join a site, display amount to pay and collect the payment
  • Allow users to download Receipt for their records

 

The Registration form will be based on SmartForms. Of course, you can built your own form/ui  but here are the reasons why you might consider Smartforms:

  • Flexibility - non-technical staff can edit the form and change wording to adjust or add explanations/instructions/conditions on the form.
  • Styling made easy - fully separated from software code and can be done by web designer.
  • Simple Integration with your other systems via webservices to bring data to the form.
  • Easy field value extraction from the submitted form into Liferay data stores or your other systems. 
  • But the most important - cutting costs on the porlet development, as form creation and further adjustments/changes can be done without software modifications.

You can download SmartForms from Liferay marketplace - https://web.liferay.com/marketplace/-/mp/application/106661792 or from SmartForms downloads page, additional information can be located here https://smartfor.ms.

 

Even if you are not going to use Smartforms I hope these blogs will help you as I will be providing samples of how to use Liferay services/APIs for your user registration purposes.

Victor Zorin 2018-08-06T01:19:00Z
Categories: CMS, ECM

The new Web Experience functionalities released in 7.1

Liferay - Fri, 08/03/2018 - 05:52

In Liferay Portal 7.1 a lot of effort has been put in improving the experience of building sites and pages and, as a result, some new functionalities have been introduced to empower marketers and other business users in the creation of stunning websites.

We have achieved this by considering differentiated needs for the marketer, designer and web developer; and adding a new approach to content and page creation. We will continue working to expand the initiative in future releases, but some exciting features have already been introduced.

The key elements added to Web Experience under what we call Modern Site Building are Page Fragments, Content Pages, Display Pages and Multiple Navigation Menus. This is the first entry of a series of posts in which we will have the opportunity to go into more detail of each of those. But, as a starter, we will briefly introduce the big picture and how every component fits in it.

 

The context

Authors, marketers, designers, HR teams or whoever is in charge of creating a piece of content or site pages sometimes face repeat work, but not always, right? Sometimes they are happy to use web content structures because content reusability sounds like pure magic. Some other times they wish there was a faster way to create pages because it is a one-off and what really matters is minimizing the number of clicks while making sure it looks awesome. Put simply, the needs are not always the same.

Liferay has always used a very content centric and decoupled approach. The creation of content in the form of different assets and the display of it have always been two separate things. Thus, the long-established approach to page creation with Liferay is by combining content and applications. These pages are flexible, powerful and offer advanced functionality; but also add complexity to some use cases.

 

The authoring flow considered

For the new functionalities introduced we have considered a design and authoring flow like the one described below.

 

The marketer or the business user needs to create an effective landing page that communicates the desired message and increases customer engagement. The marketer does not have front-end knowledge but would like to have an interface to create site pages in a few clicks with editing options and making sure it looks great.

In order to achieve this, previous work of the Design and Web Development team is necessary. First of all, the designer designs several reusable page parts called Page Fragments (list of contents, headers, title sections, footers…).

The Web developer then implements these designs, coding the appearance and behavior of the Page Fragments and loads them into the Page Fragment collection library. Thereafter, the designer can create Page Templates by adding and combining Page Fragments, making a Page Template collection available for the marketer to directly start working on.

Using this approach, marketers will focus on writing content in Content Pages, using Page Templates created by designers which will consist of Page Fragments previously developed by web developers

 

Key new elements.

With this authoring flow considered, we have introduced new functionalities attending to the different roles and responsibilities in the creation process:

  • Page Fragments, which are collections of “design blocks” created by web developers using HTML, CSS and JavaScript that are made available to the non-technical users to build the Pages by composing them.

  • Content Pages, a new way of creating pages with a few clicks. The main benefits of content page are the easy edition of its content directly on the page.

  • Display Pages, which allow to control how a particular content is displayed in full page.

  • Multiple Navigation Menus, that leverages the Navigation by allowing to introduce several Menus on the header, on the footer, sidebar...

 

Themes with Page Fragments available from the Marketplace

To ease the Blank Page Syndrome you can start standing on some already defined examples. If you want to leverage on existing Page Fragment collections to start exploring the possibilities of the new features right away, you can already download the Fjord theme for Liferay Portal 7.1 from Liferay Marketplace. It contains a series of Page Fragments and Page Templates that can be used as a starter kit. More themes will follow soon!

 


 

I want to know more.

Read along the coming series of posts to learn about Page Fragments, Content Pages, Display Pages and Multiple Navigation Menus.

Also, if you want to learn more about how to empower your team to build modern sites, you can sign in for free to the lesson on “Building Engaging Websites” available in Liferay University or accessing Liferay Documentation.

 

Ianire Cobeaga 2018-08-03T10:52:00Z
Categories: CMS, ECM

New Project SDK Installers 3.2.0 GA1 Released

Liferay - Thu, 08/02/2018 - 21:45

We are pleased to announce the first general available release of Liferay Project SDK Installers that support Liferay 7.1.

New Installers:

For customers, they can download all of them on the customer studio download page.

The installer is the full fledged Liferay Developer Studio installer which installs Liferay workspace, blade, Developer Studio and comes pre-bundled with latest Liferay DXP server. It also supports to config a proxy using for download gradle dependencies.

Upgrade From previous 3.1.x:
  1. Download updatesite here

  2. Go to Help > Install New Software… > Add…

  3. Select Archive..., Browse to the downloaded updatesite

  4. Click OK to close Add repository dialog

  5. Select all features to upgrade, then click > Next, again click > Next and accept the license agreements

  6. Finish and restart to complete the upgrade

Release highlights:

Installers Improvements:

1. Better support HTTP, HTTPS, SOCKS5 proxy

 2. Bundle latest Liferay Portal

   - bundle 7.1.0 GA1 in LiferayProjectSDKwithDevStudioCommunityEdition installers

   - bundle DXP 7.1.10 GA1 in LiferayProjectSDKwithDevStudioDXP installers

Improvements for Deployment:

1. Support Liferay Watch Task

2. Support for Target Platform

3. Improve wizard to download dependencies in background

4. Better deployment support for Liferay DXP/7

   - support DXP 7.1.10 GA1 Tomcat and Wildfly

   - support Liferay 71 CE GA1 Tomcat and Wildfly

   - integration of Blade CLI 3.1.1

   - support Plugins sdk 1.0.19

   - support Liferay Workspace Gradle 1.10.2

5. Third party plugins update

   - update m2e to 1.8.3

   - updategradle plugin buildship to latest 3.0.0

6. Miscellaneous bug fixes

Feedback

If you run into any issues or have any suggestions please come find us on our community forums or report them on JIRA (IDE project), we are always around to try to help you out. Good luck!

Yanan Yuan 2018-08-03T02:45:00Z
Categories: CMS, ECM

Oh no, my URLs disappeared…(and how to get them back)

Liferay - Thu, 08/02/2018 - 02:30

Recently we got a couple of complains about new Web Content article behaviour, specifically about the JournalArticle.getContent() method’s return value. The main problem developers experience is when they embed an Image into the Web Content article or use ddm-image or ddm-document-library field in their structures they expect to see the URL of the object(Image or D&M asset) in the raw XML when using JournalArticle.getContent() method, it actually was there in the 7.0 and the raw XML looked like this:

(...) <dynamic-element name="Image8r1v" type="image" index-type="text" instance-id="ryns"> <dynamic-content language-id="en_US" alt="" name="blonde.png" title="blonde.png" type="journal" fileEntryId="34506" id="34835"> /image/journal/article?img_id=34835&amp;t=1531817578959 </dynamic-content> </dynamic-element> (...)

 

There are two main differences in the 7.1:
We switched from the internal table JournalArticleImage to the common Documents and Media repository as a storage for the Web Content article images
DDM fields for Image and D&M assets changed their internal representation from the URL to the JSON object

Now the raw XML of the article with Images or ddm-image(ddm-documentlibrary) fields looks like this:

(...) <dynamic-element name="Image54q7" type="image" index-type="text" instance-id="wscg"> <dynamic-content language-id="en_US"> <![CDATA[\{ "groupId":"20124","name":"allatonce.png","alt":"", "title":"allatonce.png","type":"journal", "uuid":"80269faa-dea9-fd5a-cb78-3c7aa9da51ea", "fileEntryId":"36774","resourcePrimKey":"36772"} ]]> </dynamic-content> </dynamic-element> (...)

 

It was an internal decision and we didn’t realize that out there could be developers who actually use the raw XML content for their own needs…

First I would like to explain why it was done, not to try to excuse for this case, but to prevent such cases in the future. On the one hand, JournalArticle.getContent() method is a public API and its behaviour must be, at least, backward compatible, but on the other hand its behaviour depends on many components behind it, the signature of the method didn’t change and the implementation details (including the raw XML format of the content) never were published. To avoid such a problem we strongly recommend the developers to use published means for Web Content processing, such as JournalContent and JournalArticleDisplay. Both of them provide processed content of the Article without need to work with the raw XML, clear example can be found in the Web Content Display portlet:

(...) JournalArticleDisplay articleDisplay = _journalContent.getDisplay( article, ddmTemplateKey, viewMode, languageId, page, new PortletRequestModel(renderRequest, renderResponse), themeDisplay); String processedContent = articleDisplay.getContent(); (...) @Reference private JournalContent _journalContent; (...)

 

Also there is a taglib which allows to render a specific journal article using its JournalArticleDisplay instance:

<liferay-journal:journal-article-display articleDisplay="<%= articleDisplay %>" />

 

Or the developer can use JournalContent.getContent() method directly, the result must be the same - processed content where all the fields behave as expected.
Now let’s talk about how to get the URLs back because I understand that it could be a problem to refactor hundreds of lines of your code and the best way for the developers who use the raw XML would be to keep processing the URLs as they were doing it before.
Here I have to mention one detail - there is no way to return to the old format of the URLs for embedded images, so if you have some sort of regular expression catching “/image/journal/article...” - there is no way to make it work again.
There are two options to get the URLs back, both need to adapt your existing code, which works with the raw XML, a little bit.

First option is applicable when you have a concrete file entry ID:

(...) // here fieldValue is raw XML field value for your Image/DM field JSONObject jsonObject = JSONFactoryUtil.createJSONObject(fieldValue); long fileEntryId = jsonObject.getLong("fileEntryId"); FileEntry fileEntry = PortletFileRepositoryUtil.getPortletFileEntry(fileEntryId); String fileEntryURL = PortletFileRepositoryUtil.getDownloadPortletFileEntryURL( themeDisplay, fileEntry, StringPool.BLANK); (...)


And the second option is applicable in case when you don’t have specific file entry ID, but have UUID and group ID of the target entry:

(...) // here fieldValue is raw XML field value for your Image/DM field JSONObject jsonObject = JSONFactoryUtil.createJSONObject(fieldValue); String fileEntryGroupId = jsonObject.getLong("groupId"); String fileEntryUuid = jsonObject.getLong("uuid"); FileEntry fileEntry = PortletFileRepositoryUtil.getPortletFileEntry( fileEntryUuid, fileEntryGroupId); String fileEntryURL = PortletFileRepositoryUtil.getDownloadPortletFileEntryURL( themeDisplay, fileEntry, StringPool.BLANK); (...)

 

Hope these 5 lines of code help you to solve the problem. We understand that it could be frustrating to deal with such changes and we are trying to do our best to avoid them without actual need.
 

Pavel Savinov 2018-08-02T07:30:00Z
Categories: CMS, ECM

Monitor c3p0 in Liferay

Liferay - Tue, 07/31/2018 - 20:05
The Problem

The c3p0 connection pool exposes MBeans for JMX tools like jconsole to monitor its run-time states. However, both c3p0 and Liferay intentionally generate random MBean names on each startup. The random name of each data source makes it difficult to setup an independent monitoring tool.

The second problem is, by default Liferay creates several data sources, one used by most Liferay features, one for the counter service and one for analytics. They all use the same portal property prefix "jdbc.default.", making them identical in every way except for their random bean names.

The Solution

A default c3p0 bean name looks like this:

com.mchange.v2.c3p0:identityToken=z8kflt9r6r9q5c1rhd86p|5bed8ee2,name=z8kflt9r6r9q5c1rhd86p|5bed8ee2,type=PooledDataSource

The default behavior sets the value of both  identityToken and name to the same random string. According to c3p0 documentation, identityToken can be removed with a system property. The second part of the solution is explicitly name the data sources created by Liferay.

Remove identityToken

Add the following line to TOMCAT_HOME/bin/setenv.sh:

JAVA_OPTS="$JAVA_OPTS -Dcom.mchange.v2.c3p0.management.ExcludeIdentityToken=true" Explicitly Name Data Sources

Add the following lines to LIFERAY_HOME/portal-ext.properties:

jdbc.default.dataSourceName=liferayDataSource counter.jdbc.prefix=jdbc.counter. jdbc.counter.dataSourceName=counterDataSource jdbc.analytics.dataSourceName=analyticsDataSource

These lines name the default data source liferayDataSource, the counter service data source counterDataSource and similarly analyticsDataSource. These names are all arbitrary, so you can chose other desired values.

The second line gives the counter service data source a new property prefix "jdbc.counter." Use this prefix to set counterDataSource properties you wish to be different from the liferayDataSource. Conversely, any properties not set with this prefix will use the value set using the "jdbc.default." prefix.

Now in JMX, c3p0 beans look like this:

  • com.mchange.v2.c3p0:name=analyticsDataSource,type=PooledDataSource
  • com.mchange.v2.c3p0:name=counterDataSource,type=PooledDataSource
  • com.mchange.v2.c3p0:name=liferayDataSource,type=PooledDataSource

Name Other Data Sources

If your Liferay is configured with other data source(s), you must also give each of them a unique name. Otherwise, multiple MBeans will have the same name as liferayDataSource, and only one of them will be detected by JMX tools.

For example, if you have a third data source defined with prefix "jdbc.sap.", add this line to LIFERAY_HOME/portal-ext.properties:

jdbc.sap.dataSourceName=sapDataSource

Then you should see these data sources:

  • com.mchange.v2.c3p0:name=analyticsDataSource,type=PooledDataSource

  • com.mchange.v2.c3p0:name=counterDataSource,type=PooledDataSource

  • com.mchange.v2.c3p0:name=liferayDataSource,type=PooledDataSource

  • com.mchange.v2.c3p0:name=sapDataSource,type=PooledDataSource

What to Watch

In a JMX monitoring tool, for each data source, monitor these 3 properties of its MBean:

  • numConnectionsAllUsers

  • numBusyConnectionsAllUsers

  • numIdleConnectionsAllUsers

An alert should be configured when numBusyConnectionsAllUsers exceeds a certain percentage of maxPoolSize, which forecasts a possible exhaustion of database connections with that data source.

.

Michael Chen 2018-08-01T01:05:00Z
Categories: CMS, ECM

Dynamic Widget

Liferay - Tue, 07/31/2018 - 15:20
Genesis

My colleague Vagif proposed a new way to develop a Liferay dynamic widget (the best name I can think of for now) with these pieces:

  1. Use the “Basic Web Content” structure or define a new structure.
  2. For each dynamic widget type, define a new display template.
  3. Create an OSGi service that returns a data model to be used by the display template.
  4. Create a web content article with the above structure/template pair.
  5. Place this web content article onto the page with Web Content Display (OOTB).

All business logics are in the OSGi service. The structure can serve as a preference or parameter holder for the display template. The display template draws most meaningful data from the model returned by the service.

You can build a portlet-less Liferay site with this "Crafter-ish" approach.

Rationales

A typical Liferay feature has program code spread out among many technologies: service Java code, portlet Java code, template code, template Java code (JSP), language files, property files, XML files, etc. You often find a feature morphs into some JSP files with 100+ lines of Java code, a 2000 line portlet, plus several service builders, all competing for business logic implementations and MVC roles.

A Simpler Pattern

Dynamic widget may serve as a Liferay implementation pattern that simplifies and promotes good programming practices (I think this is better pattern, but some may disagree):

  • Write Java code in and only in service modules (no portlet).
  • Implement all business logic in services.
  • The display template calls a single service to retrieve a data model.
  • The display template then renders the model with almost no business logic.

A Simple Rule:

  • Let the services build the most foolproof model for the template to render it in the most simplistic way.

There is nothing stopping a template from calling multiple services then juggle multiple models to render the view. However, do your self a favor, write a new service method that combines all of them into a single model for the template. You will thank yourself later.

Why Freemarker?

Cannot use JSP may be a down side of dynamic widget, or is it?

Compared to JSP, template languages like Freemarker cannot mix Java code with UI code, so they promote a cleaner separation of view from model and controller. Some may argue that template code getting the model from services is controller-ish. Agree, but beyond that keeping Freemarker code simple is not hard at all, because complex Freemarker code is ugly and painful to write, not to mention tough to debug, log, handle errors, etc.

Pseudo Web Content

Dynamic widget is a Liferay web content, but its "content" is not in its web content structure. The content comes from the services. If you think of a Web Content Display rendering a dynamic widget in a page as calling a function in a program, then its web content structure is like the parameter for that function call.

The widget's web content may be empty, which is similar to calling a function with no parameter. The service that builds the model for the template has everything it needs. In other instances, the structure can have variables used as parameters for calling the service or be used directly by the template just like any other web content.

Search and Preview

Note that variables in the web content structure can be searchable, making them available to Liferay search and Assert Publisher. For example, a dynamic widget may be titled "Annual Sales Figures 2018", which renders a bar chart from a model provided by a service module. Then this widget may be a search hit, along with its pretty chart if you wish. You can't have that if the chart is implemented in a portlet.

Don't forget, you can easily mark the web content not searchable too:

Another convenience of dynamic widget over portlet is preview. Open Liferay Control Panel > (a site) > Web Content. You can preview a widget independent of a page from its Options menu:

Managing Display Templates

For the rest of this topic, lets call the template associated with the web content structure the parent template. The parent template can render the entire widget all by itself, but it's much more likely for the parent template to use other templates with the '<#include />' Freemarker directive.

Here we discuss three ways to develop templates for dynamic widgets.

Liferay Generic Templates

Open Liferay Control Panel > (a site) > Web Content > (corner Options menu) > Templates. Note the first column of table view called "ID". The value of that column is the template ID, also known as template key.

From this Templates view, you can create a template and leave the "Structure" field empty. That creates a generic template. Then in a parent template, you can use a generic template like this:

<#include "${templatesPath}/TEMPLATE_KEY"/>

With this framework, you can implement everything inside Liferay user interface. However, exporting then importing generic templates a LAR file will invalidate all template keys. All parent templates must be manually modified with new keys.

Package Templates In A Module

In the attached xyz-templates.zip, "modules/my-templates" demonstrates packaging Freemarker templates in a OSGi module. The key for parent templates to use templates in this module is the "Web-ContextPath" header:

modules/my-templates/bnd.bnd Bundle-Name: My Templates Bundle Bundle-SymbolicName: my.liferay.templates Bundle-Version: 1.0.0 Web-ContextPath: /my-templates

In a parent template, use the Liferay infix _SERVLET_CONTEXT_ like this:

<#include "my-templates_SERVLET_CONTEXT_/widget/software-project.ftl" />

where before the infix is the "Web-ContextPath" value, and after which is the template path in the module project under "src/main/resources".

The Liferay Freemarker engine allows a template to include sibling templates in the same module using relative path of the current template. For example,

modules/my-templates/src/main/resources/widget/software-project.ftl <h3>software-project.ftl</h3> From parent path: <#include "../common/left-navigation.ftl"/> From child path: <#include "more/right-navigation.ftl"/>

Now the fun part of how Freemarker templates find and call services. First of all, you need to remove Liferay's default restriction on template variables by creating this configuration file as the following:

LIFERAY_HOME/osgi/configs/com.liferay.portal.template.freemarker.configuration.FreeMarkerEngineConfiguration.cfg restrictedVariables=

Then a template can access services in several ways, for example, using the "staticUtil" or "serviceLocator" variables:

modules/my-templates/src/main/resources/widget/software-project.ftl Using restricted variables: <#assign userLocalServiceUtil = staticUtil['com.liferay.portal.kernel.service.UserLocalServiceUtil'] /> ${userLocalServiceUtil.getDefaultUserId(companyId)}, <#assign userLocalService = serviceLocator.findService('com.liferay.portal.kernel.service.UserLocalService') /> ${userLocalService.getDefaultUser(companyId).originalEmailAddress}

Refer to the "Extras" section for a complete list of all Liferay variables available to Freemarker templates.

Package Templates In A Theme

The attached xyz-templates.zip also includes an example theme in "wars/my-theme". Following the folder convention of a theme, templates in the projects are under folder "src/main/webapp/templates". The Gradle build automatically generates a "Web-ContactPath" header with the value of the project folder. Therefore, a parent template can reference a template in this theme as:

<#include "my-theme_SERVLET_CONTEXT_/templates/widget/software-project.ftl" />

Other aspects of templates in a theme are identical to ones in a module.

Deployment

You can deploy both the module JAR and the theme WAR files by copying them to the Liferay auto deploy folder. Here is what they look like when running:

$ telnet localhost 11311 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. ____________________________ Welcome to Apache Felix Gogo g! lb my START LEVEL 20 ID|State |Level|Name 759|Active | 10|Liferay My Account Web (1.0.11) 826|Active | 10|Liferay Portal Security AntiSamy (2.0.12) 994|Active | 10|Liferay Site My Sites Web (1.0.8) 1232|Active | 10|My Templates Bundle (1.0.0) 1237|Active | 10|my-theme (7.0.10) g! Extras

A complete list of all Liferay variables available to Freemarker templates, including their implementation class names and snippets of Liferay source code:

FreeMarkerEngineConfiguration.java: @Meta.AD( deflt = "serviceLocator|utilLocator|objectUtil|staticFieldGetter|staticUtil", required = false ) public String[] restrictedVariables(); com.liferay.portal.template.TemplateContextHelper.getHelperUtilities(, false) { accountPermission=com.liferay.portal.service.permission.AccountPermissionImpl, arrayUtil=com.liferay.portal.kernel.util.ArrayUtil_IW, auditMessageFactoryUtil=com.liferay.portal.security.audit.internal.AuditMessageFactoryImpl, auditRouterUtil=null, browserSniffer=com.liferay.portal.servlet.BrowserSnifferImpl, calendarFactory=com.liferay.portal.util.CalendarFactoryImpl, commonPermission=com.liferay.portal.service.permission.CommonPermissionImpl, dateFormatFactory=com.liferay.portal.util.FastDateFormatFactoryImpl, dateFormats=com.liferay.portal.util.FastDateFormatFactoryImpl, dateTool=May 5, 2018 10:34:58 AM, dateUtil=com.liferay.portal.kernel.util.DateUtil_IW, escapeTool=org.apache.velocity.tools.generic.EscapeTool, expandoColumnLocalService=com.liferay.portlet.expando.service.impl.ExpandoColumnLocalServiceImpl, expandoRowLocalService=com.liferay.portlet.expando.service.impl.ExpandoRowLocalServiceImpl, expandoTableLocalService=com.liferay.portlet.expando.service.impl.ExpandoTableLocalServiceImpl, expandoValueLocalService=com.liferay.portlet.expando.service.impl.ExpandoValueLocalServiceImpl, getterUtil=com.liferay.portal.kernel.util.GetterUtil_IW, groupPermission=com.liferay.portal.service.permission.GroupPermissionImpl, htmlUtil=com.liferay.portal.util.HtmlImpl, httpUtil=com.liferay.portal.util.HttpImpl, imageToken=com.liferay.portal.webserver.WebServerServletTokenImpl, imageToolUtil=com.liferay.portal.image.ImageToolImpl, iteratorTool=org.apache.velocity.tools.generic.IteratorTool, jsonFactoryUtil=com.liferay.portal.json.JSONFactoryImpl, languageUtil=com.liferay.portal.language.LanguageImpl, layoutPermission=com.liferay.portal.service.permission.LayoutPermissionImpl, listTool=org.apache.velocity.tools.generic.ListTool, localeUtil=com.liferay.portal.kernel.util.LocaleUtil, locationPermission=com.liferay.portal.service.permission.OrganizationPermissionImpl, mathTool=org.apache.velocity.tools.generic.MathTool, numberTool=org.apache.velocity.tools.generic.NumberTool, organizationPermission=com.liferay.portal.service.permission.OrganizationPermissionImpl, paramUtil=com.liferay.portal.kernel.util.ParamUtil_IW, passwordPolicyPermission=com.liferay.portal.service.permission.PasswordPolicyPermissionImpl, portal=com.liferay.portal.util.PortalImpl, portalPermission=com.liferay.portal.service.permission.PortalPermissionImpl, portalUtil=com.liferay.portal.util.PortalImpl, portletModeFactory=com.liferay.portal.kernel.portlet.PortletModeFactory_IW, portletPermission=com.liferay.portal.service.permission.PortletPermissionImpl, portletProviderAction={ADD=ADD, BROWSE=BROWSE, MANAGE=MANAGE, EDIT=EDIT, PREVIEW=PREVIEW, VIEW=VIEW}, portletURLFactory=com.liferay.portlet.PortletURLFactoryImpl, prefsPropsUtil=com.liferay.portal.util.PrefsPropsImpl, propsUtil=com.liferay.portal.util.PropsImpl, randomizer=com.liferay.portal.kernel.util.Randomizer, rolePermission=com.liferay.portal.service.permission.RolePermissionImpl, saxReaderUtil=com.liferay.portal.xml.SAXReaderImpl, serviceLocator=com.liferay.portal.template.ServiceLocator, sessionClicks=com.liferay.portal.kernel.util.SessionClicks_IW, sortTool=org.apache.velocity.tools.generic.SortTool, staticFieldGetter=com.liferay.portal.kernel.util.StaticFieldGetter, stringUtil=com.liferay.portal.kernel.util.StringUtil_IW, timeZoneUtil=com.liferay.portal.kernel.util.TimeZoneUtil_IW, unicodeFormatter=com.liferay.portal.kernel.util.UnicodeFormatter_IW, unicodeLanguageUtil=com.liferay.portal.language.UnicodeLanguageImpl, userGroupPermission=com.liferay.portal.service.permission.UserGroupPermissionImpl, userPermission=com.liferay.portal.service.permission.UserPermissionImpl, utilLocator=com.liferay.portal.template.UtilLocator, validator=com.liferay.portal.kernel.util.Validator_IW, velocityPortletPreferences=, webServerToken=com.liferay.portal.webserver.WebServerServletTokenImpl, windowStateFactory=com.liferay.portal.kernel.portlet.WindowStateFactory_IW, }

.

Michael Chen 2018-07-31T20:20:00Z
Categories: CMS, ECM

A Simplified Pattern for Liferay 7 Services

Liferay - Tue, 07/31/2018 - 13:07
Introduction

This is a simplified OSGi service API and implementation pattern. It follows the traditional Java interface-implementation pattern, in which the programmer is only required to keep the interface class and implementation class in sync. It does not use Liferay 7 service builder.

The attached archive is a fully implemented ORM service based on MyBatis. Unzip it into the modules folder in a Blade created workspace. A script to create the back-end database will be added soon to make this example fully operational.

Implementation Pattern

In this ORM example, there are two top level packages: 'api' and 'impl'. The 'api' and its children packages are to be exported and used by consumers of this service API. The 'impl' and its children packages are for implementation only and should remain private packages of this OSGi module.

Factory Usage Pattern

The 'api.Factory' class is the access point for consumers to get the services they need. A consumer class uses the Factory like this:

import com.acme.orm.api.bean.Order import static com.acme.orm.api.Facotry.getOrderLocalService; class OnlineStore { public Order checkOrder(String orderId) { Order order = getOrderLocalService().getOrderDetailsById(orderId); // Do something else return order; } }

In order to preserve OSGi's runt-time life cycle management of this module (start, stop, install, uninstall), it is important NOT to keep a reference of the service object obtained from the Factory:

// DO NOT DO THIS OrderLocalService myService = getOrderLocalService(); // DO NOT DO THIS OrderLocalService myService = Factory.getOrderLocalService();

The Liferay service builder went to great length to prevent programmers from keeping a reference of the service object by generating and forcing people to use the static methods in the XyzServiceUtil class. It also creates other confusing and irrelevant artifacts: XyzServiceBaseImpl, XyzServiceWrapper and two projects (two jars) for one service.

Instead of making it foolproof with all those complexities, why not just tell programmers, a very intelligent bunch, not to keep references of OSGi service objects. The result is this clean implementation pattern, with no generated artifact, and two easy to understand rules:

  • Keep the API interface class in sync with the implementation class.
  • Do not keep a reference of the service object obtained from the Factory.
Understanding ServiceTracker

When an OSGi module (or bundle) is replaced at run-time due to it being stopped, started, uninstalled or re-installed, the desired effect is that the services provided by that module be replaced as well. ServiceTracker is the OSGi class that keeps track of module life cycle changes. Module life cycle changes are transparent to service consumer code as long as the consumer code always access the service from the ServiceTracker.

OSGi is a component framework running in a JVM instance that exhibits the same run-time behavior as any Java programs. When the consumer code saves a reference to a service object, that service object will live on even when OSGi replaced its module with a new instance. That service object now becomes an orphan and out-dated instance only known to that consumer code. This is the reason for not keeping a reference to the service.

In this implementation pattern, the Factory class retrieves the service object from its corresponding ServiceTracker. The getService() method of the ServiceTracker shields the module's life cycle changes from the consumer code:

@ProviderType public class Factory { private static ServiceTracker<OrderLocalService, OrderLocalService> _OrderLocalService = ServiceTrackerFactory.open(OrderLocalService.class); public static OrderLocalService getOrderLocalService() { return _OrderLocalService.getService(); } } Local vs. Remote Service

The differences between a Liferay 7.0 local service and remote service are:

  1. The base interface of the API.
  2. Specific annotations for the remote interface.

In the ORM example, OrderLocalService is local service interface:

Local Service API Declaration @ProviderType @Transactional(isolation = Isolation.PORTAL, rollbackFor = { PortalException.class, SystemException.class}) public interface OrderLocalService extends BaseLocalService { }

while OrderService is the remote service interface exposed as RESTful web service:

Remote Service API Declaration @AccessControlled @JSONWebService @OSGiBeanProperties(property = { "json.web.service.context.name=acme", "json.web.service.context.path=Order" }, service = OrderService.class) @ProviderType @Transactional(isolation = Isolation.PORTAL, rollbackFor = { PortalException.class, SystemException.class}) public interface OrderService extends BaseService { public Order getOrderDetailsById(String orderId); }

This RESTful web service can be found in the following catalog under Context Name "acme" (click the drop down box to find "acme" or other context names):

http://localhost:8080/api/jsonws?contextName=acme

Both the local and remote service implementation classes just implement their corresponding API interfaces. In the ORM example, OrderLocalServiceImpl is the local implementation that does the actual work of mapping to the database. The remote implementation, as shown below, simply calls the local Factory services:

Remote Service Implementation @ProviderType public class OrderServiceImpl implements OrderService { public Order getOrderDetailsById(String orderId) { return Factory.getOrderLocalService().getOrderDetailsById(orderId); } } Development Details

Here are some key files for creating and implementing a service in this pattern.

Eclipse .project and .classpath

These two files in the example archive must be used to start your service project for Eclipse to recognize it as a Gradle project. You can change the project name in the <name> tag of the .project file before importing the project to Eclipse:

<name>orm.api</name>

You must also create these two folder structures to hold your Java and resource files:

src/main/java
src/main/resources

Once imported to Eclipse, be sure to right click on the project and select "Gradle" -> "Refresh Gradle Project". You can also do the same thing with the parent Gradle project created by Liferay Blade.

build.gradle

The two 'org.osgi:org.osgi.*' dependencies are required for OSGi features.

dependencies { compile group: "com.liferay", name: "com.liferay.osgi.util", version: "3.0.3" compile group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0" compile 'org.osgi:org.osgi.core:5.0.0' compile 'org.osgi:org.osgi.annotation:6.0.0' compile group: 'javax.servlet', name: 'servlet-api', version: '2.5' compile group: "org.mybatis", name: "mybatis", version: "3.4.1" compile files('./resources/lib/sqljdbc4.jar') compileOnly group: "com.liferay", name: "com.liferay.journal.api", version: "1.0.0" } bnd.bnd

All packages under 'api' should be exported in the "Export-Package:" setting. The "Liferay-Spring-Context:" setting directs Liferay to load the Spring bean definition in the module-spring.xml file discussed below. "Lifer-Require-SchemaVersion:", "Liferay-Service:" and "Require-Capability:" settings are also required.

Bundle-Version: 1.0.0 Bundle-ClassPath: .,lib/sqljdbc4.jar Export-Package: \ com.acme.orm.api,\ com.acme.orm.api.bean,\ com.acme.orm.api.exception Import-Package: \ !com.microsoft.sqlserver.*,\ !microsoft.sql.*,\ !com.sun.jdi.*,\ !net.sf.cglib.proxy.*,\ !org.apache.logging.*,\ * Include-Resource: @mybatis-3.4.1.jar Liferay-Require-SchemaVersion: 1.0.0 Liferay-Service: true Liferay-Spring-Context: META-INF/spring Require-Capability: liferay.extender;filter:="(&(liferay.extender=spring.extender)(version>=2.0)(!(version>=3.0)))" src/main/resources/META-INF/spring/module-spring.xml

For each bean definition, the "class=" value is the implementation class name, and the "id=" value is the interface class name.

<?xml version="1.0"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" default-destroy-method="destroy" default-init-method="afterPropertiesSet" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean class="com.acme.orm.impl.CommunicationMediumLocalServiceImpl" id="com.acme.orm.api.CommunicationMediumLocalService" /> <bean class="com.acme.orm.impl.MessageCenterLocalServiceImpl" id="com.acme.orm.api.MessageCenterLocalService" /> <bean class="com.acme.orm.impl.NSMUserLocalServiceImpl" id="com.acme.orm.api.NSMUserLocalService" /> <bean class="com.acme.orm.impl.OrderLocalServiceImpl" id="com.acme.orm.api.OrderLocalService" /> <bean class="com.acme.orm.impl.OrderServiceImpl" id="com.acme.orm.api.OrderService" /> <bean class="com.acme.orm.impl.RoutingAreaLocalServiceImpl" id="com.acme.orm.api.RoutingAreaLocalService" /> <bean class="com.acme.orm.impl.WebContentArticleLocalServiceImpl" id="com.acme.orm.api.WebContentArticleLocalService" /> </beans>

.

Michael Chen 2018-07-31T18:07:00Z
Categories: CMS, ECM

How to disable XML Log Files in Liferay 7.x

Liferay - Tue, 07/31/2018 - 11:52

Liferay 7.0 started to produce a pair of log files per day:

ls -l /opt/liferay/home7/logs/ total 3104 -rw-r--r-- 1 michael admin 285201 May 26 13:24 liferay.2016-05-26.log -rw-r--r-- 1 michael admin 898027 May 26 13:24 liferay.2016-05-26.xml -rw-r--r-- 1 michael admin 400811 Aug 19 13:08 liferay.2016-08-19.log -rw-r--r-- 1 michael admin 0 Aug 19 12:26 liferay.2016-08-19.xml

To disable Liferay logging to XML files, create portal-log4j-ext.xml in the following path:

tomcat-8.0.32/webapps/ROOT/WEB-INF/classes/META-INF/portal-log4j-ext.xml <?xml version="1.0"?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">       <appender name="XML_FILE" class="org.apache.log4j.ConsoleAppender"/>       <root>         <priority value="INFO" />         <!--appender-ref ref="CONSOLE" /-->         <appender-ref ref="TEXT_FILE" />         <!--appender-ref ref="XML_FILE" /-->     </root> </log4j:configuration>

This file overrides two elements of META-INF/portal-log4j.xml in ROOT/WEB-INF/lib/portal-impl.jar, which stops Liferay from copying log entries to catalina.out and the daily XML log file.

You can also create the extension XML file as a copy of the original in the JAR as the following:

cd tomcat-8.0.32/webapps/ROOT/WEB-INF unzip -p lib/portal-impl.jar META-INF/portal-log4j.xml > classes/META-INF/portal-log4j-ext.xml

.

Michael Chen 2018-07-31T16:52:00Z
Categories: CMS, ECM

Joomla 3.8.11 Release

Joomla! - Tue, 07/31/2018 - 08:45

Joomla 3.8.11 is now available. This is a bug fix release for the 3.x series of Joomla including over 35 bug fixes and improvements.

Categories: CMS

Blade Project Version

Liferay - Mon, 07/30/2018 - 13:08

TL;DR - Add "-v 7.0" to your Blade command line to create modules for Liferay CE 7.0 and Liferay DXP 7.0.

Hey, just a quick blog post here for something that developers may need to know...

I updated my Blade command line tool recently (since I use Intellij, I rely on the command line tool quite a bit for new module project creation), but I hadn't started a new module after updating.

That changed last Friday... Had a hot customer request for a customization so I used Blade to create a new workspace and started creating my modules. Modules loaded into Intellij just fine, and I happily start cranking out the code.

When I was happy with development, I built my 3 modules (SB API and Service modules and one UI module) and dropped them into my Liferay 7.0 DXP deployment folder.

Normally, I see the happy messages that my modules have started. I mean, these modules were not rocket science and they had no weird transitive dependency issues, so they should have just started.

But none of them did, not even the API module and those never have an issue starting.

So I fire up the Gogo shell, issue my lb command, and there are my modules at the end of the list, all in the Installed state.

So I command Gogo to start my API module, but it won't start, it has unresolved references on portal-kernel 3.0 and other such references.

I realize right away that portal-kernel 3.0 is from Liferay 7.1, but I wasn't building anything for Liferay 7.1, I'm still building for 7.0.

All of my modules suffered from the same problem, all had references to 7.1 artifacts.

Not knowing any better, I went back into the build.gradle files for my 3 modules and updated all of the versions so they were back at the normal 7.0 versions, rebuilt and redeployed and all was good.

So I asked my friend Greg Amerson what the deal was, and he informed me that the new default project version for the Blade tool is now 7.1. To have Blade create a project for Liferay CE 7.0 or Liferay DXP 7.0, you have to add "-v 7.0" to the Blade command line arguments.

So, there you go.  Avoid my Blade update headaches and just remember to tack on the "-v 7.0" for all of your new 7.0 modules.

Update 08/2018

So I checked with the folks that know, unfortunately there is no way to "default" the new blade to 7.0.  There are plans on the roadmap, but I don't know what that means for when it is available.

In the mean time, if you are using and developing for Liferay CE 7.0 or Liferay DXP 7.0, don't upgrade your blade until you move to 7.1 or they get the default setting in place.

If you did upgrade your blade, you can either remember to add the -v 7.0 args to your invoke of blade, or the easier option might be to just revert to an older version of Blade.

All you need to do is:

  1. jpm remove blade
  2. jpm install -f https://releases.liferay.com/tools/blade-cli/3.1.0.201807032155/blade.jar

That should get you back to a pre-7.1 version where the default will be 7.0.

David H Nebinger 2018-07-30T18:08:00Z
Categories: CMS, ECM

American Councils for International Education

Drupal - Fri, 07/27/2018 - 08:11
Completed Drupal site or project URL: https://www.americancouncils.org

Since 1974, American Councils for International Education (AC) has been making international education accessible for all. And today, the organization has built a global community of 85 countries and 89,000 alumni through cultural and academic exchanges, research assessments, language immersion programs, and professional development. AC’s alumni include everyone from high school students to professionals, national leaders, ministers, members of parliament, ambassadors, and CEOs.

AC recently created a new set of strategic goals, and brought in Threespot, a digital communications agency, for a website redesign. The goals of the project were to expand global impact, strengthen the AC brand, and build a stronger understanding of their work. Threespot partnered with Inclind, AC’s ongoing Drupal development partner, to implement the new design and upgrade the website's functionality.

DC-based Threespot provides digital strategy, creative, and development services exclusively for organizations and ideas that align with their progressive values. Known for strong collaborative capabilities, Inclind brings to the table nearly 20 years of experience developing, designing, supporting and maintaining web content management systems. With AC's mission to prepare tomorrow's leaders for an ever-changing world and its need for a more modern and sophisticated Drupal site, a collaboration among the three organizations was a no-brainer.

“We’ve worked with Inclind in the past and they’re a trusted partner,” says Liz Ott, Director of Client Engagement with Threespot. “With collaborative projects like this, there’s a real value in leveraging partner agencies for their strengths. Our track record working strategically with progressive nonprofits dovetailed nicely with Inclind’s strengths as Drupal developers, giving AC the best of both worlds.”

Categories: CMS
Syndicate content