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

com.github.loyada.dollarx.ElementPropertiesHelper.scala Maven / Gradle / Ivy

There is a newer version: 0.35
Show newest version
package com.github.loyada.dollarx

import com.github.loyada.dollarx.ElementProperties.has
import com.github.loyada.dollarx.util.XpathUtils


object ElementPropertiesHelper {

  //lazy...
   def hasHierarchy(xpath: String) = xpath.contains("/")

  def transformXpathToCorrectAxis(sourcePath: Path): Option[String] = {
    if (!sourcePath.getXPath.isEmpty && hasHierarchy(sourcePath.getXPath.get)) {
      sourcePath.getAlternateXPath
    } else sourcePath.getXPath
  }

  val axisRegex = "^(parent|child|ancestor|descendant|following|preceding|self|ancestor-or-self|descendant-or-self|following-sibling|preceding-sibling)::(.*)$".r
  val oppositeRelation = Map(
    "parent" -> "child",
    "child" -> "parent",
    "ancestor" -> "descendant",
    "following" -> "preceding",
    "preceding" -> "following",
    "self" -> "self",
    "ancestor-or-self" -> "descendant-or-self",
    "descendant-or-self" -> "ancestor-or-self",
    "descendant" -> "ancestor",
    "following-sibling" -> "preceding-sibling",
    "preceding-sibling" -> "following-sibling"
  )

  case class Or(p1: ElementProperty, p2: ElementProperty) extends ElementProperty {
    override def toString() = s"($p1 or $p2)"

    override def toXpath() = p1.toXpath + " or " + p2.toXpath

    override def or(p: ElementProperty) = Or(this, p)

    //I commented the line below, since it will transform logic like : [(A or B) and C]  to [A or (B and C)], which is wrong
    //override def and(p: ElementProperties) = Or(p1, And(p2, p))
  }

  case class And(p1: ElementProperty, p2: ElementProperty) extends ElementProperty {
    override def toXpath() = {
      "(" + p1.toXpath + " and " + p2.toXpath + ")"
    }

    override def toString() = {
      s"($p1 and $p2)"
    }
  }

  case class Not(p: ElementProperty) extends ElementProperty {
    override def toString() = "not (" + p.toString + ")"

    override def toXpath = "not(" + p.toXpath + ")"
  }

  case class hasClass(cssClass: String) extends ElementProperty {
    override def toXpath = XpathUtils.hasClass(cssClass)

    override def toString = s"""has class "$cssClass""""

  }


  case class isNthFromLastSibling(reverseIndex: Integer) extends ElementProperty {
    def toXpath: String = {
      return String.format("count(following-sibling::*)=%d", reverseIndex)
    }

    override def toString: String = {
      return String.format("is in place %d from the last sibling", reverseIndex)
    }
  }

  case class isNthSibling(index: Integer) extends ElementProperty {
    def toXpath: String = {
      return String.format("count(preceding-sibling::*)=%d", index)
    }

    override def toString: String = {
      return String.format("is in place %d among its siblings", index)
    }
  }


  object HasHelper {

    import RelationOperator._

    private val countXpath: (String => String) = { (relation: String) => s"count($relation::* )" }
    private val countChildrenXpath = "count(./*)"
    private val countDescendantsXpath = countXpath("descendant")
    countXpath("descendant")
    private val countSiblingsXpath = s"${countXpath("preceding-sibling")} + ${countXpath("following-sibling")}"

    class HasChildren(n: Option[NCount] = None) extends ElementProperty {
      override def toXpath = if (n.isEmpty) s"$countChildrenXpath > 0"
      else {
        val count = n.get
        s"$countChildrenXpath${opAsXpathString(count.relationOperator)}${count.n}"
      }

      override def toString = {
        val number = if (n.isDefined) {
          val count = n.get
          (opAsEnglish(count.relationOperator) + count.n)
        } else " some"
        s"has$number children"
      }
    }

