Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
...
  • 13 commits
  • 10 files changed
  • 0 commit comments
  • 2 contributors
View
5 CHANGELOG
@@ -1,8 +1,11 @@
+Version 0.2.0 (2012-04-14)
+--------------------------
+Decoupled JAXB & Jackson (un)marshallers
+
Version 0.1.1 (2012-04-10)
--------------------------
Changed org to co.orderly
-
Version 0.1.0 (2012-04-09)
--------------------------
First public release
View
2 project/BuildSettings.scala
@@ -17,7 +17,7 @@ object BuildSettings {
lazy val basicSettings = Seq[Setting[_]](
organization := "co.orderly",
- version := "0.1.1",
+ version := "0.2.0",
description := "Narcolepsy is a Scala framework for building typesafe clients for RESTful web services",
scalaVersion := "2.9.1",
scalacOptions := Seq("-deprecation", "-encoding", "utf8"),
View
7 src/main/scala/co/orderly/narcolepsy/Client.scala
@@ -15,6 +15,7 @@ package co.orderly.narcolepsy
// Orderly
import utils._
import adapters._
+import marshallers.{MultiMarshaller, MultiUnmarshaller}
/**
* Client is an abstract Narcolepsy class you can use to build a web services client
@@ -55,6 +56,12 @@ abstract class Client(
// Map resource slug names against the Representation subclasses required by this RESTful API
protected val resources: Api
+ // The marshaller
+ val marshaller: MultiMarshaller
+
+ // The unmarshaller
+ val unmarshaller: MultiUnmarshaller
+
// -------------------------------------------------------------------------------------------------------------------
// You can override the following defaults in your NarcolepsyClient if you want
// -------------------------------------------------------------------------------------------------------------------
View
35 src/main/scala/co/orderly/narcolepsy/Query.scala
@@ -19,10 +19,6 @@ import java.util.UUID
import adapters._
import utils._
-// TODO: remove these when we decouple specific unmarshallers from the Query engine
-import marshallers.jackson.UnmarshalJson
-import marshallers.jaxb.UnmarshalXml
-
/**
* Query is a fluent interface for constructing a call (GET, POST, DELETE, PUT or
* similar) to a RESTful web service. It is typed so that the representations
@@ -46,6 +42,8 @@ abstract class Query[
protected var payload: Option[String] = None
+ protected val _client: Client = client // Because can't explicit self type on a class constructor arg
+
protected var id: Option[String] = None
protected var params: Option[RestfulParams] = None
@@ -133,13 +131,13 @@ abstract class Query[
if (RestfulHelpers.isError(code)) {
Left(RestfulError(code, body, null)) // TODO: add unmarshalling of errors in here
} else {
- Right(body map( b =>
- client.configuration.contentType match {
+ Right(body map( r => _client.unmarshaller.toRepresentation(client.configuration.contentType, r, typeR)))
+ // TODO: pass in client.configuration.contentType
- case "application/json" => UnmarshalJson(b, true).toRepresentation[R](typeR) // TODO: remove rootKey bool
- case "text/xml" => UnmarshalXml(b).toRepresentation[R](typeR)
- case _ => throw new ClientConfigurationException("Narcolepsy can only unmarshal JSON and XML currently, not %s".format(client.configuration.contentType))
- }))
+ // case "application/json" => null // UnmarshalJson(b, true).toRepresentation[R](typeR) // TODO: remove rootKey bool
+ // case "text/xml" => null // UnmarshalXml(b).toRepresentation[R](typeR)
+ // case _ => throw new ClientConfigurationException("Narcolepsy can only unmarshal JSON and XML currently, not %s".format(client.configuration.contentType))
+ // }))
}
}
}
@@ -153,13 +151,22 @@ abstract class Query[
* request. Typically used by PUT and POST requests.
*/
// TODO: need to update this so that payload can be typed
-trait Payload {
+trait Payload[R <: Representation] {
// Grab _payload from Query
self: {
var payload: Option[String]
+ val _client: Client
} =>
+ def addPayload(representation: R): this.type = {
+ this.payload = Option(_client.marshaller.fromRepresentation(
+ _client.configuration.contentType,
+ representation)
+ )
+ this
+ }
+
def addPayload(payload: String): this.type = {
this.payload = Option(payload)
this
@@ -173,7 +180,7 @@ trait Payload {
*/
trait Id {
- // Grab _id from Query
+ // Grab id from Query
self: {
var id: Option[String]
} =>
@@ -258,7 +265,7 @@ class DeleteQuery(client: Client, resource: String)
class PutQuery[R <: Representation](client: Client, resource: String, typeR: Class[R])
extends Query[R](PutMethod, client, resource, typeR)
with Id
- with Payload
+ with Payload[R]
/**
* PostQuery is for performing a POST on a resource. This is typically used for creating
@@ -266,7 +273,7 @@ class PutQuery[R <: Representation](client: Client, resource: String, typeR: Cla
*/
class PostQuery[R <: Representation](client: Client, resource: String, typeR: Class[R])
extends Query[R](PostMethod, client, resource, typeR)
- with Payload
+ with Payload[R]
// TODO: add HeadQuery
View
7 src/main/scala/co/orderly/narcolepsy/Representation.scala
@@ -26,12 +26,7 @@ import marshallers.jaxb.JaxbMarshaller
* Scala class that has been marshalled from XML/JSON/whatever by JAXB, Jackson
* or similar.
*/
-// TODO: I have added Jaxb and Jackson support back in via extends.
-// TODO: In the future this should be decoupled (a Representation
-// TODO: should be marshallable by any given Marshaller technology
-// TODO: - this can be done by making an implicit conversion
-// TODO: available at the correct point.
-trait Representation extends JaxbMarshaller with JacksonMarshaller
+trait Representation
/**
* ErrorRepresentation is the parent trait for all representations which
View
20 src/main/scala/co/orderly/narcolepsy/marshallers/Marshaller.scala
@@ -14,13 +14,25 @@ package co.orderly.narcolepsy
package marshallers
/**
- * Empty trait for now, but extended by any format-specific marshaller
+ * Parent marshalling trait, extended by any format-specific marshaller
*/
trait Marshaller {
/**
- * Abstract method to marshal a given representation
+ * Abstract method to marshal a given representation into a string
*/
- // TODO: potentially bring this back in due course
- // def marshal(): String
+ def fromRepresentation[R <: Representation](representation: R): String
+}
+
+/**
+ * A MultiMarshaller can choose between different marshallers based on
+ * the supplied content type.
+ */
+trait MultiMarshaller {
+
+ /**
+ * Abstract method to marshal a given representation into a string,
+ * based on the supplied content type.
+ */
+ def fromRepresentation[R <: Representation](contentType: String, representation: R): String
}
View
25 src/main/scala/co/orderly/narcolepsy/marshallers/Unmarshaller.scala
@@ -21,8 +21,8 @@ trait Unmarshaller {
* Example usage;
* val order = UnmarshalXml(xml).toRepresentation[Order]
*/
- def toRepresentation[T <: Representation](implicit m: Manifest[T]): T =
- toRepresentation[T](m.erasure.asInstanceOf[Class[T]])
+ def toRepresentation[R <: Representation](implicit m: Manifest[R], marshalled: String): R =
+ toRepresentation[R](marshalled, m.erasure.asInstanceOf[Class[R]])
/**
* Turns the case class's xml into a Representation subclass - use
@@ -33,5 +33,24 @@ trait Unmarshaller {
* (where you have grabbed and stored typeOfT using another
* implicit Manifest at the point of declaring T.
*/
- def toRepresentation[T <: Representation](typeT: Class[T]): T
+ def toRepresentation[R <: Representation](marshalled: String, typeR: Class[R]): R
+}
+
+/**
+ * A MultiUnmarshaller can choose between different unmarshallers based on
+ * the supplied content type.
+ */
+trait MultiUnmarshaller {
+
+ /**
+ * Abstract method to unmarshal a string into a representation,
+ * based on the supplied content type.
+ */
+ def toRepresentation[R <: Representation](implicit m: Manifest[R], contentType: String, marshalled: String): R =
+ toRepresentation[R](contentType, marshalled, m.erasure.asInstanceOf[Class[R]])
+
+ /**
+ * See equivalent definition in Unmarshaller above.
+ */
+ def toRepresentation[R <: Representation](contentType: String, marshalled: String, typeR: Class[R]): R
}
View
130 src/main/scala/co/orderly/narcolepsy/marshallers/jackson/Jackson.scala
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2012 Orderly Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package co.orderly.narcolepsy
+package marshallers
+package jackson
+
+// Java
+import java.text.SimpleDateFormat
+
+// Jackson
+import org.codehaus.jackson.map._
+import org.codehaus.jackson.map.introspect._
+import org.codehaus.jackson.xc._
+
+/**
+ * Specify the different strategies for including a root value with a given representation.
+ */
+object RootValueStrategy extends Enumeration {
+ type RootValueStrategy = Value
+ val All, NotWrappers, None = Value
+}
+import RootValueStrategy._
+
+/**
+ * JacksonConfiguration allows the Jackson marshalling and unmarshalling
+ * to be tweaked/customized for a given API.
+ */
+case class JacksonConfiguration(dateFormat: SimpleDateFormat, // new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
+ rootValueStrategy: RootValueStrategy,
+ propertyNamingStrategy: PropertyNamingStrategy) // new PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy() // Translates typical camel case Java property names to lower case JSON element names, separated by underscore
+
+/**
+ * Mini-DSL to unmarshal a JSON string into a Representation.
+ *
+ * Design as per Neil Essy's answer on:
+ * http://stackoverflow.com/questions/8162345/how-do-i-create-a-class-hierarchy-of-typed-factory-method-constructors-and-acces
+ */
+case class JacksonUnmarshaller(conf: JacksonConfiguration) extends Unmarshaller with JacksonHelpers {
+
+ def toRepresentation[R <: Representation](marshalled: String, typeR: Class[R]): R = {
+
+ val (mapper, ai) = createObjectMapperAndIntrospector(conf)
+
+ mapper.getDeserializationConfig().withAnnotationIntrospector(ai)
+ mapper.getDeserializationConfig().setDateFormat(conf.dateFormat) // TODO: setDateFormat has been deprecated
+
+ // Whether or not to add a root key aka "top level segment" when (un)marshalling JSON, as
+ // per http://stackoverflow.com/questions/5728276/jackson-json-top-level-segment-inclusion
+ // Unmarshalling only
+ mapper.configure(DeserializationConfig.Feature.UNWRAP_ROOT_VALUE, unwrapRootValue(conf.rootValueStrategy, typeR))
+
+ // Return the representation
+ mapper.readValue(marshalled, typeR).asInstanceOf[R]
+ }
+}
+
+/**
+ * Case class mini-DSL for marshalling via Jackson.
+ */
+case class JacksonMarshaller(conf: JacksonConfiguration) extends Marshaller with JacksonHelpers {
+
+ /**
+ * Marshals this representation into JSON via Jackson
+ */
+ def fromRepresentation[R <: Representation](representation: R): String = {
+
+ val (mapper, ai) = createObjectMapperAndIntrospector(conf)
+
+ mapper.getSerializationConfig().withAnnotationIntrospector(ai)
+ mapper.getSerializationConfig().setDateFormat(conf.dateFormat) // TODO: setDateFormat has been deprecated
+
+ // Return a pretty printed String
+ val writer = mapper.defaultPrettyPrintingWriter // Deprecated, replace
+ writer.writeValueAsString(this)
+ }
+}
+
+/**
+ * Helpers used by Jackson for both marshalling and unmarshalling.
+ */
+trait JacksonHelpers {
+
+ /**
+ * Factory to create and configure a Jackson ObjectMapper based on
+ * the supplied configuration. Same for marshalling and
+ * unmarshalling.
+ */
+ def createObjectMapperAndIntrospector(conf: JacksonConfiguration): (ObjectMapper, AnnotationIntrospector) = {
+
+ val mapper = new ObjectMapper()
+
+ // How to name the properties (e.g. lower case with underscores)
+ mapper.setPropertyNamingStrategy(conf.propertyNamingStrategy)
+
+ // Use Jackson annotations first then fall back on JAXB annotations
+ // TODO: make this into a FallbackStrategy
+ val introspectorPair = new AnnotationIntrospector.Pair(
+ new JacksonAnnotationIntrospector(),
+ new JaxbAnnotationIntrospector()
+ )
+
+ (mapper, introspectorPair) // Return the tuple
+ }
+
+ /**
+ * Whether to set unwrap root value to true or false
+ */
+ def unwrapRootValue[R <: Representation](rvs: RootValueStrategy, typeR: Class[R]): Boolean = rvs match {
+ case All => true
+ case None => false
+ case NotWrappers => !isWrapper(typeR)
+ }
+
+ /**
+ * Uses reflection to determine whether a given reified type subclasses
+ * RepresentationWrapper or not. Used to help determine whether Jackson
+ * should be setting a root key or not.
+ */
+ private def isWrapper(typeR: Class[_]) = classOf[RepresentationWrapper[_]].isAssignableFrom(typeR)
+}
View
127 src/main/scala/co/orderly/narcolepsy/marshallers/jackson/Json.scala
@@ -1,127 +0,0 @@
-/*
- * Copyright (c) 2012 Orderly Ltd. All rights reserved.
- *
- * This program is licensed to you under the Apache License Version 2.0,
- * and you may not use this file except in compliance with the Apache License Version 2.0.
- * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the Apache License Version 2.0 is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
- */
-package co.orderly.narcolepsy
-package marshallers
-package jackson
-
-// Java
-import java.text.SimpleDateFormat
-
-// Jackson
-import org.codehaus.jackson.map._
-import org.codehaus.jackson.map.introspect._
-import org.codehaus.jackson.xc._
-
-/**
- * Mini-DSL to unmarshal a JSON string into a Representation.
- *
- * Design as per Neil Essy's answer on:
- * http://stackoverflow.com/questions/8162345/how-do-i-create-a-class-hierarchy-of-typed-factory-method-constructors-and-acces
- */
-case class UnmarshalJson(json: String, rootKey: Boolean = false) extends Unmarshaller with JacksonHelpers {
-
- def toRepresentation[T <: Representation](typeT: Class[T]): T = {
-
- // Define the Jackson mapper and configure it
- val mapper = new ObjectMapper()
-
- // TODO: turn these into JacksonConfiguration-based or similar
- mapper.configure(DeserializationConfig.Feature.UNWRAP_ROOT_VALUE, (rootKey && (!isWrapper(typeT))))
- // mapper.getDeserializationConfig().setDateFormat(getDateFormat)
-
- // Translates typical camel case Java property names to lower case JSON element names, separated by underscore
- mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy())
-
- // Use Jackson annotations first then fall back on JAXB annotations
- val introspectorPair = new AnnotationIntrospector.Pair(
- new JacksonAnnotationIntrospector(),
- new JaxbAnnotationIntrospector()
- )
- mapper.getDeserializationConfig().withAnnotationIntrospector(introspectorPair)
-
- // Return the representation
- mapper.readValue(json, typeT).asInstanceOf[T]
- }
-}
-
-trait JacksonMarshaller extends Marshaller with JacksonHelpers {
-
- /**
- * Marshals this representation into JSON via Jackson
- * (using Jackson / JAXB annotations)
- */
- // TODO: rename this back to marshal() when it's no longer attached to all Representations
- def marshalToJson(): String = {
-
- // Define the Jackson mapper and configure it
- val mapper = new ObjectMapper()
-
- // TODO: we need to inject a JacksonConfiguration into this OR make it easy to override JacksonMarshaller
- // TODO in an individual Narcolepsy client
- // mapper.configure(SerializationConfig.Feature.WRAP_ROOT_VALUE, needRootKey(this))
- // mapper.getSerializationConfig().setDateFormat(getDateFormat)
-
- // Translates typical camel case Java property names to lower case JSON element names, separated by underscore
- mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy())
-
- // Use Jackson annotations first then fall back on JAXB annotations
- val introspectorPair = new AnnotationIntrospector.Pair(
- new JacksonAnnotationIntrospector(),
- new JaxbAnnotationIntrospector()
- )
- mapper.getSerializationConfig().withAnnotationIntrospector(introspectorPair)
-
- val writer = mapper.defaultPrettyPrintingWriter
- writer.writeValueAsString(this)
- }
-}
-
-/**
- * Helpers used by Jackson for both marshalling and unmarshalling.
- */
-trait JacksonHelpers {
-
- /**
- * Uses reflection to determine whether a given reified type subclasses
- * RepresentationWrapper or not. Used to help determine whether Jackson
- * should be setting a root key or not.
- */
- protected def isWrapper(typeR: Class[_]) = classOf[RepresentationWrapper[_]].isAssignableFrom(typeR)
-}
-
-/* Archive of JSONy unmarshalling stuff
-
-
- // -------------------------------------------------------------------------------------------------------------------
- // Helper methods
- // -------------------------------------------------------------------------------------------------------------------
-
- // TODO: check if this is is still used
- // TODO: make this a JSON configuration parameter
- /**
- * Whether or not to add a root key aka "top level segment" when (un)marshalling JSON, as
- * per http://stackoverflow.com/questions/5728276/jackson-json-top-level-segment-inclusion
- */
- def needRootKey(obj: Any) = obj match {
- case o:RepresentationWrapper[_] => false // Don't include as we get the root key for free with a wrapper
- case _ => true // Yes include a root key
- }
-
- // TODO: make this a JSON configuration parameter to the client (along with needRootKey)
- /**
- * Standardise the date format to use for (un)marshalling
- */
- def getDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
-
-
-*/
View
42 ...rly/narcolepsy/marshallers/jaxb/Xml.scala → ...ly/narcolepsy/marshallers/jaxb/Jaxb.scala
@@ -30,45 +30,57 @@ import javax.xml.bind.JAXBContext
// Narcolepsy
import namespaces.NonNamespacedXmlStreamWriter
-case class UnmarshalXml(xml: String) extends Unmarshaller {
+/**
+ * JaxbConfiguration allows the Jaxb marshalling and unmarshalling
+ * to be tweaked/customized for a given API.
+ */
+case class JaxbConfiguration(namespaced: Boolean)
+
+/**
+ * Case class mini-DSL for unmarshalling via JAXB.
+ *
+ * Design as per Neil Essy's answer on:
+ * http://stackoverflow.com/questions/8162345/how-do-i-create-a-class-hierarchy-of-typed-factory-method-constructors-and-acces
+ */
+case class JaxbUnmarshaller(conf: JaxbConfiguration) extends Unmarshaller {
/**
* Turns the case class's xml into a Representation subclass - use
* this form with an abstract Representation type, like so:
*
- * val order = UnmarshalXml(xml).toRepresentation[T](typeOfT)
+ * val order = UnmarshalXml(xml).toRepresentation[R](typeOfR)
*
- * (where you have grabbed and stored typeOfT using another
- * implicit Manifest at the point of declaring T.
+ * (where you have grabbed and stored typeOfR using another
+ * implicit Manifest at the point of declaring R.
*/
- def toRepresentation[T <: Representation](typeT: Class[T]): T = {
+ def toRepresentation[R <: Representation](marshalled: String, typeR: Class[R]): R = {
- // TODO: need to add non-namespaced support in (although not sure
- // it is strictly necessary - seems to work fine without)
+ // TODO: need to add non-namespaced support in (although not sure it is strictly necessary - seems to work fine without)
- val context = JAXBContext.newInstance(typeT)
+ val context = JAXBContext.newInstance(typeR)
val unmarshaller = context.createUnmarshaller()
unmarshaller.unmarshal(
- new StringReader(xml)
- ).asInstanceOf[T]
+ new StringReader(marshalled)
+ ).asInstanceOf[R]
}
}
-trait JaxbMarshaller extends Marshaller {
+/**
+ * Case class mini-DSL for marshalling via JAXB.
+ */
+case class JaxbMarshaller(conf: JaxbConfiguration) extends Marshaller {
/**
* Marshals this representation into XML using JAXB
*/
- // TODO: rename this back to marshal() when it's no longer attached to all Representations
- // TODO: move namespaced into a separate configuration object or similar
- def marshalToXml(namespaced: Boolean = true): String = {
+ def fromRepresentation[R <: Representation](representation: R): String = {
val context = JAXBContext.newInstance(this.getClass())
val writer = new StringWriter
- if (namespaced) {
+ if (conf.namespaced) {
context.createMarshaller.marshal(this, writer)
} else { // Use the custom NonNamespacedXmlStreamWriter to produce XML without the namespace noise everywhere
val xof = XMLOutputFactory.newFactory()

No commit comments for this range

Something went wrong with that request. Please try again.