Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add representation of search results as Atom feeds #1275

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions rest/src/main/groovy/whelk/rest/api/Crud.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class Crud extends HttpServlet {
ConverterUtils converterUtils

SiteSearch siteSearch
SearchFeed searchFeed

Map<String, Tuple2<Document, String>> cachedFetches = [:]

Expand Down Expand Up @@ -92,6 +93,7 @@ class Crud extends HttpServlet {
targetVocabMapper = new TargetVocabMapper(jsonld, contextDoc.data)
}

searchFeed = new SearchFeed(jsonld, whelk.locales)
}

protected void cacheFetchedResource(String resourceUri) {
Expand Down Expand Up @@ -264,12 +266,24 @@ class Crud extends HttpServlet {
private Object getNegotiatedDataBody(CrudGetRequest request, Object contextData, Map data, String uri) {
if (!(request.getContentType() in [MimeTypes.JSON, MimeTypes.JSONLD])) {
data[JsonLd.CONTEXT_KEY] = contextData
if ((request.getContentType() in [MimeTypes.ATOM])) {
var feedId = getFeedId(data, uri)
return searchFeed.represent(feedId, data)
}
return converterUtils.convert(data, uri, request.getContentType())
Comment on lines 267 to 273
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!(request.getContentType() in [MimeTypes.JSON, MimeTypes.JSONLD])) {
data[JsonLd.CONTEXT_KEY] = contextData
if ((request.getContentType() in [MimeTypes.ATOM])) {
var feedId = getFeedId(data, uri)
return searchFeed.represent(feedId, data)
}
return converterUtils.convert(data, uri, request.getContentType())
if ((request.getContentType() in [MimeTypes.ATOM])) {
data[JsonLd.CONTEXT_KEY] = contextData
var feedId = getFeedId(data, uri)
return searchFeed.represent(feedId, data)
}
else if (!(request.getContentType() in [MimeTypes.JSON, MimeTypes.JSONLD])) {
data[JsonLd.CONTEXT_KEY] = contextData
return converterUtils.convert(data, uri, request.getContentType())

} else {
return data
}
}

String getFeedId(Object data, String uri) {
var searchPath = uri
if (data instanceof Map) {
searchPath = (String) data[JsonLd.ID_KEY]
}
return "${whelk.applicationId}${searchPath.substring(1)}"
}

private static Map frameRecord(Document document) {
return JsonLd.frame(document.getCompleteId(), document.data)
}
Expand Down
2 changes: 1 addition & 1 deletion rest/src/main/groovy/whelk/rest/api/CrudGetRequest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class CrudGetRequest {
private CrudGetRequest(HttpServletRequest request) {
this.request = request
parsePath(getPath())
contentType = getBestContentType(getAcceptHeader(request), dataLeaf)
contentType = getBestContentType(getAcceptHeader(request), dataLeaf ?: resourceId)
lens = parseLens(request)
profile = parseProfile(request)
}
Expand Down
6 changes: 4 additions & 2 deletions rest/src/main/groovy/whelk/rest/api/CrudUtils.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class CrudUtils {
final static MediaType TRIG = MediaType.parse(MimeTypes.TRIG)
final static MediaType RDFXML = MediaType.parse(MimeTypes.RDF)
final static MediaType N3 = MediaType.parse(MimeTypes.N3)
final static MediaType ATOM = MediaType.parse(MimeTypes.ATOM)

static final Map ALLOWED_MEDIA_TYPES_BY_EXT = [
'': [JSONLD, JSON],
Expand All @@ -29,7 +30,8 @@ class CrudUtils {
'ttl': [TURTLE],
'rdf': [RDFXML],
'xml': [RDFXML],
'n3': [N3]
'n3': [N3],
'atom': [ATOM],
]

static Map EXTENSION_BY_MEDIA_TYPE = [:]
Expand All @@ -43,7 +45,7 @@ class CrudUtils {
}
}

static final List ALLOWED_MEDIA_TYPES = [JSON, JSONLD, TRIG, TURTLE, RDFXML, N3]
static final List ALLOWED_MEDIA_TYPES = [JSON, JSONLD, TRIG, TURTLE, RDFXML, N3, ATOM]

static String getBestContentType(String acceptHeader, String resourcePath) {
def desired = parseAcceptHeader(acceptHeader)
Expand Down
1 change: 1 addition & 0 deletions rest/src/main/groovy/whelk/rest/api/MimeTypes.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ class MimeTypes {
static final RDF = "application/rdf+xml"
static final JSON = "application/json"
static final N3 = "text/n3"
static final ATOM = "application/atom+xml"
}
180 changes: 180 additions & 0 deletions rest/src/main/groovy/whelk/rest/api/SearchFeed.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package whelk.rest.api

import groovy.transform.CompileStatic
import static groovy.transform.TypeCheckingMode.SKIP

import groovy.xml.MarkupBuilder
import groovy.xml.StreamingMarkupBuilder

import whelk.JsonLd
import static whelk.JsonLd.ID_KEY
import static whelk.JsonLd.TYPE_KEY
import static whelk.JsonLd.REVERSE_KEY
import static whelk.JsonLd.asList

@CompileStatic
class SearchFeed {

static final String BULLET_SEP = " • "

JsonLd jsonld
List<String> locales

Set<String> skipKeys = [ID_KEY, REVERSE_KEY, 'meta', 'reverseLinks'] as Set
Set<String> skipDetails = skipKeys + ([TYPE_KEY, 'commentByLang'] as Set)

SearchFeed(JsonLd jsonld, List<String> locales) {
this.jsonld = jsonld
this.locales = locales
}

@CompileStatic(SKIP)
String represent(String feedId, Object searchResults) {
var lastMod = searchResults.items?[0]?.meta?.modified
var feedTitle = buildTitle(searchResults)
return new StreamingMarkupBuilder().bind { mb ->
feed(xmlns: 'http://www.w3.org/2005/Atom') {
title(feedTitle)
id(feedId)
link(rel: 'self', href: searchResults[ID_KEY])
for (rel in ['next', 'prev', 'first', 'last']) {
def ref = searchResults[rel]
if (ref) {
link(rel: rel, href: ref[ID_KEY])
}
}
if (lastMod) updated(lastMod)
for (item in searchResults.items) {
entry {
id(item[ID_KEY])
link(rel: 'alternate', type: 'text/html', href: item[ID_KEY])
updated(item.meta.modified)
title(toChipString(item))
summary(type: 'xhtml') {
toEntryCard(mb, item)
}
content(href: item[ID_KEY])
}
}
}
}.toString()
}

@CompileStatic(SKIP)
String buildTitle(Map searchResults) {
var title = getByLang((Map) searchResults['titleByLang'])
def params = searchResults.search?.mapping?.findResults {
if (it.value !instanceof Boolean) {
return toValueString(it.value ?: it.object, skipDetails)
}
}
if (params) {
return title + ': ' + params.join(' & ')
} else {
return title
}
}

@CompileStatic(SKIP)
void toEntryCard(mb, Map item) {
mb.div(xmlns: 'http://www.w3.org/1999/xhtml') {
asList(item.meta?.hasChangeNote).each { note ->
p { b(toChipString(note)) }
}
for (kv in item) {
div {
if (kv.key !in skipKeys) {
var label = getLabelFor(kv.key)
var values = getValues(kv.value, kv.key)
if (label && values) {
span(label + ": ")
span {
values.eachWithIndex { v, i ->
if (i > 0) {
span(", " + v)
} else {
span(v)
}
}
}
}
}
}
}
}
}

String toChipString(Object item) {
if (item instanceof Map) {
def chip = jsonld.toChip(item)
return toValueString(chip)
} else if (item == null) {
return ""
} else {
return item.toString()
}
}

String toValueString( Object o, Set skipKeys=skipKeys) {
var sb = new StringBuilder()
buildValueString(sb, o, skipKeys)
return sb.toString()
}

void buildValueString(StringBuilder sb, Object o, Set skipKeys=skipKeys) {
if (o instanceof List) {
for (v in o) buildValueString(sb, v, skipKeys)
} else if (o instanceof Map) {
for (kv in o) {
if (kv.key !in skipKeys) {
buildValueString(sb, getValues(kv.value, (String) kv.key), skipKeys)
}
}
} else {
if (sb.size() > 0) sb.append(BULLET_SEP)
sb.append(o.toString())
}
}

List<String> getValues(Object o, String viaKey) {
if (viaKey == TYPE_KEY || jsonld.isVocabTerm(viaKey)) {
return asList(o).collect { getLabelFor((String) it) }
} else if (jsonld.isLangContainer(jsonld.context[viaKey])) {
return (List<String>) asList(o).findResults { getByLang((Map) it) }
} else {
return (List<String>) asList(o).findResults { toChipString(it) ?: null }
}
}

String getLabelFor(String key) {
String lookup = key == TYPE_KEY ? 'rdf:type' : key
def term = jsonld.vocabIndex[lookup]
if (term instanceof Map) {
def byLang = term.get('labelByLang')
if (byLang instanceof Map) {
String s = getByLang(byLang)
if (s) {
return s[0].toUpperCase() + s.substring(1)
}
}
}
return key
}

String getByLang(Map byLang) {
for (lang in locales) {
if (lang in byLang) {
def o = byLang[lang]
if (o instanceof String) {
return o
} else if (o instanceof List && o.size() > 0) {
return o.get(0).toString()
}
}
}
for (value in byLang.values()) {
return value
}
return null
}
}
6 changes: 6 additions & 0 deletions rest/src/main/groovy/whelk/rest/api/SearchUtils.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,12 @@ class SearchUtils {
termKey = stripPrefix(termKey, ESQuery.AND_PREFIX)
termKey = stripPrefix(termKey, ESQuery.OR_PREFIX)

if (termKey.startsWith(ESQuery.EXISTS_PREFIX)) {
termKey = stripPrefix(termKey, ESQuery.EXISTS_PREFIX)
valueProp = 'value'
value = ESQuery.parseBoolean(termKey, val)
}

result << [
'variable': param,
'predicate': lookup.chip(termKey),
Expand Down
10 changes: 8 additions & 2 deletions rest/src/main/groovy/whelk/rest/api/SiteSearch.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,18 @@ class SiteSearch {
if (!queryParameters['_statsrepr'] && searchSettings['statsindex']) {
queryParameters.put('_statsrepr', [mapper.writeValueAsString(searchSettings['statsindex'])] as String[])
}
return toDataIndexDescription(appsIndex["${activeSite}data" as String], queryParameters)
var appDesc = appsIndex["${activeSite}data" as String]
return toDataIndexDescription(appDesc, queryParameters)
} else {
if (!queryParameters['_statsrepr'] && searchSettings['statsfind']) {
queryParameters.put('_statsrepr', [mapper.writeValueAsString(searchSettings['statsfind'])] as String[])
}
return search.doSearch(queryParameters)
var results = search.doSearch(queryParameters)

var appDesc = appsIndex["${activeSite}find" as String]
results['titleByLang'] = appDesc['titleByLang']

return results
}
}

Expand Down