    class HasDescendants(n: Option[NCount] = None) extends ElementProperty {
      override def toXpath = if (n.isEmpty) s"$countChildrenXpath > 0" else {
        val count = n.get
        s"$countDescendantsXpath${opAsXpathString(count.relationOperator)}${count.n}"
      }

      override def toString = {
        val number = if (n.isDefined) {
          val count = n.get
          (opAsEnglish(count.relationOperator) + count.n)
        } else " some"
        s"has$number descendants"
      }
    }

    class HasSiblings(n: Option[NCount] = None) extends ElementProperty {
      override def toXpath = if (n.isEmpty) s"$countChildrenXpath > 0" else {
        val count = n.get
        s"$countSiblingsXpath${opAsXpathString(count.relationOperator)}${count.n}"
      }

      override def toString = {
        val number = if (n.isDefined) {
          val count = n.get
          (opAsEnglish(count.relationOperator) + count.n)
        } else " some"
        s"has$number siblings"
      }
    }

    object HasNoChildren extends ElementProperty {
      override def toXpath = s"$countChildrenXpath = 0"

      override def toString = "has no children"
    }

    case class HasN(n: NCount) {
      def children = new HasChildren(Some(n))
      def siblings = new HasSiblings(Some(n))
      def descendants = new HasDescendants(Some(n))
    }

    trait HasNotProperty {
      def get: ElementProperty
    }

  }

  object lastSiblingOfType extends ElementProperty {
    override def toXpath = "last()"

    override def toString = "is last sibling"
  }


  object uniqueOfType extends ElementProperty {
    override def toXpath = "count(*)=1"

    override def toString = "only one of its type"
  }

  case class hasId(id: String) extends ElementProperty {
    override def toXpath = XpathUtils.hasId(id)

    override def toString = s"""has Id "$id""""
  }

  case class hasOneOfClasses(cssClasses: String*) extends ElementProperty {
    override def toXpath = XpathUtils.hasOneOfClasses(cssClasses: _*)

    override def toString = s"has at least one of the classes: [${cssClasses.mkString(", ")}]"
  }

  case class hasClasses(cssClasses: String*) extends ElementProperty {
    override def toXpath = XpathUtils.hasClasses(cssClasses: _*)

    override def toString = s"has classes [${cssClasses.mkString(", ")}]"
  }

  case class withoutClasses(cssClasses: String*) extends ElementProperty {
    override def toXpath = XpathUtils.DoesNotExist(XpathUtils.hasOneOfClasses(cssClasses: _*))

    override def toString = {
      if (cssClasses.size > 1) {
        s"has non of the classes: [${cssClasses.mkString(", ")}]"
      }
      else s"does not have the class ${cssClasses.mkString(", ")}"

    }
  }

  case class IsWithIndex(n: Int) extends ElementProperty {
    override def toXpath() = s"position()=${n + 1}"

    override def toString() = s"with index $n"
  }


  case class hasText(txt: String) extends ElementProperty {
    override def toXpath() = XpathUtils.textEquals(txt)

    override def toString() = s"""has the text "${txt}""""
  }

  case class hasNoText(txt: String = "") extends ElementProperty {
    override def toXpath() = {
      val hasItProperty = if (txt == "") hasSomeText else hasText(txt)
      val hasNoProperty = Not(hasItProperty)
      hasNoProperty.toXpath
    }

    override def toString() = if (txt == "") "has no text" else s"""has no text equal to "${txt}""""
  }

  case class hasTextContaining(txt: String) extends ElementProperty {
    override def toXpath() = XpathUtils.textContains(txt)

    override def toString() = s"""has text containing "${txt}""""

  }

  case class withAggregatedTextEqualTo(txt: String) extends ElementProperty {
    override def toXpath() = XpathUtils.aggregatedTextEquals(txt)

    override def toString() = s"""with aggregated text "$txt""""

  }

  case class withAggregatedTextContaining(txt: String) extends ElementProperty {
    override def toXpath() = XpathUtils.aggregatedTextContains(txt)

    override def toString() = s"""with aggregated text containing "$txt""""
  }

