0

I have this setup where I want to populate an object fields from an XML NodeSeq. A cutdown version of my setup is shown. It works well, but when I put None in testObj, I get a java.util.NoSuchElementException: None.get.

The problem I believe is that I cannot find a way to get the class of the Option[_] when it is None. I've been stuck on this for ages and I cannot see a way out. I feel there must be a way to branch and populate the object field when the original value is None, but how?

import xml.NodeSeq
object test {
  case class TestObject(name: Option[String] = None, value: Option[Double] = None)
  def main(args: Array[String]): Unit = {
  val testObj = TestObject(Some("xxx"), Some(12.0))
//    val testObj = TestObject(None, Some(12.0))
    val nodeSeq = <test> <name> yyy </name> <value> 34.0 </value> </test>
    val result = getObject[TestObject](nodeSeq, Option(testObj))
    println("result = " + result) // result = Some(TestObject(Some(yyy),Some(34.0)))
  }
  def getObject[A](nodeSeq: NodeSeq, obj: Option[A]): Option[A] = {
    if ((nodeSeq.isEmpty) || (!obj.isDefined)) None else {
      for (field <- obj.get.getClass.getDeclaredFields) {
    field.setAccessible(true)
    val fieldValue = field.get(obj.get)
    if (fieldValue == null || !fieldValue.isInstanceOf[Option[_]]) None
    var newValue: Option[_] = None
    fieldValue.asInstanceOf[Option[_]].get match {    // <---- None.get is the error here
      case x: Double => newValue = Some((nodeSeq \ field.getName).text.trim.toDouble)
      case x: String => newValue = Some((nodeSeq \ field.getName).text.trim)
    }
    val decField = obj.get.getClass.getDeclaredField(field.getName)
    decField.setAccessible(true)
    decField.set(obj.get, newValue)
   }
    obj
    }
  }
}
bad_coder
  • 11,289
  • 20
  • 44
  • 72
  • 1
    Then `fieldValue` *is* `None`. A cast (asInstanceOf) does *not* change the underlying object; merely the static-type view of it. The `get` method is virtual/polymorphic as [Option](http://www.scala-lang.org/api/current/index.html#scala.Option) is the super-type of both Some and None. –  Jan 16 '13 at 10:19
  • While @pst's comment is right, you might be able to use Scala 2.10 reflection library http://docs.scala-lang.org/overviews/reflection/overview.html – Alexey Romanov Jan 16 '13 at 12:14
  • Also, changing fields in this way is ugly and will easily lead to bugs. Instead, your signature should be `def getObject[A : ru.TypeTag](nodeSeq: NodeSeq): Option[A]` (or `ClassManifest` instead of `TypeTag` before 2.10) and create a new object. – Alexey Romanov Jan 16 '13 at 12:18
  • thanks all, I' ve learned a lot from the comments and answers, but I'm still not getting the result I was seeking, although getting closer. with the input val testObj = TestObject(None, Some(12.0)) I get result = Some(TestObject(None,Some(34.0))) I was looking for result = Some(TestObject(Some(yyy),Some(34.0))) – user1899790 Jan 17 '13 at 11:30

2 Answers2

1

I'm ignoring some other parts of this code that I don't like, however, you can't call .get on a None. You can call map and it will return what I assume you want. Here's the modified code:

  def getObject[A](nodeSeq: NodeSeq, obj: Option[A]): Option[A] = {
    if (nodeSeq.isEmpty || obj.isEmpty) {
      None
    }
    else {
      for (field <- obj.get.getClass.getDeclaredFields) {
        field.setAccessible(true)
        val fieldValue = field.get(obj.get)
        if (fieldValue == null || !fieldValue.isInstanceOf[Option[_]]) None

        val newValue = fieldValue.asInstanceOf[Option[_]].map(a => {
          a match {
            case x: Double => Some((nodeSeq \ field.getName).text.trim.toDouble)
            case x: String => Some((nodeSeq \ field.getName).text.trim)
          }
        })

        val decField = obj.get.getClass.getDeclaredField(field.getName)
        decField.setAccessible(true)
        decField.set(obj.get, newValue)
      }
      obj
    }
  }

In practice, I usually avoid calling .get on options as it results in bugs quite often.

UPDATE:

This version is a little more functional, though it still alters the obj that you send in:

  def getObject[A](nodeSeq: NodeSeq, obj: Option[A]): Option[A] = {
    obj.map(o => {
      for {
        field <- o.getClass.getDeclaredFields
        _ = field.setAccessible(true)
        fieldValue <- Option(field.get(o))
      } yield {
        val newValue = fieldValue match {
          case Some(a) => {
            a match {
              case x: Double => Some((nodeSeq \ field.getName).text.trim.toDouble)
              case x: String => Some((nodeSeq \ field.getName).text.trim)
            }
          }
          case _ => None
        }
        val decField = o.getClass.getDeclaredField(field.getName)
        decField.setAccessible(true)
        decField.set(o, newValue)
      }
      o
    })
  }
Noah
  • 13,821
  • 4
  • 36
  • 45
0

Actually, this might be closer to what you want: check the type of the field instead.

import xml.NodeSeq
object test {
  case class TestObject(name: Option[String] = None, value: Option[Double] = None)
  def main(args: Array[String]): Unit = {
  val testObj = TestObject(Some("xxx"), Some(12.0))
//    val testObj = TestObject(None, Some(12.0))
    val nodeSeq = <test> <name> yyy </name> <value> 34.0 </value> </test>
    val result = getObject[TestObject](nodeSeq, Option(testObj))
    println("result = " + result) // result = Some(TestObject(Some(yyy),Some(34.0)))
  }
  def getObject[A](nodeSeq: NodeSeq, obj: Option[A]): Option[A] = {
    if ((nodeSeq.isEmpty) || (!obj.isDefined)) None else {
      for (field <- obj.get.getClass.getDeclaredFields) {
    field.setAccessible(true)
    val newValue = field.getGenericType match {
      case t: ParametrizedType if (t.getActualType == classOf[Option[_]]) => 
        val paramType = t.getActualTypeArguments()(0)
        if (paramType == classOf[java.lang.Double]) // since Double can't be a generic type parameter on JVM
          // from your original code, but it probably needs to be modified
          // to handle cases where field isn't in nodeSeq or not a number
          Some((nodeSeq \ field.getName).text.trim.toDouble) 
        else if (paramType == classOf[String])
          Some((nodeSeq \ field.getName).text.trim)
        else None
      case _ => None
    }
    if (newValue.isDefined)
      field.set(obj.get, newValue)
   }
    obj
    }
  }
}

Version creating a new object instead (might need modification for your requirements):

  def getObject[A](nodeSeq: NodeSeq, objClass: Class[A]): Option[A] = {
    if (nodeSeq.isEmpty) None else {
      try {
        val fieldValues = objClass.getDeclaredFields.flatMap { field =>
          field.setAccessible(true)
          field.getGenericType match {
            case t: ParametrizedType if (t.getActualType == classOf[Option[_]]) => 
              val paramType = t.getActualTypeArguments()(0)
              if (paramType == classOf[java.lang.Double])
                Some(Some((nodeSeq \ field.getName).text.trim.toDouble))
              else if (paramType == classOf[String])
                Some(Some((nodeSeq \ field.getName).text.trim))
              else None
            case _ => None
          }
        }
        // assumes only one constructor, otherwise get the desired one
        val ctor = objClass.getConstructors()(0)
        Some(ctor.newInstance(fieldValues))
      } catch {
        case _ => None
      }
    }
  }
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487