AEM開発者ブログ by YAMATO

アドビ社のデリバリーパートナー大和株式会社のAEM開発者ブログです。

AEMサイトにおけるリダイレクト/リライト手法 <後編>

こんにちは。大和株式会社の牧野です。

こちらは"AEMサイトにおけるリダイレクト/リライト手法 <前編>"の続きの記事になります。

プロジェクトコードとして管理/展開 マーケティング/コンテンツチームによる運用
1. Apache mod_rewrite
2. /etc/map
3. ACS Commons Redirect Manager
4. LocationHeaderAdjuster
5. Alias
6. Vanity URL
7. ACS Commons Redirect Map Manager
8. Redirect Page Property
9. URL Mapping
10. Apache Sling Servlet
11. Vanity Path Rewrite Mapper

Vanity URL

OSGi Configuration (Resource Resolver Factory)

Property Description
Mapping Observation このパス配下のvanity pathの追加、変更がリアルタイムでシステムに反映されます。
Default Vanity Path Redirect Status vanity pathでリダイレクトが行われた時のデフォルトのステータスコードの設定。
Enable Vanity Paths 有効にするとvanity pathが使えるようになります。この設定がオフの場合、vanity pathはマッピングテーブルに追加されず無視されます。
Allowed Vanity Path Location Slingがsling:vanityPathを設定したリソースを検索するときに、ここで設定されたパス配下だけを探します。デフォルトでは、/apps/、/libs/、/content/が設定されています。この設定が空白の場合は全リポジトリが対象となります。
Denied vanity path location ここで設定されたパス配下以外のvanity pathを検索します。空の場合は全てのvanity pathが使用されます。
Vanity Path Precedence この設定を有効にすると、etc/mapよりvanityパスが優先されます。

Vanity URLもaliasと同様にページ単位で設定を行います。 今回は/content/we-retail/us/en/women.htmlのVanity URLを設定していきます。 対象ページ(/content/we-retail/us/en/women.html)のプロパティのBasicタブを開きます。

その中にVanity URLという項目があるので、そこにVanity URLを設定します。 Vanity URLは複数設定も可能です。今回はvanitywomenというVanity URLを設定しました。

/content/we-retail/us/en/women.html

/vanitywomen.htmlのリクエストから/content/we-retail/us/en/women.htmlを取得できました。

/etc/map, Vanity URLの優先順位に関する小ネタ

Vanity Path項目を終える前に一つ小ネタを紹介したいと思います。 /etc/mapとVanity URLが競合した場合、OSGi configのVanity Path Precedenceでどちらを優先するかを決めることが出来ます。デフォルトでは/etc/mapがVanity URLより優先されるようになっており、Vanity Path Precedenceがtrueの時はVanity URLが優先されます。

ただVanity Path Precedenceがtrueではない時も、ある条件を満たすと/etc/mapの設定よりもVanity URLの設定が優先されます。

Apache Sling Resource ResolverのMapEntriesクラスのコードを見てみます。

github.com

このクラスのseek()関数内に以下の条件式があります。

else if (this.nextGlobal.getPattern().length() >= this.nextSpecial.getPattern().length())

Vanity Path Precedenceがfalseだったとしても、この条件式がfalseであれば/etc/mapよりもVanity URLが優先されます。

/etc/mapを

+ etc
  + map
    + http
      + localhost_any
        - jcr:primaryType = "sling:Mapping"
        - sling:redirect = "/content/we-retail1/us/en/men.html"
        - sling:match = "[^/]+/redirect([^/]+).html$"

とし、/content/we-retail1/us/en/women.htmlのvanity pathを"redirectWomen1","redirectWomen10"とします。

結論から言うと、このケースはリクエストがredirectWomen1.htmlだと/etc/mapが優先され、redirectWomen10.htmlだとVanity URLが優先されます。

this.nextGlobal.getPattern()

の中身は

^http/[^/]+/redirect([^/]+).html$

となり

this.nextSpecial.getPattern()

はの中身は

^[^/]+/[^/]+/redirectWomen1(\..*)
^[^/]+/[^/]+/redirectWomen10(\..*)

になり、その長さが比較されます。

this.nextGlobal.getPattern().length()

は33であり

this.nextSpecial.getPattern().length()

はredirectWomen1.htmlのときは33,redirectWomen10.htmlのときは34となるため、条件式がfalse(33 >= 34)になるredirectWomen10.htmlのときに/etc/mapよりもVanity URLが優先されます。

$ curl -w "%{redirect_url}"  http://localhost/redirectWomen1.html
http://localhost/content/we-retail/us/en/men.html
$ curl -w "%{redirect_url}"  http://localhost/redirectWomen10.html
http://localhost/content/we-retail/us/en/women.html

ACS Commons Redirect Map Manager

Redirect Map Managerというツールでコンテンツ管理者はApacheの再起動、AuthorページのPublishを必要とせずに、Apache httpd Redirect Mapファイルを用いてApache httpd / AEM Dispatcherの大規模なリダイレクトとリライトのリストを一括で管理できます。

ConfigureタブのRedirect Configurationで新たにvanity pathを設定したページをPublishすることなくエンドユーザーに対してvanity pathの設定を反映させることが出来ます。

今回は例としてAuthor環境の/content/we-retail/us/en/men.htmlに新たに"vanitymen"というvanity pathを設定します。 次にエンドユーザーがアクセスするURLを設定します。今回はローカルのDispathcerで設定を行うのでhttp://localhost/vanitymen.htmlで/content/we-retail/us/en/men.htmlページを取得する設定になります。 以下画像のように設定をします。

Property Description
Scheme リクエストスキーム(例:httpまたはhttps)。これは、パスを公開URLにマッピングするために使用されます。
Domain リクエストのドメイン名。これは、パスを公開URLにマッピングするために使用されます。
Path この設定は、AEM内でcq:Pageおよびdam:Assetsを探すためのパスを示します。これらの要素は、リダイレクトプロパティが非nullの値を持つ場合に見つけられます。
Property 非nullの任意の値は、マルチバリューとして扱われ、ページ/アセットへのリダイレクトソースとして使用されます。

この設定により、/content/配下のsling:vanityPathが検索されます。 Edit EntriesタブのView Entries内の検索ボックスに*を入力すると、先ほど指定したパス配下のPorpertyの値とRedirect Map Fileの全マッピングが表示されます。

ここで各マッピングを編集・削除することが出来ます。 OriginがFileと書かれている行はRedirect Map FileのRewriteの設定になります。 Originにパスが書かれている行はvanity pathが設定されているページを指しています。 ID:1のvanitymenの設定はまだPublishされていないので、Dispatcherに/vanitymen.htmlのリクエストを送っても404になります。

またRedirect Map FileのRewrite設定ですが、Edit Entriesタブから追加することが出来ます。

PreviewタブからRedirect Map FileのRewriteの設定とvanity pathのマッピングの設定をまとめたCombined Redirect Map FileのPreviewを見ることが出来ます。

Redirect Map Managerで作成したCombined Redirect Map FileからApache RewriteMapのためのdbmファイルを生成し、それをApache側で読み込みませることにより、vanitymen.htmlをpublishなしで、Redirect Map Fileのrewriteの設定をApacheの再起動なしで適用することができます。

テキストファイルからdbmファイルを生成するためにhttxt2dbmを使用します。

httxt2dbm [ -v ] [ -f DBM_TYPE ] -i SOURCE_TXT -o OUTPUT_DBM

以下のスクリプトでCombined Redirect Map Fileのテキストファイルを取得し、dbmファイルを生成しApacheの設定ディレクトリに配置します。

#!/bin/bash

PUBLISHER_IP=YOUR_PUBLISH_IP
PORT=YOUR_PORT_NUMBER
LOG_FILE="YOUR_LOG_FILE_PATH"
TMP_DIR="YOUR_TEMP_DIR_PATH"
MAPS=(
    "YOUR_MAP_FILE_NAME"
)

APACHE_CONF_DIR="YOUR_APACHE_CONF_DIR_PATH"
USERNAME="YOUR_USERNAME"
PASSWORD="YOUR_PASSWORD"

for MAP_FILE in "${MAPS[@]}"
do
    rm "${TMP_DIR}/${MAP_FILE}.txt"
    curl -u ${USERNAME}:${PASSWORD} "http://${PUBLISHER_IP}:${PORT}/etc/acs-commons/redirect-maps/${MAP_FILE}/_jcr_content.redirectmap.txt" -o "${TMP_DIR}/${MAP_FILE}.txt" >> "${LOG_FILE}" 2>&1
    httxt2dbm -i "${TMP_DIR}/${MAP_FILE}.txt" -o "${APACHE_CONF_DIR}/tmp-${MAP_FILE}.map" >> "${LOG_FILE}" 2>&1
    mv "${APACHE_CONF_DIR}/tmp-${MAP_FILE}.map.dir" "${APACHE_CONF_DIR}/${MAP_FILE}.map.dir"
    mv "${APACHE_CONF_DIR}/tmp-${MAP_FILE}.map.pag" "${APACHE_CONF_DIR}/${MAP_FILE}.map.pag"
done

Apacheの設定を以下のようにし、スクリプトで生成したファイルを読み込みます。

RewriteMap examplemap "dbm:YOUR_REDIRECT_MAP_PATH"
RewriteCond ${examplemap:$1} !=""
RewriteRule ^(.*)$ ${examplemap:$1|/} [L,R=301]

スクリプトを実行した後に/vanitymenにリクエストを送信してみます。

$ curl -w "%{redirect_url}" localhost/vanitymen
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="http://localhost/content/we-retail/us/en/men.html">here</a>.</p>
</body></html>
http://localhost/content/we-retail/us/en/men.html

次に/addEntry2にリクエストを送信してみます。

$ curl -w "%{redirect_url}" localhost/addEntry2
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="http://localhost/content/we-retail/us/en/products/women.html">here</a>.</p>
</body></html>
http://localhost/content/we-retail/us/en/products/women.html

ページのPublish、Apacheの再起動なしに設定が反映されていることが確認できました。 毎回Redirect Map Managerでvanity pathとrewriteの設定を変更した後にスクリプトを実行するもの大変なので、運用はwindowsのタスクスケジューラーやLinuxのcronジョブなどを使用して定期的に自動でスクリプトが実行されるようにします。

Redirect Page Property

こちらもVanity URLと同様に対象ページのプロパティから設定します。 プロパティのAdvancedタブのSettings項目内のRedirectからリダイレクト先のページを選択します。

$ curl -w "%{redirect_url}" http://localhost/content/we-retail/us/en/women.html
http://localhost/content/we-retail/us/en/men.html

設定通りのリダイレクトが行われているのが確認できました。

URL Mappings

/system/console/configMgrのResource Resolver Factoryからもマッピングの設定が行えます。

URL Mappingsの設定を追加します。 URL Mappingsのシンタックスは以下です。

<internalPathPrefix><op><externalPathPrefix>

"op"は3種類のマッピングから選択できます。

">":Incoming mapping
"<":Outgoing mapping
":":Biredction mapping

今回は

/content/we-retail/us/en/:/

と設定しました。

/products.htmlのリクエストを送ると、/content/we-retail/us/en/products.htmlのページを取得出来ることが確認できました。

次にproducts直下にあるMenページへアクセスしてみます。

URL Mappings通りに外部URLが生成されていました。

Apache Sling Servlet

forword

package com.example.myproject.core.servlets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.Component;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(service=Servlet.class)
@SlingServletResourceTypes(
    resourceTypes="myproject/components/structure/page",
    methods= "GET",
    extensions="html")
public class RedirectServlet extends SlingSafeMethodsServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(final SlingHttpServletRequest req,
                         final SlingHttpServletResponse resp) throws ServletException, IOException{

        req.getRequestDispatcher("/content/we-retail/us/en/women").forward(req, resp);
    }
}

該当するリソースタイプ+拡張子htmlでGETメソッドのリクエストを送り、/content/we-retail/us/en/women.htmlへforwardされました。

sendRedirect

package com.example.myproject.core.servlets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.Component;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(service=Servlet.class)
@SlingServletResourceTypes(
    resourceTypes="myproject/components/structure/page",
    methods= "GET",
    extensions="html")
public class RedirectServlet extends SlingSafeMethodsServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(final SlingHttpServletRequest req,
                         final SlingHttpServletResponse resp) throws ServletException, IOException{

        resp.sendRedirect("http://localhost:4503/content/we-retail/us/en/women.html");
    }
}

リクエストを送ってみます。

$ curl -w "%{redirect_url}" http://localhost:4503/content/myproject/us/en.html
http://localhost:4503/content/we-retail/us/en/women.html

実装通りにリダイレクトされていました。

以下の表は、リクエストの転送方法、ブラウザの関与、リクエストとレスポンスオブジェクトの扱い、アドレス表示、速度、データの再利用といった観点から、forward()とsendRedirect()の違いをまとめたものです。

forward() sendRedirect()
リクエストの転送 同じサーバ内の他のリソースに転送。 別ドメインまたはサーバの別リソースに転送。
ブラウザの関与 ウェブコンテナが全てのプロセスを内部で処理し、クライアントやブラウザは関与しない。 コンテナがリクエストをクライアントまたはブラウザに転送するため、リダイレクトURLは新しいリクエストとしてクライアントに表示。
リクエストとレスポンスオブジェクト 状態は引き継がれる。 ブラウザによって新しいリクエストとして扱われるため状態は引き継がれない。
アドレスの表示 転送されたアドレスはアドレスバーで確認不可。 リダイレクトされた新しいアドレスはアドレスバーに表示。
速度 sendRedirect()メソッドよりも高速。 新しいリクエストを生成するため低速。
データの再利用 リクエスト属性を使用。 セッション属性、URLのクエリパラメータを使用。

Vanity Path Rewrite Mapper

OSGi Configuration (ACS AEM Commons - Error Page Handler)

Property Description
Vanity Dispatch Check 有効にすると、リソースリゾルバのマッピングの後に有効なvanity pathだった場合、リクエストはそこへフォアワードされます。

この機能を使うとIncoming mappingsとOutgoing mappingsをURL MappingsとApache Rewriteを用いて行っている場合でもVanity Pathはそれらの影響を一切受けずに対象ページを取得出来るようになります。

Vanity Path Rewrite Mapper

上記ページに書かれている設定をまず行います。

今回の例ではResource Resolver Factoryが/content/we-retail/us/en/を除去し、Apache Rewriteが/content/we-retail/us/en/を付与する設定にします。

[Apache]

RewriteCond %{REQUEST_URI} !^/(content|etc|bin)
RewriteRule ^/(.*)$ /content/we-retail/us/en/$1 [R,L]

[Resource Resolver Factory URL Mappings]

/content/we-retail/us/en/</

次に/libs/sling/servlet/errorhandler/404.jspをオーバーライドし、404.jspの内容を以下にします。

<%@page session="false"
        import="com.adobe.acs.commons.wcm.vanity.VanityURLService"%><%
%><%@include file="/libs/foundation/global.jsp" %><%
    final VanityURLService vanityURLService = sling.getService(VanityURLService.class);
    
    if (vanityURLService != null && vanityURLService.dispatch(slingRequest, slingResponse)){
        return;
    }      
%><%@include file="/libs/sling/servlet/errorhandler/404.jsp" %>

/content/we-retail/us/en/experience/skitouringのvanity pathをskiとします。

本来であればlocalhost/ski.htmlとリクエストを送った場合、ApacheのRewriteで/content/we-retail/us/en/ski.htmlとなり、そのページは存在しないため404となります。 しかしVanity Path Rewrite Mapperの機能でリソース解決が行われlocalhost/ski.htmlで/content/we-retail/us/en/experience/skitouringが取得できます。

/content/we-retail/us/en/experience/skitouring.html

最後に

いかかだったでしょうか。URL管理の方法は色々とありますが、それらをまとめた記事がなかったため今回作成してみました。URL管理に関して新しい技術が出てきたらこの記事に追記しようと考えています。

最後までご覧いただき、ありがとうございました。