All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fm.xml.XmlReaderPath.scala Maven / Gradle / Ivy

/*
 * Copyright 2018 Frugal Mechanic (http://frugalmechanic.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fm.xml

import fm.common.Implicits._
import fm.common.Logging
import javax.xml.bind.{JAXBContext, Unmarshaller}
import javax.xml.stream.XMLStreamConstants.START_ELEMENT
import org.codehaus.stax2.XMLStreamReader2
import scala.reflect.{ClassTag, classTag}

object XmlReaderPath {
  // Apply helper for reading a non-mapped value
  def apply[XmlValue: ClassTag](itemPath: String): XmlReaderPath[XmlValue, XmlValue] = XmlReaderPath[XmlValue, XmlValue](itemPath, identity)

  sealed trait XmlReaderPathMatchType {
    def isPrefixMatch: Boolean = this === XmlReaderPathMatchType.PrefixMatch
    def isFullMatch: Boolean = this === XmlReaderPathMatchType.FullMatch
    def isNoMatch: Boolean = this === XmlReaderPathMatchType.NoMatch
  }
  object XmlReaderPathMatchType {
    case object FullMatch extends XmlReaderPathMatchType
    case object PrefixMatch extends XmlReaderPathMatchType
    case object NoMatch extends XmlReaderPathMatchType
  }
}



/**
  *
  * @param itemPath The XPath-like path to the element we are interested in.  (e.g. "part", "items/part", etc)
  * @param toMappedValue Map the XmlValue into another return type, this should match the return type in MultiXmlReader
  * @tparam XmlValue The unmarshalled element class type
  * @tparam MappedXmlValue A mapped XmlValue type
  */
final case class XmlReaderPath[XmlValue: ClassTag, MappedXmlValue](
  itemPath: String,
  toMappedValue: XmlValue => MappedXmlValue
) extends Logging {
  import XmlReaderPath._

  // Make this public, so we can use it for the XmlWriter JAXBContext
  val itemClass: Class[XmlValue] = classTag[XmlValue].runtimeClass.asInstanceOf[Class[XmlValue]]
  private[this] val jaxbContext: JAXBContext = JAXBContext.newInstance(itemClass)
  private[this] val unmarshaller: Unmarshaller = jaxbContext.createUnmarshaller()

  // The XPath-like path to the element we are interested in
  // part => Array("part"), items/part => Array("items","part")
  private[xml] val path: Array[String] = itemPath.split('/')

  // The name of the element we care about (last part of the path)
  // items/part => part
  private[xml] val itemName: String = path.last

  private[xml] val targetDepth: Int = path.length

  private[xml] def readValue(xmlStreamReader: XMLStreamReader2): MappedXmlValue =  {
    xmlStreamReader.require(START_ELEMENT, null, itemName)

    val value: XmlValue = unmarshaller.unmarshal(xmlStreamReader, itemClass).getValue
    toMappedValue(value)
  }

  /**
    * Given a current element nesting sequence, getMatchType returns whether this path is an exact match, no match, or if the prefix matches.
    *
    * Example: if the itemPath is "items/part" then:
    *
    *   When currentPath is IndexedSeq("items") the match type will be XmlReaderPathMatchType.PrefixMatch
    *   When currentPath is IndexedSeq("items", "part") the match type will be XmlReaderPathMatchType.FullMatch
    *   When currentPath is IndexedSeq("items", "price") the match type will be XmlReaderPathMatchType.NoMatch
    *   When currentPath is IndexedSeq("prices") the match type will be XmlReaderPathMatchType.NoMatch
    *
    * @param currentPath An IndexedSeq of the current nested XML-element tree.  (e.g. "items/part/price" => Seq("items", "part", "price")
    * @return
    */
  private[xml] def getMatchType(currentPath: IndexedSeq[String]): XmlReaderPathMatchType = {
    if (currentPath.isEmpty || currentPath.length > targetDepth) return XmlReaderPathMatchType.NoMatch

    // Optimized to avoid a closure
    var idx: Int = 0

    while (idx < currentPath.length && currentPath(idx) === path(idx)) {
      idx += 1
    }

    if (idx === targetDepth) XmlReaderPathMatchType.FullMatch
    else if (idx === currentPath.length) XmlReaderPathMatchType.PrefixMatch
    else XmlReaderPathMatchType.NoMatch
  }

  override def toString(): String = {
    s"XmlReaderPath[${itemClass.toString}](itemPath: ${itemPath})"
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy