Tuesday, April 16, 2019

Liferayのサイトにタグを埋め込む

こんにちは。おおたにです。

最近ではアクセス解析、Web接客、マーケティングオートメーション等々、より良いデジタルエクスペリエンスを提供するためのツールが一般的に利用されるようになり、これらのツールをWebサイトに組み込む機会も多くなっているかと思います。今回は、これらのツール組み込みに必要な、カスタムタグ(特にJavaScriptのスクリプト)をLiferayサイトに埋め込む方法について紹介したいと思います。

ページ毎に直接JavaScriptを埋め込む


まずはお手軽な方法から紹介します。

1. 以下のようにページの上部、「ページ設定」アイコンをクリックします。


2. 「詳細設定」をクリックして「ページの下に実行されるJavaScriptを貼り付ける」欄に埋め込むJavaScriptコードをコピーし、「保存」をクリックします。


以上でページへのJavaScript埋め込みは完了です。とてもお手軽!

特定のサイトの公開ページ/非公開ページ全部に一括で埋め込みたい場合は、画面上部の「メニュー」アイコン->「ページ作成設定」->「サイトページ」で「公開ページ」/「非公開ページ」のギアアイコンをクリックしてみましょう。先ほどの手順2.のような画面が表示され、公開ページ/非公開ページ一括で埋め込むことができます。


なお、この方法については以下の点にご注意ください。
  • ページ毎/サイト毎に設定する必要がある
  • JavaScriptしか埋め込めない
  • <body>タグにしか埋め込めない
少数のページに必要なJavaScriptを埋め込む場合にはとても役立ちますが、多数のページにタグを埋め込む場合には管理面で現実的ではない可能性があります。

全てのページにタグを埋め込む


ページ数が少ない/今後ページが増えることがない状況であれば、先ほど紹介した方法で対応できます。しかし、ページ数が多い/今後ページが増えていくという状況では、埋め込み作業に膨大な工数がかかったり、埋め込み忘れが発生したりということが考えられます。また、埋め込みたいタグがJavaScriptでない場合も先ほどの方法では対応できません。

そのような場合は、テーマ(Theme)を開発することで実現することができます。テーマとはWebサイトの見栄えを制御するLiferayの仕組みのことで、その中に必要なタグを埋め込むと、テーマが適用された全てのページにタグが自動的に埋め込まれます。

また、複数のタグを複数ページに埋め込む必要がある場合、一般的にはタグマネージャーが使われるかと思います。この場合はタグマネージャーのタグをテーマに埋め込む形になります。以下では、Google Tag Manager(以下GTM)を例に、カスタムテーマにGTMタグを埋め込むためのおおまかな手順を紹介します。

1. GTM公式ドキュメントを参考にGTMをセットアップします。
  •  セットアップが進むと、こちらのドキュメントにあるように<head>タグと<body>タグにそれぞれGTMタグを埋め込むように指示されます。
2. Liferayのテーマを開発します。まだテーマ開発を行っていない方は以下の記事/ドキュメントを参考にテーマを開発します。
  Liferay 7 / DXPでのテーマの作り方

3. タグを埋め込む先のファイル /src/templates/portal-normal.ftl をエディタ等で開きます。このファイルがWebサイトの<html><head><body>タグのひな型になっています。
  • もし上記ファイル存在しない場合は、一度テーマをビルドしてください。 /build/templates/portal_normal.ftl が生成されるので、このファイルを /src/templates フォルダにコピーします(/src 以下に templatesフォルダが無い場合は作成してください)。
4. GTMセットアップの指示に従い、<head>タグと<body>タグにそれぞれGTMタグを追加します。
埋め込みの一例(styledテーマへの埋め込み)

5. テーマをビルドし、生成されたwarファイルを<LIFERAY_HOME>/deploy フォルダにコピーすればデプロイ完了です。

6. 埋め込み対象のページ/サイトのテーマを先ほど作成したカスタムテーマに変更し、GTMタグが埋め込まれていることを確認します(ブラウザのデベロッパーツールでHTMLソースを確認し、"googletagmanager"等で検索すれば<script>タグ、<noscript>タグが埋め込まれていることを確認できるはず)。

以上でGTMタグの埋め込みは完了です。あとはGTM側でタグとトリガーを設定し、必要なページに必要なタグを配信するだけです。


今回の紹介内容は以上です。テーマ開発のハードルは若干高いですが、一度作ってしまえばググっとハードルが下がりますので、ぜひチャレンジしてみてください。

