Asset Publisher and Lucene indexes

As I wrote in previous posts here and here in latest version of Liferay there is performance loss around Asset Publisher and permission checking. We discussed that problem in LPS-22332 and we prepared a proof of concept based on concept that Lucene will be faster that plain DB queries. Just as we thought this was a good idea. Full document with results of our comparison tests you can read here.

Advertisement

Asset Publisher and queries count

In this post, I will mainly describe my expierience with Asset Publisher in Liferay 6.1 (87489 revision). In the previous post I showed how Asset Publisher constructs entry query and how many records it picks up.

The starting point of my test was the following configuration:

  • About 10 000 AssetEntries
  • First page with 1 Asset Publisher without any filters. It gets ~4900 records.
  • Second page with 1 Asset Publisher with categories filter. It gets ~ 300 records.

Following table shows results of my tests.

Scenario Guest Logged In user
1st request.
Asset Publisher with ~4900 results. 20 per page.
~ 9800 queries to database ~ 29300 queries to database
2nd request.
Asset Publisher with ~4900 results. 20 per page.
0 queries to database ~ 29300 queries to database
1st request.
Asset Publisher with ~300 results. 20 per page.
~ 900 queries to database ~ 900 queries to database
2nd request.
Asset Publisher with ~300 results. 20 per page.
0 queries to database ~ 30 queries to database

Interesting situation is the second row of this table (Asset Publisher with ~4900 results. 20 per page and logged in user). Asset Publisher retrievs 5000 records from database. Now in a loop it sends one query to transform AssetEntry to specific entity (JournalArticle in my case) and sends two queries to check permissions. It gives three queries in one record.

So Liferay sends 2 * (5000 * 3 queries per item) = 30 000 queries. In the trunk version the second query is cached so it gives 15 000 queries.

Fortunately, most of the queries is partially cached.

From a business point of view I want to display 20 articles in one AssetPublisher with pagination so I don’t understand why my portal sends over 30 000 queries to database. It’s a horrible situation when simple task frequently overuses my server resources.

One of bottleneck in Liferay 6.1

A few days ago we’ve prepared a jmeter’s test. Our goal was to find a bottlenecks in the newest Liferay 6.1 portal. So we load about 7k AssetEntries (JournalArticles only) and drag and drop one Asset Publisher on the site. To my surprise – page loaded for about 30 seconds. In my mind was born a strange thought: is it possible have we done something wrong? Maybe our content’s importer is killing the portal. So it’s time to debug Liferay.

In AssetEntryServiceImpl we found interesting code:

protected Object[] filterQuery(AssetEntryQuery entryQuery)
throws PortalException, SystemException {
[...]
int end = entryQuery.getEnd();
int start = entryQuery.getStart();
entryQuery.setEnd(end + PropsValues.ASSET_FILTER_SEARCH_LIMIT);
entryQuery.setStart(0);
List entries = assetEntryLocalService.getEntries(entryQuery);
[...]
}

We want to pick 20 entries and Liferay get 20 + 5000 records from database. Twenty – because AssetPublisher has to display this records, plus 5000 – just in case. It gives 10040 records per request in every Asset Publisher. Furthermore – Liferay calls this method two times in every request. Firstly to get countEntries (pagination), secondly to get entities.
Later in this method Liferay checks permissions on every entity and picks only entities which have View permission. I understand this logic, but it’s very expensive.

Maybe Liferay developers should find more optimal way to get Asset Entries. I leave this question open.

Tips & Tricks: include jsp into vm template

This tip&trick is dedicated to all developers who work with themes and vm’s. Probably you know that you have only little set of services from VelocityVariables class and some variables from init.vm. If you want call normal service method this is a huge problem. My question was: how can I include a jps file to vm file? Liferay has a lot of scriptlets and calls services from jsp. Why I can’t call it from VM? Many of you tell me: “I know how can you do it. Put this line of code and it’s ready”

#set ($categoriesService = $portal.getClass()
.forName("com.liferay.portlet.asset.service.AssetCategoryLocalServiceUtil")
.getMethod("getService", null).invoke(null, null))

My answer is: Yessss, it works. But I want use a jstl lib (e.g. <aui> or <ui>) as well. In my opinion the simpliest way to do it is include jsp file to themeServletContext. Put your file.jsp into your theme:
your-theme/docroot/path/to/your/jsp/file.jsp

and paste this piece of code into your vm (for example portal_normal.vm):

$theme.include($themeServletContext, "/path/to/your/jsp/file.jsp")

And enjoy all Liferay’s functionalities!

Why I can’t use portal-impl in my portlet?

When I started my adventure with liferay and I was young I had a big headache with a view: „Why liferay developers didn’t share portal-impl.jar for external portlets. This is so stupid”. When we don’t have access to this module our development is very difficult. I can’t extend my Action class and I must manually attend all incoming requests beacause portal-impl.jar provides most classes used by the portal’s portlets. Why I can’t use this one?

