Forums

angle-left Back

Disable OSGI component and use custom one

JB
Jacopo Bartolini, modified 7 Months ago.

Disable OSGI component and use custom one

Junior Member Posts: 28 Join Date: 1/19/17 Recent Posts
Hi, I have developed a custom OSGI component, DLFileEntryActivityInterpreterHook, which changes some behavior of the DLFileEntryActivityInterpreter component. When I deploy it and use the command scr:disable from the GoGo shell, everything works fine. But when I restart the portal, the default component kicks in and I have to disable it from the GoGo shell again. What should I do?

Here's my code for the custom component, which is only a class:

@Component(
    immediate=true,
    property = {
            "javax.portlet.name=" + DLPortletKeys.DOCUMENT_LIBRARY,
            "service.ranking:Integer=100"
    },
    service = SocialActivityInterpreter.class
)
public class DLFileEntryActivityInterpreterHook
    extends BaseSocialActivityInterpreter {

    @Override
    public String[] getClassNames() {
        return _CLASS_NAMES;
    }
   
    @Override
    protected String getLink(
            SocialActivity activity, ServiceContext serviceContext)
        throws Exception {
       
        String className = activity.getClassName();
        long classPK = activity.getClassPK();

        String viewEntryInTrashURL = getViewEntryInTrashURL(
            className, classPK, serviceContext);

        if (viewEntryInTrashURL != null) {
            return viewEntryInTrashURL;
        }
       
        try {
            PortletURL newURL = null;
            Layout layout = LayoutLocalServiceUtil.getLayout(
                        _portal.getPlidFromPortletId(
                            serviceContext.getScopeGroupId(), DLPortletKeys.DOCUMENT_LIBRARY));
            
            LayoutTypePortlet ltp = (LayoutTypePortlet) layout.getLayoutType();
            
            for(Portlet por : ltp.getPortlets()) {
                if(por.getPortletName().equals(DLPortletKeys.DOCUMENT_LIBRARY)) {
                   
                    newURL = PortletURLFactoryUtil.create(
                            serviceContext.getRequest(), por, layout, PortletRequest.RENDER_PHASE);
                   
                    newURL.setParameter("mvcRenderCommandName", "/document_library/view_file_entry");
                    newURL.setParameter("backURL", serviceContext.getCurrentURL());
                    newURL.setParameter("fileEntryId", String.valueOf(activity.getClassPK()));
                   
                    break;
                }
               
            }
            
            if(Validator.isNotNull(newURL)) {
                return newURL.toString();
            }
        } catch(Exception e) {
            _log.error(e);
        }

        String path = getPath(activity, serviceContext);

        if (Validator.isNull(path)) {
            return null;
        }
       
        path = addNoSuchEntryRedirect(path, className, classPK, serviceContext);

        if (!path.startsWith(StringPool.SLASH)) {
            return path;
        }
       
        return serviceContext.getPortalURL() + serviceContext.getPathMain() +
            path;
    }
   
   
    @Override
    protected String getBody(
            SocialActivity activity, ServiceContext serviceContext)
        throws Exception {

        FileEntry fileEntry = _dlAppLocalService.getFileEntry(
            activity.getClassPK());

        if (TrashUtil.isInTrash(
                DLFileEntry.class.getName(), fileEntry.getFileEntryId())) {

            return StringPool.BLANK;
        }

        StringBundler sb = new StringBundler(3);

        AssetRendererFactory<?> assetRendererFactory =
            AssetRendererFactoryRegistryUtil.getAssetRendererFactoryByClassName(
                DLFileEntry.class.getName());

        AssetRenderer<?> assetRenderer = assetRendererFactory.getAssetRenderer(
            fileEntry.getFileEntryId());

        String fileEntryLink = assetRenderer.getURLDownload(
            serviceContext.getThemeDisplay());
       
        sb.append(wrapLinkHook(fileEntryLink, "download-file", serviceContext));

        return sb.toString();
    }

    private String wrapLinkHook(String link, String key, ServiceContext serviceContext) {
        ResourceBundle resourceBundle = getResourceBundleLoader().
                loadResourceBundle(serviceContext.getLanguageId());

        String title = HtmlUtil.escape(LanguageUtil.get(resourceBundle, key));

        if (link == null) {
            return title;
        }

        StringBundler sb = new StringBundler(5);

        sb.append("<a href=\"");
        sb.append(link);
        sb.append("\">");
        sb.append("<div class=\"btn btn-default btn-sm\">");
        sb.append(title);
        sb.append("</div></a>");

        return sb.toString();
    }

    protected String getFolderLink(
        FileEntry fileEntry, ServiceContext serviceContext) {

        StringBundler sb = new StringBundler(6);

        sb.append(serviceContext.getPortalURL());
        sb.append(serviceContext.getPathMain());
        sb.append("/document_library/find_folder?groupId=");
        sb.append(fileEntry.getRepositoryId());
        sb.append("&folderId=");
        sb.append(fileEntry.getFolderId());

        return sb.toString();
    }

    @Override
    protected String getPath(
        SocialActivity activity, ServiceContext serviceContext) {

        return "/document_library/find_file_entry?fileEntryId=" +
            activity.getClassPK();
    }

    @Override
    protected ResourceBundleLoader getResourceBundleLoader() {
        return _resourceBundleLoader;
    }

    @Override
    protected Object[] getTitleArguments(
            String groupName, SocialActivity activity, String link,
            String title, ServiceContext serviceContext)
        throws Exception {

        if (activity.getType() == SocialActivityConstants.TYPE_ADD_COMMENT) {
            String creatorUserName = getUserName(
                activity.getUserId(), serviceContext);
            String receiverUserName = getUserName(
                activity.getReceiverUserId(), serviceContext);

            return new Object[] {
                groupName, creatorUserName, receiverUserName,
                wrapLink(link, title)
            };
        }
        else {
            return super.getTitleArguments(
                groupName, activity, link, title, serviceContext);
        }
    }

    @Override
    protected String getTitlePattern(
        String groupName, SocialActivity activity) {

        int activityType = activity.getType();

        if (activityType == DLActivityKeys.ADD_FILE_ENTRY) {
            if (Validator.isNull(groupName)) {
                return "activity-document-library-file-add-file";
            }
            else {
                return "activity-document-library-file-add-file-in";
            }
        }
        else if (activityType == DLActivityKeys.UPDATE_FILE_ENTRY) {
            if (Validator.isNull(groupName)) {
                return "activity-document-library-file-update-file";
            }
            else {
                return "activity-document-library-file-update-file-in";
            }
        }
        else if (activityType == SocialActivityConstants.TYPE_ADD_COMMENT) {
            if (Validator.isNull(groupName)) {
                return "activity-document-library-file-add-comment";
            }
            else {
                return "activity-document-library-file-add-comment-in";
            }
        }
        else if (activityType == SocialActivityConstants.TYPE_MOVE_TO_TRASH) {
            if (Validator.isNull(groupName)) {
                return "activity-document-library-file-move-to-trash";
            }
            else {
                return "activity-document-library-file-move-to-trash-in";
            }
        }
        else if (activityType ==
                    SocialActivityConstants.TYPE_RESTORE_FROM_TRASH) {

            if (Validator.isNull(groupName)) {
                return "activity-document-library-file-restore-from-trash";
            }
            else {
                return "activity-document-library-file-restore-from-trash-in";
            }
        }

        return null;
    }

    @Override
    protected boolean hasPermissions(
            PermissionChecker permissionChecker, SocialActivity activity,
            String actionId, ServiceContext serviceContext)
        throws Exception {

        return DLFileEntryPermission.contains(
            permissionChecker, activity.getClassPK(), actionId);
    }

    @Reference(unbind = "-")
    protected void setDLAppLocalService(DLAppLocalService dlAppLocalService) {
        _dlAppLocalService = dlAppLocalService;
    }

    @Reference(
        target = "(bundle.symbolic.name=com.liferay.document.library.web)",
        unbind = "-"
    )
    protected void setResourceBundleLoader(
        ResourceBundleLoader resourceBundleLoader) {

        _resourceBundleLoader = new AggregateResourceBundleLoader(
            resourceBundleLoader,
            ResourceBundleLoaderUtil.getPortalResourceBundleLoader());
    }

    @Reference
    private Portal _portal;
   
    private static final String[] _CLASS_NAMES = {DLFileEntry.class.getName()};

    private DLAppLocalService _dlAppLocalService;
    private ResourceBundleLoader _resourceBundleLoader;
   
    private final static Log _log = LogFactoryUtil.getLog(DLFileEntryActivityInterpreterHook.class);

}
David H Nebinger, modified 7 Months ago.

