null

Liferay - проблема с PermissionChecker в ExpandoBridge и её решение

Совершенно внезапно пришлось поплясать с бубном вокруг лайфрейных API в процессе решения достаточно тривиальной задачи. Суть задачи - потребовалось периодически считывать "глобальные" конфигурационные параметры приложения, хранящиеся в БД портала, причём для доступа к этим параметрам использовалась такая замечательная штука, как Expando API.

Технологическая справка

Expando - это небольшой фреймворк в составе Liferay, предоставляющий достаточно удобный API для сохранения произвольных данных в БД портала и работы с ними в своих приложениях. Периодически мы используем его в наших проектах для ряда задач; в частности, для сохранения в БД каких-либо "глобальных" конфигурационных параметров разрабатываемоего приложения и для "расширения" функционала стандартных сущностей Liferay путём добавления к ним дополнительных атрибутов. К примеру, добавить новый атрибут к группе (сущность Group) дефолтной организации портала можно вот так:

// (...)
ExpandoBridge bridge = GroupLocalServiceUtil.getCompanyGroup(PortalUtil.getDefaultCompanyId()).getExpandoBridge();
if (bridge.hasAttribute("myNewAttr")) {
    System.out.println("attribute \"myNewAttr\" has already exists");
} else {
    bridge.addAttribute(attr.getAttrName());
}
// (...)

Как видим, всё достаточно просто - проверяем, не создан ли уже атрибут, и, если не создан, добавляем его.

Получать и устанавливать значения атрибутов тоже очень просто:

// (...)
bridge.setAttribute(attr.getAttrName(), attrValue); // attrValue - любой объект
// (...)
Object attrValue = bridge.getAttribute(attr.getAttrName());
// (...)

Тут даже комментировать особо нечего, всё и так понятно. В общем, для несложных задач Expando - достаточно удобная штука.

Суть проблемы

Такая схема успешно работала у нас в предыдущих проектах, но сейчас мне потребовалось, чтобы вызов методов Expando, осуществляющих установку и получение атрибутов производился не в ответ на какие-либо действия пользователя портала, а периодически, через некоторые временные промежутки. Вообще, для этого в Liferay существует возможность создавать специальные классы-планировщики, которые "просыпаются" в соответствии с заданным "расписанием", выполняют некоторые действия, и снова "засыпают". Но, так как бизнес-логика нашего приложения пишется на EJB, мне захотелось использовать для этого EJB Timer Service, благо сервер приложений у нас используется Full Profile и никаких ограничений в использовании EJB из-за него нет.

Казалось бы, всё просто - создаём Singleton EJB-компонент, аннотируем его правилами вызова планировщика, добавляем в нужный метод вызов Expando API и радуемся. Но не тут-то было - после того, как тестовая версия приложения была развёрнута на сервере, портал стал выдавать в лог вот такие вот весёлые стектрейсы:

