четверг, 23 октября 2014 г.

IE11 vs CKEditor Filemanager (Liferay 6.1.x): Object doesn't support property or method createNSResolver

В браузере Internet Explorer 11 (возможно и 10й версии тоже) не работает файлменеджер визуального редактора CKEditor используемого в Liferay 6.1.x. В консоли бьёт ошибку "Object doesn't support property or method createNSResolver".

Необходимо в HTML-файлы файлменеджера:

[liferay-app-dir]/html/js/editor/ckeditor/editor/filemanager/browser/liferay/browser.html

добавить метатег совместимости с IE9:
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />

среда, 12 марта 2014 г.

четверг, 30 января 2014 г.

Liferay вложенные портлеты 4

Шаблоны страниц в Liferay, к сожалению, не работают так, как хотелось бы:
  • Если для страницы созданной по шаблону включен режим propagation изменений шаблона, то нет возможности кастомизировать страницу (хотя бы в какой-то одной определенной области разметки) LPS-30114
  • Отключение propagation сводит на нет управление сквозными областями разметки (например боковые колоки в 2-х или 3-х колончатом макете страниц)
В результате, для того чтобы добавить портлет в сквозную область настроенный портлет (например, баннерный блок под главное меню в левой колонке) необходимо пройти по всем страницам сайта, повторяя одно и то же действие "Добавить портлет". Даже с учетом возможности использования архивированных настроек портлета, это очень не удобно и не гарантирует внесение изменения на всех страницах сайта.

Можно конечно использовать layout-template устанавливаемые в виде плагина, но в этом случае требуется инструментарий типа IDE. Да и вообще шаблоны страниц должны редактироваться средствами CMS.

Workaround: В JournalArticle есть возможность runtime портлета с помощью тега <runtime-portlet/>, но задать необходимые настройки он не позволяет. Можно предвариательно сохранить PortletPreferences для вызываемого портлета в VM скрипте шаблона сетевого контента. Создаем структуру "LAYOUT_PORTLETS" и шаблон для нее:
#set($themeDisplay=$request.get("theme-display")) 
#set($companyId=$themeDisplay.get("company-id")) 
#set($ownerId=$getterUtil.getLong("0")) 
#set($ownerType=$getterUtil.getInteger("3")) 
#set($plid=$getterUtil.getLong($themeDisplay.get("plid"))) 
#set($portletPreferencesService=$serviceLocator.findService("com.liferay.portal.service.PortletPreferencesLocalService")) 

## Настраиваем портлет
$velocityPortletPreferences.setValue("portletSetupUseCustomTitle", "true") 
$velocityPortletPreferences.setValue("portletSetupTitle_ru_RU", "Урааа!!!") 
$velocityPortletPreferences.setValue("groupId", "10180") 
$velocityPortletPreferences.setValue("articleId", "10870") 

## Проверяем сохранены ли настройки портлета в БД, при необходимости сохраняем
#set($portletId="56_INSTANCE_banner1") 
#set($portletPreferences=$portletPreferencesService.getPortletPreferences($ownerId, $ownerType, $plid, $portletId)) 
#if(!portletPreferences || "$portletPreferences" == "")
     #set($portletPreferences=$portletPreferencesService.updatePreferences($ownerId, $ownerType, $plid, $portletId, $velocityPortletPreferences.toString()))
#end
$velocityPortletPreferences.reset() 

## Вызываем портлет
<runtime-portlet instance="banner1" name="56" querystring=""/>

Предварительно в свойствах портала разрешаем работу с serviceLocator. Добавляем в portal-ext.properties директиву:
journal.template.velocity.restricted.variables=
Добавляем статью в созданную структуру и накидываем портлет "Отображение сетевого контента" с выводом этой статьи: портлет внутри статьи отрабатывает уже настроенным!!! При этом, число портлетов вызываемых в статье по шаблону может быть более одного.

В результате, я создал 2 статьи "Левая колонка" и "Правая колонка", закрепил на всех страницах сайта в соответствующих областях разметки. Для изменения левой или правой колонки редактирую соответствующую статью.

Получилось некое подобие стандартного портлета Liferay "Вложенные портлеты", при этом конфигурация вложенных портлетов инициализируется на страницах автоматически.

пятница, 29 ноября 2013 г.

Плавающая ошибка из-за PersistenceContextType

