diff --git a/adapter/adapter-api/src/main/scala/net/shrine/adapter/client/RemoteAdapterClient.scala b/adapter/adapter-api/src/main/scala/net/shrine/adapter/client/RemoteAdapterClient.scala index 5d956e957..db928c738 100644 --- a/adapter/adapter-api/src/main/scala/net/shrine/adapter/client/RemoteAdapterClient.scala +++ b/adapter/adapter-api/src/main/scala/net/shrine/adapter/client/RemoteAdapterClient.scala @@ -1,131 +1,136 @@ package net.shrine.adapter.client import java.net.{SocketTimeoutException, URL} import org.xml.sax.SAXParseException import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.blocking import scala.concurrent.duration.Duration import scala.concurrent.duration.DurationInt import scala.util.control.NonFatal import scala.xml.{NodeSeq, XML} import com.sun.jersey.api.client.ClientHandlerException import net.shrine.client.{HttpResponse, Poster, TimeoutException} import net.shrine.problem.{AbstractProblem, ProblemNotYetEncoded, ProblemSources} import net.shrine.protocol.BroadcastMessage import net.shrine.protocol.ErrorResponse import net.shrine.protocol.NodeId import net.shrine.protocol.Result import scala.util.{Failure, Success, Try} import net.shrine.protocol.ResultOutputType /** * @author clint * @since Nov 15, 2013 * * */ final class RemoteAdapterClient private (nodeId:NodeId,val poster: Poster, val breakdownTypes: Set[ResultOutputType]) extends AdapterClient { import RemoteAdapterClient._ //NB: Overriding apply in the companion object screws up case-class code generation for some reason, so //we add the would-have-been-generated methods here override def toString = s"RemoteAdapterClient($poster)" override def hashCode: Int = 31 * (if(poster == null) 1 else poster.hashCode) override def equals(other: Any): Boolean = other match { case that: RemoteAdapterClient if that != null => poster == that.poster case _ => false } def url:Option[URL] = Some(new URL(poster.url)) //TODO: Revisit this import scala.concurrent.ExecutionContext.Implicits.global override def query(request: BroadcastMessage): Future[Result] = { val requestXml = request.toXml Future { blocking { val response: HttpResponse = poster.post(requestXml.toString()) interpretResponse(response) } }.recover { case e if isTimeout(e) => throw new TimeoutException(s"Invoking adapter at ${poster.url} timed out", e) } } def interpretResponse(response:HttpResponse):Result = { if(response.statusCode <= 400){ val responseXml = response.body import scala.concurrent.duration._ //Should we know the NodeID here? It would let us make a better error response. Try(XML.loadString(responseXml)).flatMap(Result.fromXml(breakdownTypes)) match { case Success(result) => result case Failure(x) => { val errorResponse = x match { case sx: SAXParseException => ErrorResponse(CouldNotParseXmlFromAdapter(poster.url,response.statusCode,responseXml,sx)) case _ => ErrorResponse(ProblemNotYetEncoded(s"Couldn't understand response from adapter at '${poster.url}': $responseXml", x)) } Result(nodeId, 0.milliseconds, errorResponse) } } } else { Result(nodeId,0.milliseconds,ErrorResponse(HttpErrorCodeFromAdapter(poster.url,response.statusCode,response.body))) } } } object RemoteAdapterClient { def apply(nodeId:NodeId,poster: Poster, breakdownTypes: Set[ResultOutputType]): RemoteAdapterClient = { //NB: Replicate URL-munging that used to be performed by JerseyAdapterClient val posterToUse = { if(poster.url.endsWith("requests")) { poster } else { poster.mapUrl(_ + "/requests") } } new RemoteAdapterClient(nodeId,posterToUse, breakdownTypes) } def isTimeout(e: Throwable): Boolean = e match { case e: SocketTimeoutException => true case e: ClientHandlerException => { val cause = e.getCause cause != null && cause.isInstanceOf[SocketTimeoutException] } case _ => false } } case class HttpErrorCodeFromAdapter(url:String,statusCode:Int,responseBody:String) extends AbstractProblem(ProblemSources.Adapter) { override def summary: String = "Hub received a fatal error response" override def description: String = s"Hub received error code $statusCode from the adapter at $url" - override def detailsXml:NodeSeq =
{s"Http response body was $responseBody"}
+ override def detailsXml:NodeSeq = { + if (responseBody.isEmpty) +
"Error response contained no body"
+ else +
{s"Http response body was $responseBody"}
+ } } case class CouldNotParseXmlFromAdapter(url:String,statusCode:Int,responseBody:String,saxx: SAXParseException) extends AbstractProblem(ProblemSources.Adapter) { override def throwable = Some(saxx) override def summary: String = s"Hub could not parse response from adapter" override def description: String = s"Hub could not parse xml from $url due to ${saxx.toString}" override def detailsXml:NodeSeq =
{s"Http response code was $statusCode and the body was $responseBody"} {throwableDetail}
} \ No newline at end of file diff --git a/apps/dashboard-app/src/main/js/src/app/diagnostic/views/problems.controller.js b/apps/dashboard-app/src/main/js/src/app/diagnostic/views/problems.controller.js index 551b9c2ff..af70ef39a 100644 --- a/apps/dashboard-app/src/main/js/src/app/diagnostic/views/problems.controller.js +++ b/apps/dashboard-app/src/main/js/src/app/diagnostic/views/problems.controller.js @@ -1,232 +1,234 @@ (function () { 'use strict'; // -- register controller with angular -- // angular.module('shrine-tools', ['ui.bootstrap', 'ui.bootstrap.datepicker']) .controller('ProblemsController', ProblemsController) .directive('myPagination', function () { return { restrict: 'A', replace: true, templateUrl: 'src/app/diagnostic/templates/my-pagination-template.html' } }) .directive('myPages', function() { function rangeGen(max) { var result = []; for (var i = 1; i < max - 1; i++) { result[i] = i + 1; } return result; } function checkPage(value, activePage, maxPage, minPage) { if (maxPage == minPage) { return false; } else if (maxPage - minPage <= 5) { return isFinite(value) && value <= maxPage && value >= minPage; } else if (value == maxPage || value == minPage || value == activePage) { return true; } else if (value == '‹') { return activePage != minPage; } else if (value == '›') { return activePage != maxPage; } else if (value == "..") { return activePage > 5; } else if (value == "...") { return maxPage - activePage > 4; } else if (value < activePage) { return activePage <= 5 || activePage - value <= 2; } else if (value > activePage) { return maxPage - activePage <= 4 || value - activePage <= 2; } } return { restrict: 'E', templateUrl: 'src/app/diagnostic/templates/paginator-template.html', scope: { maxPage: '=', minPage: '=', handleButton: '=', activePage: '=' }, link: function(scope) { scope.rangeGen = rangeGen; scope.checkPage = checkPage; } } }); ProblemsController.$inject = ['$app', '$log', '$sce', '$scope']; function ProblemsController ($app, $log, $sce, $scope) { var vm = this; init(); /** * */ function init () { vm.isOpen = false; vm.date = new Date(); vm.dateOptions = {max: new Date()}; vm.pageSizes = [5, 10, 20]; vm.format = "dd/MM/yyyy"; vm.url = "https://open.med.harvard.edu/wiki/display/SHRINE/"; vm.submitDate = submitDate; vm.newPage = newPage; vm.floor = Math.floor; vm.handleButton = handleButton; vm.open = function() { vm.isOpen = !vm.isOpen }; vm.checkDate = function() { return vm.date != undefined }; vm.showP = function(target) { return vm.probsSize > target}; vm.pageSizeCheck = function(n) { return n < vm.probsSize }; vm.parseDetails = function(details) { return $sce.trustAsHtml(parseDetails(details)) }; vm.numCheck = function(any) { return isFinite(any)? (any - 1) * vm.probsN: vm.probsOffset }; vm.changePage = function() { vm.newPage(vm.probsOffset, vm.pageSize > 20? 20: vm.pageSize < 0? 0: vm.pageSize) }; vm.formatDate = function(dateObject) { var split = dateObject.toString().split(" "); return split[1] + " " + split[2] + ", " + split[3]; }; //todo: Get rid of this and figure out something less hacky vm.formatCodec = function(word) { var index = word.lastIndexOf('.'); var arr = word.trim().split(""); arr[index] = '\n'; return arr.join(""); }; $app.model.getProblems().then(setProblems) } function handleButton(value) { var page = function(offset) { newPage(offset, vm.probsN) }; switch(value) { case '..': break; case '‹': page(vm.probsOffset - vm.probsN); break; case '›': page(vm.probsOffset + vm.probsN); break; case '...': break; default: page((value - 1) * vm.probsN); } } function floorMod(num1, num2) { if (!(num1 && num2)) { // can't mod without real numbers return num1; } else { var n1 = Math.floor(num1); var n2 = Math.floor(num2); return n1 - (n1 % n2); } } function submitDate() { var epoch = vm.date.getTime() + 86400000; // + a day vm.showDateError = false; newPage(vm.probsOffset, vm.probsN, epoch); } function newPage(offset, n, epoch) { if (!(n && isFinite(n))) { n = 20; } if (!(offset && isFinite(offset))) { offset = 0; } var clamp = function(num1) { if (!vm.probsSize) { // Can't clamp, since probsSize isn't set yet. return num1; } else { return Math.max(0, Math.min(vm.probsSize - 1, num1)); } }; var num = floorMod(clamp(offset), vm.probsN); $app.model.getProblems(num, n, epoch) .then(setProblems) } function setProblems(probs) { vm.problems = probs.problems; vm.probsSize = probs.size; vm.probsOffset = probs.offset; vm.probsN = probs.n; vm.pageSize = vm.probsN; } function parseDetails(detailsObject) { var detailsTag = '

details

'; var detailsField = detailsObject['details']; - if (detailsField === '') { + if (detailsField === '' && detailsObject == '') { return '

No details associated with this problem

' + } else if (detailsField === '') { + return detailsTag + '
'+sanitizeString(JSON.stringify(detailsObject))+'
' } else if (typeof(detailsField) === 'string') { return detailsTag + '

'+sanitizeString(detailsField)+'

'; } else if (typeof(detailsField) === 'object' && detailsField.hasOwnProperty('exception')) { return detailsTag + parseException(detailsField['exception']); } else { return detailsTag + '
'+sanitizeString(JSON.stringify(detailsField))+'
' } } function parseException(exceptionObject) { var exceptionTag = '

exception

'; var nameTag = '
'+sanitizeString(exceptionObject['name'])+'
'; var messageTag = '

'+sanitizeString(exceptionObject['message'])+'

'; var stackTrace = exceptionObject['stacktrace']; - return exceptionTag + nameTag + messageTag + (stackTrace == null? '': parseStackTrace(stackTrace)); + return exceptionTag + nameTag + messageTag + (stackTrace === undefined? '': parseStackTrace(stackTrace)); } function parseStackTrace(stackTraceObject) { if (stackTraceObject.hasOwnProperty('exception')) { return '

'+sanitizeString(stackTraceObject['line'])+'

' + parseException(stackTraceObject['exception']); } else if (stackTraceObject.hasOwnProperty('line')) { return '

stack trace

' + parseLines(stackTraceObject['line']); } else { return JSON.stringify(stackTraceObject); } } function parseLines(lineArray) { var result = '

'; for (var i =0; i < lineArray.length; i++) { result += sanitizeString(lineArray[i]) + '
'; } result += '

'; return result; } function sanitizeString(str) { var chars = str.split(''); var escapes = { '<': '<', '>': '>', '&': '&', '"': '"', "'": ''', ' ': ' ', '!': '!', '@': '@', '$': '$', '%': '%', '(': '(', ')': ')', '=': '=', '+': '+', '{': '{', '}': '}', '[': '[', ']': ']' }; for (var i = 0; i < chars.length; i++) { var c = chars[i]; if (escapes.hasOwnProperty(c)) { chars[i] = escapes[c] } } return chars.join(''); } } })();