  object hasSomeText extends ElementProperty {
    override def toXpath() = XpathUtils.hasSomeText

    override def toString() = "has some text"

  }

  object isHidden extends ElementProperty {
    override def toXpath() = XpathUtils.isHidden

    override def toString() = "is hidden"
  }

  case class isChildOf(path: Path) extends ElementProperty with relationBetweenElement {
    override def toXpath() = getRelationXpath("parent")

    override def toString() = {
      "is child of: " + path
    }

  }

  def contains(paths: Path*) = hasDescendant(paths: _*)

  case class hasDescendant(paths: Path*) extends ElementProperty with relationBetweenMultiElement {
    protected val relation = "descendant"
    override def toString() = asString("has descendant")
  }

  case class hasAncesctor(path: Path) extends ElementProperty with relationBetweenElement {
    override def toXpath() = getRelationXpath("ancestor")

    override def toString = {
      "has ancestor: " + rValueToString(path)
    }

  }

  case class hasParent(path: Path) extends ElementProperty with relationBetweenElement {
    override def toXpath() = getRelationXpath("parent")
    override def toString() = {
      "has parent: " + rValueToString(path)
    }
  }

  case class hasChild(paths: Path*) extends ElementProperty with relationBetweenMultiElement {
    protected val relation = "child"

    override def toString() = asString("has " + (if (paths.size == 1) "child" else "children"))

    override protected def plural(relation: String) = relation

  }

  case class hasAttribute(attribute: String, value: String) extends ElementProperty {
    override def toXpath: String = {
      XpathUtils.hasAttribute(attribute, value)
    }

    override def toString: String = {
      String.format("has %s: \"%s\"", attribute, value)
    }
  }

  trait IsProperty extends ElementProperty

  object IsHelpers {


    implicit def intToNPathBuilder(n: Int): NPathBuilder = NPathBuilder(n)

    object IsSiblingProperty extends ElementProperty with IsProperty {
      def apply(npath: NPath) = new ElementProperty {
        val path = npath.path
        val n = npath.n

        override def toXpath: String = s"count(following-sibling::${transformXpathToCorrectAxis(path).get})+count(preceding-sibling::${transformXpathToCorrectAxis(path).get})>=$n"

        override def toString: String = s"is a sibling of $n occurrences of $path"
      }

      def apply(paths: Path*) = isSibling(paths: _*)

      override def toXpath: String = ???
    }

    object IsAfterSiblingProperty extends ElementProperty with IsProperty {
      val relation = "preceding-sibling"

      def apply(npath: NPath) = new ElementProperty {
        val path = npath.path
        val n = npath.n

        override def toXpath: String = s"count($relation::${transformXpathToCorrectAxis(path).get})>=$n"

        override def toString: String = s"is after $n occurrences of $path siblings"
      }

      def apply(paths: Path*) = isAfterSibling(paths: _*)

      override def toXpath: String = ???
    }

    object IsBeforeSiblingProperty extends ElementProperty with IsProperty {
      val relation = "following-sibling"

      def apply(npath: NPath) = new ElementProperty {
        val path = npath.path
        val n = npath.n

        override def toXpath: String = s"count($relation::${transformXpathToCorrectAxis(path).get})>=$n"

        override def toString: String = s"is before $n occurrences of $path siblings"
      }

      def apply(paths: Path*) = isBeforeSibling(paths: _*)

      override def toXpath: String = ???
    }

    object IsAfterProperty extends ElementProperty with IsProperty {
      val relation = "preceding"

      def apply(npath: NPath) = new ElementProperty with IsProperty {
        val path = npath.path
        val n = npath.n

        override def toXpath: String = s"count($relation::${transformXpathToCorrectAxis(path).get})>=$n"

        override def toString: String = s"is after $n occurrences of $path"
      }

      def apply(paths: Path*) = isAfter(paths: _*)

      override def toXpath: String = ???
    }