Сдали проект. Запустили в эксплуатацию. Через 3-4 дня приходит первый алёрт от Заказчика об ошибке, блокирующей работу админки. Перезагрузка или переустановка приложения проблемного модуля восстанавливает работоспособность админки. Приложение работает с использованием Spring 3.0 + Hibernate 4.1 по спецификации JPA 2.0.

Недели 2 переписки с Заказчиком и выяснения обстоятельств проявления ошибки + 2 недели активного поиска причины ошибки. Ошибка плавающая, проявляется с периодичностью 3-5 дней только при сохранении @Entity объектов. В stacktrace рутовая ошибка разная, но чаще всего:
Caused by: org.hibernate.HibernateException: Flush during cascade is dangerous

Экспериментальным путем было выявлено, что ошибка проявляется только при одновременной интенсивной работе нескольких редакторов. Важная деталь: другой модуль этого же приложения такой ошибки не проявляет. Похоже на проблему конфигурации или модели данных конкретного модуля?

Манипуляции с исходным кодом админки, DAO и модели в результате привели к интересной ошибке:
java.util.ConcurrentModificationException
 at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)

Это навело на мысль, что ошибка возникает в условиях одновременного сохранения в БД достаточно большого числа разных потоков. Нашли в документации Spring упоминание специфики использования DAO на базе JPA, и в частности подключения EntityManager к DAO с PersistenceContextType.EXTENDED:
The alternative, PersistenceContextType.EXTENDED, is a completely different affair: This results in a so-called "extended EntityManager", which is not thread-safe and hence must not be used in a concurrently accessed component such as a Spring-managed singleton bean.

Проверили: так и есть, один из предыдущих разработчиков модуля включил в аннотацию @PersistanceContext атрибут PersistenceContextType.EXTENDED. Включение этого атрибута позволило ему упростить код в отношении получения коллекций связанных сущностей по LazyFetch, однако в результате привело к плавающей ошибке!

вторник, 19 ноября 2013 г.

Постраничная навигация AssetPublisher

В Liferay Portal 6.1.1 GA2 недокументированная особенность метода AssetEntryServiceUtil.getEntriesCount

Если настроить Публикатор по одному типу ресурса (например, "Сетевой контент" подтип "Новость"), без фильтрации и группировки по категориям и тегам, включить постраничную навигацию и отметить галочку "Включить права", то подсчет найденных ассетов никогда не превысит 200! В результате если у вас наберется новостей скажем 398, то при количестве элементов на страницу 20 выдасться всего 10 страниц. Игра с настройками не помогает - похоже какое-то внутрисистемное ограничение на максимальное количество ассетов выбираемых для проверки разрешений, типа asset.filter.search.limit

