Wicket sitemap を出力してみる


作成中のアプリケーションで、sitemap.xml を出力する必要はあり、
wicketで作っているので、
なんかないか調べたところ、wicketstuff に sitemap出力用のライブラリがあったので、
それを使ってみた結果を書いておきます。


稼働環境の情報

Java Version 、Wicket の Version は以下の通りです。

  • OS
OS X El Capitan 
バージョン 10.11.6 
  • Java
java -version
------------------------------
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)
------------------------------
  • Wicket
<dependency>
    <groupId>org.apache.wicket</groupId>
    <artifactId>wicket-core</artifactId>
    <version>7.4.0</version>
</dependency>

まず、サイトマップに関するガイドラインに記載がある。 一般的なサイトマップXMLの仕様について

サイトマップのガイドラインは、google の Search Console のヘルプ、
サイトマップの仕様を説明しているサイト(これもgoogleが作成?) が参考になりました。

以下、サイトマップの仕様で気になったところ(個人的に重要だと思ったところ)を記載します。

  • 1 つのサイトマップにはサイズが 10 MB、 URL は 50,000 件以下にする。

  • サイトマップの形式は、[XML]、[RSS、mRSS、Atom 1.0]、[テキスト]、[Google サイト] の4つ

  • サイトマップ インデックス ファイル を作る場合は、 サイトマップは送信しなくて良い。

  • サイトマップ インデックス ファイルのxmlフォーマットは、 サイトマップのxmlフォーマットと少し違う

  • サイトマップ ファイルを http://example.co.jp/catalog/sitemap.xml に置いた場合は、
    http://example.co.jp/catalog/ から始まる URL を含めることができるが、
    http://example.co.jp/images/ から始まる URL を含めることはできない。


Example の 挙動の確認

sitemap indexファイルの生成もできるのかわからないので、
実際にwicket stuff の example を参考に挙動を確認してみます。

1. pom.xml に 依存関係を追加

    <!-- https://mvnrepository.com/artifact/org.wicketstuff/wicketstuff-sitemap-xml -->
    <dependency>
        <groupId>org.wicketstuff</groupId>
        <artifactId>wicketstuff-sitemap-xml</artifactId>
        <version>7.4.0</version>
    </dependency>

2. SiteMap.java を作成する

core/ExampleSiteMap.java at master · wicketstuff/coreをコピーして、
SiteMap.javaを作成します。

3. Applicationクラスで、SiteMap を登録

Application#mountResource()で、SiteMap.javaとsitemap.xmlを紐付けします。

        // -------------------------------------------------------------------------------
        // SiteMap
        // -------------------------------
        mountResource("sitemap.xml", new SiteMap());

4. デプロイして、出力を確認

ローカル環境にデプロイして、http://127.0.0.1:18080/sitemap.xmlにアクセスすると、
以下のsitemap.xmlが出力されます。
sitemapというか、sitemap index ファイルが出力されます。
※サーバをポート 18080 で起動させてます。

<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<script/>
<sitemap>
<loc>
http://127.0.0.1:18080/sitemap.xml?sourceindex=0&offset=0
</loc>
<lastmod>2016-10-29</lastmod>
</sitemap>
<sitemap>
<loc>
http://127.0.0.1:18080/sitemap.xml?sourceindex=0&offset=1000
</loc>
<lastmod>2016-10-29</lastmod>
</sitemap>
...
</sitemapindex>

続いて、sitemap index ファイルに含まれるURL http://127.0.0.1:18080/sitemap.xml?sourceindex=0&offset=0 にアクセスすると、
以下のxmlが出力されます。

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<script/>
<url>
<loc>http://127.0.0.1:18080/sitemap.xml?number=0</loc>
<lastmod>2016-10-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>http://127.0.0.1:18080/sitemap.xml?number=1</loc>
<lastmod>2016-10-29</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
...
</urlset>

sitemap.xml が出力されました。
sitemap.xml の出力、 sitemap index ファイルの出力ともにサポートされているようです。
※50000件以上のエントリーが存在する場合も、使用できそう。
sitemap インデックスファイルに パラメータを渡すと、sitemap.xml になる動作は少しグレーですが、
サイトマップに関するガイドライン の求める挙動はしているように思います。

Exmaple実装を、実アプリケーションで使用できるように、 サイトマップURLの出力のされ方、ページの指定方法、エントリーへのリンクの出力方法を変更します。

アプリケーションでサイトマップに出力したいエントリーの前提事項は以下の通りです。

前提事項

  • 作ってるのは音楽フェスのサイトで、音楽フェスごとのページと、アーティストページがある

  • フェスのページ、アーティストページとも記事のインデックスIDはRDBに保存している

  • RDBからはJPAでデータを取得する


Example を 変更

ExampleからSitemap.javaは以下の通り変更しています。

1. Sitemap.java