[01:33:00.000] [DEBUG] SchedulerEJB    | ##### Starting job...
[01:33:00.008] [ERROR] SchedulerEJB    | com.liferay.portal.security.auth.PrincipalException: PermissionChecker not initialized
java.lang.RuntimeException: com.liferay.portal.security.auth.PrincipalException: PermissionChecker not initialized
	at com.liferay.portlet.expando.model.impl.ExpandoBridgeImpl.getAttribute(ExpandoBridgeImpl.java:219) ~[na:na]
	at com.liferay.portlet.expando.model.impl.ExpandoBridgeImpl.getAttribute(ExpandoBridgeImpl.java:199) ~[na:na]
	at com.tuneit.example.SchedulerEJB.execute(SchedulerEJB.java:116) ~[SchedulerEJB.class:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.7.0_51]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) ~[na:1.7.0_51]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_51]
	at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_51]
	at org.glassfish.ejb.security.application.EJBSecurityManager.runMethod(EJBSecurityManager.java:1052) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at org.glassfish.ejb.security.application.EJBSecurityManager.invoke(EJBSecurityManager.java:1124) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.BaseContainer.invokeBeanMethod(BaseContainer.java:5388) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.EjbInvocation.invokeBeanMethod(EjbInvocation.java:619) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.AroundInvokeChainImpl.invokeNext(InterceptorManager.java:800) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.EjbInvocation.proceed(EjbInvocation.java:571) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.SystemInterceptorProxy.doAround(SystemInterceptorProxy.java:162) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.SystemInterceptorProxy.aroundTimeout(SystemInterceptorProxy.java:149) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.7.0_51]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) ~[na:1.7.0_51]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_51]
	at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_51]
	at com.sun.ejb.containers.interceptors.AroundInvokeInterceptor.intercept(InterceptorManager.java:861) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.AroundInvokeChainImpl.invokeNext(InterceptorManager.java:800) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.InterceptorManager.intercept(InterceptorManager.java:370) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.BaseContainer.__intercept(BaseContainer.java:5360) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.BaseContainer.intercept(BaseContainer.java:5348) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.BaseContainer.callEJBTimeout(BaseContainer.java:4058) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.EJBTimerService.deliverTimeout(EJBTimerService.java:1832) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.EJBTimerService.access$100(EJBTimerService.java:108) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.EJBTimerService$TaskExpiredWork.run(EJBTimerService.java:2646) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) [na:1.7.0_51]
	at java.util.concurrent.FutureTask.run(FutureTask.java:262) [na:1.7.0_51]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [na:1.7.0_51]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [na:1.7.0_51]
	at java.lang.Thread.run(Thread.java:744) [na:1.7.0_51]
Caused by: com.liferay.portal.security.auth.PrincipalException: PermissionChecker not initialized
	at com.liferay.portal.service.BaseServiceImpl.getPermissionChecker(BaseServiceImpl.java:82) ~[portal-service.jar:na]
	at com.liferay.portlet.expando.service.impl.ExpandoValueServiceImpl.getData(ExpandoValueServiceImpl.java:123) ~[na:na]
	at sun.reflect.GeneratedMethodAccessor301.invoke(Unknown Source) ~[na:na]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_51]
	at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_51]
	at com.liferay.portal.spring.aop.ServiceBeanMethodInvocation.proceed(ServiceBeanMethodInvocation.java:115) ~[na:na]
	at com.liferay.portal.spring.transaction.DefaultTransactionExecutor.execute(DefaultTransactionExecutor.java:62) ~[na:na]
	at com.liferay.portal.spring.transaction.TransactionInterceptor.invoke(TransactionInterceptor.java:51) ~[na:na]
	at com.liferay.portal.spring.aop.ServiceBeanMethodInvocation.proceed(ServiceBeanMethodInvocation.java[01:33:00.008] [ERROR] PaymentsParserEJB    | com.liferay.portal.security.auth.PrincipalException: PermissionChecker not initialized
java.lang.RuntimeException: com.liferay.portal.security.auth.PrincipalException: PermissionChecker not initialized
	at com.liferay.portlet.expando.model.impl.ExpandoBridgeImpl.getAttribute(ExpandoBridgeImpl.java:219) ~[na:na]
	at com.liferay.portlet.expando.model.impl.ExpandoBridgeImpl.getAttribute(ExpandoBridgeImpl.java:199) ~[na:na]
	at com.tuneit.example.SchedulerEJB.execute(SchedulerEJB.java:116) ~[SchedulerEJB.class:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.7.0_51]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) ~[na:1.7.0_51]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_51]
	at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_51]
	at org.glassfish.ejb.security.application.EJBSecurityManager.runMethod(EJBSecurityManager.java:1052) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at org.glassfish.ejb.security.application.EJBSecurityManager.invoke(EJBSecurityManager.java:1124) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.BaseContainer.invokeBeanMethod(BaseContainer.java:5388) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.EjbInvocation.invokeBeanMethod(EjbInvocation.java:619) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.AroundInvokeChainImpl.invokeNext(InterceptorManager.java:800) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.EjbInvocation.proceed(EjbInvocation.java:571) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.SystemInterceptorProxy.doAround(SystemInterceptorProxy.java:162) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.SystemInterceptorProxy.aroundTimeout(SystemInterceptorProxy.java:149) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.7.0_51]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) ~[na:1.7.0_51]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_51]
	at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_51]
	at com.sun.ejb.containers.interceptors.AroundInvokeInterceptor.intercept(InterceptorManager.java:861) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.AroundInvokeChainImpl.invokeNext(InterceptorManager.java:800) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.interceptors.InterceptorManager.intercept(InterceptorManager.java:370) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.BaseContainer.__intercept(BaseContainer.java:5360) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.BaseContainer.intercept(BaseContainer.java:5348) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.BaseContainer.callEJBTimeout(BaseContainer.java:4058) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.EJBTimerService.deliverTimeout(EJBTimerService.java:1832) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.EJBTimerService.access$100(EJBTimerService.java:108) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at com.sun.ejb.containers.EJBTimerService$TaskExpiredWork.run(EJBTimerService.java:2646) [ejb-container.jar:3.1.2.1-SNAPSHOT]
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) [na:1.7.0_51]
	at java.util.concurrent.FutureTask.run(FutureTask.java:262) [na:1.7.0_51]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) [na:1.7.0_51]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) [na:1.7.0_51]
	at java.lang.Thread.run(Thread.java:744) [na:1.7.0_51]
Caused by: com.liferay.portal.security.auth.PrincipalException: PermissionChecker not initialized
	at com.liferay.portal.service.BaseServiceImpl.getPermissionChecker(BaseServiceImpl.java:82) ~[portal-service.jar:na]
	at com.liferay.portlet.expando.service.impl.ExpandoValueServiceImpl.getData(ExpandoValueServiceImpl.java:123) ~[na:na]
	at sun.reflect.GeneratedMethodAccessor301.invoke(Unknown Source) ~[na:na]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_51]
	at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_51]
	at com.liferay.portal.spring.aop.ServiceBeanMethodInvocation.proceed(ServiceBeanMethodInvocation.java:115) ~[na:na]
	at com.liferay.portal.spring.transaction.DefaultTransactionExecutor.execute(DefaultTransactionExecutor.java:62) ~[na:na]
	at com.liferay.portal.spring.transaction.TransactionInterceptor.invoke(TransactionInterceptor.java:51) ~[na:na]
	at com.liferay.portal.spring.aop.ServiceBeanMethodInvocation.proceed(ServiceBeanMethodInvocation.java:111) ~[na:na]
	at com.liferay.portal.spring.aop.ChainableMethodAdvice.invoke(ChainableMethodAdvice.java:56) ~[na:na]
	at com.liferay.portal.spring.aop.ServiceBeanMethodInvocation.proceed(ServiceBeanMethodInvocation.java:111) ~[na:na]
	at com.liferay.portal.spring.aop.ChainableMethodAdvice.invoke(ChainableMethodAdvice.java:56) ~[na:na]
	at com.liferay.portal.spring.aop.ServiceBeanMethodInvocation.proceed(ServiceBeanMethodInvocation.java:111) ~[na:na]
	at com.liferay.portal.spring.aop.ServiceBeanAopProxy.invoke(ServiceBeanAopProxy.java:175) ~[na:na]
	at com.sun.proxy.$Proxy245.getData(Unknown Source) ~[na:na]
	at com.liferay.portlet.expando.service.ExpandoValueServiceUtil.getData(ExpandoValueServiceUtil.java:107) ~[portal-service.jar:na]
	at com.liferay.portlet.expando.model.impl.ExpandoBridgeImpl.getAttribute(ExpandoBridgeImpl.java:208) ~[na:na]
	... 33 common frames omitted:111) ~[na:na]
	at com.liferay.portal.spring.aop.ChainableMethodAdvice.invoke(ChainableMethodAdvice.java:56) ~[na:na]
	at com.liferay.portal.spring.aop.ServiceBeanMethodInvocation.proceed(ServiceBeanMethodInvocation.java:111) ~[na:na]
	at com.liferay.portal.spring.aop.ChainableMethodAdvice.invoke(ChainableMethodAdvice.java:56) ~[na:na]
	at com.liferay.portal.spring.aop.ServiceBeanMethodInvocation.proceed(ServiceBeanMethodInvocation.java:111) ~[na:na]
	at com.liferay.portal.spring.aop.ServiceBeanAopProxy.invoke(ServiceBeanAopProxy.java:175) ~[na:na]
	at com.sun.proxy.$Proxy245.getData(Unknown Source) ~[na:na]
	at com.liferay.portlet.expando.service.ExpandoValueServiceUtil.getData(ExpandoValueServiceUtil.java:107) ~[portal-service.jar:na]
	at com.liferay.portlet.expando.model.impl.ExpandoBridgeImpl.getAttribute(ExpandoBridgeImpl.java:208) ~[na:na]
	... 33 common frames omitted

