Adding Propagate All...

Add a Propagate All for your fragment propagation needs...

Introduction

So when you start building your own fragments and using them on your content pages, you will often times run into an issue with propagation...

So I like to create fragments for headers and footers and then use those fragments on every page that I build. But when I update those fragments, I have to go into the usages panel to do the propagation so they all get updated.

If you have a single page of usages or a couple of pages, this is not too bad. You hit the checkbox in the bar and then you can pick the Propagate button to update them all. When there are multiple pages, you just repeat this until you've finished all of the updates.

However, if you have many pages of results to get through, this UI can be quite painful. Checkbox, Propagate, wait for page to update, Checkbox, Propagate, Wait, Checkbox, Propagate, Wait, ... I have a site with 750+ pages which (using the 20 per page) translates to 38 pages or 38 times I have to go through this title. Even if I change the window size, I'm still going to have to go through this over and over again...

Given this UI annoyance, I decided to set out and fix it, and I'm going to share it with you because there's lots of learning opportunities here...

Challenge #1 - Add an MVCActionCommand to the Liferay Fragment Portlet

When you start digging into the modules/apps/fragment/fragment-web module that has the Fragment portlet, you'll find that it is a typical Liferay MVC-based portlet. It does have a PropagateFragmentEntryChangesMVCActionCommand which shows us how the checked fragment entries would each be propagated, so this is a good source for how we would be doing our implementation.

You may not be aware, but it is super easy to add a custom command to an existing [Liferay] portlet. All you need is a module with your MVCActionCommand implementation and have the right properties to make it work.

I'm not going to step you through how I built my implementation, but the code should be pretty much self-explanatory:

/**
* class PropagateAllFragmentEntriesMVCActionCommand: This new mvc action command will 
* enable propagating all changes for the fragment to every usage.
*
* @author dnebinger
*/
@Component(
  immediate = true,
  property = {
    "javax.portlet.name=" + FragmentPortletKeys.FRAGMENT,
    "mvc.command.name=/fragment/propagate_all_fragment_entries"
  },
  service = MVCActionCommand.class
)
public class PropagateAllFragmentEntriesMVCActionCommand extends BaseMVCActionCommand {
  @Override
  protected void doProcessAction(ActionRequest actionRequest,
      ActionResponse actionResponse)
      throws Exception {
    // we should probably check to see if the user has
    // permission to propagate before doing anything else
    
    // get the fragment entry to propagate
    final long fragmentEntryId =
      ParamUtil.getLong(actionRequest, "fragmentEntryId");
    
    if (fragmentEntryId == 0) {
      if (_log.isInfoEnabled()) {
        _log.info("Propagate all with invalid fragment entry id");
      }
      throw new PortalException("Fragment entry id is required");
    }
    
    // Get the fragment entry, it will provide the key fields necessary for propagation
    final FragmentEntry fragmentEntry =
      _fragmentEntryLocalService.getFragmentEntry(fragmentEntryId);
    
    // to handle the propagation, we'll use an actionable dynamic query...
    ActionableDynamicQuery propagationQuery =
      _fragmentEntryLinkLocalService.getActionableDynamicQuery();
    
    // match on group and fragment entry id
    propagationQuery.setAddCriteriaMethod(
      new ActionableDynamicQuery.AddCriteriaMethod() {
        @Override
        public void addCriteria(DynamicQuery dynamicQuery) {
          dynamicQuery.add(RestrictionsFactoryUtil.eq(
            "groupId", fragmentEntry.getGroupId()));
          dynamicQuery.add(RestrictionsFactoryUtil.eq(
            "fragmentEntryId", fragmentEntryId));
        }
    });
    
    // for each fragment entry link for this fragment entry
    propagationQuery.setPerformActionMethod(
      new ActionableDynamicQuery.PerformActionMethod<FragmentEntryLink>() {
        @Override
        public void performAction(FragmentEntryLink fragmentEntryLink)
          throws PortalException {
          if (_log.isDebugEnabled()) {
            _log.debug("Updating the fragment entry link {} to the latest version",
              fragmentEntryLink.getFragmentEntryLinkId());
          }
          
          // use the link service to it to update to the latest version
          _fragmentEntryLinkLocalService.updateLatestChanges(
            fragmentEntryLink.getFragmentEntryLinkId());
        }
    });
    
    try {
      // do the propagation
      propagationQuery.performActions();
    } catch (PortalException e) {
      _log.error("Error updating the fragment entry links: {}", e.getMessage(), e);
      throw e;
    }
  }
  
