Skip to content

Commit

Permalink
Add representation of search results as Atom feeds
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasl committed Jun 27, 2023
1 parent 9635c82 commit d8ad065
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 5 deletions.
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())
} 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"
}
177 changes: 177 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,177 @@
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 {

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)

String feedTitle

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 {
var o = toValueString(it.object, skipDetails)
return o
}
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 {
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("")
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
}
}
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

0 comments on commit d8ad065

Please sign in to comment.