Skip to content

Commit

Permalink
Migrate to scalaxb for SOAP WSDL parsing
Browse files Browse the repository at this point in the history
As noted in #327,
it looks like`salesforce-message-handler` needs to drop sbt-cxf to work with
versions of Java newer than Java 8 (the JAXB library is no longer a core part
of Java).

`scalaxb` (https://github.com/eed3si9n/scalaxb) looks like a good replacement
for `sbt-cxf`/[`wsdl2java`](https://cxf.apache.org/docs/wsdl-to-java.html),
it's maintained and has Scala-specific support:

https://index.scala-lang.org/eed3si9n/scalaxb/artifacts/scalaxb

The generated classes created by `scalaxb` are pretty similar to the ones
generated by `wsdl2java`, there was just a small wrinkle with
nillable id fields, which was not too hard to cope with.
  • Loading branch information
rtyley committed Apr 24, 2024
1 parent 7180275 commit 224c958
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
role-to-assume: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }}

- name: Setup Java
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'corretto'
java-version: '8'
Expand Down
26 changes: 13 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,36 @@ description:= "handle outbound messages from salesforce to update zuora and iden

version := "1.0"

scalaVersion := "2.12.15"
scalaVersion := "2.12.19"

scalacOptions ++= Seq(
"-deprecation",
"-encoding", "UTF-8",
"-target:jvm-1.8",
"-release:8",
"-Ywarn-dead-code"
)

enablePlugins(ScalaxbPlugin)

Compile / scalaxb / scalaxbPackageName := "salesforce.soap"
Compile / scalaxb / scalaxbGenerateDispatchClient := false // we don't need to use the 'dispatch' library

val AwsVersion = "1.12.705"

libraryDependencies ++= Seq(
"com.amazonaws" % "aws-lambda-java-core" % "1.2.1",
"com.amazonaws" % "aws-java-sdk-sqs" % AwsVersion,
"com.amazonaws" % "aws-java-sdk-s3" % AwsVersion,
"org.scala-lang.modules" %% "scala-xml" % "2.2.0",
"org.scala-lang.modules" %% "scala-parser-combinators" % "2.4.0",
"com.typesafe" % "config" % "1.4.1",
"org.slf4j" % "slf4j-simple" % "1.7.35",
"com.typesafe.play" %% "play-json" % "2.9.2",
"org.scala-lang.modules" %% "scala-xml" % "2.0.1",
"org.specs2" %% "specs2-core" % "4.13.2" % "test",
"org.specs2" %% "specs2-matcher-extra" % "4.13.2" % "test",
"org.specs2" %% "specs2-mock" % "4.13.2" % "test",
"org.hamcrest" % "hamcrest-all" % "1.3" % "test"
"org.specs2" %% "specs2-core" % "4.13.2" % Test,
"org.specs2" %% "specs2-matcher-extra" % "4.13.2" % Test,
"org.specs2" %% "specs2-mock" % "4.13.2" % Test,
"org.hamcrest" % "hamcrest-all" % "1.3" % Test
)

/* required to bump jackson versions due to CVE-2020-36518 */
Expand All @@ -54,13 +61,6 @@ dependencyOverrides ++= jacksonDependencies
assembly / assemblyJarName := s"${name.value}.jar"
assembly / assemblyOutputPath := file(s"${(assembly/assemblyJarName).value}")

cxfTarget := (sourceManaged.value / "cxf" / "sfOutboundMessages" / "main")
cxfWsdls +=
Wsdl(
id = "",
wsdlFile = baseDirectory.value / "wsdl/salesforce-outbound-message.wsdl"
)

