liferay-util:includeで動的に拡張できるページを作ってみた

こんにちは。

みなさまのご存知通り、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の利用法ではないと思います

RANKING
2021.01.08
2020.12.01
2020.10.30
2020.12.28
2020.12.18