    object IsBeforeProperty extends ElementProperty with IsProperty {
      val relation = "following"

      def apply(npath: NPath) = new ElementProperty with IsProperty {
        val path = npath.path
        val n = npath.n

        override def toXpath: String = s"count($relation::${transformXpathToCorrectAxis(path).get})>=$n"

        override def toString: String = s"is before $n occurrences of $path"
      }

      def apply(paths: Path*) = isBefore(paths: _*)

      override def toXpath: String = ???
    }

  }

  case class NPath(n: Int, path: Path)

  case class NPathBuilder(n: Int) {

    object occurrencesOf {
      def apply(path: Path) = NPath(n, path)
    }

  }

  implicit def intToNPathBuilder(n: Int): NPathBuilder = NPathBuilder(n)

  private def rValueToString(path: Path): String = {
    if ((path.toString.trim.contains(" "))) "(" + path + ")" else path.toString
  }

  case class isAfter(paths: Path*) extends ElementProperty with relationBetweenMultiElement with IsProperty {
    protected val relation = "preceding"
    override def toString() = asString("is after")
    override protected def plural(relation: String)  = relation
  }

  case class isSibling(paths: Path*) extends ElementProperty with relationBetweenMultiElement with IsProperty {
    protected val relation = ""
    override protected def getXpathExpressionForSingle(path: Path): String = (has sibling(path)).toXpath
    override def toString() = asString("has sibling")
  }

  case class isAfterSibling(paths: Path*) extends ElementProperty with relationBetweenMultiElement with IsProperty {
    protected val relation = "preceding-sibling"
    override def toString() = asString("is after sibling")
  }

  case class isBefore(paths: Path*) extends ElementProperty with relationBetweenMultiElement with IsProperty {
    protected val relation = "following"
    override def toString() = asString("is before")
  }

  case class isBeforeSibling(paths: Path*) extends ElementProperty with relationBetweenMultiElement with IsProperty {
    protected val relation = "following-sibling"
    override def toString() = asString("is before sibling")
  }

  case class IsWithIndexInRange(first: Int, last: Int) extends ElementProperty with IsProperty {
    override def toXpath: String = {
      s"position()>=${first + 1} and position()<=${last + 1}"
    }

    override def toString: String = {
      s"with index from $first to $last"
    }
  }

}

trait relationBetweenElement {
  protected val path: Path

  protected def getRelationXpath(relation: String) = {
    if (path.getUnderlyingSource().isDefined || path.getXPath.isEmpty) throw new IllegalArgumentException("must use a pure xpath path")
    s"$relation::" + ElementPropertiesHelper.transformXpathToCorrectAxis(path).get
  }
}

trait relationBetweenMultiElement {
  protected val relation: String
  protected val paths: Seq[Path]

  def toXpath() = {
    getRelationXpath(relation)
  }

  protected def asString(prefix: String) = {
    val asList = List(paths: _*).map(path => rValueToString(path)).mkString(", ")
    s"${plural(prefix)}: " + (if (paths.size > 1) s"[$asList]" else asList)
  }

  protected def plural(relation: String) = if (paths.size == 1) relation else relation + "s"

  protected def getXpathExpressionForSingle(path: Path) = s"$relation::${ElementPropertiesHelper.transformXpathToCorrectAxis(path).get}"


  private def getRelationForSingleXpath(path: Path, relation: String) = {
    if (path.getUnderlyingSource.isDefined || path.getXPath.isEmpty) throw new IllegalArgumentException("must use a pure xpath BasicPath")
    getXpathExpressionForSingle(path)
  }

  protected def getRelationXpath(relation: String) = {
    val result = List(paths: _*).map(path => {
      getRelationForSingleXpath(path, relation)
    }).mkString(") and (")
    if (paths.length > 1) s"($result)" else s"$result"
  }

  private def rValueToString(path: Path): String = {
    if ((path.toString.trim.contains(" "))) "(" + path + ")" else path.toString
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy