Показаны сообщения с ярлыком liferay. Показать все сообщения
Показаны сообщения с ярлыком liferay. Показать все сообщения

Vaadin сервлет приложение внутри портлета

Это всего-лишь небольшой трюк, который кому-то пригодится, а кому-то нет. Речь пойдет о Liferay портале, портлетах и Vaadin приложении.

Как известно, Vaadin позволяет создать как сервлетное так и портлетное приложение. Портлеты же позволяют создавать внутри себя сервлеты. Предлагаемый трюк можно представить себе как Vaadin сервлет приложение врнутри портлета. То есть именно сервлетное приложение.

Что для этого надо:

  1. Создать портлет используя liferay-sdk;
  2. В файле liferay-plugin-package.properties указать зависимость для библиотеки vaadin.jar:
    portal-dependency-jars=vaadin.jar
    
  3. Описать приложения в web.xml:
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://java.sun.com/xml/ns/javaee"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
          http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
       id="WebApp_ID" version="2.5">
    
       <display-name>dashboard</display-name>
    
       <context-param>
          <description>Vaadin production mode</description>
          <param-name>productionMode</param-name>
          <param-value>false</param-value>
       </context-param>
    
       <servlet>
          <display-name>Dashboard</display-name>
          <servlet-name>dashboardServlet</servlet-name>
          <servlet-class>
             com.vaadin.terminal.gwt.server.ApplicationServlet</servlet-class>
    
          <init-param>
             <param-name>application</param-name>
             <param-value>org.demo.dashboard.DashboardApplication</param-value>
          </init-param>
       </servlet>
    
       <servlet-mapping>
          <servlet-name>dashboardServlet</servlet-name>
          <url-pattern>/*</url-pattern>
       </servlet-mapping>
    </web-app>
    
  4. Написать само приложение:
    package org.demo.dashboard;
    
    import com.vaadin.Application;
    import com.vaadin.ui.Label;
    import com.vaadin.ui.Window;
    
    public class DashboardApplication extends Application {
       @Override
       public void init() {
          Window mainWindow = new Window("Dashboard Application");
          setMainWindow(mainWindow);
    
          String AE = "Make everything as simple as possible, but not simpler.";
          mainWindow.addComponent(new Label(AE));
        }
    }
    
  5. Ну и собственно url — {host}/dashboard-portlet, например:
    http://localhost:7080/dashboard-portlet
    

К такому приложению можно обращаться по фиксированной линке, вход на сайт не нужен, это не часть страницы портала, не надо ломать голову со стилями портала и размерами занимаемого пространства, это просто vaadin приложение. Плюс еще в том, что приложение разворачивается внутри портлета, а это дает возможность использовать весь функционал портала, который доступен в портлете.

Как использовать пусть каждый придумает сам, в моем случае это некий dashboard-portlet, который позволяет быстро выполнять рутинные задачи. Как я уже говорил — это всего-лишь небольшой трюк, который кому-то пригодится, а кому-то нет.

Userpic в Liferay

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

Итак, для начала необходимо создать тестового пользователя, скажем Agent Smith.

Как видим стандартная картинка. Теперь берем любую картинку в png формате и добавляем пользователю userpic. Для примера можно взять эту — http://findicons.com/icon/13483/user,
512x512, 126 KB.

Liferay производит преобразование этой картинки и сохраняет ее в удобном для себя размере, при этом разширение файла сохраняется, но прозрачность теряется.

В базе данных можно найти информацию об этом файле. Как видим размеры совсем не те.

А по ID записи можно найти сохраненное изображение на диске.

Вся примудрость заключается в том, чтобы просто заменить этот файл на исходный. При этом данные в базе менять не нужно. А если поменять, то портал автоматически сделает повторное преобразование. Итак, файл заменили, очищаем кэш и получаем такой результат:

Кстати, чтобы получить доступ к изображению пользователя где-нибудь на jsp страничке, необходимо сделать такую ссылку:

<img src="${themeDisplay.pathImage}/user_portrait?screenName=${user.screenName}&companyId=${user.companyId}" />

При этом на страничку необходимо передать объект com.liferay.portal.model.User с необходимым пользователем.

Проверено только в Liferay 6.0.5.

Обновление базы данных

Liferay сервисы поддерживают версионность базы данных. Другими словами, в зависиммости от описанных в service.xml сущностей, будут созданы соответствующие таблицы в базе данных и будет сохранена версия этих таблиц на момент создания. При последующем изменениии сущностей, пересборке и развертывании сервисов произойдет автоматическое обновления соответствующих таблиц. Это удобный и легкий способ создания и поддержки слоя для работы с базой данных, но также существуют и некоторые недостатки.

Предмет обновления зависит от того, что было изменено от версии к версии. Добавление новых колонок, удаление или модификация существующих происходит просто и почти без ошибок. А вот переименование колонки влечет за собой ее удаление и создание новой, при этом данные теряются навсегда.

Есть проблема обновления таблиц с большим количеством данных. Этот процесс выполняется средствами java, поэтому банально может не хватить памяти. В этом случае можно вручную переименовывать необходимую таблицу и запускать обновление, затем удалять сгенерированную таблицу и возвращать старой прежнее имя.

Также могут возникнуть проблемы, если структуру базы надо обновить на этапе сборки проекта. В этом случае будут недоступны изменения, которые автоматически появятся после развертывания. Как выход можно отключить автообновление структуры. Для этого надо создать файл service-ext.properties в той же директории что и service.properties файл, и переопределить следующий параметр:
build.auto.upgrade=false

Mocking Liferay Services

Если вы пишете под Liferay, используете Service Builder и не знаете как протестировать свою логику, тогда этот пост для вас.

В сети можно найти различные варианты решения данной задачи. Многие из них направлены на специфическую конфигурацию зависимостей, которые позволят сервисам работать с реальной базой данных. Такие подходы сложны и противоречат принципам юнит тестирования. В этом случае надо либо поднимать базу в памяти и настраивать на ее использования сервиса, либо создавать заглушки, они же «моки», самих сервисов. Поэтому бизнес логику лучше вынести в отдельный слой.

Пока нет ничего сложного. С подходом определились, теперь необходимо найти инструмент, который позволит создавать заглушки на любой сгенерированный сервис. Такой инструмент есть — Mockito. В качестве льтернативы могу предложить Easymock, но примеры будут на Mockito.

Для примера возьмем сущность Status и опишем ее в service.xml:
<entity name="Status" table="status" local-service="true" remote-service="false">
    <column name="id" db-name="status_id" type="long" primary="true" id-type="increment"/>
    <column name="name" db-name="name" type="String"/>
</entity>

Соберем сервиса, после чего получим ряд классов, который все вместе образуют DAO слой для этой сущности. Обычно в бизнес логике используются классы *LocalServiceUtil, в нашем случае это класс StatusLocalServiceUtil. Допустим что у нас есть некий класс Controller, который вызывает метод getStatus класса StatusLocalServiceUtil и выполняет некую логику с результатом:
public class Controller {
    public void doAction(long statusId) throws SystemException, PortalException {
        // Получаем статус по его id
        Status status = StatusLocalServiceUtil.getStatus(statusId);
        // и выводим его в консоль
        System.out.println("Status = " + status);
    }
}

Целью является создание заглушки, которая будет эмулировать работу метода getStatus. А сложность в том, что этот метод статический и подменить его сложно, но возможно. Далее следует пример юнит теста, в котором происходит создание заглушки:
public class ControllerTest {
    private StatusLocalService service;
    private Controller controller;

    @BeforeMethod
    public void beforeMethod() {
        // Создаем мок сервиса используя библиотеку mockito
        service = mock(StatusLocalService.class);
        // После этой строчки класс StatusLocalServiceUtil 
        // будет использовать зуглушку или мок сервиса
        new StatusLocalServiceUtil().setService(service);
        // А это контроллер, который работает со
        // сгенерированными сервисами и ничего не знает
        // ни о каких моках
        controller = new Controller();
    }

    @Test
    public void testGetStatus()
            throws ServletException, SystemException, PortalException {
        // Конфигурируем сервис таким образом, что при вызове
        // метода с параметром 7 вернется уже другой мок —
        // мок объекта Status
        when(service.getStatus(eq(7L))).thenReturn(mock(Status.class));

        // Вот он самый, вызов метода, который тестируется
        controller.doAction(7);

        // А здесь проверяем был ли вызов метода getStatus
        // именно с параметром 7
        verify(service).getStatus(eq(7L));
    }
}

На самом деле дела обстоят сложнее и требуют более глубоких объяснений иерархии генерируемых сервисов, но для тестирования этого достаточно.

Транзакции в Liferay

Те, кто активно используют Service Builder понимают, что поддержка транзакций в сгенерированных сервисах просто необходима. Более того, это даже предусмотрено разработчиками портала. Но есть загвоздка — это не работает. По крайней мере с версией 6.0.5.

На странице http://issues.liferay.com/browse/LPS-15904 описана причина этого дефекта.

Как правило, портлеты создают новую SessionFactory для загрузки собственных hbm файлов. Также известно, что портал и портлеты используют один и тот же менеджер для создания транзакций, который в свою очередь использует SessionFactory портала. Однако портлеты должны использовать свои собственные SessionFactory, иначе портлетные сессии не будут использоваться в портальных транзакциях. Результат — транзакции в портлетах вообще не работают.

Как вариант решения можно использовать JTA transaction manager. Eсть две библиотеки на выбор: JOTM или Atomikos. На сайте Liferay есть даже статья, которая описывает как настроить портал для каждой из библитек — http://www.liferay.com/community/wiki/-/wiki/Main/JTA-XA+on+Tomcat. Но к сожалению статья не помогла. Только после продолжительного поиска удалось найти работающее решение.

Настройка портала с библиотекой JOTM:
  1. Скачиваем библиотеку с сайта http://forge.ow2.org/projects/jotm, отдельно обновляем библиотеку xapool до последней версии, даже если это бета-версия;
  2. Добавляем необходимые jar-файлы в {tomcat}/lib:
  3. Удаляем commons-logging.jar из {tomcat}/lib/ext и log4j.jar из {tomcat}/webapps/ROOT/WEB-INF/lib а также из каждого портлета;
  4. Находим файл {tomcat}/conf/context.xml и добавляем следующее в конец секции Context:
  5. <Resource 
       auth="Container" 
       name="UserTransaction" 
       type="javax.transaction.UserTransaction" />
        
    <Resource 
       auth="Container" 
       driverclassname="com.mysql.jdbc.Driver" 
       factory="org.objectweb.jotm.datasource.DataSourceFactory" 
       max-connections="100" 
       maxactive="20" 
       name="jdbc/LiferayPool" 
       username="root" 
       password="" 
       type="javax.sql.DataSource" 
       url="jdbc:mysql://localhost/lportal?useUnicode=true&amp;characterEncoding=UTF-8
          &amp;useFastDateParsing=false" /> 
        
    <Transaction 
      factory="org.objectweb.jotm.UserTransactionFactory" 
      jotm.timeout="60" />
    
    
  6. Комментируем все jdbc.default.* свойства в portal-ext.properties, а затем добавляем следующее:
  7. transaction.manager.impl=org.springframework.transaction.jta.JtaTransactionManager
    transaction.manager.property.allowCustomIsolationLevels=true
    transaction.manager.property.globalRollbackOnParticipationFailure=true
    jdbc.default.jndi.name=jdbc/LiferayPool
    
Это собственно и все. Гарантий никаких, в моем случае это заработало, в любом другом может и не заработать — это Liferay.

Обновляем Vaadin в Liferay

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

А вот что:
  1. Идем на страницу https://vaadin.com/releases;
  2. В правой колонке выбираем желаемую версию;
  3. Загружаем «Jar-only for all platforms» jar-файл и «Liferay update package» zip-архив;
  4. Удаляем существующую библиотеку {liferayWebapp}/WEB-INF/lib/vaadin.jar;
  5. Перемещаем и переименовываем новую библиотеку на место старой (см. пункт 4) — это обновление самой библиотеки;
  6. Распаковываем скачанный zip-архив, копируем с заменой его содержимое в папку {liferayWebapp}/html/VAADIN — это обновление наборов виджетов;
  7. Если используются сторонние дополнения, то набор виджетов необходимо пересобрать локально.
После запуска портала очищаем кэш браузера и радуемся новым возможностям Vaadin. 

Share plugins services

Представьте два портлета: «CLP» и «CLP Consumer». Первый содержит сервис-проект (service-plugin), сущность MyObject и соответственно набор сгенерированных классов, а второй нуждается в доступе к этому слою сервисов. Для обеспечения такого доступа необходимо следующее:
  1. Выполнить сборку сервиса в «CLP» портлете и в результате появится файл CLP-portlet-service.jar в директории {clp-portlet}/docroot/WEB-INF/lib;
  2. Сделать CLP-portlet-service.jar доступным для «CLP Consumer» портлета. Это можно сделать двумя способами:
    1. Скопировать jar файл в {clp-consumer-portlet}/docroot/WEB-INF/lib;
    2. Скопировать jar файл в глобальную директорию библиотек сервера приложений — {tomcatHome}/lib/ext но при этом удалить из всех других мест;
  3. Теперь можно вызвать метод сервиса (который находится в «CLP» портлете) из «CLP Consumer» портлета. Например:
  4. com.liferay.clp.service.MyObjectServiceUtil.testMethod();
    
Используя Tomcat 6.0 с этим были проблемы, но они вроде как решены в Liferay Portal 6.0.1 RC. Первоисточник

Service Builder, размер поля

Liferay Service Builder, по умолчанию, задает размер поля типа String равным 75 символов. Свой размер поля можно задать в файле portlet-model-hints.xml.

Пример:
  1. Описываем произвольную сущность в service.xml:
  2. <entity local-service="true" name="Note" remote-service="false" table="note">
        <column id-type="increment" name="id" primary="true" type="long"/>
        <column name="noteText" type="String"/>
    </entity>
  3. Собираем сервис;
  4. Открываем файл /docroot/WEB-INF/src/META-INF/portlet-model-hints.xml;
  5. Задаем размер поля noteText = 512:
  6. <model name="mypackage.model.Note">
        <field name="id" type="long"/>
        <field name="noteText" type="String">
            <hint name="max-length">512</hint>
        </field>
    </model>
  7. Еще раз собираем сервис.
Результат можно посмотреть в файле /docroot/WEB-INF/sql/tables.sql, в котором длина поля noteText поменялась с 75 на 512:
create table note (id LONG not null primary key, noteText VARCHAR(512) null);

Источник: http://issues.liferay.com/browse/LEP-7406

Liferay properties

Для получения значения свойства из файлов portal.properties и portal-ext.properties можно использовать класс PropsUtil.
String p = PropsUtil.get("example.property.name");

Также можно использовать класс GetterUtil для преобразования значения в нужный тип или получение значения по-умолчанию.
// если свойство существует, по GetterUtil постарается
// преобразовать его в тип long иначе вернется значение
// по-умолчанию, которое указано вторым параметром - 111
long p = GetterUtil.getLong(PropsUtil.get("example.property.id"), 111);