//Having a merge strategy here is necessary as there is an conflict in the file contents for the jackson libs, there are two same versions with different contents.
//As a result we're picking the first file found on the classpath - this may not be required if the contents match in a future release
assembly / assemblyMergeStrategy := {
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.5.8
sbt.version=1.9.9
4 changes: 1 addition & 3 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10")

addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.3")

addSbtPlugin("io.dapas" % "sbt-cxf" % "0.2.0")
addSbtPlugin("org.scalaxb" % "sbt-scalaxb" % "1.12.0")
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ package com.gu.salesforce.messageHandler
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.sqs.model.SendMessageResult
import com.gu.salesforce.messageHandler.APIGatewayResponse._
import com.sforce.soap._2005._09.outbound._
import play.api.libs.json.{ JsValue, Json }
import com.gu.salesforce.messageHandler.SOAPNotificationsParser.parseMessage
import salesforce.soap.ContactNotification
import play.api.libs.json.{JsValue, Json}

import java.io.{ ByteArrayInputStream, InputStream, OutputStream }
import javax.xml.bind.JAXBContext
import javax.xml.soap.MessageFactory
import scala.collection.JavaConverters._
import java.io.{InputStream, OutputStream}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{ Await, Future }
import scala.util.{ Failure, Try }
import scala.concurrent.{Await, Future}
import scala.util.{Failure, Try}

trait RealDependencies {
val queueClient = SqsClient
Expand All @@ -24,21 +22,6 @@ trait MessageHandler extends Logging {

val queueName = s"salesforce-outbound-messages-${Config.stage}"

def parseMessage(requestBody: String) = {
val is = new ByteArrayInputStream(requestBody.getBytes)
val messageFactory = MessageFactory.newInstance()
val soapMessage = messageFactory.createMessage(null, is)
val body = soapMessage.getSOAPBody
val jc = JAXBContext.newInstance(classOf[Notifications])
val unmarshaller = jc.createUnmarshaller()
val je = unmarshaller.unmarshal(body.extractContentAsDocument(), classOf[Notifications])
je.getValue()
}

case class QueueMessage(contactId: String)

implicit val messageFormat = Json.format[QueueMessage]

def credentialsAreValid(inputEvent: JsValue): Boolean = {

val maybeApiClientId = (inputEvent \ "queryStringParameters" \ "apiClientId").asOpt[String]
Expand All @@ -56,13 +39,13 @@ trait MessageHandler extends Logging {
}

def sendToQueue(notification: ContactNotification): Future[Try[SendMessageResult]] = {
val queueMessage = QueueMessage(notification.getSObject.getId)
val queueMessage = QueueMessage(notification.sObject.Id.get)
val queueMessageString = Json.prettyPrint(Json.toJson(queueMessage))
queueClient.send(queueName, queueMessageString)
}

def processNotifications(notifications: List[ContactNotification], outputStream: OutputStream) = {
val contactListStr = notifications.map(_.getSObject.getId).mkString(", ")
def processNotifications(notifications: Seq[ContactNotification], outputStream: OutputStream) = {
val contactListStr = notifications.flatMap(_.sObject.Id).mkString(", ")
logger.info(s"contacts found in salesforce xml: [$contactListStr]")
val FutureResponses = notifications.map(sendToQueue)
val future = Future.sequence(FutureResponses).map { responses =>
Expand Down Expand Up @@ -91,8 +74,8 @@ trait MessageHandler extends Logging {
logger.info("Authenticated request successfully...")
val body = (inputEvent \ "body").as[String]
val parsedMessage = parseMessage(body)
if (parsedMessage.getOrganizationId.startsWith(Config.salesforceOrganizationId)) {
processNotifications(asScalaBuffer(parsedMessage.getNotification).toList, outputStream)
if (parsedMessage.OrganizationId.startsWith(Config.salesforceOrganizationId)) {
processNotifications(parsedMessage.Notification, outputStream)
} else {
logger.info("Unexpected salesforce organization id in xml message")
outputForAPIGateway(outputStream, unauthorized)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gu.salesforce.messageHandler

import play.api.libs.json.{Format, Json}

case class QueueMessage(contactId: String)

object QueueMessage {
implicit val messageFormat: Format[QueueMessage] = Json.format[QueueMessage]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.gu.salesforce.messageHandler

import salesforce.soap.Notifications
import scalaxb._
import soapenvelope11.Envelope

import scala.xml.{Elem, XML}

object SOAPNotificationsParser {

/**
* Given a 'notifications' request from Salesforce (which comes embedded in a SOAP Envelope), extract the
* 'Notifications' object.
*
* https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_om_outboundmessaging_wsdl.htm
*/
def parseMessage(soapRequestEnvelope: String): Notifications = {
val envelope: Envelope = fromXML[soapenvelope11.Envelope](XML.loadString(soapRequestEnvelope))

val notificationsXml = envelope.Body.any.collect({
case DataRecord(_, _, x: Elem) if x.label != "Fault" => x
}).head

fromXML[Notifications](notificationsXml)
}
}
File renamed without changes.
18 changes: 18 additions & 0 deletions src/test/resources/soapNotificationsRequest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Body>
<notifications xmlns="http://soap.sforce.com/2005/09/outbound">
<OrganizationId>someOrganizationId</OrganizationId>
<ActionId>SomeActionId</ActionId>
<SessionId xsi:nil="true"/>
<EnterpriseUrl>someEnterpriseUrl</EnterpriseUrl>
<PartnerUrl>somePartnerUrl</PartnerUrl>
<Notification>
<Id>notificationID</Id>
<sObject xsi:type="sf:Contact" xmlns:sf="urn:sobject.enterprise.soap.sforce.com">
<sf:Id>aContactId</sf:Id>
</sObject>
</Notification>
</notifications>
</soapenv:Body>
</soapenv:Envelope>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.gu.salesforce.messageHandler

import com.amazonaws.util.IOUtils
import salesforce.soap.Notifications
import org.specs2.mutable.Specification

class SOAPNotificationsParserTest extends Specification {
def getTestString(fileName: String) = IOUtils.toString(getClass.getResourceAsStream(s"/$fileName"))

"Parser" should {
"parse a SOAP message" in {
val notifications: Notifications =
SOAPNotificationsParser.parseMessage(getTestString("soapNotificationsRequest.xml"))

notifications.Notification.head.sObject.Id must beSome("aContactId")
}
}
}

0 comments on commit 224c958

Please sign in to comment.