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

Commit

Permalink
Adds SWORD endpoint checking on destination page
Browse files Browse the repository at this point in the history
Closes #344
  • Loading branch information
Richard Rodgers committed Aug 24, 2015
1 parent 388261b commit 449300c
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 1 deletion.
10 changes: 9 additions & 1 deletion app/controllers/Application.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import models._
import services.contentModelJson._
import services.publisherModelJson._
import services.subscriberModelJson._
import services.Emailer
import services.{Emailer, SwordClient}
import workers.Cataloger

case class HubContext(user: Option[User])
Expand Down Expand Up @@ -1039,6 +1039,14 @@ object Application extends Controller with Security {
).getOrElse(NotFound(views.html.static.trouble("No such subscriber destination: " + id)))
}

def checkChannel(id: Int) = isAuthenticated { identity => implicit request =>
Channel.findById(id).map( chan =>
subscriberMember(identity, chan.subscriber, { val msg = SwordClient.checkEndpoint(chan).split('|');
Redirect(routes.Application.channel(chan.id)).flashing(
msg(0) -> msg(1)) })
).getOrElse(NotFound(views.html.static.trouble("No such subscriber destination: " + id)))
}

def newChannel(sid: Int) = isAuthenticated { identity => implicit request =>
subscriberMember(identity, Subscriber.findById(sid).get, Ok(views.html.channel.create(sid, channelForm)))
}
Expand Down
115 changes: 115 additions & 0 deletions app/services/sword.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Copyright (c) 2015 MIT Libraries
* Licensed under: http://www.apache.org/licenses/LICENSE-2.0
*/
package services

import java.io.ByteArrayInputStream

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent._
import scala.concurrent.duration._
import scala.io.Source
import scala.xml.pull._
import scala.util.{Success, Failure}

import play.api._
import play.api.libs.ws._
import play.api.Play.current
import models.Channel

/** Services for SWORD protocol operations
* Currently a few SWORD v1.3 client actions
*
* @author richardrodgers
*/

object SwordClient {

/**
* Check subscriber SWORD channel info by composing a request for the service
* document from the server, and if successful attempting to match the
* configured endpoint, package format, etc. This is implemented as a
* blocking WS call (not usually recommended) to test response time from
* the server. Returns status string.
*/
def checkEndpoint(channel: Channel): String = {

val endpoint = channel.channelUrl

def readSwordResponse(response: WSResponse): String = {
response.status match {
case 200 =>
Logger.info("Successful retrieval of service document")
validateServiceDocument(new XMLEventReader(Source.fromInputStream(
new ByteArrayInputStream(response.body.getBytes))))
case 401 =>
Logger.info("Authorization failure")
s"warning|The request failed authorization on the server: check credentials"
case _ =>
Logger.warn("The SWORD server did not accept the request. Response was " + response.toString)
s"danger|The SWORD server did not accept the request. Response: ${response.toString}"
}
}

def invalid(field: Option[String], value: String): Boolean = {
field.isEmpty || field.get != value
}

def validateServiceDocument(xmlReader: XMLEventReader): String = {
// we want to validate a few things: server is ver 1.3 of SWORD and
// collection matches endpoint and accepts application/zip, DSpaceMetsSip packages
var readingVer = false
var version: Option[String] = None
var readingCollection = false
var readingName = false
var collectionName: Option[String] = None
var readingAccept = false
var accept: Option[String] = None
var readingPackage = false
var acceptedPackages: List[String] = List()
while (xmlReader.hasNext) {
xmlReader.next match {
case EvElemStart("sword","version",_,_) => readingVer = true
case EvElemStart("app","collection",attrs,_) if (attrs("href").text == endpoint) => readingCollection = true
case EvElemStart("atom","title",_,_) if readingCollection => readingName = true
case EvElemStart("app","accept",_,_) if readingCollection => readingAccept = true
case EvElemStart("sword","acceptPackaging",_,_) if readingCollection => readingPackage = true
case EvText(text) if readingVer => version = Some(text); readingVer = false
case EvText(text) if readingName => collectionName = Some(text); readingName = false
case EvText(text) if readingAccept => accept = Some(text); readingAccept = false
case EvText(text) if readingPackage => acceptedPackages = text :: acceptedPackages; readingPackage = false
case EvElemEnd("app", "collection") if readingCollection => readingCollection = false
case _ =>
}
}
// return first validation failure encountered - no real need to pile it on
if (invalid(version, "1.3"))
s"danger|Incompatible server SWORD versions: need 1.3 but server supports ${version.get}"
else if (collectionName.isEmpty)
s"warning|No SWORD collection exposed at endpoint: '$endpoint' on server"
else if (invalid(accept, "application/zip"))
s"danger|Incompatible collection content type: need 'application/zip' but server requires ${accept.get}"
else if (! acceptedPackages.contains("http://purl.org/net/sword-types/METSDSpaceSIP"))
s"danger|SWORD collection on server does not support required packaging format: 'http://purl.org/net/sword-types/METSDSpaceSIP'"
else
s"success|Destination matches reachable SWORD collection: '${collectionName.get}' on server"
}

// construct a serviceDocument URL from channelUrl (using the DSpace URL convention)
val svcUrl = endpoint.substring(0, endpoint.indexOf("sword") + 5) + "/servicedocument"
val until = Duration(5000, "millis")
Logger.info("About to call: " + svcUrl)
try {
val resp = Await.ready(WS.url(svcUrl).withAuth(channel.userId, channel.password, WSAuthScheme.BASIC).get(),
until)
resp.value map {
case Success(response) => readSwordResponse(response)
case Failure(t) => s"danger|Request for service document failed with message: '${t.getMessage}'"
} getOrElse("danger|Unknown Error")
} catch {
case e: TimeoutException => s"warning|Timed out: no server response in ${until.toSeconds} seconds"
case e: Throwable => s"danger|Unexpected exception: '${e.getMessage}'"
}
}
}
3 changes: 3 additions & 0 deletions app/views/channel/show.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ <h2>@channel.description</h2>
<p>URL: @channel.channelUrl</p>
<p>Created: @HubUtils.fmtDate(channel.created)</p>
<p>@HubUtils.pluralize(channel.transfers, "transfer") - most recent: @HubUtils.fmtDate(channel.updated)</p>
@if(channel.protocol == "sword") {
<a rel="tooltip" title="check destination" href="@routes.Application.checkChannel(channel.id)" class="btn btn-primary btn-large pull-right">Check Destination &raquo;</a>
}
</div>
@*<h3>Subscriptions</h3>
<ul>
Expand Down
1 change: 1 addition & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ GET /interests/remove/:iid controllers.Application.removeSubscriberInterest

# Channel Pages
GET /channel/:id controllers.Application.channel(id: Int)
GET /channel/:id/check controllers.Application.checkChannel(id: Int)
GET /channels/:sid/create controllers.Application.newChannel(sid: Int)
POST /subscriber/:sid/channels controllers.Application.createChannel(sid: Int)

Expand Down

0 comments on commit 449300c

Please sign in to comment.