Skip to content

Commit

Permalink
Merge branch '887-rss' of https://github.com/hbz/lobid-resources
Browse files Browse the repository at this point in the history
  • Loading branch information
fsteeg authored and sol committed Aug 2, 2018
2 parents b00a3a7 + 15c95d2 commit 3beb03f
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 52 deletions.
12 changes: 11 additions & 1 deletion web/app/controllers/resources/Accept.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ enum Format {
HTML("html", "text/html"), //
RDF_XML("rdf", "application/rdf+xml", "application/xml", "text/xml"), //
N_TRIPLE("nt", "application/n-triples", "text/plain"), //
TURTLE("ttl", "text/turtle", "application/x-turtle");
TURTLE("ttl", "text/turtle", "application/x-turtle"), //
RSS("rss", "application/rss+xml");

String[] types;
String queryParamString;
Expand All @@ -33,6 +34,15 @@ private Format(String format, String... types) {
this.queryParamString = format;
this.types = types;
}

public static Format of(String format) {
for (Format f : Format.values()) {
if (format.equals(f.queryParamString)) {
return f;
}
}
return Format.JSON_LD;
}
}

/**
Expand Down
132 changes: 93 additions & 39 deletions web/app/controllers/resources/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.Collator;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -69,6 +71,7 @@
import views.html.details_item;
import views.html.index;
import views.html.query;
import views.html.rss;
import views.html.stars;

/**
Expand Down Expand Up @@ -106,6 +109,14 @@ public class Application extends Controller {
/** The number of seconds in one day. */
public static final int ONE_DAY = 24 * ONE_HOUR;

/** Date format used in RSS feeds. */
public static final DateFormat RSS_DATE_FORMAT =
new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss Z");

/** Date format used in lobid-resources describedBy.dateCreated field. */
public static final DateFormat LOBID_DATE_FORMAT =
new SimpleDateFormat("yyyyMMdd");

/**
* @return The index page.
*/
Expand Down Expand Up @@ -177,64 +188,60 @@ public static Promise<Result> query(final String q, final String agent,
String filter) {
// bulk -> jsonl, see https://github.com/hbz/lobid-resources/issues/861
final String format = f != null && f.equals("bulk") ? "jsonl" : f;

final String aggregations = aggs == null ? "" : aggs;
if (!aggregations.isEmpty() && !Search.SUPPORTED_AGGREGATIONS
.containsAll(Arrays.asList(aggregations.split(",")))) {
return Promise.promise(() -> badRequest(
String.format("Unsupported aggregations: %s (supported: %s)",
aggregations, Search.SUPPORTED_AGGREGATIONS)));
}
addCorsHeader();
String uuid = session("uuid");
if (uuid == null) {
uuid = UUID.randomUUID().toString();
session("uuid", uuid);
}

String responseFormat = Accept.formatFor(format, request().acceptedTypes());
boolean isBulkRequest =
responseFormat.equals(Accept.Format.BULK.queryParamString);
if (isBulkRequest) {
response().setHeader("Content-Disposition",
String.format(
"attachment; filename=\"lobid-resources-bulk-%s.jsonl\"",
System.currentTimeMillis()));
}
String cacheId = String.format("%s-%s-%s-%s", uuid, request().uri(),
Accept.formatFor(format, request().acceptedTypes()), starredIds());
addResponseHeaders(responseFormat);

String cacheId = createCacheId(format);
@SuppressWarnings("unchecked")
Promise<Result> cachedResult = (Promise<Result>) Cache.get(cacheId);
if (cachedResult != null)
return cachedResult;

Logger.debug("Not cached: {}, will cache for one hour", cacheId);
QueryBuilder queryBuilder = new Queries.Builder().q(q).agent(agent)
.name(name).subject(subject).id(id).publisher(publisher).issued(issued)
.medium(medium).t(t).owner(owner).nested(nested).location(location)
.filter(filter).word(word).build();
String sortBy =
responseFormat.equals(Accept.Format.RSS.queryParamString) ? "newest"
: sort;
Search index = new Search.Builder().query(queryBuilder).from(from)
.size(size).sort(sort).aggs(aggregations).build();
Promise<Result> result;
if (isBulkRequest) {
result = bulkResult(q, nested, owner, index);
} else {
result = Promise.promise(() -> {
Search queryResources = index.queryResources();
boolean returnSuggestions = responseFormat.startsWith("json:");
JsonNode json = returnSuggestions
? toSuggestions(queryResources.getResult(), format.split(":")[1])
: queryResources.getResult();
String s = json.toString();
boolean htmlRequested =
responseFormat.equals(Accept.Format.HTML.queryParamString);
return htmlRequested
? ok(query.render(s, q, agent, name, subject, id, publisher, issued,
medium, from, size, queryResources.getTotal(), owner, t, sort,
word))
: (returnSuggestions ? withCallback(json)
: responseFor(withQueryMetadata(json, index),
Accept.Format.JSON_LD.queryParamString));
});
}
.size(size).sort(sortBy).aggs(aggregations).build();

Promise<Result> result =
createResult(q, agent, name, subject, id, publisher, issued, medium,
from, size, owner, t, sort, word, nested, responseFormat, index);
cacheOnRedeem(cacheId, result, ONE_HOUR);

return resultOrError(q, agent, name, subject, id, publisher, issued, medium,
from, size, owner, t, sort, word, result);
}