RE: Disable OSGI component and use custom one

Community Moderator Liferay Legend Posts: 13824 Join Date: 9/1/06 Recent Posts
Service rankings don't work that way.

SocialActivityInterpreters are all collected using a ServiceTracker and each will be invoked as appropriate.

A service ranking only applies when you are trying to replace one service with another.

In this current case, the service tracker will not know that you are actually trying to replace one SAI with another, from its perspective you are just adding an additional SAI.
JB
Jacopo Bartolini, modified 7 Months ago.

RE: Disable OSGI component and use custom one

Junior Member Posts: 28 Join Date: 1/19/17 Recent Posts
I'm sorry for my late response, holidays got me. First of all, thanks for your response. So, I understand what you're saying David. Do you have a suggestion for me on how to reach my goal, to override one specific SocialActivityInterpreter with my own?
David H Nebinger, modified 7 Months ago.

RE: Disable OSGI component and use custom one

Community Moderator Liferay Legend Posts: 13824 Join Date: 9/1/06 Recent Posts
The only way you can is to replace the original file. This could involve doing a "module extending a module" thing I blogged about previously if you're trying to replace a Liferay interpreter.
Minhchau Dang, modified 7 Months ago.

RE: Disable OSGI component and use custom one (Answer)

LIFERAY STAFF Expert Posts: 461 Join Date: 10/22/07 Recent Posts
Jacopo Bartolini:
Do you have a suggestion for me on how to reach my goal, to override one specific SocialActivityInterpreter with my own?

If you wish to disable the DLFileEntryActivityInterpreter, you could do so by calling the ServiceComponentRuntime when your custom component activates.

@Activate
public void activate(
        ComponentContext componentContext, BundleContext bundleContext,
        Map<String, Object> config)
    throws Exception {

    String componentName = DLFileEntryActivityInterpreter.class.getName();

    Collection<ServiceReference<SocialActivityInterpreter>>
        serviceReferences =
            bundleContext.getServiceReferences(
                SocialActivityInterpreter.class,
                "(component.name=" + componentName + ")");

    for (ServiceReference serviceReference : serviceReferences) {
        Bundle bundle = serviceReference.getBundle();

        ComponentDescriptionDTO description =
            _serviceComponentRuntime.getComponentDescriptionDTO(
                bundle, componentName);

        _serviceComponentRuntime.disableComponent(description);
    }
}

@Reference
private ServiceComponentRuntime _serviceComponentRuntime;

If for some reason you wish to reactivate the original component once your component has deactivated, you could use similar code when your custom component deactivates, but call the enableComponent method instead.
JB
Jacopo Bartolini, modified 7 Months ago.

RE: Disable OSGI component and use custom one

Junior Member Posts: 28 Join Date: 1/19/17 Recent Posts
Thank you very much Minchau, that worked like a charm.
And also thank you David too.
JB
Jacopo Bartolini, modified 6 Months ago.

RE: Disable OSGI component and use custom one

Junior Member Posts: 28 Join Date: 1/19/17 Recent Posts
I'm sorry to bother you again, but I've encountered a problem: when i restart the app server, sometimes my custom component fails on finding the default one, so that now I'm back at having two components running (and two product menu icons). While looking at the logs, I can clearly see that my component is activated (second picture) after the bundle containing the default component (first picture).
Here's the code i wrote for the activate() method, which is pretty much the same you wrote:

       @Activate
    public void activate(
            ComponentContext componentContext, BundleContext bundleContext,
            Map<String, Object> config)
        throws Exception {
       
        String componentName = "com.liferay.product.navigation.product.menu.web.internal.product.navigation.control.menu.ProductMenuProductNavigationControlMenuEntry";
       
        Collection<ServiceReference<ProductNavigationControlMenuEntry>> serviceReferences;
        int i = 0;
       
        do {
       
            _log.info("Attempt number " + (i+1));
       
            serviceReferences = bundleContext.getServiceReferences(ProductNavigationControlMenuEntry.class,
                "(component.name=" + componentName + ")");
       
            i++;
       
            Thread.sleep(1000);
       
        } while(serviceReferences.isEmpty() && i<5);
       
        if(i==5 && serviceReferences.isEmpty()) {
            _log.warn("No service reference found");
        }
       
        for(ServiceReference serviceReference : serviceReferences) {
            Bundle bundle = serviceReference.getBundle();
            
            ComponentDescriptionDTO description =
                _serviceComponentRuntime.getComponentDescriptionDTO(bundle, componentName);
            
            _serviceComponentRuntime.disableComponent(description);
        }
    }


