LoginSignup
2
4

More than 3 years have passed since last update.

【Java】RomeでRSSリーダーAPIを簡単に実装する

Last updated at Posted at 2021-03-24

まえがき

・RSSリーダーって何?という方は Wikipedia:フィードリーダー をご参考ください。
・あまり流行してないですが、作る機会があったので載せます。
・RSSリーダーはあまり処理を行わず戻りのオブジェクトをJSON形式で出力、 JS側で整形する方式を想定。
・JSで直接他ドメインのRSSを読もうとすると CORS のエラーが出てできないので、サーバー側でRSSリーダーの処理をしています。
Caffeine 利用のキャッシュ有り
・JSで呼び出す方法も記載

環境

・Windows10 64bit
・Spring Framework 4
・Rome 1.15.0

Rome とは

Rome is a Java framework for RSS and Atom feeds. The framework consist of several modules

Java の RSS, Atom フィード用ライブラリですね。

テストしてみたところ、国内の有名どころのRSSフィードは対応できているようです。
内容によってはエラーが出るものもありますが、特定のブログであり、書き方でカバーできそうです。

API (json)

RssApiController.java
@RestController
public class RssApiController {

    private static Logger logger = LoggerFactory.getLogger(RssApiController.class);
    private static @NonNull Cache<String, Object> rssReaderCache = null;

    public static final String TYPE_ALL = "ALL";
    public static final String TYPE_ENTRY = "ENTRY";

    // キャッシュ設定(お好みで)
    @PostConstruct
    public void initRssReaderCache() {
        rssReaderCache =
                Caffeine.newBuilder()
                .maximumSize(256)
                .expireAfterWrite(30, TimeUnit.MINUTES)
                .build();
    }

    /**
     * RSS リーダーAPI
     * @param q        RSS の URL
     * @param type     出力タイプ(ALL:すべて, ENTRY:エントリのみ)
     * @param limit    出力するエントリ件数
     * @param request
     * @param response
     * @return
     */
    @RequestMapping(value = "/api/rssReader/", 
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE, method = RequestMethod.GET)
    public Object rssReader(@RequestParam(required=true) String q,
            @RequestParam(required=false) String type,
            @RequestParam(required=false) Integer limit,
            HttpServletRequest request, HttpServletResponse response) {

        if (StringUtils.isBlank(q)) {
            return new ErrorJson("URL が不正です。");
        }
        if (StringUtils.isBlank(type)) {
            type = TYPE_ALL;
        }
        if (!TYPE_ALL.equalsIgnoreCase(type)) {
            type = TYPE_ENTRY;
        }
        Object result = null;
        try {
            result = readCachedRss(q, type, limit);
        } catch (Exception e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            logger.error(sw.toString());
            return new ErrorJson("RSS読み込みに失敗しました。");
        }
        return result;
    }