Today, when I spent a long time with liferay my answer is: NO, you should not use a portal-impl.jar in your portlet. Remember: NEVER put portal-impl into anywhere other than where it came from. This will cause untold problems beacause portal-impl is the central core of the portal and it is a main piece of cake. So you can’t put portal-impl in lib directory in your portlets or in other strange places. Why? Liferay uses all kinds of classloader tricks to get access to various classes. So adding portal-impl to library folder in your portlet will realy mess this up. There will be duplicated instances of classes which are load from different class loaders. Additionaly portal-impl.jar contains internal classes of liferay core implementation, so if you use it in this version remember that next one can change all method’s or just remove some classes.

In portal-service.jar liferay provides a set of methods, which you can use in your own implementation. It sits in the global classpath (on tomcat this is <tomcat>/lib/ext/*.jar). All the services and utilities are available from these.

Wait a second… this feature looks nice, but how can I develop my controller (for example struts)? You should use apache struts bridge to handle lifecycle of struts portlet (refer sample-struts-portlet in community svn plugin repository). Also Liferay provides some kinds of bridges (e.g. mvc, php, python and more) and it is the simplest and the best way to create your own action class or your own controller. In my opinion there are some very important reasons to do with this way:

  • your code is totally independent
  • portlets are more flexible
  • there is a big chance that your code will be working with newer version of liferay
  • you write code with framework you like

But Liferay’s StrutsPortlet bridge should not be used for any new development. If you really must use Struts 1.x it’s better to use Apache portlet bridge. You can also use Liferay’s MVCPortlet. Or, you can use one of the other frameworks supported by Liferay: Spring MVC, Struts 2, and JSF.

DOC_22

Tips & Tricks: add new structure’s field into web content

During my everyday work some people asked me: „Is it possible to add new type of structure’s field. If yes, how can I do it?”. In my opinion the fastest way to achieve this result is overriding few files in ext plugin or hook. We decided to use hook. In this post We show you the example, how we did it. Our new field is a datetime component.

Firstly, we should know in liferay there are two places where we can add and edit structure’s fields, So we must check our new functionality two times:

  1. CP -> Web content -> Structures (tab) -> Add or edit structure
  2. CP -> Web content -> add or edit web content -> change structure

Files which are responsible to render and serve structure’s fields in first point:

  • html/portlet/journal/js/main.js
  • html/portlet/journal/edit_article_content_xsd_el.jsp
  • html/portlet/journal/edit_structure_xsd_el.jsp

In second point:

  • html/portlet/journal/edit_article_structure extra.jspf
  • html/portlet/journal/edit_article_extra.jsp

Ok, let’s get started.

1. Create a new hook and configure that it can override jsps files. For example:

<hook>
<custom-jsp-dir>/custom_jsps</custom-jsp-dir>
</hook>

2. Copy html/portlet/journal/js/main.js into custom_jsps folder. In this step we must register new model’s field (below registerFieldModel method’s declaration).

registerFieldModel('Date', 'date', 'DateField', true);

Next we add new field’s type to map called „model” in function _fieldInstanceFactory()

var model = {
  'boolean': Journal.FieldModel.Boolean,
  'document_library': Journal.FieldModel.DocumentLibrary,
  'image': Journal.FieldModel.Image,
  'image_gallery': Journal.FieldModel.ImageGallery,
  'link_to_layout': Journal.FieldModel.LinkToPage,
  'list': Journal.FieldModel.List,
  'multi-list': Journal.FieldModel.MultiList,
  'selection_break': Journal.FieldModel.SelectionBreak,
  'text': Journal.FieldModel.Text,
  'text_area': Journal.FieldModel.TextArea,
  'text_box': Journal.FieldModel.TextBox,
  'date': Journal.FieldModel.Date
};

3. Copy html/portlet/journal/edit_article_content_xsd_el.jsp and add code inside journal-article-component-container. We compare our new type with elType variable.

<c:if test='<%= elType.equals("date") %>'>
  <%@ include file="/html/portlet/journal/edit_article_content_xsd_date_el.jspf" %>
</c:if>

Ok, it  looks fine, but what’s is in edit_article_content_xsd_date_el.jspf“? This is the main point of this tutorial. In this file we define „look and feel” our new component. For example:

<aui:fieldset>
<%
Calendar cal = CalendarFactoryUtil.getCalendar(timeZone, locale);
if (elContent != null && !elContent.equals("")) {
  DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd H:m");
  Date date = (Date)formatter.parse(elContent);
  cal.setTime(date);
}

%>

<aui:field-wrapper>
  <div>
    <liferay-ui:input-date
      dayParam="dateDay"
      dayValue="<%= cal.get(Calendar.DATE) %>"
      disabled="<%= false %>"
      firstDayOfWeek="<%= cal.getFirstDayOfWeek() - 1 %>"
      monthParam="dateMonth"
      monthValue="<%= cal.get(Calendar.MONTH) %>"
      yearParam="dateYear"
      yearValue="<%= cal.get(Calendar.YEAR) %>"
      yearRangeStart="<%= cal.get(Calendar.YEAR) %>"
      yearRangeEnd="<%= cal.get(Calendar.YEAR) + 5 %>"
    />

    &nbsp;

    <liferay-ui:input-time
      amPmParam="dateAmPm"
      amPmValue="<%= cal.get(Calendar.AM_PM) %>"
      hourParam="dateHour"
      hourValue="<%= cal.get(Calendar.HOUR) %>"
      minuteParam="dateMinute"
      minuteValue="<%= cal.get(Calendar.MINUTE) %>"
      minuteInterval="<%= 1 %>"
    />
  </div>
</aui:field-wrapper>
</aui:fieldset>

You must know that elContent variable contains our field’s value (date in this example).

4. Copy html/portlet/journal/edit_structure_xsd_el.jsp and add our new type into select’s list.

<option <%= elType.equals("date") ? "selected" : "" %> value="date"><liferay-ui:message key="date" /></option>

Okey, let’s deploy our hook and look into add structure form. What are we seeing? Oh, our new field’s type.
What’s next? We must allow to edit structure directly in edit article form.

5. Let’s look inside edit_article_structure_extra.jspf file and copy them. Then we define „look and feel” our new field inside journalFieldModelContainer div by adding new div with dataType=”<type>” attribute.

<div dataType="date">
<%
String elContent = "";
%>
<%@ include file="/html/portlet/journal/edit_article_content_xsd_date_el.jspf" %>
</div>

6. In file html/portlet/journal/edit_article_extra.jsp we add to menu (lists of field’s types) our new component because we want allow „drag and drop” functionality. So we create new div’s tag with dataType=”<type>” attribute inside journalComponentList div.

<div dataType="date">
<liferay-ui:message key="date" />
</div>

We have new component with great design but how we save selected date and time value. Let’s see main.js file. There is getContent method where there are a lot of “if” conditions. So, we must write a new one:

if (type == 'date') {
	var dateContents = componentContainer.all('select');
	var date = [Number.NaN, Number.NaN, Number.NaN, Number.NaN, Number.NaN, Number.NaN, Number.NaN];

	if (dateContents) {
		dateContents.each(
			function(select, selectIndex, selects) {
				var options = select.all('option');
				if (options) {
					options.each(
						function(option, optionIndex, options) {
							if (option.get('selected')) {
								//name selecta, zawierajacego wybrany item
								var selectName = select.get('name');
								var selectedValue = option.val();

								if (selectName.match(/dateDay$/)) {
									date[0] = selectedValue;
								} else if (selectName.match(/dateMonth$/)) {
									date[1] = selectedValue;
								} else if (selectName.match(/dateYear$/)) {
									date[2] = selectedValue;
								} else if (selectName.match(/dateHour$/)) {
									date[3] = selectedValue;
								} else if (selectName.match(/dateMinute$/)) {
									date[4] = selectedValue;
								} else if (selectName.match(/dateAmPm$/)) {
									date[6] = selectedValue;
								}
							}
						},
						select
					);
				}
			}
		);

	}
	content = instance.getDateAsString(date);

}

Also we must define the „getDateAsString” function

getDateAsString: function(stringDateArray) {
		if (stringDateArray.length != 7) {
			return '';
		}
		var intDateArray = new Array(stringDateArray.length);
		var notNaNExist = false;
		for (var i = 0; i < stringDateArray.length; i++) {
			intDateArray[i] = parseInt(stringDateArray[i], 10);
			if (intDateArray[i] != NaN) {
				notNaNExist = true;
			}
		}

		if (!notNaNExist) {
			return '';
		}

		var day = intDateArray[0];
		var month = intDateArray[1];
		var year = intDateArray[2];
		var hour = intDateArray[3];
		var minute = intDateArray[4];
		var second = 0;
		var ampm = intDateArray[6];

		if (!isNaN(month)) {
			month = month + 1;
		}

		if (ampm == 1 && !isNaN(hour)) {
			hour = hour + 12;
		}

		if (month < 10) {
			month = '0' + month;
		}
		if (day < 10) {
			day = '0' + day;
		}
		if (hour < 10) {
			hour = '0' + hour;
		}
		if (minute < 10) {
			minute = '0' + minute;
		}
		if (second < 10) {
			second = '0' + second;
		}

		return year + '-' + month + '-' + day + ' ' + hour + ':' + minute;
	}

I know that my code is not optimal. This post is only tutorial how to write new fields of structure. If you have better idea how resolve this problem just write to me.

If you want download all source files or download ready to deploy war file click links:

* I wrote this hook using trunk version of liferay CE (rev. 70450)