Т.е., планировщик "просыпается", пытается прочитать параметры, и радостно крэшится на том, что у него отсутствуют необходимые права доступа к таблицам Expando, т.к. Permission Checker не проинициализирован.

OK, Google

Смотрим в поисковиках, и достаточно быстро находим интереснейшую запись в форумах Liferay. Оказывается, эта проблема возникает достаточно часто, причём не только при доступе к API портала "извне" (в нашем случае, из EJB-компонента), но и в случае использования "родных" лайфрейных планировщиков событий. Логика её возникновения тоже становится понятной - т.к. вызов планировщика у нас не ассоциирован ни с каким пользователем, никакие права доступа с этим планировщиком на портале тоже не ассоциируются. Т.е., решение проблемы - принудительно проинициализировать Permission Checker с правами доступа нужного нам пользователя.

Решение

Смотрим, как это можно сделать. Находим другую похожую тему в форумах, в которой предлагается возможное решение:

Company companyqq = CompanyLocalServiceUtil.getCompanyByWebId("myCompanyWebId");
Role adminRole = RoleLocalServiceUtil.getRole(companyqq.getCompanyId(),"Administrator");
List<User> adminUsers = UserLocalServiceUtil.getRoleUsers(adminRole.getRoleId());

PrincipalThreadLocal.setName(adminUsers.get(0).getUserId());
PermissionChecker permissionChecker =PermissionCheckerFactoryUtil.create(adminUsers.get(0), true);
PermissionThreadLocal.setPermissionChecker(permissionChecker);

Т.е., получаем список администраторов заданного сайта, берём первого из них и инициализируем Permission Checker с правами доступа этого пользователя.

В принципе, хорошее решение, но мне не понравилась первая строчка с "хардкодной" привязкой к конкретному WebID. Из за этого возможны разные неприятности - например, переразвернули приложение на другом портале, и всё развалилось, т.к. на нём другой WebID "главной" организации сайта. В общем, я подумал немного, и придумал улучшенный вариант решения. Получилось как-то так:

private void initPermChecker() {
    try {
        Role adminRole = RoleLocalServiceUtil.getRole(PortalUtil.getDefaultCompanyId(), "Administrator");
        List<User> adminUsers = UserLocalServiceUtil.getRoleUsers(adminRole.getRoleId());
        PrincipalThreadLocal.setName(adminUsers.get(0).getUserId());
        PermissionChecker permissionChecker = PermissionCheckerFactoryUtil.create(adminUsers.get(0));
        PermissionThreadLocal.setPermissionChecker(permissionChecker);
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
}

Тут сделаны следующие допущения:

  • На любом портале есть организация по умолчанию.
  • На любом портале есть хотя бы один пользователь, обладающий правами администратора применительно к организации по умолчанию.

Первый пункт выполняется всегда в силу архитектуры портала, второй - в 99.9% случаев - хотя бы суперпользователь портала должен обладать правами администратора дефолтной организации. Т.е., способ должен работать. Добавляем вызов нового метода в код, устанавливающий и получающий значения атрибутов и проверяем:

[02:00:00.000] [DEBUG] SchedulerEJB    | ##### Starting job...
[02:00:00.009] [DEBUG] SchedulerEJB    | ##### ...Job successfully finished!

Работает!

Коротко о себе:

Работаю ведущим программистом в компании Tune IT и ассистентом кафедры Вычислительной техники в Университете ИТМО .

Занимаюсь проектами, связанными с разработкой разного рода веб-приложений (порталы, CRM-системы, системы электронного документооборота), а также, в рамках научной работы на кафедре, изучаю возможности применения семантического анализа в задачах САПР.