    private Object readCachedRss(final String q, final String type, final Integer limit)
            throws Exception {

        Object result = null;
        String strLimit = limit == null ? "" : limit.toString();
        final String[] params = { q, type, strLimit };
        final String cacheKey = String.join("-", params);
        try {
            result = rssReaderCache.get(cacheKey, key -> {
                try {
                    return readRss(q, type, limit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
        } catch (Exception e) {
            throw e;
        }
        return result;
    }

    private Object readRss(String q, String type, Integer limit) throws Exception {
        URL url = null;
        try {
            url = new URL(q);
        } catch (MalformedURLException e) {
            throw e;
        }
        XmlReader reader = null;
        Object result = null;
        try {
            reader = new XmlReader(url);
            SyndFeed feeder = new SyndFeedInput().build(reader);
            List<SyndEntry> entryList = feeder.getEntries();
            if (limit != null && entryList != null && entryList.size() > limit) {
                feeder.setEntries(entryList.stream().limit(limit).collect(Collectors.toList()));
            }
            if (Objects.equal(TYPE_ENTRY, type)) {
                result = feeder.getEntries();
            } else {
                result = feeder;
            }
        } catch (Exception e) {
            throw e;
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    throw e;
                }
            }
        }
        return result;
    }
}

Romeのお陰で簡単にできますね。
ちなみに ErrorJson は単なるエラー用のクラスです。

ErrorJson.java
public class ErrorJson {
    private String error;

    // 何らかのオブジェクトを付録として返したい場合に使う
    private Object appendix;

    public ErrorJson(String error) {
        this.error = error;
    }

    public ErrorJson(String error, Object appendix) {
        this.error = error;
        this.appendix = appendix;
    }

    public void setError(String error) {
        this.error = error;
    }

    public String getError() {
        return error;
    }

    public Object getAppendix() {
        return appendix;
    }

    public void setAppendix(Object appendix) {
        this.appendix = appendix;
    }
}

Javascript(API呼び出し~整形)

  <script>
    // HTMLから画像のURLを抽出する
    function extractImgSrcFromHtml(str) {

      var tmpDiv = document.createElement('div');
      tmpDiv.innerHTML = str;
      var imgs = tmpDiv.querySelectorAll('img');
      var result = [];
      for(var i=0;i < imgs.length;i++) {
        result.push(imgs[i].getAttribute('src'));
      }
      return result;
    }

    $(function(){
      // rss 処理 (最新1件を整形表示)
      $.ajax({
        url: '/your/api/rssReader/',
        type: 'GET',
        data: { 'q': '${rssUrl}', 'type': 'entry', 'limit': 1 },
        dataType: "json",
      })
      .done( function(data, textStatus, jqXHR) {
        if (data.error) {
          // エラー処理
          console.log(data.error);
          $('.rss_block').hide();    // rss 出力枠ごと非表示
          return;
        }
        if (data && data.length > 0) {
          // テンプレート
          var tmpl =
          '<div class="title">{title}</div>\
            <dl class="list">\
            <dt><img src="{teaserImageUrl}" alt="{title}" style="">\
            </dt>\
            <dd>{bodyPlain}\
              {externalUrlBlock}\
            </dd>\
            </dl>\
          </div>\
          ';
          var externalUrl = "${blogUrl}";
          var externalUrlBlock = '';
          if (externalUrl && externalUrl.length > 0) {
            externalUrlBlock = '<div><a href="${blogUrl}" target="_blank">ブログを見る</a></div>';
          }
          $.each(data, function(index, v){
            var title = "";
            if (v.title) title = v.title;
            if (!v.description || !v.description.value) return true;
            // タグを除去した内容を取得
            var bodyPlain = v.description.value.replace("<![CDATA[", "").replace("]]>", "");
            bodyPlain = bodyPlain.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g,'');
            // 長い場合は以下略で
            if (bodyPlain && bodyPlain.length > 200) {
              bodyPlain = bodyPlain.substr(0,200) + "...";
            }
            // 画像のURLを抽出
            var imgSrcs = extractImgSrcFromHtml(v.description.value);
            var imgUrl = "/image/no-img.png";   // 画像がない場合に表示する画像
            if (imgSrcs && imgSrcs.length > 0 && imgSrcs[ 0 ]) {
              imgUrl = imgSrcs[ 0 ];
            }
            // description にない場合でも contents にある場合がある
            else if (v.contents && v.contents.length > 0 && v.contents[0].value) {
              imgSrcs = extractImgSrcFromHtml(v.contents[0].value);
              if (imgSrcs && imgSrcs.length > 0 && imgSrcs[ 0 ]) {
                imgUrl = imgSrcs[ 0 ];
              }
            }
            // テンプレートを置換
            var replaced = tmpl.replace(/\{title\}/g, title);
            replaced = replaced.replace(/\{bodyPlain\}/g, bodyPlain);
            replaced = replaced.replace(/\{teaserImageUrl\}/g, imgUrl);
            replaced = replaced.replace(/\{externalUrlBlock\}/g, externalUrlBlock);
            var rssFeeds = document.getElementById('rss-feeds');
            rssFeeds.insertAdjacentHTML('beforeend', replaced);
          });
        }
        else {
          $('.rss_block').hide();
        }
      })
      .fail( function(data, textStatus, jqXHR) {
        $('.rss_block').hide();
        console.log("rssReader failed.");
      });
    });
  </script>

以上、お疲れさまでした!

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4