変更点は以下の通りです。

  • getDataSources()で IOffsetSiteMapEntryIterable の、2つ実装クラスを返すようにした。

フェスごとのページと、アーティストページがあるので、
フェスページのsitemapを作成するクラスと、
アーティストページのsitemapを作成するクラスのインスタンスを生成して、
返すようにしました。

  • getDomain() はオーバーライド

親クラスSiteMapIndex.java の getDomain()の実装は、以下の通りです。

    public String getDomain() {
        if (domain == null) {
            final Request rawRequest = RequestCycle.get().getRequest();
            if (!(rawRequest instanceof WebRequest)) {
                throw new WicketRuntimeException("sitemap.xml generation is only possible for http requests");
            }
            WebRequest wr = (WebRequest) rawRequest;
            domain = "http://" + ((HttpServletRequest) wr.getContainerRequest()).getHeader("host");
        }
        return domain;
    }

HTTP headerの host から host 名 取得しているので、以下、記事の問題(経験上たまに見舞われる)
nginxでリバースプロキシ先にホスト名が引き継がれない - Fujimura にぶつかりかねないので、
リソースバンドルから、取得するなければhostから取得するように変更しました。

package xyz.monotalk.sitemap;

import com.google.api.client.repackaged.com.google.common.base.Strings;
import org.apache.wicket.extensions.sitemap.IOffsetSiteMapEntryIterable;
import org.apache.wicket.extensions.sitemap.SiteMapIndex;

import java.util.MissingResourceException;
import java.util.ResourceBundle;

/**
 * SiteMap
 */
public class SiteMap extends SiteMapIndex {

    private static final long serialVersionUID = 7074357449807043532L;

    @Override
    public IOffsetSiteMapEntryIterable[] getDataSources() {

        return new IOffsetSiteMapEntryIterable[]{
                new FestivalOffsetSiteMapEntryIterable(getDomain()),
                new ArtistOffsetSiteMapEntryIterable(getDomain())
        };
    }

    /**
     * getDomain
     *
     * @return
     */
    public String getDomain() {
        String domain = null;
        try {
            domain = ResourceBundle.getBundle("WicketApplication").getString("domainName");
        } catch (MissingResourceException e) {
            // Do Nothing...
            return super.getDomain();
        }
        if (Strings.isNullOrEmpty(domain)) {
            return super.getDomain();
        }
        return domain;
    }
}

続いて、FestivalOffsetSiteMapEntryIterable.java について

2. FestivalOffsetSiteMapEntryIterable.java

以下、変更点の説明になります。

  • RDBからの取得は遅延初期化する
    SiteMap Entry クラスでは、何回もEntryを取得する必要はないように思ったので、
    1回 ELEMENTS_PER_BLOCK数分のレコードを取得して、そのレコードを使い回すようにしています。

  • Guice の @Transactional アノテーションを付与しているので、close() では何もしない。
    @Transactionnal をつけておくと、勝手にEntiryManager.remove()まで実行してくれるので、
    close()メソッドでは何もしないように実装しました。

  • BasicSiteMapEntryBuilder を作った。
    BasicSiteMapEntry のコンストラクタに4つ引数を渡すのが何かしっくりこなかったので、
    Builderクラスを作成しました。
    ※これは趣味の話かと思います。

  • JPAのリポジトリクラスは、class内でInjector経由でInject
    FestivalOffsetSiteMapEntryIterableクラス内で、Injector経由でInjectしています。
    @Inject アノテーションでInjectする場合は、mountResourceメソッド実行時に以下のようにインスタンスを、
    生成する必要があります。

mountResource("/sitemap.xml", InjectorHolder._new(SiteMap.class));
package xyz.monotalk.sitemap;

import lombok.NonNull;
import org.apache.wicket.extensions.sitemap.IOffsetSiteMapEntryIterable;
import org.apache.wicket.extensions.sitemap.ISiteMapEntry;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import xyz.monotalk.models.rdb.entity.Festival;
import xyz.monotalk.models.rdb.repository.FestivalRepository;
import xyz.monotalk.inject.initialize.InjectorHolder;
import xyz.monotalk.pages.festival.detail.FestivalDetailPage;

import java.util.Date;
import java.util.List;

/**
 * FestivalOffsetSiteMapEntryIterable
 */
public class FestivalOffsetSiteMapEntryIterable implements IOffsetSiteMapEntryIterable {

    private FestivalRepository repository = InjectorHolder._new(FestivalRepository.class);

    private int festivalTotalCount = -1;

    private Date changedDate = null;

    private String domain = null;

    private static final int ELEMENTS_PER_BLOCK = 1000;

    /**
     * FestivalOffsetSiteMapEntryIterable
     *
     * @param domain
     */
    public FestivalOffsetSiteMapEntryIterable(@NonNull String domain) {
        this.domain = domain;
    }

