diff --git a/.gitignore b/.gitignore index 744436d78..98128232a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,25 @@ *.class *.log +*.log.* # mvn target/ # emacs cruft *.*~ # javascript artifacts **/src/main/js/build/ **/js/node/ **/js/node_modules/ **/resources/client/config/ **/resources/client/happy/happy-whoami.xml **/resources/client/index.html **/resources/client/src/ # idea .idea/ *.iml # osx cruft .DS_Store 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 7fc7eaf48..5d956e957 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,129 +1,131 @@ package net.shrine.adapter.client -import java.net.{URL, SocketTimeoutException} -import net.shrine.problem.{ProblemNotYetEncoded, ProblemSources, AbstractProblem} +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, TimeoutException, Poster} +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"}
} 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/adapter/adapter-api/src/main/scala/net/shrine/adapter/service/AdapterConfigSource.scala b/adapter/adapter-api/src/main/scala/net/shrine/adapter/service/AdapterConfigSource.scala index 8bcd746f2..a2fd8aac4 100644 --- a/adapter/adapter-api/src/main/scala/net/shrine/adapter/service/AdapterConfigSource.scala +++ b/adapter/adapter-api/src/main/scala/net/shrine/adapter/service/AdapterConfigSource.scala @@ -1,36 +1,14 @@ package net.shrine.adapter.service -import com.typesafe.config.{Config, ConfigFactory} -import net.shrine.config.AtomicConfigSource +import net.shrine.config.ConfigSource /** * Source of config for the Adapter. Put new config fields here, not in AdapterConfig, to enable Config-based apply() methods. * * @author david * @since 8/25/15 */ -object AdapterConfigSource { +object AdapterConfigSource extends ConfigSource { - val atomicConfig = new AtomicConfigSource(ConfigFactory.load("shrine")) - - def config:Config = { - atomicConfig.config - } - - def configForBlock[T](key:String,value:AnyRef,origin:String)(block: => T):T = { - atomicConfig.configForBlock(key,value,origin)(block) - } - - //todo move this to common code somewhere - def objectForName[T](objectName:String):T = { - - import scala.reflect.runtime.universe - val runtimeMirror = universe.runtimeMirror(getClass.getClassLoader) - val module = runtimeMirror.staticModule(objectName) - - val reflectedObj = runtimeMirror.reflectModule(module) - val obj = reflectedObj.instance - - obj.asInstanceOf[T] - } + override val configName = "shrine" } \ No newline at end of file diff --git a/adapter/adapter-service/src/main/scala/net/shrine/adapter/AbstractReadQueryResultAdapter.scala b/adapter/adapter-service/src/main/scala/net/shrine/adapter/AbstractReadQueryResultAdapter.scala index 3c8eb8dc5..6e948f3e2 100644 --- a/adapter/adapter-service/src/main/scala/net/shrine/adapter/AbstractReadQueryResultAdapter.scala +++ b/adapter/adapter-service/src/main/scala/net/shrine/adapter/AbstractReadQueryResultAdapter.scala @@ -1,296 +1,298 @@ package net.shrine.adapter import java.util.concurrent.Executors import java.util.concurrent.TimeUnit + import net.shrine.adapter.audit.AdapterAuditDb -import net.shrine.problem.{AbstractProblem, ProblemSources} import scala.Option.option2Iterable import scala.concurrent.Await import scala.concurrent.ExecutionContext import scala.concurrent.Future import scala.util.Failure import scala.util.Success import scala.util.Try import scala.xml.NodeSeq import net.shrine.adapter.Obfuscator.obfuscateResults import net.shrine.adapter.dao.AdapterDao import net.shrine.adapter.dao.model.Breakdown import net.shrine.adapter.dao.model.ShrineQueryResult -import net.shrine.protocol.{HiveCredentials, AuthenticationInfo, BroadcastMessage, ErrorResponse, HasQueryResults, QueryResult, ReadResultRequest, ReadResultResponse, ResultOutputType, ShrineRequest, ShrineResponse, BaseShrineRequest} +import net.shrine.protocol.{AuthenticationInfo, BaseShrineRequest, BroadcastMessage, ErrorResponse, HasQueryResults, HiveCredentials, QueryResult, ReadResultRequest, ReadResultResponse, ResultOutputType, ShrineRequest, ShrineResponse} import net.shrine.protocol.query.QueryDefinition import net.shrine.util.StackTrace import net.shrine.util.Tries.sequence + import scala.concurrent.duration.Duration import net.shrine.client.Poster +import net.shrine.problem.{AbstractProblem, ProblemSources} /** * @author clint * @since Nov 2, 2012 * */ object AbstractReadQueryResultAdapter { private final case class RawResponseAttempts(countResponseAttempt: Try[ReadResultResponse], breakdownResponseAttempts: Seq[Try[ReadResultResponse]]) private final case class SpecificResponseAttempts[R](responseAttempt: Try[R], breakdownResponseAttempts: Seq[Try[ReadResultResponse]]) } //noinspection RedundantBlock abstract class AbstractReadQueryResultAdapter[Req <: BaseShrineRequest, Rsp <: ShrineResponse with HasQueryResults]( poster: Poster, override val hiveCredentials: HiveCredentials, dao: AdapterDao, doObfuscation: Boolean, getQueryId: Req => Long, getProjectId: Req => String, toResponse: (Long, QueryResult) => Rsp, breakdownTypes: Set[ResultOutputType], collectAdapterAudit:Boolean ) extends WithHiveCredentialsAdapter(hiveCredentials) { //TODO: Make this configurable private val numThreads = math.max(5, Runtime.getRuntime.availableProcessors) //TODO: Use scala.concurrent.ExecutionContext.Implicits.global instead? private lazy val executorService = Executors.newFixedThreadPool(numThreads) private lazy val executionContext = ExecutionContext.fromExecutorService(executorService) override def shutdown() { try { executorService.shutdown() executorService.awaitTermination(5, TimeUnit.SECONDS) } finally { executorService.shutdownNow() super.shutdown() } } import AbstractReadQueryResultAdapter._ override protected[adapter] def processRequest(message: BroadcastMessage): ShrineResponse = { val req = message.request.asInstanceOf[Req] val queryId = getQueryId(req) def findShrineQueryRow = dao.findQueryByNetworkId(queryId) def findShrineQueryResults = dao.findResultsFor(queryId) findShrineQueryRow match { case None => { debug(s"Query $queryId not found in the Shrine DB") ErrorResponse(QueryNotFound(queryId)) } case Some(shrineQueryRow) => { findShrineQueryResults match { case None => { debug(s"Query $queryId found but its results are not available") //TODO: When precisely can this happen? Should we go back to the CRC here? ErrorResponse(QueryResultNotAvailable(queryId)) } case Some(shrineQueryResult) => { if (shrineQueryResult.isDone) { debug(s"Query $queryId is done and already stored, returning stored results") makeResponseFrom(queryId, shrineQueryResult) } else { debug(s"Query $queryId is incomplete, asking CRC for results") val result: ShrineResponse = retrieveQueryResults(queryId, req, shrineQueryResult, message) if (collectAdapterAudit) AdapterAuditDb.db.insertResultSent(queryId,result) result } } } } } } private def makeResponseFrom(queryId: Long, shrineQueryResult: ShrineQueryResult): ShrineResponse = { shrineQueryResult.toQueryResults(doObfuscation).map(toResponse(queryId, _)).getOrElse(ErrorResponse(QueryNotFound(queryId))) } private def retrieveQueryResults(queryId: Long, req: Req, shrineQueryResult: ShrineQueryResult, message: BroadcastMessage): ShrineResponse = { //NB: If the requested query was not finished executing on the i2b2 side when Shrine recorded it, attempt to //retrieve it and all its sub-components (breakdown results, if any) in parallel. Asking for the results in //parallel is quite possibly too clever, but may be faster than asking for them serially. //TODO: Review this. //Make requests for results in parallel val futureResponses = scatter(message.networkAuthn, req, shrineQueryResult) //Gather all the results (block until they're all returned) val SpecificResponseAttempts(countResponseAttempt, breakdownResponseAttempts) = gather(queryId, futureResponses, req.waitTime) countResponseAttempt match { //If we successfully received the parent response (the one with query type PATIENT_COUNT_XML), re-store it along //with any retrieved breakdowns before returning it. case Success(countResponse) => { //NB: Only store the result if needed, that is, if all results are done //TODO: REVIEW THIS storeResultIfNecessary(shrineQueryResult, countResponse, req.authn, queryId, getFailedBreakdownTypes(breakdownResponseAttempts)) countResponse } case Failure(e) => ErrorResponse(CouldNotRetrieveQueryFromCrc(queryId,e)) } } private def scatter(authn: AuthenticationInfo, req: Req, shrineQueryResult: ShrineQueryResult): Future[RawResponseAttempts] = { def makeRequest(localResultId: Long) = ReadResultRequest(hiveCredentials.projectId, req.waitTime, hiveCredentials.toAuthenticationInfo, localResultId.toString) def process(localResultId: Long): ShrineResponse = { delegateResultRetrievingAdapter.process(authn, makeRequest(localResultId)) } implicit val executionContext = this.executionContext import scala.concurrent.blocking def futureBlockingAttempt[T](f: => T): Future[Try[T]] = Future(blocking(Try(f))) val futureCountAttempt: Future[Try[ShrineResponse]] = futureBlockingAttempt { process(shrineQueryResult.count.localId) } val futureBreakdownAttempts = Future.sequence(for { Breakdown(_, localResultId, resultType, data) <- shrineQueryResult.breakdowns } yield futureBlockingAttempt { process(localResultId) }) //Log errors retrieving count futureCountAttempt.collect { case Success(e: ErrorResponse) => error(s"Error requesting count result from the CRC: '$e'") case Failure(e) => error(s"Error requesting count result from the CRC: ", e) } //Log errors retrieving breakdown for { breakdownResponseAttempts <- futureBreakdownAttempts } { breakdownResponseAttempts.collect { case Success(e: ErrorResponse) => error(s"Error requesting breakdown result from the CRC: '$e'") case Failure(e) => error(s"Error requesting breakdown result from the CRC: ", e) } } //"Filter" for non-ErrorResponses val futureNonErrorCountAttempt: Future[Try[ReadResultResponse]] = futureCountAttempt.collect { case Success(resp: ReadResultResponse) => Success(resp) //NB: Need to repackage response here to avoid ugly, obscure, superfluous cast case unexpected => Failure(new Exception(s"Getting count result failed. Response is: '$unexpected'")) } //"Filter" for non-ErrorResponses val futureNonErrorBreakdownResponseAttempts: Future[Seq[Try[ReadResultResponse]]] = for { breakdownResponseAttempts <- futureBreakdownAttempts } yield { breakdownResponseAttempts.collect { case Success(resp: ReadResultResponse) => Try(resp) } } for { countResponseAttempt <- futureNonErrorCountAttempt breakdownResponseAttempts <- futureNonErrorBreakdownResponseAttempts } yield { RawResponseAttempts(countResponseAttempt, breakdownResponseAttempts) } } private def gather(queryId: Long, futureResponses: Future[RawResponseAttempts], waitTime: Duration): SpecificResponseAttempts[Rsp] = { val RawResponseAttempts(countResponseAttempt, breakdownResponseAttempts) = Await.result(futureResponses, waitTime) //Log any failures (countResponseAttempt +: breakdownResponseAttempts).collect { case Failure(e) => e }.foreach(error("Error retrieving result from the CRC: ", _)) //NB: Count response and ALL breakdown responses must be available (not Failures) or else a Failure will be returned val responseAttempt = for { countResponse: ReadResultResponse <- countResponseAttempt countQueryResult = countResponse.metadata breakdownResponses: Seq[ReadResultResponse] <- sequence(breakdownResponseAttempts) } yield { val breakdownsByType = (for { breakdownResponse <- breakdownResponses resultType <- breakdownResponse.metadata.resultType } yield resultType -> breakdownResponse.data).toMap val queryResultWithBreakdowns = countQueryResult.withBreakdowns(breakdownsByType) val queryResultToReturn = if(doObfuscation) Obfuscator.obfuscate(queryResultWithBreakdowns) else queryResultWithBreakdowns toResponse(queryId, queryResultToReturn) } SpecificResponseAttempts(responseAttempt, breakdownResponseAttempts) } private def getFailedBreakdownTypes(attempts: Seq[Try[ReadResultResponse]]): Set[ResultOutputType] = { val successfulBreakdownTypes = attempts.collect { case Success(ReadResultResponse(_, metadata, _)) => metadata.resultType }.flatten breakdownTypes -- successfulBreakdownTypes } private def storeResultIfNecessary(shrineQueryResult: ShrineQueryResult, response: Rsp, authn: AuthenticationInfo, queryId: Long, failedBreakdownTypes: Set[ResultOutputType]) { val responseIsDone = response.results.forall(_.statusType.isDone) if (responseIsDone) { storeResult(shrineQueryResult, response, authn, queryId, failedBreakdownTypes) } } private def storeResult(shrineQueryResult: ShrineQueryResult, response: Rsp, authn: AuthenticationInfo, queryId: Long, failedBreakdownTypes: Set[ResultOutputType]) { val rawResults = response.results val obfuscatedResults = obfuscateResults(doObfuscation)(response.results) for { shrineQuery <- dao.findQueryByNetworkId(queryId) queryResult <- rawResults.headOption obfuscatedQueryResult <- obfuscatedResults.headOption } { val queryDefinition = QueryDefinition(shrineQuery.name, shrineQuery.queryDefinition.expr) dao.inTransaction { dao.deleteQuery(queryId) dao.storeResults(authn, shrineQueryResult.localId, queryId, queryDefinition, rawResults, obfuscatedResults, failedBreakdownTypes.toSeq, queryResult.breakdowns, obfuscatedQueryResult.breakdowns) } } } private type Unmarshaller[R] = Set[ResultOutputType] => NodeSeq => Try[R] private final class DelegateAdapter[Rqst <: ShrineRequest, Rspns <: ShrineResponse](unmarshaller: Unmarshaller[Rspns]) extends CrcAdapter[Rqst, Rspns](poster, hiveCredentials) { def process(authn: AuthenticationInfo, req: Rqst): Rspns = processRequest(BroadcastMessage(authn, req)).asInstanceOf[Rspns] override protected def parseShrineResponse(xml: NodeSeq): ShrineResponse = unmarshaller(breakdownTypes)(xml).get //TODO: Avoid .get call } private lazy val delegateResultRetrievingAdapter = new DelegateAdapter[ReadResultRequest, ReadResultResponse](ReadResultResponse.fromI2b2 _) } case class QueryNotFound(queryId:Long) extends AbstractProblem(ProblemSources.Adapter) { override def summary: String = s"Query not found" override def description:String = s"No query with id $queryId found on ${stamp.host.getHostName}" } case class QueryResultNotAvailable(queryId:Long) extends AbstractProblem(ProblemSources.Adapter) { override def summary: String = s"Query $queryId found but its results are not available yet" override def description:String = s"Query $queryId found but its results are not available yet on ${stamp.host.getHostName}" } case class CouldNotRetrieveQueryFromCrc(queryId:Long,x: Throwable) extends AbstractProblem(ProblemSources.Adapter) { override def summary: String = s"Could not retrieve query $queryId from the CRC" override def description:String = s"Unhandled exception while retrieving query $queryId while retrieving it from the CRC on ${stamp.host.getHostName}" override def throwable = Some(x) } diff --git a/adapter/adapter-service/src/main/scala/net/shrine/adapter/Adapter.scala b/adapter/adapter-service/src/main/scala/net/shrine/adapter/Adapter.scala index b892e0796..a7c6d0777 100644 --- a/adapter/adapter-service/src/main/scala/net/shrine/adapter/Adapter.scala +++ b/adapter/adapter-service/src/main/scala/net/shrine/adapter/Adapter.scala @@ -1,88 +1,88 @@ package net.shrine.adapter import java.sql.SQLException import net.shrine.log.Loggable -import net.shrine.problem.{Problem, ProblemNotYetEncoded, LoggingProblemHandler, ProblemSources, AbstractProblem} -import net.shrine.protocol.{ShrineRequest, BroadcastMessage, ErrorResponse, BaseShrineResponse, AuthenticationInfo} +import net.shrine.problem._ +import net.shrine.protocol._ /** * @author Bill Simons * @since 4/8/11 * @see http://cbmi.med.harvard.edu * @see http://chip.org *

* NOTICE: This software comes with NO guarantees whatsoever and is * licensed as Lgpl Open Source * @see http://www.gnu.org/licenses/lgpl.html */ abstract class Adapter extends Loggable { final def perform(message: BroadcastMessage): BaseShrineResponse = { def problemToErrorResponse(problem:Problem):ErrorResponse = { LoggingProblemHandler.handleProblem(problem) ErrorResponse(problem) } val shrineResponse = try { processRequest(message) } catch { case e: AdapterLockoutException => problemToErrorResponse(AdapterLockout(message.request.authn,e)) case e @ CrcInvocationException(invokedCrcUrl, request, cause) => problemToErrorResponse(CrcCouldNotBeInvoked(invokedCrcUrl,request,e)) case e: AdapterMappingException => problemToErrorResponse(AdapterMappingProblem(e)) case e: SQLException => problemToErrorResponse(AdapterDatabaseProblem(e)) //noinspection RedundantBlock case e: Exception => { val summary = if(message == null) "Unknown problem in Adapter.perform with null BroadcastMessage" else s"Unexpected exception in Adapter" problemToErrorResponse(ProblemNotYetEncoded(summary,e)) } } shrineResponse } protected[adapter] def processRequest(message: BroadcastMessage): BaseShrineResponse //NOOP, may be overridden by subclasses def shutdown(): Unit = () } case class AdapterLockout(authn:AuthenticationInfo,x:AdapterLockoutException) extends AbstractProblem(ProblemSources.Adapter) { - override val throwable = Some(x) - override val summary: String = s"User '${authn.domain}:${authn.username}' locked out." - override val description:String = s"User '${authn.domain}:${authn.username}' has run too many queries that produce the same result at ${x.url} ." + override lazy val throwable = Some(x) + override lazy val summary: String = s"User '${authn.domain}:${authn.username}' locked out." + override lazy val description:String = s"User '${authn.domain}:${authn.username}' has run too many queries that produce the same result at ${x.url} ." } case class CrcCouldNotBeInvoked(crcUrl:String,request:ShrineRequest,x:CrcInvocationException) extends AbstractProblem(ProblemSources.Adapter) { - override val throwable = Some(x) - override val summary: String = s"Error communicating with I2B2 CRC." - override val description: String = s"Error invoking the CRC at '$crcUrl' with a ${request.getClass.getSimpleName} due to ${throwable.get}." - override val detailsXml =

+ override lazy val throwable = Some(x) + override lazy val summary: String = s"Error communicating with I2B2 CRC." + override lazy val description: String = s"Error invoking the CRC at '$crcUrl' with a ${request.getClass.getSimpleName} due to ${throwable.get}." + override lazy val detailsXml =
Request is {request} {throwableDetail.getOrElse("")}
} case class AdapterMappingProblem(x:AdapterMappingException) extends AbstractProblem(ProblemSources.Adapter) { - override val throwable = Some(x) - override val summary: String = "Could not map query term(s)." - override val description = s"The Shrine Adapter on ${stamp.host.getHostName} cannot map this query to its local terms." - override val detailsXml =
+ override lazy val throwable = Some(x) + override lazy val summary: String = "Could not map query term(s)." + override lazy val description = s"The Shrine Adapter on ${stamp.host.getHostName} cannot map this query to its local terms." + override lazy val detailsXml =
Query Defitiontion is {x.runQueryRequest.queryDefinition} RunQueryRequest is ${x.runQueryRequest.elideAuthenticationInfo} {throwableDetail.getOrElse("")}
} case class AdapterDatabaseProblem(x:SQLException) extends AbstractProblem(ProblemSources.Adapter) { - override val throwable = Some(x) - override val summary: String = "Problem using the Adapter database." - override val description = "The Shrine Adapter encountered a problem using a database." + override lazy val throwable = Some(x) + override lazy val summary: String = "Problem using the Adapter database." + override lazy val description = "The Shrine Adapter encountered a problem using a database." } diff --git a/adapter/adapter-service/src/main/scala/net/shrine/adapter/CrcAdapter.scala b/adapter/adapter-service/src/main/scala/net/shrine/adapter/CrcAdapter.scala index e6ffc4608..e23c911b3 100644 --- a/adapter/adapter-service/src/main/scala/net/shrine/adapter/CrcAdapter.scala +++ b/adapter/adapter-service/src/main/scala/net/shrine/adapter/CrcAdapter.scala @@ -1,107 +1,108 @@ package net.shrine.adapter -import net.shrine.problem.{ProblemSources, AbstractProblem} import org.xml.sax.SAXParseException import scala.xml.NodeSeq import scala.xml.XML -import net.shrine.protocol.{HiveCredentials, AuthenticationInfo, BroadcastMessage, Credential, ShrineRequest, ShrineResponse, TranslatableRequest, BaseShrineRequest, ErrorResponse, BaseShrineResponse} +import net.shrine.protocol.{AuthenticationInfo, BaseShrineRequest, BaseShrineResponse, BroadcastMessage, Credential, ErrorResponse, HiveCredentials, ShrineRequest, ShrineResponse, TranslatableRequest} import net.shrine.util.XmlDateHelper import net.shrine.client.Poster +import net.shrine.problem.{AbstractProblem, ProblemSources} + import scala.util.Try import scala.util.control.NonFatal /** * @author Bill Simons * @since 4/11/11 * @see http://cbmi.med.harvard.edu * @see http://chip.org *

* NOTICE: This software comes with NO guarantees whatsoever and is * licensed as Lgpl Open Source * @see http://www.gnu.org/licenses/lgpl.html */ abstract class CrcAdapter[T <: ShrineRequest, V <: ShrineResponse]( poster: Poster, override protected val hiveCredentials: HiveCredentials) extends WithHiveCredentialsAdapter(hiveCredentials) { protected def parseShrineResponse(nodeSeq: NodeSeq): ShrineResponse private[adapter] def parseShrineErrorResponseWithFallback(xmlResponseFromCrc: String): ShrineResponse = { //NB: See https://open.med.harvard.edu/jira/browse/SHRINE-534 //NB: https://open.med.harvard.edu/jira/browse/SHRINE-745 val shrineResponseAttempt = for { crcXml <- Try(XML.loadString(xmlResponseFromCrc)) shrineResponse <- Try(parseShrineResponse(crcXml)).recover { case NonFatal(e) => info(s"Exception while parsing $crcXml",e) ErrorResponse.fromI2b2(crcXml) } //todo pass the exception to build a proper error response, and log the exception } yield shrineResponse shrineResponseAttempt.recover { case saxx:SAXParseException => ErrorResponse(CannotParseXmlFromCrc(saxx,xmlResponseFromCrc)) case NonFatal(e) => error(s"Error parsing response from CRC: ", e) ErrorResponse(ExceptionWhileLoadingCrcResponse(e,xmlResponseFromCrc)) }.get } //NB: default is a noop; only RunQueryAdapter needs this for now protected[adapter] def translateNetworkToLocal(request: T): T = request protected[adapter] override def processRequest(message: BroadcastMessage): BaseShrineResponse = { val i2b2Response = callCrc(translateRequest(message.request)) parseShrineErrorResponseWithFallback(i2b2Response) } protected def callCrc(request: ShrineRequest): String = { debug(s"Sending Shrine-formatted request to the CRC at '${poster.url}': $request") val crcRequest = request.toI2b2String val crcResponse = XmlDateHelper.time(s"Calling the CRC at '${poster.url}'")(debug(_)) { //Wrap exceptions in a more descriptive form, to enable sending better error messages back to the legacy web client try { poster.post(crcRequest) } catch { case NonFatal(e) => throw CrcInvocationException(poster.url, request, e) } } crcResponse.body } private[adapter] def translateRequest(request: BaseShrineRequest): ShrineRequest = request match { case transReq: TranslatableRequest[T] => //noinspection RedundantBlock { val HiveCredentials(domain, username, password, project) = hiveCredentials val authInfo = AuthenticationInfo(domain, username, Credential(password, isToken = false)) translateNetworkToLocal(transReq.withAuthn(authInfo).withProject(project).asRequest) } case req: ShrineRequest => req case _ => throw new IllegalArgumentException(s"Unexpected request: $request") } } case class CannotParseXmlFromCrc(saxx:SAXParseException,xmlResponseFromCrc: String) extends AbstractProblem(ProblemSources.Adapter) { - override val throwable = Some(saxx) - override val summary: String = "Could not parse response from CRC." - override val description:String = s"${saxx.getMessage} while parsing the response from the CRC." - override val detailsXml =

+ override lazy val throwable = Some(saxx) + override lazy val summary: String = "Could not parse response from CRC." + override lazy val description:String = s"${saxx.getMessage} while parsing the response from the CRC." + override lazy val detailsXml =
{throwableDetail.getOrElse("")} Response is {xmlResponseFromCrc}
} case class ExceptionWhileLoadingCrcResponse(t:Throwable,xmlResponseFromCrc: String) extends AbstractProblem(ProblemSources.Adapter) { - override val throwable = Some(t) - override val summary: String = "Unanticipated exception with response from CRC." - override val description:String = s"${t.getMessage} while parsing the response from the CRC." - override val detailsXml =
+ override lazy val throwable = Some(t) + override lazy val summary: String = "Unanticipated exception with response from CRC." + override lazy val description:String = s"${t.getMessage} while parsing the response from the CRC." + override lazy val detailsXml =
{throwableDetail.getOrElse("")} Response is {xmlResponseFromCrc}
} \ No newline at end of file diff --git a/adapter/adapter-service/src/main/scala/net/shrine/adapter/components/QueryDefinitions.scala b/adapter/adapter-service/src/main/scala/net/shrine/adapter/components/QueryDefinitions.scala index 1044d8d0c..6f947393f 100644 --- a/adapter/adapter-service/src/main/scala/net/shrine/adapter/components/QueryDefinitions.scala +++ b/adapter/adapter-service/src/main/scala/net/shrine/adapter/components/QueryDefinitions.scala @@ -1,39 +1,39 @@ package net.shrine.adapter.components import net.shrine.adapter.dao.AdapterDao -import net.shrine.problem.{ProblemSources, AbstractProblem} +import net.shrine.problem.{AbstractProblem, ProblemSources} import net.shrine.protocol.ShrineResponse import net.shrine.protocol.ReadQueryDefinitionRequest import net.shrine.protocol.ReadQueryDefinitionResponse import net.shrine.protocol.ErrorResponse import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol.AbstractReadQueryDefinitionRequest /** * @author clint * @since Apr 4, 2013 * * NB: Tested by ReadQueryDefinitionAdapterTest */ final case class QueryDefinitions[Req <: AbstractReadQueryDefinitionRequest](dao: AdapterDao) { def get(request: Req): ShrineResponse = { val resultOption = for { shrineQuery <- dao.findQueryByNetworkId(request.queryId) } yield { ReadQueryDefinitionResponse( shrineQuery.networkId, shrineQuery.name, shrineQuery.username, shrineQuery.dateCreated, //TODO: I2b2 or Shrine format? shrineQuery.queryDefinition.toI2b2String) } resultOption.getOrElse(ErrorResponse(QueryNotInDatabase(request))) } } case class QueryNotInDatabase(request:AbstractReadQueryDefinitionRequest) extends AbstractProblem(ProblemSources.Hub) { - override val summary: String = s"Couldn't find query definition." - override val description:String = s"The query definition with network id: ${request.queryId} does not exist at this site." + override lazy val summary: String = s"Couldn't find query definition." + override lazy val description:String = s"The query definition with network id: ${request.queryId} does not exist at this site." } \ No newline at end of file diff --git a/adapter/adapter-service/src/main/scala/net/shrine/adapter/dao/squeryl/SquerylAdapterDao.scala b/adapter/adapter-service/src/main/scala/net/shrine/adapter/dao/squeryl/SquerylAdapterDao.scala index 64777b48c..a40d46778 100644 --- a/adapter/adapter-service/src/main/scala/net/shrine/adapter/dao/squeryl/SquerylAdapterDao.scala +++ b/adapter/adapter-service/src/main/scala/net/shrine/adapter/dao/squeryl/SquerylAdapterDao.scala @@ -1,464 +1,464 @@ package net.shrine.adapter.dao.squeryl import javax.xml.datatype.XMLGregorianCalendar import net.shrine.adapter.dao.AdapterDao import net.shrine.adapter.dao.model.{ObfuscatedPair, ShrineQuery, ShrineQueryResult} import net.shrine.adapter.dao.model.squeryl.{SquerylBreakdownResultRow, SquerylCountRow, SquerylPrivilegedUser, SquerylQueryResultRow, SquerylShrineError, SquerylShrineQuery} import net.shrine.adapter.dao.squeryl.tables.Tables import net.shrine.dao.DateHelpers import net.shrine.dao.squeryl.{SquerylEntryPoint, SquerylInitializer} import net.shrine.log.Loggable import net.shrine.problem.{AbstractProblem, ProblemSources} import net.shrine.protocol.{AuthenticationInfo, I2b2ResultEnvelope, QueryResult, ResultOutputType} import net.shrine.protocol.query.QueryDefinition import net.shrine.util.XmlDateHelper import org.squeryl.Query import org.squeryl.dsl.GroupWithMeasures import scala.util.Try import scala.xml.NodeSeq /** * @author clint * @since May 22, 2013 */ final class SquerylAdapterDao(initializer: SquerylInitializer, tables: Tables)(implicit breakdownTypes: Set[ResultOutputType]) extends AdapterDao with Loggable { initializer.init() override def inTransaction[T](f: => T): T = SquerylEntryPoint.inTransaction { f } import SquerylEntryPoint._ override def flagQuery(networkQueryId: Long, flagMessage: Option[String]): Unit = mutateFlagField(networkQueryId, newIsFlagged = true, flagMessage) override def unFlagQuery(networkQueryId: Long): Unit = mutateFlagField(networkQueryId, newIsFlagged = false, None) private def mutateFlagField(networkQueryId: Long, newIsFlagged: Boolean, newFlagMessage: Option[String]): Unit = { inTransaction { update(tables.shrineQueries) { queryRow => where(queryRow.networkId === networkQueryId). set(queryRow.isFlagged := newIsFlagged, queryRow.flagMessage := newFlagMessage) } } } override def storeResults( authn: AuthenticationInfo, masterId: String, networkQueryId: Long, queryDefinition: QueryDefinition, rawQueryResults: Seq[QueryResult], obfuscatedQueryResults: Seq[QueryResult], failedBreakdownTypes: Seq[ResultOutputType], mergedBreakdowns: Map[ResultOutputType, I2b2ResultEnvelope], obfuscatedBreakdowns: Map[ResultOutputType, I2b2ResultEnvelope]): Unit = { inTransaction { val insertedQueryId = insertQuery(masterId, networkQueryId, authn, queryDefinition, isFlagged = false, hasBeenRun = true, flagMessage = None) val insertedQueryResultIds = insertQueryResults(insertedQueryId, rawQueryResults) storeCountResults(rawQueryResults, obfuscatedQueryResults, insertedQueryResultIds) storeErrorResults(rawQueryResults, insertedQueryResultIds) storeBreakdownFailures(failedBreakdownTypes.toSet, insertedQueryResultIds) insertBreakdownResults(insertedQueryResultIds, mergedBreakdowns, obfuscatedBreakdowns) } } private[adapter] def storeCountResults(raw: Seq[QueryResult], obfuscated: Seq[QueryResult], insertedIds: Map[ResultOutputType, Seq[Int]]): Unit = { val notErrors = raw.filter(!_.isError) val obfuscatedNotErrors = obfuscated.filter(!_.isError) if(notErrors.size > 1) { warn(s"Got ${notErrors.size} raw (hopefully-)count results; more than 1 is unusual.") } if(obfuscatedNotErrors.size > 1) { warn(s"Got ${obfuscatedNotErrors.size} obfuscated (hopefully-)count results; more than 1 is unusual.") } if(notErrors.size != obfuscatedNotErrors.size) { warn(s"Got ${notErrors.size} raw and ${obfuscatedNotErrors.size} obfuscated (hopefully-)count results; that these numbers are different is unusual.") } import ResultOutputType.PATIENT_COUNT_XML def isCount(qr: QueryResult): Boolean = qr.resultType.contains(PATIENT_COUNT_XML) inTransaction { //NB: Take the count/setSize from the FIRST PATIENT_COUNT_XML QueryResult, //though the same count should be there for all of them, if there are more than one for { Seq(insertedCountQueryResultId) <- insertedIds.get(PATIENT_COUNT_XML) notError <- notErrors.find(isCount) //NB: Find a count result, just to be sure obfuscatedNotError <- obfuscatedNotErrors.find(isCount) //NB: Find a count result, just to be sure } { insertCountResult(insertedCountQueryResultId, notError.setSize, obfuscatedNotError.setSize) } } } private[adapter] def storeErrorResults(results: Seq[QueryResult], insertedIds: Map[ResultOutputType, Seq[Int]]): Unit = { val errors = results.filter(_.isError) val insertedErrorResultIds = insertedIds.getOrElse(ResultOutputType.ERROR,Nil) val insertedIdsToErrors = insertedErrorResultIds zip errors inTransaction { for { (insertedErrorResultId, errorQueryResult) <- insertedIdsToErrors } { val pd = errorQueryResult.problemDigest.get //it's an error so it will have a problem digest insertErrorResult( insertedErrorResultId, errorQueryResult.statusMessage.getOrElse("Unknown failure"), pd.codec, pd.stampText, pd.summary, pd.description, pd.detailsXml ) } } } private[adapter] def storeBreakdownFailures(failedBreakdownTypes: Set[ResultOutputType], insertedIds: Map[ResultOutputType, Seq[Int]]): Unit = { val insertedIdsForFailedBreakdownTypes = insertedIds.filterKeys(failedBreakdownTypes.contains) inTransaction { for { (failedBreakdownType, Seq(resultId)) <- insertedIdsForFailedBreakdownTypes } { //todo propagate backwards to the breakdown failure to create the corect problem object BreakdownFailure extends AbstractProblem(ProblemSources.Adapter) { - override val summary: String = "Couldn't retrieve result breakdown" - override val description:String = s"Couldn't retrieve result breakdown of type '$failedBreakdownType'" + override lazy val summary: String = "Couldn't retrieve result breakdown" + override lazy val description:String = s"Couldn't retrieve result breakdown of type '$failedBreakdownType'" } val pd = BreakdownFailure.toDigest insertErrorResult( resultId, s"Couldn't retrieve breakdown of type '$failedBreakdownType'", pd.codec, pd.stampText, pd.summary, pd.description, pd.detailsXml ) } } } override def findRecentQueries(howMany: Int): Seq[ShrineQuery] = { inTransaction { Queries.queriesForAllUsers.take(howMany).map(_.toShrineQuery).toSeq } } def findAllCounts():Seq[SquerylCountRow] = { inTransaction{ Queries.allCountResults.toSeq } } override def renameQuery(networkQueryId: Long, newName: String) { inTransaction { update(tables.shrineQueries) { queryRow => where(queryRow.networkId === networkQueryId). set(queryRow.name := newName) } } } override def deleteQuery(networkQueryId: Long): Unit = { inTransaction { tables.shrineQueries.deleteWhere(_.networkId === networkQueryId) } } override def deleteQueryResultsFor(networkQueryId: Long): Unit = { inTransaction { val resultIdsForNetworkQueryId = join(tables.shrineQueries, tables.queryResults) { (queryRow, resultRow) => where(queryRow.networkId === networkQueryId). select(resultRow.id). on(queryRow.id === resultRow.queryId) }.toSet tables.queryResults.deleteWhere(_.id in resultIdsForNetworkQueryId) } } override def isUserLockedOut(authn: AuthenticationInfo, defaultThreshold: Int): Boolean = Try { inTransaction { val privilegedUserOption = Queries.privilegedUsers(authn.domain, authn.username).singleOption val threshold:Int = privilegedUserOption.flatMap(_.threshold).getOrElse(defaultThreshold.intValue) val thirtyDaysInThePast: XMLGregorianCalendar = DateHelpers.daysFromNow(-30) val overrideDate: XMLGregorianCalendar = privilegedUserOption.map(_.toPrivilegedUser).flatMap(_.overrideDate).getOrElse(thirtyDaysInThePast) //sorted instead of just finding max val counts: Seq[Long] = Queries.repeatedResults(authn.domain, authn.username, overrideDate).toSeq.sorted //and then grabbing the last, highest value in the sorted sequence val repeatedResultCount: Long = counts.lastOption.getOrElse(0L) val result = repeatedResultCount > threshold debug(s"User ${authn.domain}:${authn.username} locked out? $result") result } }.getOrElse(false) override def insertQuery(localMasterId: String, networkId: Long, authn: AuthenticationInfo, queryDefinition: QueryDefinition, isFlagged: Boolean, hasBeenRun: Boolean, flagMessage: Option[String]): Int = { inTransaction { val inserted = tables.shrineQueries.insert(new SquerylShrineQuery( 0, localMasterId, networkId, authn.username, authn.domain, XmlDateHelper.now, isFlagged, flagMessage, hasBeenRun, queryDefinition)) inserted.id } } /** * Insert rows into QueryResults, one for each QueryResult in the passed RunQueryResponse * Inserted rows are 'children' of the passed ShrineQuery (ie, they are the results of the query) */ override def insertQueryResults(parentQueryId: Int, results: Seq[QueryResult]): Map[ResultOutputType, Seq[Int]] = { def execTime(result: QueryResult): Option[Long] = { //TODO: How are locales handled here? Do we care? def toMillis(xmlGc: XMLGregorianCalendar) = xmlGc.toGregorianCalendar.getTimeInMillis for { start <- result.startDate end <- result.endDate } yield toMillis(end) - toMillis(start) } val typeToIdTuples = inTransaction { for { result <- results resultType = result.resultType.getOrElse(ResultOutputType.ERROR) //TODO: under what circumstances can QueryResults NOT have start and end dates set? elapsed = execTime(result) } yield { val lastInsertedQueryResultRow = tables.queryResults.insert(new SquerylQueryResultRow(0, result.resultId, parentQueryId, resultType, result.statusType, elapsed, XmlDateHelper.now)) (resultType, lastInsertedQueryResultRow.id) } } typeToIdTuples.groupBy { case (resultType, _) => resultType }.mapValues(_.map { case (_, count) => count }) } override def insertCountResult(resultId: Int, originalCount: Long, obfuscatedCount: Long) { //NB: Squeryl steers us toward inserting with dummy ids :( inTransaction { tables.countResults.insert(new SquerylCountRow(0, resultId, originalCount, obfuscatedCount, XmlDateHelper.now)) } } override def insertBreakdownResults(parentResultIds: Map[ResultOutputType, Seq[Int]], originalBreakdowns: Map[ResultOutputType, I2b2ResultEnvelope], obfuscatedBreakdowns: Map[ResultOutputType, I2b2ResultEnvelope]) { def merge(original: I2b2ResultEnvelope, obfuscated: I2b2ResultEnvelope): Map[String, ObfuscatedPair] = { Map.empty ++ (for { (key, originalValue) <- original.data obfuscatedValue <- obfuscated.data.get(key) } yield (key, ObfuscatedPair(originalValue, obfuscatedValue))) } inTransaction { for { (resultType, Seq(resultId)) <- parentResultIds if resultType.isBreakdown originalBreakdown <- originalBreakdowns.get(resultType) obfuscatedBreakdown <- obfuscatedBreakdowns.get(resultType) (key, ObfuscatedPair(original, obfuscated)) <- merge(originalBreakdown, obfuscatedBreakdown) } { tables.breakdownResults.insert(SquerylBreakdownResultRow(0, resultId, key, original, obfuscated)) } } } override def insertErrorResult(parentResultId: Int, errorMessage: String, codec:String, stampText:String, summary:String, digestDescription:String,detailsXml:NodeSeq) { //NB: Squeryl steers us toward inserting with dummy ids :( inTransaction { tables.errorResults.insert(SquerylShrineError(0, parentResultId, errorMessage, codec, stampText, summary, digestDescription, detailsXml.toString())) } } override def findQueryByNetworkId(networkQueryId: Long): Option[ShrineQuery] = { inTransaction { Queries.queriesByNetworkId(networkQueryId).headOption.map(_.toShrineQuery) } } override def findQueriesByUserAndDomain(domain: String, username: String, howMany: Int): Seq[ShrineQuery] = { inTransaction { Queries.queriesForUser(username, domain).take(howMany).toSeq.map(_.toShrineQuery) } } override def findQueriesByDomain(domain: String): Seq[ShrineQuery] = { inTransaction { Queries.queriesForDomain(domain).toList.map(_.toShrineQuery) } } override def findResultsFor(networkQueryId: Long): Option[ShrineQueryResult] = { inTransaction { val breakdownRowsByType = Queries.breakdownResults(networkQueryId).toSeq.groupBy { case (outputType, _) => outputType.toQueryResultRow.resultType }.mapValues(_.map { case (_, row) => row.toBreakdownResultRow }) val queryRowOption = Queries.queriesByNetworkId(networkQueryId).headOption.map(_.toShrineQuery) val countRowOption = Queries.countResults(networkQueryId).headOption.map(_.toCountRow) val queryResultRows = Queries.resultsForQuery(networkQueryId).toSeq.map(_.toQueryResultRow) val errorResultRows = Queries.errorResults(networkQueryId).toSeq.map(_.toShrineError) for { queryRow <- queryRowOption countRow <- countRowOption shrineQueryResult <- ShrineQueryResult.fromRows(queryRow, queryResultRows, countRow, breakdownRowsByType, errorResultRows) } yield { shrineQueryResult } } } /** * @author clint * @since Nov 19, 2012 */ object Queries { def privilegedUsers(domain: String, username: String): Query[SquerylPrivilegedUser] = { from(tables.privilegedUsers) { user => where(user.username === username and user.domain === domain).select(user) } } def repeatedResults(domain: String, username: String, overrideDate: XMLGregorianCalendar): Query[Long] = { val counts: Query[GroupWithMeasures[Long, Long]] = join(tables.shrineQueries, tables.queryResults, tables.countResults) { (queryRow, resultRow, countRow) => where(queryRow.username === username and queryRow.domain === domain and (countRow.originalValue <> 0L) and queryRow.dateCreated > DateHelpers.toTimestamp(overrideDate)). groupBy(countRow.originalValue). compute(count(countRow.originalValue)). on(queryRow.id === resultRow.queryId, resultRow.id === countRow.resultId) } //Filter for result counts > 0 from(counts) { cnt => where(cnt.measures gt 0).select(cnt.measures) } } val queriesForAllUsers: Query[SquerylShrineQuery] = { from(tables.shrineQueries) { queryRow => select(queryRow).orderBy(queryRow.dateCreated.desc) } } //TODO: Find a way to parameterize on limit, to avoid building the query every time //TODO: limit def queriesForUser(username: String, domain: String): Query[SquerylShrineQuery] = { from(tables.shrineQueries) { queryRow => where(queryRow.domain === domain and queryRow.username === username). select(queryRow). orderBy(queryRow.dateCreated.desc) } } def queriesForDomain(domain: String): Query[SquerylShrineQuery] = { from(tables.shrineQueries) { queryRow => where(queryRow.domain === domain). select(queryRow). orderBy(queryRow.dateCreated.desc) } } val allCountResults: Query[SquerylCountRow] = { from(tables.countResults) { queryRow => select(queryRow) } } def queriesByNetworkId(networkQueryId: Long): Query[SquerylShrineQuery] = { from(tables.shrineQueries) { queryRow => where(queryRow.networkId === networkQueryId).select(queryRow) } } //TODO: Find out how to compose queries, to re-use queriesByNetworkId def queryNamesByNetworkId(networkQueryId: Long): Query[String] = { from(tables.shrineQueries) { queryRow => where(queryRow.networkId === networkQueryId).select(queryRow.name) } } def resultsForQuery(networkQueryId: Long): Query[SquerylQueryResultRow] = { val resultsForNetworkQueryId = join(tables.shrineQueries, tables.queryResults) { (queryRow, resultRow) => where(queryRow.networkId === networkQueryId). select(resultRow). on(queryRow.id === resultRow.queryId) } from(resultsForNetworkQueryId)(select(_)) } def countResults(networkQueryId: Long): Query[SquerylCountRow] = { join(tables.shrineQueries, tables.queryResults, tables.countResults) { (queryRow, resultRow, countRow) => where(queryRow.networkId === networkQueryId). select(countRow). on(queryRow.id === resultRow.queryId, resultRow.id === countRow.resultId) } } def errorResults(networkQueryId: Long): Query[SquerylShrineError] = { join(tables.shrineQueries, tables.queryResults, tables.errorResults) { (queryRow, resultRow, errorRow) => where(queryRow.networkId === networkQueryId). select(errorRow). on(queryRow.id === resultRow.queryId, resultRow.id === errorRow.resultId) } } //NB: using groupBy here is too much of a pain; do it 'manually' later def breakdownResults(networkQueryId: Long): Query[(SquerylQueryResultRow, SquerylBreakdownResultRow)] = { join(tables.shrineQueries, tables.queryResults, tables.breakdownResults) { (queryRow, resultRow, breakdownRow) => where(queryRow.networkId === networkQueryId). select((resultRow, breakdownRow)). on(queryRow.id === resultRow.queryId, resultRow.id === breakdownRow.resultId) } } } } \ No newline at end of file diff --git a/adapter/adapter-service/src/main/scala/net/shrine/adapter/service/AdapterService.scala b/adapter/adapter-service/src/main/scala/net/shrine/adapter/service/AdapterService.scala index 8f0c6ca76..e785eb11d 100644 --- a/adapter/adapter-service/src/main/scala/net/shrine/adapter/service/AdapterService.scala +++ b/adapter/adapter-service/src/main/scala/net/shrine/adapter/service/AdapterService.scala @@ -1,102 +1,103 @@ package net.shrine.adapter.service import net.shrine.log.Loggable -import net.shrine.problem.{ProblemSources, AbstractProblem} -import net.shrine.protocol.{RequestType, Signature, NodeId, Result, BroadcastMessage, ErrorResponse, BaseShrineResponse} +import net.shrine.protocol.{BaseShrineResponse, BroadcastMessage, ErrorResponse, NodeId, RequestType, Result, Signature} import net.shrine.adapter.AdapterMap import net.shrine.crypto.Verifier +import net.shrine.problem.{AbstractProblem, ProblemSources} + import scala.concurrent.duration.Duration import scala.concurrent.duration._ /** * Heart of the adapter. * * @author clint * @since Nov 14, 2013 */ final class AdapterService( nodeId: NodeId, signatureVerifier: Verifier, maxSignatureAge: Duration, adapterMap: AdapterMap) extends AdapterRequestHandler with Loggable { import AdapterService._ logStartup(adapterMap) override def handleRequest(message: BroadcastMessage): Result = { handleInvalidSignature(message).orElse { for { adapter <- adapterMap.adapterFor(message.request.requestType) } yield time(nodeId) { adapter.perform(message) } }.getOrElse { Result(nodeId, 0.milliseconds, ErrorResponse(UnknownRequestType(message.request.requestType))) } } /** * @return None if the signature is fine, Some(result with an ErrorResponse) if not */ private def handleInvalidSignature(message: BroadcastMessage): Option[Result] = { val (sigIsValid, elapsed) = time(signatureVerifier.verifySig(message, maxSignatureAge)) if(sigIsValid) { None } else { info(s"Incoming message had invalid signature: $message") Some(Result(nodeId, elapsed.milliseconds, ErrorResponse(CouldNotVerifySignature(message)))) } } } object AdapterService extends Loggable { private def logStartup(adapterMap: AdapterMap) { info("Adapter service initialized, will respond to the following queries: ") val sortedByReqType = adapterMap.requestsToAdapters.toSeq.sortBy { case (k, _) => k } sortedByReqType.foreach { case (requestType, adapter) => info(s" $requestType:\t(${adapter.getClass.getSimpleName})") } } private[service] def time[T](f: => T): (T, Long) = { val start = System.currentTimeMillis val result = f val elapsed = System.currentTimeMillis - start (result, elapsed) } private[service] def time(nodeId: NodeId)(f: => BaseShrineResponse): Result = { val (response, elapsed) = time(f) Result(nodeId, elapsed.milliseconds, response) } } case class CouldNotVerifySignature(message: BroadcastMessage) extends AbstractProblem(ProblemSources.Adapter){ val signature: Option[Signature] = message.signature - override val summary: String = signature.fold("A message was not signed")(sig => s"The trust relationship with ${sig.signedBy} is not properly configured.") - override val description: String = signature.fold(s"The Adapter at ${stamp.host.getHostName} could not properly validate a request because it had no signature.")(sig => s"The Adapter at ${stamp.host.getHostName} could not properly validate the request from ${sig.signedBy}. An incoming message from the hub had an invalid signature.") - override val detailsXml = signature.fold( + override lazy val summary: String = signature.fold("A message was not signed")(sig => s"The trust relationship with ${sig.signedBy} is not properly configured.") + override lazy val description: String = signature.fold(s"The Adapter at ${stamp.host.getHostName} could not properly validate a request because it had no signature.")(sig => s"The Adapter at ${stamp.host.getHostName} could not properly validate the request from ${sig.signedBy}. An incoming message from the hub had an invalid signature.") + override lazy val detailsXml = signature.fold(
)( sig =>
Signature is {sig}
) } case class UnknownRequestType(requestType: RequestType) extends AbstractProblem(ProblemSources.Adapter){ - override val summary: String = s"Unknown request type $requestType" - override val description: String = s"The Adapter at ${stamp.host.getHostName} received a request of type $requestType that it cannot process." + override lazy val summary: String = s"Unknown request type $requestType" + override lazy val description: String = s"The Adapter at ${stamp.host.getHostName} received a request of type $requestType that it cannot process." } \ No newline at end of file diff --git a/adapter/adapter-service/src/main/sql/mssql.ddl b/adapter/adapter-service/src/main/sql/mssql.ddl index 56f76568f..d61789116 100644 --- a/adapter/adapter-service/src/main/sql/mssql.ddl +++ b/adapter/adapter-service/src/main/sql/mssql.ddl @@ -1,4 +1,5 @@ create table "queriesReceived" ("shrineNodeId" TEXT NOT NULL,"userName" TEXT NOT NULL,"networkQueryId" BIGINT NOT NULL,"queryName" TEXT NOT NULL,"topicId" TEXT,"topicName" TEXT,"timeQuerySent" BIGINT NOT NULL,"timeReceived" BIGINT NOT NULL); create table "executionsStarted" ("networkQueryId" BIGINT NOT NULL,"queryName" TEXT NOT NULL,"timeExecutionStarted" BIGINT NOT NULL); create table "executionsCompleted" ("networkQueryId" BIGINT NOT NULL,"replyId" BIGINT NOT NULL,"queryName" TEXT NOT NULL,"timeExecutionCompleted" BIGINT NOT NULL); create table "resultsSent" ("networkQueryId" BIGINT NOT NULL,"replyId" BIGINT NOT NULL,"queryName" TEXT NOT NULL,"timeResultsSent" BIGINT NOT NULL); +create table "problems" ("id" BIGINT NOT NULL, "codec" TEXT NOT NULL,"stampText" TEXT NOT NULL,"summary" TEXT NOT NULL,"description" TEXT NOT NULL,"detailsXml" TEXT NOT NULL,"epoch" BIGINT NOT NULL); \ No newline at end of file diff --git a/adapter/adapter-service/src/main/sql/mysql.ddl b/adapter/adapter-service/src/main/sql/mysql.ddl index 0db28556b..8c1b0ee65 100644 --- a/adapter/adapter-service/src/main/sql/mysql.ddl +++ b/adapter/adapter-service/src/main/sql/mysql.ddl @@ -1,4 +1,5 @@ create table `queriesReceived` (`shrineNodeId` TEXT NOT NULL,`userName` TEXT NOT NULL,`networkQueryId` BIGINT NOT NULL,`queryName` TEXT NOT NULL,`topicId` TEXT,`topicName` TEXT,`timeQuerySent` BIGINT NOT NULL,`timeReceived` BIGINT NOT NULL); create table `executionsStarted` (`networkQueryId` BIGINT NOT NULL,`queryName` TEXT NOT NULL,`timeExecutionStarted` BIGINT NOT NULL); create table `executionsCompleted` (`networkQueryId` BIGINT NOT NULL,`replyId` BIGINT NOT NULL,`queryName` TEXT NOT NULL,`timeExecutionCompleted` BIGINT NOT NULL); -create table `resultsSent` (`networkQueryId` BIGINT NOT NULL,`replyId` BIGINT NOT NULL,`queryName` TEXT NOT NULL,`timeResultsSent` BIGINT NOT NULL); \ No newline at end of file +create table `resultsSent` (`networkQueryId` BIGINT NOT NULL,`replyId` BIGINT NOT NULL,`queryName` TEXT NOT NULL,`timeResultsSent` BIGINT NOT NULL); +create table `problems` (`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,`codec` TEXT NOT NULL,`stampText` TEXT NOT NULL,`summary` TEXT NOT NULL,`description` TEXT NOT NULL,`detailsXml` TEXT NOT NULL,`epoch` BIGINT NOT NULL); \ No newline at end of file diff --git a/adapter/adapter-service/src/main/sql/oracle.ddl b/adapter/adapter-service/src/main/sql/oracle.ddl index eda9bbaab..dece08037 100644 --- a/adapter/adapter-service/src/main/sql/oracle.ddl +++ b/adapter/adapter-service/src/main/sql/oracle.ddl @@ -1,4 +1,5 @@ create table "queriesReceived" ("shrineNodeId" VARCHAR2(256) NOT NULL,"userName" VARCHAR2(256) NOT NULL,"networkQueryId" NUMBER NOT NULL,"queryName" VARCHAR2(256) NOT NULL,"topicId" VARCHAR2(256),"topicName" VARCHAR2(256),"timeQuerySent" NUMBER NOT NULL,"timeReceived" NUMBER NOT NULL); create table "executionsStarted" ("networkQueryId" NUMBER NOT NULL,"queryName" VARCHAR2(256) NOT NULL,"timeExecutionStarted" NUMBER NOT NULL); create table "executionsCompleted" ("networkQueryId" NUMBER NOT NULL,"replyId" NUMBER NOT NULL,"queryName" VARCHAR2(256) NOT NULL,"timeExecutionCompleted" NUMBER NOT NULL); -create table "resultsSent" ("networkQueryId" NUMBER NOT NULL,"replyId" NUMBER NOT NULL,"queryName" VARCHAR2(256) NOT NULL,"timeResultsSent" NUMBER NOT NULL); \ No newline at end of file +create table "resultsSent" ("networkQueryId" NUMBER NOT NULL,"replyId" NUMBER NOT NULL,"queryName" VARCHAR2(256) NOT NULL,"timeResultsSent" NUMBER NOT NULL); +create table "problems" ("id" NUMBER NOT NULL, "codec" VARCHAR2(256) NOT NULL,"stampText" VARCHAR2(256) NOT NULL,"summary" VARCHAR2(256) NOT NULL,"description" VARCHAR2(256) NOT NULL,"detailsXml" VARCHAR2(256) NOT NULL,"epoch" NUMBER NOT NULL); \ No newline at end of file diff --git a/adapter/adapter-service/src/test/scala/net/shrine/adapter/AbstractQueryRetrievalTestCase.scala b/adapter/adapter-service/src/test/scala/net/shrine/adapter/AbstractQueryRetrievalTestCase.scala index 2ba6d8a17..6ee1c050d 100644 --- a/adapter/adapter-service/src/test/scala/net/shrine/adapter/AbstractQueryRetrievalTestCase.scala +++ b/adapter/adapter-service/src/test/scala/net/shrine/adapter/AbstractQueryRetrievalTestCase.scala @@ -1,377 +1,378 @@ package net.shrine.adapter -import net.shrine.problem.{TestProblem, ProblemSources, AbstractProblem} - import scala.xml.NodeSeq import net.shrine.util.ShouldMatchersForJUnit import ObfuscatorTest.within3 import javax.xml.datatype.XMLGregorianCalendar + import net.shrine.adapter.dao.AdapterDao import net.shrine.adapter.dao.squeryl.AbstractSquerylAdapterTest import net.shrine.client.HttpClient import net.shrine.client.HttpResponse -import net.shrine.protocol.{HiveCredentials, AuthenticationInfo, BroadcastMessage, CrcRequest, Credential, ErrorResponse, I2b2ResultEnvelope, QueryResult, ReadResultRequest, ReadResultResponse, ResultOutputType, ShrineRequest, ShrineResponse, BaseShrineResponse, BaseShrineRequest, RunQueryRequest, RunQueryResponse, DefaultBreakdownResultOutputTypes} +import net.shrine.protocol.{AuthenticationInfo, BaseShrineRequest, BaseShrineResponse, BroadcastMessage, CrcRequest, Credential, DefaultBreakdownResultOutputTypes, ErrorResponse, HiveCredentials, I2b2ResultEnvelope, QueryResult, ReadResultRequest, ReadResultResponse, ResultOutputType, RunQueryRequest, RunQueryResponse, ShrineRequest, ShrineResponse} import net.shrine.protocol.DefaultBreakdownResultOutputTypes.PATIENT_AGE_COUNT_XML import net.shrine.protocol.ResultOutputType.PATIENT_COUNT_XML import net.shrine.protocol.DefaultBreakdownResultOutputTypes.PATIENT_GENDER_COUNT_XML import net.shrine.protocol.query.{QueryDefinition, Term} import net.shrine.util.XmlDateHelper import net.shrine.util.XmlDateHelper.now import net.shrine.util.XmlGcEnrichments import net.shrine.client.Poster import net.shrine.adapter.translators.QueryDefinitionTranslator import net.shrine.adapter.translators.ExpressionTranslator +import net.shrine.problem.TestProblem + import scala.util.Success /** * @author clint * @since Nov 8, 2012 */ //noinspection UnitMethodIsParameterless abstract class AbstractQueryRetrievalTestCase[R <: BaseShrineResponse]( makeAdapter: (AdapterDao, HttpClient) => WithHiveCredentialsAdapter, makeRequest: (Long, AuthenticationInfo) => BaseShrineRequest, extractor: R => Option[(Long, QueryResult)]) extends AbstractSquerylAdapterTest with ShouldMatchersForJUnit { private val authn = AuthenticationInfo("some-domain", "some-user", Credential("alskdjlkasd", false)) def doTestProcessRequestMissingQuery { val adapter = makeAdapter(dao, MockHttpClient) val response = adapter.processRequest(BroadcastMessage(0L, authn, makeRequest(-1L, authn))) response.isInstanceOf[ErrorResponse] should be(true) } def doTestProcessInvalidRequest { val adapter = makeAdapter(dao, MockHttpClient) intercept[ClassCastException] { //request must be a type of request we can handle adapter.processRequest(BroadcastMessage(0L, authn, new AbstractQueryRetrievalTestCase.BogusRequest)) } } private val localMasterId = "alksjdkalsdjlasdjlkjsad" private val shrineNetworkQueryId = 123L private def doGetResults(adapter: Adapter) = adapter.processRequest(BroadcastMessage(shrineNetworkQueryId, authn, makeRequest(shrineNetworkQueryId, authn))) private def toMillis(xmlGc: XMLGregorianCalendar): Long = xmlGc.toGregorianCalendar.getTimeInMillis private val instanceId = 999L private val setSize = 12345L private val obfSetSize = setSize + 1 private val queryExpr = Term("foo") private val topicId = "laskdjlkasd" private val fooQuery = QueryDefinition("some-query",queryExpr) def doTestProcessRequestIncompleteQuery(countQueryShouldWork: Boolean = true): Unit = afterCreatingTables { val dbQueryId = dao.insertQuery(localMasterId, shrineNetworkQueryId, authn, fooQuery, isFlagged = false, hasBeenRun = true, flagMessage = None) import ResultOutputType._ import XmlDateHelper.now val breakdowns = Map(PATIENT_AGE_COUNT_XML -> I2b2ResultEnvelope(PATIENT_AGE_COUNT_XML, Map("a" -> 1L, "b" -> 2L))) val obfscBreakdowns = breakdowns.mapValues(_.mapValues(_ + 1)) val startDate = now val elapsed = 100L val endDate = { import XmlGcEnrichments._ import scala.concurrent.duration._ startDate + elapsed.milliseconds } val countResultId = 456L val breakdownResultId = 98237943265436L val incompleteCountResult = QueryResult( resultId = countResultId, instanceId = instanceId, resultType = Some(PATIENT_COUNT_XML), setSize = setSize, startDate = Option(startDate), endDate = Option(endDate), description = Some("results from node X"), statusType = QueryResult.StatusType.Processing, statusMessage = None, breakdowns = breakdowns) val breakdownResult = breakdowns.head match { case (resultType, data) => incompleteCountResult.withId(breakdownResultId).withBreakdowns(Map(resultType -> data)).withResultType(resultType) } val queryStartDate = now val idsByResultType = dao.insertQueryResults(dbQueryId, incompleteCountResult :: breakdownResult :: Nil) final class MightWorkMockHttpClient(expectedHiveCredentials: HiveCredentials) extends HttpClient { override def post(input: String, url: String): HttpResponse = { def makeFinished(queryResult: QueryResult) = queryResult.copy(statusType = QueryResult.StatusType.Finished) def validateAuthnAndProjectId(req: ShrineRequest) { req.authn should equal(expectedHiveCredentials.toAuthenticationInfo) req.projectId should equal(expectedHiveCredentials.projectId) } val response = CrcRequest.fromI2b2String(DefaultBreakdownResultOutputTypes.toSet)(input) match { case Success(req: ReadResultRequest) if req.localResultId == countResultId.toString => { validateAuthnAndProjectId(req) if (countQueryShouldWork) { ReadResultResponse(123L, makeFinished(incompleteCountResult), I2b2ResultEnvelope(PATIENT_COUNT_XML, Map(PATIENT_COUNT_XML.name -> incompleteCountResult.setSize))) } else { ErrorResponse(TestProblem(summary = "Retrieving count result failed")) } } case Success(req: ReadResultRequest) if req.localResultId == breakdownResultId.toString => { validateAuthnAndProjectId(req) ReadResultResponse(123L, makeFinished(breakdownResult), breakdowns.head._2) } case _ => fail(s"Unknown input: $input") } HttpResponse.ok(response.toI2b2String) } } val adapter: WithHiveCredentialsAdapter = makeAdapter(dao, new MightWorkMockHttpClient(AbstractQueryRetrievalTestCase.hiveCredentials)) def getResults = doGetResults(adapter) getResults.isInstanceOf[ErrorResponse] should be(true) dao.insertCountResult(idsByResultType(PATIENT_COUNT_XML).head, setSize, obfSetSize) dao.insertBreakdownResults(idsByResultType, breakdowns, obfscBreakdowns) //The query shouldn't be 'done', since its status is PROCESSING dao.findResultsFor(shrineNetworkQueryId).get.count.statusType should be(QueryResult.StatusType.Processing) //Now, calling processRequest (via getResults) should cause the query to be re-retrieved from the CRC val result = getResults.asInstanceOf[R] //Which should cause the query to be re-stored with a 'done' status (since that's what our mock CRC returns) val expectedStatusType = if (countQueryShouldWork) QueryResult.StatusType.Finished else QueryResult.StatusType.Processing dao.findResultsFor(shrineNetworkQueryId).get.count.statusType should be(expectedStatusType) if (!countQueryShouldWork) { result.isInstanceOf[ErrorResponse] should be(true) } else { val Some((actualNetworkQueryId, actualQueryResult)) = extractor(result) actualNetworkQueryId should equal(shrineNetworkQueryId) import ObfuscatorTest.within3 actualQueryResult.resultType should equal(Some(PATIENT_COUNT_XML)) within3(setSize, actualQueryResult.setSize) should be(true) actualQueryResult.description should be(Some("results from node X")) actualQueryResult.statusType should equal(QueryResult.StatusType.Finished) actualQueryResult.statusMessage should be(Some(QueryResult.StatusType.Finished.name)) actualQueryResult.breakdowns.foreach { case (rt, I2b2ResultEnvelope(_, data)) => { data.forall { case (key, value) => within3(value, breakdowns.get(rt).get.data.get(key).get) } } } for { startDate <- actualQueryResult.startDate endDate <- actualQueryResult.endDate } { val actualElapsed = toMillis(endDate) - toMillis(startDate) actualElapsed should equal(elapsed) } } } def doTestProcessRequestQueuedQuery: Unit = afterCreatingTables { import ResultOutputType._ import XmlDateHelper.now val startDate = now val elapsed = 100L val endDate = { import XmlGcEnrichments._ import scala.concurrent.duration._ startDate + elapsed.milliseconds } val countResultId = 456L val incompleteCountResult = QueryResult(-1L, -1L, Some(PATIENT_COUNT_XML), -1L, Option(startDate), Option(endDate), Some("results from node X"), QueryResult.StatusType.Queued, None) dao.inTransaction { val insertedQueryId = dao.insertQuery(localMasterId, shrineNetworkQueryId, authn, fooQuery, isFlagged = false, hasBeenRun = false, flagMessage = None) //NB: We need to insert dummy QueryResult and Count records so that calls to StoredQueries.retrieve() in //AbstractReadQueryResultAdapter, called when retrieving results for previously-queued-or-incomplete //queries, will work. val insertedQueryResultIds = dao.insertQueryResults(insertedQueryId, Seq(incompleteCountResult)) val countQueryResultId = insertedQueryResultIds(ResultOutputType.PATIENT_COUNT_XML).head dao.insertCountResult(countQueryResultId, -1L, -1L) } val queryStartDate = now object MockHttpClient extends HttpClient { override def post(input: String, url: String): HttpResponse = ??? } val adapter: WithHiveCredentialsAdapter = makeAdapter(dao, MockHttpClient) def getResults = doGetResults(adapter) getResults.isInstanceOf[ErrorResponse] should be(true) //The query shouldn't be 'done', since its status is QUEUED dao.findResultsFor(shrineNetworkQueryId).get.count.statusType should be(QueryResult.StatusType.Queued) //Now, calling processRequest (via getResults) should NOT cause the query to be re-retrieved from the CRC, because the query was previously queued val result = getResults result.isInstanceOf[ErrorResponse] should be(true) dao.findResultsFor(shrineNetworkQueryId).get.count.statusType should be(QueryResult.StatusType.Queued) } def doTestProcessRequest = afterCreatingTables { val adapter = makeAdapter(dao, MockHttpClient) def getResults = doGetResults(adapter) getResults match { case errorResponse:ErrorResponse => errorResponse.problemDigest.codec should be (classOf[QueryNotFound].getName) case x => fail(s"Got $x, not an ErrorResponse") } val dbQueryId = dao.insertQuery(localMasterId, shrineNetworkQueryId, authn, fooQuery, isFlagged = false, hasBeenRun = false, flagMessage = None) getResults match { case errorResponse:ErrorResponse => errorResponse.problemDigest.codec should be (classOf[QueryResultNotAvailable].getName) case x => fail(s"Got $x, not an ErrorResponse") } import ResultOutputType._ import XmlDateHelper.now val breakdowns = Map( PATIENT_AGE_COUNT_XML -> I2b2ResultEnvelope(PATIENT_AGE_COUNT_XML, Map("a" -> 1L, "b" -> 2L)), PATIENT_GENDER_COUNT_XML -> I2b2ResultEnvelope(PATIENT_GENDER_COUNT_XML, Map("x" -> 3L, "y" -> 4L))) val obfscBreakdowns = breakdowns.mapValues(_.mapValues(_ + 1)) val startDate = now val elapsed = 100L val endDate = { import XmlGcEnrichments._ import scala.concurrent.duration._ startDate + elapsed.milliseconds } val countResult = QueryResult( resultId = 456L, instanceId = instanceId, resultType = Some(PATIENT_COUNT_XML), setSize = setSize, startDate = Option(startDate), endDate = Option(endDate), description = Some("results from node X"), statusType = QueryResult.StatusType.Finished, statusMessage = None, breakdowns = breakdowns ) val breakdownResults = breakdowns.map { case (resultType, data) => countResult.withBreakdowns(Map(resultType -> data)).withResultType(resultType) }.toSeq val queryStartDate = now val idsByResultType = dao.insertQueryResults(dbQueryId, countResult +: breakdownResults) getResults.isInstanceOf[ErrorResponse] should be(true) dao.insertCountResult(idsByResultType(PATIENT_COUNT_XML).head, setSize, obfSetSize) dao.insertBreakdownResults(idsByResultType, breakdowns, obfscBreakdowns) val result = getResults.asInstanceOf[R] val Some((actualNetworkQueryId, actualQueryResult)) = extractor(result) actualNetworkQueryId should equal(shrineNetworkQueryId) actualQueryResult.resultType should equal(Some(PATIENT_COUNT_XML)) actualQueryResult.setSize should equal(obfSetSize) actualQueryResult.description should be(None) //TODO: This is probably wrong actualQueryResult.statusType should equal(QueryResult.StatusType.Finished) actualQueryResult.statusMessage should be(None) actualQueryResult.breakdowns should equal(obfscBreakdowns) for { startDate <- actualQueryResult.startDate endDate <- actualQueryResult.endDate } { val actualElapsed = toMillis(endDate) - toMillis(startDate) actualElapsed should equal(elapsed) } } } object AbstractQueryRetrievalTestCase { val hiveCredentials = HiveCredentials("some-hive-domain", "hive-username", "hive-password", "hive-project") val doObfuscation = true def runQueryAdapter(dao: AdapterDao, poster: Poster): RunQueryAdapter = { val translator = new QueryDefinitionTranslator(new ExpressionTranslator(Map("foo" -> Set("bar")))) new RunQueryAdapter( poster, dao, AbstractQueryRetrievalTestCase.hiveCredentials, translator, 10000, doObfuscation, runQueriesImmediately = true, DefaultBreakdownResultOutputTypes.toSet, collectAdapterAudit = false ) } import scala.concurrent.duration._ final class BogusRequest extends ShrineRequest("fooProject", 1.second, null) { override val requestType = null protected override def i2b2MessageBody: NodeSeq = override def toXml = } } \ No newline at end of file diff --git a/adapter/adapter-service/src/test/scala/net/shrine/adapter/AdapterTest.scala b/adapter/adapter-service/src/test/scala/net/shrine/adapter/AdapterTest.scala index 410c82705..47cabba6d 100644 --- a/adapter/adapter-service/src/test/scala/net/shrine/adapter/AdapterTest.scala +++ b/adapter/adapter-service/src/test/scala/net/shrine/adapter/AdapterTest.scala @@ -1,78 +1,78 @@ package net.shrine.adapter -import net.shrine.problem.ProblemNotYetEncoded +import net.shrine.problem.{ProblemNotYetEncoded, TurnOffProblemConnector} import net.shrine.protocol.query.QueryDefinition import net.shrine.util.ShouldMatchersForJUnit -import net.shrine.protocol.{RunQueryRequest, BaseShrineResponse, BroadcastMessage, DeleteQueryResponse, AuthenticationInfo, Credential, ErrorResponse, DeleteQueryRequest} +import net.shrine.protocol.{AuthenticationInfo, BaseShrineResponse, BroadcastMessage, Credential, DeleteQueryRequest, DeleteQueryResponse, ErrorResponse, RunQueryRequest} import org.junit.Test /** * @author clint * @since Mar 31, 2014 */ //noinspection UnitMethodIsParameterless -final class AdapterTest extends ShouldMatchersForJUnit { +final class AdapterTest extends ShouldMatchersForJUnit with TurnOffProblemConnector { private final class MockAdapter(toReturn: => BaseShrineResponse) extends Adapter { override protected[adapter] def processRequest(message: BroadcastMessage): BaseShrineResponse = toReturn } import scala.concurrent.duration._ private val lockedOutAuthn = AuthenticationInfo("d", "u", Credential("p", isToken = false)) private val networkAuthn = AuthenticationInfo("nd", "nu", Credential("np", isToken = false)) private val req = DeleteQueryRequest("pid", 1.second, lockedOutAuthn, 12345L) private val resp = DeleteQueryResponse(12345) @Test def testHandlesNonFailureCase: Unit = { val adapter = new MockAdapter(resp) adapter.perform(null) should equal(resp) } @Test def testHandlesLockoutCase: Unit = { doErrorResponseTest(new AdapterLockoutException(lockedOutAuthn,"test.com"),classOf[AdapterLockout]) } @Test def testHandlesCrcFailureCase: Unit = { val url = "http://example.com" doErrorResponseTest(CrcInvocationException(url, req, new Exception),classOf[CrcCouldNotBeInvoked]) } @Test def testHandlesMappingFailureCase: Unit = { val authn = AuthenticationInfo("some-domain", "some-user", Credential("some-password", isToken = false)) val projectId = "projectId" val queryDef = QueryDefinition("test query",None) val runQueryRequest = RunQueryRequest(projectId, 1.millisecond, authn, Some("topicId"), Some("Topic Name"), Set.empty, queryDef) doErrorResponseTest(new AdapterMappingException(runQueryRequest,"blarg", new Exception),classOf[AdapterMappingProblem]) } @Test def testHandlesGeneralFailureCase: Unit = { doErrorResponseTest(new Exception("blerg"),classOf[ProblemNotYetEncoded]) } //noinspection ScalaUnreachableCode,RedundantBlock private def doErrorResponseTest(exception: Throwable,problemClass:Class[_]) = { val adapter = new MockAdapter(throw exception) val response = adapter.perform(BroadcastMessage(networkAuthn, req)) response match { case errorResponse:ErrorResponse => { val pd = errorResponse.problemDigest pd.codec should be (problemClass.getName) } case x => fail(s"$x is not an ErrorResponse") } } } \ No newline at end of file diff --git a/adapter/adapter-service/src/test/scala/net/shrine/adapter/ReadQueryDefinitionAdapterTest.scala b/adapter/adapter-service/src/test/scala/net/shrine/adapter/ReadQueryDefinitionAdapterTest.scala index 3cfef55e0..9b8a9748a 100644 --- a/adapter/adapter-service/src/test/scala/net/shrine/adapter/ReadQueryDefinitionAdapterTest.scala +++ b/adapter/adapter-service/src/test/scala/net/shrine/adapter/ReadQueryDefinitionAdapterTest.scala @@ -1,83 +1,82 @@ package net.shrine.adapter import net.shrine.adapter.components.QueryNotInDatabase -import net.shrine.problem.ProblemNotYetEncoded import org.junit.Test import net.shrine.util.ShouldMatchersForJUnit import net.shrine.adapter.dao.squeryl.AbstractSquerylAdapterTest import net.shrine.protocol.AuthenticationInfo import net.shrine.protocol.BroadcastMessage import net.shrine.protocol.Credential import net.shrine.protocol.ErrorResponse import net.shrine.protocol.ReadQueryDefinitionRequest import net.shrine.protocol.ReadQueryDefinitionResponse import net.shrine.protocol.RenameQueryRequest import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol.query.Term /** * @author clint * @since Nov 28, 2012 */ final class ReadQueryDefinitionAdapterTest extends AbstractSquerylAdapterTest with AdapterTestHelpers with ShouldMatchersForJUnit { private val networkAuthn = AuthenticationInfo("network-domain", "network-user", Credential("skajdhkasjdh", isToken = false)) import scala.concurrent.duration._ @Test def testProcessRequest = afterCreatingTables { val name = "blarg" val expr = Term("foo") val fooQuery = QueryDefinition(name,expr) val adapter = new ReadQueryDefinitionAdapter(dao) //Should get error for non-existent query { val ErrorResponse(msg,problemDigest) = adapter.processRequest(BroadcastMessage(123L, networkAuthn, ReadQueryDefinitionRequest("proj", 1.second, authn, queryId))) msg should not be(null) problemDigest.codec should be (classOf[QueryNotInDatabase].getName) } //Add a query dao.insertQuery(localMasterId, queryId, authn, fooQuery, isFlagged = false, hasBeenRun = true, flagMessage = None) //sanity check that it's there { val Some(query) = dao.findQueryByNetworkId(queryId) query.networkId should equal(queryId) } { //Should still get error for non-existent query val ErrorResponse(msg, problemDigest) = adapter.processRequest(BroadcastMessage(123L, networkAuthn, ReadQueryDefinitionRequest("proj", 1.second, authn, bogusQueryId))) msg should not be(null) problemDigest.codec should be (classOf[QueryNotInDatabase].getName) } { //try to read a real query val ReadQueryDefinitionResponse(rQueryId, rName, userId, createDate, queryDefinition) = adapter.processRequest(BroadcastMessage(123L, networkAuthn, ReadQueryDefinitionRequest("proj", 1.second, authn, queryId))) rQueryId should equal(queryId) rName should equal(name) userId should equal(authn.username) createDate should not be(null) // :( queryDefinition should equal(QueryDefinition(name, expr).toI2b2String) } } @Test def testProcessRequestBadRequest { val adapter = new ReadQueryDefinitionAdapter(dao) intercept[Exception] { adapter.processRequest(BroadcastMessage(123L, networkAuthn, RenameQueryRequest("proj", 1.second, authn, queryId, "foo"))) } } } \ No newline at end of file diff --git a/adapter/adapter-service/src/test/scala/net/shrine/adapter/RunQueryAdapterTest.scala b/adapter/adapter-service/src/test/scala/net/shrine/adapter/RunQueryAdapterTest.scala index f094d5b47..73178795b 100644 --- a/adapter/adapter-service/src/test/scala/net/shrine/adapter/RunQueryAdapterTest.scala +++ b/adapter/adapter-service/src/test/scala/net/shrine/adapter/RunQueryAdapterTest.scala @@ -1,965 +1,966 @@ package net.shrine.adapter -import net.shrine.problem.{ProblemSources, AbstractProblem, TestProblem} - import scala.concurrent.duration.DurationInt import org.junit.Test import net.shrine.util.ShouldMatchersForJUnit import ObfuscatorTest.within3 import net.shrine.adapter.dao.AdapterDao import net.shrine.adapter.dao.squeryl.AbstractSquerylAdapterTest import net.shrine.adapter.translators.ExpressionTranslator import net.shrine.adapter.translators.QueryDefinitionTranslator import net.shrine.client.HttpClient import net.shrine.client.HttpResponse import net.shrine.client.Poster -import net.shrine.protocol.{HiveCredentials, AuthenticationInfo, BroadcastMessage, CrcRequest, Credential, I2b2ResultEnvelope, QueryResult, RawCrcRunQueryResponse, ReadResultRequest, ReadResultResponse, ResultOutputType, RunQueryRequest, RunQueryResponse, ErrorResponse, BaseShrineResponse, DefaultBreakdownResultOutputTypes} +import net.shrine.protocol.{AuthenticationInfo, BaseShrineResponse, BroadcastMessage, CrcRequest, Credential, DefaultBreakdownResultOutputTypes, ErrorResponse, HiveCredentials, I2b2ResultEnvelope, QueryResult, RawCrcRunQueryResponse, ReadResultRequest, ReadResultResponse, ResultOutputType, RunQueryRequest, RunQueryResponse} import net.shrine.protocol.RawCrcRunQueryResponse.toQueryResultMap import net.shrine.protocol.DefaultBreakdownResultOutputTypes.PATIENT_AGE_COUNT_XML import net.shrine.protocol.ResultOutputType.PATIENT_COUNT_XML import net.shrine.protocol.DefaultBreakdownResultOutputTypes.PATIENT_GENDER_COUNT_XML import net.shrine.protocol.DefaultBreakdownResultOutputTypes.PATIENT_RACE_COUNT_XML import net.shrine.protocol.query.OccuranceLimited import net.shrine.protocol.query.Or import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol.query.Term import net.shrine.util.XmlDateHelper import net.shrine.util.XmlUtil + import scala.util.Success import net.shrine.dao.squeryl.SquerylEntryPoint + import scala.concurrent.duration.Duration import net.shrine.adapter.dao.model.ShrineError import net.shrine.adapter.dao.model.QueryResultRow +import net.shrine.problem.TestProblem /** * @author Bill Simons * @author Clint Gilbert * @since 4/19/11 * @see http://cbmi.med.harvard.edu */ final class RunQueryAdapterTest extends AbstractSquerylAdapterTest with ShouldMatchersForJUnit { private val queryDef = QueryDefinition("foo", Term("foo")) private val broadcastMessageId = 1234563789L private val queryId = 123L private val expectedNetworkQueryId = 999L private val expectedLocalMasterId = queryId.toString private val masterId = 99L private val instanceId = 456L private val resultId = 42L private val projectId = "projectId" private val setSize = 17L private val xmlResultId = 98765L private val userId = "userId" private val groupId = "groupId" private val topicId = "some-topic-id-123-foo" private val topicName = "Topic Name" private val justCounts = Set(PATIENT_COUNT_XML) private val now = XmlDateHelper.now private val countQueryResult = QueryResult(resultId, instanceId, Some(PATIENT_COUNT_XML), setSize, Some(now), Some(now), None, QueryResult.StatusType.Finished, None) private val dummyBreakdownData = Map("x" -> 99L, "y" -> 42L, "z" -> 3000L) private val hiveCredentials = HiveCredentials("some-hive-domain", "hive-username", "hive-password", "hive-project") private val authn = AuthenticationInfo("some-domain", "username", Credential("jksafhkjaf", false)) private val adapterLockoutThreshold = 99 private val altI2b2ErrorXml = XmlUtil.stripWhitespace { 1.1 2.4 edu.harvard.i2b2.crc 1.5 i2b2 Hive i2b2_QueryTool 0.2 i2b2 Hive 1 i2b2 Log information DONE Query result instance id 3126 not found }.toString private val otherNetworkId: Long = 12345L @Test def testProcessRawCrcRunQueryResponseCountQueryOnly: Unit = afterCreatingTables{ val outputTypes = Set(PATIENT_COUNT_XML) val translator = new QueryDefinitionTranslator(new ExpressionTranslator(Map("network" -> Set("local1a", "local1b")))) val adapter = new RunQueryAdapter( Poster("crc-url", null), dao, hiveCredentials, translator, adapterLockoutThreshold, doObfuscation = false, runQueriesImmediately = true, breakdownTypes = DefaultBreakdownResultOutputTypes.toSet, collectAdapterAudit = false ) val request = RunQueryRequest(projectId, 1.second, authn, expectedNetworkQueryId, Option(topicId), Option(topicName), outputTypes, queryDef) val networkAuthn = AuthenticationInfo("some-domain", "username", Credential("sadasdasdasd", false)) val broadcastMessage = BroadcastMessage(queryId, networkAuthn, request) val rawRunQueryResponse = RawCrcRunQueryResponse( queryId = queryId, createDate = XmlDateHelper.now, userId = request.authn.username, groupId = request.authn.domain, requestXml = request.queryDefinition, queryInstanceId = otherNetworkId, singleNodeResults = toQueryResultMap(Seq(countQueryResult))) val resp = adapter.processRawCrcRunQueryResponse(networkAuthn, request, rawRunQueryResponse).asInstanceOf[RunQueryResponse] resp should not be (null) //Validate the response resp.createDate should not be(null) resp.groupId should be(request.authn.domain) resp.userId should be(request.authn.username) resp.queryId should be(queryId) resp.queryInstanceId should be(otherNetworkId) resp.requestXml should equal(request.queryDefinition) (countQueryResult eq resp.singleNodeResult) should be(false) within3(resp.singleNodeResult.setSize, countQueryResult.setSize) should be(true) resp.singleNodeResult.resultType.get should equal(PATIENT_COUNT_XML) resp.singleNodeResult.breakdowns should equal(Map.empty) //validate the DB val expectedNetworkTerm = queryDef.expr.get.asInstanceOf[Term] //We should have one row in the shrine_query table, for the query just performed val Seq(queryRow) = list(queryRows) { queryRow.dateCreated should not be (null) queryRow.domain should equal(request.authn.domain) queryRow.name should equal(queryDef.name) queryRow.localId should equal(expectedLocalMasterId) queryRow.networkId should equal(expectedNetworkQueryId) queryRow.username should equal(authn.username) queryRow.queryDefinition.expr.get should equal(expectedNetworkTerm) } //We should have one row in the count_result table, with the right obfuscated value, which is within the expected amount from the original count val Seq(countRow) = list(countResultRows) { countRow.creationDate should not be (null) countRow.originalValue should equal(countQueryResult.setSize) within3(countRow.obfuscatedValue, countRow.originalValue) should be(true) } } @Test def testProcessRawCrcRunQueryResponseCountAndBreakdownQuery: Unit = afterCreatingTables { val allBreakdownTypes = DefaultBreakdownResultOutputTypes.toSet val breakdownTypes = Seq(PATIENT_GENDER_COUNT_XML) val outputTypes = Set(PATIENT_COUNT_XML) ++ breakdownTypes val translator = new QueryDefinitionTranslator(new ExpressionTranslator(Map("network" -> Set("local1a", "local1b")))) val request = RunQueryRequest(projectId, 1.second, authn, expectedNetworkQueryId, Option(topicId), Option(topicName), outputTypes, queryDef) val networkAuthn = AuthenticationInfo("some-domain", "username", Credential("sadasdasdasd", false)) val broadcastMessage = BroadcastMessage(queryId, networkAuthn, request) val breakdownQueryResults = breakdownTypes.zipWithIndex.map { case (rt, i) => countQueryResult.withId(resultId + i + 1).withResultType(rt) } val singleNodeResults = toQueryResultMap(countQueryResult +: breakdownQueryResults) val rawRunQueryResponse = RawCrcRunQueryResponse( queryId = queryId, createDate = XmlDateHelper.now, userId = request.authn.username, groupId = request.authn.domain, requestXml = request.queryDefinition, queryInstanceId = otherNetworkId, singleNodeResults = singleNodeResults) //Set up our mock CRC val poster = Poster("crc-url", new HttpClient { def post(input: String, url: String): HttpResponse = HttpResponse.ok { (RunQueryRequest.fromI2b2String(allBreakdownTypes)(input) orElse ReadResultRequest.fromI2b2String(allBreakdownTypes)(input)).get match { case runQueryReq: RunQueryRequest => rawRunQueryResponse.toI2b2String case readResultReq: ReadResultRequest => ReadResultResponse(xmlResultId = 42L, metadata = breakdownQueryResults.head, data = I2b2ResultEnvelope(PATIENT_GENDER_COUNT_XML, dummyBreakdownData)).toI2b2String case _ => sys.error(s"Unknown request: '$input'") //Fail loudly } } }) val adapter = new RunQueryAdapter( poster, dao, hiveCredentials, translator, adapterLockoutThreshold, doObfuscation = false, runQueriesImmediately = true, breakdownTypes = DefaultBreakdownResultOutputTypes.toSet, collectAdapterAudit = false ) val resp = adapter.processRawCrcRunQueryResponse(networkAuthn, request, rawRunQueryResponse).asInstanceOf[RunQueryResponse] resp should not be (null) //Validate the response resp.createDate should not be(null) resp.groupId should be(request.authn.domain) resp.userId should be(request.authn.username) resp.queryId should be(queryId) resp.queryInstanceId should be(otherNetworkId) resp.requestXml should equal(request.queryDefinition) (countQueryResult eq resp.singleNodeResult) should be(false) within3(resp.singleNodeResult.setSize, countQueryResult.setSize) should be(true) resp.singleNodeResult.resultType.get should equal(PATIENT_COUNT_XML) resp.singleNodeResult.breakdowns.keySet should equal(Set(PATIENT_GENDER_COUNT_XML)) val breakdownEnvelope = resp.singleNodeResult.breakdowns.values.head breakdownEnvelope.resultType should equal(PATIENT_GENDER_COUNT_XML) breakdownEnvelope.data.keySet should equal(dummyBreakdownData.keySet) //All breakdowns are obfuscated for { (key, value) <- breakdownEnvelope.data } { within3(value, dummyBreakdownData(key)) should be(true) } //validate the DB val expectedNetworkTerm = queryDef.expr.get.asInstanceOf[Term] //We should have one row in the shrine_query table, for the query just performed val Seq(queryRow) = list(queryRows) { queryRow.dateCreated should not be (null) queryRow.domain should equal(request.authn.domain) queryRow.name should equal(queryDef.name) queryRow.localId should equal(expectedLocalMasterId) queryRow.networkId should equal(expectedNetworkQueryId) queryRow.username should equal(authn.username) queryRow.queryDefinition.expr.get should equal(expectedNetworkTerm) } //We should have one row in the count_result table, with the right obfuscated value, which is within the expected amount from the original count val Seq(countRow) = list(countResultRows) { countRow.creationDate should not be (null) countRow.originalValue should equal(countQueryResult.setSize) within3(countRow.obfuscatedValue, countRow.originalValue) should be(true) } val breakdownRows @ Seq(xRow, yRow, zRow) = list(breakdownResultRows) breakdownRows.map(_.dataKey).toSet should equal(dummyBreakdownData.keySet) within3(xRow.obfuscatedValue, xRow.originalValue) should be(true) xRow.originalValue should be(dummyBreakdownData(xRow.dataKey)) within3(yRow.obfuscatedValue, yRow.originalValue) should be(true) yRow.originalValue should be(dummyBreakdownData(yRow.dataKey)) within3(zRow.obfuscatedValue, zRow.originalValue) should be(true) zRow.originalValue should be(dummyBreakdownData(zRow.dataKey)) } //NB: See https://open.med.harvard.edu/jira/browse/SHRINE-745 @Test def testParseAltErrorXml { val adapter = new RunQueryAdapter( Poster("crc-url", null), null, hiveCredentials, null, adapterLockoutThreshold, doObfuscation = false, runQueriesImmediately = false, DefaultBreakdownResultOutputTypes.toSet, collectAdapterAudit = false ) val resp: ErrorResponse = adapter.parseShrineErrorResponseWithFallback(altI2b2ErrorXml).asInstanceOf[ErrorResponse] resp should not be (null) resp.errorMessage should be("Query result instance id 3126 not found") } @Test def testParseErrorXml { val xml = { 1.1 2.4 edu.harvard.i2b2.crc 1.4 i2b2 Hive i2b2web 1.4 i2b2 Hive 1 Demo Log information Message error connecting Project Management cell admin 0 0 CRC_QRY_runQueryInstance_fromQueryDefinition Age 0 1 0 0 1 2 Age \\i2b2\i2b2\Demographics\Age\ concept_dimension concept_path \i2b2\Demographics\Age\ T concept_cd false }.toString val adapter = new RunQueryAdapter( Poster("crc-url", null), null, hiveCredentials, null, adapterLockoutThreshold, doObfuscation = false, runQueriesImmediately = true, DefaultBreakdownResultOutputTypes.toSet, collectAdapterAudit = false ) val resp = adapter.parseShrineErrorResponseWithFallback(xml).asInstanceOf[ErrorResponse] resp should not be (null) resp.errorMessage should not be ("") } @Test def testObfuscateBreakdowns { val breakdown1 = I2b2ResultEnvelope(PATIENT_AGE_COUNT_XML, Map.empty) val breakdown2 = I2b2ResultEnvelope(PATIENT_GENDER_COUNT_XML, Map("foo" -> 123, "bar" -> 345)) val breakdown3 = I2b2ResultEnvelope(PATIENT_RACE_COUNT_XML, Map("x" -> 999, "y" -> 888)) val original = Map.empty ++ Seq(breakdown1, breakdown2, breakdown3).map(env => (env.resultType, env)) val obfuscated = RunQueryAdapter.obfuscateBreakdowns(original) original.keySet should equal(obfuscated.keySet) original.keySet.forall(resultType => original(resultType).data.keySet == obfuscated(resultType).data.keySet) should be(true) val localTerms = Set("local1a", "local1b") for { (resultType, origBreakdown) <- original mappings = Map("network" -> localTerms) translator = new QueryDefinitionTranslator(new ExpressionTranslator(mappings)) obfscBreakdown <- obfuscated.get(resultType) key <- origBreakdown.data.keySet } { (origBreakdown eq obfscBreakdown) should be(false) ObfuscatorTest.within3(origBreakdown.data(key), obfscBreakdown.data(key)) should be(true) } } @Test def testTranslateNetworkToLocalDoesntLeakCredentialsViaException: Unit = { val mappings = Map.empty[String, Set[String]] val translator = new QueryDefinitionTranslator(new ExpressionTranslator(mappings)) val adapter = new RunQueryAdapter( Poster("crc-url", MockHttpClient), null, null, translator, adapterLockoutThreshold, doObfuscation = false, runQueriesImmediately = true, DefaultBreakdownResultOutputTypes.toSet, collectAdapterAudit = false ) val queryDefinition = QueryDefinition("foo", Term("blah")) val authn = AuthenticationInfo("d", "u", Credential("p", false)) val req = RunQueryRequest("projectId", Duration.Inf, authn, otherNetworkId, None, None, Set.empty, queryDef) try { adapter.translateNetworkToLocal(req) fail("Expected an AdapterMappingException") } catch { case e: AdapterMappingException => { e.getMessage.contains(authn.rawToString) should be(false) e.getMessage.contains(AuthenticationInfo.elided.toString) should be(true) } } } @Test def testTranslateQueryDefinitionXml { val localTerms = Set("local1a", "local1b") val mappings = Map("network" -> localTerms) val translator = new QueryDefinitionTranslator(new ExpressionTranslator(mappings)) val adapter = new RunQueryAdapter( Poster("crc-url", MockHttpClient), null, null, translator, adapterLockoutThreshold, doObfuscation = false, runQueriesImmediately = true, DefaultBreakdownResultOutputTypes.toSet, collectAdapterAudit = false ) val queryDefinition = QueryDefinition("10-17 years old@14:39:20", OccuranceLimited(1, Term("network"))) val newDef = adapter.conceptTranslator.translate(queryDefinition) val expected = QueryDefinition("10-17 years old@14:39:20", Or(Term("local1a"), Term("local1b"))) newDef should equal(expected) } @Test def testQueuedRegularCountQuery: Unit = afterCreatingTables { val adapter = RunQueryAdapter( Poster("crc-url", MockHttpClient), dao, null, null, adapterLockoutThreshold, doObfuscation = false, runQueriesImmediately = false, DefaultBreakdownResultOutputTypes.toSet, collectAdapterAudit = false ) val networkAuthn = AuthenticationInfo("nd", "nu", Credential("np", false)) import scala.concurrent.duration._ val req = RunQueryRequest(projectId, 1.second, authn, expectedNetworkQueryId, Option(topicId), Option(topicName), Set(PATIENT_COUNT_XML), queryDef) val broadcastMessage = BroadcastMessage(queryId, networkAuthn, req) val resp = adapter.processRequest(broadcastMessage).asInstanceOf[RunQueryResponse] resp.groupId should equal(networkAuthn.domain) resp.createDate should not be (null) // :\ resp.queryId should equal(-1L) resp.queryInstanceId should equal(-1L) resp.requestXml should equal(queryDef) resp.userId should equal(networkAuthn.username) resp.singleNodeResult.breakdowns should equal(Map.empty) resp.singleNodeResult.description.isDefined should be(true) resp.singleNodeResult.elapsed should equal(Some(0L)) resp.singleNodeResult.endDate.isDefined should be(true) resp.singleNodeResult.startDate.isDefined should be(true) resp.singleNodeResult.instanceId should equal(-1L) resp.singleNodeResult.isError should be(false) resp.singleNodeResult.resultId should equal(-1L) resp.singleNodeResult.resultType should be(Some(PATIENT_COUNT_XML)) resp.singleNodeResult.setSize should equal(-1L) resp.singleNodeResult.statusMessage.isDefined should be(true) resp.singleNodeResult.statusType should be(QueryResult.StatusType.Held) resp.singleNodeResult.endDate.isDefined should be(true) val Some(storedQuery) = dao.findQueryByNetworkId(expectedNetworkQueryId) storedQuery.dateCreated should not be (null) // :\ storedQuery.domain should equal(networkAuthn.domain) storedQuery.isFlagged should equal(false) storedQuery.localId should equal(-1L.toString) storedQuery.name should equal(queryDef.name) storedQuery.networkId should equal(expectedNetworkQueryId) storedQuery.queryDefinition should equal(queryDef) storedQuery.username should equal(networkAuthn.username) } private def doTestRegularCountQuery(status: QueryResult.StatusType, count: Long) = afterCreatingTables { require(!status.isError) val countQueryResultToUse = countQueryResult.copy(statusType = status, setSize = count) val outputTypes = justCounts val resp = doQuery(outputTypes) { import RawCrcRunQueryResponse.toQueryResultMap RawCrcRunQueryResponse(queryId, now, userId, groupId, queryDef, instanceId, toQueryResultMap(Seq(countQueryResultToUse))).toI2b2String }.asInstanceOf[RunQueryResponse] doBasicRunQueryResponseTest(resp) val firstResult = resp.results.head resp.results should equal(Seq(firstResult)) val Some(savedQuery) = dao.findResultsFor(expectedNetworkQueryId) savedQuery.wasRun should equal(true) savedQuery.isFlagged should equal(false) savedQuery.networkQueryId should equal(expectedNetworkQueryId) savedQuery.breakdowns should equal(Nil) savedQuery.count.creationDate should not be (null) savedQuery.count.localId should equal(countQueryResultToUse.resultId) //savedQuery.count.resultId should equal(resultId) TODO: REVISIT savedQuery.count.statusType should equal(status) if (status.isDone && !status.isError) { savedQuery.count.data.get.startDate should not be (null) savedQuery.count.data.get.endDate should not be (null) savedQuery.count.data.get.originalValue should be(count) ObfuscatorTest.within3(savedQuery.count.data.get.obfuscatedValue, count) should be(true) } else { savedQuery.count.data should be(None) } } @Test def testRegularCountQuery = doTestRegularCountQuery(QueryResult.StatusType.Finished, countQueryResult.setSize) @Test def testRegularCountQueryComesBackProcessing = doTestRegularCountQuery(QueryResult.StatusType.Processing, -1L) @Test def testRegularCountQueryComesBackQueued = doTestRegularCountQuery(QueryResult.StatusType.Queued, -1L) @Test def testRegularCountQueryComesBackError = afterCreatingTables { val errorQueryResult = QueryResult.errorResult(Some("some-description"), "some-status-message",TestProblem()) val outputTypes = justCounts val resp = doQuery(outputTypes) { import RawCrcRunQueryResponse.toQueryResultMap RawCrcRunQueryResponse(queryId, now, userId, groupId, queryDef, instanceId, toQueryResultMap(Seq(errorQueryResult))).toI2b2String } doBasicRunQueryResponseTest(resp) //TODO: Why are status and description messages from CRC dropped when unmarshalling QueryResults? //resp.results should equal(Seq(errorQueryResult)) resp.asInstanceOf[RunQueryResponse].results.head.statusType should be(QueryResult.StatusType.Error) dao.findResultsFor(expectedNetworkQueryId) should be(None) val Some(savedQueryRow) = dao.findQueryByNetworkId(expectedNetworkQueryId) val Seq(queryResultRow: QueryResultRow) = { import SquerylEntryPoint._ implicit val breakdownTypes = DefaultBreakdownResultOutputTypes.toSet inTransaction { from(tables.queryResults) { row => where(row.queryId === savedQueryRow.id). select(row.toQueryResultRow) }.toSeq } } val Seq(errorRow: ShrineError) = { import SquerylEntryPoint._ inTransaction { from(tables.errorResults) { row => where(row.resultId === queryResultRow.id). select(row.toShrineError) }.toSeq } } errorRow should not be (null) //TODO: ErrorMessage //errorRow.message should equal(errorQueryResult.statusMessage) } private def doTestBreakdownsAreObfuscated(result: QueryResult): Unit = { result.breakdowns.values.map(_.data).foreach { actualBreakdowns => actualBreakdowns.keySet should equal(dummyBreakdownData.keySet) for { breakdownName <- actualBreakdowns.keySet } { within3(actualBreakdowns(breakdownName), dummyBreakdownData(breakdownName)) should be(true) } } } @Test def testGetBreakdownsWithRegularCountQuery { val breakdowns = DefaultBreakdownResultOutputTypes.values.map(breakdownFor) val resp = doTestGetBreakdowns(breakdowns) val firstResult = resp.results.head firstResult.resultType should equal(Some(PATIENT_COUNT_XML)) firstResult.setSize should equal(setSize) firstResult.description should equal(None) firstResult.breakdowns.keySet should equal(DefaultBreakdownResultOutputTypes.toSet) //NB: Verify that breakdowns are obfuscated doTestBreakdownsAreObfuscated(firstResult) resp.results.size should equal(1) } @Test def testGetBreakdownsSomeFailures { val resultTypesExpectedToSucceed = Seq(PATIENT_AGE_COUNT_XML, PATIENT_GENDER_COUNT_XML) val breakdowns = resultTypesExpectedToSucceed.map(breakdownFor) val resp = doTestGetBreakdowns(breakdowns) val firstResult = resp.results.head firstResult.resultType should equal(Some(PATIENT_COUNT_XML)) firstResult.setSize should equal(setSize) firstResult.description should equal(None) firstResult.breakdowns.keySet should equal(resultTypesExpectedToSucceed.toSet) //NB: Verify that breakdowns are obfuscated doTestBreakdownsAreObfuscated(firstResult) resp.results.size should equal(1) } @Test def testErrorResponsesArePassedThrough: Unit = { val errorResponse = ErrorResponse(TestProblem(summary = "blarg!")) val resp = doQuery(Set(PATIENT_COUNT_XML)) { errorResponse.toI2b2String } resp should equal(errorResponse) } private def breakdownFor(resultType: ResultOutputType) = I2b2ResultEnvelope(resultType, dummyBreakdownData) private def doTestGetBreakdowns(successfulBreakdowns: Seq[I2b2ResultEnvelope]): RunQueryResponse = { val outputTypes = justCounts ++ DefaultBreakdownResultOutputTypes.toSet val resp = doQueryThatReturnsSpecifiedBreakdowns(outputTypes, successfulBreakdowns) doBasicRunQueryResponseTest(resp) resp } private def doBasicRunQueryResponseTest(r: BaseShrineResponse) { val resp = r.asInstanceOf[RunQueryResponse] resp.createDate should equal(now) resp.groupId should equal(groupId) resp.queryId should equal(queryId) resp.queryInstanceId should equal(instanceId) resp.queryName should equal(queryDef.name) resp.requestXml should equal(queryDef) } private def doQueryThatReturnsSpecifiedBreakdowns(outputTypes: Set[ResultOutputType], successfulBreakdowns: Seq[I2b2ResultEnvelope]): RunQueryResponse = afterCreatingTablesReturn { val breakdownQueryResults = DefaultBreakdownResultOutputTypes.values.zipWithIndex.map { case (rt, i) => countQueryResult.withId(resultId + i + 1).withResultType(rt) } //Need this rigamarole to ensure that resultIds line up such that the type of breakdown the adapter asks for //(PATIENT_AGE_COUNT_XML, etc) is what the mock HttpClient actually returns. Here, we build up maps of QueryResults //and I2b2ResultEnvelopes, keyed on resultIds generated in the previous expression, to use to look up values to use //to build ReadResultResponses val successfulBreakdownsByType = successfulBreakdowns.map(e => e.resultType -> e).toMap val successfulBreakdownTypes = successfulBreakdownsByType.keySet val breakdownQueryResultsByResultId = breakdownQueryResults.collect { case qr if successfulBreakdownTypes(qr.resultType.get) => qr.resultId -> qr }.toMap val breakdownsToBeReturnedByResultId = breakdownQueryResultsByResultId.map { case (resultId, queryResult) => (resultId, successfulBreakdownsByType(queryResult.resultType.get)) } val expectedLocalTerm = Term("bar") val httpClient = new HttpClient { override def post(input: String, url: String): HttpResponse = { val resp = CrcRequest.fromI2b2String(DefaultBreakdownResultOutputTypes.toSet)(input) match { case Success(req: RunQueryRequest) => { //NB: Terms should be translated req.queryDefinition.expr.get should equal(expectedLocalTerm) //Credentials should be "translated" req.authn.username should equal(hiveCredentials.username) req.authn.domain should equal(hiveCredentials.domain) //I2b2 Project ID should be translated req.projectId should equal(hiveCredentials.projectId) val queryResultMap = RawCrcRunQueryResponse.toQueryResultMap(countQueryResult +: breakdownQueryResults) RawCrcRunQueryResponse(queryId, now, "userId", "groupId", queryDef, instanceId, queryResultMap) } //NB: return a ReadResultResponse with new breakdown data each time, but will throw if the asked-for breakdown //is not one of the ones passed to the enclosing method, simulating an error calling the CRC case Success(req: ReadResultRequest) => { val resultId = req.localResultId.toLong ReadResultResponse(xmlResultId, breakdownQueryResultsByResultId(resultId), breakdownsToBeReturnedByResultId(resultId)) } case _ => ??? //fail loudly } HttpResponse.ok(resp.toI2b2String) } } val result = doQuery(outputTypes, dao, httpClient) validateDb(successfulBreakdowns, breakdownQueryResultsByResultId) result.asInstanceOf[RunQueryResponse] } private def validateDb(breakdownsReturned: Seq[I2b2ResultEnvelope], breakdownQueryResultsByResultId: Map[Long, QueryResult]) { val expectedNetworkTerm = Term("foo") //We should have one row in the shrine_query table, for the query just performed val queryRow = first(queryRows) { queryRow.dateCreated should not be (null) queryRow.domain should equal(authn.domain) queryRow.name should equal(queryDef.name) queryRow.localId should equal(expectedLocalMasterId) queryRow.networkId should equal(expectedNetworkQueryId) queryRow.username should equal(authn.username) queryRow.queryDefinition.expr.get should equal(expectedNetworkTerm) } list(queryRows).size should equal(1) //We should have one row in the count_result table, with the right obfuscated value, which is within the expected amount from the original count val countRow = first(countResultRows) { countRow.creationDate should not be (null) countRow.originalValue should equal(countQueryResult.setSize) within3(countRow.obfuscatedValue, countQueryResult.setSize) should be(true) within3(countRow.obfuscatedValue, countRow.originalValue) should be(true) } list(countResultRows).size should equal(1) //We should have 5 rows in the query_result table, one for the count result and one for each of the 4 requested breakdown types val queryResults = list(queryResultRows) { val countQueryResultRow = queryResults.find(_.resultType == PATIENT_COUNT_XML).get countQueryResultRow.localId should equal(countQueryResult.resultId) countQueryResultRow.queryId should equal(queryRow.id) val resultIdsByResultType = breakdownQueryResultsByResultId.map { case (resultId, queryResult) => queryResult.resultType.get -> resultId }.toMap for (breakdownType <- DefaultBreakdownResultOutputTypes.values) { val breakdownQueryResultRow = queryResults.find(_.resultType == breakdownType).get breakdownQueryResultRow.queryId should equal(queryRow.id) //We'll have a result id if this breakdown type didn't fail if (resultIdsByResultType.contains(breakdownQueryResultRow.resultType)) { breakdownQueryResultRow.localId should equal(resultIdsByResultType(breakdownQueryResultRow.resultType)) } } } queryResults.size should equal(5) val returnedBreakdownTypes = breakdownsReturned.map(_.resultType).toSet val notReturnedBreakdownTypes = DefaultBreakdownResultOutputTypes.toSet -- returnedBreakdownTypes val errorResults = list(errorResultRows) //We should have a row in the error_result table for each breakdown that COULD NOT be retrieved { for { queryResult <- queryResults if notReturnedBreakdownTypes.contains(queryResult.resultType) resultType = queryResult.resultType resultId = queryResult.id } { errorResults.find(_.resultId == resultId).isDefined should be(true) } } errorResults.size should equal(notReturnedBreakdownTypes.size) //We should have properly-obfuscated rows in the breakdown_result table for each of the breakdown types that COULD be retrieved val breakdownResults = list(breakdownResultRows) val bdrs = breakdownResults.toIndexedSeq { for { queryResult <- queryResults if returnedBreakdownTypes.contains(queryResult.resultType) resultType = queryResult.resultType resultId = queryResult.id } { //Find all the rows for a particular breakdown type val rowsWithType = breakdownResults.filter(_.resultId == resultId) //Combining the rows should give the expected dummy data rowsWithType.map(row => row.dataKey -> row.originalValue).toMap should equal(dummyBreakdownData) for (breakdownRow <- rowsWithType) { within3(breakdownRow.obfuscatedValue, dummyBreakdownData(breakdownRow.dataKey)) should be(true) } } } } private def doQuery(outputTypes: Set[ResultOutputType])(i2b2XmlToReturn: => String): BaseShrineResponse = { doQuery(outputTypes, dao, MockHttpClient(i2b2XmlToReturn)) } private def doQuery(outputTypes: Set[ResultOutputType], adapterDao: AdapterDao, httpClient: HttpClient): BaseShrineResponse = { val translator = new QueryDefinitionTranslator(new ExpressionTranslator(Map("foo" -> Set("bar")))) //NB: Don't obfuscate, for simpler testing val adapter = new RunQueryAdapter( Poster("crc-url", httpClient), adapterDao, hiveCredentials, translator, adapterLockoutThreshold, doObfuscation = false, runQueriesImmediately = true, DefaultBreakdownResultOutputTypes.toSet, collectAdapterAudit = false ) import scala.concurrent.duration._ val req = RunQueryRequest(projectId, 1.second, authn, expectedNetworkQueryId, Option(topicId), Option(topicName), outputTypes, queryDef) val networkAuthn = AuthenticationInfo("some-domain", "username", Credential("sadasdasdasd", false)) val broadcastMessage = BroadcastMessage(queryId, networkAuthn, req) adapter.processRequest(broadcastMessage) } } \ No newline at end of file diff --git a/adapter/adapter-service/src/test/scala/net/shrine/adapter/dao/model/ShrineErrorTest.scala b/adapter/adapter-service/src/test/scala/net/shrine/adapter/dao/model/ShrineErrorTest.scala index 49182a56e..e39ab7c1f 100644 --- a/adapter/adapter-service/src/test/scala/net/shrine/adapter/dao/model/ShrineErrorTest.scala +++ b/adapter/adapter-service/src/test/scala/net/shrine/adapter/dao/model/ShrineErrorTest.scala @@ -1,20 +1,20 @@ package net.shrine.adapter.dao.model -import net.shrine.util.ShouldMatchersForJUnit import net.shrine.problem.TestProblem +import net.shrine.util.ShouldMatchersForJUnit import net.shrine.protocol.QueryResult /** * @author clint * @since Nov 1, 2012 */ final class ShrineErrorTest extends ShouldMatchersForJUnit { def testToQueryResult() { val message = "something broke" val testProblem = TestProblem() val tpd = testProblem.toDigest val error = ShrineError(1, 123, message,tpd.codec,tpd.stampText,tpd.summary,tpd.description,tpd.detailsXml) error.toQueryResult should equal(QueryResult.errorResult(Some(message), QueryResult.StatusType.Error.name,testProblem)) } } \ No newline at end of file diff --git a/adapter/adapter-service/src/test/scala/net/shrine/adapter/dao/squeryl/SquerylAdapterDaoTest.scala b/adapter/adapter-service/src/test/scala/net/shrine/adapter/dao/squeryl/SquerylAdapterDaoTest.scala index b882d394b..b71d37932 100644 --- a/adapter/adapter-service/src/test/scala/net/shrine/adapter/dao/squeryl/SquerylAdapterDaoTest.scala +++ b/adapter/adapter-service/src/test/scala/net/shrine/adapter/dao/squeryl/SquerylAdapterDaoTest.scala @@ -1,669 +1,669 @@ package net.shrine.adapter.dao.squeryl -import net.shrine.problem.TestProblem import net.shrine.util.ShouldMatchersForJUnit import net.shrine.protocol.AuthenticationInfo import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol.Credential import net.shrine.util.XmlDateHelper import net.shrine.protocol.query.Term import net.shrine.protocol.ResultOutputType import net.shrine.protocol.I2b2ResultEnvelope import net.shrine.protocol.QueryResult import net.shrine.protocol.RawCrcRunQueryResponse import org.junit.Test import net.shrine.adapter.dao.model.ShrineQuery import net.shrine.adapter.dao.model.ShrineQueryResult import net.shrine.adapter.dao.model.ObfuscatedPair import net.shrine.adapter.dao.model.QueryResultRow import net.shrine.protocol.DefaultBreakdownResultOutputTypes import net.shrine.adapter.dao.model.squeryl.SquerylShrineQuery import net.shrine.dao.DateHelpers +import net.shrine.problem.TestProblem /** * @author clint * @since May 24, 2013 */ final class SquerylAdapterDaoTest extends AbstractSquerylAdapterTest with ShouldMatchersForJUnit { private val authn = AuthenticationInfo("some-domain", "some-user", Credential("laskhdakslhd", false)) private val queryDef1 = QueryDefinition("foo", Term("blarg")) private def now = Option(XmlDateHelper.now) import ResultOutputType._ import DefaultBreakdownResultOutputTypes._ private val topicId = "some-topic-id" private val count = 999L private val resultId = 1L private val instanceId = 2L private val desc = Some("xyz") private val message1 = "something bad happened" private val message2 = "blah blah blah" private val onlyAgeBreakdown = Map(PATIENT_AGE_COUNT_XML -> I2b2ResultEnvelope(PATIENT_AGE_COUNT_XML, Map("x" -> 1, "y" -> 2))) private val onlyGenderBreakdown = Map(PATIENT_GENDER_COUNT_XML -> I2b2ResultEnvelope(PATIENT_GENDER_COUNT_XML, Map("a" -> 123, "b" -> 456))) private val breakdownsByType = onlyAgeBreakdown ++ onlyGenderBreakdown private val networkQueryId1 = 123L private val networkQueryId2 = 456L private val networkQueryId3 = 999L private val masterId1 = "abc" private val masterId2 = "def" private val masterId3 = "ghi" private val countQueryResult = QueryResult(resultId, instanceId, Some(PATIENT_COUNT_XML), count, now, now, desc, QueryResult.StatusType.Finished, None) private val errorQueryResult1 = QueryResult.errorResult(desc, message1,TestProblem()) private val errorQueryResult2 = QueryResult.errorResult(desc, message2,TestProblem()) private val breakdownQueryResult1 = QueryResult(resultId, instanceId, Some(PATIENT_AGE_COUNT_XML), countQueryResult.setSize, now, now, desc, QueryResult.StatusType.Finished, None, breakdowns = onlyAgeBreakdown) private val breakdownQueryResult2 = QueryResult(resultId, instanceId, Some(PATIENT_GENDER_COUNT_XML), countQueryResult.setSize, now, now, desc, QueryResult.StatusType.Finished, None, breakdowns = onlyGenderBreakdown) import RawCrcRunQueryResponse.toQueryResultMap private val countRunQueryResponse = RawCrcRunQueryResponse(networkQueryId1, now.get, authn.username, authn.domain, queryDef1, instanceId, toQueryResultMap(Seq(countQueryResult))) private val onlyErrorsRunQueryResponse = countRunQueryResponse.withResults(Seq(errorQueryResult1, errorQueryResult2)) private val countAndBreakdownsRunQueryResponse = countRunQueryResponse.withResults(Seq(countQueryResult, breakdownQueryResult1, breakdownQueryResult2)) private val onlyBreakdownsRunQueryResponse = countRunQueryResponse.withResults(Seq(breakdownQueryResult1, breakdownQueryResult2)) @Test def testFindQueryByNetworkIdNullQueryDefNonNullQueryExpr: Unit = afterCreatingTables { val id = 123 val localId = "localId" val networkQueryId = 42L val queryName = "some-query" val user = "u" val domain = "d" val expr = Term("""\\FOO\BAR\BAZ""") val dateCreated = XmlDateHelper.now val isFlagged = true val flagMessage = Some("fm") val hasBeenRun = true val squerylShrineQuery = SquerylShrineQuery( id, localId, networkQueryId, queryName, user, domain, Some(expr.toXmlString), DateHelpers.toTimestamp(dateCreated), isFlagged, flagMessage, hasBeenRun, queryXml = None) tables.shrineQueries.insert(squerylShrineQuery) val shrineQuery = squerylShrineQuery.toShrineQuery shrineQuery.dateCreated should equal(dateCreated) shrineQuery.domain should equal(domain) shrineQuery.username should equal(user) shrineQuery.flagMessage should equal(flagMessage) shrineQuery.id should equal(1) shrineQuery.isFlagged should equal(isFlagged) shrineQuery.localId should equal(localId) shrineQuery.name should equal(queryName) shrineQuery.networkId should equal(networkQueryId) shrineQuery.queryDefinition should equal(QueryDefinition(queryName, expr)) val fromDb = dao.findQueryByNetworkId(networkQueryId).get fromDb should equal(shrineQuery) } @Test def testFindPreviousQueries = afterCreatingTables { dao.findRecentQueries(0) should equal(Nil) dao.findRecentQueries(1) should equal(Nil) dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) //Make next query happen 10 milliseconds later, so we can distinguish it from the one we just inserted //(java.sql.Timestamps have 1ms resolution, it appears?) Thread.sleep(10) dao.findRecentQueries(0) should equal(Nil) { val Seq(query) = dao.findRecentQueries(1) query.networkId should equal(networkQueryId1) } dao.insertQuery(masterId2, networkQueryId2, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) //Make next query happen 10 milliseconds later, so we can distinguish it from the one we just inserted //(java.sql.Timestamps, or perhaps XmlGregorianCalendars, have 1ms resolution?) Thread.sleep(10) dao.insertQuery(masterId3, networkQueryId3, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) //Should come back newest-queries-first dao.findRecentQueries(2).map(_.networkId) should equal(Seq(networkQueryId3, networkQueryId2)) } @Test def testDeleteQuery = afterCreatingTables { //A RunQueryResponse with all types of QueryResults: count, breakdown, and error val response = countRunQueryResponse.withResults(Seq(countQueryResult, breakdownQueryResult1, breakdownQueryResult2, errorQueryResult1, errorQueryResult2)) doTestFindResultsFor(response) { resultIdsByType => insertCount(resultIdsByType) insertErrors(resultIdsByType) insertBreakdowns(resultIdsByType, breakdownsByType) } { (resultIdsByType, resultOption) => val Some(result) = resultOption validateCount(resultIdsByType, result) validateBreakdowns(result) validateErrors(result) } dao.deleteQuery(networkQueryId1) dao.findQueryByNetworkId(networkQueryId1) should be(None) } @Test def testRenameQuery = afterCreatingTables { dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) { val Some(query) = dao.findQueryByNetworkId(networkQueryId1) query.name should equal(queryDef1.name) } val newName = "zuh" dao.renameQuery(networkQueryId1, newName) val Some(renamedQuery) = dao.findQueryByNetworkId(networkQueryId1) renamedQuery.name should equal(newName) renamedQuery.id should be(1) renamedQuery.networkId should equal(networkQueryId1) renamedQuery.username should equal(authn.username) renamedQuery.domain should equal(authn.domain) renamedQuery.dateCreated should not be (null) renamedQuery.queryDefinition.name should not equal(queryDef1.name) renamedQuery.queryDefinition.expr should equal(queryDef1.expr) renamedQuery.queryDefinition.timing should equal(queryDef1.timing) renamedQuery.queryDefinition.id should equal(queryDef1.id) renamedQuery.queryDefinition.queryType should equal(queryDef1.queryType) renamedQuery.queryDefinition.constraints should equal(queryDef1.constraints) renamedQuery.queryDefinition.subQueries should equal(queryDef1.subQueries) renamedQuery.isFlagged should be(false) renamedQuery.queryDefinition should not equal(queryDef1) } @Test def testFlagQuery = afterCreatingTables { dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) { val Some(query) = dao.findQueryByNetworkId(networkQueryId1) query.isFlagged should be(false) } dao.flagQuery(networkQueryId1, None) { val Some(flaggedQuery) = dao.findQueryByNetworkId(networkQueryId1) flaggedQuery.isFlagged should be(true) flaggedQuery.flagMessage should be(None) } val message = "askldjjkalshd" dao.flagQuery(networkQueryId1, Some(message)) { val Some(flaggedQuery) = dao.findQueryByNetworkId(networkQueryId1) flaggedQuery.isFlagged should be(true) flaggedQuery.flagMessage should be(Some(message)) } } @Test def testUnFlagQuery = afterCreatingTables { dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) { val Some(query) = dao.findQueryByNetworkId(networkQueryId1) query.isFlagged should be(false) } val message = "askdljasd" dao.flagQuery(networkQueryId1, Some(message)) val Some(flaggedQuery) = dao.findQueryByNetworkId(networkQueryId1) flaggedQuery.isFlagged should be(true) flaggedQuery.flagMessage should be(Some(message)) dao.unFlagQuery(networkQueryId1) val Some(unFlaggedQuery) = dao.findQueryByNetworkId(networkQueryId1) unFlaggedQuery.isFlagged should be(false) unFlaggedQuery.flagMessage should be(None) } @Test def testInsertQueryAndFindQueryByNetworkId = afterCreatingTables { dao.findQueryByNetworkId(networkQueryId1) should be(None) dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) { val Some(ShrineQuery(id, localMasterId, actualNetworkId, name, username, domain, dateCreated, isFlagged, flagMessage,queryDef)) = dao.findQueryByNetworkId(networkQueryId1) id should be(1) localMasterId should equal(masterId1) actualNetworkId should equal(networkQueryId1) name should equal(queryDef.name) username should equal(authn.username) domain should equal(authn.domain) dateCreated should not be (null) //NB: Don't compare, to avoid off-by-a-few errors :/ isFlagged should be(false) queryDef should be(queryDef1) } //Inserting a query with the same values should be allowed, it should just create a new row with a new id dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) { //However, the first shrine_query row with the passed networkQueryId should be returned, not the one we just inserted val Some(ShrineQuery(id, localMasterId, actualNetworkId, name, username, domain, dateCreated, isFlagged, flagMessage,queryDef)) = dao.findQueryByNetworkId(networkQueryId1) id should be(1) localMasterId should equal(masterId1) actualNetworkId should equal(networkQueryId1) username should equal(authn.username) domain should equal(authn.domain) dateCreated should not be (null) //NB: Don't compare, to avoid off-by-a-few errors :/ queryDef should equal(queryDef1) isFlagged should be(false) } } @Test def testFindQueriesByUserAndDomain: Unit = afterCreatingTables { dao.findQueriesByUserAndDomain("", "", 50) should equal(Nil) dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) //Make next query happen 10 milliseconds later, so we can distinguish it from the one we just inserted //(java.sql.Timestamps have 1ms resolution, it appears?) Thread.sleep(10) dao.insertQuery(masterId2, networkQueryId2, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) dao.findQueriesByUserAndDomain("", "", 50) should equal(Nil) val Seq(foundQuery1, foundQuery2) = dao.findQueriesByUserAndDomain(authn.domain, authn.username, 50) foundQuery1.domain should equal(authn.domain) foundQuery1.username should equal(authn.username) foundQuery1.localId should equal(masterId2) foundQuery1.networkId should equal(networkQueryId2) foundQuery1.queryDefinition should equal(queryDef1) foundQuery2.domain should equal(authn.domain) foundQuery2.username should equal(authn.username) foundQuery2.localId should equal(masterId1) foundQuery2.networkId should equal(networkQueryId1) foundQuery2.queryDefinition should equal(queryDef1) val Seq(foundQuery) = dao.findQueriesByUserAndDomain(authn.domain, authn.username, 1) foundQuery should equal(foundQuery1) } @Test def testInsertQuery: Unit = { def doTestInsertQuery(flagged: Boolean, run: Boolean): Unit = afterCreatingTables { val ids = for (i <- 1 to 2) yield dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = flagged, hasBeenRun = run, flagMessage = None) ids should equal(Seq(1, 2)) def testRow(r: ShrineQuery) { r.localId should equal(masterId1) r.networkId should equal(networkQueryId1) r.dateCreated should not be (null) // :/ r.domain should equal(authn.domain) r.username should equal(authn.username) r.queryDefinition should equal(queryDef1) r.isFlagged should equal(flagged) } val rows = list(queryRows) val Seq(row1, row2) = rows row1.id should be(1) row2.id should be(2) rows.foreach(testRow) } doTestInsertQuery(true, true) doTestInsertQuery(true, false) doTestInsertQuery(false, true) doTestInsertQuery(false, false) } @Test def testInsertQueryResultsOnlyCount = afterCreatingTables { intercept[Exception] { //Should fail due to foreign key constraint dao.insertQueryResults(-1, countRunQueryResponse.results) } val (resultIdsByType, resultRows) = doInsertQueryResultsTest(countRunQueryResponse) val Seq(resultRow) = resultRows resultRow.resultType should equal(PATIENT_COUNT_XML) resultRow.status should equal(QueryResult.StatusType.Finished) resultIdsByType should equal(Map(PATIENT_COUNT_XML -> Seq(resultRow.id))) } @Test def testInsertQueryResultsOnlyErrors = afterCreatingTables { val (resultIdsByType, resultRows) = doInsertQueryResultsTest(onlyErrorsRunQueryResponse) resultRows.foreach { resultRow => resultRow.elapsed should be(None) resultRow.resultType should equal(ERROR) resultRow.status should equal(QueryResult.StatusType.Error) } } @Test def testInsertQueryResultsNoErrorsOneCountSomeBreakdowns = afterCreatingTables { val (resultIdsByType, resultRows) = doInsertQueryResultsTest(countAndBreakdownsRunQueryResponse) resultIdsByType.keySet should equal(Set(PATIENT_COUNT_XML, PATIENT_AGE_COUNT_XML, PATIENT_GENDER_COUNT_XML)) resultIdsByType.values.forall(_.size == 1) should be(true) val countResultRow = resultRows.filter(_.resultType == PATIENT_COUNT_XML).head countResultRow.resultType should equal(PATIENT_COUNT_XML) resultRows.filter(row => Set(PATIENT_AGE_COUNT_XML, PATIENT_GENDER_COUNT_XML).contains(row.resultType)).size should be(2) resultRows.foreach { resultRow => resultRow.elapsed should not be (None) resultRow.status should equal(QueryResult.StatusType.Finished) } } private def doInsertQueryResultsTest(response: RawCrcRunQueryResponse): (Map[ResultOutputType, Seq[Int]], Seq[QueryResultRow]) = { list(queryResultRows).isEmpty should be(true) val queryId = dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) val resultIdsByType = dao.insertQueryResults(queryId, response.results) val resultRows = list(queryResultRows) resultRows.foreach { resultRow => resultRow.elapsed should not be (null) resultRow.lastUpdated should not be (null) resultRow.queryId should equal(queryId) } list(errorResultRows).isEmpty should be(true) list(countResultRows).isEmpty should be(true) list(breakdownResultRows).isEmpty should be(true) (resultIdsByType, resultRows) } @Test def testInsertCountResult = afterCreatingTables { val queryId = dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) val response = RawCrcRunQueryResponse(networkQueryId1, now.get, authn.username, authn.domain, queryDef1, instanceId, toQueryResultMap(Seq(countQueryResult))) val resultIdsByType = dao.insertQueryResults(queryId, response.results) val Seq(resultRow) = list(queryResultRows) list(countResultRows).isEmpty should be(true) dao.insertCountResult(resultRow.id, countQueryResult.setSize, countQueryResult.setSize + 2) val Seq(countResultRow) = list(countResultRows) countResultRow.creationDate should not be (null) countResultRow.obfuscatedValue should equal(countQueryResult.setSize + 2) countResultRow.originalValue should equal(countQueryResult.setSize) countResultRow.resultId should equal(resultRow.id) intercept[Exception] { //Should fail due to foreign key constraint dao.insertCountResult(-12345, countQueryResult.setSize, countQueryResult.setSize + 2) } } @Test def testInsertBreakdownResults = afterCreatingTables { val (resultIdsByType, resultRows) = doInsertQueryResultsTest(onlyBreakdownsRunQueryResponse) resultIdsByType.keySet should equal(Set(PATIENT_AGE_COUNT_XML, PATIENT_GENDER_COUNT_XML)) resultIdsByType.values.forall(_.size == 1) should be(true) val Seq(breakdownResultRow1, breakdownResultRow2) = resultRows dao.insertBreakdownResults(resultIdsByType, breakdownsByType, breakdownsByType.mapValues(_.mapValues(_ + 1))) val ageBreakdownRows = list(breakdownResultRows).filter(_.resultId == resultIdsByType(PATIENT_AGE_COUNT_XML).head) val genderBreakdownRows = list(breakdownResultRows).filter(_.resultId == resultIdsByType(PATIENT_GENDER_COUNT_XML).head) { val ageRowsByName = ageBreakdownRows.map(row => (row.dataKey, row)).toMap ageRowsByName("x").originalValue should equal(1) ageRowsByName("y").originalValue should equal(2) ageRowsByName("x").obfuscatedValue should equal(2) ageRowsByName("y").obfuscatedValue should equal(3) } { val genderRowsByName = genderBreakdownRows.map(row => (row.dataKey, row)).toMap genderRowsByName("a").originalValue should equal(123) genderRowsByName("b").originalValue should equal(456) genderRowsByName("a").obfuscatedValue should equal(124) genderRowsByName("b").obfuscatedValue should equal(457) } intercept[Exception] { //Should fail due to foreign key constraint dao.insertBreakdownResults(Map(PATIENT_AGE_COUNT_XML -> Seq(-1000)), breakdownsByType, breakdownsByType) } } @Test def testInsertErrorResult = afterCreatingTables { val queryId = dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) val response = countRunQueryResponse.withResults(Seq(errorQueryResult1)) val resultIdsByType = dao.insertQueryResults(queryId, response.results) val Seq(resultRow) = list(queryResultRows) list(errorResultRows).isEmpty should be(true) val pd = TestProblem().toDigest dao.insertErrorResult(resultRow.id, message1,pd.codec,pd.stampText,pd.summary,pd.description,pd.detailsXml) val Seq(errorResultRow) = list(errorResultRows) errorResultRow.message should equal(message1) errorResultRow.resultId should equal(resultRow.id) intercept[Exception] { //Should fail due to foreign key constraint dao.insertErrorResult(-12345, "",pd.codec,pd.stampText,pd.summary,pd.description,pd.detailsXml) } } @Test def testFindResultsFor = afterCreatingTables { //A RunQueryResponse with all types of QueryResults: count, breakdown, and error val response = countRunQueryResponse.withResults(Seq(countQueryResult, breakdownQueryResult1, breakdownQueryResult2, errorQueryResult1, errorQueryResult2)) doTestFindResultsFor(response) { resultIdsByType => insertCount(resultIdsByType) insertErrors(resultIdsByType) insertBreakdowns(resultIdsByType, breakdownsByType) } { case (resultIdsByType, resultOption) => val Some(result) = resultOption validateCount(resultIdsByType, result) validateBreakdowns(result) validateErrors(result) } } @Test def testFindResultsForOnlyCount = afterCreatingTables { //A RunQueryResponse with only a count QueryResult val response = countRunQueryResponse doTestFindResultsFor(response) { resultIdsByType => insertCount(resultIdsByType) } { (resultIdsByType, resultOption) => val Some(result) = resultOption validateCount(resultIdsByType, result) result.breakdowns.isEmpty should be(true) result.errors.isEmpty should be(true) } } @Test def testFindResultsForOnlyErrors = afterCreatingTables { //A RunQueryResponse with only errors val response = onlyErrorsRunQueryResponse doTestFindResultsFor(response) { resultIdsByType => insertErrors(resultIdsByType) } { (resultIdsByType, resultOption) => resultOption should be(None) } } @Test def testFindResultsForOnlyBreakdowns = afterCreatingTables { //A RunQueryResponse with only breakdowns val response = countRunQueryResponse.withResults(Seq(countQueryResult, breakdownQueryResult1, breakdownQueryResult2, errorQueryResult1, errorQueryResult2)) doTestFindResultsFor(response) { resultIdsByType => insertBreakdowns(resultIdsByType, breakdownsByType) } { (resultIdsByType, resultOption) => resultOption should be(None) } } private def insertCount(resultIdsByType: Map[ResultOutputType, Seq[Int]]) { dao.insertCountResult(resultIdsByType(PATIENT_COUNT_XML).head, count, count + 42) } private def insertErrors(resultIdsByType: Map[ResultOutputType, Seq[Int]]) { (resultIdsByType(ERROR) zip Seq(message1, message2)).foreach { case (resultId, message) => val pd = TestProblem().toDigest dao.insertErrorResult(resultId, message,pd.codec,pd.stampText,pd.summary,pd.description,pd.detailsXml) } } private def insertBreakdowns(resultIdsByType: Map[ResultOutputType, Seq[Int]], breakdownsByType: Map[ResultOutputType, I2b2ResultEnvelope]) { dao.insertBreakdownResults(resultIdsByType, breakdownsByType, breakdownsByType.mapValues(_.mapValues(_ + 42))) } private def validateCount(resultIdsByType: Map[ResultOutputType, Seq[Int]], result: ShrineQueryResult) { val foundCount = result.count foundCount.resultId should equal(resultIdsByType(PATIENT_COUNT_XML).head) foundCount.data.get.originalValue should equal(count) foundCount.data.get.obfuscatedValue should equal(count + 42) foundCount.creationDate should not be (null) } private def validateBreakdowns(result: ShrineQueryResult) { val retrievedBreakdownsByType = result.breakdowns.groupBy(_.resultType).mapValues(_.head) retrievedBreakdownsByType(PATIENT_AGE_COUNT_XML).resultType should equal(PATIENT_AGE_COUNT_XML) retrievedBreakdownsByType(PATIENT_AGE_COUNT_XML).data should equal(Map("x" -> ObfuscatedPair(1, 43), "y" -> ObfuscatedPair(2, 44))) retrievedBreakdownsByType(PATIENT_GENDER_COUNT_XML).resultType should equal(PATIENT_GENDER_COUNT_XML) retrievedBreakdownsByType(PATIENT_GENDER_COUNT_XML).data should equal(Map("a" -> ObfuscatedPair(123, 165), "b" -> ObfuscatedPair(456, 498))) } private def validateErrors(result: ShrineQueryResult) { val Seq(err1, err2) = result.errors err1.message should equal(message1) err2.message should equal(message2) } private def doTestFindResultsFor(response: RawCrcRunQueryResponse)(inserts: Map[ResultOutputType, Seq[Int]] => Any)(validate: (Map[ResultOutputType, Seq[Int]], Option[ShrineQueryResult]) => Any) = { dao.findResultsFor(networkQueryId1) should be(None) val queryId = dao.insertQuery(masterId1, networkQueryId1, authn, queryDef1, isFlagged = false, hasBeenRun = true, flagMessage = None) val resultIdsByType = dao.insertQueryResults(queryId, response.results) inserts(resultIdsByType) val resultOption = dao.findResultsFor(networkQueryId1) validate(resultIdsByType, resultOption) } } \ No newline at end of file diff --git a/adapter/adapter-service/src/test/scala/net/shrine/adapter/service/AbstractI2b2AdminResourceJaxrsTest.scala b/adapter/adapter-service/src/test/scala/net/shrine/adapter/service/AbstractI2b2AdminResourceJaxrsTest.scala index 0e49be7ee..37695ffb6 100644 --- a/adapter/adapter-service/src/test/scala/net/shrine/adapter/service/AbstractI2b2AdminResourceJaxrsTest.scala +++ b/adapter/adapter-service/src/test/scala/net/shrine/adapter/service/AbstractI2b2AdminResourceJaxrsTest.scala @@ -1,102 +1,102 @@ package net.shrine.adapter.service import junit.framework.TestCase import net.shrine.adapter.AdapterTestHelpers import net.shrine.adapter.dao.squeryl.AbstractSquerylAdapterTest import net.shrine.client.{HttpClient, HttpResponse, JerseyHttpClient} import net.shrine.crypto.TrustParam.AcceptAllCerts -import net.shrine.problem.{TestProblem, ProblemSources, AbstractProblem} +import net.shrine.problem.TestProblem import net.shrine.protocol.{DefaultBreakdownResultOutputTypes, ErrorResponse, I2b2AdminReadQueryDefinitionRequest, I2b2AdminRequestHandler, QueryMaster, ReadI2b2AdminPreviousQueriesRequest, ReadPreviousQueriesResponse, ReadQueryDefinitionResponse} import net.shrine.protocol.query.QueryDefinition import net.shrine.util.{ShouldMatchersForJUnit, XmlUtil} import org.junit.{After, Before} import scala.xml.XML /** * @author clint * @since Apr 24, 2013 */ abstract class AbstractI2b2AdminResourceJaxrsTest extends TestCase with JerseyTestComponent[I2b2AdminRequestHandler] with AbstractSquerylAdapterTest with ShouldMatchersForJUnit with CanLoadTestData with AdapterTestHelpers { import scala.concurrent.duration._ protected def adminClient = I2b2AdminClient(resourceUrl, JerseyHttpClient(AcceptAllCerts, 5.minutes)) override def resourceClass(handler: I2b2AdminRequestHandler) = I2b2AdminResource(handler, DefaultBreakdownResultOutputTypes.toSet) override val basePath = "i2b2/admin/request" @Before override def setUp(): Unit = this.JerseyTest.setUp() @After override def tearDown(): Unit = this.JerseyTest.tearDown() protected object NeverAuthenticatesMockPmHttpClient extends HttpClient { override def post(input: String, url: String): HttpResponse = HttpResponse.ok(ErrorResponse(TestProblem(summary = "blarg")).toI2b2String) } protected object AlwaysAuthenticatesMockPmHttpClient extends HttpClient { override def post(input: String, url: String): HttpResponse = { HttpResponse.ok(XmlUtil.stripWhitespace { Some user { userId } { domain } { password } MANAGER }.toString) } } protected def doTestReadQueryDefinition(networkQueryId: Long, expectedQueryNameAndQueryDef: Option[(String, QueryDefinition)]) { val request = I2b2AdminReadQueryDefinitionRequest(projectId, waitTime, authn, networkQueryId) val resp = adminClient.readQueryDefinition(request) def stripNamespaces(s: String) = XmlUtil.stripNamespaces(XML.loadString(s)) expectedQueryNameAndQueryDef match { case Some((expectedQueryName, expectedQueryDef)) => { val response @ ReadQueryDefinitionResponse(masterId, name, userId, createDate, queryDefinition) = resp masterId should be(networkQueryId) name should be(expectedQueryName) userId should be(authn.username) createDate should not be (null) //NB: I'm not sure why whacky namespaces were coming back from the resource; //this checks that the gist of the queryDef XML makes it back. //TODO: revisit this stripNamespaces(queryDefinition) should equal(stripNamespaces(expectedQueryDef.toI2b2String)) } case None => resp.isInstanceOf[ErrorResponse] should be(true) } } protected def doTestReadI2b2AdminPreviousQueries(request: ReadI2b2AdminPreviousQueriesRequest, expectedQueryMasters: Seq[QueryMaster]) { val ReadPreviousQueriesResponse(queryMasters) = adminClient.readI2b2AdminPreviousQueries(request) if(expectedQueryMasters.isEmpty) { queryMasters.isEmpty should be(true) } else { (queryMasters zip expectedQueryMasters).foreach { case (queryMaster, expected) => queryMaster.createDate should not be(null) queryMaster.name should equal(expected.name) queryMaster.queryMasterId should equal(expected.queryMasterId) queryMaster.userId should equal(expected.userId) queryMaster.groupId should equal(expected.groupId) queryMaster.flagged should equal(expected.flagged) } } } } \ No newline at end of file diff --git a/apps/dashboard-app/pom.xml b/apps/dashboard-app/pom.xml index 86871c619..9337bc20f 100644 --- a/apps/dashboard-app/pom.xml +++ b/apps/dashboard-app/pom.xml @@ -1,176 +1,190 @@ shrine-base net.shrine 1.22.0-SNAPSHOT ../../pom.xml 4.0.0 dashboard-app Dashboard App jar net.alchim31.maven scala-maven-plugin com.github.eirslett frontend-maven-plugin 0.0.23 src/main/js install node and npm install-node-and-npm v0.10.33 2.7.4 npm install npm generate-resources install bower install bower install grunt default grunt --no-color + + log4j + log4j + io.spray spray-routing_2.11 ${spray-version} io.spray spray-servlet_2.11 ${spray-version} io.spray spray-util_2.11 ${spray-version} io.spray spray-testkit_2.11 ${spray-version} test com.typesafe.akka akka-actor_2.11 ${akka-version} com.typesafe.akka akka-slf4j_2.11 ${akka-version} com.typesafe.akka akka-testkit_2.11 ${akka-testkit-version} test org.json4s json4s-native_2.11 ${json4s-version} com.typesafe.slick slick_2.11 ${slick-version} org.slf4j slf4j-log4j12 ${slf4j-version} com.h2database h2 ${h2-version} test net.shrine shrine-protocol ${project.version} net.shrine - shrine-client + shrine-utility-commons ${project.version} net.shrine shrine-crypto ${project.version} test-jar test net.shrine shrine-auth ${project.version} + + net.shrine + shrine-data-commons + ${project.version} + mysql mysql-connector-java ${mysql-version} io.jsonwebtoken jjwt 0.6.0 + + net.shrine + shrine-adapter-client-api + 1.22.0-SNAPSHOT + diff --git a/apps/dashboard-app/src/main/js/src/app/diagnostic/diagnostic.model.js b/apps/dashboard-app/src/main/js/src/app/diagnostic/diagnostic.model.js index 759f1a8b1..0b0034290 100644 --- a/apps/dashboard-app/src/main/js/src/app/diagnostic/diagnostic.model.js +++ b/apps/dashboard-app/src/main/js/src/app/diagnostic/diagnostic.model.js @@ -1,166 +1,179 @@ (function (){ 'use strict'; // -- angular module -- // angular.module('shrine-tools') .factory('DiagnosticModel', DiagnosticModel) DiagnosticModel.$inject = ['$http', '$q', 'UrlGetter', 'XMLService']; function DiagnosticModel (h, q, urlGetter, xmlService) { var cache = {}; // -- private const -- // var Config = { OptionsEndpoint: 'admin/status/options', ConfigEndpoint: 'admin/status/config', SummaryEndpoint: 'admin/status/summary', - HapyAllEndpoint: 'admin/happy/all' + ProblemEndpoint: 'admin/status/problems', + HappyAllEndpoint: 'admin/happy/all' }; // -- public -- // return { - getOptions: getOptions, - getConfig: getConfig, - getSummary: getSummary, - getHappyAll:getHappyAll, - cache: cache + getOptions: getOptions, + getConfig: getConfig, + getSummary: getSummary, + getProblems: getProblems, + getHappyAll: getHappyAll, + cache: cache }; /** * Method for Handling a failed rest call. * @param failedResult * @returns {*} */ function onFail(failedResult) { return failedResult; } /*** * Method for handling a successful rest call. * @param result * @returns {*} */ function parseJsonResult(result) { return result.data; } /** * * @param result * @returns {*} */ function parseHappyAllResult(result) { var happyObj = {}; if(isQEPError(result.data)) { return $q.reject(result.data); } // -- append all -- // happyObj.all = xmlService.xmlStringToJson(result.data).all; // -- parse and append summary -- // happyObj.summary = parseSummaryFromAll(happyObj.all); return happyObj; } /** * * @param all * @returns {{}} */ function parseSummaryFromAll (all) { // var summary = {}; summary.isHub = !Boolean("" == all.notAHub); summary.shrineVersion = all.versionInfo.shrineVersion; summary.shrineBuildDate = all.versionInfo.buildDate; summary.ontologyVersion = all.versionInfo.ontologyVersion summary.ontologyTerm = ""; //to be implemented in config. summary.adapterOk = all.adapter.result.response.errorResponse === undefined; summary.keystoreOk = true; summary.qepOk = true; // -- verify hub is operating, if necessary -- // if(!summary.isHub) { summary.hubOk = true; } else if(all.net !== undefined) { var hasFailures = Number(all.net.failureCount) > 0; var hasInvalidResults = Number(all.net.validResultCount) != Number(all.net.expectedResultCount); var hasTimeouts = Number(all.net.timeoutCount) > 0; summary.hubOk = !hasFailures && !hasInvalidResults && !hasTimeouts; } return summary; } /** * Get View Options, initial call from diagnostic. * @param verb * @returns {*} */ function getOptions() { var url = urlGetter(Config.OptionsEndpoint) return h.get(url) .then(parseJsonResult, onFail); } /** * Returns the Shrine Configuration object. * @returns {*} */ function getConfig () { var url = urlGetter(Config.ConfigEndpoint) return h.get(url) .then(parseJsonResult, onFail); } /** * * @returns {*} */ function getSummary () { var url = urlGetter(Config.SummaryEndpoint) return h.get(url) .then(parseJsonResult, onFail); } + /** + * //todo + * ProblemEndpoint: 'admin/status/problems', + * @returns {*} + */ + function getProblems(n) { + var url = urlGetter(Config.ProblemEndpoint+'?offset='+n); + return h.get(url) + .then(parseJsonResult, onFail); + } + /** * * @returns {*} */ function getHappyAll() { - var url = urlGetter(Config.HapyAllEndpoint, '.xml') + var url = urlGetter(Config.HappyAllEndpoint, '.xml') return h.get(url) .then(parseHappyAllResult, onFail); } /** * * @param resultXML * @returns {boolean} */ function isQEPError(resultXML) { var result = resultXML.indexOf('') + resultXML.indexOf(''); return result == -2 } } })(); diff --git a/apps/dashboard-app/src/main/js/src/app/diagnostic/sidebar/sidebar.tpl.html b/apps/dashboard-app/src/main/js/src/app/diagnostic/sidebar/sidebar.tpl.html index d7b936342..b4b8f1eb2 100755 --- a/apps/dashboard-app/src/main/js/src/app/diagnostic/sidebar/sidebar.tpl.html +++ b/apps/dashboard-app/src/main/js/src/app/diagnostic/sidebar/sidebar.tpl.html @@ -1,54 +1,58 @@ 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 new file mode 100644 index 000000000..12bf37975 --- /dev/null +++ b/apps/dashboard-app/src/main/js/src/app/diagnostic/views/problems.controller.js @@ -0,0 +1,77 @@ +(function () { + 'use strict'; + + + // -- register controller with angular -- // + angular.module('shrine-tools') + .controller('ProblemsController', ProblemsController); + + + /** + * + * @type {string[]} + */ + ProblemsController.$inject = ['$app', '$log']; + function ProblemsController ($app, $log) { + var vm = this; + + init(); + + /** + * + */ + function init () { + vm.num = 0; + vm.url = "https://open.med.harvard.edu/wiki/display/SHRINE/"; + vm.newPage = newPage; + vm.floorMod = floorMod; + vm.numCheck = function(any) {return isFinite(any)? any - 1: vm.probsOffset}; + vm.formatCodec = function(word) { + var index = word.lastIndexOf('.'); + var arr = word.trim().split(""); + arr[index] = '\n'; + return arr.join(""); + }; + vm.min = Math.min; + vm.stringify = function(arg) { return JSON.stringify(arg, null, 2); }; + newPage(0) + } + + 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 newPage(n) { + if (!isFinite(n)) { + return; + } + 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, num1)); + } + }; + var num = vm.floorMod(clamp(n), vm.probsN); + $app.model.getProblems(num) + .then(setProblems) + } + + function setProblems(probs) { + vm.problems = probs.problems; + vm.probsSize = probs.size; + vm.probsOffset = probs.offset; + vm.probsN = probs.n; + } + + } +})(); diff --git a/apps/dashboard-app/src/main/js/src/app/diagnostic/views/problems.tpl.html b/apps/dashboard-app/src/main/js/src/app/diagnostic/views/problems.tpl.html new file mode 100644 index 000000000..551929c81 --- /dev/null +++ b/apps/dashboard-app/src/main/js/src/app/diagnostic/views/problems.tpl.html @@ -0,0 +1,82 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Problems: Total = {{vm.probsSize}} +
+ Codec + + Time Logged + + Summary + + Description +
{{vm.formatCodec(problem.codec)}}
+
+ {{problem.stampText}} + + {{problem.summary}} + + {{problem.description}} +
+
+                    {{vm.stringify(problem.detailsXml)}}
+                
+
+
+ + ← + + | + + → + +
+
+ {{vm.floorMod(vm.probsOffset, vm.probsN) + 1}}-{{vm.min(vm.probsSize, vm.floorMod(vm.probsOffset, vm.probsN) + vm.probsN)}} / {{vm.probsSize}} +
+
+ +
+
+
\ No newline at end of file diff --git a/apps/dashboard-app/src/main/js/src/app/shrine-tools.configuration.js b/apps/dashboard-app/src/main/js/src/app/shrine-tools.configuration.js index 3317ec195..6fc37b1c5 100644 --- a/apps/dashboard-app/src/main/js/src/app/shrine-tools.configuration.js +++ b/apps/dashboard-app/src/main/js/src/app/shrine-tools.configuration.js @@ -1,237 +1,254 @@ (function () { 'use strict'; angular .module('shrine-tools') .config(ShrineToolsConfiguration); // -- todo: move to service or constant -- // var stateConfig = { 'login': { name:'login', files:[ 'src/app/common/authentication/authentication.module.js', 'src/app/common/authentication/authentication.service.js', 'src/app/login/login.controller.js' ] }, 'diagnostic': { name:'diagnostic', files:[ 'src/app/diagnostic/sidebar/sidebar.model.js', 'src/app/diagnostic/sidebar/sidebar.service.js', 'src/app/diagnostic/sidebar/node-menu.js', 'src/app/diagnostic/sidebar/downstream-node.js', 'src/app/diagnostic/sidebar/sidebar.js' ] }, 'diagnostic.summary': { name:'diagnostic.summary', files:[ 'src/app/diagnostic/views/summary.controller.js' ] }, 'diagnostic.i2b2-connections': { name:'diagnostic.i2b2-connections', files:[ 'src/app/diagnostic/views/i2b2-connections.controller.js' ] }, 'diagnostic.keystore': { name:'diagnostic.keystore', files:[ 'src/app/diagnostic/views/keystore.controller.js' ] }, 'diagnostic.hub': { name:'diagnostic.keystore', files:[ 'src/app/diagnostic/views/hub.controller.js' ] }, 'diagnostic.adapter': { name:'diagnostic.adapter', files:[ 'src/app/diagnostic/views/adapter.controller.js' ] }, 'diagnostic.qep': { name:'diagnostic.qep', files:[ 'src/app/diagnostic/views/qep.controller.js' ] }, 'diagnostic.config': { name: 'diagnostic.config', files: [ 'src/app/diagnostic/views/config.controller.js', 'src/app/diagnostic/views/bootcordion.js' ] + }, + 'diagnostic.problems': { + name: 'diagnostic.problems', + files: [ + 'src/app/diagnostic/views/problems.controller.js' + ] } }; ShrineToolsConfiguration.$inject = ['$stateProvider', '$urlRouterProvider', '$ocLazyLoadProvider', '$httpProvider']; function ShrineToolsConfiguration ($stateProvider, $urlRouterProvider, $ocLazyLoadProvider, $httpProvider) { // -- set default view -- // $urlRouterProvider.otherwise('/diagnostic/summary'); configureLazyLoader($ocLazyLoadProvider); configureHttpProvider($httpProvider); $stateProvider .state('login',{ url: '/login', controller: 'LoginController', controllerAs: 'vm', templateUrl: 'src/app/login/login.tpl.html', resolve: { loadFiles:function($ocLazyLoad) { return $ocLazyLoad.load(stateConfig['login']); } } }) .state('diagnostic', { url: '/diagnostic', controller: 'STCtrl', templateUrl: 'src/app/diagnostic/diagnostic.tpl.html', resolve: { loadFiles:function($ocLazyLoad){ return $ocLazyLoad.load(stateConfig['diagnostic']) } } }) .state('diagnostic.summary',{ url: '/summary', controller: 'SummaryController', controllerAs: 'vm', templateUrl: 'src/app/diagnostic/views/summary.tpl.html', resolve: { loadMyFiles:function($ocLazyLoad) { return $ocLazyLoad.load(stateConfig['diagnostic.summary']); } } }) .state('diagnostic.i2b2-connections',{ url: '/i2b2-connections', controller: 'I2B2ConnectionsController', controllerAs: 'vm', templateUrl: 'src/app/diagnostic/views/i2b2-connections.tpl.html', resolve: { loadMyFiles:function($ocLazyLoad) { return $ocLazyLoad.load(stateConfig['diagnostic.i2b2-connections']); } } }) .state('diagnostic.keystore',{ url: '/keystore', templateUrl: 'src/app/diagnostic/views/keystore.tpl.html', controller: 'KeystoreController', controllerAs: 'vm', resolve: { loadMyFiles: function ($ocLazyLoad) { return $ocLazyLoad.load(stateConfig['diagnostic.keystore']); } } //@todo: load files }) .state('diagnostic.hub',{ url:'/hub', templateUrl: 'src/app/diagnostic/views/hub.tpl.html', controller: 'HubController', controllerAs: 'vm', resolve: { loadMyFiles: function ($ocLazyLoad) { return $ocLazyLoad.load(stateConfig['diagnostic.hub']) } } //@todo: load files }) .state('diagnostic.adapter',{ url: '/adapter', templateUrl: 'src/app/diagnostic/views/adapter.tpl.html', controller: 'AdapterController', controllerAs: 'vm', resolve: { loadMyFiles:function($ocLazyLoad) { return $ocLazyLoad.load(stateConfig['diagnostic.adapter']); } } }) .state('diagnostic.qep',{ url: '/qep', templateUrl: 'src/app/diagnostic/views/qep.tpl.html', controller: 'QEPController', controllerAs: 'vm', resolve: { loadMyFiles: function ($ocLazyLoad) { return $ocLazyLoad.load(stateConfig['diagnostic.qep']); } } }) .state('diagnostic.config',{ url: '/config', controller: 'ShrineConfigurationController', controllerAs: 'vm', templateUrl: 'src/app/diagnostic/views/config.tpl.html', resolve: { loadFiles: function ($ocLazyLoad) { return $ocLazyLoad.load(stateConfig['diagnostic.config']); } } }) .state('diagnostic.downstream-nodes',{ url:'/downstream-nodes' //@todo: load files - }); + }) + .state('diagnostic.problems', { + url: '/problems', + controller: 'ProblemsController', + controllerAs: 'vm', + templateUrl: 'src/app/diagnostic/views/problems.tpl.html', + resolve: { + loadFiles: function($ocLazyLoad) { + return $ocLazyLoad.load(stateConfig['diagnostic.problems']); + } + } + }) } /** * Configure lazy loader to log all errors to console, broadcast event when file, * module or component loads * @param lazyLoader * @todo: configure module dependencies here. * @see: https://oclazyload.readme.io/docs/oclazyloadprovider */ function configureLazyLoader(lazyLoader) { var lazyLoadConfig = { debug: true, events:true } lazyLoader.config(lazyLoadConfig); return lazyLoader; } /** * Set up cross domain voodoo, if running from deployment, No IE Cache * @param httpProvider * @returns {*} * @see: http://stackoverflow.com/questions/16098430/angular-ie-caching-issue-for-http */ function configureHttpProvider (httpProvider) { // -- set up cross domain -- // httpProvider.defaults.useXDomain = true; delete httpProvider.defaults.headers.common['X-Requested-With']; // -- If running from deployment, No IE Cache -- // if (window.location.origin.indexOf('http://localhost:63342') === -1) { //initialize get if not there if (!httpProvider.defaults.headers.get) { httpProvider.defaults.headers.get = {}; } //disable IE ajax request caching httpProvider.defaults.headers.get['If-Modified-Since'] = 'Sat, 26 Jul 1997 05:00:00 GMT'; httpProvider.defaults.headers.get['Cache-Control'] = 'no-cache'; httpProvider.defaults.headers.get['Pragma'] = 'no-cache'; } return httpProvider; } })(); diff --git a/apps/dashboard-app/src/main/js/test/admin/status/problems b/apps/dashboard-app/src/main/js/test/admin/status/problems new file mode 100644 index 000000000..107186f04 --- /dev/null +++ b/apps/dashboard-app/src/main/js/test/admin/status/problems @@ -0,0 +1,995 @@ +{"size":20,"offset":0,"n":20,"problems": +[{ + "codec":"net.shrine.protocol.ErrorStatusFromCrc", + "description": "Proin risus.", + "summary": "Vivamus in felis eu sapien cursus vestibulum.", + "stampText": "2016-07-25T17:42:42Z", + "epoch": "1464931235", + "detailsXml": { + "exception": { + "name": "PellentesqueEget.js", + "message": "Vivamus vestibulum sagittis sapien. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "stacktrace": { + "line": [ + "Sed ante.", + "Morbi non quam nec dui luctus rutrum.", + "Praesent blandit lacinia erat.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Nulla facilisi.", + "Integer tincidunt ante vel ipsum.", + "Aenean fermentum.", + "Nam nulla.", + "Fusce consequat.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Proin at turpis a pede posuere nonummy.", + "In hac habitasse platea dictumst.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Mauris ullamcorper purus sit amet nulla.", + "Mauris lacinia sapien quis libero.", + "Mauris sit amet eros.", + "Morbi non lectus.", + "Fusce consequat.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Maecenas rhoncus aliquam lacus.", + "Donec ut mauris eget massa tempor convallis.", + "Praesent blandit lacinia erat.", + "Pellentesque eget nunc.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Quisque porta volutpat erat.", + "Quisque porta volutpat erat.", + "Nunc nisl.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", + "Duis at velit eu est congue elementum.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Proin at turpis a pede posuere nonummy.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Donec vitae nisi.", + "Nullam varius.", + "Morbi non lectus.", + "Sed sagittis.", + "Integer a nibh.", + "Nunc nisl.", + "Phasellus in felis.", + "Pellentesque ultrices mattis odio.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Proin interdum mauris non ligula pellentesque ultrices.", + "Donec dapibus.", + "Praesent blandit.", + "Morbi non lectus.", + "Nullam porttitor lacus at turpis." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.CannotParseXmlFromCrc", + "description": "Aenean sit amet justo.", + "summary": "Donec ut dolor. Morbi vel lectus in quam fringilla rhoncus.", + "stampText": "2015-09-20T04:34:50Z", + "epoch": "1467940628", + "detailsXml": { + "exception": { + "name": "InAnte.js", + "message": "Mauris lacinia sapien quis libero. Nullam sit amet turpis elementum ligula vehicula consequat. Morbi a ipsum.", + "stacktrace": { + "line": [ + "Nulla facilisi.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Ut at dolor quis odio consequat varius.", + "Morbi ut odio.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Nulla tempus.", + "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", + "Maecenas tincidunt lacus at velit.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "In hac habitasse platea dictumst.", + "Nam tristique tortor eu pede.", + "Pellentesque at nulla.", + "Ut tellus.", + "Sed sagittis.", + "Nullam porttitor lacus at turpis.", + "Vestibulum rutrum rutrum neque.", + "In congue.", + "Nulla facilisi.", + "Vivamus tortor." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.NoValidResponsesToAggregate", + "description": "Suspendisse ornare consequat lectus.", + "summary": "Donec posuere metus vitae ipsum.", + "stampText": "2015-08-30T15:48:40Z", + "epoch": "1467096518", + "detailsXml": { + "exception": { + "name": "Eget.css", + "message": "Curabitur gravida nisi at nibh.", + "stacktrace": { + "line": [ + "Praesent blandit lacinia erat.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Maecenas pulvinar lobortis est.", + "Phasellus id sapien in sapien iaculis congue.", + "Mauris ullamcorper purus sit amet nulla.", + "Suspendisse potenti.", + "In hac habitasse platea dictumst.", + "Proin interdum mauris non ligula pellentesque ultrices.", + "Nam dui.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Fusce consequat.", + "Sed accumsan felis.", + "Curabitur convallis.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Donec posuere metus vitae ipsum.", + "Praesent blandit lacinia erat.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Fusce consequat.", + "Proin risus.", + "In quis justo.", + "Pellentesque eget nunc.", + "Nulla ac enim.", + "Nulla ut erat id mauris vulputate elementum.", + "Nunc rhoncus dui vel sem.", + "Proin risus.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Integer tincidunt ante vel ipsum.", + "In congue.", + "Praesent id massa id nisl venenatis lacinia.", + "Suspendisse potenti.", + "Sed accumsan felis.", + "Cras non velit nec nisi vulputate nonummy.", + "Nullam molestie nibh in lectus.", + "Mauris ullamcorper purus sit amet nulla.", + "Vestibulum sed magna at nunc commodo placerat.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Donec vitae nisi.", + "Nam tristique tortor eu pede.", + "Pellentesque eget nunc.", + "Duis ac nibh.", + "Nulla mollis molestie lorem." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.NoValidResponsesToAggregate", + "description": "Maecenas rhoncus aliquam lacus.", + "summary": "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "stampText": "2015-11-15T20:17:04Z", + "epoch": "1467123461", + "detailsXml": { + "exception": { + "name": "Id.js", + "message": "Quisque ut erat. Curabitur gravida nisi at nibh. In hac habitasse platea dictumst.", + "stacktrace": { + "line": [ + "Maecenas pulvinar lobortis est.", + "Mauris ullamcorper purus sit amet nulla.", + "Curabitur in libero ut massa volutpat convallis.", + "Maecenas tincidunt lacus at velit.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "Sed accumsan felis.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Morbi porttitor lorem id ligula.", + "Suspendisse potenti.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "Nunc nisl.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Proin eu mi.", + "Duis aliquam convallis nunc.", + "Nunc nisl.", + "Vivamus vestibulum sagittis sapien.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Donec quis orci eget orci vehicula condimentum.", + "Vestibulum sed magna at nunc commodo placerat.", + "Ut tellus.", + "Etiam justo.", + "In congue.", + "Phasellus in felis.", + "Vivamus vestibulum sagittis sapien.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Aliquam erat volutpat.", + "Phasellus sit amet erat.", + "Morbi non lectus.", + "Maecenas tincidunt lacus at velit.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "Quisque ut erat." + ] + } + } + } +}, { + "codec":"net.shrine.qep.queries.QepDatabaseProblem", + "description": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "summary": "Vivamus vel nulla eget eros elementum pellentesque.", + "stampText": "2016-03-01T14:48:34Z", + "epoch": "1469226821", + "detailsXml": { + "exception": { + "name": "IdSapienIn.java", + "message": "Curabitur in libero ut massa volutpat convallis. Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "stacktrace": { + "line": [ + "Etiam pretium iaculis justo.", + "Vestibulum sed magna at nunc commodo placerat.", + "In congue.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "In hac habitasse platea dictumst.", + "Integer tincidunt ante vel ipsum.", + "In sagittis dui vel nisl.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Duis mattis egestas metus.", + "Duis ac nibh.", + "Aliquam non mauris.", + "Aliquam non mauris.", + "Etiam justo.", + "Sed accumsan felis.", + "Praesent blandit lacinia erat.", + "Duis ac nibh.", + "Etiam justo.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Cras in purus eu magna vulputate luctus.", + "Aliquam erat volutpat.", + "Nunc rhoncus dui vel sem.", + "Nunc nisl.", + "Vestibulum sed magna at nunc commodo placerat.", + "Curabitur convallis.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Nunc purus.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Aliquam erat volutpat.", + "Vestibulum rutrum rutrum neque.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "In hac habitasse platea dictumst.", + "Quisque porta volutpat erat.", + "Nulla justo.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Donec ut dolor.", + "Nam dui.", + "Sed vel enim sit amet nunc viverra dapibus.", + "In quis justo." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.CrcCouldNotBeInvoked", + "description": "Curabitur in libero ut massa volutpat convallis.", + "summary": "Curabitur in libero ut massa volutpat convallis. Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "stampText": "2016-05-12T22:27:05Z", + "epoch": "1446949742", + "detailsXml": { + "exception": { + "name": "PedeUllamcorper.js", + "message": "Pellentesque eget nunc. Donec quis orci eget orci vehicula condimentum.", + "stacktrace": { + "line": [ + "Phasellus id sapien in sapien iaculis congue.", + "In eleifend quam a odio.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "Quisque porta volutpat erat.", + "Nullam porttitor lacus at turpis.", + "Suspendisse ornare consequat lectus.", + "Phasellus in felis.", + "In hac habitasse platea dictumst.", + "Nam tristique tortor eu pede.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Morbi ut odio.", + "Mauris ullamcorper purus sit amet nulla.", + "Cras in purus eu magna vulputate luctus.", + "Praesent blandit lacinia erat.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Nulla justo.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "Ut tellus.", + "Praesent blandit lacinia erat.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Ut at dolor quis odio consequat varius.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Proin eu mi.", + "Proin eu mi.", + "Curabitur gravida nisi at nibh.", + "Aenean fermentum.", + "Praesent lectus.", + "Nulla mollis molestie lorem.", + "Morbi non quam nec dui luctus rutrum.", + "Pellentesque ultrices mattis odio." + ] + } + } + } +}, { + "codec":"net.shrine.qep.queries.QepDatabaseProblem", + "description": "Integer non velit.", + "summary": "Integer a nibh. In quis justo. Maecenas rhoncus aliquam lacus.", + "stampText": "2015-10-19T01:39:35Z", + "epoch": "1451699738", + "detailsXml": { + "exception": { + "name": "NatoquePenatibus.java", + "message": "Curabitur in libero ut massa volutpat convallis. Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo. Maecenas pulvinar lobortis est.", + "stacktrace": { + "line": [ + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Integer tincidunt ante vel ipsum.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "In congue.", + "Pellentesque viverra pede ac diam.", + "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "Morbi a ipsum.", + "Duis mattis egestas metus.", + "Nulla suscipit ligula in lacus.", + "Donec dapibus.", + "Praesent blandit.", + "Proin at turpis a pede posuere nonummy.", + "Phasellus id sapien in sapien iaculis congue.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Sed ante.", + "Phasellus sit amet erat.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Praesent blandit.", + "Vestibulum rutrum rutrum neque.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "In hac habitasse platea dictumst.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Etiam vel augue.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Nulla suscipit ligula in lacus.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "Pellentesque eget nunc.", + "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", + "Etiam justo." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.dao.squeryl.BreakdownFailure", + "description": "Nulla suscipit ligula in lacus.", + "summary": "Etiam justo. Etiam pretium iaculis justo.", + "stampText": "2015-12-18T19:40:27Z", + "epoch": "1457212349", + "detailsXml": { + "exception": { + "name": "CrasMiPede.js", + "message": "Morbi a ipsum.", + "stacktrace": { + "line": [ + "Cras non velit nec nisi vulputate nonummy.", + "Phasellus in felis.", + "Nulla mollis molestie lorem.", + "Nullam porttitor lacus at turpis.", + "Nulla justo.", + "Nulla facilisi.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Quisque ut erat.", + "Duis mattis egestas metus.", + "Aenean fermentum.", + "Nunc rhoncus dui vel sem.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Donec semper sapien a libero.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Nam dui.", + "Ut tellus.", + "Proin at turpis a pede posuere nonummy.", + "Mauris ullamcorper purus sit amet nulla.", + "Sed accumsan felis.", + "Etiam vel augue.", + "Aliquam non mauris.", + "Nulla tellus.", + "Nullam varius.", + "Curabitur gravida nisi at nibh.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Morbi non quam nec dui luctus rutrum.", + "Nulla tellus.", + "Integer ac leo.", + "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.HttpErrorResponseProblem", + "description": "Nullam sit amet turpis elementum ligula vehicula consequat.", + "summary": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam. Suspendisse potenti.", + "stampText": "2016-02-04T18:18:08Z", + "epoch": "1463644845", + "detailsXml": { + "exception": { + "name": "MorbiNon.css", + "message": "Nulla suscipit ligula in lacus.", + "stacktrace": { + "line": [ + "Nulla tempus.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Aenean fermentum.", + "Vestibulum sed magna at nunc commodo placerat.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Sed sagittis.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "Aliquam erat volutpat.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Pellentesque eget nunc.", + "In hac habitasse platea dictumst.", + "Ut at dolor quis odio consequat varius.", + "Mauris ullamcorper purus sit amet nulla.", + "Fusce consequat.", + "Duis aliquam convallis nunc.", + "Aenean fermentum.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Morbi ut odio.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Maecenas ut massa quis augue luctus tincidunt.", + "In hac habitasse platea dictumst.", + "Nunc purus.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Integer a nibh.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Maecenas tincidunt lacus at velit.", + "Duis aliquam convallis nunc.", + "Nulla ut erat id mauris vulputate elementum.", + "Pellentesque ultrices mattis odio.", + "Nulla nisl.", + "Etiam vel augue.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Donec vitae nisi.", + "Pellentesque ultrices mattis odio.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Vivamus vestibulum sagittis sapien.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Nulla nisl.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio." + ] + } + } + } +}, { + "codec":"ProblemNotYetEncoded", + "description": "In hac habitasse platea dictumst.", + "summary": "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam. Nam tristique tortor eu pede.", + "stampText": "2016-04-14T19:38:30Z", + "epoch": "1438484431", + "detailsXml": { + "exception": { + "name": "UltricesAliquet.js", + "message": "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "stacktrace": { + "line": [ + "In hac habitasse platea dictumst.", + "Integer ac leo.", + "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "Nulla justo.", + "Mauris ullamcorper purus sit amet nulla.", + "Donec semper sapien a libero.", + "Praesent blandit lacinia erat.", + "Duis at velit eu est congue elementum.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Cras pellentesque volutpat dui.", + "Nulla tellus.", + "Nunc purus.", + "Curabitur at ipsum ac tellus semper interdum.", + "Nulla ac enim.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Curabitur gravida nisi at nibh.", + "Pellentesque at nulla.", + "Vivamus vestibulum sagittis sapien.", + "Proin risus.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Duis aliquam convallis nunc.", + "Nulla suscipit ligula in lacus.", + "Aenean fermentum.", + "Morbi porttitor lorem id ligula.", + "Integer tincidunt ante vel ipsum." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.client.HttpErrorCodeFromAdapter", + "description": "Mauris ullamcorper purus sit amet nulla.", + "summary": "Aenean sit amet justo. Morbi ut odio.", + "stampText": "2016-06-18T19:41:42Z", + "epoch": "1442204097", + "detailsXml": { + "exception": { + "name": "NonLectusAliquam.js", + "message": "Morbi non lectus. Aliquam sit amet diam in magna bibendum imperdiet.", + "stacktrace": { + "line": [ + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Vestibulum sed magna at nunc commodo placerat.", + "Donec quis orci eget orci vehicula condimentum.", + "Donec ut mauris eget massa tempor convallis.", + "Ut at dolor quis odio consequat varius.", + "Morbi non lectus.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Aliquam non mauris.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Duis aliquam convallis nunc.", + "Sed sagittis.", + "Morbi non lectus.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Mauris sit amet eros.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Proin eu mi.", + "Nunc purus.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Quisque porta volutpat erat.", + "Donec ut mauris eget massa tempor convallis.", + "Nullam varius.", + "Nullam molestie nibh in lectus.", + "Etiam faucibus cursus urna.", + "Suspendisse ornare consequat lectus.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Vestibulum ac est lacinia nisi venenatis tristique.", + "In sagittis dui vel nisl.", + "In hac habitasse platea dictumst.", + "Curabitur convallis.", + "Vestibulum sed magna at nunc commodo placerat.", + "Mauris sit amet eros.", + "Duis at velit eu est congue elementum.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Cras pellentesque volutpat dui.", + "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "Suspendisse potenti.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Duis ac nibh.", + "Pellentesque ultrices mattis odio.", + "Ut tellus.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Nullam varius.", + "Pellentesque viverra pede ac diam.", + "Nulla ut erat id mauris vulputate elementum.", + "Cras in purus eu magna vulputate luctus.", + "Phasellus sit amet erat." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.InvalidResultProblem", + "description": "Curabitur gravida nisi at nibh.", + "summary": "Nunc nisl.", + "stampText": "2015-11-01T01:07:51Z", + "epoch": "1464352796", + "detailsXml": { + "exception": { + "name": "MaurisSit.css", + "message": "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo. Maecenas pulvinar lobortis est. Phasellus sit amet erat.", + "stacktrace": { + "line": [ + "Nullam varius.", + "Maecenas rhoncus aliquam lacus.", + "Suspendisse potenti.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "Aliquam erat volutpat.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Aliquam non mauris.", + "Nulla facilisi.", + "Nulla suscipit ligula in lacus.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Integer non velit.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Integer ac leo.", + "Curabitur in libero ut massa volutpat convallis.", + "Morbi porttitor lorem id ligula.", + "Maecenas pulvinar lobortis est.", + "Aenean lectus.", + "Phasellus sit amet erat.", + "Duis at velit eu est congue elementum.", + "Proin risus.", + "Suspendisse potenti.", + "Quisque id justo sit amet sapien dignissim vestibulum." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.HttpErrorResponseProblem", + "description": "Etiam vel augue.", + "summary": "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "stampText": "2016-06-30T18:59:38Z", + "epoch": "1439625535", + "detailsXml": { + "exception": { + "name": "NullaSuspendisse.js", + "message": "Phasellus in felis. Donec semper sapien a libero. Nam dui.", + "stacktrace": { + "line": [ + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Nunc rhoncus dui vel sem.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Cras in purus eu magna vulputate luctus.", + "Vestibulum rutrum rutrum neque.", + "Cras in purus eu magna vulputate luctus.", + "Mauris lacinia sapien quis libero.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Nullam molestie nibh in lectus.", + "Ut tellus.", + "Morbi a ipsum.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Integer ac leo.", + "Curabitur gravida nisi at nibh.", + "Phasellus id sapien in sapien iaculis congue.", + "Vivamus tortor.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Nulla tempus.", + "Curabitur gravida nisi at nibh.", + "Suspendisse accumsan tortor quis turpis.", + "Mauris ullamcorper purus sit amet nulla.", + "Duis bibendum." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.HMSNotAuthenticatedProblem", + "description": "Nunc purus.", + "summary": "Sed sagittis. Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "stampText": "2016-06-21T10:22:12Z", + "epoch": "1439450598", + "detailsXml": { + "exception": { + "name": "OdioOdioElementum.css", + "message": "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla. Nunc purus. Phasellus in felis.", + "stacktrace": { + "line": [ + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Morbi non lectus.", + "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "Pellentesque at nulla.", + "Etiam vel augue.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "In quis justo.", + "Duis at velit eu est congue elementum.", + "Nunc rhoncus dui vel sem.", + "Maecenas pulvinar lobortis est.", + "Curabitur at ipsum ac tellus semper interdum.", + "Quisque porta volutpat erat.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "Morbi a ipsum.", + "In congue.", + "Praesent lectus.", + "Curabitur convallis.", + "Donec posuere metus vitae ipsum." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.AdapterDatabaseProblem", + "description": "Donec vitae nisi.", + "summary": "Suspendisse ornare consequat lectus. In est risus, auctor sed, tristique in, tempus sit amet, sem. Fusce consequat.", + "stampText": "2015-09-17T07:23:28Z", + "epoch": "1452735093", + "detailsXml": { + "exception": { + "name": "VestibulumVelitId.css", + "message": "Aliquam non mauris.", + "stacktrace": { + "line": [ + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Nulla tellus.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Integer ac leo.", + "Pellentesque ultrices mattis odio.", + "Sed ante.", + "Aenean sit amet justo.", + "Pellentesque at nulla.", + "Phasellus sit amet erat.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Aenean lectus.", + "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "In hac habitasse platea dictumst.", + "Duis bibendum.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Ut at dolor quis odio consequat varius.", + "Nulla mollis molestie lorem.", + "Nunc purus.", + "Vivamus tortor.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Cras in purus eu magna vulputate luctus.", + "In hac habitasse platea dictumst.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Nulla tellus.", + "Praesent blandit lacinia erat.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Praesent blandit.", + "In blandit ultrices enim.", + "Curabitur convallis.", + "Proin risus.", + "Nullam porttitor lacus at turpis.", + "Vestibulum sed magna at nunc commodo placerat.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Maecenas ut massa quis augue luctus tincidunt.", + "In hac habitasse platea dictumst.", + "In blandit ultrices enim." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.QueryNotFound", + "description": "Duis bibendum.", + "summary": "In hac habitasse platea dictumst. Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem. Integer tincidunt ante vel ipsum.", + "stampText": "2015-09-06T09:08:14Z", + "epoch": "1456823507", + "detailsXml": { + "exception": { + "name": "Nulla.java", + "message": "Nulla tellus.", + "stacktrace": { + "line": [ + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Aliquam non mauris.", + "Cras non velit nec nisi vulputate nonummy.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Morbi ut odio.", + "Proin at turpis a pede posuere nonummy.", + "Fusce posuere felis sed lacus.", + "Quisque porta volutpat erat.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Sed accumsan felis.", + "Maecenas tincidunt lacus at velit.", + "Morbi non quam nec dui luctus rutrum.", + "Vivamus vestibulum sagittis sapien.", + "Mauris sit amet eros.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Suspendisse potenti.", + "Vivamus vestibulum sagittis sapien.", + "Cras non velit nec nisi vulputate nonummy.", + "Nulla tellus.", + "Vestibulum sed magna at nunc commodo placerat.", + "Integer ac leo.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", + "Suspendisse potenti.", + "Etiam faucibus cursus urna.", + "Mauris lacinia sapien quis libero.", + "Maecenas tincidunt lacus at velit.", + "In sagittis dui vel nisl.", + "Nulla ac enim.", + "Aenean sit amet justo.", + "Proin eu mi.", + "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Vivamus vestibulum sagittis sapien.", + "Curabitur at ipsum ac tellus semper interdum.", + "In hac habitasse platea dictumst.", + "Aenean sit amet justo.", + "In hac habitasse platea dictumst.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Praesent id massa id nisl venenatis lacinia.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante.", + "Nam tristique tortor eu pede.", + "Morbi porttitor lorem id ligula.", + "Morbi non quam nec dui luctus rutrum.", + "In congue.", + "Morbi non quam nec dui luctus rutrum." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.CouldNotInterpretResponseFromPmCell", + "description": "Nullam sit amet turpis elementum ligula vehicula consequat.", + "summary": "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "stampText": "2016-07-02T13:17:38Z", + "epoch": "1446474669", + "detailsXml": { + "exception": { + "name": "Pellentesque.js", + "message": "Maecenas ut massa quis augue luctus tincidunt. Nulla mollis molestie lorem.", + "stacktrace": { + "line": [ + "Integer tincidunt ante vel ipsum.", + "Aliquam quis turpis eget elit sodales scelerisque.", + "Nam tristique tortor eu pede.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Pellentesque at nulla.", + "Nunc purus.", + "Integer a nibh.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Duis mattis egestas metus.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Donec quis orci eget orci vehicula condimentum.", + "Cras non velit nec nisi vulputate nonummy.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "Aliquam erat volutpat.", + "Morbi non quam nec dui luctus rutrum.", + "Praesent blandit lacinia erat.", + "Mauris lacinia sapien quis libero.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Donec vitae nisi.", + "Etiam faucibus cursus urna." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.AdapterDatabaseProblem", + "description": "Donec quis orci eget orci vehicula condimentum.", + "summary": "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci. Nullam molestie nibh in lectus.", + "stampText": "2016-06-07T12:59:14Z", + "epoch": "1445545358", + "detailsXml": { + "exception": { + "name": "VolutpatDui.js", + "message": "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci. Mauris lacinia sapien quis libero.", + "stacktrace": { + "line": [ + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Donec quis orci eget orci vehicula condimentum.", + "Vestibulum sed magna at nunc commodo placerat.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Proin at turpis a pede posuere nonummy.", + "Donec ut dolor.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Praesent blandit.", + "Proin eu mi.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Donec vitae nisi.", + "Praesent blandit.", + "Donec semper sapien a libero.", + "Morbi a ipsum.", + "Proin risus.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Vivamus vestibulum sagittis sapien.", + "Donec ut dolor.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis." + ] + } + } + } +}, { + "codec":"net.shrine.qep.PreviousQueryDoesNotExist", + "description": "Donec ut mauris eget massa tempor convallis.", + "summary": "Proin interdum mauris non ligula pellentesque ultrices. Phasellus id sapien in sapien iaculis congue. Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "stampText": "2015-09-16T18:57:53Z", + "epoch": "1461038223", + "detailsXml": { + "exception": { + "name": "ConsectetuerAdipiscing.js", + "message": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio. Curabitur convallis.", + "stacktrace": { + "line": [ + "Praesent lectus.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Phasellus in felis.", + "Nam dui.", + "Vestibulum sed magna at nunc commodo placerat.", + "Praesent blandit.", + "Morbi non quam nec dui luctus rutrum.", + "Nulla mollis molestie lorem.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Integer a nibh.", + "Proin at turpis a pede posuere nonummy.", + "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Phasellus id sapien in sapien iaculis congue.", + "In quis justo.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "Fusce consequat.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Praesent id massa id nisl venenatis lacinia.", + "Maecenas rhoncus aliquam lacus.", + "Nunc purus.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Nullam porttitor lacus at turpis.", + "Praesent id massa id nisl venenatis lacinia.", + "Donec ut mauris eget massa tempor convallis.", + "Vivamus tortor.", + "Mauris sit amet eros.", + "Sed ante.", + "Maecenas pulvinar lobortis est.", + "Aenean auctor gravida sem.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "In blandit ultrices enim.", + "Aliquam non mauris." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.AdapterDatabaseProblem", + "description": "Aenean fermentum.", + "summary": "Quisque ut erat. Curabitur gravida nisi at nibh.", + "stampText": "2015-10-16T20:29:48Z", + "epoch": "1445211251", + "detailsXml": { + "exception": { + "name": "TristiqueIn.js", + "message": "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "stacktrace": { + "line": [ + "Vivamus tortor.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Phasellus sit amet erat.", + "Cras non velit nec nisi vulputate nonummy.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Etiam pretium iaculis justo.", + "Maecenas tincidunt lacus at velit.", + "Vestibulum ac est lacinia nisi venenatis tristique.", + "Mauris ullamcorper purus sit amet nulla.", + "Maecenas tincidunt lacus at velit.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Curabitur at ipsum ac tellus semper interdum.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Morbi non lectus.", + "In hac habitasse platea dictumst.", + "Aenean fermentum.", + "Etiam justo.", + "Nullam porttitor lacus at turpis.", + "Donec dapibus.", + "Nulla suscipit ligula in lacus.", + "In sagittis dui vel nisl.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Nunc nisl.", + "Suspendisse potenti.", + "Aenean sit amet justo.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Vivamus tortor.", + "Duis at velit eu est congue elementum.", + "Cras pellentesque volutpat dui.", + "Nunc rhoncus dui vel sem.", + "Fusce consequat.", + "Integer ac leo.", + "Duis mattis egestas metus.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Praesent lectus.", + "Praesent blandit." + ] + } + } + } +}]} \ No newline at end of file diff --git a/apps/dashboard-app/src/main/js/test/admin/status/problems?offset=20.json b/apps/dashboard-app/src/main/js/test/admin/status/problems?offset=20.json new file mode 100644 index 000000000..32c10838b --- /dev/null +++ b/apps/dashboard-app/src/main/js/test/admin/status/problems?offset=20.json @@ -0,0 +1,999 @@ +{"size":63,"offset":20,"n":20,"problems": +[{ + "codec":"net.shrine.adapter.components.QueryNotInDatabase", + "description": "Suspendisse potenti.", + "summary": "Maecenas ut massa quis augue luctus tincidunt. Nulla mollis molestie lorem. Quisque ut erat.", + "stampText": "2015-10-03T22:50:09Z", + "epoch": "1458628756", + "detailsXml": { + "exception": { + "name": "AIpsumInteger.java", + "message": "In sagittis dui vel nisl. Duis ac nibh. Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "stacktrace": { + "line": [ + "Nulla tempus.", + "Nulla mollis molestie lorem.", + "Integer a nibh.", + "Vivamus vestibulum sagittis sapien.", + "Duis ac nibh.", + "Aenean fermentum.", + "Donec quis orci eget orci vehicula condimentum.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Duis mattis egestas metus.", + "Nunc purus.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Etiam justo.", + "Nulla ut erat id mauris vulputate elementum.", + "Nulla mollis molestie lorem.", + "Aenean sit amet justo.", + "Morbi porttitor lorem id ligula.", + "Maecenas rhoncus aliquam lacus.", + "Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante.", + "Mauris ullamcorper purus sit amet nulla.", + "Integer ac leo.", + "Nunc rhoncus dui vel sem.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Ut at dolor quis odio consequat varius.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Vestibulum sed magna at nunc commodo placerat.", + "Nulla suscipit ligula in lacus.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Ut at dolor quis odio consequat varius.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Donec vitae nisi.", + "Mauris sit amet eros.", + "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "Integer ac neque.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Nulla ac enim.", + "In hac habitasse platea dictumst.", + "Suspendisse ornare consequat lectus.", + "In sagittis dui vel nisl.", + "Phasellus id sapien in sapien iaculis congue.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Sed vel enim sit amet nunc viverra dapibus.", + "In hac habitasse platea dictumst.", + "Aenean fermentum.", + "Praesent blandit.", + "Etiam pretium iaculis justo.", + "Pellentesque eget nunc.", + "Nulla tempus.", + "In hac habitasse platea dictumst." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.CouldNotReachPmCell", + "description": "Nam dui.", + "summary": "Nunc rhoncus dui vel sem. Sed sagittis.", + "stampText": "2015-09-16T10:52:48Z", + "epoch": "1460014356", + "detailsXml": { + "exception": { + "name": "FelisUt.js", + "message": "Sed vel enim sit amet nunc viverra dapibus.", + "stacktrace": { + "line": [ + "Cras non velit nec nisi vulputate nonummy.", + "Phasellus id sapien in sapien iaculis congue.", + "Vivamus tortor.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Suspendisse potenti.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Donec ut mauris eget massa tempor convallis.", + "Ut at dolor quis odio consequat varius.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Vivamus vestibulum sagittis sapien.", + "In congue.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Aenean fermentum.", + "Donec semper sapien a libero.", + "Praesent id massa id nisl venenatis lacinia.", + "In hac habitasse platea dictumst.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Duis mattis egestas metus.", + "Morbi porttitor lorem id ligula.", + "In congue.", + "Cras pellentesque volutpat dui.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Morbi porttitor lorem id ligula.", + "Nullam molestie nibh in lectus.", + "Nulla tempus.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Nam tristique tortor eu pede.", + "In eleifend quam a odio.", + "Nullam varius.", + "Morbi a ipsum.", + "Integer ac leo.", + "In hac habitasse platea dictumst.", + "Morbi non lectus.", + "Nam dui.", + "Fusce posuere felis sed lacus.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Praesent id massa id nisl venenatis lacinia.", + "Curabitur in libero ut massa volutpat convallis." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.QueryResultNotAvailable", + "description": "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "summary": "Pellentesque viverra pede ac diam.", + "stampText": "2016-07-19T06:32:56Z", + "epoch": "1442947556", + "detailsXml": { + "exception": { + "name": "Condimentum.js", + "message": "Suspendisse accumsan tortor quis turpis.", + "stacktrace": { + "line": [ + "In blandit ultrices enim.", + "Vestibulum ac est lacinia nisi venenatis tristique.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Nulla suscipit ligula in lacus.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "In sagittis dui vel nisl.", + "Aenean lectus.", + "Sed ante.", + "Praesent blandit.", + "Nulla tellus.", + "Ut at dolor quis odio consequat varius.", + "In congue.", + "Nam nulla.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Vivamus tortor.", + "Maecenas tincidunt lacus at velit.", + "Donec ut mauris eget massa tempor convallis.", + "Morbi a ipsum.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Vivamus vestibulum sagittis sapien.", + "Proin at turpis a pede posuere nonummy.", + "Proin risus.", + "Pellentesque at nulla.", + "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "Morbi non lectus.", + "Fusce consequat.", + "Mauris ullamcorper purus sit amet nulla.", + "Vestibulum sed magna at nunc commodo placerat.", + "Nam dui." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.CouldNotParseResultsProblem", + "description": "Nulla ac enim.", + "summary": "Sed vel enim sit amet nunc viverra dapibus. Nulla suscipit ligula in lacus.", + "stampText": "2015-11-02T13:49:11Z", + "epoch": "1442592113", + "detailsXml": { + "exception": { + "name": "SemperPortaVolutpat.html", + "message": "Nulla ac enim.", + "stacktrace": { + "line": [ + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Sed sagittis.", + "Integer ac neque.", + "Morbi a ipsum.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Quisque ut erat.", + "Pellentesque ultrices mattis odio.", + "Curabitur in libero ut massa volutpat convallis.", + "Duis ac nibh.", + "Donec vitae nisi.", + "Sed sagittis.", + "Donec dapibus.", + "Integer non velit.", + "Nulla suscipit ligula in lacus.", + "Phasellus id sapien in sapien iaculis congue.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante.", + "Integer ac neque.", + "Suspendisse potenti.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Nulla mollis molestie lorem.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Curabitur at ipsum ac tellus semper interdum." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.dao.squeryl.BreakdownFailure", + "description": "Morbi vel lectus in quam fringilla rhoncus.", + "summary": "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "stampText": "2015-07-26T11:40:55Z", + "epoch": "1465928224", + "detailsXml": { + "exception": { + "name": "NislNuncNisl.css", + "message": "Curabitur gravida nisi at nibh.", + "stacktrace": { + "line": [ + "Nulla facilisi.", + "Quisque porta volutpat erat.", + "In hac habitasse platea dictumst.", + "Nullam molestie nibh in lectus.", + "Etiam justo.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Donec semper sapien a libero.", + "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "In hac habitasse platea dictumst.", + "Fusce posuere felis sed lacus.", + "Duis bibendum.", + "Mauris lacinia sapien quis libero.", + "Vivamus vestibulum sagittis sapien.", + "Donec vitae nisi.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Donec semper sapien a libero.", + "Vivamus vestibulum sagittis sapien.", + "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Sed accumsan felis.", + "Duis mattis egestas metus.", + "Duis mattis egestas metus.", + "In congue.", + "Maecenas tincidunt lacus at velit.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Nunc nisl.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.AdapterMappingProblem", + "description": "Maecenas rhoncus aliquam lacus.", + "summary": "Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo placerat. Praesent blandit.", + "stampText": "2016-04-09T09:05:27Z", + "epoch": "1456898457", + "detailsXml": { + "exception": { + "name": "QuisLectus.java", + "message": "Morbi vel lectus in quam fringilla rhoncus.", + "stacktrace": { + "line": [ + "Nulla nisl.", + "Vivamus tortor.", + "Donec vitae nisi.", + "Nunc nisl.", + "Phasellus sit amet erat.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "Aliquam non mauris.", + "Proin interdum mauris non ligula pellentesque ultrices.", + "Maecenas pulvinar lobortis est.", + "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "Phasellus id sapien in sapien iaculis congue.", + "Aenean fermentum.", + "Nulla ac enim.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Curabitur gravida nisi at nibh.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Morbi non lectus.", + "Sed accumsan felis." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.QueryResultNotAvailable", + "description": "Suspendisse potenti.", + "summary": "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "stampText": "2016-03-06T22:12:33Z", + "epoch": "1467061666", + "detailsXml": { + "exception": { + "name": "DolorSitAmet.java", + "message": "Cras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit. Vivamus vel nulla eget eros elementum pellentesque.", + "stacktrace": { + "line": [ + "Nulla nisl.", + "Phasellus sit amet erat.", + "Ut at dolor quis odio consequat varius.", + "Phasellus id sapien in sapien iaculis congue.", + "Cras pellentesque volutpat dui.", + "Sed ante.", + "Nulla suscipit ligula in lacus.", + "Morbi non quam nec dui luctus rutrum.", + "Morbi ut odio.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Curabitur convallis.", + "Proin at turpis a pede posuere nonummy.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Nulla nisl.", + "Quisque ut erat.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Maecenas pulvinar lobortis est.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Duis at velit eu est congue elementum.", + "In eleifend quam a odio.", + "Donec quis orci eget orci vehicula condimentum.", + "Integer non velit.", + "Praesent blandit lacinia erat.", + "In hac habitasse platea dictumst.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "In quis justo.", + "Nulla ac enim.", + "Suspendisse potenti.", + "Aliquam quis turpis eget elit sodales scelerisque.", + "Vivamus tortor.", + "In hac habitasse platea dictumst.", + "Aenean auctor gravida sem.", + "Donec dapibus.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Etiam justo.", + "Fusce consequat.", + "Pellentesque ultrices mattis odio.", + "Vivamus vestibulum sagittis sapien.", + "Nulla mollis molestie lorem.", + "Pellentesque ultrices mattis odio.", + "Quisque ut erat.", + "Nulla tellus.", + "Maecenas tincidunt lacus at velit.", + "Praesent blandit lacinia erat.", + "Integer ac leo.", + "Duis mattis egestas metus.", + "Nam nulla." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.MissingRequiredRoles", + "description": "Mauris lacinia sapien quis libero.", + "summary": "Suspendisse potenti. Nullam porttitor lacus at turpis. Donec posuere metus vitae ipsum.", + "stampText": "2016-02-25T05:15:03Z", + "epoch": "1464961636", + "detailsXml": { + "exception": { + "name": "Suspendisse.js", + "message": "Etiam vel augue. Vestibulum rutrum rutrum neque.", + "stacktrace": { + "line": [ + "In quis justo.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "In blandit ultrices enim.", + "Nulla facilisi.", + "Curabitur gravida nisi at nibh.", + "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "Vivamus tortor.", + "Phasellus in felis.", + "Nullam molestie nibh in lectus.", + "Nunc purus.", + "Aenean fermentum.", + "Duis at velit eu est congue elementum.", + "Maecenas rhoncus aliquam lacus.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "Sed ante.", + "Aliquam non mauris.", + "Nunc rhoncus dui vel sem.", + "Integer a nibh.", + "Etiam vel augue.", + "Praesent blandit.", + "Maecenas pulvinar lobortis est." + ] + } + } + } +}, { + "codec":"net.shrine.qep.PreviousQueryDoesNotExist", + "description": "Duis bibendum.", + "summary": "Sed sagittis. Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "stampText": "2016-06-28T20:07:06Z", + "epoch": "1454371391", + "detailsXml": { + "exception": { + "name": "IpsumDolor.java", + "message": "Proin interdum mauris non ligula pellentesque ultrices. Phasellus id sapien in sapien iaculis congue. Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "stacktrace": { + "line": [ + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Nulla facilisi.", + "Maecenas tincidunt lacus at velit.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Vestibulum sed magna at nunc commodo placerat.", + "Sed sagittis.", + "Proin eu mi.", + "Nulla tellus.", + "Donec dapibus.", + "Pellentesque at nulla.", + "Etiam vel augue.", + "Nunc nisl.", + "Aenean sit amet justo.", + "Quisque porta volutpat erat.", + "Ut at dolor quis odio consequat varius.", + "Pellentesque ultrices mattis odio.", + "Duis mattis egestas metus.", + "Aliquam quis turpis eget elit sodales scelerisque.", + "Pellentesque at nulla.", + "Pellentesque ultrices mattis odio." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.NoValidResponsesToAggregate", + "description": "Vivamus vel nulla eget eros elementum pellentesque.", + "summary": "Aenean fermentum.", + "stampText": "2016-07-13T08:56:17Z", + "epoch": "1461523308", + "detailsXml": { + "exception": { + "name": "DonecOdio.js", + "message": "Proin leo odio, porttitor id, consequat in, consequat ut, nulla. Sed accumsan felis.", + "stacktrace": { + "line": [ + "Proin interdum mauris non ligula pellentesque ultrices.", + "Praesent blandit lacinia erat.", + "Nunc rhoncus dui vel sem.", + "Nulla ac enim.", + "In hac habitasse platea dictumst.", + "Fusce consequat.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "In eleifend quam a odio.", + "Proin interdum mauris non ligula pellentesque ultrices.", + "Donec semper sapien a libero.", + "Maecenas tincidunt lacus at velit.", + "Nam dui.", + "Etiam justo.", + "Proin eu mi.", + "Cras in purus eu magna vulputate luctus.", + "Nulla tellus.", + "Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante.", + "Integer tincidunt ante vel ipsum.", + "Cras non velit nec nisi vulputate nonummy.", + "Nullam varius.", + "Proin eu mi.", + "Donec posuere metus vitae ipsum.", + "Aenean fermentum.", + "Nullam sit amet turpis elementum ligula vehicula consequat." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.CrcCouldNotBeInvoked", + "description": "Aenean sit amet justo.", + "summary": "Nullam sit amet turpis elementum ligula vehicula consequat. Morbi a ipsum. Integer a nibh.", + "stampText": "2016-01-24T17:10:44Z", + "epoch": "1462758106", + "detailsXml": { + "exception": { + "name": "VehiculaCondimentumCurabitur.css", + "message": "Vivamus in felis eu sapien cursus vestibulum. Proin eu mi. Nulla ac enim.", + "stacktrace": { + "line": [ + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Aenean fermentum.", + "Nulla ut erat id mauris vulputate elementum.", + "Donec dapibus.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Ut at dolor quis odio consequat varius.", + "Nulla facilisi.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Pellentesque at nulla.", + "Nulla tellus.", + "Aenean auctor gravida sem.", + "Mauris ullamcorper purus sit amet nulla.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Integer ac leo.", + "Nulla justo.", + "Nullam molestie nibh in lectus.", + "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Nam dui.", + "Curabitur at ipsum ac tellus semper interdum.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", + "Nam dui.", + "Suspendisse accumsan tortor quis turpis.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "In sagittis dui vel nisl.", + "Maecenas tincidunt lacus at velit.", + "Nunc rhoncus dui vel sem.", + "Suspendisse potenti.", + "In eleifend quam a odio.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.client.HttpErrorCodeFromAdapter", + "description": "Curabitur gravida nisi at nibh.", + "summary": "Nulla facilisi.", + "stampText": "2015-11-18T00:04:24Z", + "epoch": "1466089609", + "detailsXml": { + "exception": { + "name": "TemporConvallis.css", + "message": "Cras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit.", + "stacktrace": { + "line": [ + "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Nulla ac enim.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Donec semper sapien a libero.", + "Nam dui.", + "Integer non velit.", + "Aenean lectus.", + "Fusce consequat.", + "Nulla mollis molestie lorem.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Aliquam erat volutpat.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "Nullam varius.", + "Suspendisse accumsan tortor quis turpis.", + "Duis bibendum.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Quisque porta volutpat erat.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc." + ] + } + } + } +}, { + "codec":"net.shrine.qep.queries.QepDatabaseProblem", + "description": "In eleifend quam a odio.", + "summary": "Suspendisse accumsan tortor quis turpis.", + "stampText": "2016-02-20T17:29:20Z", + "epoch": "1450981215", + "detailsXml": { + "exception": { + "name": "CuraeMauris.js", + "message": "Vivamus vestibulum sagittis sapien.", + "stacktrace": { + "line": [ + "Suspendisse ornare consequat lectus.", + "Nulla ac enim.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Aenean fermentum.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Duis at velit eu est congue elementum.", + "Cras in purus eu magna vulputate luctus.", + "Integer ac neque.", + "In blandit ultrices enim.", + "Suspendisse ornare consequat lectus.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Ut tellus.", + "Pellentesque viverra pede ac diam.", + "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "Sed accumsan felis.", + "Aenean lectus.", + "Duis mattis egestas metus.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Curabitur at ipsum ac tellus semper interdum.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Morbi porttitor lorem id ligula.", + "Nulla ut erat id mauris vulputate elementum.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.CouldNotReachPmCell", + "description": "In hac habitasse platea dictumst.", + "summary": "Maecenas ut massa quis augue luctus tincidunt.", + "stampText": "2015-11-26T11:15:13Z", + "epoch": "1446755667", + "detailsXml": { + "exception": { + "name": "Eget.js", + "message": "Duis mattis egestas metus. Aenean fermentum. Donec ut mauris eget massa tempor convallis.", + "stacktrace": { + "line": [ + "Nullam porttitor lacus at turpis.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Integer non velit.", + "Nulla tellus.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Aliquam non mauris.", + "Vivamus tortor.", + "Donec ut dolor.", + "Cras non velit nec nisi vulputate nonummy.", + "Proin risus.", + "Suspendisse potenti.", + "Morbi non lectus.", + "Morbi non lectus.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Etiam vel augue.", + "In eleifend quam a odio.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Aliquam non mauris.", + "Pellentesque eget nunc.", + "Proin eu mi.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Mauris lacinia sapien quis libero.", + "Cras pellentesque volutpat dui.", + "Nulla tellus.", + "Nulla suscipit ligula in lacus.", + "Maecenas pulvinar lobortis est.", + "Praesent id massa id nisl venenatis lacinia.", + "Aenean lectus.", + "Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Sed accumsan felis.", + "Suspendisse accumsan tortor quis turpis.", + "Suspendisse ornare consequat lectus.", + "Sed sagittis.", + "Etiam vel augue.", + "Sed sagittis.", + "Duis aliquam convallis nunc.", + "Phasellus sit amet erat.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Vestibulum ac est lacinia nisi venenatis tristique.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Praesent blandit lacinia erat.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Aenean sit amet justo.", + "Aliquam quis turpis eget elit sodales scelerisque.", + "In hac habitasse platea dictumst." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.HttpErrorResponseProblem", + "description": "Aenean sit amet justo.", + "summary": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "stampText": "2015-11-20T07:50:04Z", + "epoch": "1446298525", + "detailsXml": { + "exception": { + "name": "Auctor.java", + "message": "Integer tincidunt ante vel ipsum. Praesent blandit lacinia erat.", + "stacktrace": { + "line": [ + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Cras non velit nec nisi vulputate nonummy.", + "Curabitur at ipsum ac tellus semper interdum.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "Maecenas tincidunt lacus at velit.", + "Donec ut dolor.", + "Praesent blandit.", + "Donec quis orci eget orci vehicula condimentum.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Curabitur in libero ut massa volutpat convallis.", + "Donec semper sapien a libero.", + "Praesent id massa id nisl venenatis lacinia.", + "Donec quis orci eget orci vehicula condimentum.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Maecenas pulvinar lobortis est.", + "Phasellus sit amet erat.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Aliquam erat volutpat.", + "Curabitur in libero ut massa volutpat convallis.", + "Proin at turpis a pede posuere nonummy.", + "In blandit ultrices enim.", + "Sed ante.", + "Integer ac neque.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Donec semper sapien a libero.", + "Duis mattis egestas metus.", + "Nulla mollis molestie lorem.", + "Fusce posuere felis sed lacus.", + "Phasellus id sapien in sapien iaculis congue.", + "Donec vitae nisi.", + "Sed ante.", + "Cras non velit nec nisi vulputate nonummy.", + "Ut at dolor quis odio consequat varius.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Etiam justo.", + "Ut tellus.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Curabitur at ipsum ac tellus semper interdum.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Aenean auctor gravida sem.", + "In hac habitasse platea dictumst.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Maecenas tincidunt lacus at velit.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.ErrorStatusFromDataStewardApp", + "description": "Morbi non lectus.", + "summary": "Nullam sit amet turpis elementum ligula vehicula consequat.", + "stampText": "2016-04-20T00:58:49Z", + "epoch": "1445883641", + "detailsXml": { + "exception": { + "name": "Tellus.js", + "message": "Vestibulum rutrum rutrum neque. Aenean auctor gravida sem.", + "stacktrace": { + "line": [ + "Donec ut mauris eget massa tempor convallis.", + "Nulla suscipit ligula in lacus.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Integer ac leo.", + "Pellentesque at nulla.", + "Aliquam non mauris.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Pellentesque eget nunc.", + "Maecenas rhoncus aliquam lacus.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Phasellus id sapien in sapien iaculis congue.", + "Duis at velit eu est congue elementum.", + "Aenean fermentum.", + "Phasellus id sapien in sapien iaculis congue.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Nam nulla.", + "Pellentesque at nulla.", + "Etiam vel augue.", + "Maecenas pulvinar lobortis est.", + "Etiam faucibus cursus urna.", + "In hac habitasse platea dictumst.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Quisque ut erat.", + "Fusce posuere felis sed lacus.", + "Maecenas pulvinar lobortis est.", + "In blandit ultrices enim.", + "Mauris sit amet eros.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Nulla facilisi.", + "Maecenas rhoncus aliquam lacus.", + "Praesent blandit lacinia erat.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Nulla facilisi.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.CouldNotReachPmCell", + "description": "Proin interdum mauris non ligula pellentesque ultrices.", + "summary": "Praesent id massa id nisl venenatis lacinia.", + "stampText": "2015-11-27T09:03:10Z", + "epoch": "1444653978", + "detailsXml": { + "exception": { + "name": "EtiamFaucibusCursus.js", + "message": "In sagittis dui vel nisl. Duis ac nibh. Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "stacktrace": { + "line": [ + "Suspendisse ornare consequat lectus.", + "Praesent lectus.", + "Suspendisse ornare consequat lectus.", + "Etiam justo.", + "Praesent lectus.", + "Fusce consequat.", + "Cras in purus eu magna vulputate luctus.", + "Quisque porta volutpat erat.", + "Phasellus id sapien in sapien iaculis congue.", + "Vivamus tortor.", + "Vivamus tortor.", + "Praesent id massa id nisl venenatis lacinia.", + "Nulla suscipit ligula in lacus.", + "Cras non velit nec nisi vulputate nonummy.", + "Donec ut mauris eget massa tempor convallis.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Duis at velit eu est congue elementum.", + "Etiam justo.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Nam nulla.", + "Nulla ut erat id mauris vulputate elementum.", + "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "Aliquam quis turpis eget elit sodales scelerisque.", + "Cras non velit nec nisi vulputate nonummy.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Vestibulum sed magna at nunc commodo placerat.", + "Curabitur convallis.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Pellentesque at nulla.", + "Nullam varius.", + "In eleifend quam a odio.", + "Etiam faucibus cursus urna.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Integer ac leo.", + "Etiam vel augue.", + "Proin at turpis a pede posuere nonummy.", + "Proin risus.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "In eleifend quam a odio.", + "Fusce consequat.", + "Etiam justo." + ] + } + } + } +}, { + "codec":"net.shrine.protocol.ErrorStatusFromCrc", + "description": "Nunc nisl.", + "summary": "Quisque porta volutpat erat. Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "stampText": "2016-04-30T05:54:34Z", + "epoch": "1468661819", + "detailsXml": { + "exception": { + "name": "CommodoPlaceratPraesent.java", + "message": "Donec quis orci eget orci vehicula condimentum. Curabitur in libero ut massa volutpat convallis. Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "stacktrace": { + "line": [ + "In quis justo.", + "Donec quis orci eget orci vehicula condimentum.", + "In hac habitasse platea dictumst.", + "Donec posuere metus vitae ipsum.", + "Proin eu mi.", + "Aenean fermentum.", + "Nunc purus.", + "Phasellus in felis.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "In blandit ultrices enim.", + "Suspendisse potenti.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Duis at velit eu est congue elementum.", + "Nunc rhoncus dui vel sem.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Maecenas rhoncus aliquam lacus.", + "Maecenas rhoncus aliquam lacus.", + "Etiam pretium iaculis justo.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Pellentesque eget nunc.", + "Donec vitae nisi.", + "Etiam faucibus cursus urna.", + "Aenean sit amet justo.", + "Sed accumsan felis." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.dao.squeryl.BreakdownFailure", + "description": "Nunc nisl.", + "summary": "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis. Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "stampText": "2016-01-23T13:09:51Z", + "epoch": "1467999258", + "detailsXml": { + "exception": { + "name": "Eget.js", + "message": "Proin leo odio, porttitor id, consequat in, consequat ut, nulla. Sed accumsan felis. Ut at dolor quis odio consequat varius.", + "stacktrace": { + "line": [ + "Quisque porta volutpat erat.", + "Pellentesque viverra pede ac diam.", + "In hac habitasse platea dictumst.", + "Pellentesque eget nunc.", + "Aenean fermentum.", + "In hac habitasse platea dictumst.", + "Nunc purus.", + "Cras pellentesque volutpat dui.", + "Donec vitae nisi.", + "Suspendisse potenti.", + "Praesent blandit lacinia erat.", + "Nam dui.", + "Pellentesque ultrices mattis odio.", + "Pellentesque viverra pede ac diam.", + "In hac habitasse platea dictumst.", + "Donec posuere metus vitae ipsum.", + "Donec ut dolor.", + "Nulla ut erat id mauris vulputate elementum.", + "Nullam porttitor lacus at turpis.", + "Suspendisse potenti.", + "In hac habitasse platea dictumst.", + "Nulla suscipit ligula in lacus.", + "Aliquam non mauris.", + "Morbi ut odio.", + "Maecenas tincidunt lacus at velit.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Cras pellentesque volutpat dui.", + "Donec ut mauris eget massa tempor convallis.", + "Vestibulum ac est lacinia nisi venenatis tristique.", + "Morbi non lectus.", + "Quisque porta volutpat erat.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Nulla tellus.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Praesent blandit.", + "In sagittis dui vel nisl.", + "Quisque id justo sit amet sapien dignissim vestibulum." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.CouldNotConnectToAdapter", + "description": "Nulla nisl.", + "summary": "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem. Duis aliquam convallis nunc. Proin at turpis a pede posuere nonummy.", + "stampText": "2016-06-18T05:27:38Z", + "epoch": "1439300934", + "detailsXml": { + "exception": { + "name": "FaucibusOrciLuctus.js", + "message": "Nullam sit amet turpis elementum ligula vehicula consequat. Morbi a ipsum. Integer a nibh.", + "stacktrace": { + "line": [ + "Duis mattis egestas metus.", + "Ut tellus.", + "Pellentesque ultrices mattis odio.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "In congue.", + "Cras non velit nec nisi vulputate nonummy.", + "Phasellus sit amet erat.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Etiam vel augue.", + "Aliquam non mauris.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Nulla facilisi.", + "Nulla facilisi.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Vivamus tortor.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Duis mattis egestas metus.", + "Donec posuere metus vitae ipsum.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Proin at turpis a pede posuere nonummy.", + "Pellentesque eget nunc.", + "Etiam pretium iaculis justo.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Duis aliquam convallis nunc.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Integer non velit.", + "Vestibulum rutrum rutrum neque.", + "In hac habitasse platea dictumst.", + "Etiam justo.", + "In quis justo.", + "Duis ac nibh.", + "Curabitur convallis.", + "Vestibulum ac est lacinia nisi venenatis tristique.", + "Aliquam erat volutpat.", + "Cras pellentesque volutpat dui.", + "Maecenas rhoncus aliquam lacus.", + "Pellentesque eget nunc.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Nulla tempus.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla." + ] + } + } + } +}]} \ No newline at end of file diff --git a/apps/dashboard-app/src/main/js/test/admin/status/problems?offset=40.json b/apps/dashboard-app/src/main/js/test/admin/status/problems?offset=40.json new file mode 100644 index 000000000..4199ef1e2 --- /dev/null +++ b/apps/dashboard-app/src/main/js/test/admin/status/problems?offset=40.json @@ -0,0 +1,1035 @@ +{"size":63,"offset":40,"n":20,"problems": +[{ + "codec":"net.shrine.protocol.ErrorStatusFromCrc", + "description": "Nulla suscipit ligula in lacus.", + "summary": "Donec ut mauris eget massa tempor convallis. Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh. Quisque id justo sit amet sapien dignissim vestibulum.", + "stampText": "2015-09-08T11:27:03Z", + "epoch": "1446494372", + "detailsXml": { + "exception": { + "name": "Nisl.js", + "message": "In sagittis dui vel nisl. Duis ac nibh.", + "stacktrace": { + "line": [ + "Nulla facilisi.", + "Aliquam non mauris.", + "Cras pellentesque volutpat dui.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Fusce consequat.", + "Duis ac nibh.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Aenean fermentum.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Nullam varius.", + "Donec ut mauris eget massa tempor convallis.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Aliquam non mauris.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Aenean lectus.", + "Praesent blandit lacinia erat.", + "Aenean sit amet justo.", + "Nunc nisl.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Nulla justo.", + "Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante.", + "Aenean lectus.", + "Nulla nisl.", + "Pellentesque eget nunc.", + "Suspendisse potenti." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.QueryResultNotAvailable", + "description": "Nunc purus.", + "summary": "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "stampText": "2016-05-21T01:32:50Z", + "epoch": "1444778415", + "detailsXml": { + "exception": { + "name": "DiamIdOrnare.java", + "message": "Etiam faucibus cursus urna. Ut tellus.", + "stacktrace": { + "line": [ + "Maecenas pulvinar lobortis est.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "Proin at turpis a pede posuere nonummy.", + "Maecenas pulvinar lobortis est.", + "Nulla suscipit ligula in lacus.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Nam dui.", + "Nulla tellus.", + "Mauris ullamcorper purus sit amet nulla.", + "Donec dapibus.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Nam nulla.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Ut tellus.", + "Aliquam quis turpis eget elit sodales scelerisque.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Cras pellentesque volutpat dui.", + "Ut tellus.", + "Praesent blandit lacinia erat.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Suspendisse accumsan tortor quis turpis.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "Integer a nibh.", + "Sed accumsan felis.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "In hac habitasse platea dictumst.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Nulla tempus.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Aliquam erat volutpat.", + "Sed sagittis.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Curabitur in libero ut massa volutpat convallis.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "In hac habitasse platea dictumst.", + "Morbi porttitor lorem id ligula." + ] + } + } + } +}, { + "codec":"net.shrine.protocol.ErrorStatusFromCrc", + "description": "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "summary": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "stampText": "2015-08-12T13:28:35Z", + "epoch": "1455213769", + "detailsXml": { + "exception": { + "name": "Ligula.js", + "message": "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam vel augue.", + "stacktrace": { + "line": [ + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", + "Duis bibendum.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Donec posuere metus vitae ipsum.", + "Mauris ullamcorper purus sit amet nulla.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Suspendisse ornare consequat lectus.", + "Nulla ac enim.", + "Duis bibendum.", + "Nunc nisl.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Curabitur at ipsum ac tellus semper interdum.", + "Aliquam erat volutpat.", + "Etiam faucibus cursus urna.", + "Morbi non quam nec dui luctus rutrum.", + "Quisque ut erat.", + "Fusce consequat.", + "Cras pellentesque volutpat dui.", + "Phasellus id sapien in sapien iaculis congue.", + "Maecenas rhoncus aliquam lacus.", + "Morbi a ipsum.", + "Donec dapibus.", + "Praesent lectus.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Nulla mollis molestie lorem.", + "Phasellus in felis.", + "Mauris lacinia sapien quis libero.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "In hac habitasse platea dictumst." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.CrcCouldNotBeInvoked", + "description": "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "summary": "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "stampText": "2015-09-15T17:46:00Z", + "epoch": "1460771080", + "detailsXml": { + "exception": { + "name": "AugueASuscipit.css", + "message": "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo. In blandit ultrices enim.", + "stacktrace": { + "line": [ + "Donec ut mauris eget massa tempor convallis.", + "Aliquam quis turpis eget elit sodales scelerisque.", + "Curabitur in libero ut massa volutpat convallis.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Sed sagittis.", + "Nulla nisl.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Vestibulum sed magna at nunc commodo placerat.", + "Sed accumsan felis.", + "Nulla tempus.", + "Integer tincidunt ante vel ipsum.", + "Nulla suscipit ligula in lacus.", + "Nulla suscipit ligula in lacus.", + "Vestibulum sed magna at nunc commodo placerat.", + "Proin at turpis a pede posuere nonummy.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Vivamus tortor.", + "Maecenas rhoncus aliquam lacus.", + "Nulla ut erat id mauris vulputate elementum.", + "Donec ut dolor.", + "Duis ac nibh.", + "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "Duis bibendum.", + "Nulla nisl.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Vivamus tortor.", + "Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Donec vitae nisi.", + "Morbi porttitor lorem id ligula.", + "Suspendisse potenti.", + "In hac habitasse platea dictumst.", + "Suspendisse ornare consequat lectus.", + "Donec semper sapien a libero.", + "Praesent blandit.", + "Nunc nisl.", + "Etiam faucibus cursus urna.", + "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "Pellentesque at nulla.", + "Phasellus sit amet erat.", + "Curabitur convallis.", + "Maecenas pulvinar lobortis est." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.CouldNotInterpretResponseFromPmCell", + "description": "Nulla facilisi.", + "summary": "Vivamus in felis eu sapien cursus vestibulum.", + "stampText": "2016-07-22T11:53:53Z", + "epoch": "1443128443", + "detailsXml": { + "exception": { + "name": "LuctusRutrumNulla.css", + "message": "In blandit ultrices enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "stacktrace": { + "line": [ + "Pellentesque viverra pede ac diam.", + "Pellentesque viverra pede ac diam.", + "Duis aliquam convallis nunc.", + "Ut tellus.", + "Morbi non lectus.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "In hac habitasse platea dictumst.", + "Cras in purus eu magna vulputate luctus.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Nullam porttitor lacus at turpis.", + "Nulla facilisi.", + "Proin risus.", + "Sed ante.", + "Phasellus in felis.", + "Proin at turpis a pede posuere nonummy.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Phasellus id sapien in sapien iaculis congue.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Ut at dolor quis odio consequat varius.", + "Morbi non lectus.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Curabitur in libero ut massa volutpat convallis.", + "Morbi non lectus.", + "Praesent blandit lacinia erat.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Praesent lectus.", + "Aenean fermentum.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Suspendisse potenti." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.HMSNotAuthenticatedProblem", + "description": "Cras non velit nec nisi vulputate nonummy.", + "summary": "Curabitur at ipsum ac tellus semper interdum. Mauris ullamcorper purus sit amet nulla. Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "stampText": "2016-06-18T14:19:43Z", + "epoch": "1459678141", + "detailsXml": { + "exception": { + "name": "InFelisEu.js", + "message": "Donec posuere metus vitae ipsum. Aliquam non mauris.", + "stacktrace": { + "line": [ + "Sed accumsan felis.", + "Etiam faucibus cursus urna.", + "Donec quis orci eget orci vehicula condimentum.", + "Donec dapibus.", + "Nullam molestie nibh in lectus.", + "Integer a nibh.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Quisque porta volutpat erat.", + "Suspendisse ornare consequat lectus.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "In blandit ultrices enim.", + "Duis at velit eu est congue elementum.", + "Pellentesque ultrices mattis odio.", + "Suspendisse potenti.", + "Donec ut dolor.", + "Mauris sit amet eros.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "Praesent id massa id nisl venenatis lacinia.", + "Curabitur gravida nisi at nibh.", + "Nulla ac enim." + ] + } + } + } +}, { + "codec":"net.shrine.authentication.NotAuthenticatedProblem", + "description": "Quisque ut erat.", + "summary": "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo. In blandit ultrices enim.", + "stampText": "2015-11-21T20:16:23Z", + "epoch": "1442888560", + "detailsXml": { + "exception": { + "name": "Et.js", + "message": "Integer non velit. Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "stacktrace": { + "line": [ + "Donec semper sapien a libero.", + "Maecenas tincidunt lacus at velit.", + "Morbi ut odio.", + "In quis justo.", + "Aliquam quis turpis eget elit sodales scelerisque.", + "Pellentesque at nulla.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Etiam justo.", + "Nunc rhoncus dui vel sem.", + "Aenean lectus.", + "Fusce posuere felis sed lacus.", + "Integer non velit.", + "Vivamus tortor.", + "Mauris ullamcorper purus sit amet nulla.", + "Etiam vel augue.", + "In quis justo.", + "Maecenas pulvinar lobortis est.", + "Morbi ut odio.", + "Aenean sit amet justo.", + "Ut tellus.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Donec quis orci eget orci vehicula condimentum.", + "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "Maecenas pulvinar lobortis est.", + "Donec semper sapien a libero." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.ErrorStatusFromDataStewardApp", + "description": "Etiam vel augue.", + "summary": "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo. In blandit ultrices enim.", + "stampText": "2016-05-30T21:28:21Z", + "epoch": "1465148443", + "detailsXml": { + "exception": { + "name": "SapienQuisLibero.js", + "message": "Vestibulum sed magna at nunc commodo placerat. Praesent blandit.", + "stacktrace": { + "line": [ + "Vestibulum sed magna at nunc commodo placerat.", + "Suspendisse ornare consequat lectus.", + "In est risus, auctor sed, tristique in, tempus sit amet, sem.", + "Integer tincidunt ante vel ipsum.", + "Aenean auctor gravida sem.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Suspendisse accumsan tortor quis turpis.", + "Praesent blandit lacinia erat.", + "Integer a nibh.", + "Nam tristique tortor eu pede.", + "Morbi porttitor lorem id ligula.", + "Fusce consequat.", + "Nulla mollis molestie lorem.", + "Vestibulum rutrum rutrum neque.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Integer a nibh.", + "Phasellus sit amet erat.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Fusce posuere felis sed lacus.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Nulla justo.", + "Nulla tempus.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Maecenas tincidunt lacus at velit.", + "Nam nulla.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Maecenas rhoncus aliquam lacus.", + "In quis justo.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Aenean fermentum.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Nulla tellus.", + "Phasellus sit amet erat.", + "Nam tristique tortor eu pede.", + "Nulla suscipit ligula in lacus.", + "Nullam molestie nibh in lectus.", + "Morbi ut odio.", + "Nullam porttitor lacus at turpis.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Proin at turpis a pede posuere nonummy.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Nulla mollis molestie lorem.", + "Curabitur at ipsum ac tellus semper interdum.", + "Integer ac leo.", + "Pellentesque ultrices mattis odio." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.AdapterMappingProblem", + "description": "Aenean sit amet justo.", + "summary": "Duis ac nibh.", + "stampText": "2016-01-02T13:31:23Z", + "epoch": "1448832282", + "detailsXml": { + "exception": { + "name": "OrnareImperdietSapien.js", + "message": "Nullam porttitor lacus at turpis. Donec posuere metus vitae ipsum. Aliquam non mauris.", + "stacktrace": { + "line": [ + "In congue.", + "Donec quis orci eget orci vehicula condimentum.", + "In congue.", + "Donec vitae nisi.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Pellentesque at nulla.", + "Duis bibendum.", + "Morbi ut odio.", + "Vivamus vestibulum sagittis sapien.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "In hac habitasse platea dictumst.", + "Quisque porta volutpat erat.", + "In hac habitasse platea dictumst.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Sed accumsan felis.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Maecenas tincidunt lacus at velit.", + "Quisque porta volutpat erat.", + "Nulla justo.", + "Etiam vel augue.", + "Mauris lacinia sapien quis libero.", + "Pellentesque at nulla.", + "Maecenas pulvinar lobortis est.", + "Duis bibendum.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Sed accumsan felis.", + "Morbi non lectus.", + "Maecenas ut massa quis augue luctus tincidunt." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.MissingRequiredRoles", + "description": "Integer ac leo.", + "summary": "Nam nulla.", + "stampText": "2015-11-03T08:26:29Z", + "epoch": "1450761449", + "detailsXml": { + "exception": { + "name": "Rutrum.java", + "message": "Integer ac neque. Duis bibendum.", + "stacktrace": { + "line": [ + "Etiam justo.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Suspendisse potenti.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Praesent blandit.", + "Nullam porttitor lacus at turpis.", + "Duis ac nibh.", + "Duis bibendum.", + "Suspendisse potenti.", + "Mauris sit amet eros.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Suspendisse accumsan tortor quis turpis.", + "Donec dapibus.", + "Pellentesque viverra pede ac diam.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "In congue.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Nam dui.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Etiam vel augue.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.QueryResultNotAvailable", + "description": "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "summary": "Nulla suscipit ligula in lacus. Curabitur at ipsum ac tellus semper interdum.", + "stampText": "2016-02-08T13:04:04Z", + "epoch": "1442090109", + "detailsXml": { + "exception": { + "name": "VenenatisNonSodales.js", + "message": "In blandit ultrices enim. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "stacktrace": { + "line": [ + "Donec ut mauris eget massa tempor convallis.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Nullam molestie nibh in lectus.", + "Proin at turpis a pede posuere nonummy.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Donec semper sapien a libero.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "In hac habitasse platea dictumst.", + "Maecenas rhoncus aliquam lacus.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Curabitur gravida nisi at nibh.", + "Donec posuere metus vitae ipsum.", + "Cras in purus eu magna vulputate luctus.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Integer a nibh.", + "In hac habitasse platea dictumst.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "Mauris lacinia sapien quis libero.", + "Integer ac leo.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Curabitur in libero ut massa volutpat convallis.", + "Pellentesque eget nunc.", + "Donec dapibus." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.CrcCouldNotBeInvoked", + "description": "Aliquam quis turpis eget elit sodales scelerisque.", + "summary": "Phasellus sit amet erat.", + "stampText": "2015-12-28T18:03:43Z", + "epoch": "1456490453", + "detailsXml": { + "exception": { + "name": "AnteNullaJusto.js", + "message": "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "stacktrace": { + "line": [ + "Aliquam erat volutpat.", + "Mauris sit amet eros.", + "Duis bibendum.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Curabitur in libero ut massa volutpat convallis.", + "Nulla justo.", + "Nunc purus.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Praesent id massa id nisl venenatis lacinia.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Duis at velit eu est congue elementum.", + "Maecenas tincidunt lacus at velit.", + "Pellentesque ultrices mattis odio.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Nulla suscipit ligula in lacus.", + "Integer ac leo.", + "Phasellus in felis.", + "Suspendisse potenti.", + "Aenean sit amet justo.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Praesent id massa id nisl venenatis lacinia.", + "Nunc nisl.", + "Maecenas tincidunt lacus at velit.", + "Quisque porta volutpat erat.", + "Integer ac neque.", + "Vestibulum ac est lacinia nisi venenatis tristique.", + "Donec semper sapien a libero.", + "Phasellus sit amet erat.", + "Morbi a ipsum.", + "Ut at dolor quis odio consequat varius.", + "Quisque porta volutpat erat.", + "Vestibulum ac est lacinia nisi venenatis tristique.", + "Donec ut dolor.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Sed ante.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.NoValidResponsesToAggregate", + "description": "Etiam vel augue.", + "summary": "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede. Morbi porttitor lorem id ligula.", + "stampText": "2015-08-21T03:32:47Z", + "epoch": "1443171111", + "detailsXml": { + "exception": { + "name": "PellentesqueVolutpat.html", + "message": "Nulla facilisi. Cras non velit nec nisi vulputate nonummy. Maecenas tincidunt lacus at velit.", + "stacktrace": { + "line": [ + "Vivamus tortor.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Duis mattis egestas metus.", + "Integer non velit.", + "Suspendisse potenti.", + "Vestibulum rutrum rutrum neque.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "Maecenas pulvinar lobortis est.", + "Pellentesque ultrices mattis odio.", + "Nam dui.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Nullam molestie nibh in lectus.", + "Sed accumsan felis.", + "Nam dui.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "Cras pellentesque volutpat dui.", + "Pellentesque viverra pede ac diam.", + "Duis bibendum.", + "Integer a nibh.", + "In hac habitasse platea dictumst.", + "Cras pellentesque volutpat dui.", + "Praesent blandit lacinia erat.", + "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Morbi a ipsum.", + "In blandit ultrices enim.", + "Nullam molestie nibh in lectus.", + "Donec quis orci eget orci vehicula condimentum.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Aliquam erat volutpat.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Praesent lectus.", + "Vestibulum sed magna at nunc commodo placerat." + ] + } + } + } +}, { + "codec":"net.shrine.protocol.ErrorStatusFromCrc", + "description": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio.", + "summary": "Praesent blandit lacinia erat. Vestibulum sed magna at nunc commodo placerat. Praesent blandit.", + "stampText": "2015-10-09T02:47:12Z", + "epoch": "1468591554", + "detailsXml": { + "exception": { + "name": "Molestie.js", + "message": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est. Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "stacktrace": { + "line": [ + "Nullam porttitor lacus at turpis.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est.", + "Ut at dolor quis odio consequat varius.", + "In hac habitasse platea dictumst.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Mauris sit amet eros.", + "Donec vitae nisi.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Pellentesque ultrices mattis odio.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Pellentesque viverra pede ac diam.", + "Morbi non lectus.", + "Aenean lectus.", + "Nulla ut erat id mauris vulputate elementum.", + "Vivamus tortor.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Vestibulum rutrum rutrum neque.", + "Donec diam neque, vestibulum eget, vulputate ut, ultrices vel, augue.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Maecenas rhoncus aliquam lacus.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Etiam faucibus cursus urna.", + "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Aliquam quis turpis eget elit sodales scelerisque.", + "Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.NoValidResponsesToAggregate", + "description": "Praesent id massa id nisl venenatis lacinia.", + "summary": "Mauris lacinia sapien quis libero. Nullam sit amet turpis elementum ligula vehicula consequat.", + "stampText": "2016-01-03T17:10:20Z", + "epoch": "1468028190", + "detailsXml": { + "exception": { + "name": "Venenatis.js", + "message": "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Duis faucibus accumsan odio. Curabitur convallis. Duis consequat dui nec nisi volutpat eleifend.", + "stacktrace": { + "line": [ + "Donec semper sapien a libero.", + "Nam ultrices, libero non mattis pulvinar, nulla pede ullamcorper augue, a suscipit nulla elit ac nulla.", + "Sed accumsan felis.", + "Nulla tellus.", + "Morbi non quam nec dui luctus rutrum.", + "Proin at turpis a pede posuere nonummy.", + "Nulla ut erat id mauris vulputate elementum.", + "Aliquam erat volutpat.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "Etiam justo.", + "Praesent id massa id nisl venenatis lacinia.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Morbi a ipsum.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Proin eu mi.", + "Nulla mollis molestie lorem.", + "In blandit ultrices enim.", + "Nullam porttitor lacus at turpis.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Sed ante.", + "Cras pellentesque volutpat dui.", + "Pellentesque at nulla.", + "Quisque ut erat.", + "Morbi porttitor lorem id ligula.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Etiam justo.", + "Suspendisse potenti.", + "Vivamus tortor.", + "Pellentesque viverra pede ac diam.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Suspendisse potenti.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Donec dapibus.", + "In quis justo.", + "Nullam varius.", + "Mauris lacinia sapien quis libero.", + "Vivamus vel nulla eget eros elementum pellentesque.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci.", + "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "Sed accumsan felis.", + "Vestibulum sed magna at nunc commodo placerat.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Donec ut dolor.", + "Nullam varius.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Nunc nisl." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.client.CouldNotParseXmlFromAdapter", + "description": "Nullam sit amet turpis elementum ligula vehicula consequat.", + "summary": "Fusce consequat.", + "stampText": "2016-05-20T11:26:14Z", + "epoch": "1456730753", + "detailsXml": { + "exception": { + "name": "Quam.html", + "message": "Vestibulum ac est lacinia nisi venenatis tristique. Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue. Aliquam erat volutpat.", + "stacktrace": { + "line": [ + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Aliquam erat volutpat.", + "Morbi vel lectus in quam fringilla rhoncus.", + "Nam nulla.", + "Integer ac neque.", + "Donec posuere metus vitae ipsum.", + "Morbi ut odio.", + "Curabitur in libero ut massa volutpat convallis.", + "Curabitur convallis.", + "In hac habitasse platea dictumst.", + "Praesent id massa id nisl venenatis lacinia.", + "In hac habitasse platea dictumst.", + "Proin interdum mauris non ligula pellentesque ultrices.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Ut tellus.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Duis ac nibh.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Fusce posuere felis sed lacus.", + "Suspendisse potenti.", + "Maecenas tincidunt lacus at velit.", + "Pellentesque at nulla.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.TimedOutWithAdapter", + "description": "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "summary": "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa. Donec dapibus. Duis at velit eu est congue elementum.", + "stampText": "2015-11-10T15:01:04Z", + "epoch": "1453270745", + "detailsXml": { + "exception": { + "name": "Nisl.java", + "message": "Sed sagittis. Nam congue, risus semper porta volutpat, quam pede lobortis ligula, sit amet eleifend pede libero quis orci. Nullam molestie nibh in lectus.", + "stacktrace": { + "line": [ + "Ut at dolor quis odio consequat varius.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Etiam justo.", + "Integer ac neque.", + "Etiam vel augue.", + "Pellentesque eget nunc.", + "Nullam varius.", + "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Morbi quis tortor id nulla ultrices aliquet.", + "Suspendisse potenti.", + "Pellentesque eget nunc.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Integer ac leo.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci.", + "Morbi ut odio.", + "Aenean fermentum.", + "Sed sagittis.", + "Nulla tellus.", + "Mauris lacinia sapien quis libero.", + "In congue.", + "Suspendisse potenti.", + "Maecenas rhoncus aliquam lacus.", + "Sed ante.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "In eleifend quam a odio.", + "Donec vitae nisi.", + "Curabitur convallis.", + "Nulla tellus.", + "Duis at velit eu est congue elementum.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Suspendisse potenti.", + "Ut tellus.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Vivamus tortor.", + "In tempor, turpis nec euismod scelerisque, quam turpis adipiscing lorem, vitae mattis nibh ligula nec sem.", + "Aenean fermentum.", + "Maecenas ut massa quis augue luctus tincidunt.", + "Quisque erat eros, viverra eget, congue eget, semper rutrum, nulla.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Vivamus in felis eu sapien cursus vestibulum.", + "Integer ac neque." + ] + } + } + } +}, { + "codec":"net.shrine.authorization.ErrorStatusFromDataStewardApp", + "description": "Quisque id justo sit amet sapien dignissim vestibulum.", + "summary": "Sed ante. Vivamus tortor. Duis mattis egestas metus.", + "stampText": "2016-03-03T13:06:21Z", + "epoch": "1465365396", + "detailsXml": { + "exception": { + "name": "DiamErat.java", + "message": "Suspendisse potenti. Cras in purus eu magna vulputate luctus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "stacktrace": { + "line": [ + "Integer ac leo.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "Morbi non quam nec dui luctus rutrum.", + "Praesent blandit lacinia erat.", + "Nullam varius.", + "Maecenas rhoncus aliquam lacus.", + "Vestibulum sed magna at nunc commodo placerat.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Vivamus tortor.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "In hac habitasse platea dictumst.", + "Integer ac leo.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Curabitur in libero ut massa volutpat convallis.", + "Morbi ut odio.", + "Etiam justo.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Suspendisse potenti.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Nullam porttitor lacus at turpis.", + "Vestibulum sed magna at nunc commodo placerat.", + "Quisque porta volutpat erat.", + "Pellentesque viverra pede ac diam.", + "Nulla facilisi.", + "Nulla nisl.", + "Nulla tempus.", + "Nulla suscipit ligula in lacus.", + "Fusce consequat.", + "Nulla ac enim.", + "Nulla nisl.", + "Phasellus id sapien in sapien iaculis congue.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Nulla dapibus dolor vel est." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.HttpErrorResponseProblem", + "description": "Nulla ac enim.", + "summary": "Integer a nibh. In quis justo. Maecenas rhoncus aliquam lacus.", + "stampText": "2016-04-28T07:00:31Z", + "epoch": "1453805041", + "detailsXml": { + "exception": { + "name": "PortaVolutpat.html", + "message": "Nulla tempus.", + "stacktrace": { + "line": [ + "Integer ac neque.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris viverra diam vitae quam.", + "Duis at velit eu est congue elementum.", + "Pellentesque ultrices mattis odio.", + "Phasellus in felis.", + "Integer ac neque.", + "Fusce congue, diam id ornare imperdiet, sapien urna pretium nisl, ut volutpat sapien arcu sed augue.", + "Phasellus id sapien in sapien iaculis congue.", + "Integer ac neque.", + "In congue.", + "Proin eu mi.", + "Vivamus tortor.", + "Vivamus metus arcu, adipiscing molestie, hendrerit at, vulputate vitae, nisl.", + "Vestibulum rutrum rutrum neque.", + "Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "Suspendisse accumsan tortor quis turpis.", + "Fusce consequat.", + "Praesent id massa id nisl venenatis lacinia.", + "Proin leo odio, porttitor id, consequat in, consequat ut, nulla.", + "Donec posuere metus vitae ipsum.", + "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "Donec posuere metus vitae ipsum.", + "Aenean auctor gravida sem.", + "Vivamus tortor.", + "Nulla mollis molestie lorem.", + "Curabitur convallis.", + "Quisque ut erat.", + "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "Aenean lectus.", + "Duis at velit eu est congue elementum.", + "In hac habitasse platea dictumst.", + "Nulla nisl.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Vestibulum sed magna at nunc commodo placerat." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.ExceptionWhileLoadingCrcResponse", + "description": "Cras mi pede, malesuada in, imperdiet et, commodo vulputate, justo.", + "summary": "In est risus, auctor sed, tristique in, tempus sit amet, sem. Fusce consequat.", + "stampText": "2016-06-19T01:15:22Z", + "epoch": "1449072574", + "detailsXml": { + "exception": { + "name": "EtiamFaucibusCursus.js", + "message": "Nulla neque libero, convallis eget, eleifend luctus, ultricies eu, nibh.", + "stacktrace": { + "line": [ + "Duis mattis egestas metus.", + "Pellentesque viverra pede ac diam.", + "Mauris sit amet eros.", + "In eleifend quam a odio.", + "Mauris sit amet eros.", + "Fusce posuere felis sed lacus.", + "In hac habitasse platea dictumst.", + "Etiam justo.", + "Donec ut dolor.", + "Nam tristique tortor eu pede.", + "Vestibulum sed magna at nunc commodo placerat.", + "Maecenas tristique, est et tempus semper, est quam pharetra magna, ac consequat metus sapien ut nunc.", + "Etiam pretium iaculis justo.", + "Aenean sit amet justo.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Quisque ut erat.", + "Ut at dolor quis odio consequat varius.", + "Aenean fermentum.", + "Nullam sit amet turpis elementum ligula vehicula consequat.", + "In hac habitasse platea dictumst.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Mauris lacinia sapien quis libero.", + "Aenean lectus.", + "Phasellus id sapien in sapien iaculis congue.", + "Sed vel enim sit amet nunc viverra dapibus.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Pellentesque viverra pede ac diam.", + "Phasellus id sapien in sapien iaculis congue.", + "Nunc rhoncus dui vel sem.", + "Nulla tempus.", + "Donec odio justo, sollicitudin ut, suscipit a, feugiat et, eros.", + "Donec ut dolor.", + "Nulla nisl.", + "Phasellus sit amet erat.", + "Nullam molestie nibh in lectus.", + "Pellentesque viverra pede ac diam.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Donec quis orci eget orci vehicula condimentum.", + "Pellentesque ultrices mattis odio.", + "Aenean auctor gravida sem.", + "Praesent lectus.", + "Pellentesque eget nunc.", + "Nullam varius.", + "Nam dui.", + "In sagittis dui vel nisl.", + "Integer ac leo.", + "Vestibulum sed magna at nunc commodo placerat.", + "Nullam varius.", + "Quisque ut erat.", + "Nulla tempus." + ] + } + } + } +}] +} \ No newline at end of file diff --git a/apps/dashboard-app/src/main/js/test/admin/status/problems?offset=60.json b/apps/dashboard-app/src/main/js/test/admin/status/problems?offset=60.json new file mode 100644 index 000000000..cf07cd9f3 --- /dev/null +++ b/apps/dashboard-app/src/main/js/test/admin/status/problems?offset=60.json @@ -0,0 +1,170 @@ +{"size":63,"offset":60,"n":20,"problems": +[{ + "codec":"net.shrine.adapter.CannotParseXmlFromCrc", + "description": "Suspendisse potenti.", + "summary": "Maecenas ut massa quis augue luctus tincidunt. Nulla mollis molestie lorem. Quisque ut erat.", + "stampText": "2015-10-03T22:50:09Z", + "epoch": "1458628756", + "detailsXml": { + "exception": { + "name": "AIpsumInteger.java", + "message": "In sagittis dui vel nisl. Duis ac nibh. Fusce lacus purus, aliquet at, feugiat non, pretium quis, lectus.", + "stacktrace": { + "line": [ + "Nulla tempus.", + "Nulla mollis molestie lorem.", + "Integer a nibh.", + "Vivamus vestibulum sagittis sapien.", + "Duis ac nibh.", + "Aenean fermentum.", + "Donec quis orci eget orci vehicula condimentum.", + "Quisque arcu libero, rutrum ac, lobortis vel, dapibus at, diam.", + "Duis mattis egestas metus.", + "Nunc purus.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Etiam justo.", + "Nulla ut erat id mauris vulputate elementum.", + "Nulla mollis molestie lorem.", + "Aenean sit amet justo.", + "Morbi porttitor lorem id ligula.", + "Maecenas rhoncus aliquam lacus.", + "Morbi vestibulum, velit id pretium iaculis, diam erat fermentum justo, nec condimentum neque sapien placerat ante.", + "Mauris ullamcorper purus sit amet nulla.", + "Integer ac leo.", + "Nunc rhoncus dui vel sem.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Ut at dolor quis odio consequat varius.", + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", + "Vestibulum sed magna at nunc commodo placerat.", + "Nulla suscipit ligula in lacus.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Ut at dolor quis odio consequat varius.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Donec vitae nisi.", + "Mauris sit amet eros.", + "Morbi odio odio, elementum eu, interdum eu, tincidunt in, leo.", + "Integer ac neque.", + "Nullam orci pede, venenatis non, sodales sed, tincidunt eu, felis.", + "Aliquam sit amet diam in magna bibendum imperdiet.", + "Nulla ac enim.", + "In hac habitasse platea dictumst.", + "Suspendisse ornare consequat lectus.", + "In sagittis dui vel nisl.", + "Phasellus id sapien in sapien iaculis congue.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Sed vel enim sit amet nunc viverra dapibus.", + "In hac habitasse platea dictumst.", + "Aenean fermentum.", + "Praesent blandit.", + "Etiam pretium iaculis justo.", + "Pellentesque eget nunc.", + "Nulla tempus.", + "In hac habitasse platea dictumst." + ] + } + } + } +}, { + "codec":"net.shrine.aggregation.CouldNotParseResultsProblem", + "description": "Nam dui.", + "summary": "Nunc rhoncus dui vel sem. Sed sagittis.", + "stampText": "2015-09-16T10:52:48Z", + "epoch": "1460014356", + "detailsXml": { + "exception": { + "name": "FelisUt.js", + "message": "Sed vel enim sit amet nunc viverra dapibus.", + "stacktrace": { + "line": [ + "Cras non velit nec nisi vulputate nonummy.", + "Phasellus id sapien in sapien iaculis congue.", + "Vivamus tortor.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Duis bibendum, felis sed interdum venenatis, turpis enim blandit mi, in porttitor pede justo eu massa.", + "Suspendisse potenti.", + "Aliquam augue quam, sollicitudin vitae, consectetuer eget, rutrum at, lorem.", + "Donec ut mauris eget massa tempor convallis.", + "Ut at dolor quis odio consequat varius.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Vivamus vestibulum sagittis sapien.", + "In congue.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Morbi sem mauris, laoreet ut, rhoncus aliquet, pulvinar sed, nisl.", + "Aenean fermentum.", + "Donec semper sapien a libero.", + "Praesent id massa id nisl venenatis lacinia.", + "In hac habitasse platea dictumst.", + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec pharetra, magna vestibulum aliquet ultrices, erat tortor sollicitudin mi, sit amet lobortis sapien sapien non mi.", + "Quisque id justo sit amet sapien dignissim vestibulum.", + "Duis mattis egestas metus.", + "Morbi porttitor lorem id ligula.", + "In congue.", + "Cras pellentesque volutpat dui.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Morbi porttitor lorem id ligula.", + "Nullam molestie nibh in lectus.", + "Nulla tempus.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Nam tristique tortor eu pede.", + "In eleifend quam a odio.", + "Nullam varius.", + "Morbi a ipsum.", + "Integer ac leo.", + "In hac habitasse platea dictumst.", + "Morbi non lectus.", + "Nam dui.", + "Fusce posuere felis sed lacus.", + "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "Praesent id massa id nisl venenatis lacinia.", + "Curabitur in libero ut massa volutpat convallis." + ] + } + } + } +}, { + "codec":"net.shrine.adapter.client.CouldNotParseXmlFromAdapter", + "description": "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis.", + "summary": "Pellentesque viverra pede ac diam.", + "stampText": "2016-07-19T06:32:56Z", + "epoch": "1442947556", + "detailsXml": { + "exception": { + "name": "Condimentum.js", + "message": "Suspendisse accumsan tortor quis turpis.", + "stacktrace": { + "line": [ + "In blandit ultrices enim.", + "Vestibulum ac est lacinia nisi venenatis tristique.", + "Duis consequat dui nec nisi volutpat eleifend.", + "Nulla suscipit ligula in lacus.", + "Integer pede justo, lacinia eget, tincidunt eget, tempus vel, pede.", + "In sagittis dui vel nisl.", + "Aenean lectus.", + "Sed ante.", + "Praesent blandit.", + "Nulla tellus.", + "Ut at dolor quis odio consequat varius.", + "In congue.", + "Nam nulla.", + "Maecenas leo odio, condimentum id, luctus nec, molestie sed, justo.", + "Vivamus tortor.", + "Maecenas tincidunt lacus at velit.", + "Donec ut mauris eget massa tempor convallis.", + "Morbi a ipsum.", + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.", + "Vivamus vestibulum sagittis sapien.", + "Proin at turpis a pede posuere nonummy.", + "Proin risus.", + "Pellentesque at nulla.", + "Vestibulum quam sapien, varius ut, blandit non, interdum in, ante.", + "Morbi non lectus.", + "Fusce consequat.", + "Mauris ullamcorper purus sit amet nulla.", + "Vestibulum sed magna at nunc commodo placerat.", + "Nam dui." + ] + } + } + } +}]} \ No newline at end of file diff --git a/apps/dashboard-app/src/main/resources/reference.conf b/apps/dashboard-app/src/main/resources/reference.conf index 18cb2001d..d70049d56 100644 --- a/apps/dashboard-app/src/main/resources/reference.conf +++ b/apps/dashboard-app/src/main/resources/reference.conf @@ -1,56 +1,82 @@ shrine { dashboard { gruntWatch = false //false for production, true for mvn tomcat7:run . Allows the client javascript and html files to be loaded via gruntWatch . happyBaseUrl = "https://localhost:6443/shrine/rest/happy" statusBaseUrl = "https://localhost:6443/shrine/rest/internalstatus" remoteDashboard { protocol = "https://" port = ":6443" pathPrefix = "shrine-dashboard/fromDashboard" } + + database { + dataSourceFrom = "JNDI" //Can be JNDI or testDataSource . Use testDataSource for tests, JNDI everywhere else + jndiDataSourceName = "java:comp/env/jdbc/stewardDB" //or leave out for tests + slickProfileClassName = "slick.driver.MySQLDriver$" // Can be + // slick.driver.H2Driver$ + // slick.driver.MySQLDriver$ + // slick.driver.PostgresDriver$ + // slick.driver.SQLServerDriver$ + // slick.driver.JdbcDriver$ + // freeslick.OracleProfile$ + // freeslick.MSSQLServerProfile$ + // + // (Yes, with the $ on the end) + + // For testing without JNDI + // testDataSource { + + //typical test settings for unit tests + //driverClassName = "org.h2.Driver" + + //url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" //H2 embedded in-memory for unit tests + //url = "jdbc:h2:~/stewardTest.h2" //H2 embedded on disk at ~/test + // } + createTablesOnStart = false //for testing with H2 in memory, when not running unit tests. Set to false normally + } } pmEndpoint { url = "http://changeme.com/i2b2/services/PMService/getServices" //"http://services.i2b2.org/i2b2/services/PMService/getServices" acceptAllCerts = true timeout { seconds = 10 } } authenticate { realm = "SHRINE Steward API" usersource { type = "PmUserSource" //Must be ConfigUserSource (for isolated testing) or PmUserSource (for everything else) domain = "set shrine.authenticate.usersource.domain to the PM authentication domain in dashboard.conf" //"i2b2demo" } } // If the pmEndpoint acceptAllCerts = false then you need to supply a keystore // Or if you would like dashboard-to-dashboard comms to work. // keystore { // file = "shrine.keystore" // password = "chiptesting" // privateKeyAlias = "test-cert" // keyStoreType = "JKS" // caCertAliases = [carra ca] // } } //todo typesafe config precedence seems to do the right thing, but I haven't found the rules that say this reference.conf should override others akka { loglevel = INFO // log-config-on-start = on loggers = ["akka.event.slf4j.Slf4jLogger"] // logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" // Toggles whether the threads created by this ActorSystem should be daemons or not daemonic = on } spray.servlet { boot-class = "net.shrine.dashboard.Boot" request-timeout = 30s } diff --git a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardConfigSource.scala b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardConfigSource.scala index 820773817..18b9cd8c8 100644 --- a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardConfigSource.scala +++ b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardConfigSource.scala @@ -1,25 +1,14 @@ package net.shrine.dashboard -import com.typesafe.config.{Config, ConfigFactory} -import net.shrine.config.AtomicConfigSource +import net.shrine.config.ConfigSource /** * Source of typesafe config for the dashboard app. * * @author david * @since 4/29/15 */ -object DashboardConfigSource { - - val atomicConfig = new AtomicConfigSource(ConfigFactory.load("dashboard")) - - def config:Config = { - atomicConfig.config - } - - def configForBlock[T](key:String,value:AnyRef,origin:String)(block: => T):T = { - atomicConfig.configForBlock(key,value,origin)(block) - } - +object DashboardConfigSource extends ConfigSource { + override val configName = "dashboard" } diff --git a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardService.scala b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardService.scala index 3f2551f97..3ef153aa3 100644 --- a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardService.scala +++ b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardService.scala @@ -1,437 +1,475 @@ package net.shrine.dashboard import akka.actor.Actor import akka.event.Logging import net.shrine.authentication.UserAuthenticator import net.shrine.authorization.steward.OutboundUser import net.shrine.dashboard.jwtauth.ShrineJwtAuthenticator import net.shrine.i2b2.protocol.pm.User import net.shrine.status.protocol.{Config => StatusProtocolConfig} import net.shrine.dashboard.httpclient.HttpClientDirectives.{forwardUnmatchedPath, requestUriThenRoute} import net.shrine.log.Loggable +import net.shrine.problem.{ProblemDigest, Problems} +import net.shrine.serialization.NodeSeqSerializer import shapeless.HNil import spray.http.{HttpRequest, HttpResponse, StatusCodes, Uri} import spray.httpx.Json4sSupport import spray.routing.directives.LogEntry -import spray.routing.{AuthenticationFailedRejection, Directive0, HttpService, Rejected, Route, RouteConcatenation} +import spray.routing._ import org.json4s.{DefaultFormats, Formats} import org.json4s.native.JsonMethods.{parse => json4sParse} +import org.json4s.native.Serialization._ import scala.collection.immutable.Iterable +import scala.concurrent.duration.{Duration, FiniteDuration, SECONDS} import scala.concurrent.ExecutionContext.Implicits.global /** * Mixes the DashboardService trait with an Akka Actor to provide the actual service. */ class DashboardServiceActor extends Actor with DashboardService { // the HttpService trait defines only one abstract member, which // connects the services environment to the enclosing actor or test def actorRefFactory = context // this actor only runs our route, but you could add // other things here, like request stream processing // or timeout handling def receive = runRoute(route) } /** * A web service that provides the Dashboard endpoints. It is a trait to support testing independent of Akka. */ trait DashboardService extends HttpService with Json4sSupport with Loggable { implicit def json4sFormats: Formats = DefaultFormats val userAuthenticator = UserAuthenticator(DashboardConfigSource.config) //don't need to do anything special for unauthorized users, but they do need access to a static form. lazy val route:Route = gruntWatchCorsSupport{ redirectToIndex ~ staticResources ~ makeTrouble ~ about ~ authenticatedInBrowser ~ authenticatedDashboard } /** logs the request method, uri and response at info level */ def logEntryForRequestResponse(req: HttpRequest): Any => Option[LogEntry] = { case res: HttpResponse => Some(LogEntry(s"\n Request: $req\n Response: $res", Logging.InfoLevel)) case _ => None // other kind of responses } /** logs just the request method, uri and response status at info level */ def logEntryForRequest(req: HttpRequest): Any => Option[LogEntry] = { case res: HttpResponse => Some(LogEntry(s"\n Request: $req\n Response status: ${res.status}", Logging.InfoLevel)) case _ => None // other kind of responses } def authenticatedInBrowser: Route = pathPrefixTest("user"|"admin"|"toDashboard") { logRequestResponse(logEntryForRequestResponse _) { //logging is controlled by Akka's config, slf4j, and log4j config reportIfFailedToAuthenticate { authenticate(userAuthenticator.basicUserAuthenticator) { user => pathPrefix("user") { userRoute(user) } ~ pathPrefix("admin") { adminRoute(user) } ~ pathPrefix("toDashboard") { toDashboardRoute(user) } } } } } val reportIfFailedToAuthenticate = routeRouteResponse { case Rejected(List(AuthenticationFailedRejection(_,_))) => complete("AuthenticationFailed") } def authenticatedDashboard:Route = pathPrefix("fromDashboard") { logRequestResponse(logEntryForRequestResponse _) { //logging is controlled by Akka's config, slf4j, and log4j config get { //all remote dashboard calls are gets. authenticate(ShrineJwtAuthenticator.authenticate) { user => adminRoute(user) } } } } def makeTrouble = pathPrefix("makeTrouble") { complete(throw new IllegalStateException("fake trouble")) } lazy val redirectToIndex = pathEnd { redirect("shrine-dashboard/client/index.html", StatusCodes.PermanentRedirect) //todo pick up "shrine-dashboard" programatically } ~ ( path("index.html") | pathSingleSlash) { redirect("client/index.html", StatusCodes.PermanentRedirect) } lazy val staticResources = pathPrefix("client") { pathEnd { redirect("client/index.html", StatusCodes.PermanentRedirect) } ~ pathSingleSlash { redirect("index.html", StatusCodes.PermanentRedirect) } ~ { getFromResourceDirectory("client") } } lazy val about = pathPrefix("about") { complete("Nothing here yet") //todo } def userRoute(user:User):Route = get { pathPrefix("whoami") { complete(OutboundUser.createFromUser(user)) } } //todo check that this an admin. def adminRoute(user:User):Route = get { pathPrefix("happy") { val happyBaseUrl: String = DashboardConfigSource.config.getString("shrine.dashboard.happyBaseUrl") forwardUnmatchedPath(happyBaseUrl) } ~ pathPrefix("messWithHappyVersion") { //todo is this used? val happyBaseUrl: String = DashboardConfigSource.config.getString("shrine.dashboard.happyBaseUrl") def pullClasspathFromConfig(httpResponse:HttpResponse,uri:Uri):Route = { ctx => { val result = httpResponse.entity.asString ctx.complete(s"Got '$result' from $uri") } } requestUriThenRoute(happyBaseUrl+"/version",pullClasspathFromConfig) } ~ pathPrefix("ping") {complete("pong")}~ pathPrefix("status"){statusRoute(user)} } //Manually test this by running a curl command //curl -k -w "\n%{response_code}\n" -u dave:kablam "https://shrine-dev1.catalyst:6443/shrine-dashboard/toDashboard/shrine-dev2.catalyst/shrine-dashboard/fromDashboard/ping" /** * Forward a request from this dashboard to a remote dashboard */ def toDashboardRoute(user:User):Route = get { pathPrefix(Segment) { dnsName => val remoteDashboardProtocol = DashboardConfigSource.config.getString("shrine.dashboard.remoteDashboard.protocol") val remoteDashboardPort = DashboardConfigSource.config.getString("shrine.dashboard.remoteDashboard.port") val remoteDashboardPathPrefix = DashboardConfigSource.config.getString("shrine.dashboard.remoteDashboard.pathPrefix") val baseUrl = s"$remoteDashboardProtocol$dnsName$remoteDashboardPort/$remoteDashboardPathPrefix" forwardUnmatchedPath(baseUrl,Some(ShrineJwtAuthenticator.createOAuthCredentials(user))) } } def statusRoute(user:User):Route = get { pathPrefix("config"){getConfig}~ pathPrefix("classpath"){getClasspath}~ pathPrefix("options"){getOptionalParts}~ //todo rename path to optionalParts - pathPrefix("summary"){getSummary} + pathPrefix("summary"){getSummary}~ + pathPrefix("problems"){getProblems} } val statusBaseUrl = DashboardConfigSource.config.getString("shrine.dashboard.statusBaseUrl") lazy val getConfig:Route = { def completeConfigRoute(httpResponse:HttpResponse,uri:Uri):Route = { ctx => { val config = ParsedConfig(httpResponse.entity.asString) ctx.complete( ShrineConfig(config) ) } } requestUriThenRoute(statusBaseUrl + "/config", completeConfigRoute) } lazy val getClasspath:Route = { def pullClasspathFromConfig(httpResponse:HttpResponse,uri:Uri):Route = { ctx => { val result = httpResponse.entity.asString val shrineConfig = ShrineConfig(ParsedConfig(result)) ctx.complete(shrineConfig) } } requestUriThenRoute(statusBaseUrl + "/config",pullClasspathFromConfig) } lazy val getOptionalParts:Route = { requestUriThenRoute(statusBaseUrl + "/optionalParts") } lazy val getSummary:Route = { requestUriThenRoute(statusBaseUrl + "/summary") } + // table based view, can see N problems at a time. Front end sends how many problems that they want + // to skip, and it will take N the 'nearest N' ie with n = 20, 0-19 -> 20, 20-39 -> 20-40 + lazy val getProblems:Route = { + + def floorMod(x: Int, y: Int) = { + x - (x % y) + } + + val formats = DefaultFormats + new NodeSeqSerializer + + parameter("offset" ? "0") { offsetString: String => + val n = 20 + // TODO: Once Bamboo/Deploy is running Java 8, switch to using Math.floorMod + + + // Try and grab the offset. If a number wasn't passed in, just default to 0 + val offset = try { floorMod(Math.max(0, offsetString.toInt), n) } catch { case a:java.lang.NumberFormatException => + println(s"Could not parse problems GET request parameter, received $offsetString, threw $a") + 0 + } + + val p = Problems + val db = p.DatabaseConnector + val timeout: Duration = new FiniteDuration(15, SECONDS) + val problemsAndSize: (Seq[ProblemDigest], Int) = db.runBlocking(db.IO.sizeAndProblemDigest(n, offset))(timeout) + val response = ProblemResponse(problemsAndSize._2, offset, n, problemsAndSize._1) + //todo: Find a better way to do this besides writing and parsing the json response + complete(json4sParse(write(response)(formats))) + } + } + } /** * Centralized parsing logic for map of shrine.conf * the class literal `T.class` in Java. */ //todo most of this info should come directly from the status service in Shrine, not from reading the config case class ParsedConfig(configMap:Map[String, String]){ private val trueVal = "true" private val rootKey = "shrine" def isHub = getOrElse(rootKey + ".hub.create", "") .toLowerCase == trueVal def stewardEnabled = configMap.keySet .contains(rootKey + ".queryEntryPoint.shrineSteward") def shouldQuerySelf = getOrElse(rootKey + ".hub.shouldQuerySelf", "") .toLowerCase == trueVal def fromJsonString(jsonString:String): String = jsonString.split("\"").mkString("") def get(key:String): Option[String] = configMap.get(key).map(fromJsonString) def getOrElse(key:String, elseVal:String = ""): String = get(key).getOrElse(elseVal) } object ParsedConfig { def apply(jsonString:String):ParsedConfig = { implicit def json4sFormats: Formats = DefaultFormats ParsedConfig(json4sParse(jsonString).extract[StatusProtocolConfig].keyValues.filterKeys(_.toLowerCase.startsWith("shrine"))) } } case class DownstreamNode(name:String, url:String) object DownstreamNode { def create(configMap:Map[String,String]):Iterable[DownstreamNode] = { for ((k, v) <- configMap.filterKeys(_.toLowerCase.startsWith ("shrine.hub.downstreamnodes"))) yield DownstreamNode(k.split('.').last,v.split("\"").mkString("")) } } +case class ProblemResponse(size: Int, offset: Int, n: Int, problems: Seq[ProblemDigest]) + //todo replace with the actual config, scrubbed of passwords case class ShrineConfig(isHub:Boolean, hub:Hub, pmEndpoint:Endpoint, ontEndpoint:Endpoint, hiveCredentials: HiveCredentials, adapter: Adapter, queryEntryPoint:QEP, networkStatusQuery:String ) object ShrineConfig{ def apply(config:ParsedConfig):ShrineConfig = { val hub = Hub(config) val isHub = config.isHub val pmEndpoint = Endpoint("pm",config) val ontEndpoint = Endpoint("ont",config) val hiveCredentials = HiveCredentials(config) val adapter = Adapter(config) val queryEntryPoint = QEP(config) val networkStatusQuery = config.configMap("shrine.networkStatusQuery") ShrineConfig(isHub, hub, pmEndpoint, ontEndpoint, hiveCredentials, adapter, queryEntryPoint, networkStatusQuery) } } case class Endpoint(acceptAllCerts:Boolean, url:String, timeoutSeconds:Int) object Endpoint{ def apply(endpointType:String,parsedConfig:ParsedConfig):Endpoint = { val prefix = "shrine." + endpointType.toLowerCase + "Endpoint." val acceptAllCerts = parsedConfig.configMap.getOrElse(prefix + "acceptAllCerts", "") == "true" val url = parsedConfig.configMap.getOrElse(prefix + "url","") val timeoutSeconds = parsedConfig.configMap.getOrElse(prefix + "timeout.seconds", "0").toInt Endpoint(acceptAllCerts, url, timeoutSeconds) } } case class HiveCredentials(domain:String, username:String, password:String, crcProjectId:String, ontProjectId:String) object HiveCredentials{ def apply(parsedConfig:ParsedConfig):HiveCredentials = { val key = "shrine.hiveCredentials." val domain = parsedConfig.configMap.getOrElse(key + "domain","") val username = parsedConfig.configMap.getOrElse(key + "username","") val password = "REDACTED" val crcProjectId = parsedConfig.configMap.getOrElse(key + "crcProjectId","") val ontProjectId = parsedConfig.configMap.getOrElse(key + "ontProjectId","") HiveCredentials(domain, username, password, crcProjectId, ontProjectId) } } // -- hub only -- // //todo delete when the Dashboard front end can use the status service's hub method case class Hub(shouldQuerySelf:Boolean, create:Boolean, downstreamNodes:Iterable[DownstreamNode]) object Hub{ def apply(parsedConfig:ParsedConfig):Hub = { val shouldQuerySelf = parsedConfig.shouldQuerySelf val create = parsedConfig.isHub val downstreamNodes = DownstreamNode.create(parsedConfig.configMap) Hub(shouldQuerySelf, create, downstreamNodes) } } // -- adapter info -- // case class Adapter(crcEndpointUrl:String, setSizeObfuscation:Boolean, adapterLockoutAttemptsThreshold:Int, adapterMappingsFilename:String) object Adapter{ def apply(parsedConfig:ParsedConfig):Adapter = { val key = "shrine.adapter." val crcEndpointUrl = parsedConfig.configMap.getOrElse(key + "crcEndpoint.url","") val setSizeObfuscation = parsedConfig.configMap.getOrElse(key + "setSizeObfuscation","").toLowerCase == "true" val adapterLockoutAttemptsThreshold = parsedConfig.configMap.getOrElse(key + "adapterLockoutAttemptsThreshold", "0").toInt val adapterMappingsFileName = parsedConfig.configMap.getOrElse(key + "adapterMappingsFileName","") Adapter(crcEndpointUrl, setSizeObfuscation, adapterLockoutAttemptsThreshold, adapterMappingsFileName) } } case class Steward(qepUserName:String, stewardBaseUrl:String) object Steward { def apply (parsedConfig:ParsedConfig):Steward = { val key = "shrine.queryEntryPoint.shrineSteward." val qepUserName = parsedConfig.configMap.getOrElse(key + "qepUserName","") val stewardBaseUrl = parsedConfig.configMap.getOrElse(key + "stewardBaseUrl","") Steward(qepUserName, stewardBaseUrl) } } // -- if needed -- // case class TimeoutInfo (timeUnit:String, description:String) case class DatabaseInfo(createTablesOnStart:Boolean, dataSourceFrom:String, jndiDataSourceName:String, slickProfileClassName:String) case class Audit(database:DatabaseInfo, collectQepAudit:Boolean) object Audit{ def apply(parsedConfig:ParsedConfig):Audit = { val key = "shrine.queryEntryPoint.audit." val createTablesOnStart = parsedConfig.configMap.getOrElse(key + "database.createTablesOnStart","") == "true" val dataSourceFrom = parsedConfig.configMap.getOrElse(key + "database.dataSourceFrom","") val jndiDataSourceName = parsedConfig.configMap.getOrElse(key + "database.jndiDataSourceName","") val slickProfileClassName = parsedConfig.configMap.getOrElse(key + "database.slickProfileClassName","") val collectQepAudit = parsedConfig.configMap.getOrElse(key + "collectQepAudit","") == "true" val database = DatabaseInfo(createTablesOnStart, dataSourceFrom, jndiDataSourceName, slickProfileClassName) Audit(database, collectQepAudit) } } case class QEP( maxQueryWaitTimeMinutes:Int, create:Boolean, attachSigningCert:Boolean, authorizationType:String, includeAggregateResults:Boolean, authenticationType:String, audit:Audit, shrineSteward:Steward, broadcasterServiceEndpointUrl:Option[String] ) object QEP{ val key = "shrine.queryEntryPoint." def apply(parsedConfig:ParsedConfig):QEP = QEP( maxQueryWaitTimeMinutes = parsedConfig.configMap.getOrElse(key + "maxQueryWaitTime.minutes", "0").toInt, create = parsedConfig.configMap.getOrElse(key + "create","") == "true", attachSigningCert = parsedConfig.configMap.getOrElse(key + "attachSigningCert","") == "true", authorizationType = parsedConfig.configMap.getOrElse(key + "authorizationType",""), includeAggregateResults = parsedConfig.configMap.getOrElse(key + "includeAggregateResults","") == "true", authenticationType = parsedConfig.configMap.getOrElse(key + "authenticationType", ""), audit = Audit(parsedConfig), shrineSteward = Steward(parsedConfig), broadcasterServiceEndpointUrl = parsedConfig.configMap.get(key + "broadcasterServiceEndpoint.url") ) } //adapted from https://gist.github.com/joseraya/176821d856b43b1cfe19 object gruntWatchCorsSupport extends Directive0 with RouteConcatenation { import spray.http.HttpHeaders.{`Access-Control-Allow-Methods`, `Access-Control-Max-Age`, `Access-Control-Allow-Headers`,`Access-Control-Allow-Origin`} import spray.routing.directives.RespondWithDirectives.respondWithHeaders import spray.routing.directives.MethodDirectives.options import spray.routing.directives.RouteDirectives.complete import spray.http.HttpMethods.{OPTIONS,GET,POST} import spray.http.AllOrigins private val allowOriginHeader = `Access-Control-Allow-Origin`(AllOrigins) private val optionsCorsHeaders = List( `Access-Control-Allow-Headers`("Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, Referer, User-Agent, Authorization"), `Access-Control-Max-Age`(1728000)) //20 days val gruntWatch:Boolean = DashboardConfigSource.config.getBoolean("shrine.dashboard.gruntWatch") override def happly(f: (HNil) => Route): Route = { if(gruntWatch) { options { respondWithHeaders(`Access-Control-Allow-Methods`(OPTIONS, GET, POST) :: allowOriginHeader :: optionsCorsHeaders){ complete(StatusCodes.OK) } } ~ f(HNil) } else f(HNil) } } diff --git a/apps/dashboard-app/src/test/resources/dashboard.conf b/apps/dashboard-app/src/test/resources/dashboard.conf index 3cc20c3c0..189eea6f5 100644 --- a/apps/dashboard-app/src/test/resources/dashboard.conf +++ b/apps/dashboard-app/src/test/resources/dashboard.conf @@ -1,35 +1,65 @@ shrine { + dashboard { + database { + dataSourceFrom = "testDataSource" + slickProfileClassName = "slick.driver.H2Driver$" + createTablesOnStart = true + // For testing without JNDI + testDataSource { + + //typical test settings for unit tests + driverClassName = "org.h2.Driver" + + url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" + } + } + } authenticate { usersource { //Bogus security for testing type = "ConfigUserSource" //Must be ConfigUserSource (for isolated testing) or PmUserSource (for everything else) researcher { username = "ben" password = "kapow" } steward { username = "dave" password = "kablam" } qep{ username = "qep" password = "trustme" } admin{ username = "keith" password = "shh!" } } } dashboard { happyBaseUrl = "classpath://resources/testhappy" statusBaseUrl = "classpath://resources/teststatus" + + database { + dataSourceFrom = "testDataSource" + slickProfileClassName = "slick.driver.H2Driver$" + createTablesOnStart = true + // For testing without JNDI + testDataSource { + + //typical test settings for unit tests + driverClassName = "org.h2.Driver" + + url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" + } + + } } keystore { file = "shrine.keystore" password = "chiptesting" privateKeyAlias = "test-cert" keyStoreType = "JKS" caCertAliases = [carra ca] } } \ No newline at end of file diff --git a/apps/dashboard-app/src/test/scala/net/shrine/dashboard/DashboardServiceTest.scala b/apps/dashboard-app/src/test/scala/net/shrine/dashboard/DashboardServiceTest.scala index f681cbdc3..4edc8906b 100644 --- a/apps/dashboard-app/src/test/scala/net/shrine/dashboard/DashboardServiceTest.scala +++ b/apps/dashboard-app/src/test/scala/net/shrine/dashboard/DashboardServiceTest.scala @@ -1,291 +1,314 @@ package net.shrine.dashboard import java.security.PrivateKey import java.util.Date import io.jsonwebtoken.impl.TextCodec import io.jsonwebtoken.{Jwts, SignatureAlgorithm} import net.shrine.authorization.steward.OutboundUser import net.shrine.crypto.{KeyStoreCertCollection, KeyStoreDescriptorParser} import net.shrine.dashboard.jwtauth.ShrineJwtAuthenticator import net.shrine.i2b2.protocol.pm.User import net.shrine.protocol.Credential import org.json4s.native.JsonMethods.parse import org.junit.runner.RunWith import org.scalatest.FlatSpec import org.scalatest.junit.JUnitRunner import spray.http.StatusCodes.{OK, PermanentRedirect, Unauthorized} import spray.http.{BasicHttpCredentials, OAuth2BearerToken} import spray.testkit.ScalatestRouteTest import scala.language.postfixOps @RunWith(classOf[JUnitRunner]) class DashboardServiceTest extends FlatSpec with ScalatestRouteTest with DashboardService { def actorRefFactory = system import scala.concurrent.duration._ implicit val routeTestTimeout = RouteTestTimeout(10 seconds) val adminUserName = "keith" val adminFullName = adminUserName /** * to run these tests with I2B2 * add a user named keith, to be the admin * add a Boolean parameter for keith, Admin, true * add all this user to the i2b2 project */ val adminCredentials = BasicHttpCredentials(adminUserName,"shh!") val brokenCredentials = BasicHttpCredentials(adminUserName,"wrong password") val adminUser = User( fullName = adminUserName, username = adminFullName, domain = "domain", credential = new Credential("admin's password",false), params = Map(), rolesByProject = Map() ) val adminOutboundUser = OutboundUser.createFromUser(adminUser) "DashboardService" should "return an OK and a valid outbound user for a user/whoami request" in { Get(s"/user/whoami") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val userJson = new String(body.data.toByteArray) val outboundUser = parse(userJson).extract[OutboundUser] assertResult(adminOutboundUser)(outboundUser) } } "DashboardService" should "return an OK and a valid outbound user for a user/whoami request and an '' " in { Get(s"/user/whoami") ~> addCredentials(brokenCredentials) ~> route ~> check { assertResult(OK)(status) val response = new String(body.data.toByteArray) assertResult(""""AuthenticationFailed"""")(response) } } "DashboardService" should "redirect several urls to client/index.html" in { Get() ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } Get("/") ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } Get("/index.html") ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } Get("/client") ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } Get("/client/") ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } } "DashboardService" should "return an OK and the right version string for an admin/happy/all test" in { Get(s"/admin/happy/all") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val allString = new String(body.data.toByteArray) //println(allString) //todo test it to see if it's right } } "DashboardService" should "return an OK and mess with the right version string for an admin/messWithHappyVersion test" in { Get(s"/admin/messWithHappyVersion") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val versionString = new String(body.data.toByteArray) //todo test it to see if it's right } } "DashboardService" should "return an OK for admin/status/config" in { Get(s"/admin/status/config") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val configString = new String(body.data.toByteArray) //println(configString) } } "DashboardService" should "return an OK for admin/status/classpath" in { Get(s"/admin/status/classpath") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val classpathString = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/options" in { Get(s"/admin/status/options") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val options = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/summary" in { Get(s"/admin/status/summary") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val summary = new String(body.data.toByteArray) } } + + "DashboardService" should "return an OK for admin/status/problems" in { + + Get("/admin/status/problems") ~> + addCredentials(adminCredentials) ~> + route ~> check { + assertResult(OK)(status) + + val problems = new String(body.data.toByteArray) + } + } + + "DashboardService" should "return an OK for admin/status/problems with queries" in { + + Get("/admin/status/problems?offset=2") ~> + addCredentials(adminCredentials) ~> + route ~> check { + assertResult(OK)(status) + + val problems = new String(body.data.toByteArray) + } + } + val dashboardCredentials = BasicHttpCredentials(adminUserName,"shh!") "DashboardService" should "return an OK and pong for fromDashboard/ping" in { Get(s"/fromDashboard/ping") ~> addCredentials(ShrineJwtAuthenticator.createOAuthCredentials(adminUser)) ~> route ~> check { assertResult(OK)(status) val string = new String(body.data.toByteArray) assertResult(""""pong"""")(string) } } "DashboardService" should "reject a fromDashboard/ping with an expired jwts header" in { val config = DashboardConfigSource.config val shrineCertCollection: KeyStoreCertCollection = KeyStoreCertCollection.fromFileRecoverWithClassPath(KeyStoreDescriptorParser(config.getConfig("shrine.keystore"))) val base64Cert = new String(TextCodec.BASE64URL.encode(shrineCertCollection.myCert.get.getEncoded)) val key: PrivateKey = shrineCertCollection.myKeyPair.privateKey val expiration: Date = new Date(System.currentTimeMillis() - 300 * 1000) //bad for 5 minutes val jwtsString = Jwts.builder(). setHeaderParam("kid", base64Cert). setSubject(java.net.InetAddress.getLocalHost.getHostName). setExpiration(expiration). signWith(SignatureAlgorithm.RS512, key). compact() Get(s"/fromDashboard/ping") ~> addCredentials(OAuth2BearerToken(jwtsString)) ~> sealRoute(route) ~> check { assertResult(Unauthorized)(status) } } "DashboardService" should "reject a fromDashboard/ping with no subject" in { val config = DashboardConfigSource.config val shrineCertCollection: KeyStoreCertCollection = KeyStoreCertCollection.fromClassPathResource(KeyStoreDescriptorParser(config.getConfig("shrine.keystore"))) val base64Cert = new String(TextCodec.BASE64URL.encode(shrineCertCollection.myCert.get.getEncoded)) val key: PrivateKey = shrineCertCollection.myKeyPair.privateKey val expiration: Date = new Date(System.currentTimeMillis() + 30 * 1000) val jwtsString = Jwts.builder(). setHeaderParam("kid", base64Cert). setExpiration(expiration). signWith(SignatureAlgorithm.RS512, key). compact() Get(s"/fromDashboard/ping") ~> addCredentials(OAuth2BearerToken(jwtsString)) ~> sealRoute(route) ~> check { assertResult(Unauthorized)(status) } } "DashboardService" should "reject a fromDashboard/ping with no Authorization header" in { Get(s"/fromDashboard/ping") ~> sealRoute(route) ~> check { assertResult(Unauthorized)(status) } } "DashboardService" should "reject a fromDashboard/ping with an Authorization header for the wrong authorization spec" in { Get(s"/fromDashboard/ping") ~> addCredentials(adminCredentials) ~> sealRoute(route) ~> check { assertResult(Unauthorized)(status) } } /* "DashboardService" should "not find a bogus web service to talk to" in { Get(s"/toDashboard/bogus.harvard.edu/ping") ~> addCredentials(adminCredentials) ~> sealRoute(route) ~> check { val string = new String(body.data.toByteArray) assertResult(NotFound)(status) } } */ } diff --git a/apps/shrine-app/src/test/scala/net/shrine/status/StatusJaxrsTest.scala b/apps/shrine-app/src/test/scala/net/shrine/status/StatusJaxrsTest.scala index fe851a016..156efa97a 100644 --- a/apps/shrine-app/src/test/scala/net/shrine/status/StatusJaxrsTest.scala +++ b/apps/shrine-app/src/test/scala/net/shrine/status/StatusJaxrsTest.scala @@ -1,132 +1,133 @@ package net.shrine.status import com.typesafe.config.ConfigFactory +import net.shrine.problem.TurnOffProblemConnector import net.shrine.util.ShouldMatchersForJUnit import org.json4s.{DefaultFormats, Formats} import org.junit.Test import org.json4s.native.Serialization import scala.collection.immutable.Map /** * Tests for StatusJaxrs * * @author david * @since 12/2/15 */ -class StatusJaxrsTest extends ShouldMatchersForJUnit { +class StatusJaxrsTest extends ShouldMatchersForJUnit with TurnOffProblemConnector { implicit def json4sFormats: Formats = DefaultFormats val expectedConfig = ConfigFactory.load("shrine") val statusJaxrs = StatusJaxrs(expectedConfig) @Test def testVersion() = { val versionString = statusJaxrs.version val version = Serialization.read[Version](versionString) version should equal(Version("changeMe")) } @Test def testConfig() = { val expectedJson4sConfig = Json4sConfig(expectedConfig) val configString = statusJaxrs.config val config = Serialization.read[Json4sConfig](configString) config should equal(expectedJson4sConfig) val passwordKeys = config.keyValues.filter(x => Json4sConfig.isPassword(x._1)) passwordKeys should equal(Map.empty[String,String]) } @Test def testSummary() = { val summaryString = statusJaxrs.summary val summary = Serialization.read[Summary](summaryString) summary.isHub should be (true) summary.adapterMappingsFileName.isDefined should be (true) summary.adapterMappingsDate.isEmpty should be (true) summary.adapterOk should be (true) summary.keystoreOk should be (true) summary.hubOk should be (false) summary.qepOk should be (true) } @Test def testI2b2() = { val i2b2String = statusJaxrs.i2b2 val i2b2 = Serialization.read[I2b2](i2b2String) i2b2.crcUrl.isDefined should be (true) } @Test def testOptionalParts() = { val string = statusJaxrs.optionalParts val actual = Serialization.read[OptionalParts](string) actual.isHub should be (true) actual.stewardEnabled should be (true) actual.shouldQuerySelf should be (false) actual.downstreamNodes.size should be (4) } @Test def testHub() = { val string = statusJaxrs.hub val actual = Serialization.read[Hub](string) actual.create should be (true) actual.shouldQuerySelf should be (false) actual.downstreamNodes.size should be (4) } @Test def testQep() = { val string = statusJaxrs.qep val actual = Serialization.read[Qep](string) actual.create should be (true) actual.attachSigningCert should be (true) actual.authenticationType should be ("PmAuthenticator") actual.authorizationType should be ("StewardQueryAuthorizationService") actual.includeAggregateResults should be (false) actual.maxQueryWaitTimeMillis should be (300000000L) } @Test def testAdapter() = { val string = statusJaxrs.adapter val actual = Serialization.read[Adapter](string) actual.adapterLockoutAttemptsThreshold should be (10) } @Test def testKeyStore() = { val string = statusJaxrs.keystore val actual = Serialization.read[KeyStoreReport](string) println(s"KeyStoreReport is $actual") } } diff --git a/apps/steward-app/src/main/scala/net/shrine/steward/StewardConfigSource.scala b/apps/steward-app/src/main/scala/net/shrine/steward/StewardConfigSource.scala index 8859a3859..8507fd334 100644 --- a/apps/steward-app/src/main/scala/net/shrine/steward/StewardConfigSource.scala +++ b/apps/steward-app/src/main/scala/net/shrine/steward/StewardConfigSource.scala @@ -1,53 +1,31 @@ package net.shrine.steward -import com.typesafe.config.{Config, ConfigFactory} import net.shrine.authorization.steward.TopicState -import net.shrine.config.AtomicConfigSource +import net.shrine.config.ConfigSource /** - * Source of typesafe config for the data steward app. - * - * @author david - * @since 4/29/15 - */ - -object StewardConfigSource { - - val atomicConfig = new AtomicConfigSource(ConfigFactory.load("steward")) - - def config:Config = { - atomicConfig.config - } - - def configForBlock[T](key:String,value:AnyRef,origin:String)(block: => T):T = { - atomicConfig.configForBlock(key,value,origin)(block) - } - + * Source of typesafe config for the data steward app. + * + * @author david + * @since 4/29/15 + */ + +object StewardConfigSource extends ConfigSource { + override val configName = "steward" val createTopicsModeConfigKey = "shrine.steward.createTopicsMode" def createTopicsInState:CreateTopicsMode = CreateTopicsMode.namesToCreateTopicsMode(config.getString(createTopicsModeConfigKey)) - def objectForName[T](objectName:String):T = { - - import scala.reflect.runtime.universe - val runtimeMirror = universe.runtimeMirror(getClass.getClassLoader) - val module = runtimeMirror.staticModule(objectName) - - val reflectedObj = runtimeMirror.reflectModule(module) - val obj = reflectedObj.instance - - obj.asInstanceOf[T] - } } sealed case class CreateTopicsMode(name:String,topicState: TopicState) object CreateTopicsMode{ val Pending = CreateTopicsMode(TopicState.pending) val Approved = CreateTopicsMode(TopicState.approved) val TopicsIgnoredJustLog = CreateTopicsMode("TopicsIgnoredJustLog",TopicState.approved) val all = Set(Pending,Approved,TopicsIgnoredJustLog) val namesToCreateTopicsMode: Map[String, CreateTopicsMode] = all.map(x => (x.name,x)).toMap def apply(topicState: TopicState):CreateTopicsMode = CreateTopicsMode(topicState.name,topicState) } \ No newline at end of file diff --git a/commons/auth/src/main/scala/net/shrine/authentication/NotAuthenticatedException.scala b/commons/auth/src/main/scala/net/shrine/authentication/NotAuthenticatedException.scala index ec5e2bd41..3b8952fa9 100644 --- a/commons/auth/src/main/scala/net/shrine/authentication/NotAuthenticatedException.scala +++ b/commons/auth/src/main/scala/net/shrine/authentication/NotAuthenticatedException.scala @@ -1,36 +1,36 @@ package net.shrine.authentication import net.shrine.authentication.AuthenticationResult.NotAuthenticated -import net.shrine.problem.{ProblemSources, AbstractProblem} +import net.shrine.problem.{AbstractProblem, ProblemSources} import scala.xml.NodeSeq /** * @author clint * @since Dec 13, 2013 */ final case class NotAuthenticatedException(domain: String, username: String,message: String, cause: Throwable) extends RuntimeException(message, cause) { def problem = NotAuthenticatedProblem(this) } object NotAuthenticatedException { def apply(na:NotAuthenticated):NotAuthenticatedException = NotAuthenticatedException(na.domain,na.username,na.message,na.cause.getOrElse(null)) } case class NotAuthenticatedProblem(nax:NotAuthenticatedException) extends AbstractProblem(ProblemSources.Qep){ - override val summary = s"Can not authenticate ${nax.domain}:${nax.username}." + override lazy val summary = s"Can not authenticate ${nax.domain}:${nax.username}." - override val throwable = Some(nax) + override lazy val throwable = Some(nax) - override val description = s"Can not authenticate ${nax.domain}:${nax.username}. ${nax.getLocalizedMessage}" + override lazy val description = s"Can not authenticate ${nax.domain}:${nax.username}. ${nax.getLocalizedMessage}" - override val detailsXml: NodeSeq = NodeSeq.fromSeq( + override lazy val detailsXml: NodeSeq = NodeSeq.fromSeq(
{throwableDetail.getOrElse("")}
) } \ No newline at end of file diff --git a/commons/auth/src/main/scala/net/shrine/authorization/PmAuthorizerComponent.scala b/commons/auth/src/main/scala/net/shrine/authorization/PmAuthorizerComponent.scala index 6b103dcb1..0ca15c035 100644 --- a/commons/auth/src/main/scala/net/shrine/authorization/PmAuthorizerComponent.scala +++ b/commons/auth/src/main/scala/net/shrine/authorization/PmAuthorizerComponent.scala @@ -1,111 +1,112 @@ package net.shrine.authorization import net.shrine.log.Loggable -import net.shrine.problem.{LoggingProblemHandler, Problem, ProblemSources, AbstractProblem, ProblemDigest} import scala.util.{Failure, Success, Try} import net.shrine.client.HttpResponse import net.shrine.i2b2.protocol.pm.GetUserConfigurationRequest import net.shrine.i2b2.protocol.pm.User +import net.shrine.problem._ import net.shrine.protocol.AuthenticationInfo import net.shrine.protocol.ErrorResponse + import scala.util.control.NonFatal /** * @author clint * @since Apr 5, 2013 */ trait PmAuthorizerComponent { self: PmHttpClientComponent with Loggable => import PmAuthorizerComponent._ //noinspection RedundantBlock object Pm { def parsePmResult(authn: AuthenticationInfo)(httpResponse: HttpResponse): Try[Either[ErrorResponse, User]] = { User.fromI2b2(httpResponse.body).map(Right(_)).recoverWith { case NonFatal(e) => { debug(s"Couldn't extract a User from '$httpResponse'") Try(Left(ErrorResponse.fromI2b2(httpResponse.body))) } }.recover { case NonFatal(e) => { val problem = CouldNotInterpretResponseFromPmCell(pmPoster.url,authn,httpResponse,e) LoggingProblemHandler.handleProblem(problem) Left(ErrorResponse(problem)) } } } def authorize(projectId: String, neededRoles: Set[String], authn: AuthenticationInfo): AuthorizationStatus = { val request = GetUserConfigurationRequest(authn) val responseAttempt: Try[HttpResponse] = Try { debug(s"Authorizing with PM cell at ${pmPoster.url}") pmPoster.post(request.toI2b2String) } val authStatusAttempt: Try[AuthorizationStatus with Product with Serializable] = responseAttempt.flatMap(parsePmResult(authn)).map { case Right(user) => { val managerUserOption = for { roles <- user.rolesByProject.get(projectId) if neededRoles.forall(roles.contains) } yield user managerUserOption.map(Authorized).getOrElse { NotAuthorized(MissingRequiredRoles(projectId,neededRoles,authn)) } } case Left(errorResponse) => { //todo remove when ErrorResponse gets its message info(s"ErrorResponse message '${errorResponse.errorMessage}' may not have carried through to the NotAuthorized object") NotAuthorized(errorResponse.problemDigest) } } authStatusAttempt match { case Success(s) => s case Failure(x) => NotAuthorized(CouldNotReachPmCell(pmPoster.url,authn,x)) } } } } object PmAuthorizerComponent { sealed trait AuthorizationStatus case class Authorized(user: User) extends AuthorizationStatus case class NotAuthorized(problemDigest: ProblemDigest) extends AuthorizationStatus { def toErrorResponse = ErrorResponse(problemDigest.summary,problemDigest) } object NotAuthorized { def apply(problem:Problem):NotAuthorized = NotAuthorized(problem.toDigest) } } case class MissingRequiredRoles(projectId: String, neededRoles: Set[String], authn: AuthenticationInfo) extends AbstractProblem(ProblemSources.Qep) { - override val summary: String = s"User ${authn.domain}:${authn.username} is missing roles in project '$projectId'" + override lazy val summary: String = s"User ${authn.domain}:${authn.username} is missing roles in project '$projectId'" - override val description:String = s"User ${authn.domain}:${authn.username} does not have all the needed roles: ${neededRoles.map("'" + _ + "'").mkString(", ")} in the project '$projectId'" + override lazy val description:String = s"User ${authn.domain}:${authn.username} does not have all the needed roles: ${neededRoles.map("'" + _ + "'").mkString(", ")} in the project '$projectId'" } case class CouldNotReachPmCell(pmUrl:String,authn: AuthenticationInfo,x:Throwable) extends AbstractProblem(ProblemSources.Qep) { - override val throwable = Some(x) - override val summary: String = s"Could not reach PM cell." - override val description:String = s"Shrine encountered ${throwable.get} while attempting to reach the PM cell at $pmUrl for ${authn.domain}:${authn.username}." + override lazy val throwable = Some(x) + override lazy val summary: String = s"Could not reach PM cell." + override lazy val description:String = s"Shrine encountered ${throwable.get} while attempting to reach the PM cell at $pmUrl for ${authn.domain}:${authn.username}." } case class CouldNotInterpretResponseFromPmCell(pmUrl:String,authn: AuthenticationInfo,httpResponse: HttpResponse,x:Throwable) extends AbstractProblem(ProblemSources.Qep) { - override val throwable = Some(x) + override lazy val throwable = Some(x) override def summary: String = s"Could not interpret response from PM cell." override def description: String = s"Shrine could not interpret the response from the PM cell at ${pmUrl} for ${authn.domain}:${authn.username}: due to ${throwable.get}" - override val detailsXml =
+ override lazy val detailsXml =
Response is {httpResponse} {throwableDetail.getOrElse("")}
} \ No newline at end of file diff --git a/commons/auth/src/main/scala/net/shrine/authorization/StewardQueryAuthorizationService.scala b/commons/auth/src/main/scala/net/shrine/authorization/StewardQueryAuthorizationService.scala index 7774bb1de..7e5e7dec1 100644 --- a/commons/auth/src/main/scala/net/shrine/authorization/StewardQueryAuthorizationService.scala +++ b/commons/auth/src/main/scala/net/shrine/authorization/StewardQueryAuthorizationService.scala @@ -1,240 +1,236 @@ package net.shrine.authorization import java.net.URL -import javax.net.ssl.{KeyManager, X509TrustManager, SSLContext} +import javax.net.ssl.{KeyManager, SSLContext, X509TrustManager} import java.security.cert.X509Certificate import akka.io.IO import com.typesafe.config.{Config, ConfigFactory} -import net.shrine.authorization.AuthorizationResult.{NotAuthorized, Authorized} -import net.shrine.authorization.steward.{TopicIdAndName, ResearchersTopics, InboundShrineQuery} +import net.shrine.authorization.AuthorizationResult.{Authorized, NotAuthorized} +import net.shrine.authorization.steward.{InboundShrineQuery, ResearchersTopics, TopicIdAndName} import net.shrine.log.Loggable -import net.shrine.problem.{ProblemSources, AbstractProblem} -import net.shrine.protocol.{AuthenticationInfo, ApprovedTopic, RunQueryRequest, ReadApprovedQueryTopicsResponse, ErrorResponse, ReadApprovedQueryTopicsRequest} +import net.shrine.protocol.{ApprovedTopic, AuthenticationInfo, ErrorResponse, ReadApprovedQueryTopicsRequest, ReadApprovedQueryTopicsResponse, RunQueryRequest} import net.shrine.config.ConfigExtensions - import org.json4s.native.JsonMethods.parse import org.json4s.{DefaultFormats, Formats} - import akka.actor.ActorSystem import akka.util.Timeout import akka.pattern.ask - +import net.shrine.problem.{AbstractProblem, ProblemSources} import spray.can.Http import spray.can.Http.{HostConnectorInfo, HostConnectorSetup} - -import spray.http.{HttpResponse, HttpRequest, BasicHttpCredentials} -import spray.http.StatusCodes.{Unauthorized, OK, UnavailableForLegalReasons} +import spray.http.{BasicHttpCredentials, HttpRequest, HttpResponse} +import spray.http.StatusCodes.{OK, Unauthorized, UnavailableForLegalReasons} import spray.httpx.TransformerPipelineSupport.WithTransformation import spray.httpx.Json4sSupport -import spray.client.pipelining.{addCredentials,sendReceive,Post,Get} -import spray.io.{SSLContextProvider, PipelineContext, ClientSSLEngineProvider} +import spray.client.pipelining.{Get, Post, addCredentials, sendReceive} +import spray.io.{ClientSSLEngineProvider, PipelineContext, SSLContextProvider} -import scala.concurrent.duration.{Duration, FiniteDuration, DurationInt} +import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration} import scala.concurrent.{Await, Future} import scala.language.postfixOps /** * A QueryAuthorizationService that talks to the standard data steward application to learn about topics (intents) and check that a * shrine query can be run * * @author david * @since 4/2/15 */ final case class StewardQueryAuthorizationService(qepUserName:String, qepPassword:String, stewardBaseUrl:URL, defaultTimeout:FiniteDuration = 10 seconds) extends QueryAuthorizationService with Loggable with Json4sSupport { import system.dispatcher // execution context for futures implicit val system = ActorSystem("AuthorizationServiceActors",ConfigFactory.load("shrine")) //todo use shrine's config implicit val timeout:Timeout = Timeout.durationToTimeout(defaultTimeout)//10 seconds implicit def json4sFormats: Formats = DefaultFormats val qepCredentials = BasicHttpCredentials(qepUserName,qepPassword) def sendHttpRequest(httpRequest: HttpRequest):Future[HttpResponse] = { // Place a special SSLContext in scope here to be used by HttpClient. // It trusts all server certificates. // Most important - it will encrypt all of the traffic on the wire. implicit def trustfulSslContext: SSLContext = { object BlindFaithX509TrustManager extends X509TrustManager { def checkClientTrusted(chain: Array[X509Certificate], authType: String) = (info(s"Client asked BlindFaithX509TrustManager to check $chain for $authType")) def checkServerTrusted(chain: Array[X509Certificate], authType: String) = (info(s"Server asked BlindFaithX509TrustManager to check $chain for $authType")) def getAcceptedIssuers = Array[X509Certificate]() } val context = SSLContext.getInstance("TLS") context.init(Array[KeyManager](), Array(BlindFaithX509TrustManager), null) context } implicit def trustfulSslContextProvider: SSLContextProvider = { SSLContextProvider.forContext(trustfulSslContext) } class CustomClientSSLEngineProvider extends ClientSSLEngineProvider { def apply(pc: PipelineContext) = ClientSSLEngineProvider.default(trustfulSslContextProvider).apply(pc) } implicit def sslEngineProvider: ClientSSLEngineProvider = new CustomClientSSLEngineProvider val requestWithCredentials = httpRequest ~> addCredentials(qepCredentials) val responseFuture: Future[HttpResponse] = for { HostConnectorInfo(hostConnector, _) <- { val hostConnectorSetup = new HostConnectorSetup(httpRequest.uri.authority.host.address, httpRequest.uri.authority.port, sslEncryption = httpRequest.uri.scheme=="https")( sslEngineProvider = sslEngineProvider) IO(Http) ask hostConnectorSetup } response <- sendReceive(hostConnector).apply(requestWithCredentials) _ <- hostConnector ask Http.CloseAll } yield response responseFuture } /* todo to recycle connections with http://spray.io/documentation/1.2.3/spray-client/ if needed def sendHttpRequest(httpRequest: HttpRequest):Future[HttpResponse] = { import akka.io.IO import akka.pattern.ask import spray.can.Http val requestWithCredentials = httpRequest ~> addCredentials(qepCredentials) //todo failures via onFailure callbacks for{ sendR:SendReceive <- connectorSource response:HttpResponse <- sendR(requestWithCredentials) } yield response } val connectorSource: Future[SendReceive] = //Future[HttpRequest => Future[HttpResponse]] for ( //keep asking for a connector until you get one //todo correct URL // Http.HostConnectorInfo(connector, _) <- IO(Http) ? Http.HostConnectorSetup("www.spray.io", port = 8080) Http.HostConnectorInfo(connector, _) <- IO(Http) ? Http.HostConnectorSetup("localhost", port = 6060) ) yield sendReceive(connector) */ def sendAndReceive(httpRequest: HttpRequest,timeout:Duration = defaultTimeout):HttpResponse = { info("StewardQueryAuthorizationService will request "+httpRequest.uri) //todo someday log request and response val responseFuture = sendHttpRequest(httpRequest) val response:HttpResponse = Await.result(responseFuture,timeout) info("StewardQueryAuthorizationService received response with status "+response.status) response } //Contact a data steward and either return an Authorized or a NotAuthorized or throw an exception override def authorizeRunQueryRequest(runQueryRequest: RunQueryRequest): AuthorizationResult = { debug(s"authorizeRunQueryRequest started for ${runQueryRequest.queryDefinition.name}") val interpreted = runQueryRequest.topicId.fold( authorizeRunQueryRequestNoTopic(runQueryRequest) )( authorizeRunQueryRequestForTopic(runQueryRequest,_) ) debug(s"authorizeRunQueryRequest completed with $interpreted) for ${runQueryRequest.queryDefinition.name}") interpreted } def authorizeRunQueryRequestNoTopic(runQueryRequest: RunQueryRequest): AuthorizationResult = { val userName = runQueryRequest.authn.username val queryId = runQueryRequest.queryDefinition.name //xml's .text returns something that looks like xquery with backwards slashes. toString() returns xml. val queryForJson = InboundShrineQuery(runQueryRequest.networkQueryId,queryId,runQueryRequest.queryDefinition.toXml.toString()) val request = Post(s"$stewardBaseUrl/steward/qep/requestQueryAccess/user/$userName", queryForJson) val response:HttpResponse = sendAndReceive(request,runQueryRequest.waitTime) interpretAuthorizeRunQueryResponse(response) } def authorizeRunQueryRequestForTopic(runQueryRequest: RunQueryRequest,topicIdString:String): AuthorizationResult = { val userName = runQueryRequest.authn.username val queryId = runQueryRequest.queryDefinition.name //xml's .text returns something that looks like xquery with backwards slashes. toString() returns xml. val queryForJson = InboundShrineQuery(runQueryRequest.networkQueryId,queryId,runQueryRequest.queryDefinition.toXml.toString()) val request = Post(s"$stewardBaseUrl/steward/qep/requestQueryAccess/user/$userName/topic/$topicIdString", queryForJson) val response:HttpResponse = sendAndReceive(request,runQueryRequest.waitTime) debug(s"authorizeRunQueryRequestForTopic response is $response") interpretAuthorizeRunQueryResponse(response) } /** Interpret the response from the steward app. Primarily here for testing. */ def interpretAuthorizeRunQueryResponse(response:HttpResponse):AuthorizationResult = { response.status match { case OK => { val topicJson = new String(response.entity.data.toByteArray) debug(s"topicJson is $topicJson") val topic:Option[TopicIdAndName] = parse(topicJson).extractOpt[TopicIdAndName] debug(s"topic is $topic") Authorized(topic.map(x => (x.id,x.name))) } case UnavailableForLegalReasons => NotAuthorized(response.entity.asString) case Unauthorized => throw new AuthorizationException(s"steward rejected qep's login credentials. $response") case _ => throw new AuthorizationException(s"QueryAuthorizationService detected a problem: $response") } } //Either read the approved topics from a data steward or have an error response. override def readApprovedEntries(readTopicsRequest: ReadApprovedQueryTopicsRequest): Either[ErrorResponse, ReadApprovedQueryTopicsResponse] = { val userName = readTopicsRequest.authn.username val request = Get(s"$stewardBaseUrl/steward/qep/approvedTopics/user/$userName") val response:HttpResponse = sendAndReceive(request,readTopicsRequest.waitTime) if(response.status == OK) { val topicsJson = new String(response.entity.data.toByteArray) val topicsFromSteward: ResearchersTopics = parse(topicsJson).extract[ResearchersTopics] val topics: Seq[ApprovedTopic] = topicsFromSteward.topics.map(topic => ApprovedTopic(topic.id, topic.name)) Right(ReadApprovedQueryTopicsResponse(topics)) } else Left(ErrorResponse(ErrorStatusFromDataStewardApp(response,stewardBaseUrl))) } override def toString() = { super.toString().replaceAll(qepPassword,"REDACTED") } } object StewardQueryAuthorizationService { def apply(config:Config):StewardQueryAuthorizationService = StewardQueryAuthorizationService ( qepUserName = config.getString("qepUserName"), qepPassword = config.getString("qepPassword"), stewardBaseUrl = config.get("stewardBaseUrl", new URL(_)) ) } case class ErrorStatusFromDataStewardApp(response:HttpResponse,stewardBaseUrl:URL) extends AbstractProblem(ProblemSources.Qep) { - override val summary: String = s"Data Steward App responded with status ${response.status}" - override val description:String = s"The Data Steward App at ${stewardBaseUrl} responded with status ${response.status}, not OK." - override val detailsXml =
+ override lazy val summary: String = s"Data Steward App responded with status ${response.status}" + override lazy val description:String = s"The Data Steward App at ${stewardBaseUrl} responded with status ${response.status}, not OK." + override lazy val detailsXml =
Response is {response} {throwableDetail.getOrElse("")}
} \ No newline at end of file diff --git a/commons/auth/src/test/scala/net/shrine/authorization/PmAuthorizerComponentTest.scala b/commons/auth/src/test/scala/net/shrine/authorization/PmAuthorizerComponentTest.scala index b5ab67f56..34d653ccc 100644 --- a/commons/auth/src/test/scala/net/shrine/authorization/PmAuthorizerComponentTest.scala +++ b/commons/auth/src/test/scala/net/shrine/authorization/PmAuthorizerComponentTest.scala @@ -1,199 +1,199 @@ package net.shrine.authorization import net.shrine.log.Loggable -import net.shrine.problem.ProblemNotYetEncoded import org.junit.Test import net.shrine.util.ShouldMatchersForJUnit import net.shrine.client.HttpClient import net.shrine.i2b2.protocol.pm.User import net.shrine.protocol.AuthenticationInfo import net.shrine.protocol.Credential import net.shrine.util.XmlUtil import net.shrine.client.Poster +import net.shrine.problem.TurnOffProblemConnector /** * @author clint * @since Apr 5, 2013 */ //noinspection UnitMethodIsParameterless,UnitMethodIsParameterless,EmptyParenMethodAccessedAsParameterless,ScalaUnnecessaryParentheses -final class PmAuthorizerComponentTest extends ShouldMatchersForJUnit { +final class PmAuthorizerComponentTest extends ShouldMatchersForJUnit with TurnOffProblemConnector { import PmAuthorizerComponentTest._ import PmAuthorizerComponent._ //Adds a bogus URL, for convenience private[this] def poster(httpClient: HttpClient) = Poster("", httpClient) @Test def testGoodResponseNotManager { val component = new TestPmAuthorizerComponent(poster(new LazyMockHttpClient(validUserResponseXml.toString))) val NotAuthorized(problemDigest) = component.Pm.authorize(projectId1, Set(User.Roles.Manager), authn) problemDigest should not be(null) problemDigest.codec should be (classOf[MissingRequiredRoles].getName) } @Test def testGoodResponseIsManager { val component = new TestPmAuthorizerComponent(poster(new LazyMockHttpClient(validUserResponseXml.toString))) val Authorized(user) = component.Pm.authorize(projectId2, Set(User.Roles.Manager), authn) user should not be(null) user.fullName should be(fullName) user.username should be(username) user.domain should be(domain) user.credential should be(authn.credential) } @Test def testErrorResponse { val component = new TestPmAuthorizerComponent(poster(new LazyMockHttpClient(i2b2ErrorXml.toString))) val NotAuthorized(problemDigest) = component.Pm.authorize(projectId1, Set.empty, authn) problemDigest should not be(null) problemDigest.codec should be (classOf[CouldNotInterpretResponseFromPmCell].getName) } @Test def testJunkResponse { val component = new TestPmAuthorizerComponent(poster(new LazyMockHttpClient("jahfskajhkjashfdkjashkfd"))) val NotAuthorized(problemDigest) = component.Pm.authorize(projectId1, Set.empty, authn) problemDigest should not be(null) problemDigest.codec should be (classOf[CouldNotReachPmCell].getName) } @Test def testResponseThatBlowsUp() { val component = new TestPmAuthorizerComponent(poster(new LazyMockHttpClient(throw new Exception with scala.util.control.NoStackTrace))) val NotAuthorized(problemDigest) = component.Pm.authorize(projectId1, Set.empty, authn) problemDigest should not be(null) problemDigest.codec should be (classOf[CouldNotReachPmCell].getName) } private val fullName = "Some Person" private val username = "some-user" private val domain = "some-place" private val password = "sal;dk;aslkd;" private val errorMessage = "Something blew up" private lazy val authn = AuthenticationInfo(domain, username, Credential(password, isToken = true)) private val projectId1 = "foo" private val projectId2 = "bar" private val params1 = Map("x" -> "1", "y" -> "2") private val params2 = Map("y" -> "2", "z" -> "3") private val roles1 = Set("a", "b", "c") private val roles2 = Set("MANAGER", "x", "y") private lazy val projects = Seq((projectId1, params1, roles1), (projectId2, params2, roles2)) private lazy val validUserResponseXml = XmlUtil.stripWhitespace { 1.1 2.4 SHRINE 1.3-compatible SHRINE 2011-04-08T16:21:12.251-04:00 DONE DEVELOPMENT http://www.i2b2.org {fullName} {username} {password} {domain} { projects.map { case (projectId, params, roles) => Demo Group { projectId } http://www.i2b2.org { params.map { case (name, value) => { value } } } { roles.map(r => { r }) } } } Data Repository http://localhost/crc REST Ontology Cell http://localhost/ont REST false 200 false } private val i2b2ErrorXml = XmlUtil.stripWhitespace { 1.1 2.4 SHRINE 1.3-compatible SHRINE 2011-04-08T16:21:12.251-04:00 { errorMessage } } } object PmAuthorizerComponentTest { final class TestPmAuthorizerComponent(override val pmPoster: Poster) extends PmAuthorizerComponent with PmHttpClientComponent with Loggable } \ No newline at end of file diff --git a/commons/config/src/main/scala/net/shrine/config/ConfigSource.scala b/commons/config/src/main/scala/net/shrine/config/ConfigSource.scala new file mode 100644 index 000000000..686a46685 --- /dev/null +++ b/commons/config/src/main/scala/net/shrine/config/ConfigSource.scala @@ -0,0 +1,36 @@ +package net.shrine.config +import com.typesafe.config.{Config, ConfigFactory} + +/** + * @author ty + * @since 7/22/16 + */ +trait ConfigSource { + + val configName: String + + lazy val atomicConfig = new AtomicConfigSource(ConfigFactory.load(configName)) + + def config: Config = { + atomicConfig.config + } + def configForBlock[T](key: String, value: AnyRef, origin: String)(block: => T): T = { + atomicConfig.configForBlock(key, value, origin)(block) + } + + def configForBlock[T](config:Config,origin:String)(block: => T):T = { + atomicConfig.configForBlock(config,origin)(block) + } + + def objectForName[T](objectName: String): T = { + + import scala.reflect.runtime.universe + val runtimeMirror = universe.runtimeMirror(getClass.getClassLoader) + val module = runtimeMirror.staticModule(objectName) + + val reflectedObj = runtimeMirror.reflectModule(module) + val obj = reflectedObj.instance + + obj.asInstanceOf[T] + } +} diff --git a/commons/data-commons/pom.xml b/commons/data-commons/pom.xml index a1d0df156..061aba0d2 100644 --- a/commons/data-commons/pom.xml +++ b/commons/data-commons/pom.xml @@ -1,79 +1,83 @@ 4.0.0 SHRINE Data Access Classes shrine-data-commons jar net.shrine shrine-base 1.22.0-SNAPSHOT ../../pom.xml src/main/scala src/test/scala net.alchim31.maven scala-maven-plugin org.apache.maven.plugins maven-jar-plugin test-jar net.shrine shrine-test-commons ${project.version} test-jar test + + net.shrine + shrine-config + ${project.version} + + + net.shrine + shrine-util + ${project.version} + org.squeryl squeryl_${scala-major-version} org.scala-lang scalap org.scala-lang scalap ${scala-version} - - net.shrine - shrine-protocol - ${project.version} - - org.springframework spring-jdbc test com.h2database h2 test diff --git a/commons/data-commons/src/main/resources/reference.conf b/commons/data-commons/src/main/resources/reference.conf new file mode 100644 index 000000000..ea14ea6b8 --- /dev/null +++ b/commons/data-commons/src/main/resources/reference.conf @@ -0,0 +1,30 @@ +shrine { + dashboard { + database { + dataSourceFrom = "JNDI" //Can be JNDI or testDataSource . Use testDataSource for tests, JNDI everywhere else + jndiDataSourceName = "java:comp/env/jdbc/shrineDB" //or leave out for tests + slickProfileClassName = "slick.driver.MySQLDriver$" // Can be + // slick.driver.H2Driver$ + // slick.driver.MySQLDriver$ + // slick.driver.PostgresDriver$ + // slick.driver.SQLServerDriver$ + // slick.driver.JdbcDriver$ + // freeslick.OracleProfile$ + // freeslick.MSSQLServerProfile$ + // + // (Yes, with the $ on the end) + + // For testing without JNDI + // testDataSource { + + //typical test settings for unit tests + //driverClassName = "org.h2.Driver" + + //url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" //H2 embedded in-memory for unit tests + //url = "jdbc:h2:~/stewardTest.h2" //H2 embedded on disk at ~/test + // } + createTablesOnStart = false //for testing with H2 in memory, when not running unit tests. Set to false normally + } + } +} + diff --git a/commons/data-commons/src/main/scala/net/shrine/problem/DashboardProblemsDatabase.scala b/commons/data-commons/src/main/scala/net/shrine/problem/DashboardProblemsDatabase.scala new file mode 100644 index 000000000..c14ccc6c5 --- /dev/null +++ b/commons/data-commons/src/main/scala/net/shrine/problem/DashboardProblemsDatabase.scala @@ -0,0 +1,181 @@ +package net.shrine.problem + +import java.util.concurrent.TimeoutException +import javax.sql.DataSource + +import com.typesafe.config.Config +import net.shrine.slick.{CouldNotRunDbIoActionException, TestableDataSourceCreator} +import slick.dbio.SuccessAction +import slick.driver.JdbcProfile +import slick.jdbc.meta.MTable + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} +import scala.util.control.NonFatal +import scala.xml.XML + +/** + * Problems database object, defines the PROBLEMS table schema and related queries, + * as well as all interactions with the database. + * @author ty + * @since 07/16 + */ +object Problems { + val config:Config = ProblemConfigSource.config.getConfig("shrine.dashboard.database") + val slickProfileClassName = config.getString("slickProfileClassName") + // TODO: Can we not pay this 2 second cost here? + val slickProfile:JdbcProfile = ProblemConfigSource.objectForName(slickProfileClassName) + + import slickProfile.api._ + + val dataSource: DataSource = TestableDataSourceCreator.dataSource(config) + lazy val db = { + val db = Database.forDataSource(dataSource) + if (config.hasPath("createTablesOnStart") && config.getBoolean("createTablesOnStart")) + Await.ready(db.run(IOActions.createIfNotExists), FiniteDuration(3, SECONDS)) + db + } + + /** + * The Problems Table. This is the table schema. + */ + class ProblemsT(tag: Tag) extends Table[ProblemDigest](tag, Queries.tableName) { + def id = column[Int]("id", O.PrimaryKey, O.AutoInc) + def codec = column[String]("codec") + def stampText = column[String]("stampText") + def summary = column[String]("summary") + def description = column[String]("description") + def xml = column[String]("detailsXml") + def epoch= column[Long]("epoch") + // projection between table row and problem digest + def * = (id, codec, stampText, summary, description, xml, epoch) <> (rowToProblem, problemToRow) + def idx = index("idx_epoch", epoch, unique=false) + + /** + * Converts a table row into a ProblemDigest. + * @param args the table row, represented as a five-tuple string + * @return the corresponding ProblemDigest + */ + def rowToProblem(args: (Int, String, String, String, String, String, Long)): ProblemDigest = args match { + case (id, codec, stampText, summary, description, detailsXml, epoch) => + ProblemDigest(codec, stampText, summary, description, XML.loadString(detailsXml), epoch) + } + + /** + * Converts a ProblemDigest into an Option of a table row. For now there is no failure + * condition, ie a ProblemDigest can always be a table row, but this is a place for + * possible future error handling + * @param problem the ProblemDigest to convert + * @return an Option of a table row. + */ + def problemToRow(problem: ProblemDigest): Option[(Int, String, String, String, String, String, Long)] = problem match { + case ProblemDigest(codec, stampText, summary, description, detailsXml, epoch) => + // 0 is ignored on insert and replaced with an auto incremented id + Some((0, codec, stampText, summary, description, detailsXml.toString, epoch)) + } + } + + /** + * Queries related to the Problems table. + */ + object Queries extends TableQuery(new ProblemsT(_)) { + /** + * The table name + */ + val tableName = "problems" + + /** + * Equivalent to Select * from Problems; + */ + val selectAll = this + + /** + * Selects all the details xml sorted by the problem's time stamp. + */ + val selectDetails = this.map(_.xml) + + /** + * Selects the last N problems, after the offset + */ + def lastNProblems(n: Int, offset: Int = 0) = this.sortBy(_.epoch.desc).drop(offset).take(n) + } + + + /** + * DBIO Actions. These are pre-defined IO actions that may be useful. + * Using it to centralize the location of DBIOs. + */ + object IOActions { + val problems = Queries + val tableExists = MTable.getTables(problems.tableName).map(_.nonEmpty) + val createIfNotExists = tableExists.flatMap( + if (_) SuccessAction(NoOperation) else problems.schema.create) + val dropIfExists = tableExists.flatMap( + if (_) problems.schema.drop else SuccessAction(NoOperation)) + val resetTable = createIfNotExists >> problems.selectAll.delete + val selectAll = problems.result + def sizeAndProblemDigest(n: Int, offset: Int = 0) = for { + length <- problems.length.result + allProblems <- problems.lastNProblems(n, offset).result + } yield (allProblems, length) + } + + + /** + * Entry point for interacting with the database. Runs IO actions. + */ + object DatabaseConnector { + val IO = IOActions + /** + * Executes a series of IO actions as a single transactions + */ + def executeTransaction(actions: DBIOAction[_, NoStream, _]*): Future[Unit] = { + db.run(DBIO.seq(actions:_*).transactionally) + } + + /** + * Executes a series of IO actions as a single transaction, synchronous + */ + def executeTransactionBlocking(actions: DBIOAction[_, NoStream, _]*)(implicit timeout: Duration): Unit = { + try { + Await.ready(this.executeTransaction(actions: _*), timeout) + } catch { + // TODO: Handle this better + case tx:TimeoutException => throw CouldNotRunDbIoActionException(Problems.dataSource, tx) + case NonFatal(x) => throw CouldNotRunDbIoActionException(Problems.dataSource, x) + } + } + + /** + * Straight copy of db.run + */ + def run[R](dbio: DBIOAction[R, NoStream, _]): Future[R] = { + db.run(dbio) + } + + /** + * Synchronized copy of db.run + */ + def runBlocking[R](dbio: DBIOAction[R, NoStream, _])(implicit timeout: Duration = new FiniteDuration(15, SECONDS)): R = { + try { + Await.result(this.run(dbio), timeout) + } catch { + case tx:TimeoutException => throw CouldNotRunDbIoActionException(Problems.dataSource, tx) + case NonFatal(x) => throw CouldNotRunDbIoActionException(Problems.dataSource, x) + } + } + + /** + * Inserts a problem into the database + * @param problem the ProblemDigest + */ + def insertProblem(problem: ProblemDigest) = { + run(Queries += problem) + } + } +} + + +// For SuccessAction, just a no_op. +case object NoOperation \ No newline at end of file diff --git a/commons/util/src/main/scala/net/shrine/problem/Problem.scala b/commons/data-commons/src/main/scala/net/shrine/problem/Problem.scala similarity index 71% rename from commons/util/src/main/scala/net/shrine/problem/Problem.scala rename to commons/data-commons/src/main/scala/net/shrine/problem/Problem.scala index e9bec7030..fbe8beac8 100644 --- a/commons/util/src/main/scala/net/shrine/problem/Problem.scala +++ b/commons/data-commons/src/main/scala/net/shrine/problem/Problem.scala @@ -1,168 +1,198 @@ package net.shrine.problem import java.net.InetAddress +import java.text.SimpleDateFormat import java.util.Date import net.shrine.log.Loggable -import net.shrine.serialization.{XmlUnmarshaller, XmlMarshaller} +import net.shrine.serialization.{XmlMarshaller, XmlUnmarshaller} +import scala.concurrent.Future import scala.xml.{Elem, Node, NodeSeq} /** * Describes what information we have about a problem at the site in code where we discover it. * * @author david * @since 8/6/15 */ -trait Problem { +trait Problem extends DelayedInit { def summary:String def problemName = getClass.getName def throwable:Option[Throwable] = None def stamp:Stamp def description:String - def exceptionXml(exception:Option[Throwable]): Option[Elem] = exception.map{x => + def exceptionXml(exception:Option[Throwable]): Option[Elem] = { + exception.map{x => {x.getClass.getName} {x.getMessage} {x.getStackTrace.map(line => {line})}{exceptionXml(Option(x.getCause)).getOrElse("")} - } + }} - def throwableDetail = exceptionXml(throwable) + def throwableDetail: Option[Elem] = exceptionXml(throwable) def detailsXml: NodeSeq = NodeSeq.fromSeq(
{throwableDetail.getOrElse("")}
) - def toDigest:ProblemDigest = ProblemDigest(problemName,stamp.pretty,summary,description,detailsXml) + def toDigest:ProblemDigest = ProblemDigest(problemName,stamp.pretty,summary,description,detailsXml, stamp.time) + + override def delayedInit(code: => Unit): Unit = { + code + if (!ProblemConfigSource.turnOffConnector) { + val problem = Problems + problem.DatabaseConnector.insertProblem(this.toDigest) + } + } } -case class ProblemDigest(codec: String, stampText: String, summary: String, description: String, detailsXml: NodeSeq) extends XmlMarshaller { +case class ProblemDigest(codec: String, stampText: String, summary: String, description: String, detailsXml: NodeSeq, epoch: Long) extends XmlMarshaller { override def toXml: Node = { {codec} {stampText} {summary} {description} + {epoch} {detailsXml} } /** * Ignores detailXml. equals with scala.xml is impossible. See http://www.scala-lang.org/api/2.10.3/index.html#scala.xml.Equality$ */ override def equals(other: Any): Boolean = other match { case that: ProblemDigest => (that canEqual this) && codec == that.codec && stampText == that.stampText && summary == that.summary && - description == that.description + description == that.description && + epoch == that.epoch case _ => false } /** * Ignores detailXml */ override def hashCode: Int = { val prime = 67 - codec.hashCode + prime * (stampText.hashCode + prime *(summary.hashCode + prime * description.hashCode)) + codec.hashCode + prime * (stampText.hashCode + prime *(summary.hashCode + prime * (description.hashCode + prime * epoch.hashCode()))) } } object ProblemDigest extends XmlUnmarshaller[ProblemDigest] with Loggable { - override def fromXml(xml: NodeSeq): ProblemDigest = { val problemNode = xml \ "problem" require(problemNode.nonEmpty,s"No problem tag in $xml") def extractText(tagName:String) = (problemNode \ tagName).text val codec = extractText("codec") val stampText = extractText("stamp") val summary = extractText("summary") val description = extractText("description") val detailsXml: NodeSeq = problemNode \ "details" + val epoch = + try { extractText("epoch").toLong } + catch { case nx:NumberFormatException => + error(s"While parsing xml representing a ProblemDigest, the epoch could not be parsed into a long", nx) + 0 + } - ProblemDigest(codec,stampText,summary,description,detailsXml) + + ProblemDigest(codec,stampText,summary,description,detailsXml,epoch) } + + } case class Stamp(host:InetAddress,time:Long,source:ProblemSources.ProblemSource) { def pretty = s"${new Date(time)} on ${host.getHostName} ${source.pretty}" } object Stamp { - def apply(source:ProblemSources.ProblemSource): Stamp = Stamp(InetAddress.getLocalHost,System.currentTimeMillis(),source) + //TODO: val dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")? + //TODO: Currently the stamp text is locale specific, which can change depending on the jre/computer running it... + def apply(source:ProblemSources.ProblemSource, timer: => Long): Stamp = Stamp(InetAddress.getLocalHost, timer,source) } +/** + * An abstract problem to enable easy creation of Problems. Note that when overriding fields, + * you should only use def or lazy val, and not val. + * See: http://stackoverflow.com/questions/15346600/field-inside-object-which-extends-app-trait-is-set-to-null-why-is-that-so + * @param source + */ abstract class AbstractProblem(source:ProblemSources.ProblemSource) extends Problem { - val stamp = Stamp(source) + def timer = System.currentTimeMillis + lazy val stamp = Stamp(source, timer) } trait ProblemHandler { def handleProblem(problem:Problem) } /** * An example problem handler */ object LoggingProblemHandler extends ProblemHandler with Loggable { override def handleProblem(problem: Problem): Unit = { problem.throwable.fold(error(problem.toString))(throwable => error(problem.toString,throwable) ) } } object ProblemSources{ sealed trait ProblemSource { def pretty = getClass.getSimpleName.dropRight(1) } case object Adapter extends ProblemSource case object Hub extends ProblemSource case object Qep extends ProblemSource case object Dsa extends ProblemSource case object Unknown extends ProblemSource def problemSources = Set(Adapter,Hub,Qep,Dsa,Unknown) } case class ProblemNotYetEncoded(internalSummary:String,t:Option[Throwable] = None) extends AbstractProblem(ProblemSources.Unknown){ override val summary = "An unanticipated problem encountered." override val throwable = { val rx = t.fold(new IllegalStateException(s"$summary"))( new IllegalStateException(s"$summary",_) ) rx.fillInStackTrace() Some(rx) } val reportedAtStackTrace = new IllegalStateException("Capture reporting stack trace.") override val description = "This problem is not yet classified in Shrine source code. Please report the details to the Shrine dev team." override val detailsXml: NodeSeq = NodeSeq.fromSeq(
{internalSummary} {throwableDetail.getOrElse("")}
) } object ProblemNotYetEncoded { def apply(summary:String,x:Throwable):ProblemNotYetEncoded = ProblemNotYetEncoded(summary,Some(x)) } diff --git a/commons/data-commons/src/main/scala/net/shrine/problem/ProblemConfigSource.scala b/commons/data-commons/src/main/scala/net/shrine/problem/ProblemConfigSource.scala new file mode 100644 index 000000000..c823e3ff1 --- /dev/null +++ b/commons/data-commons/src/main/scala/net/shrine/problem/ProblemConfigSource.scala @@ -0,0 +1,18 @@ +package net.shrine.problem + +import com.typesafe.config.{ConfigValue, ConfigValueFactory} +import net.shrine.config.ConfigSource + +/** + * Source of typesafe config for the problems database + * + * @author ty + * @since 7/22/16 + */ +object ProblemConfigSource extends ConfigSource { + override val configName: String = "dashboard" + + // Makes it so constructing a problem in this context won't log it to the connector + // Does not stop you from constructing the connector and using it manually + var turnOffConnector = false +} diff --git a/commons/util/src/test/scala/net/shrine/problem/TestProblem.scala b/commons/data-commons/src/main/scala/net/shrine/problem/TestProblem.scala similarity index 59% rename from commons/util/src/test/scala/net/shrine/problem/TestProblem.scala rename to commons/data-commons/src/main/scala/net/shrine/problem/TestProblem.scala index eeffb2e1b..f961c80dd 100644 --- a/commons/util/src/test/scala/net/shrine/problem/TestProblem.scala +++ b/commons/data-commons/src/main/scala/net/shrine/problem/TestProblem.scala @@ -1,9 +1,15 @@ package net.shrine.problem /** * @author david * @since 1.22 */ case class TestProblem(override val summary: String = "test summary", override val description:String = "test description", - override val throwable: Option[Throwable] = None) extends AbstractProblem(ProblemSources.Unknown) + override val throwable: Option[Throwable] = None) extends AbstractProblem(ProblemSources.Unknown) { + override def timer = 0 + // No point in logging test problems + //override def delayedInit(code: => Unit) = { + // code + //} +} diff --git a/commons/data-commons/src/main/scala/net/shrine/problem/TurnOffProblemConnector.scala b/commons/data-commons/src/main/scala/net/shrine/problem/TurnOffProblemConnector.scala new file mode 100644 index 000000000..c8290495b --- /dev/null +++ b/commons/data-commons/src/main/scala/net/shrine/problem/TurnOffProblemConnector.scala @@ -0,0 +1,9 @@ +package net.shrine.problem + +/** + * Created by ty on 7/29/16. + * Turns off the problem database connector, so tests don't have to run a config file. + */ +trait TurnOffProblemConnector { + ProblemConfigSource.turnOffConnector = true +} diff --git a/commons/data-commons/src/main/scala/net/shrine/slick/CouldNotRunDbIoActionException.scala b/commons/data-commons/src/main/scala/net/shrine/slick/CouldNotRunDbIoActionException.scala new file mode 100644 index 000000000..5994d0c9d --- /dev/null +++ b/commons/data-commons/src/main/scala/net/shrine/slick/CouldNotRunDbIoActionException.scala @@ -0,0 +1,10 @@ +package net.shrine.slick + +import javax.sql.DataSource + +/** + * Created by ty on 7/22/16. + */ +case class CouldNotRunDbIoActionException(dataSource: DataSource, exception: Throwable) extends RuntimeException(exception) { + override def getMessage:String = s"Could not use the database defined by $dataSource due to ${exception.getLocalizedMessage}" +} diff --git a/commons/data-commons/src/main/scala/net/shrine/slick/TestableDataSourceCreator.scala b/commons/data-commons/src/main/scala/net/shrine/slick/TestableDataSourceCreator.scala index ac32919a3..82c786782 100644 --- a/commons/data-commons/src/main/scala/net/shrine/slick/TestableDataSourceCreator.scala +++ b/commons/data-commons/src/main/scala/net/shrine/slick/TestableDataSourceCreator.scala @@ -1,67 +1,78 @@ package net.shrine.slick import java.io.PrintWriter -import java.sql.{DriverManager, Connection} +import java.sql.{Connection, DriverManager} import java.util.logging.Logger -import javax.naming.InitialContext +import javax.naming.{Context, InitialContext} import javax.sql.DataSource import com.typesafe.config.Config import net.shrine.config.ConfigExtensions /** * @author david * @since 1/26/16 */ object TestableDataSourceCreator { def dataSource(config:Config):DataSource = { val dataSourceFrom = config.getString("dataSourceFrom") if(dataSourceFrom == "JNDI") { val jndiDataSourceName = config.getString("jndiDataSourceName") val initialContext:InitialContext = new InitialContext() - + // check to see what part blows up + val secondaryContext = initialContext.lookup("java:comp/env/").asInstanceOf[Context] + val printKeyValues = (a: java.util.Enumeration[_]) => while (a.hasMoreElements) { println(a.nextElement()) } + println("Seconday keys:") + printKeyValues(secondaryContext.getEnvironment.keys()) + println("Secondary values") + printKeyValues(secondaryContext.getEnvironment.elements()) + println("Primary keys:") + printKeyValues(initialContext.getEnvironment.keys()) + println("Primary values:") + printKeyValues(initialContext.getEnvironment.elements()) + println(initialContext.getEnvironment) initialContext.lookup(jndiDataSourceName).asInstanceOf[DataSource] } else if (dataSourceFrom == "testDataSource") { val testDataSourceConfig = config.getConfig("testDataSource") val driverClassName = testDataSourceConfig.getString("driverClassName") val url = testDataSourceConfig.getString("url") case class Credentials(username: String,password:String) def configToCredentials(config:Config) = new Credentials(config.getString("username"),config.getString("password")) val credentials: Option[Credentials] = testDataSourceConfig.getOptionConfigured("credentials",configToCredentials) //Creating an instance of the driver register it. (!) From a previous epoch, but it works. Class.forName(driverClassName).newInstance() object TestDataSource extends DataSource { override def getConnection: Connection = { credentials.fold(DriverManager.getConnection(url))(credentials => DriverManager.getConnection(url,credentials.username,credentials.password)) } override def getConnection(username: String, password: String): Connection = { DriverManager.getConnection(url, username, password) } //unused methods override def unwrap[T](iface: Class[T]): T = ??? override def isWrapperFor(iface: Class[_]): Boolean = ??? override def setLogWriter(out: PrintWriter): Unit = ??? override def getLoginTimeout: Int = ??? override def setLoginTimeout(seconds: Int): Unit = ??? override def getParentLogger: Logger = ??? override def getLogWriter: PrintWriter = ??? } TestDataSource } else throw new IllegalArgumentException(s"dataSourceFrom config value must be either JNDI or testDataSource, not $dataSourceFrom") } } diff --git a/commons/data-commons/src/test/resources/dashboard.conf b/commons/data-commons/src/test/resources/dashboard.conf new file mode 100644 index 000000000..a6c6913d3 --- /dev/null +++ b/commons/data-commons/src/test/resources/dashboard.conf @@ -0,0 +1,17 @@ +shrine{ + dashboard { + database { + dataSourceFrom = "testDataSource" + slickProfileClassName = "slick.driver.H2Driver$" + createTablesOnStart = true + // For testing without JNDI + testDataSource { + + //typical test settings for unit tests + driverClassName = "org.h2.Driver" + + url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" + } + } + } +} \ No newline at end of file diff --git a/commons/data-commons/src/test/scala/net/shrine/problem/DashboardProblemDatabaseTest.scala b/commons/data-commons/src/test/scala/net/shrine/problem/DashboardProblemDatabaseTest.scala new file mode 100644 index 000000000..7aee188d8 --- /dev/null +++ b/commons/data-commons/src/test/scala/net/shrine/problem/DashboardProblemDatabaseTest.scala @@ -0,0 +1,91 @@ +package net.shrine.problem + +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers} +import slick.driver.H2Driver.api._ + +import scala.concurrent.duration._ + +/** + * Test creation, insertion, querying, and deletion of ProblemDigest values into an + * in-memory H2 database. Demonstrates proof of concept mapping of ProblemDigest + * case class into a database. + */ +class DashboardProblemDatabaseTest extends FlatSpec with BeforeAndAfter with ScalaFutures with Matchers { + implicit val timeout = 10.seconds + + val connector = Problems.DatabaseConnector + val IO = connector.IO + val problemDigests = Seq( + ProblemDigest("MJPG", "01:01:01", "summary here", "description here" ,
uh not sure
, 2), + ProblemDigest("wewu", "01:02:01", "coffee spill", "coffee everywhere" ,
He chose decaf
, 1), + ProblemDigest("wuwu", "02:01:01", "squirrel" , "chewed all the cables",
Like ALL of them
, 0), + ProblemDigest("code", "10:01:02", "such summary", "such description" ,
Wow
, 3)) + + before { + connector.runBlocking(IO.dropIfExists >> IO.tableExists) shouldBe false + connector.runBlocking(IO.createIfNotExists >> IO.tableExists) shouldBe true + connector.runBlocking(IO.createIfNotExists) shouldBe NoOperation + connector.runBlocking(IO.selectAll) shouldBe empty + } + + after { + connector.runBlocking(IO.tableExists) shouldBe true + connector.runBlocking(IO.dropIfExists >> IO.tableExists) shouldBe false + connector.runBlocking(IO.dropIfExists) shouldBe NoOperation + } + + "The Database" should "Connect without any problems" in { + // Insert the suppliers and ProblemDigests + connector.executeTransactionBlocking(IO.problems ++= problemDigests) + + // Test that they are all in the table + var * = connector.runBlocking(IO.selectAll) + * should contain theSameElementsAs problemDigests + * should have length problemDigests.length + + // Reset the table + connector.runBlocking(IO.resetTable >> IO.selectAll) shouldBe empty + + // Run the test again + connector.executeTransactionBlocking(IO.problems += problemDigests.head, + IO.problems += problemDigests(1), + IO.problems += problemDigests(2), + IO.problems += problemDigests(3)) + // Test that they are all in the table + * = connector.runBlocking(IO.selectAll) + * should contain theSameElementsAs problemDigests + * should have length problemDigests.length + + + // Test that the simple select and filter queries work + val filtered = connector.runBlocking(IO.problems.filter(_.codec === "code").map(_.description).result) + filtered should have length 1 + filtered.head shouldBe problemDigests(3).description + + // This also tests that our conversion from xml to strings works + val xml = connector.runBlocking(IO.problems.map(_.xml).result) + xml should have length problemDigests.length + xml should contain theSameElementsAs problemDigests.map(_.detailsXml.toString()) + + val result = connector.runBlocking(IO.sizeAndProblemDigest(2)) + result._1 should have length 2 + result._2 shouldBe problemDigests.length + result._1.head shouldBe problemDigests(3) + result._1(1) shouldBe problemDigests.head + + val resultOverLength = connector.runBlocking(IO.sizeAndProblemDigest(10)) + resultOverLength._1 should have length 4 + resultOverLength._1 should contain theSameElementsAs problemDigests + + connector.runBlocking(IO.problems.size.result) shouldBe problemDigests.size + + val testProblem = ProblemDatabaseTestProblem(ProblemSources.Unknown) + connector.runBlocking(IO.problems.size.result) shouldBe problemDigests.size + 1 + } +} + +case class ProblemDatabaseTestProblem(source: ProblemSources.ProblemSource) extends AbstractProblem(source: ProblemSources.ProblemSource) { + override def summary: String = "This is a test problem! No user should ever see this." + override def description: String = "Wow, this is a nice looking problem. I mean really, just look at it." +} \ No newline at end of file diff --git a/commons/util/src/test/scala/net/shrine/problem/ProblemDigestTest.scala b/commons/data-commons/src/test/scala/net/shrine/problem/ProblemDigestTest.scala similarity index 88% rename from commons/util/src/test/scala/net/shrine/problem/ProblemDigestTest.scala rename to commons/data-commons/src/test/scala/net/shrine/problem/ProblemDigestTest.scala index e84ae7f3e..6ebc5860b 100644 --- a/commons/util/src/test/scala/net/shrine/problem/ProblemDigestTest.scala +++ b/commons/data-commons/src/test/scala/net/shrine/problem/ProblemDigestTest.scala @@ -1,20 +1,20 @@ package net.shrine.problem import net.shrine.util.ShouldMatchersForJUnit /** * * @author dwalend * @since 1.20 */ final class ProblemDigestTest extends ShouldMatchersForJUnit { def testRoundTrip() = { - val problemDigest = ProblemDigest(getClass.getName, "stampText", "Test problem", "A problem for testing",
"We use this problem for testing. Don't worry about it"
) + val problemDigest = ProblemDigest(getClass.getName, "stampText", "Test problem", "A problem for testing",
"We use this problem for testing. Don't worry about it"
, 0) val xml = problemDigest.toXml val fromXml = ProblemDigest.fromXml(xml) fromXml should be(problemDigest) } } \ No newline at end of file diff --git a/commons/util/src/test/scala/net/shrine/problem/ProblemHandlerTest.scala b/commons/data-commons/src/test/scala/net/shrine/problem/ProblemHandlerTest.scala similarity index 100% rename from commons/util/src/test/scala/net/shrine/problem/ProblemHandlerTest.scala rename to commons/data-commons/src/test/scala/net/shrine/problem/ProblemHandlerTest.scala diff --git a/commons/protocol/pom.xml b/commons/protocol/pom.xml index 59f72e914..9447f514c 100644 --- a/commons/protocol/pom.xml +++ b/commons/protocol/pom.xml @@ -1,87 +1,92 @@ 4.0.0 SHRINE Protocol shrine-protocol jar net.shrine shrine-base 1.22.0-SNAPSHOT ../../pom.xml net.shrine shrine-protocol-query ${project.version} net.shrine shrine-util ${project.version} net.shrine shrine-util ${project.version} test-jar test net.shrine shrine-config ${project.version} + + net.shrine + shrine-data-commons + ${project.version} + net.liftweb lift-json_${scala-major-version} org.easymock easymock test net.shrine shrine-test-commons ${project.version} test-jar test src/main/scala src/test/scala src/main/resources true shrine-versions.properties net.alchim31.maven scala-maven-plugin net.alchim31.maven scala-maven-plugin org.apache.maven.plugins maven-jar-plugin test-jar diff --git a/commons/protocol/src/main/scala/net/shrine/protocol/ErrorResponse.scala b/commons/protocol/src/main/scala/net/shrine/protocol/ErrorResponse.scala index 904ea7093..5e7359e09 100644 --- a/commons/protocol/src/main/scala/net/shrine/protocol/ErrorResponse.scala +++ b/commons/protocol/src/main/scala/net/shrine/protocol/ErrorResponse.scala @@ -1,116 +1,117 @@ package net.shrine.protocol import net.shrine.log.Loggable import net.shrine.problem.{LoggingProblemHandler, Problem, ProblemDigest} import scala.xml.{NodeBuffer, NodeSeq} import net.shrine.util.XmlUtil import net.shrine.serialization.XmlUnmarshaller import net.shrine.serialization.I2b2Unmarshaller import net.shrine.util.NodeSeqEnrichments + import scala.util.Try import scala.util.control.NonFatal /** * @author Bill Simons * @since 4/25/11 * @see http://cbmi.med.harvard.edu * @see http://chip.org *

* NOTICE: This software comes with NO guarantees whatsoever and is * licensed as Lgpl Open Source * @see http://www.gnu.org/licenses/lgpl.html * * NB: Now a case class for structural equality */ final case class ErrorResponse(errorMessage: String,problemDigest:ProblemDigest) extends ShrineResponse { override protected def status: NodeSeq = { val buffer = new NodeBuffer buffer += { errorMessage } buffer += problemDigest.toXml } override protected def i2b2MessageBody = null import ErrorResponse.rootTagName override def toXml = { XmlUtil.stripWhitespace { val xml = XmlUtil.renameRootTag(rootTagName) { { errorMessage } {problemDigest.toXml} } xml } } } object ErrorResponse extends XmlUnmarshaller[ErrorResponse] with I2b2Unmarshaller[ErrorResponse] with HasRootTagName with Loggable { val rootTagName = "errorResponse" def apply(problem:Problem) = { LoggingProblemHandler.handleProblem(problem) //todo someday hook up to the proper problem handler hierarchy. new ErrorResponse(problem.summary,problem.toDigest) } override def fromXml(xml: NodeSeq): ErrorResponse = { val messageXml = xml \ "message" //NB: Fail fast require(messageXml.nonEmpty) val problemDigest = ProblemDigest.fromXml(xml) ErrorResponse(XmlUtil.trim(messageXml),problemDigest) } override def fromI2b2(xml: NodeSeq): ErrorResponse = { import NodeSeqEnrichments.Strictness._ //todo what determines parseFormatA vs parseFormatB when written? It looks like our ErrorResponses use A. def parseFormatA: Try[ErrorResponse] = { for { statusXml <- xml withChild "response_header" withChild "result_status" withChild "status" resultStatusXml <- xml withChild "response_header" withChild "result_status" typeText <- statusXml attribute "type" if typeText == "ERROR" //NB: Fail fast{ statusMessage = XmlUtil.trim(statusXml) problemDigest = ProblemDigest.fromXml(resultStatusXml) } yield { ErrorResponse(statusMessage,problemDigest) } } def parseFormatB: Try[ErrorResponse] = { for { conditionXml <- xml withChild "message_body" withChild "response" withChild "status" withChild "condition" typeText <- conditionXml attribute "type" if typeText == "ERROR" statusMessage = XmlUtil.trim(conditionXml) problemDigest = ErrorStatusFromCrc(Option(statusMessage),xml.text).toDigest//here's another place where an ERROR can have no ProblemDigest } yield { ErrorResponse(statusMessage,problemDigest) } } parseFormatA.recoverWith { case NonFatal(e) => { warn(s"Encountered a problem while parsing an error from I2B2 with 'format A', trying 'format B' ${xml.text}",e) parseFormatB } }.get } /** * * * * * * Query result instance id 3126 not found * * * * */ } \ No newline at end of file diff --git a/commons/protocol/src/main/scala/net/shrine/protocol/NonI2b2ableResponse.scala b/commons/protocol/src/main/scala/net/shrine/protocol/NonI2b2ableResponse.scala index 86d350fcf..7fc8a38ff 100644 --- a/commons/protocol/src/main/scala/net/shrine/protocol/NonI2b2ableResponse.scala +++ b/commons/protocol/src/main/scala/net/shrine/protocol/NonI2b2ableResponse.scala @@ -1,22 +1,22 @@ package net.shrine.protocol -import net.shrine.problem.{ProblemSources, AbstractProblem} +import net.shrine.problem.{AbstractProblem, ProblemSources} import scala.xml.NodeSeq /** * @author clint * @since Apr 30, 2013 */ trait NonI2b2ableResponse { self: ShrineResponse => //Fail loudly here protected override def i2b2MessageBody: NodeSeq = ??? override def toI2b2: NodeSeq = ErrorResponse(NoI2b2AnalogExists(this.getClass)).toI2b2 } case class NoI2b2AnalogExists(claz:Class[_ <: NonI2b2ableResponse]) extends AbstractProblem(ProblemSources.Unknown) { override def summary: String = s"${ claz.getSimpleName } can't be marshalled to i2b2 XML, as it has no i2b2 analog" override def description: String = s"${ claz.getSimpleName } can't be marshalled to i2b2 XML, as it has no i2b2 analog" } \ No newline at end of file diff --git a/commons/protocol/src/main/scala/net/shrine/protocol/QueryResult.scala b/commons/protocol/src/main/scala/net/shrine/protocol/QueryResult.scala index 26b2e4572..be7f831fa 100644 --- a/commons/protocol/src/main/scala/net/shrine/protocol/QueryResult.scala +++ b/commons/protocol/src/main/scala/net/shrine/protocol/QueryResult.scala @@ -1,398 +1,402 @@ package net.shrine.protocol import javax.xml.datatype.XMLGregorianCalendar + import net.shrine.log.Loggable -import net.shrine.problem.{ProblemSources, AbstractProblem, Problem, ProblemDigest} +import net.shrine.problem.{AbstractProblem, Problem, ProblemDigest, ProblemSources} import net.shrine.protocol.QueryResult.StatusType import scala.xml.NodeSeq -import net.shrine.util.{Tries, XmlUtil, NodeSeqEnrichments, SEnum, XmlDateHelper, OptionEnrichments} -import net.shrine.serialization.{ I2b2Marshaller, XmlMarshaller } +import net.shrine.util.{NodeSeqEnrichments, OptionEnrichments, SEnum, Tries, XmlDateHelper, XmlUtil} +import net.shrine.serialization.{I2b2Marshaller, XmlMarshaller} + import scala.util.Try /** * @author Bill Simons * @since 4/15/11 * @see http://cbmi.med.harvard.edu * @see http://chip.org *

* NOTICE: This software comes with NO guarantees whatsoever and is * licensed as Lgpl Open Source * @see http://www.gnu.org/licenses/lgpl.html * * NB: this is a case class to get a structural equality contract in hashCode and equals, mostly for testing */ final case class QueryResult ( resultId: Long, instanceId: Long, resultType: Option[ResultOutputType], setSize: Long, startDate: Option[XMLGregorianCalendar], endDate: Option[XMLGregorianCalendar], description: Option[String], statusType: StatusType, statusMessage: Option[String], problemDigest: Option[ProblemDigest] = None, breakdowns: Map[ResultOutputType,I2b2ResultEnvelope] = Map.empty ) extends XmlMarshaller with I2b2Marshaller with Loggable { //only used in tests def this( resultId: Long, instanceId: Long, resultType: ResultOutputType, setSize: Long, startDate: XMLGregorianCalendar, endDate: XMLGregorianCalendar, statusType: QueryResult.StatusType) = { this( resultId, instanceId, Option(resultType), setSize, Option(startDate), Option(endDate), None, //description statusType, None) //statusMessage } def this( resultId: Long, instanceId: Long, resultType: ResultOutputType, setSize: Long, startDate: XMLGregorianCalendar, endDate: XMLGregorianCalendar, description: String, statusType: QueryResult.StatusType) = { this( resultId, instanceId, Option(resultType), setSize, Option(startDate), Option(endDate), Option(description), statusType, None) //statusMessage } def resultTypeIs(testedResultType: ResultOutputType): Boolean = resultType match { case Some(rt) => rt == testedResultType case _ => false } import QueryResult._ //NB: Fragile, non-type-safe == def isError = statusType == StatusType.Error def elapsed: Option[Long] = { def inMillis(xmlGc: XMLGregorianCalendar) = xmlGc.toGregorianCalendar.getTimeInMillis for { start <- startDate end <- endDate } yield inMillis(end) - inMillis(start) } //Sorting isn't strictly necessary, but makes deterministic unit testing easier. //The number of breakdowns will be at most 4, so performance should not be an issue. private def sortedBreakdowns: Seq[I2b2ResultEnvelope] = { breakdowns.values.toSeq.sortBy(_.resultType.name) } override def toI2b2: NodeSeq = { import OptionEnrichments._ XmlUtil.stripWhitespace { { resultId } { instanceId } { description.toXml() } { resultType.fold( ResultOutputType.ERROR.toI2b2NameOnly("") ){ rt => if(rt.isBreakdown) rt.toI2b2NameOnly() else if (rt.isError) rt.toI2b2NameOnly() //The result type can be an error else if (statusType.isError) rt.toI2b2NameOnly() //Or the status type can be an error else rt.toI2b2 } } { setSize } { startDate.toXml() } { endDate.toXml() } { statusType } { statusType.toI2b2(this) } { //NB: Deliberately use Shrine XML format instead of the i2b2 one. Adding breakdowns to i2b2-format XML here is deviating from the i2b2 XSD schema in any case, //so if we're going to do that, let's produce saner XML. sortedBreakdowns.map(_.toXml.head).map(XmlUtil.renameRootTag("breakdown_data")) } } } override def toXml: NodeSeq = XmlUtil.stripWhitespace { import OptionEnrichments._ { resultId } { instanceId } { resultType.toXml(_.toXml) } { setSize } { startDate.toXml() } { endDate.toXml() } { description.toXml() } { statusType } { statusMessage.toXml() } { //Sorting isn't strictly necessary, but makes deterministic unit testing easier. //The number of breakdowns will be at most 4, so performance should not be an issue. sortedBreakdowns.map(_.toXml) } { problemDigest.map(_.toXml).getOrElse("") } } def withId(id: Long): QueryResult = copy(resultId = id) def withInstanceId(id: Long): QueryResult = copy(instanceId = id) def modifySetSize(f: Long => Long): QueryResult = withSetSize(f(setSize)) def withSetSize(size: Long): QueryResult = copy(setSize = size) def withDescription(desc: String): QueryResult = copy(description = Option(desc)) def withResultType(resType: ResultOutputType): QueryResult = copy(resultType = Option(resType)) def withBreakdown(breakdownData: I2b2ResultEnvelope) = copy(breakdowns = breakdowns + (breakdownData.resultType -> breakdownData)) def withBreakdowns(newBreakdowns: Map[ResultOutputType, I2b2ResultEnvelope]) = copy(breakdowns = newBreakdowns) } object QueryResult { final case class StatusType( name: String, isDone: Boolean, i2b2Id: Option[Int] = Some(-1), private val doToI2b2:(QueryResult => NodeSeq) = StatusType.defaultToI2b2) extends StatusType.Value { def isError = this == StatusType.Error def toI2b2(queryResult: QueryResult): NodeSeq = doToI2b2(queryResult) } object StatusType extends SEnum[StatusType] { private val defaultToI2b2: QueryResult => NodeSeq = { queryResult => val i2b2Id: Int = queryResult.statusType.i2b2Id.getOrElse{ throw new IllegalStateException(s"queryResult.statusType ${queryResult.statusType} has no i2b2Id") } { i2b2Id }{ queryResult.statusType.name } } val noMessage:NodeSeq = null val Error = StatusType("ERROR", isDone = true, None, { queryResult => (queryResult.statusMessage, queryResult.problemDigest) match { case (Some(msg),Some(pd)) => { if(msg != "ERROR") msg else pd.summary } ++ pd.toXml case (Some(msg),None) => { msg } case (None,Some(pd)) => { pd.summary } ++ pd.toXml case (None, None) => noMessage } }) val Finished = StatusType("FINISHED", isDone = true, Some(3)) //TODO: Can we use the same for Queued, Processing, and Incomplete? val Processing = StatusType("PROCESSING", isDone = false, Some(2)) //todo only used in tests val Queued = StatusType("QUEUED", isDone = false, Some(2)) val Incomplete = StatusType("INCOMPLETE", isDone = false, Some(2)) //TODO: What s should these have? Does anyone care? val Held = StatusType("HELD", isDone = false) val SmallQueue = StatusType("SMALL_QUEUE", isDone = false) val MediumQueue = StatusType("MEDIUM_QUEUE", isDone = false) val LargeQueue = StatusType("LARGE_QUEUE", isDone = false) val NoMoreQueue = StatusType("NO_MORE_QUEUE", isDone = false) } def extractLong(nodeSeq: NodeSeq)(elemName: String): Long = (nodeSeq \ elemName).text.toLong private def parseDate(lexicalRep: String): Option[XMLGregorianCalendar] = XmlDateHelper.parseXmlTime(lexicalRep).toOption def elemAt(path: String*)(xml: NodeSeq): NodeSeq = path.foldLeft(xml)(_ \ _) def asText(path: String*)(xml: NodeSeq): String = elemAt(path: _*)(xml).text.trim def asResultOutputTypeOption(elemNames: String*)(breakdownTypes: Set[ResultOutputType], xml: NodeSeq): Option[ResultOutputType] = { import ResultOutputType.valueOf val typeName = asText(elemNames: _*)(xml) valueOf(typeName) orElse valueOf(breakdownTypes)(typeName) } def extractResultOutputType(xml: NodeSeq)(parse: NodeSeq => Try[ResultOutputType]): Option[ResultOutputType] = { val attempt = parse(xml) attempt.toOption } def extractProblemDigest(xml: NodeSeq):Option[ProblemDigest] = { val subXml = xml \ "problem" if(subXml.nonEmpty) Some(ProblemDigest.fromXml(xml)) else None } def fromXml(breakdownTypes: Set[ResultOutputType])(xml: NodeSeq): QueryResult = { def extract(elemName: String): Option[String] = { Option((xml \ elemName).text.trim).filter(!_.isEmpty) } def extractDate(elemName: String): Option[XMLGregorianCalendar] = extract(elemName).flatMap(parseDate) val asLong = extractLong(xml) _ import NodeSeqEnrichments.Strictness._ import Tries.sequence def extractBreakdowns(elemName: String): Map[ResultOutputType, I2b2ResultEnvelope] = { //noinspection ScalaUnnecessaryParentheses val mapAttempt = for { subXml <- xml.withChild(elemName) envelopes <- sequence(subXml.map(I2b2ResultEnvelope.fromXml(breakdownTypes))) mappings = envelopes.map(envelope => (envelope.resultType -> envelope)) } yield Map.empty ++ mappings mapAttempt.getOrElse(Map.empty) } QueryResult( resultId = asLong("resultId"), instanceId = asLong("instanceId"), resultType = extractResultOutputType(xml \ "resultType")(ResultOutputType.fromXml), setSize = asLong("setSize"), startDate = extractDate("startDate"), endDate = extractDate("endDate"), description = extract("description"), statusType = StatusType.valueOf(asText("status")(xml)).get, //TODO: Avoid fragile .get call statusMessage = extract("statusMessage"), problemDigest = extractProblemDigest(xml), breakdowns = extractBreakdowns("resultEnvelope") ) } def fromI2b2(breakdownTypes: Set[ResultOutputType])(xml: NodeSeq): QueryResult = { def asLong = extractLong(xml) _ def asTextOption(path: String*): Option[String] = elemAt(path: _*)(xml).headOption.map(_.text.trim) def asXmlGcOption(path: String): Option[XMLGregorianCalendar] = asTextOption(path).filter(!_.isEmpty).flatMap(parseDate) val statusType = StatusType.valueOf(asText("query_status_type", "name")(xml)).get //TODO: Avoid fragile .get call val statusMessage: Option[String] = asTextOption("query_status_type", "description") val encodedProblemDigest = extractProblemDigest(xml \ "query_status_type") val problemDigest = if (encodedProblemDigest.isDefined) encodedProblemDigest else if (statusType.isError) Some(ErrorStatusFromCrc(statusMessage,xml.text).toDigest) else None case class Filling( resultType:Option[ResultOutputType], setSize:Long, startDate:Option[XMLGregorianCalendar], endDate:Option[XMLGregorianCalendar] ) val filling = if(!statusType.isError) { val resultType: Option[ResultOutputType] = extractResultOutputType(xml \ "query_result_type")(ResultOutputType.fromI2b2) val setSize = asLong("set_size") val startDate = asXmlGcOption("start_date") val endDate = asXmlGcOption("end_date") Filling(resultType,setSize,startDate,endDate) } else { val resultType = None val setSize = 0L val startDate = None val endDate = None Filling(resultType,setSize,startDate,endDate) } QueryResult( resultId = asLong("result_instance_id"), instanceId = asLong("query_instance_id"), resultType = filling.resultType, setSize = filling.setSize, startDate = filling.startDate, endDate = filling.endDate, description = asTextOption("description"), statusType = statusType, statusMessage = statusMessage, problemDigest = problemDigest ) } def errorResult(description: Option[String], statusMessage: String,problemDigest:ProblemDigest):QueryResult = { QueryResult( resultId = 0L, instanceId = 0L, resultType = None, setSize = 0L, startDate = None, endDate = None, description = description, statusType = StatusType.Error, statusMessage = Option(statusMessage), problemDigest = Option(problemDigest)) } def errorResult(description: Option[String], statusMessage: String,problem:Problem):QueryResult = { val problemDigest = problem.toDigest QueryResult( resultId = 0L, instanceId = 0L, resultType = None, setSize = 0L, startDate = None, endDate = None, description = description, statusType = StatusType.Error, statusMessage = Option(statusMessage), problemDigest = Option(problemDigest)) } /** * For reconstituting errorResults from a database */ //todo remove and replace with real Problems def errorResult(description:Option[String], statusMessage:String, codec:String,stampText:String, summary:String, digestDescription:String,detailsXml:NodeSeq): QueryResult = { - - val problemDigest = ProblemDigest(codec,stampText,summary,digestDescription,detailsXml) + // This would require parsing the stamp text to change, and without a standard locale that's nigh impossible. + // If this is replaced with real problems, then this can be addressed then. For now, passing on zero is the best bet. + // TODO: REFACTOR SQUERYL ERRORS TO USE REAL PROBLEMS + val problemDigest = ProblemDigest(codec,stampText,summary,digestDescription,detailsXml,0) QueryResult( resultId = 0L, instanceId = 0L, resultType = None, setSize = 0L, startDate = None, endDate = None, description = description, statusType = StatusType.Error, statusMessage = Option(statusMessage), problemDigest = Option(problemDigest)) } } case class ErrorStatusFromCrc(messageFromCrC:Option[String], xmlResponseFromCrc: String) extends AbstractProblem(ProblemSources.Adapter) { - override val summary: String = "The I2B2 CRC reported an internal error." - override val description:String = s"The I2B2 CRC responded with status type ERROR ${messageFromCrC.fold(" but no message")(message => s"and a message of '$message'")}" - override val detailsXml =

+ override lazy val summary: String = "The I2B2 CRC reported an internal error." + override lazy val description:String = s"The I2B2 CRC responded with status type ERROR ${messageFromCrC.fold(" but no message")(message => s"and a message of '$message'")}" + override lazy val detailsXml =
CRC's Response is {xmlResponseFromCrc}
} diff --git a/commons/protocol/src/test/resources/problem.conf b/commons/protocol/src/test/resources/problem.conf new file mode 100644 index 000000000..40d9ade8f --- /dev/null +++ b/commons/protocol/src/test/resources/problem.conf @@ -0,0 +1,16 @@ +shrine{ + dashboard { + database { + dataSourceFrom = "testDataSource" + slickProfileClassName = "slick.driver.H2Driver$" + createTablesOnStart = true + // For testing without JNDI + testDataSource { + //typical test settings for unit tests + driverClassName = "org.h2.Driver" + + url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" + } + } + } +} \ No newline at end of file diff --git a/commons/protocol/src/test/scala/net/shrine/protocol/AggregatedRunQueryResponseTest.scala b/commons/protocol/src/test/scala/net/shrine/protocol/AggregatedRunQueryResponseTest.scala index f7d668c77..13f5fdfe3 100644 --- a/commons/protocol/src/test/scala/net/shrine/protocol/AggregatedRunQueryResponseTest.scala +++ b/commons/protocol/src/test/scala/net/shrine/protocol/AggregatedRunQueryResponseTest.scala @@ -1,318 +1,316 @@ package net.shrine.protocol -import net.shrine.problem.TestProblem +import net.shrine.problem.{TestProblem, TurnOffProblemConnector} import scala.xml.NodeSeq - import org.junit.Test - import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol.query.Term import net.shrine.util.XmlDateHelper import net.shrine.util.XmlUtil /** * * * @author Justin Quan * @see http://chip.org * @since 8/12/11 */ //noinspection EmptyParenMethodOverridenAsParameterless,EmptyParenMethodAccessedAsParameterless,UnitMethodIsParameterless -final class AggregatedRunQueryResponseTest extends ShrineResponseI2b2SerializableValidator { +final class AggregatedRunQueryResponseTest extends ShrineResponseI2b2SerializableValidator with TurnOffProblemConnector { private val queryId = 1L private val queryName = "queryName" private val userId = "user" private val groupId = "group" private val createDate = XmlDateHelper.now private val requestQueryDef = QueryDefinition(queryName, Term("""\\i2b2\i2b2\Demographics\Age\0-9 years old\""")) private val queryInstanceId = 2L private val resultId = 3L private val setSize = 10L private val startDate = createDate private val endDate = createDate private val resultId2 = 4L private val resultType1 = ResultOutputType.PATIENT_COUNT_XML private val resultType2 = ResultOutputType.PATIENT_COUNT_XML private val statusType = QueryResult.StatusType.Finished override def messageBody = { DONE { queryId } { queryName } { userId } { groupId } { createDate } { requestQueryDef.toI2b2 } { queryInstanceId } { queryId } { userId } { groupId } 6 COMPLETED COMPLETED { resultId } { queryInstanceId } 4 { resultType1 } CATNUM LA Number of patients { setSize } { startDate } { endDate } { statusType } 3 FINISHED { resultId2 } { queryInstanceId } 4 { resultType2 } CATNUM LA Number of patients { setSize } { startDate } { endDate } { statusType } 3 FINISHED } private val qr1 = QueryResult( resultId = resultId, instanceId = queryInstanceId, resultType = Option(resultType1), setSize = setSize, startDate = Option(createDate), endDate = Option(createDate), description = None, statusType = statusType, statusMessage = Some(statusType.name), problemDigest = None, breakdowns = Map.empty) private val qr2 = QueryResult( resultId = resultId2, instanceId = queryInstanceId, resultType = Option(resultType2), setSize = setSize, startDate = Option(createDate), endDate = Option(createDate), description = None, statusType = statusType, statusMessage = Some(statusType.name), problemDigest = None, breakdowns = Map.empty) private val runQueryResponse = XmlUtil.stripWhitespace { { queryId } { queryInstanceId } { userId } { groupId } { requestQueryDef.toXml } { createDate } { Seq(qr1, qr2).map(_.toXml) } } import DefaultBreakdownResultOutputTypes.{ values => breakdownTypes } @Test def testFromXml { val actual = AggregatedRunQueryResponse.fromXml(breakdownTypes.toSet)(runQueryResponse).get actual.queryId should equal(queryId) actual.createDate should equal(createDate) actual.userId should equal(userId) actual.groupId should equal(groupId) actual.requestXml should equal(requestQueryDef) actual.queryInstanceId should equal(queryInstanceId) actual.results should equal(Seq(qr1, qr2)) actual.queryName should equal(queryName) } @Test def testToXml { AggregatedRunQueryResponse(queryId, createDate, userId, groupId, requestQueryDef, queryInstanceId, Seq(qr1, qr2)).toXmlString should equal(runQueryResponse.toString) } @Test def testFromI2b2 { val translatedResponse = AggregatedRunQueryResponse.fromI2b2(breakdownTypes.toSet)(response).get translatedResponse.queryId should equal(queryId) translatedResponse.createDate should equal(createDate) translatedResponse.userId should equal(userId) translatedResponse.groupId should equal(groupId) translatedResponse.requestXml should equal(requestQueryDef) translatedResponse.queryInstanceId should equal(queryInstanceId) translatedResponse.results should equal(Seq(qr1, qr2)) translatedResponse.queryName should equal(queryName) } @Test def testFromI2b2StringRequestXml { val hackToProduceXml = new HasResponse { //Produces a message body where the tag contains escaped XML as a String, as is produced by the CRC override def messageBody: NodeSeq = { DONE { queryId } { queryName } { userId } { groupId } { createDate } { requestQueryDef.toI2b2String } { queryInstanceId } { queryId } { userId } { groupId } 6 COMPLETED COMPLETED { resultId } { queryInstanceId } { resultType1 } 1 LIST LA Patient list { setSize } { startDate } { endDate } { statusType } 3 FINISHED { resultId2 } { queryInstanceId } { resultType2 } 4 CATNUM LA Number of patients { setSize } { startDate } { endDate } { statusType } 3 FINISHED } } doTestFromI2b2(hackToProduceXml.response, requestQueryDef) } private def doTestFromI2b2(i2b2Response: NodeSeq, expectedQueryDef: AnyRef) { val translatedResponse = AggregatedRunQueryResponse.fromI2b2(breakdownTypes.toSet)(i2b2Response).get translatedResponse.queryId should equal(queryId) translatedResponse.createDate should equal(createDate) translatedResponse.userId should equal(userId) translatedResponse.groupId should equal(groupId) translatedResponse.requestXml should equal(expectedQueryDef) translatedResponse.queryInstanceId should equal(queryInstanceId) translatedResponse.results should equal(Seq(qr1, qr2)) translatedResponse.queryName should equal(queryName) } @Test def testResultsPartitioned { val actual = AggregatedRunQueryResponse.fromXml(breakdownTypes.toSet)(runQueryResponse).get { val (nonErrors, errors) = actual.resultsPartitioned nonErrors should equal(Seq(qr1, qr2)) errors.size should equal(0) } val error = QueryResult.errorResult(None, "something broke", TestProblem()) { val withOnlyErrors = actual.withResults(Seq(error, error)) val (nonErrors, errors) = withOnlyErrors.resultsPartitioned nonErrors.size should equal(0) errors should equal(Seq(error, error)) } { val withErrorsAndSuccesses = actual.withResults(actual.results ++ Seq(error, error)) val (nonErrors, errors) = withErrorsAndSuccesses.resultsPartitioned nonErrors should equal(Seq(qr1, qr2)) errors should equal(Seq(error, error)) } { val withNoResults = actual.withResults(Nil) val (nonErrors, errors) = withNoResults.resultsPartitioned nonErrors.size should equal(0) errors.size should equal(0) } } @Test def testToI2b2 { AggregatedRunQueryResponse(queryId, createDate, userId, groupId, requestQueryDef, queryInstanceId, Seq(qr1, qr2)).toI2b2String should equal(response.toString) } } \ No newline at end of file diff --git a/commons/protocol/src/test/scala/net/shrine/protocol/ErrorResponseTest.scala b/commons/protocol/src/test/scala/net/shrine/protocol/ErrorResponseTest.scala index f8065600d..18bd5a631 100644 --- a/commons/protocol/src/test/scala/net/shrine/protocol/ErrorResponseTest.scala +++ b/commons/protocol/src/test/scala/net/shrine/protocol/ErrorResponseTest.scala @@ -1,158 +1,159 @@ package net.shrine.protocol import junit.framework.TestCase -import net.shrine.problem.TestProblem +import net.shrine.problem.{TestProblem, TurnOffProblemConnector} import net.shrine.util.ShouldMatchersForJUnit import org.junit.Test + import scala.xml.NodeSeq import net.shrine.util.XmlUtil /** * @author clint * @since Apr 5, 2013 */ -final class ErrorResponseTest extends TestCase with ShouldMatchersForJUnit { +final class ErrorResponseTest extends TestCase with ShouldMatchersForJUnit with TurnOffProblemConnector { val message = "foo" val problem = TestProblem(message) val resp = ErrorResponse(problem) val expectedShrineXml = XmlUtil.stripWhitespace { { message } {problem.toDigest.toXml} } val expectedI2b2Xml = XmlUtil.stripWhitespace { 1.1 2.4 SHRINE 1.3-compatible SHRINE 2011-04-08T16:21:12.251-04:00 { message } {problem.toDigest.toXml} } @Test def testToXml() = doTestToXml(expectedShrineXml, _.toXml) @Test def testToI2b2() = doTestToXml(expectedI2b2Xml, _.toI2b2) @Test def testToXmlRoundTrip() = doTestRoundTrip(_.toXml, ErrorResponse.fromXml) @Test def testToI2b2RoundTrip() = doTestRoundTrip(_.toI2b2, ErrorResponse.fromI2b2) @Test def testFromXml() = doTestFromXml(expectedShrineXml, ErrorResponse.fromXml) @Test def testFromI2b2() = doTestFromXml(expectedI2b2Xml, ErrorResponse.fromI2b2) //NB: See https://open.med.harvard.edu/jira/browse/SHRINE-745 @Test def testFromI2b2AlternateFormat() { val altI2b2Xml = XmlUtil.stripWhitespace { 1.1 2.4 edu.harvard.i2b2.crc 1.5 i2b2 Hive i2b2_QueryTool 0.2 i2b2 Hive 1 i2b2 Log information DONE Query result instance id 3126 not found } val resp = ErrorResponse.fromI2b2(altI2b2Xml) resp should not be null resp.errorMessage should equal("Query result instance id 3126 not found") } private def doTestFromXml(xml: NodeSeq, deserialize: NodeSeq => ErrorResponse) { intercept[Exception] { deserialize(null) } intercept[Exception] { deserialize() } intercept[Exception] { //Correct I2b2 XML structure, wrong status type deserialize({ message }) } val deserialized = deserialize(xml) deserialized.problemDigest.detailsXml should equal(resp.problemDigest.detailsXml) deserialized.problemDigest should equal(resp.problemDigest) deserialized should equal(resp) } private def doTestToXml(expected: NodeSeq, serialize: ErrorResponse => NodeSeq) { val xml = serialize(resp) xml.toString should equal(expected.toString()) } private def doTestRoundTrip(serialize: ErrorResponse => NodeSeq, deserialize: NodeSeq => ErrorResponse) { val unmarshalled = deserialize(serialize(resp)) unmarshalled should equal(resp) } } \ No newline at end of file diff --git a/commons/protocol/src/test/scala/net/shrine/protocol/NonI2b2ableResponseTest.scala b/commons/protocol/src/test/scala/net/shrine/protocol/NonI2b2ableResponseTest.scala index 58c168184..af55a1b99 100644 --- a/commons/protocol/src/test/scala/net/shrine/protocol/NonI2b2ableResponseTest.scala +++ b/commons/protocol/src/test/scala/net/shrine/protocol/NonI2b2ableResponseTest.scala @@ -1,31 +1,32 @@ package net.shrine.protocol import junit.framework.TestCase +import net.shrine.problem.TurnOffProblemConnector import net.shrine.util.ShouldMatchersForJUnit import org.junit.Test /** * @author clint * @date Apr 30, 2013 */ -final class NonI2b2ableResponseTest extends TestCase with ShouldMatchersForJUnit { +final class NonI2b2ableResponseTest extends TestCase with ShouldMatchersForJUnit with TurnOffProblemConnector { object TestResponse extends ShrineResponse with NonI2b2ableResponse { def messageBody = i2b2MessageBody override def toXml = ??? } @Test def testI2b2MessageBody { intercept[Error] { TestResponse.messageBody } } @Test def testToI2b2 { val testResponseClassName = TestResponse.getClass.getSimpleName ErrorResponse.fromI2b2(TestResponse.toI2b2).errorMessage.contains(testResponseClassName) should be(true) } } \ No newline at end of file diff --git a/commons/protocol/src/test/scala/net/shrine/protocol/QueryResultTest.scala b/commons/protocol/src/test/scala/net/shrine/protocol/QueryResultTest.scala index 9d84499cd..1414e6cdc 100644 --- a/commons/protocol/src/test/scala/net/shrine/protocol/QueryResultTest.scala +++ b/commons/protocol/src/test/scala/net/shrine/protocol/QueryResultTest.scala @@ -1,496 +1,499 @@ package net.shrine.protocol -import net.shrine.problem.{TestProblem, ProblemSources, AbstractProblem} +import net.shrine.problem.{TestProblem, TurnOffProblemConnector} import net.shrine.util.ShouldMatchersForJUnit import org.junit.Test import net.shrine.util.XmlUtil import net.shrine.util.XmlDateHelper import net.shrine.util.XmlGcEnrichments import scala.xml.NodeSeq /** * @author Bill Simons * @author clint * @since 8/19/11 * @see http://cbmi.med.harvard.edu * @see http://chip.org *

* NOTICE: This software comes with NO guarantees whatsoever and is * licensed as Lgpl Open Source * @see http://www.gnu.org/licenses/lgpl.html */ //noinspection EmptyParenMethodAccessedAsParameterless,NameBooleanParameters -final class QueryResultTest extends ShouldMatchersForJUnit with XmlRoundTripper[QueryResult] with I2b2SerializableValidator { +final class QueryResultTest extends ShouldMatchersForJUnit with XmlRoundTripper[QueryResult] + with I2b2SerializableValidator with TurnOffProblemConnector +{ private val date = XmlDateHelper.now private val resultId = 1L private val instanceId = 2L private val resultType = ResultOutputType.PATIENT_COUNT_XML private val setSize = 12L private val statusType = QueryResult.StatusType.Finished private val description = "description" private val statusMessage = "lakjdalsjd" private val queryResult = QueryResult(resultId, instanceId, Some(resultType), setSize, Option(date), Option(date), Option(description), statusType, Option(statusType.name)) import DefaultBreakdownResultOutputTypes.{ values => breakdownTypes, _ } private val resultWithBreakDowns = queryResult.copy( statusMessage = Some(statusMessage), breakdowns = Map(PATIENT_AGE_COUNT_XML -> I2b2ResultEnvelope(PATIENT_AGE_COUNT_XML, Map("foo" -> 1L, "bar" -> 2L)), PATIENT_RACE_COUNT_XML -> I2b2ResultEnvelope(PATIENT_RACE_COUNT_XML, Map("nuh" -> 3L, "zuh" -> 4L)), PATIENT_VITALSTATUS_COUNT_XML -> I2b2ResultEnvelope(PATIENT_VITALSTATUS_COUNT_XML, Map("blarg" -> 5L, "glarg" -> 6L)), PATIENT_GENDER_COUNT_XML -> I2b2ResultEnvelope(PATIENT_GENDER_COUNT_XML, Map("huh" -> 7L, "yeah" -> 8)))) private val expectedWhenBreakdownsArePresent = XmlUtil.stripWhitespace { { resultId } { instanceId } { resultType.toXml } { setSize } { date } { date } { description } { statusType } { statusMessage } { PATIENT_AGE_COUNT_XML } bar 2 foo 1 { PATIENT_GENDER_COUNT_XML } huh 7 yeah 8 { PATIENT_RACE_COUNT_XML } nuh 3 zuh 4 { PATIENT_VITALSTATUS_COUNT_XML } blarg 5 glarg 6 }.toString private val expectedI2b2Xml = XmlUtil.stripWhitespace { { resultId } { instanceId } { description } 4 { resultType } CATNUMLANumber of patients { setSize } { date } { date } { statusType } 3FINISHED }.toString private val expectedI2b2XmlWithBreakdowns = XmlUtil.stripWhitespace { { resultId } { instanceId } { description } { resultType.toI2b2 } { setSize } { date } { date } { statusType } 3FINISHED { PATIENT_AGE_COUNT_XML } bar 2 foo 1 { PATIENT_GENDER_COUNT_XML } huh 7 yeah 8 { PATIENT_RACE_COUNT_XML } nuh 3 zuh 4 { PATIENT_VITALSTATUS_COUNT_XML } blarg 5 glarg 6 }.toString private val expectedI2b2ErrorXml = XmlUtil.stripWhitespace { 0 0 { description } 0 ERROR { statusMessage } }.toString //NB: See https://open.med.harvard.edu/jira/browse/SHRINE-745 private val expectedI2b2IncompleteXml = XmlUtil.stripWhitespace { 0 0 { description } 0 INCOMPLETE { statusMessage } }.toString import scala.xml.XML.loadString //NB: See https://open.med.harvard.edu/jira/browse/SHRINE-745 @Test def testParseIncomplete() { val qr = QueryResult.fromI2b2(breakdownTypes.toSet)(loadString(expectedI2b2IncompleteXml)) qr.statusType should be(QueryResult.StatusType.Incomplete) } @Test def testElapsed() { queryResult.copy(startDate = None).elapsed should be(None) queryResult.copy(endDate = None).elapsed should be(None) queryResult.copy(startDate = None, endDate = None).elapsed should be(None) { val now = XmlDateHelper.now queryResult.copy(startDate = Some(now), endDate = Some(now)).elapsed should equal(Some(0L)) } { val start = XmlDateHelper.now val delta = 123L import XmlGcEnrichments._ import scala.concurrent.duration._ val end = start + delta.milliseconds queryResult.copy(startDate = Some(start), endDate = Some(end)).elapsed should equal(Some(delta)) } } @Test def testIsError() { queryResult.isError should be(false) queryResult.copy(statusType = QueryResult.StatusType.Processing).isError should be(false) queryResult.copy(statusType = QueryResult.StatusType.Finished).isError should be(false) queryResult.copy(statusType = QueryResult.StatusType.Queued).isError should be(false) queryResult.copy(statusType = QueryResult.StatusType.Incomplete).isError should be(false) queryResult.copy(statusType = QueryResult.StatusType.Error).isError should be(true) } @Test def testToXml() { val queryResultForShrine = queryResult.copy(statusMessage = Some(statusMessage)) val expectedWhenNoBreakdowns = XmlUtil.stripWhitespace { { resultId } { instanceId } { resultType.toXml } { setSize } { date } { date } { description } { statusType } { statusMessage } }.toString queryResultForShrine.copy(statusMessage = Some(statusMessage)).toXmlString should equal(expectedWhenNoBreakdowns) val expectedWhenNoStartDate = XmlUtil.stripWhitespace { { resultId } { instanceId } { resultType.toXml } { setSize } { date } { description } { statusType } { statusMessage } }.toString queryResultForShrine.copy(startDate = None).toXmlString should equal(expectedWhenNoStartDate) val expectedWhenNoEndDate = XmlUtil.stripWhitespace { { resultId } { instanceId } { resultType.toXml } { setSize } { date } { description } { statusType } { statusMessage } }.toString queryResultForShrine.copy(endDate = None).toXmlString should equal(expectedWhenNoEndDate) val expectedWhenNoDescription = XmlUtil.stripWhitespace { { resultId } { instanceId } { resultType.toXml } { setSize } { date } { date } { statusType } { statusMessage } }.toString queryResultForShrine.copy(description = None).toXmlString should equal(expectedWhenNoDescription) val expectedWhenNoStatusMessage = XmlUtil.stripWhitespace { { resultId } { instanceId } { resultType.toXml } { setSize } { date } { date } { description } { statusType } }.toString queryResult.copy(statusMessage = None).toXmlString should equal(expectedWhenNoStatusMessage) resultWithBreakDowns.toXmlString should equal(expectedWhenBreakdownsArePresent) } @Test def testFromXml() { QueryResult.fromXml(breakdownTypes.toSet)(loadString(expectedWhenBreakdownsArePresent)) should equal(resultWithBreakDowns) } @Test def testShrineRoundTrip() = { QueryResult.fromXml(breakdownTypes.toSet)(resultWithBreakDowns.toXml) should equal(resultWithBreakDowns) } private def compareIgnoringBreakdowns(actual: QueryResult, expected: QueryResult) { //Ignore breakdowns field, since this can't be serialized to i2b2 format as part of a actual.breakdowns should equal(Map.empty) actual.description should equal(expected.description) actual.endDate should equal(expected.endDate) actual.instanceId should equal(expected.instanceId) actual.resultId should equal(expected.resultId) actual.resultType should equal(expected.resultType) actual.setSize should equal(expected.setSize) actual.startDate should equal(expected.startDate) actual.statusMessage should equal(expected.statusMessage) actual.statusType should equal(expected.statusType) } @Test def testI2b2RoundTrip() = { //NB: Needed because i2b2 handles status messages differently. In the error case, statusMessage is //descriptive; otherwise, it's the all-caps name of the status type. This is different from how //Shrine creates and parses statusMessage XML, so we need a new QueryResult here. (Previously, we //could use the same one, since we were ignoring statusMessage and description when unmarshalling //from i2b2 format.) val newStatusMessage = Some(resultWithBreakDowns.statusType.name) val resultWithBreakDownsForI2b2 = resultWithBreakDowns.copy(statusMessage = newStatusMessage) val unmarshalled = QueryResult.fromI2b2(breakdownTypes.toSet)(resultWithBreakDownsForI2b2.toI2b2) compareIgnoringBreakdowns(unmarshalled, resultWithBreakDownsForI2b2) } @Test def testFromI2b2() { compareIgnoringBreakdowns(QueryResult.fromI2b2(breakdownTypes.toSet)(loadString(expectedI2b2Xml)), queryResult) } @Test def testFromI2b2WithErrors() { val errorResult = QueryResult.errorResult(Some(description), statusMessage,TestProblem()) val actual = QueryResult.fromI2b2(breakdownTypes.toSet)(loadString(expectedI2b2ErrorXml)) compareIgnoringBreakdowns(actual, errorResult) } @Test def testToI2b2() { queryResult.toI2b2String should equal(expectedI2b2Xml) } @Test def testToI2b2WithBreakdowns() { resultWithBreakDowns.toI2b2String should equal(expectedI2b2XmlWithBreakdowns) } @Test def testToI2b2AllStatusTypes(): Unit = { def doTest(statusType: QueryResult.StatusType) { val expectedI2b2Xml = XmlUtil.stripWhitespace { { resultId } { instanceId } { description } { resultType.toI2b2 } { setSize } { date } { date } { statusType } { statusType.i2b2Id.get }{ statusType } }.toString val result = queryResult.copy(statusType = statusType) result.toI2b2String should equal(expectedI2b2Xml) } import QueryResult.StatusType //NB: Error is tested by testToI2b2WithErrors() val nonErrorStatuses = StatusType.values.toSet - StatusType.Error for (statusType <- nonErrorStatuses) { doTest(statusType) } } @Test def testToI2b2WithErrors(): Unit = { QueryResult.errorResult(Some(description), statusMessage, TestProblem()).toI2b2String } @Test def testWithErrorsAndProblemDigest():Unit = { val testProblem:TestProblem = new TestProblem() val expected: NodeSeq = 0 0 description 0 ERROR lakjdalsjd {testProblem.problemName} {testProblem.stamp.pretty}

{testProblem.summary} {testProblem.description} + {testProblem.stamp.time} {testProblem.detailsXml} val actual = QueryResult.errorResult( Some(description), statusMessage, testProblem) val i2b2Xml: NodeSeq = actual.toI2b2 val pretty = new scala.xml.PrettyPrinter(80, 2) pretty.formatNodes(i2b2Xml) should equal(pretty.formatNodes(expected)) val fromI2b2 = QueryResult.fromI2b2(Set.empty)(i2b2Xml) fromI2b2 should equal(actual) val xml = actual.toXml val fromXml = QueryResult.fromXml(Set.empty)(xml) fromXml should equal(actual) } } \ No newline at end of file diff --git a/commons/protocol/src/test/scala/net/shrine/protocol/ReadPreviousQueriesResponseTest.scala b/commons/protocol/src/test/scala/net/shrine/protocol/ReadPreviousQueriesResponseTest.scala index 44d14217e..6168e10fa 100644 --- a/commons/protocol/src/test/scala/net/shrine/protocol/ReadPreviousQueriesResponseTest.scala +++ b/commons/protocol/src/test/scala/net/shrine/protocol/ReadPreviousQueriesResponseTest.scala @@ -1,178 +1,181 @@ package net.shrine.protocol -import net.shrine.problem.TestProblem import org.junit.Test import net.shrine.util.XmlUtil import net.shrine.util.XmlDateHelper import javax.xml.datatype.XMLGregorianCalendar +import net.shrine.problem.{TestProblem, TurnOffProblemConnector} + /** * @author Bill Simons * @since 4/12/11 * @see http://cbmi.med.harvard.edu * @see http://chip.org *

* NOTICE: This software comes with NO guarantees whatsoever and is * licensed as Lgpl Open Source * @see http://www.gnu.org/licenses/lgpl.html */ -final class ReadPreviousQueriesResponseTest extends ShrineResponseI2b2SerializableValidator { +final class ReadPreviousQueriesResponseTest extends ShrineResponseI2b2SerializableValidator + with TurnOffProblemConnector +{ val queryMasterId1 = 1111111L val queryMasterId2 = 222222L val networkQueryId1 = 123455667L val networkQueryId2 = 98327592L val queryName1 = "name1" val queryName2 = "name2" val userId1 = Some("user1") val userId2 = Some("user2") val groupId1 = Some("group1") val groupId2 = Some("group2") val createDate1 = XmlDateHelper.now val createDate2 = XmlDateHelper.now val flagged1 = Some(true) val flagged2 = None val flagMessage1 = Some("askldhlaksdjlkasdjklasdjl") val flagMessage2 = None val queryMaster1 = makeQueryMaster(queryMasterId1, networkQueryId1, queryName1, userId1, groupId1, createDate1, flagged1, flagMessage1) val queryMaster2 = makeQueryMaster(queryMasterId2, networkQueryId2, queryName2, userId2, groupId2, createDate2, flagged2, flagMessage2) def makeQueryMaster(queryMasterId: Long, networkQueryId: Long, queryName: String, userId: Option[String], groupId: Option[String], createDate: XMLGregorianCalendar, flagged: Option[Boolean], flagMessage: Option[String]) = { QueryMaster(String.valueOf(queryMasterId), networkQueryId, queryName, userId.get, groupId.get, createDate, flagged, flagMessage) } def messageBody = XmlUtil.stripWhitespace { DONE { queryMasterId1 } { networkQueryId1 } { queryName1 } { userId1.get } { groupId1.get } { createDate1 } { flagged1.get } { flagMessage1.get } { queryMasterId2 } { networkQueryId2 } { queryName2 } { userId2.get } { groupId2.get } { createDate2 } } //keep the held field around to be sure that old messages can be read. def oldMessageBody = XmlUtil.stripWhitespace { DONE { queryMasterId1 } { networkQueryId1 } { queryName1 } { userId1.get } { groupId1.get } { createDate1 } { flagged1.get } { flagMessage1.get } { queryMasterId2 } { networkQueryId2 } { queryName2 } { userId2.get } { groupId2.get } { createDate2 } false } val readPreviousQueriesResponse = XmlUtil.stripWhitespace { { queryMasterId1 } { networkQueryId1 } { queryName1 } { createDate1 } { userId1.get } { groupId1.get } { flagged1.get } { flagMessage1.get } { queryMasterId2 } { networkQueryId2 } { queryName2 } { createDate2 } { userId2.get } { groupId2.get } } @Test def testFromI2b2FailsFast() { intercept[Exception] { ReadPreviousQueriesResponse.fromI2b2() } intercept[Exception] { ReadPreviousQueriesResponse.fromI2b2(ErrorResponse(TestProblem("foo!")).toI2b2) } } @Test def testFromXml() { val actual = ReadPreviousQueriesResponse.fromXml(readPreviousQueriesResponse) val expectedQueryMasters = Set(queryMaster1, queryMaster2) actual.queryMasters.toSet should equal(expectedQueryMasters) } @Test def testToXml() { //we compare the string versions of the xml because Scala's xml equality does not always behave properly ReadPreviousQueriesResponse(Seq(queryMaster1, queryMaster2)).toXmlString should equal(readPreviousQueriesResponse.toString) } @Test def testFromI2b2() { val translatedResponse = ReadPreviousQueriesResponse.fromI2b2(response) translatedResponse.queryMasters.toSet should equal(Set(queryMaster1, queryMaster2)) } @Test def testToI2b2() { //Per-queryMaster userids and groupids //we compare the string versions of the xml because Scala's xml equality does not always behave properly val actual = ReadPreviousQueriesResponse(Seq(queryMaster1, queryMaster2)).toI2b2String val expected = response.toString actual should equal(expected) } } \ No newline at end of file diff --git a/commons/protocol/src/test/scala/net/shrine/protocol/RunQueryResponseTest.scala b/commons/protocol/src/test/scala/net/shrine/protocol/RunQueryResponseTest.scala index 30a2c1cdc..07f287807 100644 --- a/commons/protocol/src/test/scala/net/shrine/protocol/RunQueryResponseTest.scala +++ b/commons/protocol/src/test/scala/net/shrine/protocol/RunQueryResponseTest.scala @@ -1,254 +1,252 @@ package net.shrine.protocol -import net.shrine.problem.ProblemDigest +import net.shrine.problem.{ProblemDigest, TurnOffProblemConnector} import scala.xml.NodeSeq - import org.junit.Test - import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol.query.Term import net.shrine.util.XmlDateHelper import net.shrine.util.XmlUtil /** * * * @author Justin Quan * @see http://chip.org * Date: 8/12/11 */ //noinspection EmptyParenMethodOverridenAsParameterless,EmptyParenMethodAccessedAsParameterless,UnitMethodIsParameterless -final class RunQueryResponseTest extends ShrineResponseI2b2SerializableValidator { +final class RunQueryResponseTest extends ShrineResponseI2b2SerializableValidator with TurnOffProblemConnector { private val queryId = 1L private val queryName = "queryName" private val userId = "user" private val groupId = "group" private val createDate = XmlDateHelper.now private val requestQueryDef = QueryDefinition(queryName, Term("""\\i2b2\i2b2\Demographics\Age\0-9 years old\""")) private val queryInstanceId = 2L private val resultId = 3L private val setSize = 10L private val startDate = createDate private val endDate = createDate private val resultId2 = 4L private val resultType1 = ResultOutputType.PATIENT_COUNT_XML private val resultType2 = ResultOutputType.PATIENT_COUNT_XML private val statusType = QueryResult.StatusType.Finished override def messageBody: NodeSeq = { DONE { queryId } { queryName } { userId } { groupId } { createDate } { requestQueryDef.toI2b2 } { queryInstanceId } { queryId } { userId } { groupId } 6 COMPLETED COMPLETED { resultId } { queryInstanceId } 4 { resultType1 } CATNUM LA Number of patients { setSize } { startDate } { endDate } { statusType } 3 FINISHED } private val qr1 = QueryResult( resultId = resultId, instanceId = queryInstanceId, resultType = Option(resultType1), setSize = setSize, startDate = Option(createDate), endDate = Option(createDate), description = None, statusType = statusType, statusMessage = Some(statusType.name), problemDigest = None ) private val runQueryResponse = XmlUtil.stripWhitespace { { queryId } { queryInstanceId } { userId } { groupId } { requestQueryDef.toXml } { createDate } { qr1.toXml } } import DefaultBreakdownResultOutputTypes.{ values => breakdownTypes } @Test def testFromXml: Unit = { val actual = RunQueryResponse.fromXml(breakdownTypes.toSet)(runQueryResponse).get actual.queryId should equal(queryId) actual.createDate should equal(createDate) actual.userId should equal(userId) actual.groupId should equal(groupId) actual.requestXml should equal(requestQueryDef) actual.queryInstanceId should equal(queryInstanceId) actual.results should equal(Seq(qr1)) actual.queryName should equal(queryName) } @Test def testToXml: Unit = { RunQueryResponse(queryId, createDate, userId, groupId, requestQueryDef, queryInstanceId, qr1).toXmlString should equal(runQueryResponse.toString) } @Test def testFromI2b2: Unit = { val translatedResponse = RunQueryResponse.fromI2b2(breakdownTypes.toSet)(response).get translatedResponse.queryId should equal(queryId) translatedResponse.createDate should equal(createDate) translatedResponse.userId should equal(userId) translatedResponse.groupId should equal(groupId) translatedResponse.requestXml should equal(requestQueryDef) translatedResponse.queryInstanceId should equal(queryInstanceId) translatedResponse.results should equal(Seq(qr1)) translatedResponse.queryName should equal(queryName) } @Test def testFromI2b2StringRequestXml: Unit = { def hackToProduceXml(statusType: QueryResult.StatusType): HasResponse = new HasResponse { //Produces a message body where the tag contains escaped XML as a String, as is produced by the CRC override def messageBody: NodeSeq = { DONE { queryId } { queryName } { userId } { groupId } { createDate } { requestQueryDef.toI2b2String } { queryInstanceId } { queryId } { userId } { groupId } 6 COMPLETED COMPLETED { resultId } { queryInstanceId } { resultType1 } 1 LIST LA Timeline { setSize } { startDate } { endDate } { statusType } 3 FINISHED { resultId2 } { queryInstanceId } { resultType2 } 4 CATNUM LA Number of patients { setSize } { startDate } { endDate } { statusType } 3 FINISHED } } for { statusType <- QueryResult.StatusType.values } { doTestFromI2b2(hackToProduceXml(statusType).response, requestQueryDef, statusType) } } private def doTestFromI2b2(i2b2Response: NodeSeq, expectedQueryDef: AnyRef, expectedStatusType: QueryResult.StatusType, expectedProblemDigest:Option[ProblemDigest] = None) { val translatedResponse = RunQueryResponse.fromI2b2(breakdownTypes.toSet)(i2b2Response).get translatedResponse.queryId should equal(queryId) translatedResponse.createDate should equal(createDate) translatedResponse.userId should equal(userId) translatedResponse.groupId should equal(groupId) translatedResponse.requestXml should equal(expectedQueryDef) translatedResponse.queryInstanceId should equal(queryInstanceId) if(!expectedStatusType.isError) translatedResponse.results should equal(Seq(qr1.copy(statusType = expectedStatusType,problemDigest = expectedProblemDigest))) else { translatedResponse.results.size should equal(1) val result: QueryResult = translatedResponse.results.head result.copy(problemDigest = None) should equal(qr1.copy(statusType = expectedStatusType,resultType = None,setSize = 0,startDate = None,endDate = None)) result.problemDigest.get.codec should equal(classOf[ErrorStatusFromCrc].getName) } translatedResponse.queryName should equal(queryName) translatedResponse.singleNodeResult.statusType should be(expectedStatusType) } @Test def testToI2b2 { RunQueryResponse(queryId, createDate, userId, groupId, requestQueryDef, queryInstanceId, qr1).toI2b2String should equal(response.toString) } } \ No newline at end of file diff --git a/commons/protocol/src/test/scala/net/shrine/protocol/ShrineMessageTest.scala b/commons/protocol/src/test/scala/net/shrine/protocol/ShrineMessageTest.scala index 343acf579..8da07e917 100644 --- a/commons/protocol/src/test/scala/net/shrine/protocol/ShrineMessageTest.scala +++ b/commons/protocol/src/test/scala/net/shrine/protocol/ShrineMessageTest.scala @@ -1,166 +1,168 @@ package net.shrine.protocol -import net.shrine.problem.TestProblem import net.shrine.util.ShouldMatchersForJUnit import org.junit.Test + import scala.xml.NodeSeq import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol.query.Term import net.shrine.util.XmlDateHelper import java.math.BigInteger +import net.shrine.problem.{TestProblem, TurnOffProblemConnector} + /** * @author clint * @since Feb 24, 2014 */ -final class ShrineMessageTest extends ShouldMatchersForJUnit { +final class ShrineMessageTest extends ShouldMatchersForJUnit with TurnOffProblemConnector { @Test def testRoundTrips { val projectId = "salkdjksaljdkla" import scala.concurrent.duration._ val waitTime: Duration = 98374L.milliseconds val userId = "foo-user" val groupId = "foo-group" val authn = AuthenticationInfo("blarg-domain", userId, Credential("sajkhdkjsadh", true)) val queryId = 485794359L val patientSetCollId = "ksaldjksal" val optionsXml: NodeSeq = x val fetchSize = 12345 val queryName = "saljkd;salda" val topicId = "saldjkasljdasdsadsadasdas" val topicName = "Topic Name" val outputTypes = ResultOutputType.nonBreakdownTypes.toSet val queryDefinition = QueryDefinition(queryName, Term("oiweruoiewkldfhsofi")) val queryDefinition2 = QueryDefinition(queryName, Term("a;slkjflfjlsdkjf")) val localResultId = "aoiduaojsdpaojcmsal" val nodeId = NodeId("foo") val nodeId2 = NodeId("bar") val queryTopicId1 = 123L val queryTopicId2 = 456L val queryTopicName1 = "nuh" val queryTopicName2 = "zuh" val shrineNetworkQueryId = 1287698235L val start = Some(XmlDateHelper.now) val end = Some(XmlDateHelper.now) val singleNodeResult1 = QueryResult.errorResult(Some("blarg"), "glarg",TestProblem()) val singleNodeResult2 = QueryResult( 42L, 99L, Option(ResultOutputType.PATIENT_COUNT_XML), 123L, start, end, Some("description"), QueryResult.StatusType.Finished, Some("status")) val param1 = ParamResponse("foo", "bar", "baz") val queryMaster1 = QueryMaster("kjasdh", 12345L, "name1", userId, groupId, start.get) val queryMaster2 = QueryMaster("skdjlhlasf", 873563L, "name2", userId, groupId, end.get) val queryInstance1 = QueryInstance("asd", "42", userId, groupId, start.get, end.get) val queryInstance2 = QueryInstance("asdasd", "99", userId, groupId, start.get, end.get) val envelope = I2b2ResultEnvelope(DefaultBreakdownResultOutputTypes.PATIENT_AGE_COUNT_XML, Map("x" -> 1, "y" -> 2)) //BroadcastMessage //Non-CA-signed signing cert doMarshallingRoundTrip(BroadcastMessage(123456L, authn, DeleteQueryRequest(projectId, waitTime, authn, queryId), Some(Signature(XmlDateHelper.now, CertId(new BigInteger("1234567890")), None, "asdf".getBytes)))) //CA-signed signing cert doMarshallingRoundTrip(BroadcastMessage(123456L, authn, DeleteQueryRequest(projectId, waitTime, authn, queryId), Some(Signature(XmlDateHelper.now, CertId(new BigInteger("1234567890")), Some(CertData("cert signed by ca".getBytes)), "asdf".getBytes)))) //Non-i2b2able requests doMarshallingRoundTrip(ReadTranslatedQueryDefinitionRequest(authn, waitTime, queryDefinition)) doMarshallingRoundTrip(ReadQueryResultRequest(projectId, waitTime, authn, queryId)) //I2b2able requests doMarshallingRoundTrip(DeleteQueryRequest(projectId, waitTime, authn, queryId)) doMarshallingRoundTrip(ReadApprovedQueryTopicsRequest(projectId, waitTime, authn, userId)) doMarshallingRoundTrip(ReadInstanceResultsRequest(projectId, waitTime, authn, queryId)) doMarshallingRoundTrip(ReadPreviousQueriesRequest(projectId, waitTime, authn, userId, fetchSize)) doMarshallingRoundTrip(ReadQueryDefinitionRequest(projectId, waitTime, authn, queryId)) doMarshallingRoundTrip(ReadQueryInstancesRequest(projectId, waitTime, authn, queryId)) doMarshallingRoundTrip(RenameQueryRequest(projectId, waitTime, authn, queryId, queryName)) doMarshallingRoundTrip(RunQueryRequest(projectId, waitTime, authn, queryId, Option(topicId), Option(topicName), outputTypes, queryDefinition)) doMarshallingRoundTrip(RunQueryRequest(projectId, waitTime, authn, queryId, None, None, outputTypes, queryDefinition)) doMarshallingRoundTrip(ReadResultRequest(projectId, waitTime, authn, localResultId)) //Non-i2b2able responses doMarshallingRoundTrip(SingleNodeReadTranslatedQueryDefinitionResponse(SingleNodeTranslationResult(nodeId, queryDefinition))) doMarshallingRoundTrip(AggregatedReadTranslatedQueryDefinitionResponse(Seq(SingleNodeTranslationResult(nodeId, queryDefinition), SingleNodeTranslationResult(nodeId2, queryDefinition2)))) //I2b2able responses doMarshallingRoundTrip(DeleteQueryResponse(queryId)) doMarshallingRoundTrip(ReadApprovedQueryTopicsResponse(Seq(ApprovedTopic(queryTopicId1, queryTopicName1), ApprovedTopic(queryTopicId2, queryTopicName2)))) doMarshallingRoundTrip(ReadInstanceResultsResponse(shrineNetworkQueryId, singleNodeResult2)) doMarshallingRoundTrip(AggregatedReadInstanceResultsResponse(shrineNetworkQueryId, Seq(singleNodeResult1, singleNodeResult2))) doMarshallingRoundTrip(ReadPdoResponse(Seq(EventResponse("foo", "bar", start, end, Seq(param1))), Seq(PatientResponse("nuh", Seq(param1))), Nil)) doMarshallingRoundTrip(ReadPreviousQueriesResponse(Seq(queryMaster1, queryMaster2))) doMarshallingRoundTrip(ReadQueryDefinitionResponse(42L, "name", userId, start.get, queryDefinition.toXmlString)) doMarshallingRoundTrip(ReadQueryInstancesResponse(42L, userId, groupId, Seq(queryInstance1, queryInstance2))) doMarshallingRoundTrip(RenameQueryResponse(12345L, queryName)) doMarshallingRoundTrip(RunQueryResponse(queryId, start.get, userId, groupId, queryDefinition, 12345L, singleNodeResult1)) doMarshallingRoundTrip(AggregatedRunQueryResponse(queryId, start.get, userId, groupId, queryDefinition, 12345L, Seq(singleNodeResult1, singleNodeResult2))) doMarshallingRoundTrip(ReadResultResponse(42L, singleNodeResult2, envelope)) } private def doMarshallingRoundTrip[T <: ShrineMessage](message: T) { val xml = message.toXml val unmarshalled = ShrineMessage.fromXml(DefaultBreakdownResultOutputTypes.toSet)(xml).get message match { //NB: Special handling of ReadInstanceResultsResponse because its member QueryRequests are munged //before serialization case readInstanceResultsResponse: ReadInstanceResultsResponse => { val unmarshalledResp = unmarshalled.asInstanceOf[ReadInstanceResultsResponse] val expected = readInstanceResultsResponse.copy(singleNodeResult = readInstanceResultsResponse.singleNodeResult.copy(instanceId = readInstanceResultsResponse.shrineNetworkQueryId)) unmarshalledResp should equal(expected) } //NB: Special handling of AggregatedReadInstanceResultsResponse because its member QueryRequests are munged //before serialization case aggReadInstanceResultsResponse: AggregatedReadInstanceResultsResponse => { val unmarshalledResp = unmarshalled.asInstanceOf[AggregatedReadInstanceResultsResponse] val expected = aggReadInstanceResultsResponse.copy(results = aggReadInstanceResultsResponse.results.map(_.copy(instanceId = aggReadInstanceResultsResponse.shrineNetworkQueryId))) unmarshalledResp.results(0).problemDigest should equal(expected.results(0).problemDigest) unmarshalledResp.results(0) should equal(expected.results(0)) unmarshalledResp.results should equal(expected.results) unmarshalledResp should equal(expected) } //NB: Special handling of ReadQueryInstancesResponse because its member QueryInstances are not exactly preserved //on serialization round trips case readQueryInstancesResponse: ReadQueryInstancesResponse => { val unmarshalledResp = unmarshalled.asInstanceOf[ReadQueryInstancesResponse] val expected = unmarshalledResp.withInstances(unmarshalledResp.queryInstances.map(_.copy(queryMasterId = unmarshalledResp.queryMasterId.toString))) } //NB: Special handling of RunQueryResponse because its member QueryRequest is munged //during serialization case runQueryResponse: RunQueryResponse => { val unmarshalledResp = unmarshalled.asInstanceOf[RunQueryResponse] val expected = runQueryResponse.withResult(runQueryResponse.singleNodeResult.copy(instanceId = runQueryResponse.queryInstanceId)) unmarshalledResp should equal(expected) } //NB: Special handling of AggregatedRunQueryResponse because its member QueryRequests are munged //during serialization case aggRunQueryResponse: AggregatedRunQueryResponse => { val unmarshalledResp = unmarshalled.asInstanceOf[AggregatedRunQueryResponse] val expected = aggRunQueryResponse.withResults(aggRunQueryResponse.results.map(_.copy(instanceId = aggRunQueryResponse.queryInstanceId))) unmarshalledResp should equal(expected) } case _ => unmarshalled should equal(message) } } } \ No newline at end of file diff --git a/commons/protocol/src/test/scala/net/shrine/protocol/ShrineResponseTest.scala b/commons/protocol/src/test/scala/net/shrine/protocol/ShrineResponseTest.scala index ecd4515b3..ea155f431 100644 --- a/commons/protocol/src/test/scala/net/shrine/protocol/ShrineResponseTest.scala +++ b/commons/protocol/src/test/scala/net/shrine/protocol/ShrineResponseTest.scala @@ -1,106 +1,107 @@ package net.shrine.protocol -import net.shrine.problem.TestProblem +import net.shrine.problem.{TestProblem, TurnOffProblemConnector} import scala.xml.NodeSeq import org.junit.Test import net.shrine.util.ShouldMatchersForJUnit import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol.query.Term import net.shrine.util.XmlDateHelper import net.shrine.util.XmlUtil + import scala.util.Success /** * @author clint * @since Nov 5, 2012 */ //noinspection UnitMethodIsParameterless,NameBooleanParameters,ScalaUnnecessaryParentheses -final class ShrineResponseTest extends ShouldMatchersForJUnit { +final class ShrineResponseTest extends ShouldMatchersForJUnit with TurnOffProblemConnector { @Test def testFromXml { //ShrineResponse.fromXml(null: String).isFailure should be(true) ShrineResponse.fromXml(DefaultBreakdownResultOutputTypes.toSet)(null: NodeSeq).isFailure should be(true) ShrineResponse.fromXml(DefaultBreakdownResultOutputTypes.toSet)(NodeSeq.Empty).isFailure should be(true) def roundTrip(response: ShrineResponse): Unit = { val unmarshalled = ShrineResponse.fromXml(DefaultBreakdownResultOutputTypes.toSet)(response.toXml) unmarshalled.get.getClass should equal(response.getClass) unmarshalled should not be (null) unmarshalled should equal(Success(response)) } val queryResult1 = QueryResult( resultId = 1L, instanceId = 2342L, resultType = Some(ResultOutputType.PATIENT_COUNT_XML), setSize = 123L, startDate = None, endDate = None, description = None, statusType = QueryResult.StatusType.Finished, statusMessage = None) roundTrip(ReadQueryResultResponse(123L, queryResult1)) roundTrip(AggregatedReadQueryResultResponse(123L, Seq(queryResult1))) roundTrip(DeleteQueryResponse(123L)) roundTrip(ReadInstanceResultsResponse(2342L, queryResult1)) roundTrip(AggregatedReadInstanceResultsResponse(2342L, Seq(queryResult1))) roundTrip(ReadPreviousQueriesResponse(Seq(QueryMaster("queryMasterId", 12345L, "name", "userId", "groupId", XmlDateHelper.now, Some(false))))) roundTrip(ReadQueryDefinitionResponse(8457L, "name", "userId", XmlDateHelper.now, "queryDefXml")) roundTrip(ReadQueryInstancesResponse(12345L, "userId", "groupId", Seq.empty)) roundTrip(RenameQueryResponse(12345L, "name")) roundTrip(RunQueryResponse(38957L, XmlDateHelper.now, "userId", "groupId", QueryDefinition("foo", Term("bar")), 2342L, queryResult1)) roundTrip(AggregatedRunQueryResponse(38957L, XmlDateHelper.now, "userId", "groupId", QueryDefinition("foo", Term("bar")), 2342L, Seq(queryResult1))) roundTrip(UnFlagQueryResponse) roundTrip(FlagQueryResponse) roundTrip(ErrorResponse(TestProblem("errorMessage"))) } @Test def testToXml { val response = new FooResponse response.toXmlString should equal("") } @Test def testToI2b2 { val expected = XmlUtil.stripWhitespace( 1.1 2.4 SHRINE 1.3-compatible SHRINE 2011-04-08T16:21:12.251-04:00 DONE ) val response = new FooResponse response.toI2b2String should equal(expected.toString()) } private final class FooResponse extends ShrineResponse { protected override def i2b2MessageBody = override def toXml = i2b2MessageBody } } \ No newline at end of file diff --git a/commons/util/pom.xml b/commons/util/pom.xml index b2bf896ee..ca65f31f1 100644 --- a/commons/util/pom.xml +++ b/commons/util/pom.xml @@ -1,85 +1,104 @@ 4.0.0 SHRINE Utility Code shrine-util jar net.shrine shrine-base 1.22.0-SNAPSHOT ../../pom.xml + + com.typesafe.slick + slick_2.11 + ${slick-version} + + + org.slf4j + slf4j-simple + 1.6.4 + org.apache.commons commons-email 1.2 - net.liftweb lift-json_${scala-major-version} org.scala-lang scalap org.scala-lang scalap ${scala-version} log4j log4j net.shrine shrine-test-commons ${project.version} test-jar test + + org.json4s + json4s-native_2.11 + ${json4s-version} + + + com.h2database + h2 + test + src/main/scala src/test/scala src/main/resources true shrine-versions.properties net.alchim31.maven scala-maven-plugin org.apache.maven.plugins maven-jar-plugin 2.6 test-jar diff --git a/commons/util/src/main/scala/net/shrine/serialization/NodeSeqSerializer.scala b/commons/util/src/main/scala/net/shrine/serialization/NodeSeqSerializer.scala new file mode 100644 index 000000000..1375b9503 --- /dev/null +++ b/commons/util/src/main/scala/net/shrine/serialization/NodeSeqSerializer.scala @@ -0,0 +1,22 @@ +package net.shrine.serialization + +import org.json4s.{Formats, MappingException, Serializer, _} +import org.json4s.reflect.TypeInfo +import org.json4s.Xml.{toJson, toXml} + +import scala.xml.{NodeSeq, XML} + +/** + * Created by ty on 7/22/16. + */ +// The default json serializer throws a stack overflow when trying to serialize NodeSeq. +class NodeSeqSerializer extends Serializer[NodeSeq] { + private val NodeSeqClass = classOf[NodeSeq] + + override def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), NodeSeq] = { + case (TypeInfo(NodeSeqClass, _), json) => toXml(json) + } + override def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { + case node:NodeSeq => toJson(node) + } +} diff --git a/hms-support/hms-core/src/main/scala/net/shrine/hms/authorization/HmsDataStewardAuthorizationService.scala b/hms-support/hms-core/src/main/scala/net/shrine/hms/authorization/HmsDataStewardAuthorizationService.scala index 3a8de442d..f50e3f910 100644 --- a/hms-support/hms-core/src/main/scala/net/shrine/hms/authorization/HmsDataStewardAuthorizationService.scala +++ b/hms-support/hms-core/src/main/scala/net/shrine/hms/authorization/HmsDataStewardAuthorizationService.scala @@ -1,89 +1,89 @@ package net.shrine.hms.authorization import java.net.URL import com.typesafe.config.Config import net.shrine.authentication.{AuthenticationResult, Authenticator} import net.shrine.authorization.{AuthorizationResult, QueryAuthorizationService} import net.shrine.client.EndpointConfig import net.shrine.log.Loggable -import net.shrine.problem.{AbstractProblem, ProblemSources} -import net.shrine.protocol.{CredentialConfig, AuthenticationInfo, ErrorResponse, ReadApprovedQueryTopicsRequest, ReadApprovedQueryTopicsResponse, RunQueryRequest} +import net.shrine.protocol.{AuthenticationInfo, CredentialConfig, ErrorResponse, ReadApprovedQueryTopicsRequest, ReadApprovedQueryTopicsResponse, RunQueryRequest} import net.shrine.config.ConfigExtensions +import net.shrine.problem.{AbstractProblem, ProblemSources} /** * @author Bill Simons * @since 1/30/12 * @see http://cbmi.med.harvard.edu * @see http://chip.org *

* NOTICE: This software comes with NO guarantees whatsoever and is * licensed as Lgpl Open Source * @see http://www.gnu.org/licenses/lgpl.html */ final case class HmsDataStewardAuthorizationService( sheriffClient: SheriffClient, authenticator: Authenticator ) extends QueryAuthorizationService with Loggable { import net.shrine.hms.authorization.HmsDataStewardAuthorizationService._ override def readApprovedEntries(request: ReadApprovedQueryTopicsRequest): Either[ErrorResponse, ReadApprovedQueryTopicsResponse] = { val authn = request.authn authenticate(authn) match { case None => Left(ErrorResponse(HMSNotAuthenticatedProblem(authn))) case Some(ecommonsUsername) => val topics = sheriffClient.getApprovedEntries(ecommonsUsername) Right(ReadApprovedQueryTopicsResponse(topics)) } } override def authorizeRunQueryRequest(request: RunQueryRequest): AuthorizationResult = { val authn = request.authn if (request.topicId.isEmpty) { AuthorizationResult.NotAuthorized(s"HMS queries require a topic id; couldn't authenticate user ${toDomainAndUser(authn)}") } else { authenticate(authn) match { case None => AuthorizationResult.NotAuthorized(s"Requested topic is not approved; couldn't authenticate user ${toDomainAndUser(authn)}") case Some(ecommonsUsername) => sheriffClient.isAuthorized(ecommonsUsername, request.topicId.get, request.queryDefinition.toI2b2String) } } } private def authenticate(authn: AuthenticationInfo): Option[String] = { val authenticationResult = authenticator.authenticate(authn) identifyEcommonsUsername(authenticationResult) } } object HmsDataStewardAuthorizationService { def apply(config:Config,authenticator: Authenticator):HmsDataStewardAuthorizationService = { val endpointUrl = config.getString("sheriffEndpoint"+EndpointConfig.Keys.url) val credentials = config.getConfigured("sheriffCredentials", CredentialConfig(_)) val sheriffClient = JerseySheriffClient(endpointUrl, credentials.username, credentials.password) HmsDataStewardAuthorizationService(sheriffClient, authenticator) } private def toDomainAndUser(authn: AuthenticationInfo): String = s"${authn.domain}:${authn.username}" def identifyEcommonsUsername(authenticationResult: AuthenticationResult): Option[String] = authenticationResult match { case AuthenticationResult.Authenticated(_, ecommonsUsername) => Option(ecommonsUsername) case _ => None } } case class HMSNotAuthenticatedProblem(authn: AuthenticationInfo) extends AbstractProblem(ProblemSources.Qep){ - override val summary = s"Can not authenticate ${authn.domain}:${authn.username}." + override lazy val summary = s"Can not authenticate ${authn.domain}:${authn.username}." - override val description = s"Can not authenticate ${authn.domain}:${authn.username}." + override lazy val description = s"Can not authenticate ${authn.domain}:${authn.username}." } \ No newline at end of file diff --git a/hms-support/hms-core/src/test/scala/net/shrine/hms/authorization/HmsDataStewardAuthorizationServiceTest.scala b/hms-support/hms-core/src/test/scala/net/shrine/hms/authorization/HmsDataStewardAuthorizationServiceTest.scala index 359ab5e95..0da9ff494 100644 --- a/hms-support/hms-core/src/test/scala/net/shrine/hms/authorization/HmsDataStewardAuthorizationServiceTest.scala +++ b/hms-support/hms-core/src/test/scala/net/shrine/hms/authorization/HmsDataStewardAuthorizationServiceTest.scala @@ -1,158 +1,159 @@ package net.shrine.hms.authorization import net.shrine.authentication.{AuthenticationResult, Authenticator} import net.shrine.authorization.AuthorizationResult.{Authorized, NotAuthorized} +import net.shrine.problem.TurnOffProblemConnector import net.shrine.protocol.{ApprovedTopic, AuthenticationInfo, Credential, ErrorResponse, ReadApprovedQueryTopicsRequest, ReadApprovedQueryTopicsResponse, RunQueryRequest} import net.shrine.protocol.query.{QueryDefinition, Term} import net.shrine.util.ShouldMatchersForJUnit import org.junit.Test /** * @author Bill Simons * @since 1/30/12 * @see http://cbmi.med.harvard.edu * @see http://chip.org *

* NOTICE: This software comes with NO guarantees whatsoever and is * licensed as Lgpl Open Source * @see http://www.gnu.org/licenses/lgpl.html */ -final class HmsDataStewardAuthorizationServiceTest extends ShouldMatchersForJUnit { +final class HmsDataStewardAuthorizationServiceTest extends ShouldMatchersForJUnit with TurnOffProblemConnector { @Test def testIdentifyEcommonsUsername(): Unit = { import AuthenticationResult._ import HmsDataStewardAuthorizationService.identifyEcommonsUsername identifyEcommonsUsername(NotAuthenticated("", "", "")) should be(None) val ecommonsId = "foo" identifyEcommonsUsername(Authenticated("", ecommonsId)) should be(Some(ecommonsId)) } import HmsDataStewardAuthorizationServiceTest._ import scala.concurrent.duration._ private val authn = AuthenticationInfo("d", "u", Credential("p", isToken = false)) @Test def testReadApprovedEntriesNotAuthenticated() { val service = new HmsDataStewardAuthorizationService(null, NeverAuthenticatesAuthenticator) val result = service.readApprovedEntries(ReadApprovedQueryTopicsRequest("projectId", 0.minutes, authn, authn.username)) val Left(errorResponse: ErrorResponse) = result //noinspection ScalaUnnecessaryParentheses errorResponse.errorMessage should not be (null) } @Test def testReadApprovedEntriesAuthenticated() { val topic = ApprovedTopic(123L, "blarg") val ecommonsUsername = "abc123" val mockSheriffClient = MockSheriffClient(topics = Seq(topic)) val service = HmsDataStewardAuthorizationService(mockSheriffClient, AlwaysAuthenticatesAuthenticator(ecommonsUsername)) val result = service.readApprovedEntries(ReadApprovedQueryTopicsRequest("projectId", 0.minutes, authn, authn.username)) val Right(ReadApprovedQueryTopicsResponse(Seq(actualTopic))) = result actualTopic should equal(topic) mockSheriffClient.Params.user should be(null) mockSheriffClient.Params.topicId should be(null) mockSheriffClient.Params.queryText should be(null) mockSheriffClient.Params.ecommonsUsername should be(ecommonsUsername) } @Test def testAuthorizeRunQueryRequestNotAuthenticated() { val service = new HmsDataStewardAuthorizationService(null, NeverAuthenticatesAuthenticator) def doTest(topicId: Option[String],topicName:Option[String]): Unit = { val result = service.authorizeRunQueryRequest(RunQueryRequest("projectId", 0.minutes, authn, topicId, topicName, Set.empty, QueryDefinition("foo", Term("foo")))) result.isAuthorized should be(false) } doTest(None,None) doTest(Some("topicId"),Some("Topic Name")) } @Test def testAuthorizeRunQueryRequestAuthenticated() { def doTest(isAuthorized: Boolean, topicId: Option[String], topicName:Option[String]): Unit = { val ecommonsUsername = "abc123" val queryDef = QueryDefinition("foo", Term("foo")) val mockSheriffClient = MockSheriffClient(authorized = isAuthorized) val service = HmsDataStewardAuthorizationService(mockSheriffClient, AlwaysAuthenticatesAuthenticator(ecommonsUsername)) val result = service.authorizeRunQueryRequest(RunQueryRequest("projectId", 0.minutes, authn, topicId, topicName, Set.empty, queryDef)) val expectedIsAuthorized = isAuthorized && topicId.isDefined result.isAuthorized should be(expectedIsAuthorized) if (topicId.isDefined) { mockSheriffClient.Params.user should equal(ecommonsUsername) mockSheriffClient.Params.topicId should equal(topicId.get) mockSheriffClient.Params.queryText should equal(queryDef.toI2b2String) mockSheriffClient.Params.ecommonsUsername should be(null) } else { mockSheriffClient.Params.user should be(null) mockSheriffClient.Params.topicId should be(null) mockSheriffClient.Params.queryText should be(null) mockSheriffClient.Params.ecommonsUsername should be(null) } } doTest(isAuthorized = true, Some("topic123"), Some("Topic Name")) doTest(isAuthorized = false, Some("topic123"), Some("Topic Name")) doTest(isAuthorized = false, Some("topic123"), None) doTest(isAuthorized = true, None, None) doTest(isAuthorized = false, None, None) } } object HmsDataStewardAuthorizationServiceTest { object NeverAuthenticatesAuthenticator extends Authenticator { override def authenticate(authn: AuthenticationInfo) = AuthenticationResult.NotAuthenticated(authn.domain, authn.username, "foo") } final case class AlwaysAuthenticatesAuthenticator(ecommonsUsername: String) extends Authenticator { override def authenticate(authn: AuthenticationInfo) = AuthenticationResult.Authenticated(authn.domain, ecommonsUsername) } final case class MockSheriffClient(authorized: Boolean = false, topics: Seq[ApprovedTopic] = Nil) extends SheriffClient { object Params { var ecommonsUsername: String = _ var user: String = _ var topicId: String = _ var queryText: String = _ } override def getApprovedEntries(ecommonsUsername: String): Seq[ApprovedTopic] = { Params.ecommonsUsername = ecommonsUsername topics } override def isAuthorized(user: String, topicId: String, queryText: String) = { Params.user = user Params.topicId = topicId Params.queryText = queryText if(authorized) Authorized(Some((topicId,"Mock Topic"))) else NotAuthorized("Mock authorization failure") } } } \ No newline at end of file diff --git a/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/BasicAggregator.scala b/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/BasicAggregator.scala index 988594f2f..e8d129e82 100644 --- a/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/BasicAggregator.scala +++ b/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/BasicAggregator.scala @@ -1,130 +1,129 @@ package net.shrine.aggregation -import java.net.{UnknownHostException, ConnectException} +import java.net.{ConnectException, UnknownHostException} import com.sun.jersey.api.client.ClientHandlerException import net.shrine.broadcaster.CouldNotParseResultsException import net.shrine.log.Loggable -import net.shrine.problem.{ProblemNotYetEncoded, ProblemSources, AbstractProblem} +import net.shrine.problem.{AbstractProblem, ProblemNotYetEncoded, ProblemSources} import scala.concurrent.duration.Duration import net.shrine.protocol.ErrorResponse import net.shrine.protocol.Failure import net.shrine.protocol.NodeId import net.shrine.protocol.Result import net.shrine.protocol.SingleNodeResult import net.shrine.protocol.Timeout import net.shrine.protocol.BaseShrineResponse /** * * @author Clint Gilbert * @since Sep 16, 2011 * * @see http://cbmi.med.harvard.edu * * This software is licensed under the LGPL * @see http://www.gnu.org/licenses/lgpl.html * * Represents the basic aggregation strategy shared by several aggregators: * - Parses a sequence of SpinResultEntries into a sequence of some * combination of valid responses, ErrorResponses, and invalid * responses (cases where ShrineResponse.fromXml returns None) * - Filters the valid responses, weeding out responses that aren't of * the expected type * Invokes an abstract method with the valid responses, errors, and * invalid responses. * * Needs to be an abstract class instead of a trait due to the view bound on T (: Manifest) */ abstract class BasicAggregator[T <: BaseShrineResponse: Manifest] extends Aggregator with Loggable { private[aggregation] def isAggregatable(response: BaseShrineResponse): Boolean = { manifest[T].runtimeClass.isAssignableFrom(response.getClass) } import BasicAggregator._ override def aggregate(results: Iterable[SingleNodeResult], errors: Iterable[ErrorResponse]): BaseShrineResponse = { val resultsOrErrors: Iterable[ParsedResult[T]] = { for { result <- results } yield { val parsedResponse: ParsedResult[T] = result match { case Result(origin, _, errorResponse: ErrorResponse) => Error(Option(origin), errorResponse) case Result(origin, elapsed, response: T) if isAggregatable(response) => Valid(origin, elapsed, response) case Timeout(origin) => Error(Option(origin), ErrorResponse(TimedOutWithAdapter(origin))) case Failure(origin, cause) => cause match { case cx: ConnectException => Error(Option(origin), ErrorResponse(CouldNotConnectToAdapter(origin, cx))) case uhx: UnknownHostException => Error(Option(origin), ErrorResponse(CouldNotConnectToAdapter(origin, uhx))) case chx: ClientHandlerException => Error(Option(origin), ErrorResponse(CouldNotConnectToAdapter(origin, chx))) case cnprx:CouldNotParseResultsException => if(cnprx.statusCode >= 400) Error(Option(origin), ErrorResponse(HttpErrorResponseProblem(cnprx))) else Error(Option(origin), ErrorResponse(CouldNotParseResultsProblem(cnprx))) case x => Error(Option(origin), ErrorResponse(ProblemNotYetEncoded(s"Failure querying node ${origin.name}",x))) } case _ => Invalid(None, s"Unexpected response in $getClass:\r\n $result") } parsedResponse } } val invalidResponses = resultsOrErrors.collect { case invalid: Invalid => invalid } val validResponses = resultsOrErrors.collect { case valid: Valid[T] => valid } val errorResponses: Iterable[Error] = resultsOrErrors.collect { case error: Error => error } //Log all parsing errors invalidResponses.map(_.errorMessage).foreach(this.error(_)) val previouslyDetectedErrors = errors.map(Error(None, _)) makeResponseFrom(validResponses, errorResponses ++ previouslyDetectedErrors, invalidResponses) } private[aggregation] def makeResponseFrom(validResponses: Iterable[Valid[T]], errorResponses: Iterable[Error], invalidResponses: Iterable[Invalid]): BaseShrineResponse } object BasicAggregator { private[aggregation] sealed abstract class ParsedResult[+T] private[aggregation] final case class Valid[T](origin: NodeId, elapsed: Duration, response: T) extends ParsedResult[T] private[aggregation] final case class Error(origin: Option[NodeId], response: ErrorResponse) extends ParsedResult[Nothing] private[aggregation] final case class Invalid(origin: Option[NodeId], errorMessage: String) extends ParsedResult[Nothing] } case class CouldNotConnectToAdapter(origin:NodeId,cx: Exception) extends AbstractProblem(ProblemSources.Hub) { - override val throwable = Some(cx) - override val summary: String = "Shrine could not connect to the adapter." - override val description: String = s"Shrine could not connect to the adapter at ${origin.name} due to ${throwable.get}." + override lazy val throwable = Some(cx) + override lazy val summary: String = "Shrine could not connect to the adapter." + override lazy val description: String = s"Shrine could not connect to the adapter at ${origin.name} due to ${throwable.get}." } case class TimedOutWithAdapter(origin:NodeId) extends AbstractProblem(ProblemSources.Hub) { - override val throwable = None - override val summary: String = "Timed out with adapter." - override val description: String = s"Shrine observed a timeout with the adapter at ${origin.name}." + override lazy val throwable = None + override lazy val summary: String = "Timed out with adapter." + override lazy val description: String = s"Shrine observed a timeout with the adapter at ${origin.name}." } case class CouldNotParseResultsProblem(cnrpx:CouldNotParseResultsException) extends AbstractProblem(ProblemSources.Hub) { - override val throwable = Some(cnrpx) - override val summary: String = "Could not parse response." - override val description = s"While parsing a response from ${cnrpx.url} with http code ${cnrpx.statusCode} caught '${cnrpx.cause}'" - override val detailsXml =

+ override lazy val throwable = Some(cnrpx) + override lazy val summary: String = "Could not parse response." + override lazy val description = s"While parsing a response from ${cnrpx.url} with http code ${cnrpx.statusCode} caught '${cnrpx.cause}'" + override lazy val detailsXml =
Message body is {cnrpx.body} {throwableDetail.getOrElse("")}
- } case class HttpErrorResponseProblem(cnrpx:CouldNotParseResultsException) extends AbstractProblem(ProblemSources.Hub) { - override val throwable = Some(cnrpx) - override val summary: String = "Adapter error." - override val description = s"Observed http status code ${cnrpx.statusCode} from ${cnrpx.url} and caught ${cnrpx.cause}." - override val detailsXml =
+ override lazy val throwable = Some(cnrpx) + override lazy val summary: String = "Adapter error." + override lazy val description = s"Observed http status code ${cnrpx.statusCode} from ${cnrpx.url} and caught ${cnrpx.cause}." + override lazy val detailsXml =
Message body is {cnrpx.body} {throwableDetail.getOrElse("")}
} \ No newline at end of file diff --git a/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/IgnoresErrorsAggregator.scala b/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/IgnoresErrorsAggregator.scala index 1085f6c9f..dae2ca6f7 100644 --- a/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/IgnoresErrorsAggregator.scala +++ b/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/IgnoresErrorsAggregator.scala @@ -1,43 +1,43 @@ package net.shrine.aggregation -import net.shrine.aggregation.BasicAggregator.{Invalid, Error, Valid} -import net.shrine.problem.{ProblemSources, AbstractProblem} +import net.shrine.aggregation.BasicAggregator.{Error, Invalid, Valid} +import net.shrine.problem.{AbstractProblem, ProblemSources} import net.shrine.protocol.ErrorResponse import net.shrine.protocol.BaseShrineResponse /** * * @author Clint Gilbert * @since Sep 16, 2011 * * @see http://cbmi.med.harvard.edu * * This software is licensed under the LGPL * @see http://www.gnu.org/licenses/lgpl.html * * Extends BasicAggregator to ignore Errors and Invalid responses * * Needs to be an abstract class instead of a trait due to the view bound on T (: Manifest) */ abstract class IgnoresErrorsAggregator[T <: BaseShrineResponse : Manifest] extends BasicAggregator[T] { private[aggregation] override def makeResponseFrom(validResponses: Iterable[Valid[T]], errorResponses: Iterable[Error], invalidResponses: Iterable[Invalid]): BaseShrineResponse = { //Filter out errors and invalid responses makeResponseFrom(validResponses) } //Default implementation, just returns first valid response, or if there are none, an ErrorResponse private[aggregation] def makeResponseFrom(validResponses: Iterable[Valid[T]]): BaseShrineResponse = { validResponses.map(_.response).toSet.headOption.getOrElse{ val problem = NoValidResponsesToAggregate() ErrorResponse(problem) } } } case class NoValidResponsesToAggregate() extends AbstractProblem(ProblemSources.Hub) { - override val summary: String = "No valid responses to aggregate." + override lazy val summary: String = "No valid responses to aggregate." - override val description:String = "The hub received no valid responses to aggregate." + override lazy val description:String = "The hub received no valid responses to aggregate." } \ No newline at end of file diff --git a/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/PackagesErrorsAggregator.scala b/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/PackagesErrorsAggregator.scala index c8e0c2934..12d42f4e9 100644 --- a/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/PackagesErrorsAggregator.scala +++ b/hub/broadcaster-aggregator/src/main/scala/net/shrine/aggregation/PackagesErrorsAggregator.scala @@ -1,54 +1,54 @@ package net.shrine.aggregation -import net.shrine.problem.{ProblemSources, AbstractProblem} import net.shrine.protocol.ShrineResponse -import net.shrine.aggregation.BasicAggregator.{Invalid, Error, Valid} +import net.shrine.aggregation.BasicAggregator.{Error, Invalid, Valid} +import net.shrine.problem.{AbstractProblem, ProblemSources} import net.shrine.protocol.QueryResult /** * * @author Clint Gilbert * @since Sep 16, 2011 * * @see http://cbmi.med.harvard.edu * * This software is licensed under the LGPL * @see http://www.gnu.org/licenses/lgpl.html * * Extends BasicAggregator to package Errors and Invalid responses into QueryResults * * Needs to be an abstract class instead of a trait due to the view bound on T (: Manifest) */ abstract class PackagesErrorsAggregator[T <: ShrineResponse : Manifest]( errorMessage: Option[String] = None, invalidMessage: Option[String] = None) extends BasicAggregator[T] { private[aggregation] def makeErrorResult(error: Error): QueryResult = { val Error(originOption, errorResponse) = error //Use node name as the description, to avoid giving the web UI more data than it can display val desc = originOption.map(_.name) QueryResult.errorResult(desc, errorMessage.getOrElse(errorResponse.errorMessage),errorResponse.problemDigest) } private[aggregation] def makeInvalidResult(invalid: Invalid): QueryResult = { val Invalid(originOption, errorMessage) = invalid //Use node name as the description, to avoid giving the web UI more data than it can display val desc = originOption.map(_.name) QueryResult.errorResult(desc, invalidMessage.getOrElse(errorMessage),InvalidResultProblem(invalid)) } private[aggregation] final override def makeResponseFrom(validResponses: Iterable[Valid[T]], errorResponses: Iterable[Error], invalidResponses: Iterable[Invalid]): ShrineResponse = { makeResponse(validResponses, errorResponses.map(makeErrorResult), invalidResponses.map(makeInvalidResult)) } private[aggregation] def makeResponse(validResponses: Iterable[Valid[T]], errorResponses: Iterable[QueryResult], invalidResponses: Iterable[QueryResult]): ShrineResponse } case class InvalidResultProblem(invalid:Invalid) extends AbstractProblem(ProblemSources.Hub) { override def summary: String = s"Invalid response." override def description: String = s"${invalid.errorMessage} from ${invalid.origin.getOrElse("an unknown node")}" } \ No newline at end of file diff --git a/hub/broadcaster-aggregator/src/test/scala/net/shrine/broadcaster/SigningBroadcastAndAggregationServiceTest.scala b/hub/broadcaster-aggregator/src/test/scala/net/shrine/broadcaster/SigningBroadcastAndAggregationServiceTest.scala index 561476d7a..1a20b731c 100644 --- a/hub/broadcaster-aggregator/src/test/scala/net/shrine/broadcaster/SigningBroadcastAndAggregationServiceTest.scala +++ b/hub/broadcaster-aggregator/src/test/scala/net/shrine/broadcaster/SigningBroadcastAndAggregationServiceTest.scala @@ -1,102 +1,107 @@ package net.shrine.broadcaster -import net.shrine.problem.TestProblem - import scala.concurrent.Await import org.junit.Test import net.shrine.util.ShouldMatchersForJUnit import net.shrine.aggregation.Aggregator import net.shrine.crypto.DefaultSignerVerifier import net.shrine.crypto.TestKeystore import net.shrine.protocol.AuthenticationInfo import net.shrine.protocol.BroadcastMessage import net.shrine.protocol.Credential import net.shrine.protocol.DeleteQueryRequest import net.shrine.protocol.ErrorResponse import net.shrine.protocol.Failure import net.shrine.protocol.NodeId import net.shrine.protocol.Result import net.shrine.protocol.ShrineResponse import net.shrine.protocol.SingleNodeResult import net.shrine.protocol.Timeout import net.shrine.crypto.SigningCertStrategy import net.shrine.broadcaster.dao.MockHubDao +import net.shrine.problem.{TestProblem, TurnOffProblemConnector} /** * @author clint * @since Nov 19, 2013 */ final class SigningBroadcastAndAggregationServiceTest extends ShouldMatchersForJUnit { import scala.concurrent.duration._ import MockBroadcasters._ - private def result(description: Char) = Result(NodeId(description.toString), 1.second, ErrorResponse(TestProblem(summary = "blah blah blah"))) + private def result(description: Char) = { + val problem: TestProblem = TestProblem(summary = "blah blah blah") + Result(NodeId(description.toString), 1.second, ErrorResponse(problem)) + } private val results = "abcde".map(result) private lazy val nullResultsByOrigin: Map[NodeId, SingleNodeResult] = Map(NodeId("X") -> null, NodeId("Y") -> null) private lazy val resultsWithNullsByOrigin: Map[NodeId, SingleNodeResult] = { results.collect { case r @ Result(origin, _, _) => origin -> r }.toMap ++ nullResultsByOrigin } private lazy val signer = new DefaultSignerVerifier(TestKeystore.certCollection) private val broadcastMessage = { val authn = AuthenticationInfo("domain", "username", Credential("asdasd", false)) import scala.concurrent.duration._ BroadcastMessage(authn, DeleteQueryRequest("projectId", 12345.milliseconds, authn, 12345L)) } @Test def testAggregateHandlesNullResults { val mockBroadcaster = MockAdapterClientBroadcaster(resultsWithNullsByOrigin) val broadcastService = SigningBroadcastAndAggregationService(InJvmBroadcasterClient(mockBroadcaster), signer, SigningCertStrategy.Attach) val aggregator: Aggregator = new Aggregator { override def aggregate(results: Iterable[SingleNodeResult], errors: Iterable[ErrorResponse]): ShrineResponse = { ErrorResponse(TestProblem(results.size.toString)) } } val aggregatedResult = Await.result(broadcastService.sendAndAggregate(broadcastMessage, aggregator, true), 5.minutes) mockBroadcaster.messageParam.signature.isDefined should be(true) - + + val testProblem = TestProblem(s"${results.size}") + //testProblem.stamp.time = aggregatedResult. + aggregatedResult should equal(ErrorResponse(TestProblem(s"${results.size}"))) } @Test def testAggregateHandlesFailures { def toResult(description: Char) = Result(NodeId(description.toString), 1.second, ErrorResponse(TestProblem("blah blah blah"))) def toFailure(description: Char) = Failure(NodeId(description.toString), new Exception with scala.util.control.NoStackTrace) val failuresByOrigin: Map[NodeId, SingleNodeResult] = { "UV".map(toFailure).map { case f @ Failure(origin, _) => origin -> f }.toMap } val timeoutsByOrigin: Map[NodeId, SingleNodeResult] = Map(NodeId("Z") -> Timeout(NodeId("Z"))) val resultsWithFailuresByOrigin: Map[NodeId, SingleNodeResult] = resultsWithNullsByOrigin ++ failuresByOrigin ++ timeoutsByOrigin val mockBroadcaster = MockAdapterClientBroadcaster(resultsWithFailuresByOrigin) val broadcastService = SigningBroadcastAndAggregationService(InJvmBroadcasterClient(mockBroadcaster), signer, SigningCertStrategy.DontAttach) val aggregator: Aggregator = new Aggregator { override def aggregate(results: Iterable[SingleNodeResult], errors: Iterable[ErrorResponse]): ShrineResponse = { ErrorResponse(TestProblem(s"${results.size},${errors.size}")) } } val aggregatedResult = Await.result(broadcastService.sendAndAggregate(broadcastMessage, aggregator, true), 5.minutes) mockBroadcaster.messageParam.signature.isDefined should be(true) aggregatedResult should equal(ErrorResponse(TestProblem(s"${results.size + failuresByOrigin.size + timeoutsByOrigin.size},0"))) } } diff --git a/qep/service/src/main/scala/net/shrine/qep/I2b2BroadcastResource.scala b/qep/service/src/main/scala/net/shrine/qep/I2b2BroadcastResource.scala index d9d0e558c..ad4e4a949 100644 --- a/qep/service/src/main/scala/net/shrine/qep/I2b2BroadcastResource.scala +++ b/qep/service/src/main/scala/net/shrine/qep/I2b2BroadcastResource.scala @@ -1,96 +1,97 @@ package net.shrine.qep import java.sql.SQLException import javax.ws.rs.{POST, Path, Produces} import javax.ws.rs.core.{MediaType, Response} import javax.ws.rs.core.Response.ResponseBuilder import net.shrine.authentication.NotAuthenticatedException import net.shrine.log.Loggable import net.shrine.problem.ProblemNotYetEncoded import net.shrine.protocol.{ErrorResponse, HandleableI2b2Request, I2b2RequestHandler, ResultOutputType, ShrineRequest} -import net.shrine.qep.queries.{CouldNotRunDbIoActionException, QepDatabaseProblem} +import net.shrine.qep.queries.QepDatabaseProblem import net.shrine.serialization.I2b2Marshaller +import net.shrine.slick.CouldNotRunDbIoActionException import net.shrine.util.XmlUtil import scala.util.Try import scala.util.control.NonFatal import scala.xml.NodeSeq /** * @author Bill Simons * @since 3/10/11 * @see http://cbmi.med.harvard.edu * @see http://chip.org *

* NOTICE: This software comes with NO guarantees whatsoever and is * licensed as Lgpl Open Source * @see http://www.gnu.org/licenses/lgpl.html */ @Path("/i2b2") @Produces(Array(MediaType.APPLICATION_XML)) final case class I2b2BroadcastResource(i2b2RequestHandler: I2b2RequestHandler, breakdownTypes: Set[ResultOutputType]) extends Loggable { //NB: Always broadcast when receiving requests from the legacy i2b2/Shrine webclient, since we can't retrofit it to //Say whether broadcasting is desired for a praticular query/operation val shouldBroadcast = true @POST @Path("request") def doRequest(i2b2Request: String): Response = processI2b2Message(i2b2Request) @POST @Path("pdorequest") def doPDORequest(i2b2Request: String): Response = processI2b2Message(i2b2Request) def processI2b2Message(i2b2Request: String): Response = { // todo would be good to log $i2b2Request)") def errorResponse(e: Throwable): ErrorResponse = e match { case nax:NotAuthenticatedException => ErrorResponse(nax.problem) case cnrdax:CouldNotRunDbIoActionException => ErrorResponse(QepDatabaseProblem(cnrdax)) case sqlx:SQLException => ErrorResponse(QepDatabaseProblem(sqlx)) case _ => ErrorResponse(ProblemNotYetEncoded("The QEP encountered an unforeseen problem while processing an i2b2 request",e)) } def prettyPrint(xml: NodeSeq): String = XmlUtil.prettyPrint(xml.head).trim //NB: The legacy webclient can't deal with non-200 status codes. //It also can't deal with ErrorResponses in several cases, but we have nothing better to return for now. //TODO: Return a 500 status here, once we're using the new web client def i2b2HttpErrorResponse(e: Throwable): ResponseBuilder = Response.ok.entity(prettyPrint(errorResponse(e).toI2b2)) def handleRequest(shrineRequest: ShrineRequest with HandleableI2b2Request): Try[ResponseBuilder] = Try { info(s"Running request from user: ${shrineRequest.authn.username} of type ${shrineRequest.requestType.toString}") val shrineResponse = shrineRequest.handleI2b2(i2b2RequestHandler, shouldBroadcast) //TODO: Revisit this. For now, we bail if we get something that isn't i2b2able val responseString: String = shrineResponse match { case i2b2able: I2b2Marshaller => prettyPrint(i2b2able.toI2b2) case _ => throw new Exception(s"Shrine response $shrineResponse has no i2b2 representation") } Response.ok.entity(responseString) }.recover { case NonFatal(e) => error("Error processing request: ", e) i2b2HttpErrorResponse(e) } def handleParseError(e: Throwable): Try[ResponseBuilder] = Try { debug(s"Failed to unmarshal i2b2 request.")//todo would be good to log : $i2b2Request") error("Couldn't understand request: ", e) //NB: The legacy webclient can't deal with non-200 status codes. //It also can't deal with ErrorResponses in several cases, but we have nothing better to return for now. //TODO: Return a 400 status here, once we're using the new web client i2b2HttpErrorResponse(e) } val builder = HandleableI2b2Request.fromI2b2String(breakdownTypes)(i2b2Request).transform(handleRequest, handleParseError).get builder.build() } } \ No newline at end of file diff --git a/qep/service/src/main/scala/net/shrine/qep/QepConfigSource.scala b/qep/service/src/main/scala/net/shrine/qep/QepConfigSource.scala index 2f3b02d60..dd0e00af6 100644 --- a/qep/service/src/main/scala/net/shrine/qep/QepConfigSource.scala +++ b/qep/service/src/main/scala/net/shrine/qep/QepConfigSource.scala @@ -1,43 +1,18 @@ package net.shrine.qep -import com.typesafe.config.{Config, ConfigFactory} -import net.shrine.config.AtomicConfigSource +import net.shrine.config.ConfigSource import net.shrine.log.Log /** * Source of config for the Qep. Put new config fields here, not in QepConfig, to enable Config-based apply() methods. * * @author david * @since 8/18/15 */ -object QepConfigSource { +object QepConfigSource extends ConfigSource { - val atomicConfig = new AtomicConfigSource(ConfigFactory.load("shrine")) - - def config:Config = { - atomicConfig.config - } + override val configName = "shrine" Log.debug(s"shrine.queryEntryPoint.audit.collectQepAudit is ${config.getBoolean("shrine.queryEntryPoint.audit.collectQepAudit")}") - def configForBlock[T](key:String,value:AnyRef,origin:String)(block: => T):T = { - atomicConfig.configForBlock(key,value,origin)(block) - } - - def configForBlock[T](config:Config,origin:String)(block: => T):T = { - atomicConfig.configForBlock(config,origin)(block) - } - - //todo move this to common code somewhere - def objectForName[T](objectName:String):T = { - - import scala.reflect.runtime.universe - val runtimeMirror = universe.runtimeMirror(getClass.getClassLoader) - val module = runtimeMirror.staticModule(objectName) - - val reflectedObj = runtimeMirror.reflectModule(module) - val obj = reflectedObj.instance - - obj.asInstanceOf[T] - } } \ No newline at end of file diff --git a/qep/service/src/main/scala/net/shrine/qep/queries/QepQueryDb.scala b/qep/service/src/main/scala/net/shrine/qep/queries/QepQueryDb.scala index 734eeebf2..1721fdc4d 100644 --- a/qep/service/src/main/scala/net/shrine/qep/queries/QepQueryDb.scala +++ b/qep/service/src/main/scala/net/shrine/qep/queries/QepQueryDb.scala @@ -1,536 +1,533 @@ package net.shrine.qep.queries import java.sql.SQLException import java.util.concurrent.TimeoutException import javax.sql.DataSource import com.typesafe.config.Config import net.shrine.audit.{NetworkQueryId, QueryName, Time, UserName} import net.shrine.log.Loggable -import net.shrine.problem.{AbstractProblem, ProblemSources, ProblemDigest} -import net.shrine.protocol.{ResultOutputTypes, DeleteQueryRequest, RenameQueryRequest, I2b2ResultEnvelope, QueryResult, ResultOutputType, DefaultBreakdownResultOutputTypes, UnFlagQueryRequest, FlagQueryRequest, QueryMaster, ReadPreviousQueriesRequest, ReadPreviousQueriesResponse, RunQueryRequest} +import net.shrine.problem.{AbstractProblem, ProblemDigest, ProblemSources} +import net.shrine.protocol.{DefaultBreakdownResultOutputTypes, DeleteQueryRequest, FlagQueryRequest, I2b2ResultEnvelope, QueryMaster, QueryResult, ReadPreviousQueriesRequest, ReadPreviousQueriesResponse, RenameQueryRequest, ResultOutputType, ResultOutputTypes, RunQueryRequest, UnFlagQueryRequest} import net.shrine.qep.QepConfigSource -import net.shrine.slick.TestableDataSourceCreator +import net.shrine.slick.{CouldNotRunDbIoActionException, TestableDataSourceCreator} import net.shrine.util.XmlDateHelper import slick.driver.JdbcProfile import scala.collection.immutable.Iterable import scala.concurrent.duration.{Duration, DurationInt} import scala.concurrent.{Await, Future, blocking} import scala.language.postfixOps - import scala.concurrent.ExecutionContext.Implicits.global import scala.util.control.NonFatal import scala.xml.XML /** * DB code for the QEP's query instances and query results. * * @author david * @since 1/19/16 */ case class QepQueryDb(schemaDef:QepQuerySchema,dataSource: DataSource,timeout:Duration) extends Loggable { import schemaDef._ import jdbcProfile.api._ val database = Database.forDataSource(dataSource) def createTables() = schemaDef.createTables(database) def dropTables() = schemaDef.dropTables(database) def dbRun[R](action: DBIOAction[R, NoStream, Nothing]):R = { val future: Future[R] = database.run(action) try { blocking { Await.result(future, timeout) } } catch { case tx:TimeoutException => throw CouldNotRunDbIoActionException(dataSource,tx) case NonFatal(x) => throw CouldNotRunDbIoActionException(dataSource,x) } } def insertQepQuery(runQueryRequest: RunQueryRequest):Unit = { debug(s"insertQepQuery $runQueryRequest") insertQepQuery(QepQuery(runQueryRequest)) } def insertQepQuery(qepQuery: QepQuery):Unit = { dbRun(allQepQueryQuery += qepQuery) } def selectAllQepQueries:Seq[QepQuery] = { dbRun(mostRecentVisibleQepQueries.result) } def selectPreviousQueries(request: ReadPreviousQueriesRequest):ReadPreviousQueriesResponse = { val previousQueries: Seq[QepQuery] = selectPreviousQueriesByUserAndDomain(request.authn.username,request.authn.domain,request.fetchSize) val flags:Map[NetworkQueryId,QepQueryFlag] = selectMostRecentQepQueryFlagsFor(previousQueries.map(_.networkId).to[Set]) val queriesAndFlags = previousQueries.map(x => (x,flags.get(x.networkId))) ReadPreviousQueriesResponse(queriesAndFlags.map(x => x._1.toQueryMaster(x._2))) } def selectPreviousQueriesByUserAndDomain(userName: UserName, domain: String, limit:Int):Seq[QepQuery] = { dbRun(mostRecentVisibleQepQueries.filter(_.userName === userName).filter(_.userDomain === domain).sortBy(x => x.changeDate.desc).take(limit).result) } def renamePreviousQuery(request:RenameQueryRequest):Unit = { val networkQueryId = request.networkQueryId dbRun( for { queryResults <- mostRecentVisibleQepQueries.filter(_.networkId === networkQueryId).result _ <- allQepQueryQuery ++= queryResults.map(_.copy(queryName = request.queryName,changeDate = System.currentTimeMillis())) } yield queryResults ) } def markDeleted(request:DeleteQueryRequest):Unit = { val networkQueryId = request.networkQueryId dbRun( for { queryResults <- mostRecentVisibleQepQueries.filter(_.networkId === networkQueryId).result _ <- allQepQueryQuery ++= queryResults.map(_.copy(deleted = true,changeDate = System.currentTimeMillis())) } yield queryResults ) } def insertQepQueryFlag(flagQueryRequest: FlagQueryRequest):Unit = { insertQepQueryFlag(QepQueryFlag(flagQueryRequest)) } def insertQepQueryFlag(unflagQueryRequest: UnFlagQueryRequest):Unit = { insertQepQueryFlag(QepQueryFlag(unflagQueryRequest)) } def insertQepQueryFlag(qepQueryFlag: QepQueryFlag):Unit = { dbRun(allQepQueryFlags += qepQueryFlag) } def selectMostRecentQepQueryFlagsFor(networkIds:Set[NetworkQueryId]):Map[NetworkQueryId,QepQueryFlag] = { val flags:Seq[QepQueryFlag] = dbRun(mostRecentQueryFlags.filter(_.networkId inSet networkIds).result) flags.map(x => x.networkQueryId -> x).toMap } def insertQepResultRow(qepQueryRow:QueryResultRow) = { dbRun(allQueryResultRows += qepQueryRow) } def insertQueryResult(networkQueryId:NetworkQueryId,result:QueryResult) = { val adapterNode = result.description.getOrElse(throw new IllegalStateException("description is empty, does not have an adapter node")) val queryResultRow = QueryResultRow(networkQueryId,result) val breakdowns: Iterable[QepQueryBreakdownResultsRow] = result.breakdowns.flatMap(QepQueryBreakdownResultsRow.breakdownRowsFor(networkQueryId,adapterNode,result.resultId,_)) val problem: Seq[QepProblemDigestRow] = result.problemDigest.map(p => QepProblemDigestRow(networkQueryId,adapterNode,p.codec,p.stampText,p.summary,p.description,p.detailsXml.toString,System.currentTimeMillis())).to[Seq] dbRun( for { _ <- allQueryResultRows += queryResultRow _ <- allBreakdownResultsRows ++= breakdowns _ <- allProblemDigestRows ++= problem } yield () ) } def selectMostRecentQepResultRowsFor(networkId:NetworkQueryId): Seq[QueryResultRow] = { dbRun(mostRecentQueryResultRows.filter(_.networkQueryId === networkId).result) } def selectMostRecentQepResultsFor(networkId:NetworkQueryId): Seq[QueryResult] = { val (queryResults, breakdowns,problems) = dbRun( for { queryResults <- mostRecentQueryResultRows.filter(_.networkQueryId === networkId).result breakdowns <- mostRecentBreakdownResultsRows.filter(_.networkQueryId === networkId).result problems <- mostRecentProblemDigestRows.filter(_.networkQueryId === networkId).result } yield (queryResults, breakdowns, problems) ) val resultIdsToI2b2ResultEnvelopes: Map[Long, Map[ResultOutputType, I2b2ResultEnvelope]] = breakdowns.groupBy(_.resultId).map(rIdToB => rIdToB._1 -> QepQueryBreakdownResultsRow.resultEnvelopesFrom(rIdToB._2)) def seqOfOneProblemRowToProblemDigest(problemSeq:Seq[QepProblemDigestRow]):ProblemDigest = { if(problemSeq.size == 1) problemSeq.head.toProblemDigest else throw new IllegalStateException(s"problemSeq size was not 1. $problemSeq") } val adapterNodesToProblemDigests: Map[String, ProblemDigest] = problems.groupBy(_.adapterNode).map(nodeToProblem => nodeToProblem._1 -> seqOfOneProblemRowToProblemDigest(nodeToProblem._2) ) queryResults.map(r => r.toQueryResult( resultIdsToI2b2ResultEnvelopes.getOrElse(r.resultId,Map.empty), adapterNodesToProblemDigests.get(r.adapterNode) )) } def insertQueryBreakdown(breakdownResultsRow:QepQueryBreakdownResultsRow) = { dbRun(allBreakdownResultsRows += breakdownResultsRow) } def selectAllBreakdownResultsRows: Seq[QepQueryBreakdownResultsRow] = { dbRun(allBreakdownResultsRows.result) } } object QepQueryDb extends Loggable { val dataSource:DataSource = TestableDataSourceCreator.dataSource(QepQuerySchema.config) val timeout = QepQuerySchema.config.getInt("timeout") seconds val db = QepQueryDb(QepQuerySchema.schema,dataSource,timeout) val createTablesOnStart = QepQuerySchema.config.getBoolean("createTablesOnStart") if(createTablesOnStart) QepQueryDb.db.createTables() } /** * Separate class to support schema generation without actually connecting to the database. * * @param jdbcProfile Database profile to use for the schema */ case class QepQuerySchema(jdbcProfile: JdbcProfile,moreBreakdowns: Set[ResultOutputType]) extends Loggable { import jdbcProfile.api._ def ddlForAllTables: jdbcProfile.DDL = { allQepQueryQuery.schema ++ allQepQueryFlags.schema ++ allQueryResultRows.schema ++ allBreakdownResultsRows.schema ++ allProblemDigestRows.schema } //to get the schema, use the REPL //println(QepQuerySchema.schema.ddlForAllTables.createStatements.mkString(";\n")) def createTables(database:Database) = { try { val future = database.run(ddlForAllTables.create) Await.result(future,10 seconds) } catch { //I'd prefer to check and create schema only if absent. No way to do that with Oracle. case x:SQLException => info("Caught exception while creating tables. Recover by assuming the tables already exist.",x) } } def dropTables(database:Database) = { val future = database.run(ddlForAllTables.drop) //Really wait forever for the cleanup Await.result(future,Duration.Inf) } class QepQueries(tag:Tag) extends Table[QepQuery](tag,"previousQueries") { def networkId = column[NetworkQueryId]("networkId") def userName = column[UserName]("userName") def userDomain = column[String]("domain") def queryName = column[QueryName]("queryName") def expression = column[Option[String]]("expression") def dateCreated = column[Time]("dateCreated") def deleted = column[Boolean]("deleted") def queryXml = column[String]("queryXml") def changeDate = column[Long]("changeDate") def * = (networkId,userName,userDomain,queryName,expression,dateCreated,deleted,queryXml,changeDate) <> (QepQuery.tupled,QepQuery.unapply) } val allQepQueryQuery = TableQuery[QepQueries] val mostRecentQepQueryQuery: Query[QepQueries, QepQuery, Seq] = for( queries <- allQepQueryQuery if !allQepQueryQuery.filter(_.networkId === queries.networkId).filter(_.changeDate > queries.changeDate).exists ) yield queries val mostRecentVisibleQepQueries = mostRecentQepQueryQuery.filter(_.deleted === false) class QepQueryFlags(tag:Tag) extends Table[QepQueryFlag](tag,"queryFlags") { def networkId = column[NetworkQueryId]("networkId") def flagged = column[Boolean]("flagged") def flagMessage = column[String]("flagMessage") def changeDate = column[Long]("changeDate") def * = (networkId,flagged,flagMessage,changeDate) <> (QepQueryFlag.tupled,QepQueryFlag.unapply) } val allQepQueryFlags = TableQuery[QepQueryFlags] val mostRecentQueryFlags: Query[QepQueryFlags, QepQueryFlag, Seq] = for( queryFlags <- allQepQueryFlags if !allQepQueryFlags.filter(_.networkId === queryFlags.networkId).filter(_.changeDate > queryFlags.changeDate).exists ) yield queryFlags val qepQueryResultTypes = DefaultBreakdownResultOutputTypes.toSet ++ ResultOutputType.values ++ moreBreakdowns val stringsToQueryResultTypes: Map[String, ResultOutputType] = qepQueryResultTypes.map(x => (x.name,x)).toMap val queryResultTypesToString: Map[ResultOutputType, String] = stringsToQueryResultTypes.map(_.swap) implicit val qepQueryResultTypesColumnType = MappedColumnType.base[ResultOutputType,String] ({ (resultType: ResultOutputType) => queryResultTypesToString(resultType) },{ (string: String) => stringsToQueryResultTypes(string) }) implicit val queryStatusColumnType = MappedColumnType.base[QueryResult.StatusType,String] ({ statusType => statusType.name },{ name => QueryResult.StatusType.valueOf(name).getOrElse(throw new IllegalStateException(s"$name is not one of ${QueryResult.StatusType.values.map(_.name).mkString(", ")}")) }) class QepQueryResults(tag:Tag) extends Table[QueryResultRow](tag,"queryResults") { def resultId = column[Long]("resultId") def networkQueryId = column[NetworkQueryId]("networkQueryId") def instanceId = column[Long]("instanceId") def adapterNode = column[String]("adapterNode") def resultType = column[Option[ResultOutputType]]("resultType") def size = column[Long]("size") def startDate = column[Option[Long]]("startDate") def endDate = column[Option[Long]]("endDate") def status = column[QueryResult.StatusType]("status") def statusMessage = column[Option[String]]("statusMessage") def changeDate = column[Long]("changeDate") def * = (resultId,networkQueryId,instanceId,adapterNode,resultType,size,startDate,endDate,status,statusMessage,changeDate) <> (QueryResultRow.tupled,QueryResultRow.unapply) } val allQueryResultRows = TableQuery[QepQueryResults] //Most recent query result rows for each queryId from each adapter val mostRecentQueryResultRows: Query[QepQueryResults, QueryResultRow, Seq] = for( queryResultRows <- allQueryResultRows if !allQueryResultRows.filter(_.networkQueryId === queryResultRows.networkQueryId).filter(_.adapterNode === queryResultRows.adapterNode).filter(_.changeDate > queryResultRows.changeDate).exists ) yield queryResultRows class QepQueryBreakdownResults(tag:Tag) extends Table[QepQueryBreakdownResultsRow](tag,"queryBreakdownResults") { def networkQueryId = column[NetworkQueryId]("networkQueryId") def adapterNode = column[String]("adapterNode") def resultId = column[Long]("resultId") def resultType = column[ResultOutputType]("resultType") def dataKey = column[String]("dataKey") def value = column[Long]("value") def changeDate = column[Long]("changeDate") def * = (networkQueryId,adapterNode,resultId,resultType,dataKey,value,changeDate) <> (QepQueryBreakdownResultsRow.tupled,QepQueryBreakdownResultsRow.unapply) } val allBreakdownResultsRows = TableQuery[QepQueryBreakdownResults] //Most recent query result rows for each queryId from each adapter val mostRecentBreakdownResultsRows: Query[QepQueryBreakdownResults, QepQueryBreakdownResultsRow, Seq] = for( breakdownResultsRows <- allBreakdownResultsRows if !allBreakdownResultsRows.filter(_.networkQueryId === breakdownResultsRows.networkQueryId).filter(_.adapterNode === breakdownResultsRows.adapterNode).filter(_.resultId === breakdownResultsRows.resultId).filter(_.changeDate > breakdownResultsRows.changeDate).exists ) yield breakdownResultsRows /* case class ProblemDigest(codec: String, stampText: String, summary: String, description: String, detailsXml: NodeSeq) extends XmlMarshaller { */ class QepResultProblemDigests(tag:Tag) extends Table [QepProblemDigestRow](tag,"queryResultProblemDigests") { def networkQueryId = column[NetworkQueryId]("networkQueryId") def adapterNode = column[String]("adapterNode") def codec = column[String]("codec") def stamp = column[String]("stamp") def summary = column[String]("summary") def description = column[String]("description") def details = column[String]("details") def changeDate = column[Long]("changeDate") def * = (networkQueryId,adapterNode,codec,stamp,summary,description,details,changeDate) <> (QepProblemDigestRow.tupled,QepProblemDigestRow.unapply) } val allProblemDigestRows = TableQuery[QepResultProblemDigests] val mostRecentProblemDigestRows: Query[QepResultProblemDigests, QepProblemDigestRow, Seq] = for( problemDigests <- allProblemDigestRows if !allProblemDigestRows.filter(_.networkQueryId === problemDigests.networkQueryId).filter(_.adapterNode === problemDigests.adapterNode).filter(_.changeDate > problemDigests.changeDate).exists ) yield problemDigests } object QepQuerySchema { val allConfig:Config = QepConfigSource.config val config:Config = allConfig.getConfig("shrine.queryEntryPoint.audit.database") val slickProfileClassName = config.getString("slickProfileClassName") val slickProfile:JdbcProfile = QepConfigSource.objectForName(slickProfileClassName) import net.shrine.config.{ConfigExtensions, Keys} val moreBreakdowns: Set[ResultOutputType] = config.getOptionConfigured("breakdownResultOutputTypes",ResultOutputTypes.fromConfig).getOrElse(Set.empty) val schema = QepQuerySchema(slickProfile,moreBreakdowns) } case class QepQuery( networkId:NetworkQueryId, userName: UserName, userDomain: String, queryName: QueryName, expression: Option[String], dateCreated: Time, deleted: Boolean, queryXml: String, changeDate: Time ){ def toQueryMaster(qepQueryFlag:Option[QepQueryFlag]):QueryMaster = { QueryMaster( queryMasterId = networkId.toString, networkQueryId = networkId, name = queryName, userId = userName, groupId = userDomain, createDate = XmlDateHelper.toXmlGregorianCalendar(dateCreated), flagged = qepQueryFlag.map(_.flagged), flagMessage = qepQueryFlag.map(_.flagMessage) ) } } object QepQuery extends ((NetworkQueryId,UserName,String,QueryName,Option[String],Time,Boolean,String,Time) => QepQuery) { def apply(runQueryRequest: RunQueryRequest):QepQuery = { new QepQuery( networkId = runQueryRequest.networkQueryId, userName = runQueryRequest.authn.username, userDomain = runQueryRequest.authn.domain, queryName = runQueryRequest.queryDefinition.name, expression = runQueryRequest.queryDefinition.expr.map(_.toString), dateCreated = System.currentTimeMillis(), deleted = false, queryXml = runQueryRequest.toXmlString, changeDate = System.currentTimeMillis() ) } } case class QepQueryFlag( networkQueryId: NetworkQueryId, flagged:Boolean, flagMessage:String, changeDate:Long ) object QepQueryFlag extends ((NetworkQueryId,Boolean,String,Long) => QepQueryFlag) { def apply(flagQueryRequest: FlagQueryRequest):QepQueryFlag = { QepQueryFlag( networkQueryId = flagQueryRequest.networkQueryId, flagged = true, flagMessage = flagQueryRequest.message.getOrElse(""), changeDate = System.currentTimeMillis() ) } def apply(unflagQueryRequest: UnFlagQueryRequest):QepQueryFlag = { QepQueryFlag( networkQueryId = unflagQueryRequest.networkQueryId, flagged = false, flagMessage = "", changeDate = System.currentTimeMillis() ) } } case class QueryResultRow( resultId:Long, networkQueryId:NetworkQueryId, instanceId:Long, adapterNode:String, resultType:Option[ResultOutputType], size:Long, startDate:Option[Long], endDate:Option[Long], status:QueryResult.StatusType, statusMessage:Option[String], changeDate:Long ) { def toQueryResult(breakdowns:Map[ResultOutputType,I2b2ResultEnvelope],problemDigest:Option[ProblemDigest]) = QueryResult( resultId = resultId, instanceId = instanceId, resultType = resultType, setSize = size, startDate = startDate.map(XmlDateHelper.toXmlGregorianCalendar), endDate = endDate.map(XmlDateHelper.toXmlGregorianCalendar), description = Some(adapterNode), statusType = status, statusMessage = statusMessage, breakdowns = breakdowns, problemDigest = problemDigest ) } object QueryResultRow extends ((Long,NetworkQueryId,Long,String,Option[ResultOutputType],Long,Option[Long],Option[Long],QueryResult.StatusType,Option[String],Long) => QueryResultRow) { def apply(networkQueryId:NetworkQueryId,result:QueryResult):QueryResultRow = { new QueryResultRow( resultId = result.resultId, networkQueryId = networkQueryId, instanceId = result.instanceId, adapterNode = result.description.getOrElse(s"$result has None in its description field, not a name of an adapter node."), resultType = result.resultType, size = result.setSize, startDate = result.startDate.map(_.toGregorianCalendar.getTimeInMillis), endDate = result.endDate.map(_.toGregorianCalendar.getTimeInMillis), status = result.statusType, statusMessage = result.statusMessage, changeDate = System.currentTimeMillis() ) } } case class QepQueryBreakdownResultsRow( networkQueryId: NetworkQueryId, adapterNode:String, resultId:Long, resultType: ResultOutputType, dataKey:String, value:Long, changeDate:Long ) object QepQueryBreakdownResultsRow extends ((NetworkQueryId,String,Long,ResultOutputType,String,Long,Long) => QepQueryBreakdownResultsRow){ def breakdownRowsFor(networkQueryId:NetworkQueryId, adapterNode:String, resultId:Long, breakdown:(ResultOutputType,I2b2ResultEnvelope)): Iterable[QepQueryBreakdownResultsRow] = { breakdown._2.data.map(b => QepQueryBreakdownResultsRow(networkQueryId,adapterNode,resultId,breakdown._1,b._1,b._2,System.currentTimeMillis())) } def resultEnvelopesFrom(breakdowns:Seq[QepQueryBreakdownResultsRow]): Map[ResultOutputType, I2b2ResultEnvelope] = { def resultEnvelopeFrom(resultType:ResultOutputType,breakdowns:Seq[QepQueryBreakdownResultsRow]):I2b2ResultEnvelope = { val data = breakdowns.map(b => b.dataKey -> b.value).toMap I2b2ResultEnvelope(resultType,data) } breakdowns.groupBy(_.resultType).map(r => r._1 -> resultEnvelopeFrom(r._1,r._2)) } } case class QepProblemDigestRow( networkQueryId: NetworkQueryId, adapterNode: String, codec: String, stampText: String, summary: String, description: String, details: String, changeDate:Long ){ def toProblemDigest = { ProblemDigest( codec, stampText, summary, description, if(!details.isEmpty) XML.loadString(details) - else

+ else
, + //TODO: FIGURE OUT HOW TO GET AN ACUTAL EPOCH INTO HERE + 0 ) } } -case class CouldNotRunDbIoActionException(dataSource: DataSource, exception: Throwable) extends RuntimeException(exception) { - override def getMessage:String = s"Could not use the database defined by $dataSource due to ${exception.getLocalizedMessage}" -} - case class QepDatabaseProblem(x:Exception) extends AbstractProblem(ProblemSources.Qep){ - override val summary = "A problem encountered while using a database." + override lazy val summary = "A problem encountered while using a database." - override val throwable = Some(x) + override lazy val throwable = Some(x) - override val description = x.getMessage + override lazy val description = x.getMessage } \ No newline at end of file diff --git a/qep/service/src/test/scala/net/shrine/qep/queries/QepQueryDbTest.scala b/qep/service/src/test/scala/net/shrine/qep/queries/QepQueryDbTest.scala index 8369d13d4..3bb0a2254 100644 --- a/qep/service/src/test/scala/net/shrine/qep/queries/QepQueryDbTest.scala +++ b/qep/service/src/test/scala/net/shrine/qep/queries/QepQueryDbTest.scala @@ -1,237 +1,237 @@ package net.shrine.qep.queries +import net.shrine.problem.TestProblem import net.shrine.protocol.QueryResult.StatusType -import net.shrine.protocol.{I2b2ResultEnvelope, DefaultBreakdownResultOutputTypes, QueryResult, ResultOutputType} -import net.shrine.util.{XmlDateHelper, ShouldMatchersForJUnit} +import net.shrine.protocol.{DefaultBreakdownResultOutputTypes, I2b2ResultEnvelope, QueryResult, ResultOutputType} +import net.shrine.util.{ShouldMatchersForJUnit, XmlDateHelper} import org.junit.{After, Before, Test} -import net.shrine.problem.TestProblem /** * @author david * @since 1/20/16 */ class QepQueryDbTest extends ShouldMatchersForJUnit { val qepQuery = QepQuery( networkId = 1L, userName = "ben", userDomain = "testDomain", queryName = "testQuery", expression = Some("testExpression"), dateCreated = System.currentTimeMillis(), deleted = false, queryXml = "testXML", changeDate = System.currentTimeMillis() ) val secondQepQuery = QepQuery( networkId = 2L, userName = "dave", userDomain = "testDomain", queryName = "testQuery", expression = Some("testExpression"), deleted = false, dateCreated = System.currentTimeMillis(), queryXml = "testXML", changeDate = System.currentTimeMillis() ) val flag = QepQueryFlag( networkQueryId = 1L, flagged = true, flagMessage = "This query is flagged", changeDate = System.currentTimeMillis() ) @Test def testInsertQepQuery() { QepQueryDb.db.insertQepQuery(qepQuery) QepQueryDb.db.insertQepQuery(secondQepQuery) val results = QepQueryDb.db.selectAllQepQueries results should equal(Seq(qepQuery,secondQepQuery)) } @Test def testSelectQepQueriesForUser() { QepQueryDb.db.insertQepQuery(qepQuery) QepQueryDb.db.insertQepQuery(secondQepQuery) val results = QepQueryDb.db.selectPreviousQueriesByUserAndDomain("ben","testDomain",100) results should equal(Seq(qepQuery)) } @Test def testSelectQueryFlags() { val results1 = QepQueryDb.db.selectMostRecentQepQueryFlagsFor(Set(1L,2L)) results1 should equal(Map.empty) QepQueryDb.db.insertQepQueryFlag(flag) val results2 = QepQueryDb.db.selectMostRecentQepQueryFlagsFor(Set(1L,2L)) results2 should equal(Map(1L -> flag)) } val qepResultRowFromExampleCom = QueryResultRow( resultId = 10L, networkQueryId = 1L, instanceId = 100L, adapterNode = "example.com", resultType = Some(ResultOutputType.PATIENT_COUNT_XML), size = 30L, startDate = Some(System.currentTimeMillis() - 60), endDate = Some(System.currentTimeMillis() - 30), status = QueryResult.StatusType.Finished, statusMessage = None, changeDate = System.currentTimeMillis() ) @Test def testInsertQueryResultRow() { QepQueryDb.db.insertQepResultRow(qepResultRowFromExampleCom) val results = QepQueryDb.db.selectMostRecentQepResultRowsFor(1L) results should equal(Seq(qepResultRowFromExampleCom)) } val queryResult = QueryResult( resultId = 20L, instanceId = 200L, resultType = Some(ResultOutputType.PATIENT_COUNT_XML), setSize = 2000L, startDate = Some(XmlDateHelper.now), endDate = Some(XmlDateHelper.now), description = Some("example.com"), statusType = StatusType.Finished, statusMessage = None ) @Test def testInsertQueryResult(): Unit = { QepQueryDb.db.insertQueryResult(2L,queryResult) val results = QepQueryDb.db.selectMostRecentQepResultsFor(2L) results should equal(Seq(queryResult)) } val qepResultRowFromExampleComInThePast = QueryResultRow( resultId = 8L, networkQueryId = 1L, instanceId = 100L, adapterNode = "example.com", resultType = Some(ResultOutputType.PATIENT_COUNT_XML), size = 0L, startDate = qepResultRowFromExampleCom.startDate, endDate = None, status = QueryResult.StatusType.Processing, statusMessage = None, changeDate = qepResultRowFromExampleCom.changeDate - 40 ) val qepResultRowFromGeneralHospital = QueryResultRow( resultId = 100L, networkQueryId = 1L, instanceId = 100L, adapterNode = "generalhospital.org", resultType = Some(ResultOutputType.PATIENT_COUNT_XML), size = 100L, startDate = Some(System.currentTimeMillis() - 60), endDate = Some(System.currentTimeMillis() - 30), status = QueryResult.StatusType.Finished, statusMessage = None, changeDate = System.currentTimeMillis() ) @Test def testGetMostRecentResultRows() { QepQueryDb.db.insertQepResultRow(qepResultRowFromExampleComInThePast) QepQueryDb.db.insertQepResultRow(qepResultRowFromGeneralHospital) QepQueryDb.db.insertQepResultRow(qepResultRowFromExampleCom) val results = QepQueryDb.db.selectMostRecentQepResultRowsFor(1L) results.to[Set] should equal(Set(qepResultRowFromExampleCom,qepResultRowFromGeneralHospital)) } val maleRow = QepQueryBreakdownResultsRow( networkQueryId = 1L, adapterNode = "example.com", resultId = 100L, resultType = DefaultBreakdownResultOutputTypes.PATIENT_GENDER_COUNT_XML, dataKey = "male", value = 388, changeDate = System.currentTimeMillis() ) val femaleRow = QepQueryBreakdownResultsRow( networkQueryId = 1L, adapterNode = "example.com", resultId = 100L, resultType = DefaultBreakdownResultOutputTypes.PATIENT_GENDER_COUNT_XML, dataKey = "female", value = 390, changeDate = System.currentTimeMillis() ) val unknownRow = QepQueryBreakdownResultsRow( networkQueryId = 1L, adapterNode = "example.com", resultId = 100L, resultType = DefaultBreakdownResultOutputTypes.PATIENT_GENDER_COUNT_XML, dataKey = "unknown", value = 4, changeDate = System.currentTimeMillis() ) @Test def testInsertBreakdownRows(): Unit = { QepQueryDb.db.insertQueryBreakdown(maleRow) QepQueryDb.db.insertQueryBreakdown(femaleRow) QepQueryDb.db.insertQueryBreakdown(unknownRow) val results = QepQueryDb.db.selectAllBreakdownResultsRows results.to[Set] should equal(Set(maleRow,femaleRow,unknownRow)) } val breakdowns = Map(DefaultBreakdownResultOutputTypes.PATIENT_GENDER_COUNT_XML -> I2b2ResultEnvelope(DefaultBreakdownResultOutputTypes.PATIENT_GENDER_COUNT_XML,Map("male" -> 3000,"female" -> 4000,"unknown" -> 234))) @Test def testInsertQueryResultWithBreakdowns(): Unit = { val queryResultWithBreakdowns = queryResult.copy(breakdowns = breakdowns) QepQueryDb.db.insertQueryResult(2L,queryResultWithBreakdowns) val results = QepQueryDb.db.selectMostRecentQepResultsFor(2L) results should equal(Seq(queryResultWithBreakdowns)) } @Test def testInsertQueryResultWithProblem(): Unit = { val queryResultWithProblem = queryResult.copy(statusType = StatusType.Error,problemDigest = Some(TestProblem().toDigest)) QepQueryDb.db.insertQueryResult(2L,queryResultWithProblem) val results = QepQueryDb.db.selectMostRecentQepResultsFor(2L) results should equal(Seq(queryResultWithProblem)) } @Before def beforeEach() = { QepQueryDb.db.createTables() } @After def afterEach() = { QepQueryDb.db.dropTables() } } diff --git a/shrine-webclient/src/main/html/js-i2b2/cells/SHRINE/EnhancedError.js b/shrine-webclient/src/main/html/js-i2b2/cells/SHRINE/EnhancedError.js index 8c0e730d2..8991f1a12 100644 --- a/shrine-webclient/src/main/html/js-i2b2/cells/SHRINE/EnhancedError.js +++ b/shrine-webclient/src/main/html/js-i2b2/cells/SHRINE/EnhancedError.js @@ -1,349 +1,354 @@ /** * Created by ben on 10/13/15. */ var $hrine = window.$hrine = {}; $hrine.EnhancedError = (function(){ var EnhancedError = {}, config = config || { //@TODO: }; /** * * @returns {{}} */ function simulateI2b2Obj() { - var self = {}, - errorObject = { - summary: "SHRINE Failed to Start", - description: "The SHRINE software is not running at the queried" + - " site. This error must be corrected at the queried site.Check network status or contact your local SHRINE administrator. For faster assistance, expand this window and provide all text below this line to your local SHRINE administrator.", - details: "There is a fatal syntax error in the remote site's" + - " shrine.conf or another .conf file. The remote site admin should check to make sure that there are no stray/missing quotes or brackets, and that URLs are entered correctly.", - codec: "" - }; + var self = {}; + var errorObject = { + summary: "SHRINE Failed to Start", + + description: "The SHRINE software is not running at the queried site. This error must be corrected at the queried site." + + "Check network status or contact your local SHRINE administrator. " + + "For faster assistance, expand this window and provide all text below this line to your local SHRINE administrator.", + + details: "There is a fatal syntax error in the remote site's shrine.conf or another .conf file. " + + "The remote site admin should check to make sure that there are no stray/missing quotes or brackets, " + + "and that URLs are entered correctly.", + + codec: "" + }; self.errorObject = errorObject; self.dispDIV = document.getElementById('infoQueryStatusText'); self.dispDIV.innerHTML = '
SHRINE Critical Error
'; //which hospital self.dispDIV.innerHTML += '

Error Summary:
'; self.dispDIV.innerHTML += "" + "" + errorObject.summary + ""; return self; } /** * Scope for error dialog. */ EnhancedError.createErrorDialogue = function (container, errorObjects) { var anchors, btnExpand, btnContract, errorData, i2b2Obj; //default error. if(!container || !errorObjects) { i2b2Obj = simulateI2b2Obj(); container = i2b2Obj.dispDIV; errorObjects = [i2b2Obj.errorObject]; } //this sets up the events. anchors = container.getElementsByClassName('query-error-anchor'); //something's wrong captain, abandon ship! if(!anchors.length|| !errorObjects.length) { return; } addAnchorEvents(); function expandErrorDetailDiv (ev) { var errorDetailDiv = $('errorDetailDiv'); btnExpand.style.display = 'none'; btnContract.style.display = 'inline'; errorDetailDiv.innerHTML = getExpandedHtml(); } function retractErrorDetailDiv (ev) { var errorDetailDiv = $('errorDetailDiv'); btnExpand.style.display = 'inline'; btnContract.style.display = 'none'; errorDetailDiv.innerHTML = getRetractedHtml(); } function onClick(evt) { //ie logic. var currentTarget = (evt.currentTarget !== undefined)? evt.currentTarget : evt.srcElement.parentElement.parentElement; errorData = currentTarget.__errorData__; btnExpand = document.getElementById('btnExpandErrorDetail'); btnContract = document.getElementById('btnContractErrorDetail'); // -- add event listeners for expand and contract as well --// addEventListener(btnExpand, 'click', expandErrorDetailDiv, false); addEventListener(btnContract,'click', retractErrorDetailDiv, false); showErrorDetail(errorData); } /** * * @param errorData * @returns {string} */ function getRetractedHtml () { - var wikiBaseUrl = (i2b2.hive.cfg.wikiBaseUrl || 'https://open.med.harvard.edu/wiki/display/SHRINE/'); + var wikiBaseUrl = (cei2b2.hive.cfg.wikiBaseUrl || 'https://open.med.harvard.edu/wiki/display/SHRINE/'); if(wikiBaseUrl.lastIndexOf('/') !== wikiBaseUrl.length -1){ wikiBaseUrl += '/'; } var retractedHtml = '
Summary:
'+ '
' + errorData.summary + '

' + '
Description:
'+ '
' + errorData.description + '

' + '
For information on troubleshooting and resolution, check' + ' the SHRINE Error' + ' Codex.
'; return retractedHtml; } /** * * @param errorData * @returns {string} */ function getExpandedHtml () { var expandedHtml = getRetractedHtml() + '
' + '
Copy the text below and paste it in an email to your site administrator for a faster response.
' + '
' + '
Technical Details:
' + errorData.details + '

' + '
Codec:
' + errorData.codec + '

' + '
Stamp:
' + errorData.stamp + '

' + '
Stack Trace Name:
' + errorData.exception.name + '

' + '
Stack Trace Message:
' + errorData.exception.message + '

' + '
Stack Trace Details:
' + errorData.exception.stackTrace + '

'; return expandedHtml; } /** * * @param detailObj */ function showErrorDetail(detailObj) { var handleCancel = function() { this.cancel(); removeAllEvents(); retractErrorDetailDiv(); } var dialogErrorDetail = new YAHOO.widget.SimpleDialog("dialogErrorDetail", { width: "820px", fixedcenter: true, constraintoviewport: true, modal: true, zindex: 700, buttons: [ { text: "Done", handler: handleCancel, isDefault: true }] }); dialogErrorDetail._doClose = function (e) { this.cancel(); removeAllEvents(); retractErrorDetailDiv(); } $('dialogErrorDetail').show(); dialogErrorDetail.validate = function(){ return true; }; dialogErrorDetail.render(document.body); // / display the dialoge dialogErrorDetail.center(); dialogErrorDetail.show(); $('errorDetailDiv').innerHTML = getRetractedHtml(); } function addAnchorEvents () { var el, length = anchors.length; // -- will need to iterate over these once they are created and add event listeners. for(var i = 0; i < length; i ++) { var el = anchors[i]; el.__errorData__ = errorObjects[i]; addEventListener(el, 'click', onClick, false); } } function removeAllEvents () { removeEventListener(btnExpand, 'click', expandErrorDetailDiv); removeEventListener(btnContract, 'click', retractErrorDetailDiv); } } /** * Parse problem node. * @param qriNode * @returns {{exception: {}}} */ EnhancedError.parseProblem = function (qriNode) { - var details, - problem = { + var details; + var problem = { exception: {} - }; + }; problem.codec = grabXmlNodeData(qriNode, 'descendant-or-self::query_status_type/problem/codec'); problem.summary = grabXmlNodeData(qriNode, 'descendant-or-self::query_status_type/problem/summary'); problem.description = grabXmlNodeData(qriNode, 'descendant-or-self::query_status_type/problem/description'); problem.stamp = grabXmlNodeData(qriNode, 'descendant-or-self::query_status_type/problem/stamp'); //unescape embedded html. details = i2b2.h.XPath(qriNode, 'descendant-or-self::query_status_type/problem/details') //funky stuff goin' on...get outta here! if(!details.length) { problem.exception.name = problem.exception.message = problem.stackTrace = problem.codec = problem.summary = 'An unexpected error has occurred.'; } //error format as expected. else{ var innerHTML = (details[0].xml !== undefined)? details[0].xml : details[0].innerHTML?details[0].innerHTML:null; if(!innerHTML) { innerHTML = jQuery(details[0]).text(); } problem.details = innerHTML.unescapeHTML().replace(/(<([^>]+)>)/ig,""); problem.exception.name = grabXmlNodeData(qriNode, 'descendant-or-self::query_status_type/problem/details/exception/name'); problem.exception.message = grabXmlNodeData(qriNode, 'descendant-or-self::query_status_type/problem/details/exception/message'); problem.exception.stackTrace = parseErrorException(qriNode); } return problem; } /** * Replace all and with
tags. * @param node * @returns {*} */ function parseErrorException(node) { var innerHTML = (node.xml !== undefined)? node.xml : node.innerHTML?node.innerHTML:null;; if(!innerHTML) { innerHTML = jQuery(node).text(); } //no exception, abandon ship! if(innerHTML.indexOf('') == -1){ return ''; } var content, startIdx, endIdx; //fish out the problem section. content = innerHTML.split('') .join() .split('') .join(); //fish out the first stack trace. startIdx = content.indexOf('') + 12; endIdx = content.indexOf(''); content = content.substring(startIdx, endIdx); //remove all line tags and replace with line break. content = content.split('') .join('
') .split('
') .join() //remove all exception tags .split('') .join('
') .split('
') .join() //remove all stacktrace tags .split('') .join('
') .split('
') .join() //remove all message tags. .split('') .join('
') .split('
') .join(); return content; } /** * Grab data for node, return empty string if none. * @param node * @param xPathString * @returns {string} */ function grabXmlNodeData(node, xPathString){ var nodeVal = i2b2.h.XPath(node, xPathString); return (nodeVal.length)? nodeVal[0].firstChild.nodeValue : ''; } /** * * @param el * @param event * @param callback */ function addEventListener(el, event, callback) { if(el.addEventListener !== undefined) { el.addEventListener(event, callback, false); } else { el.attachEvent('on' + event, callback) } } /** * * @param el * @param event * @param callback */ function removeEventListener(el, event, callback) { if(el.removeEventListener !== undefined) { el.removeEventListener(event, callback); } else { el.detachEvent('on' + event, callback); } } return EnhancedError; })(); \ No newline at end of file diff --git a/tools/adapter-queries-to-qep/show.db b/tools/adapter-queries-to-qep/show.db new file mode 100644 index 000000000..767c0794a Binary files /dev/null and b/tools/adapter-queries-to-qep/show.db differ