If there is any advice that you could give me, It would be really helpful. Thanks in advance.
David H Nebinger, modified 6 Months ago.

RE: Disable OSGI component and use custom one

Community Moderator Liferay Legend Posts: 13824 Join Date: 9/1/06 Recent Posts
Jacopo Bartolini:
I'm sorry to bother you again, but I've encountered a problem: when i restart the app server, sometimes my custom component fails on finding the default one, so that now I'm back at having two components running (and two product menu icons).


That's the nature of the code. By using the activate method to look for a current implementation, at startup time (when you have no control over bundle load order) your code is running and not finding one (because it hasn't started yet) so there is nothing for it to remove. Sometime thereafter, the other starts and you're left with two instances.

I would probably bind your component to a portal lifecycle start, that way you delay the run of your activate until the portal itself has completed startup. Then it should always find the old one and can kill it.
JB
Jacopo Bartolini, modified 6 Months ago.

RE: Disable OSGI component and use custom one

Junior Member Posts: 28 Join Date: 1/19/17 Recent Posts
Thank you David, that's a good advice. I'll try it Monday morning, hopefully it will work.
Bye, and thanks again.
JB
Jacopo Bartolini, modified 5 Months ago.

RE: Disable OSGI component and use custom one

Junior Member Posts: 28 Join Date: 1/19/17 Recent Posts
Hi David,
I've tried following your advice. I've created a class which implements LifecycleAction. In this class I've replicated the code that Minhchau Dang posted here before, but I've encountered the exact same problem I was facing before: sometimes this module is able to find and disable the Liferay default components, sometimes it's not able to do it. I've tried to bind this module either to the application.startup.events or to the global.startup.events, but none of it seems to work.

That's one of the most annoying problem I've faced: modules are so easy to replace, while components are the exact some opposite.
Do you have any other suggestion?

I was also wondering if it would be better to just replace the entire Liferay module with a custom one, but I'm not sure if that is an overkill or not (and also, I'm not sure if that would work). Let me know your thoughts on this, please.

Here I include the code for the custom module I've created:


@Component(
        immediate = true,
        property = {"key=application.startup.events"},
        service = LifecycleAction.class
)
public class CustomComponentStarter implements LifecycleAction {

   
    @Override
    public void processLifecycleEvent(LifecycleEvent lifecycleEvent) throws ActionException {
       
        _log.info("Starting...");
       
        Bundle myBundle = FrameworkUtil.
                getBundle(CustomComponentStarter.class);
       
        BundleContext bundleContext = myBundle.getBundleContext();
       
        String componentNames[] = new String[] {
                DLFileEntryActivityInterpreter.class.getName(), MBMessageActivityInterpreter.class.getName()};
        for(String componentName : componentNames) {
            try {
                Collection<ServiceReference<SocialActivityInterpreter>> serviceReferences =
                        bundleContext.getServiceReferences(SocialActivityInterpreter.class,
                                "(component.name=" + componentName + ")");
               
                if(serviceReferences.isEmpty()) {
                    _log.info("No serviceReferences found for component " + componentName);
                }
               
                for(ServiceReference serviceReference : serviceReferences) {
                    Bundle bundle = serviceReference.getBundle();
                   
                    ComponentDescriptionDTO description =
                        _serviceComponentRuntime.getComponentDescriptionDTO(bundle, componentName);
                   
                    _serviceComponentRuntime.disableComponent(description);
                   
                    _log.info("Disabled component " + componentName);
                }
            } catch (InvalidSyntaxException ise) {
                _log.error(ise);   
            }
        }
       
        try {
            String productNavigationComponent = "com.liferay.product.navigation.product.menu.web.internal.product.navigation.control.menu.ProductMenuProductNavigationControlMenuEntry";
            
            Collection<ServiceReference<ProductNavigationControlMenuEntry>> serviceReferences =
                    bundleContext.getServiceReferences(ProductNavigationControlMenuEntry.class,
                            "(component.name=" + productNavigationComponent + ")");
            
            if(serviceReferences.isEmpty()) {
                _log.info("No serviceReferences found for component " + productNavigationComponent);
            }
            
            for(ServiceReference serviceReference : serviceReferences) {
                Bundle bundle = serviceReference.getBundle();
               
                ComponentDescriptionDTO description =
                    _serviceComponentRuntime.getComponentDescriptionDTO(bundle, productNavigationComponent);
               
                _serviceComponentRuntime.disableComponent(description);
               
                _log.info("Disabled component " + productNavigationComponent);
            }
        } catch (InvalidSyntaxException ise) {
            _log.error(ise);
        }
    }
   
