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 881253a93..99fe2869a 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,97 +1,96 @@ package net.shrine.adapter.client import java.net.SocketTimeoutException -import net.shrine.log.Loggable +import net.shrine.problem.{ProblemNotInCodec, ProblemSources, AbstractProblem} 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.XML import com.sun.jersey.api.client.ClientHandlerException -import com.sun.jersey.api.client.ClientResponse -import javax.ws.rs.core.MediaType -import net.shrine.client.JerseyHttpClient import net.shrine.client.TimeoutException -import net.shrine.crypto.TrustParam import net.shrine.protocol.BroadcastMessage import net.shrine.protocol.ErrorResponse import net.shrine.protocol.NodeId import net.shrine.protocol.Result -import net.shrine.util.XmlUtil import net.shrine.client.Poster -import scala.util.Try +import scala.util.{Failure, Success, Try} import net.shrine.protocol.ResultOutputType /** * @author clint - * @date Nov 15, 2013 + * @since Nov 15, 2013 * * */ final class RemoteAdapterClient private (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 } //TODO: Revisit this import scala.concurrent.ExecutionContext.Implicits.global override def query(request: BroadcastMessage): Future[Result] = { val requestXml = request.toXml Future { blocking { - val response = poster.post(requestXml.toString) + val response = poster.post(requestXml.toString()) 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)).getOrElse { - val errorResponse = ErrorResponse(s"Couldn't understand response from adapter at '${poster.url}': ${XmlUtil.prettyPrint(XML.loadString(responseXml))}") - - Result(NodeId.Unknown, 0.milliseconds, errorResponse) + Try(XML.loadString(responseXml)).flatMap(Result.fromXml(breakdownTypes)) match { + case Success(result) => result + case Failure(x) => { + val errorResponse = x match { + case _ => ErrorResponse(ProblemNotInCodec(s"Couldn't understand response from adapter at '${poster.url}': $responseXml", x)) + } + Result(NodeId.Unknown, 0.milliseconds, errorResponse) + } } } }.recover { case e if isTimeout(e) => throw new TimeoutException(s"Invoking adapter at ${poster.url} timed out", e) } } } object RemoteAdapterClient { def apply(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(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 } -} \ No newline at end of file +} diff --git a/commons/util/src/main/scala/net/shrine/problem/Problem.scala b/commons/util/src/main/scala/net/shrine/problem/Problem.scala index d5a822721..6152bc3cd 100644 --- a/commons/util/src/main/scala/net/shrine/problem/Problem.scala +++ b/commons/util/src/main/scala/net/shrine/problem/Problem.scala @@ -1,128 +1,129 @@ package net.shrine.problem import java.net.{InetAddress, ConnectException} import java.util.Date import net.shrine.log.Loggable import net.shrine.serialization.{XmlUnmarshaller, XmlMarshaller} import scala.xml.{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 { def summary:String def problemName = getClass.getName def throwable:Option[Throwable] = None def stamp:Stamp def description = s"${stamp.pretty}" - def throwableDetail = throwable.map(x => x.getStackTrace.mkString(sys.props("line.separator"))) + //todo stack trace as xml elements? would be easy + def throwableDetail = throwable.map(x => s"${x.getClass.getName} ${x.getMessage}\n${x.getStackTrace.mkString(sys.props("line.separator"))}") def details:String = s"${throwableDetail.getOrElse("")}" def toDigest:ProblemDigest = ProblemDigest(problemName,summary,description,details) } case class ProblemDigest(codec:String,summary:String,description:String,details:String) extends XmlMarshaller { override def toXml: Node = { {codec} {summary} {description}
{details}
} } object ProblemDigest extends XmlUnmarshaller[ProblemDigest] with Loggable { def apply(oldMessage:String):ProblemDigest = { val ex = new IllegalStateException(s"'$oldMessage' detected, not in codec. Please report this problem and stack trace to Shrine dev.") ex.fillInStackTrace() warn(ex) ProblemDigest("ProblemNotInCodec",oldMessage,"","") } 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 summary = extractText("summary") val description = extractText("description") val details = extractText("details") ProblemDigest(codec,summary,description,details) } } case class Stamp(host:InetAddress,time:Long,source:ProblemSources.ProblemSource) { def pretty = s"at ${new Date(time)} on $host ${source.pretty}" } object Stamp { def apply(source:ProblemSources.ProblemSource): Stamp = Stamp(InetAddress.getLocalHost,System.currentTimeMillis(),source) } abstract class AbstractProblem(source:ProblemSources.ProblemSource) extends Problem { val stamp = Stamp(source) } 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 { //todo name without $ def pretty = getClass.getSimpleName } 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 ProblemNotInCodec(summary:String,t:Throwable) extends AbstractProblem(ProblemSources.Unknown){ override val throwable = Some(t) override val description = s"${super.description} . This error is not yet in the codec. Please report the stack trace to the Shrine development team at TODO" } object ProblemNotInCodec { def apply(summary:String):ProblemNotInCodec = { val x = new IllegalStateException(s"$summary , is not yet in the codec.") x.fillInStackTrace() new ProblemNotInCodec(summary,x) } }