Быстрым решением было получение общего количества ассетов удовлетворяющих условию через AssetEntryLocalServiceUtil и а-ля аппроксимация в сторону убывания к правильному значению. Пример модификации /jsp/html/portlet/asset_publisher/view_dynamic_list.jspf (начиная со строки 93):
 else if (!groupByClass) {
  assetEntryQuery.setClassNameIds(classNameIds);

  /* total = AssetEntryServiceUtil.getEntriesCount(assetEntryQuery); */
  total = AssetEntryLocalServiceUtil.getEntriesCount(assetEntryQuery);
  if(total > delta){
   do{
    assetEntryQuery.setEnd(total);
    assetEntryQuery.setStart(total - delta);
    results = AssetEntryServiceUtil.getEntries(assetEntryQuery);
    if(results.size() < delta){
     total = total - delta + results.size();
    }
   } while (results.size() == 0);  
  }else{
   total = AssetEntryServiceUtil.getEntriesCount(assetEntryQuery);
  }

  searchContainer.setTotal(total);

среда, 15 мая 2013 г.

Liferay вложенные портлеты 3

Последние грабли с runtime портлетов внутри портлета, с которыми я столкнулся, это runtime под Liferay 6.1.1 GA2. Бьёт ошибку: PortalUtil.renderPortlet throws exception "javax.servlet.ServletException: File &quot;/html/common/themes/portlet.jsp&quot; not found". Подробности http://issues.liferay.com/browse/LPS-31508

Если же отключить обрамление и дефолтный шаблон для включаемого портлета, как в принципе и требовалось мне, то runtime отрабатывает. Итого, для моего кастомного портлета, в portlet.xml:
    <portlet>
        <portlet-name>MultimediaToolbar</portlet-name>
        <display-name>MultimediaToolbar</display-name>
        <portlet-class>com.vaadin.terminal.gwt.server.ApplicationPortlet2</portlet-class>
        <init-param>
            <name>application</name>
            <value>ru.snetwork.liferay.multimedia.portlet.back.ToolbarApplication</value>
        </init-param>
        <init-param>
            <name>view-jsp</name>
            <value>/view.jsp</value>
        </init-param>
        <expiration-cache>0</expiration-cache>
        <supports>
            <mime-type>text/html</mime-type>
            <portlet-mode>view</portlet-mode>
        </supports>
        <resource-bundle>ru.snetwork.liferay.multimedia.ToolbarPortlet</resource-bundle>
        <portlet-info>
            <title>MultimediaToolbar</title>
            <short-title>MultimediaToolbar</short-title>
        </portlet-info>
        <portlet-preferences>
            <preference>
                <name>portlet-setup-show-borders</name>
                <value>false</value>
            </preference>
        </portlet-preferences>
    </portlet>
В liferay-portlet.xml:
    <portlet>
        <portlet-name>MultimediaToolbar</portlet-name>
        <use-default-template>false</use-default-template>
        <instanceable>true</instanceable>
        <ajaxable>false</ajaxable>
        <header-portlet-css>/css/backoffice-multimedia.css</header-portlet-css>
        <add-default-resource>true</add-default-resource>
    </portlet>
Кроме того, чтобы не било ошибку "Reject ServeResource" на ресурсы портлеты (картинки, css, js - портлет Vaadin-овский), добавил через Hook в portal.properties:
portlet.add.default.resource.check.whitelist=58,86,87,88,103,113,145,MultimediaToolbar_WAR_SnCommonPortlets

Liferay вложенные портлеты 2

Способ вызова портлета внутри портлета предложенный в первом посте не работает в Liferay 5.2.3, который идет в бандле с Tomcat 6. Использовать PortletBagPool для получения портального ServletContext бесполезно, т.к. в версии 5.2.3 PortletBagPool не содержить встроенные портлеты.

Получить портальный ServletContext можно получить из аттрибутов ServletRequest - оказывается он передается с ключом WebKeys.CTX. Кроме того PortalUtil в API 5.2.3 немного отличается:
    public static String renderPortlet(final PortletRequest request, final PortletResponse response, final String portletId, final String queryString) {
        String result = "Error occured while running portlet";
        try {
            // Get servlet request / response
            HttpServletRequest servletRequest = PortalUtil.getHttpServletRequest(request);
            HttpServletResponse servletResponse = PortalUtil.getHttpServletResponse(response);
            HttpServletRequest portalServletRequest = PortalUtil.getOriginalServletRequest(servletRequest);

            // Get theme display
            final ThemeDisplay themeDisplay = (ThemeDisplay) servletRequest.getAttribute(WebKeys.THEME_DISPLAY);
            // Backup current state
            PortletDisplay portletDisplay = themeDisplay.getPortletDisplay();
            PortletDisplay portletDisplayClone = new PortletDisplay();
            portletDisplay.copyTo(portletDisplayClone);
            final Map requestAttributeBackup = new HashMap();
            for (final String key : Collections.list((Enumeration) servletRequest.getAttributeNames())) {
                requestAttributeBackup.put(key, servletRequest.getAttribute(key));
            }
            // Render the portlet as a runtime portlet
            try {
                StringBuilder sb = new StringBuilder();
                com.liferay.portal.model.Portlet portlet = PortletLocalServiceUtil.getPortletById(PortalUtil.getCompanyId(request), portletId);
                servletRequest.setAttribute(WebKeys.RENDER_PORTLET_RESOURCE, Boolean.TRUE);
                ServletContext ctx = (ServletContext) portalServletRequest.getAttribute(WebKeys.CTX);
                PortalUtil.renderPortlet(sb, ctx, servletRequest, servletResponse, portlet, queryString);
                result = sb.toString();
            } finally {
                // Restore the state
                portletDisplay.copyFrom(portletDisplayClone);
                portletDisplayClone.recycle();
                for (final String key : Collections.list((Enumeration) servletRequest.getAttributeNames())) {
                    if (!requestAttributeBackup.containsKey(key)) {
                        servletRequest.removeAttribute(key);
                    }
                }
                for (final Map.Entry entry : requestAttributeBackup.entrySet()) {
                    servletRequest.setAttribute(entry.getKey(), entry.getValue());
                }
            }
        } catch (Exception ex) {
            log.error(ex.getMessage(), ex);
        }
        return result;

    }