    @Reference
    private ServiceComponentRuntime _serviceComponentRuntime;
   
    private final static Log _log = LogFactoryUtil.getLog(CustomComponentStarter.class);
}
Minhchau Dang, modified 5 Months ago.

RE: Disable OSGI component and use custom one

LIFERAY STAFF Expert Posts: 461 Join Date: 10/22/07 Recent Posts
Jacopo Bartolini:
Sometimes this module is able to find and disable the Liferay default components, sometimes it's not able to do it.

Have you tried depending on the component you're trying to replace by adding a @Reference to it? That will delay the call to the @Activate annotated method until after the other component (that you're trying to disable) is available.

Edit: Actually, I spoke too soon. Delaying the activation won't work, because as soon as you disable your dependency, it will probably disable your component as well (because your dependency is no longer satisfied).

Rather, you'll want to make it an optional @Reference, and go ahead and call the method both on component activation and if the reference is satisfied, but only do something if the component has been activated (in other words, the BundleContext you need is not null).

@Activate
public void activate(
        ComponentContext componentContext, BundleContext bundleContext,
        Map<String, Object> config)
    throws Exception {

    _bundleContext = bundleContext;

    deactivateExistingComponent();
}

@Reference(
    cardinality = ReferenceCardinality.OPTIONAL,
    policy = ReferencePolicy.DYNAMIC,
    policyOption = ReferencePolicyOption.GREEDY
)
public void setProductNavigationControlMenuEntry(
    ProductNavigationControlMenuEntry productNavigationControlMenuEntry) {
   
    deactivateExistingComponent();
}

public void deactivateExistingComponent() {
    if (_bundleContext == null) {
        return;
    }

    // All the logic from before
}

The concept itself is usually tricky if you only want the method to ever run once, because your own custom component is providing that same interface (so you may want to limit the target to have an objectClass that's just the class you're overriding, so that it doesn't match your component), but component deactivation is something that you should be able to run multiple times.

At compile time, bnd.bnd might analyze the class and demand an unbind method, which you can provide by adding an empty method named unsetProductNavigationControlMenuEntry that takes in the same parameter types as the setProductNavigationControlMenuEntry method.
Henrique Andrade, modified 3 Months ago.

RE: Disable OSGI component and use custom one

New Member Posts: 9 Join Date: 7/18/13 Recent Posts
Minhchau Dang:
Jacopo Bartolini:
Sometimes this module is able to find and disable the Liferay default components, sometimes it's not able to do it.

Have you tried depending on the component you're trying to replace by adding a @Reference to it? That will delay the call to the @Activate annotated method until after the other component (that you're trying to disable) is available.

Edit: Actually, I spoke too soon. Delaying the activation won't work, because as soon as you disable your dependency, it will probably disable your component as well (because your dependency is no longer satisfied).

Rather, you'll want to make it an optional @Reference, and go ahead and call the method both on component activation and if the reference is satisfied, but only do something if the component has been activated (in other words, the BundleContext you need is not null).

@Activate
public void activate(
        ComponentContext componentContext, BundleContext bundleContext,
        Map<String, Object> config)
    throws Exception {

    _bundleContext = bundleContext;

    deactivateExistingComponent();
}

@Reference(
    cardinality = ReferenceCardinality.OPTIONAL,
    policy = ReferencePolicy.DYNAMIC,
    policyOption = ReferencePolicyOption.GREEDY
)
public void setProductNavigationControlMenuEntry(
    ProductNavigationControlMenuEntry productNavigationControlMenuEntry) {
   
    deactivateExistingComponent();
}

public void deactivateExistingComponent() {
    if (_bundleContext == null) {
        return;
    }

    // All the logic from before
}

The concept itself is usually tricky if you only want the method to ever run once, because your own custom component is providing that same interface (so you may want to limit the target to have an objectClass that's just the class you're overriding, so that it doesn't match your component), but component deactivation is something that you should be able to run multiple times.

At compile time, bnd.bnd might analyze the class and demand an unbind method, which you can provide by adding an empty method named unsetProductNavigationControlMenuEntry that takes in the same parameter types as the setProductNavigationControlMenuEntry method.

Hello!

I had same thread behavior, and tried this approach to 'replace' another portal component with my custom one, and worked very well even after portal restart.

Thanks!