まえがき
・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)
@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 は単なるエラー用のクラスです。
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>
以上、お疲れさまでした!