    @Override
    public int getUpperLimitNumblocks() {
        if (festivalTotalCount == -1) {
            festivalTotalCount = repository.getRecordCount().intValue();
        }
        return (int) Math.ceil((double) festivalTotalCount / ELEMENTS_PER_BLOCK);
    }

    /**
     * fullUrlFrom
     *
     * @param charSequence
     * @return
     */
    private String fullUrlFrom(@NonNull CharSequence charSequence) {
        return this.domain + "/" + charSequence.toString();
    }

    @Override
    public int getElementsPerSiteMap() {
        return ELEMENTS_PER_BLOCK;
    }

    @Override
    public Date changedDate() {
        if (changedDate == null) {
            changedDate = repository.getMaxUpdateDate();
        }
        return changedDate;
    }

    @Override
    public SiteMapIterator getIterator(final int startIndex) {

        return new SiteMapIterator() {

            int numCalled = 0;

            private List<Festival> festivals = null;

            /**
             * getFestivals
             * @param limit
             * @param offset
             * @return
             */
            private List<Festival> getFestivals(int limit, int offset) {
                if (festivals == null) {
                    festivals = repository.findAllByLimitAndOffset(limit, offset);
                }
                return festivals;
            }

            /**
             * hasNext
             * @return
             */
            public boolean hasNext() {
                return numCalled <= ELEMENTS_PER_BLOCK && numCalled < getFestivals(ELEMENTS_PER_BLOCK, startIndex).size();
            }

            /**
             * next
             * @return
             */
            public ISiteMapEntry next() {
                Festival elem = getFestivals(ELEMENTS_PER_BLOCK, startIndex).get(numCalled);
                PageParameters pageParameters = new PageParameters();
                pageParameters.add("id", elem.getId());
                numCalled++;
                final CharSequence url = RequestCycle.get()
                        .mapUrlFor(FestivalDetailPage.class, pageParameters)
                        .toString();
                return new BasicSiteMapEntryBuilder(fullUrlFrom(url))
                        .setModified(elem.getUpdateDate())
                        .setFrequency(ISiteMapEntry.CHANGEFREQ.WEEKLY)
                        .setPriority(0.5).createBasicSiteMapEntry();
            }

            /**
             * remove
             */
            public void remove() {
                throw new UnsupportedOperationException("not possible here..");
            }

            public void close() {
                // Do Nothing...
                // @Transactional 付与で勝手にcloseされるので何もしない
            }
        };
    }
}

3. BasicSiteMapEntryBuilder.java

これは、説明することはありません。
Builerクラスです。。

package xyz.monotalk.sitemap;

import lombok.NonNull;
import org.apache.wicket.extensions.sitemap.BasicSiteMapEntry;
import org.apache.wicket.extensions.sitemap.ISiteMapEntry;

import java.util.Date;

/**
 * BasicSiteMapEntryBuilder
 */
public class BasicSiteMapEntryBuilder {
    private String url;
    private Date modified;
    private double priority;
    private ISiteMapEntry.CHANGEFREQ frequency;
    private static final double DEFAULT_PRIORITY = 0.5;

    public BasicSiteMapEntryBuilder(@NonNull String url) {
        this.url = url;
        modified = new Date();
        priority = DEFAULT_PRIORITY;
        frequency = ISiteMapEntry.CHANGEFREQ.WEEKLY;
    }

    public BasicSiteMapEntryBuilder setModified(@NonNull Date modified) {
        this.modified = modified;
        return this;
    }

    public BasicSiteMapEntryBuilder setPriority(double priority) {
        if (priority > 1.0) {
            priority = 1.0;
        }
        if (priority < 0.0) {
            priority = 0.0;
        }
        this.priority = priority;
        return this;
    }

    public BasicSiteMapEntryBuilder setFrequency(@NonNull ISiteMapEntry.CHANGEFREQ frequency) {
        this.frequency = frequency;
        return this;
    }

    public BasicSiteMapEntry createBasicSiteMapEntry() {
        return new BasicSiteMapEntry(this.url, this.modified, this.priority, this.frequency);
    }
}

sitemap index ファイル に出力されるsitemap.xml のファイル名が動的パラメータなのが、
少し気に入らないですが、特に問題はなさそうです。
※SiteMapIndex.java に相当するクラスを自作すればそれも解決できます。


補足. Wicket wiki 記載のSitemap.xml 生成方法

Wicket wiki にも、Sitemap.xmlの生成例はあったので、
補足として、記載しておきます。

リンクは以下です。

wicketstuff の sitemap を使うような作りの説明ではなくて、
Wicket の page クラスで、xml を生成するような形で実装しています。
この例だと、sitemap インデックスファイルは生成ではなく、sitemap の出力で、5万件までならば、
この実装方法で問題ないように思います。

また、Enchache2系に含まれている SimplePageCachingFilter を使って、Cache してもいいんじゃない。
って記載してあり、そちらは確かにやっておいた方がいい気がしました。

以上です。

コメント