Skip to content
This repository has been archived by the owner on Oct 24, 2022. It is now read-only.

Commit

Permalink
Merge pull request #356 from MITLibraries/opensearch
Browse files Browse the repository at this point in the history
Add OpenSearch support for item searching
  • Loading branch information
Richard Rodgers committed Sep 1, 2015
2 parents 5e2ebab + 01e41a5 commit b45b4c1
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 21 deletions.
132 changes: 114 additions & 18 deletions app/controllers/Search.scala
Original file line number Diff line number Diff line change
@@ -1,44 +1,52 @@
/**
* Copyright (c) 2015 MIT Libraries
* Licensed under: http://www.apache.org/licenses/LICENSE-2.0
*/
package controllers

import org.joda.time.DateTimeZone
import org.joda.time.format.ISODateTimeFormat

import play.api.mvc._
import play.api._
import play.api.Play.current
import play.api.libs.ws._
import play.api.libs.concurrent.Execution.Implicits._
import play.utils.UriEncoding

import models.HubUtils._
import models._
import models.{HubUtils, Item, Subscriber, Topic}

/**
* Search controller manages search pages in the UI and protocol
* requests from OpenSearch operations
* TODO - add accessUrls?
*/

object Search extends Controller {

val indexSvc = Play.configuration.getString("hub.index.url").get
val adminEmail = Play.configuration.getString("hub.admin.email").get
val iso8601 = ISODateTimeFormat.dateTimeNoMillis.withZone(DateTimeZone.UTC)

def index = Action { implicit request =>
Ok(views.html.search.index())
}

def results(q: String, target: String, page: Int, perpage: Int) = Action.async {
def results(q: String, target: String, page: Int, perpage: Int, format: String) = Action.async {
implicit request =>
val indexSvc = Play.configuration.getString("hub.index.url").get
val encQuery = UriEncoding.encodePathSegment(q, "UTF-8")
val offset = (page) * perpage
val elastic_url = indexSvc + target + "/_search?q=" + encQuery + "&from=" + offset + "&size=" + perpage
val req = if (indexSvc.contains("bonsai.io")) {
Logger.debug("use basic auth for WS elasticsearch call")
WS.url(elastic_url)
.withAuth(extractCredentials("username", indexSvc),
extractCredentials("password", indexSvc),
WSAuthScheme.BASIC)
} else {
Logger.debug("no auth for WS elasticsearch call")
WS.url(elastic_url)
}

val req = makeRequest(q, target, page, perpage)
req.get().map { response =>
val json = response.json
val total_results = (json \ "hits" \\ "total")(0).as[Long]
val hits = (json \ "hits" \\ "hits").head \\ "dbId" map(_.as[Int])
if (target == "item") {
val items = hits flatMap ( id => Item.findById(id) )
Ok(views.html.search.item_results(q, target, page, perpage, items, total_results))
if ("html" == format) {
Ok(views.html.search.item_results(q, target, page, perpage, items, total_results))
} else { // at this point, whatever else they want, they're gonna get Atom regardless
itemResultsAtom(q, page, perpage, items, total_results)
}
} else if (target == "topic") {
val topics = hits flatMap ( id => Topic.findById(id) )
val sub = Subscriber.findById(Application.currentSubscriberId)
Expand All @@ -48,4 +56,92 @@ object Search extends Controller {
}
}
}

def openSearchDescription(dtype: String) = Action { implicit request =>
def templateUrl(ftype: String) = {
routes.Search.index.absoluteURL + "/results?q={searchTerms}&page={startPage?}&perpage={count?}" +
"&target=" + dtype + "&format=" + ftype
}
Ok(
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>TopicHub Search</ShortName>
<LongName>{HubUtils.siteName.take(48)}</LongName>
<Description>Search for {dtype}s currently on the hub</Description>
<Contact>{adminEmail}</Contact>
<Image height="16" width="16" type="image/vnd.microsoft.icon">{routes.Assets.at("images/favicon.png").absoluteURL}</Image>
<Url type="text/html" template={templateUrl("html")}/>
<Url type="application/atom+xml" template={templateUrl("atom")}/>
</OpenSearchDescription>
).as("application/opensearchdescription+xml")
}

def itemResultsAtom(q: String, page: Int, perpage: Int, items: Seq[Item], total: Long)(implicit request: RequestHeader) = {
val lastPage = if (total.toInt % perpage > 0) (total.toInt / perpage) else (total.toInt / perpage) - 1
def schemeUrl(id: Int) = routes.Application.scheme(id).absoluteURL
def doiUrl(item: Item) = "http://doi.org/" + item.metadataValue("doi")
Ok(
<feed xmlns="http://www.w3.org/2005/Atom"
xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
<title>{HubUtils.siteName}</title>
<id>{routes.Application.index.absoluteURL}</id>
<updated>{iso8601.print(System.currentTimeMillis)}</updated>
<opensearch:itemsPerPage>{perpage}</opensearch:itemsPerPage>
<opensearch:totalResults>{total}</opensearch:totalResults>
<opensearch:Query role="request" searchTerms={q} startPage={page.toString}/>
<link rel="self" type="application/atom+xml"
href={routes.Search.results(q, "item", page, perpage, "atom").absoluteURL}/>
<link rel="first" type="application/atom+xml"
href={routes.Search.results(q, "item", 0, perpage, "atom").absoluteURL}/>
{ if (page > 0)
<link rel="previous" type="application/atom+xml"
href={routes.Search.results(q, "item", page - 1, perpage, "atom").absoluteURL}/>
}
{ if (page < lastPage)
<link rel="next" type="application/atom+xml"
href={routes.Search.results(q, "item", page + 1, perpage, "atom").absoluteURL}/>
}
<link rel="last" type="application/atom+xml"
href={routes.Search.results(q, "item", lastPage, perpage, "atom").absoluteURL}/>
<link rel="search" type="application/opensearchdescription+xml"
href={routes.Search.openSearchDescription("item").absoluteURL}/>
{ for (item <- items) yield
<entry>
<title>{item.metadataValue("title")}</title>
<id>{routes.ItemController.item(item.id).absoluteURL}</id>
{ if (item.hasMetadata("doi"))
<link rel="alternate" href={doiUrl(item)}/>
}
<updated>{iso8601.print(item.created.getTime())}</updated>
{ for (auth <- item.metadataValues("authors")) yield
<author>
<name>{auth}</name>
</author>
}
{ for (topic <- item.topics if topic.scheme.tag != "meta") yield
<category term={topic.tag} scheme={schemeUrl(topic.scheme_id)} label={topic.name}/>
}
<summary type="text">
{item.metadataValue("abstract")}
</summary>
</entry>
}
</feed>
).as("application/atom+xml")
}

private def makeRequest(query: String, target: String, page: Int, perpage: Int): WSRequest = {
val encQuery = UriEncoding.encodePathSegment(query, "UTF-8")
val offset = (page) * perpage
val elastic_url = indexSvc + target + "/_search?q=" + encQuery + "&from=" + offset + "&size=" + perpage
if (indexSvc.contains("bonsai.io")) {
Logger.debug("use basic auth for WS elasticsearch call")
WS.url(elastic_url)
.withAuth(extractCredentials("username", indexSvc),
extractCredentials("password", indexSvc),
WSAuthScheme.BASIC)
} else {
Logger.debug("no auth for WS elasticsearch call")
WS.url(elastic_url)
}
}
}
1 change: 1 addition & 0 deletions app/views/layout/main.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
<link rel="stylesheet" media="screen" href='@routes.Assets.at("lib/bootstrap/css/bootstrap.min.css")'>
<link rel="search" type="application/opensearchdescription+xml" title="Item search" href='@routes.Search.openSearchDescription("item").absoluteURL'>
<script src='@routes.Assets.at("lib/jquery/jquery.min.js")' type="text/javascript"></script>
<script src='@routes.Assets.at("lib/bootstrap/js/bootstrap.min.js")' type="text/javascript"></script>
<script src="@routes.Assets.at("javascripts/hello.js")" type="text/javascript"></script>
Expand Down
4 changes: 2 additions & 2 deletions app/views/static/about.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ <h2>About TopicHub</h2>
<ul>
<li>Content transfers from publishers to the hub and from the hub to subscribers utilize the <a href="http://swordapp.org">SWORD</a> (v1.3) protocol.</li>
<li>Metadata for content residing on the hub is exposed on an <a href="http://www.openarchives.org">Open Archives Initiative</a> (version 2.0) OAI-PMH data provider <a href="@routes.OAIPMH.provide?verb=Identify">endpoint</a></li>
<li>Content residing on the hub may be harvested using the <a href="http://www.niso.org/workrooms/resourcesync/">NISO/OAI ResourceSync</a> (draft) protocols.</li>
<li>Content residing on the hub may be harvested using the <a href="http://www.openarchives.org/rs/toc">NISO/OAI ResourceSync</a> protocol [forthcoming].</li>
<li>In application web pages, metadata is expressed using the <a href="http://schema.org">Schema.org</a> vocabulary.</li>
<li>Topics identified by the hub are syndicated as <a href="http://www.atomenabled.org">Atom</a> feeds containing items.</li>
<li>The hub supports <a href="http://www.opensearch.org">OpenSearch</a> formats for sharing and syndicating data as <a href="http://www.atomenabled.org">Atom</a> feeds containing items.</li>
</ul>
<p>TopicHub is built upon an open-source, Apache 2 licensed platform available on <a href="https://github.com/MITLibraries/topichub">GitHub</a>.</p>
}
3 changes: 2 additions & 1 deletion conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ GET /pick/:id/resolve controllers.Application.resolvePick(id: Int,

# Search pages
GET /search controllers.Search.index
GET /search/results controllers.Search.results(q: String, target: String ?="topic", page: Int ?=0, perpage: Int ?=25)
GET /search/results controllers.Search.results(q: String, target: String ?="topic", page: Int ?=0, perpage: Int ?=25, format: String ?="html")
GET /search/description/:dtype controllers.Search.openSearchDescription(dtype: String)

# OAI-PMH services
GET /oai controllers.OAIPMH.provide
Expand Down

0 comments on commit b45b4c1

Please sign in to comment.