liferay-util:includeで動的に拡張できるページを作ってみた - aegif Labo Blog Liferay
こんにちは。
みなさまのご存知通り、liferay-utilのincludeタグは、他のjspファイルを現在のjspページに挿入することができます。
<liferay-util:include page="/path/to/page.jsp" servletContext="<%= application %>" />
普段、servletContextはapplication(すなわち当該taglibが記述されているjspのservletContext)に指定し、pageも同じポートレット内の別ページに指定されるのは一般ですが、今回はapplication以外のservletContext(すなわち、全く別モジュールのservletContext)とそのservletContext所在モジュールのjspファイルを利用したいと思います。
仕組み
Liferayでは、WebContextを有するOSGIモジュール毎に、osgi.web.symbolicname=当該OSGIモジュールのシンボリック名のServletContext osgiサービスを提供します。
g! lb | grep 'Journal Web'
1211|Active | 15|Liferay Journal Web (5.0.155)|5.0.155
g! b 1211 | grep ServletContext
{javax.servlet.ServletContext}={service.id=12254, osgi.web.contextpath=/o/journal-web,
service.scope=singleton, osgi.web.symbolicname=com.liferay.journal.web, osgi.web.version=5.0.155,
osgi.web.contextname=journal-web, service.bundleid=1211}
OSGIフレームワークから別ServletContextサービスを取得し、liferay-util:includeのservletContext属性にすると、別モジュールのjspファイルを挿入することができます。これで、OSGIフレームワークの動的な特性を利用し、ページを動的に拡張することが可能です。
ポートレット
ポートレット本体の仕組みは、IncludePanel OSGIサービスをServiceTrackerで取得し、画面上のパネル(liferay-ui:tabs)を動的に表示することです。
@Component(
immediate = true,
property = {
"com.liferay.portlet.display-category=category.sample",
"com.liferay.portlet.header-portlet-css=/css/main.css",
"com.liferay.portlet.instanceable=true",
"javax.portlet.display-name=UtilIncludeWeb",
"javax.portlet.init-param.template-path=/",
"javax.portlet.init-param.view-template=/view.jsp",
"javax.portlet.name=" + UtilIncludeWebPortletKeys.UTILINCLUDEWEB,
"javax.portlet.resource-bundle=content.Language",
"javax.portlet.security-role-ref=power-user,user"
},
service = Portlet.class
)
public class UtilIncludeWebPortlet extends MVCPortlet {
@Override
public void render(RenderRequest renderRequest, RenderResponse renderResponse)
throws IOException, PortletException {
IncludePanel[] panels = Stream.of(
panelTracker.getTrackingCount() == 0 ? new IncludePanel[0] : panelTracker.getServices()
).toArray(IncludePanel[]::new);
String[] panelTitles = Stream.of(panels).map(p -> p.getName()).toArray(String[]::new);
renderRequest.setAttribute("panels", panels);
renderRequest.setAttribute("panelTitles", String.join(",", panelTitles));
renderRequest.setAttribute("attributeA", "attr-A");
renderRequest.setAttribute("attributeB", "attr-B");
super.render(renderRequest, renderResponse);
}
@Activate
protected void activate(BundleContext ctx) {
panelTracker = new ServiceTracker<>(ctx, IncludePanel.class, null);
panelTracker.open();
}
@Deactivate
protected void deactivate() {
panelTracker.close();
}
ServiceTracker<IncludePanel, IncludePanel> panelTracker;
}
IncludePanelの定義は以下になります。
public interface IncludePanel {
public String getName();
public String getPage();
public ServletContext getServletContext();
}
ポートレットの画面(view.jsp)
ポートレット画面は、ポートレットが取得したIncludePanelのservletContextをliferay-ui:includeのパラメータとして利用し、外部のpageをliferay-ui:tabsで描画します。
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@taglib uri="http://liferay.com/tld/util" prefix="liferay-util" %>
<%@taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui" %>
<div>Registered Panels</div>
<liferay-ui:tabs names="${ panelTitles }" refresh="<%= false %>">
<c:forEach var="p" items="${ panels }">
<liferay-ui:section>
<liferay-util:include
page="${ p.page }"
servletContext="${ p.servletContext }"
/>
</liferay-ui:section>
</c:forEach>
</liferay-ui:tabs>
ここまでの画面はIncludePanelサービスオブジェクトがないのためRegistered Panelsのみが表示されています。では、次にIncludePanelを実装してみましょう

IncludePanelA
ポートレットと別のバンドルでIncludePanelA implements IncludePanelを実装しましょう。
- Liferayが自動的にがServletContextサービスを登録させるためbnd.bndにWeb-ContextPathの指定は必要
- Liferayが自動的に登録したServletContextサービスのosgi.web.symbolicnameの属性は当該バンドルのシンボリック名なのでReferenceのターゲットに指定
@Component(
immediate = true,
service = IncludePanel.class
)
public class IncludePanelA implements IncludePanel {
@Override
public String getName() {
return "Panel A";
}
@Override
public String getPage() {
return "/view.jsp";
}
@Override
public ServletContext getServletContext() {
return servletContext;
}
@Reference(
target = "(osgi.web.symbolicname={..バンドルのシンボリック名..})"
)
private ServletContext servletContext;
}
- <liferay-util:include>が利用するpageのデフォルトパスはポートレットと同じsrc/main/resources/META-INF/resroucesのため、IncludePanelAの画面表示用jspファイル(例: view.jsp)も同じフォルダーの下に置く
<div> from panel A </div>
<div> Attribute A: ${attributeA}</div>
当該バンドルをデプロイすると、前述のポートレットのServiceTrackerはIncludePanelAを捉え、画面上の表示パネルが追加されます。

パネルBとC
同様にパネルBとパネルCも実装します(OSGIサービスの実装は省略する)
Bのview.jsp
<div> from panel B </div>
<div> Attribute B: ${attributeB}</div>
Cのview.jsp
<div> from panel C </div>
<div> Attribute AB: ${attributeA}+${attributeB}</div>
こうすると、UtilIncludeWebポートレットは正常にパネルA〜Cを表示できました。


gogo shellでパネルA~Cのいずれをstopすると、そのパネルを表示画面から外すことができます。
1469|Active | 15|util-include-panel-b (1.0.0)|1.0.0
g! stop 1469

まとめとディスカッション
<liferay-util:include>のServletContextパラメータにより別モジュールのjspファイルで動的にポートレット画面を拡張できることが分かりました。また、includeタグは幾つかの特性を持っています。
- includeタグの本質はservletのdispatchなので、include元とinclude先のjspページの暗黙objectのrequestは一致します
- そのため、include元のポートレットにrequest.setAttributeで設定したパラメータをinclude先のjspファイルにもアクセスできます
- 逆に、別のポートレットのjspファイルもincludeできるが、そのjspページが利用するrequestの属性(eg: DisplayContextなど)はinclude元のポートレットが提供できないのため、エラーになりそうなので、これは正しいincludeの利用法ではないと思います