  @Reference(unbind = "-")
  private FragmentEntryLocalService _fragmentEntryLocalService;
  
  @Reference(unbind = "-")
  private FragmentEntryLinkLocalService _fragmentEntryLinkLocalService;
  
  private static final Logger _log = LoggerFactory.getLogger(
    PropagateAllFragmentEntriesMVCActionCommand.class);
}

So this is basically your typical MVCActionCommand implementation, but from the javax.portlet.name property above I'm able to bind this new MVCActionCommand to the existing Liferay portlet. This would be a class in a completely separate module jar, but I'm attaching it to another portlet.

There are some important things to remember with an implementation like this... I'm only able to attach my new action to the Liferay portlet, but this doesn't mean I have carte blanche access to all of the classes and stuff that are part of the Liferay Fragment portlet. I can only use whatever the Liferay Fragment portlet has exported. Fortunately for me, I didn't really need any of its internal stuff, so I was okay for this implementation.

Now if I build and deploy this module, it will start up cleanly, but I can't really use it yet because nothing in the Liferay portlet knows how to invoke the new MVCActionCommand. To make that happen, I'll need to override the JSP.

Challenge #2 - Override the Liferay Fragment Portlet JSP

So the action is ready, but now the portlet needs to be able to invoke it. After looking at the Liferay JSP for the Fragment portlet, I've decided that I just need to override the view_fragment_entry_usages.jsp page.

To make things easy on me, I started by creating a fragment module using the blade tool, then I updated the bnd.bnd file with a version range:

Bundle-Name: Santander Fragment Propagation
Bundle-SymbolicName: fragment.propagation.web
Bundle-Version: 1.0.0
Fragment-Host: com.liferay.fragment.web;bundle-version="[2.0.50,3.0.0)"

By using a version range, I can continue to apply new fixpacks into my DXP environment without having to recompile my fragment bundle. The risk here, though, is that if Liferay does an update to this JSP to fix, say, a security issue, my JSP file might not have that fix but it will still be overriding Liferay's patched version. So if you do go this route, try to remember to check Liferay's version for important updates that you should pull into your override file.

Speaking of which, the only other file I have in my fragment bundle is my overriding view_fragment_entry_usages.jsp page. I'm not going to show the whole thing here, but I can share the portion that I added to the original. I started by copying the Liferay file, then I inserted the following lines at line number 118:

<portlet:actionURL name="/fragment/propagate_all_fragment_entries" 
      var="propagateAllFragmentEntriesURL">
  <portlet:param name="redirect" value="<%= currentURL %>" />
  <portlet:param name="fragmentEntryId" 
    value="<%= String.valueOf(fragmentEntry.getFragmentEntryId()) %>" />
</portlet:actionURL>

<aui:form action="<%= propagateAllFragmentEntriesURL %>" 
      cssClass="container-fluid-1280" method="post" name="fm2">
  <aui:button-row>
    <aui:button type="submit" value="Propagate All" />
  </aui:button-row>
</aui:form>

So first I define a portlet action url that has the name of my action command (see same from the MVCActionCommand class above), and then I just added a simple form w/ a button with the Propagate All label.

Now, when I build and deploy this fragment bundle, now on the Fragment portlet usage page, I now see:


When you hit the button, the form will submit back to the Fragment portlet which will look up the MVCActionCommand that matches the /fragment/propagate_all_fragment_entries action, the portlet will find my custom action command handler which will use an ActionableDynamicQuery to update all 753 of the usages of my Site Footer fragment.

Conclusion

Well, my work here is done!

This isn't a perfect solution of course. I didn't use localization for my Propagate All button, I didn't really do a good integration of the button into the existing UI, and I didn't handle the group propagation either.

There's definitely some more work to do here, but I think what I have shared might give you some ideas about how you can consider adding your own custom functionality to an existing Liferay portlet!

Blogs

This should probably be a part of Liferay standard solution as it's really crucial functionality if you consider using fragments. The only thing left is some kind of history of changes in fragments/content on pages ;)

 

Anyway great post and great solution.