Thursday, March 28, 2019

Liferay DXP 7.1でシンプルにサーバサイドロジックを追加する

こんにちは。おおたにです。

今回は、Liferay DXP 7.1にサーバサイドロジックを追加する方法を紹介します(7.0でも使えます)。本記事は、主に以下のような要件にマッチします。

  • Web APIとしてロジックを公開する
  • Liferayとは直接関わりのない機能を実装する(認証や各種サービスなどLiferayが提供する機能は使わない)

Liferayが提供するサービスを使った実装を行うのであればサービスビルダを使ってリモートサービスを実装し、そうでなければJAX-RSを使って実装するのが定石かと思うのですが、ここではOSGi HTTP Whiteboardを使った実装を紹介します。
(もちろん、Liferayと完全に疎結合なロジックであれば、Liferayの前面に構築されたWebサーバのレイヤでCGI等を使って実装するのもありですが、ここではそれには触れません)

OSGi HTTP Whiteboardとは


OSGi HTTP WhiteboardとはOSGi Compendiumで規定されている仕様の1つで、サーブレットやサーブレットフィルタ、HTML/JS/画像等の静的リソースなどをOSGiバンドルを使って公開するためのシンプルな仕組みを提供します。Liferay DXPはOSGiの基盤の上に実装されているため、そのカスタマイズにOSGiの仕様を利用しない手はありません。具体的には、以下の2ステップでWeb APIを公開できます。シンプル!

  • Servlet等をコンポーネントとして実装したOSGiバンドルを開発する
  • 開発したOSGiバンドルをLiferay(OSGi実行基盤)にデプロイする

この方法は素のServletを直接公開するようなものなので、認証等でLiferayの仕組みを利用したい、もしくは関連する複数のAPIをまとめて実装/公開したい場合はJAX-RSを使い、Liferayの仕組を全く使用せずなるべく疎結合に、ちょっとしたロジックを実装したい場合はHTTP Whiteboardを使うというような使い分けが考えられます。

実装してみよう


では早速実装してみましょう。まずはServletコンポーネントを作成します。実装のポイントは以下の4点です。

  • HttpServlet の実装クラスを作成する
  • @Component アノテーションでコンポーネント宣言する
  • osgi.http.whiteboard.servlet.name でServlet実装の完全修飾クラス名を指定する
  • osgi.http.whiteboard.servlet.pattern でServletをマップするURLパスを指定する

例えば以下のように実装します。
package jp.aegif.liferay.sample;
 
@Component(
  immediate = true,
  property = {
    "osgi.http.whiteboard.servlet.name=jp.aegif.liferay.sample.SampleWhiteboardServlet",
    "osgi.http.whiteboard.servlet.pattern=/get-email-address-hash" // Web APIのパスを設定する
  },
  service = Servlet.class
)
public class SampleWhiteboardServlet extends HttpServlet {
  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
    // 必要な処理とレスポンスを記述する
    // sample implementation to generate SHA-1 hash of Munchkin API Private Key + Email Address
    response.setContentType("text/plain;charset=UTF-8");
    PrintWriter printWriter = response.getWriter();
    try {
      String source = API_PRIVATE_KEY + Objects.toString(request.getParameter("email"), "");
      byte[] bytes = MessageDigest.getInstance("SHA-1").digest(source.getBytes(StandardCharsets.UTF_8));
      printWriter.print(DatatypeConverter.printHexBinary(bytes));
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
    printWriter.close();
  }

  private static final String API_PRIVATE_KEY = "change_it"; // Set your Munchkin API Private Key
}

このサンプルは、SaaSのマーケティングオートメーションツールMarketoassociateLead API向けハッシュ生成ロジックを実装してみたものです。Liferayのフォームで入力した情報を使ってリード登録やcookie紐づけを行いたい場合に利用できるかと思います。associateLead APIのコールには、MarketoのAPIプライベートキーを利用して生成したハッシュが必要なため、サーバサイドロジックとして実装する必要があります。なお、このロジックをクライアントサイドのJavaScriptで実装すると、APIプライベートキーが全世界に向けて公開されてしまいますのでくれぐれもご注意を…。

あとはOSGiバンドル作成のためのbnd.bndやbuild.gradle等のビルド用ファイルを用意してjarファイルをビルドするだけです。bndtoolsやgradleを個別にセットアップしてビルドすることも可能ですが、Liferay Developer StudioやLiferay IDEを使うと簡単にビルドできます。

デプロイして試してみよう


ビルドが成功したら、早速デプロイしてみましょう。

  1. <LIFERAY_HOME>/deploy フォルダにjarファイルをコピーする
  2. ログに STARTED jp.aegif.liferay.sample.whiteboard.servlet_1.0.0 と表示される

以上でデプロイ完了です。ホットデプロイされるため、Liferayを再起動することなくコンポーネントの機能が有効化されるはずです。

動作確認のために http://localhost:8080/o/get-email-address-hash?email=test@example.com にアクセスしてみましょう。ハッシュコードが返ってくればOKです。LiferayではOSGi HTTPエンドポイントパスが /o/ となっているため、Web APIのURLは http://<サーバ名>:<ポート番号>/o/<ServletをマップしたURLパス> となります。リクエストパラメータ email でメールアドレスを指定します。


今回の内容は以上です。みなさんもHTTP Whiteboardを使ってシンプルにWeb APIを公開してみてください!

Wednesday, January 9, 2019

Liferay DXP 7.1 リリース

明けましておめでとうございます。小川です。
本年もよろしくお願いします。

昨年下期にLiferay DXP 7.1がリリースされました。
なので、今回は前バージョン7.0からの変更点をいくつか紹介しようと思います。

前提として、
本記事の対象者は管理者や利用ユーザを想定しています。
(ディベロッパー向けの変更点についてはほとんど触れていません。)
変更点は大小様々ありますが、比較的大きな変更点に絞っています。

下記のような流れで紹介していきます。

主な変更点を簡単に説明します。
 ①コンテンツページ
 ②ページ管理
 ③ナビゲーションメニュー
 ④フォーム
 ⑤GDPRサポート
 ⑥Adaptive Media
 ⑦ワークフロー管理

2. ナビゲーションメニューについて
1. で取り上げた変更点の1つであるナビゲーションメニューにフォーカスし、詳しく説明します。

では、本題に入ります。


検証環境
OS:macOS High Sierra 10.13.6
Liferay :Liferay 7.1.1 CE GA2


1. Liferay DXP 7.1の主な変更点と説明


さっそくですが、主な変更点を挙げ説明していきます。

①コンテンツページ


ページを作成する際に選択できるページ種別にコンテンツページが増えました。


コンテンツページはフラグメントと呼ばれるコンテンツをページ上に配置することができるページです。
フラグメントは下図のようなフラグメントエディタを用いて作成することができ、作成したフラグメントはページエディタを用いてページ上に配置することができます。


また、特定のタグを用いることで指定領域(テキストと画像)をページ上で直接編集することができます。加えて、ポートレットの埋め込みが可能です。標準ポートレットだけでなく、自分で作成したポートレットも埋め込むことができます。

簡単な使い方はこちらのサイトにある動画を見てもらうとわかりやすいかと思います。

コンテンツ作成者がコンテンツの雛形(フラグメント)のみ作成し、マーケティング担当者がコンテンツ自身に手を加えることなく、テキストや画像を目的や状況に応じてページ上で編集するような使い方があるかなと思います。

②ページ管理

下図のようにページ階層を可視化して管理できるようになりました。
ここでページの作成、設定、削除ができます。サイト内のページ管理を単一の画面に集めることで管理しやすくなると思います。



③ナビゲーションメニュー

ナビゲーションメニューとは下図のように「サイト内のページを表示しクリックすることで対象ページに移動できるメニュー」です。
今回、どのようにアップデートされたのかは次節で詳しく説明します。


④フォーム

フォームは大幅にアップデートされた部分だと思います。条件付きルールなどより高度なフォームが作成できるようになりました。新フォームに関して説明すると長くなりそうなので、次回のブログで詳しく説明しようと思います。

⑤GDPRサポート

GDPRに対応するための下記の機能が追加されています。

●Sanitizing User Data
これまでユーザを削除する機能はありましたが、ユーザに関連するコンテンツまでは対応していませんでした。本機能ではユーザに関するコンテンツまで削除することにより、GDPRの要件に対応しています。

●Exporting User Data
ユーザをExportする機能はこれまでもありましたが、上記と同様にユーザに関する個人データ(コンテンツやコメント等)も抽出できるようになりました。

⑥Adaptive Media

これはアクセスする各デバイスに応じて画像解像度を制御する機能となります。この機能によりデバイスやネットワークへの負担を軽減することができます。

⑦ワークフロー管理

ワークフローの設定等はすべてコントロールパネル内の1つのエリアにまとめられ、またワークフローの複製やバージョン管理が可能となりました。


2. ナビゲーションメニューについて


今回、ナビゲーションメニューの扱い方が大きく変わりました。以前のバージョンから利用している方はどのように使うのかわかりづらいと思うので、本節ではナビゲーションメニューにフォーカスします。

前バージョンとの大きな違いはナビゲーションメニューを自由に作成・設定できるようになった点です。自身で様々なパターンのナビゲーションメニューを作成し、ナビゲーションメニューポートレットからどのナビゲーションメニューを使用するか選択することができるようになりました。
文字だけではわかりづらいと思うため、ナビゲーションメニューの作成から設定までを詳しく説明します。

管理者メニューの[Build]→[Navigation Menus]でナビゲーションメニュー画面に遷移します。そこで右上の[+]からナビゲーションメニューを作成します。今回は下図のように”Test Navi Menu”という名のものを作成しました。
(”Default”と”Default Private"ははじめからあるナビゲーションメニューです。)



”Test Navi Menu”を開きます。まだ何もありません。


右上の[+]→[Page]を選択するとサイト内にあるページを選択することができます。


必要なページを選択します。(今回は4つのページを選択しました。)
これでナビゲーションメニューの作成完了です。


次にナビゲーションメニューの設定を行います。ページに配置してあるナビゲーションメニューポートレットの設定を開きます。



先ほど作成したナビゲーションメニューを選択します。


これで設定も完了です。
自分で作成したナビゲーションメニューが下記のように表示されました。


おそらく慣れるまでは使いにくいと思われるかもしれません。しかし、ナビゲーションメニューに自由度を持たせることで、複雑なサイトマップに対応しやすくなったり、特定のページ、コンテンツへの誘導性も高まるのではないでしょうか。

ちなみに、ナビゲーションメニューを階層(サブメニュー的に)で表示させたい場合は下記のように対象ページをドラッグして動かすことで簡単に実現することができます。




3. まとめ


いかがでしたか。簡単ではありましたが大きな変更点について紹介できたかと思います。今回詳しく紹介できなかったフラグメントやフォーム、登場しなかった変更箇所についても今後紹介していければと思います。

4. おまけ


いろいろ触ってみて感じたことを少しだけ挙げておきます。
●起動時間が短縮された気がする。従来の半分ぐらい。
●ディベロッパー向けになるかもしれませんが、画面上でGOGOシェルを実行できたり、サーバー情報を見れたりします。人によっては便利かもですね。



Friday, September 21, 2018

Alfresco:Share UIで行うコンテンツモデリング

こんにちは。たなかです。

小学生の子供達の夏休みが終わり、給食も始まったのでお弁当作りから解放されました。

長期休暇のあとは給食のありがたみをいつにも増して感じます。

今回はShare UI上管理ツールのモジュールマネージャでモデル作成をしてみたいと思います。
使用しているバージョンは、201707 GAです。


管理ツールのメニューからモデルマネージャをクリックします。


モデルの作成ボタンを押して必要事項を入力し、作成します。



次に作成したモデルをクリックしてタイプとアスペクトを作ります。

まずは、カスタムタイプの作成を行います。


作成したタイプにプロパティを作成します。


続いてアスペクトと、タイプのプロパティと同じようにアスペクトのプロパティを作成します。


各タイプとアスペクトのレイアウトをレイアウトデザイナーで設定します。
レイアウトデザイナーでの設定は必須です。
設定を行わずに有効化しても表示されないのでご注意ください。

自分でレイアウトを設定したい場合は説明に従い設定することもでき、デフォルトレイアウトの設定やデフォルトレイアウトを元にカスタマイズを行うこともできます。

デフォルト設定を適用してみます。


レイアウト設定が完了したら、保存します。

モデルのメニューリストから登録したモデルを有効化します。

有効化されたことを確認したら、

タイプを設定したいコンテンツのメニューからタイプの変更を選択し、設定します。


先に作成したカスタムタイプ(aegif:customType)というタイプがリストに存在しています。

タイプを設定すると文書詳細のプロパティエリアにタイププロパティが表示されました。


アスペクトの管理でアスペクトの付与、取り外しもできます。

タイプがないアスペクトのみのモデルの設定も行なってみたいと思います。

先ほどと同様、レイアウトデザイナーでレイアウトを保存しておきます。



有効化する前は、文書のアスペクト管理で確認しても該当のアスペクトが表示されていません。


モデルを有効化します。

再度先ほどの文書のアスペクト管理を確認すると、有効化したアスペクトが表示されています。

タイプは1つのノードに1つしか設定できませんが、アスペクトは複数設定することができます。

アスペクトを付与してみました。



設定した文書のメニューリストのプロパティエリアに追加したアスペクトのプロパティが表示されました。



一度タイプを設定するとShare UIからは変更ができないようになっているため、
すでにタイプを設定した下記コンテンツはタイプを変更しようとしてもリストには何も表示されていません。

反対にアスペクトは自由に取り外しが可能です。


モデルマネージャで行うコンテンツモデリングでは、簡易的に作成することが可能なので、細かい設定などを行いたい場合はShare UI上からはできません。
英語表記と日本語表記を分けたい場合もcontent-model.propertiesとcontent-model_ja.propertiesのようなファイルを言語別に用意して各ファイルに設定する必要があります。

Share UI上でモデルを作成し、モデルのアクションメニューからエクスポートを行うと作成したモデルのxmlファイルがダウンロードできるのでそれを利用して詳細を設定する、ということも可能です。


今回は、Share UIから設定するコンテンツモデリングについてご紹介させていただきました。

Wednesday, August 1, 2018

Demystifying Liferay categories: leftCategoryId / rightCategoryId

Liferay allows site administrators to define a "vocabulary" containing categories such as:

  • Cheese
  • Bread
  • Butter

Web content can then be classified using these categories, for instance a news article about canapé would be assigned the categories "Cheese" and "Bread".

Create a Liferay page, put a "Category Navigation" application and an "Asset Publisher" on it, link them and voilà! You have a nice page where relevant articles appear depending on the category you clicked.

Categories can have sub-categories:



Left and right


When you click on a category, you will notice this parameter in the URL: categoryId=21434

You can inspect this category by putting it in the "parentCategoryId" field of this Liferay API page:
https://yourserver/api/jsonws?signature=%2Fassetcategory%2Fget-child-categories-1-parentCategoryId#serviceResults

You will notice that each category has a "leftCategoryId" and a "rightCategoryId" field. What are those!?

They help Liferay easily display items in the category navigation widget. Here is how they are stored in database:

mysql> SELECT leftCategoryId, rightCategoryId, name FROM AssetCategory WHERE vocabularyId=21431 ORDER BY leftCategoryId;
+----------------+-----------------+-----------+
| leftCategoryId | rightCategoryId | name      |
+----------------+-----------------+-----------+
|              2 |               7 | Cheese    |
|              3 |               4 | Camembert |
|              5 |               6 | Roquefort |
|              8 |               9 | Bread     |
|             34 |              35 | Butter    |
+----------------+-----------------+-----------+
5 rows in set (0.00 sec)

When you click on a category, for instance "Cheese", Liferay will use the category identifier to get the Cheese row, and get the left and right, here "2 and "7". Liferay will then use these identifiers in the following SQL request:

SELECT categoryId, name FROM AssetCategory WHERE (groupId = ___) AND (leftCategoryId BETWEEN ___ AND ___);

... where the first blank is the Liferay site identifier, and the second and third are the first and last categories of the vocabulary, in our example 2 and 7. Let's run it:

mysql> SELECT categoryId, name FROM AssetCategory WHERE (groupId = 20181) AND (leftCategoryId BETWEEN 2 AND 7);
+------------+-----------+
| categoryId | name      |
+------------+-----------+
|      21433 | Cheese    |
|      21703 | Camembert |
|      21704 | Roquefort |
+------------+-----------+
3 rows in set (0.01 sec)

Voilà! You get all of the categories whose articles should appear on the page when you click "Cheese".

You might ask: Why not just use parentId to get all categories whose parent is the Cheese categories, and then recursively their own sub-categories using the same technique?
Answer: It would not scale. For vocabularies with n levels of sub-categories, the recursion would trigger O(n!) SQL requests, whereas the left/right strategy uses O(1) SQL requests. This technique is called nested set model. Its drawback is that left/right information must be rebuilt every time an administrator moves or deletes a category.

Rouelle du Tarn, by Tangopaso - Wikimedia Commons, Public domain

Applicable to Liferay 6.2 and Liferay 7.0
Nicolas Raoul