private static String createCacheId(final String format) {
String uuid = session("uuid");
if (uuid == null) {
uuid = UUID.randomUUID().toString();
session("uuid", uuid);
}
String cacheId = String.format("%s-%s-%s-%s", uuid, request().uri(),
Accept.formatFor(format, request().acceptedTypes()), starredIds());
return cacheId;
}

private static Promise<Result> resultOrError(final String q,
final String agent, final String name, final String subject,
final String id, final String publisher, final String issued,
final String medium, final int from, final int size, final String owner,
String t, String sort, String word, Promise<Result> result) {
return result.recover((Throwable throwable) -> {
Html html = query.render("[]", q, agent, name, subject, id, publisher,
issued, medium, from, size, 0L, owner, t, sort, word);
Expand All @@ -250,6 +257,53 @@ public static Promise<Result> query(final String q, final String agent,
});
}

private static Promise<Result> createResult(final String q,
final String agent, final String name, final String subject,
final String id, final String publisher, final String issued,
final String medium, final int from, final int size, final String owner,
String t, String sort, String word, String nested, String responseFormat,
Search index) {
Promise<Result> result =
responseFormat.equals(Accept.Format.BULK.queryParamString)
? bulkResult(q, nested, owner, index)
: Promise.promise(() -> {
Search queryResources = index.queryResources();
JsonNode json = queryResources.getResult();
String s = json.toString();
switch (Format.of(responseFormat)) {
case HTML:
return ok(query.render(s, q, agent, name, subject, id,
publisher, issued, medium, from, size,
queryResources.getTotal(), owner, t, sort, word));
case RSS:
String[] segments = request().uri().split("/");
String queryDetails =
Arrays.asList(segments).get(segments.length - 1)
.replace("search?", "").replaceAll("&?format=rss", "");
return ok(rss.render(s,
request().uri().replaceAll("&?format=rss", ""),
queryDetails)).as("application/rss+xml");
default:
return responseFormat.startsWith("json:")
? withCallback(
toSuggestions(json, responseFormat.split(":")[1]))
: responseFor(withQueryMetadata(json, index),
Accept.Format.JSON_LD.queryParamString);
}
});
return result;
}

private static void addResponseHeaders(String responseFormat) {
addCorsHeader();
if (responseFormat.equals(Accept.Format.BULK.queryParamString)) {
response().setHeader("Content-Disposition",
String.format(
"attachment; filename=\"lobid-resources-bulk-%s.jsonl\"",
System.currentTimeMillis()));
}
}

private static Promise<Result> bulkResult(final String q, final String nested,
final String owner, Search index) {
return Promise.promise(() -> {
Expand Down
7 changes: 5 additions & 2 deletions web/app/views/api.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,14 @@ <h2 id='content_types'>Inhaltstypen <small><a href='#content_types'><span class=

<p>Standardmäßig liefert dieser Dienst strukturierte API-Antworten (als JSON):</p>
<p><code>curl http://[email protected]("HT018472857")</code></p>
<p>Er unterstützt Content-Negotiation über den Accept-Header für JSON (application/json), JSON lines (application/x-jsonlines) oder HTML (text/html):</p>
<p>Er unterstützt Content-Negotiation für JSON (application/json), JSON lines (application/x-jsonlines), RSS (application/rss+xml) und HTML (text/html):</p>
<p><code>curl --header "Accept: application/json" http://[email protected]("kunst")</code></p>
<p><code>curl --header "Accept: application/rss+xml" http://[email protected]("kunst")</code></p>
<p><code>curl --header "Accept: application/x-jsonlines" http://[email protected]("kunst") > kunst.jsonl</code></p>
<p>Der Query-Parameter "format" (Werte: html,json,jsonl) kann verwendet werden, um den Accept-Header aufzuheben, z.B. zur Anzeige von JSON im Browser:</p>
<p>Der Query-Parameter "format" (Werte: html,json,jsonl,rss) kann verwendet werden, um den Accept-Header aufzuheben, z.B. zur Anzeige von JSON im Browser:</p>
<p><a href='@resources.routes.Application.resource("HT018472857", format="json")'>@resources.routes.Application.resource("HT018472857", format="json")</a></p>
<p>Oder zum Abonnieren eines RSS-Feeds:</p>
<p><a href='@resources.routes.Application.query("kunst", format="rss")'>@resources.routes.Application.query("kunst", format="rss")</a></p>
<p>Der Wert des Format-Parameters kann für Einzeltreffer auch in URLs als Dateiendung verwendet werden:</p>
<p><a href='@resources.routes.Application.resourceDotFormat("HT018472857", format="json")'>@resources.routes.Application.resourceDotFormat("HT018472857", format="json")</a></p>
<p>Für größere Anfragen kann die Antwort als gzip komprimiert werden:</p>
Expand Down
17 changes: 9 additions & 8 deletions web/app/views/main.scala.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@* Copyright 2014 Fabian Steeg, hbz. Licensed under the GPLv2 *@

@(q: String, title: String)(content: Html)
@(q: String, title: String, rss: Option[String] = None)(content: Html)

@import controllers.resources.Lobid

Expand All @@ -9,12 +9,13 @@
<html>
<head>
<title>@title</title>
<link rel="stylesheet" media="all" href="@controllers.routes.Assets.at("stylesheets/bootstrap.min.css")">
<link rel="stylesheet" media="all" href="@controllers.routes.Assets.at("stylesheets/octicons.css")">
<link rel="stylesheet" media="all" href="@controllers.routes.Assets.at("stylesheets/jquery-ui.min.css")">
<link rel="stylesheet" media="all" href='@controllers.routes.Assets.at("stylesheets/font-awesome.min.css")'>
<link rel="stylesheet" media="all" href="@controllers.routes.Assets.at("stylesheets/resources.css")">
<link rel="shortcut icon" type="image/png" href="@controllers.routes.Assets.at("images/favicon.png")">
<link rel="stylesheet" media="all" href="@controllers.routes.Assets.at("stylesheets/bootstrap.min.css")"/>
<link rel="stylesheet" media="all" href="@controllers.routes.Assets.at("stylesheets/octicons.css")"/>
<link rel="stylesheet" media="all" href="@controllers.routes.Assets.at("stylesheets/jquery-ui.min.css")"/>
<link rel="stylesheet" media="all" href='@controllers.routes.Assets.at("stylesheets/font-awesome.min.css")'/>
<link rel="stylesheet" media="all" href="@controllers.routes.Assets.at("stylesheets/resources.css")"/>
<link rel="shortcut icon" type="image/png" href="@controllers.routes.Assets.at("images/favicon.png")"/>
@for(rssPath <- rss){<link id='rss' rel="alternate" type="application/rss+xml" href="@controllers.resources.Application.CONFIG.getString("host")@rssPath"/>}
<script src="@controllers.routes.Assets.at("javascripts/jquery-1.10.2.min.js")"></script>
<script src="@controllers.routes.Assets.at("javascripts/jquery-ui.min.js")"></script>
<script src="@controllers.routes.Assets.at("javascripts/bootstrap.min.js")"></script>
Expand Down Expand Up @@ -63,7 +64,7 @@
</div><!--/.nav-collapse -->
</div><!--/.container-fluid -->
</div>
@if(request.uri.toString() != resources.routes.Application.advanced().toString()){@tags.search_form(q)}
@if(request.uri.toString() != resources.routes.Application.advanced().toString() && !title.contains("API")){@tags.search_form(q)}
@content
<div class="panel panel-default footer">
<div class="panel-body">
Expand Down
11 changes: 9 additions & 2 deletions web/app/views/query.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
if(q.contains("hasSuperordinate")) {select(("Band","Bände"))} else if (q.contains("containedIn")) {select(("Beitrag","Beiträge"))} else {""}
}

@main(q, "lobid-resources - Ergebnisliste") {
@main(q, "lobid-resources - Ergebnisliste", rss = Some(resources.routes.Application.query(q,agent,name,subject,id,publisher,issued,medium,from,size,owner,t,word=word,format="rss").toString)) {
@if(Seq(name, id, publisher).exists(!_.isEmpty)){ @* advanced search, not shown in facets *@
@tags.search_advanced("Suche aktualisieren", q, agent, name, subject, id, publisher, issued, sortParam)
<script>$("#search-simple").hide()</script>
Expand Down Expand Up @@ -82,7 +82,14 @@
<div class="panel panel-default">
<div class="panel-body" style="text-align:center">
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-1 text-left">
<a rel='alternate' type='application/rss+xml' href='@resources.routes.Application.query(q,agent,name,subject,id,publisher,issued,medium,from,size,owner,t,word=word,format="rss")'>
<span class="octicon octicon-rss" title='RSS-Feed für diese Suchanfrage'></span>
</a>
<a rel='alternate' type='application/ld+json' href='@resources.routes.Application.query(q,agent,name,subject,id,publisher,issued,medium,from,size,owner,t,word=word,format="json")'>
<img class='json-ld-icon' src='@routes.Assets.at("images/json-ld.png")' title='JSON-LD für diese Suchanfrage'>
</a>
</div>
<div class="col-md-10">
@allHits @if(!q.isEmpty && !labelRaw.isEmpty) {
@defining("http://lobid.org/resources/" + q.substring(q.lastIndexOf("/")+1, q.length()-1)) { lobidUrl =>
Expand Down
18 changes: 18 additions & 0 deletions web/app/views/rss.scala.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@* Copyright 2018 Fabian Steeg, hbz. Licensed under the GPLv2 *@
@(result: String, uri: String, query: String)@defining(play.api.libs.json.Json.parse(result).asOpt[Seq[play.api.libs.json.JsValue]].getOrElse(Seq()).zipWithIndex) { hits =><?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>lobid-resources @query</title>
<link>@controllers.resources.Application.CONFIG.getString("host")@uri</link>
<description>hbz union catalogue query @query</description>
@for((doc,i) <- hits; id = (doc\\"hbzId")(0).as[String]) {
<item>
<title>@((doc\"title").asOpt[String].getOrElse(""))</title>
<link>@controllers.resources.Application.CONFIG.getString("host")@resources.routes.Application.resource(id, null)</link>
<pubDate>@controllers.resources.Application.RSS_DATE_FORMAT.format(controllers.resources.Application.LOBID_DATE_FORMAT.parse((doc\"describedBy"\"dateCreated").asOpt[String].getOrElse("")))</pubDate>
<description><![CDATA[@tags.result_doc(doc)]]></description>
</item>
}
</channel>
</rss>
}
1 change: 1 addition & 0 deletions web/conf/resources.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
host="http://lobid.org"
hbz01.api="http://lobid.org/hbz01"
orgs.api="http://lobid.org/organisations/"

Expand Down
2 changes: 2 additions & 0 deletions web/test/tests/AcceptIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ public static Collection<Object[]> data() {
{ fakeRequest(GET, "/resources/search?q=*").header("Accept", "text/plain"), /*->*/ "application/json" },
// search, others formats as query param:
{ fakeRequest(GET, "/resources/search?q=*&format=html"), /*->*/ "text/html" },
{ fakeRequest(GET, "/resources/search?q=*&format=rss"), /*->*/ "application/rss+xml" },
// search, others formats via header:
{ fakeRequest(GET, "/resources/search?q=*").header("Accept", "application/json"), /*->*/ "application/json" },
{ fakeRequest(GET, "/resources/search?q=*").header("Accept", "text/html"), /*->*/ "text/html" },
{ fakeRequest(GET, "/resources/search?q=*").header("Accept", "application/rss+xml"), /*->*/ "application/rss+xml" },
// get, default format: JSON
{ fakeRequest(GET, "/resources/HT018907266"), /*->*/ "application/json" },
{ fakeRequest(GET, "/resources/HT018907266?format="), /*->*/ "application/json" },
Expand Down
3 changes: 3 additions & 0 deletions web/test/tests/AcceptUnitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ public static Collection<Object[]> data() {
// no header, just format parameter:
{ fakeRequest(), "html", /*->*/ "html" },
{ fakeRequest(), "json", /*->*/ "json" },
{ fakeRequest(), "json:title", /*->*/ "json:title" },
{ fakeRequest(), "rdf", /*->*/ "rdf" },
{ fakeRequest(), "ttl", /*->*/ "ttl" },
{ fakeRequest(), "nt", /*->*/ "nt" },
{ fakeRequest(), "rss", /*->*/ "rss" },
// supported content types, no format parameter given:
{ fakeRequest().header("Accept", "text/html"), null, /*->*/ "html" },
{ fakeRequest().header("Accept", "application/json"), null, /*->*/ "json" },
Expand All @@ -54,6 +56,7 @@ public static Collection<Object[]> data() {
{ fakeRequest().header("Accept", "application/xml"), null, /*->*/ "rdf" },
{ fakeRequest().header("Accept", "application/rdf+xml"), null, /*->*/ "rdf" },
{ fakeRequest().header("Accept", "text/xml"), null, /*->*/ "rdf" },
{ fakeRequest().header("Accept", "application/rss+xml"), null, /*->*/ "rss" },
// we pick the preferred content type:
{ fakeRequest().header("Accept", "text/html,application/json"), null, /*->*/"html" },
{ fakeRequest().header("Accept", "application/json,text/html"), null, /*->*/ "json" },
Expand Down
2 changes: 2 additions & 0 deletions web/test/tests/IntegrationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ private static void bulkRequestWith(String param) {
assertThat(result.contentType()).isEqualTo("application/x-jsonlines");
String text = Helpers.contentAsString(result);
assertThat(text.split("\\n").length).isGreaterThanOrEqualTo(10);
assertThat(result.header(Http.HeaderNames.CONTENT_DISPOSITION))
.isNotNull().isNotEmpty().contains("attachment; filename=");
});
}

Expand Down

0 comments on commit 3beb03f

Please sign in to comment.