From f6af1ade0d4883a4de4590454e17c3335ad65124 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Fri, 9 Dec 2011 01:29:48 +0200 Subject: [PATCH 01/27] Introducing hidden elements into Zipper. --- .../antixml/CanBuildFromWithZipper.scala | 30 +++- .../com/codecommit/antixml/Selectable.scala | 8 +- .../scala/com/codecommit/antixml/Zipper.scala | 159 ++++++++++++------ 3 files changed, 137 insertions(+), 60 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala b/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala index ffb4932..0f8e078 100644 --- a/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala +++ b/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala @@ -97,16 +97,33 @@ object CanBuildFromWithZipper { * Decorates a sequence of zipper elements with a path and an update time. This is the * basic unit of information used to construct zippers. See [[com.codecommit.antixml.CanBuildFromWithZipper]] * for more information. + * + * There are two types of elements visible and hidden. Visible ones provide the + * indexed values of a zipper, while the hidden ones provide unindexable values in the zipper. + * Both types are represented as concrete subclasses of this one. * * @tparam Elem the type of node that will be contained in the zipper. - * @param path Identifies a location (known as a "hole") in the zipper's parent. The order of the - * path is from top to bottom (the first item specifies the index of a top-level node in the parent Group). * @param updateTime the update time associated with these elements. One context is considered to have * been updated later than another if its updateTime is greater. - * @param elements the actual elements to be added to the zipper. * @see [[com.codecommit.antixml.CanBuildFromWithZipper]] */ - case class ElemsWithContext[+Elem](path: Seq[Int], updateTime: Int, elements: GenTraversableOnce[Elem]) + sealed abstract class ElemsWithContext[+Elem](updateTime: Int) + /** + * A visible zipper element. + * @param path Identifies a location (known as a "hole") in the zipper's parent. The order of the + * path is from top to bottom (the first item specifies the index of a top-level node in the parent Group). + * @param elements the actual elements to be added to the zipper. + */ + case class ElemsWithContextVisible[+Elem](path: Seq[Int], updateTime: Int, elements: GenTraversableOnce[Elem]) extends ElemsWithContext[Elem](updateTime) + /** + * A hidden zipper element. + * @tparam Elem Dummy parameterization to satisfy the signature of methods like `flatMap`. + * @param path A zipper path instance leading to the location of the hole. + * @param elements The elements to be mapped to the path contained in the context. The type + * of the elements is the most general as they are not accessible through the zipper's methods + * and hence do not participate in any sort of transformations. + */ + case class ElemsWithContextHidden(path: ZipperPath, updateTime: Int, elements: GenTraversableOnce[Node]) extends ElemsWithContext[Nothing](updateTime) /** Implicitly lifts [[scala.collection.mutable.CanBuildFrom]] instances into instances of [[com.codecommit.antixml.CanBuildFromWithZipper]]. The resulting builders simply ignore * the extra information in `ElemsWithContext` and produce their collections as usual. @@ -122,7 +139,10 @@ object CanBuildFromWithZipper { private def liftBuilder(b: Builder[Elem,To]) = new Builder[ElemsWithContext[Elem], To]() { override def += (x: ElemsWithContext[Elem]) = { - b ++= x.elements.seq + x match { + case ElemsWithContextVisible(_, _, elements) => b ++= elements.seq + case ElemsWithContextHidden(_, _, _) => // nothing to do with hidden nodes + } this } override def clear() { diff --git a/src/main/scala/com/codecommit/antixml/Selectable.scala b/src/main/scala/com/codecommit/antixml/Selectable.scala index c3a6fe4..ccf4df1 100644 --- a/src/main/scala/com/codecommit/antixml/Selectable.scala +++ b/src/main/scala/com/codecommit/antixml/Selectable.scala @@ -29,11 +29,11 @@ package com.codecommit package antixml -import com.codecommit.antixml.util.VectorCase import scala.collection.generic.{CanBuildFrom, HasNewBuilder} -import scala.collection.immutable.{Vector, VectorBuilder} +import scala.collection.immutable.VectorBuilder -import CanBuildFromWithZipper.ElemsWithContext +import com.codecommit.antixml.CanBuildFromWithZipper.ElemsWithContextVisible +import com.codecommit.antixml.util.VectorCase trait Selectable[+A <: Node] { import PathCreator.{allChildren, directChildren, fromNodes, allMaximalChildren, PathFunction, PathVal} @@ -195,7 +195,7 @@ trait Selectable[+A <: Node] { val grp = toGroup val bld = cbfwz(Some(toZipper), grp) for( PathVal(value, path) <- pf(grp) ) { - bld += ElemsWithContext[B](path, 0, VectorCase(value)) + bld += ElemsWithContextVisible[B](path, 0, VectorCase(value)) } bld.result() } diff --git a/src/main/scala/com/codecommit/antixml/Zipper.scala b/src/main/scala/com/codecommit/antixml/Zipper.scala index 7c06d51..b0c8d62 100644 --- a/src/main/scala/com/codecommit/antixml/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/Zipper.scala @@ -34,9 +34,10 @@ import scala.collection.{immutable, mutable, IndexedSeqLike, GenTraversableOnce} import scala.collection.generic.{CanBuildFrom, FilterMonadic} import scala.collection.immutable.{SortedMap, IndexedSeq} import scala.collection.mutable.Builder - import Zipper._ import CanBuildFromWithZipper.ElemsWithContext +import com.codecommit.antixml.CanBuildFromWithZipper.ElemsWithContextVisible +import com.codecommit.antixml.CanBuildFromWithZipper.ElemsWithContextHidden /** * Provides an `unselect` operation which copies this Group's nodes back to the XML tree from which @@ -141,11 +142,11 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se override protected[this] def newBuilder = Zipper.newBuilder[A] override def updated[B >: A <: Node](index: Int, node: B): Zipper[B] = context match { - case Some(Context(parent, lastUpdate, metas, additionalHoles)) => { + case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { val updatedTime = lastUpdate + 1 val (updatedPath,_) = metas(index) val updatedMetas = metas.updated(index, (updatedPath, updatedTime)) - val ctx = Context(parent, updatedTime, updatedMetas, additionalHoles) + val ctx = Context(parent, updatedTime, updatedMetas, additionalHoles, hiddenNodes) new Group(nodes.updated(index, node)) with Zipper[B] { val context = Some(ctx) @@ -155,7 +156,7 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se } override def slice(from: Int, until: Int): Zipper[A] = context match { - case Some(Context(parent, lastUpdate, metas, additionalHoles)) => { + case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { val lo = math.min(math.max(from, 0), nodes.length) val hi = math.min(math.max(until, lo), nodes.length) val cnt = hi - lo @@ -168,7 +169,7 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se for(i <- hi until nodes.length) ahs += ((metas(i)._1, lastUpdate + 1 + i - cnt)) - val ctx = Context(parent, lastUpdate + length - cnt, metas.slice(from, until), ahs.result()) + val ctx = Context(parent, lastUpdate + length - cnt, metas.slice(from, until), ahs.result(), hiddenNodes) new Group(nodes.slice(from,until)) with Zipper[A] { val context = Some(ctx) @@ -197,15 +198,19 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se override def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit cbf: CanBuildFrom[Zipper[A], B, That]): That = cbf match { case cpz: CanProduceZipper[Zipper[A], B, That] if context.isDefined => { - val Context(parent, lastUpdate, metas, additionalHoles) = context.get + val Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes) = context.get val b = cpz.lift(Some(parent), this) for(i <- 0 until nodes.length) { val (path,_) = metas(i) - b += ElemsWithContext(path, lastUpdate+i+1, f(nodes(i))) + b += ElemsWithContextVisible(path, lastUpdate+i+1, f(nodes(i))) } - for ((path,time) <- additionalHoles) { - b += ElemsWithContext[B](path,time,util.Vector0) + + for ((path, time) <- additionalHoles) { + b += ElemsWithContextVisible[B](path, time, util.Vector0) } + + b ++= hiddenNodes + b.result() } case _ => { @@ -247,7 +252,7 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se /* See the Group implementation for information about how this function is optimized. */ context match { case None => brokenZipper(new Group(nodes).conditionalFlatMapWithIndex(f).nodes) - case Some(Context(parent, lastUpdate, metas, additionalHoles)) => { + case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { //Optimistic function that uses `update` @tailrec def update(z: Zipper[B], index: Int): Zipper[B] = { @@ -268,20 +273,24 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se for(i <- 0 until index) { val (p,t) = zc.metas(i) - b += ElemsWithContext(p,t,util.Vector1(z(i))) + b += ElemsWithContextVisible(p,t,util.Vector1(z(i))) } - b += ElemsWithContext(metas(index)._1, zc.lastUpdate+1,currentReplacements) + b += ElemsWithContextVisible(metas(index)._1, zc.lastUpdate+1,currentReplacements) for(i <- (index + 1) until nodes.length) { val n = nodes(i) val m = metas(i) f(n,i) match { - case None => b += ElemsWithContext(m._1, m._2, util.Vector1(n)) - case Some(r) => b += ElemsWithContext(m._1, zc.lastUpdate + 1 + i - index, r) + case None => b += ElemsWithContextVisible(m._1, m._2, util.Vector1(n)) + case Some(r) => b += ElemsWithContextVisible(m._1, zc.lastUpdate + 1 + i - index, r) } } - for((p,t) <- additionalHoles) - b += ElemsWithContext(p,t,util.Vector0) + for((p,t) <- additionalHoles) { + b += ElemsWithContextVisible(p,t,util.Vector0) + } + + b ++= hiddenNodes + b.result } @@ -300,24 +309,51 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se private[this] class Unselector(context: Context, mergeStrategy: ZipperMergeStrategy) { /** Each hole is associated with a list of node/time pairs as well as a master update time */ - type HoleInfo = ZipperHoleMap[(VectorCase[(A,Time)],Time)] + type HoleInfo = ZipperHoleMap[(VectorCase[(Node,Time)],Time)] - private val topLevelHoleInfo: HoleInfo = { - val Context(_,_,metas,additionalHoles) = context - val init:(VectorCase[(A,Time)],Time) = (util.Vector0,0) - val hm0: HoleInfo = ZipperHoleMap.empty - val hm1 = (hm0 /: (0 until self.length)) { (hm, i) => - val item = self(i) - val (path,time) = metas(i) - val (oldItems, oldTime) = hm.getDeep(path).getOrElse(init) - val newItems = oldItems :+ (item, time) + private val initHoleInfoItem:(VectorCase[(Node,Time)],Time) = (util.Vector0,0) + + type HoleMapGet[A] = A => (ZipperPath, Time, GenTraversableOnce[Node]) + + /** Adding items to the hole info object using the given getter function to transform the items + * into the appropriate format. */ + private def addToHoleInfo[A](items: Seq[A], h: HoleInfo, get: HoleMapGet[A]) = { + (h /: items) { (hi, item) => + val (path, time, nodes) = get(item) + val (oldNodes, oldTime) = hi.getDeep(path).getOrElse(initHoleInfoItem) + val newItems = (oldNodes /: nodes) { case(old, node) => old :+ (node, time) } val newTime = math.max(oldTime, time) - hm.updatedDeep(path, (newItems, newTime)) + hi.updatedDeep(path, (newItems, newTime)) } - (hm1 /: additionalHoles) { case (hm,(path,time)) => - val (oldItems, oldTime) = hm.getDeep(path).getOrElse(init) - val newTime = math.max(oldTime, time) - hm.updatedDeep(path, (oldItems, newTime)) + } + + private val topLevelHoleInfo: HoleInfo = { + val Context(_, _, metas, additionalHoles, hiddenNodes) = context + + /* Getters for the different parts of the zipper. */ + + val indicesGet = (i: Int) => { + val (path, time) = metas(i) + val items = util.Vector1(self(i)) + (path, time, items) + } + + val hiddenGet = (ewc: ElemsWithContextHidden) => { + val ElemsWithContextHidden(path, time, items) = ewc + (path, time, items) + } + + val additonalGet = (pt: (ZipperPath, Time)) => { + val (path, time) = pt + (path, time, util.Vector0) + } + + case class ItemsGet[A](items: Seq[A], get: HoleMapGet[A]) + val itemsGetters = List(ItemsGet(indices, indicesGet), ItemsGet(hiddenNodes, hiddenGet), ItemsGet(additionalHoles, additonalGet)) + + val holeInit: HoleInfo = ZipperHoleMap.empty + (holeInit /: itemsGetters) { case (hi, ItemsGet(items, get)) => + addToHoleInfo(items, hi, get) } } @@ -393,8 +429,6 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se object Zipper { - import CanBuildFromWithZipper.ElemsWithContext - /** * Defines the zipper context * @@ -406,9 +440,15 @@ object Zipper { * @param additionalHoles assertions that indicate the specified path is associated with the zipper * and has at least the specified time. If there are paths in this structure that are not in `metas`, * then the corresponding hole will be replaced with an empty sequence during `unselect`. + * @param hiddenNodes Nodes that are contained in the Zipper but are not accessible through indexing. + * These elements will participate in the unselection process just as the other ones. TODO any special assumptions on these elements? */ - private[antixml] case class Context(parent: Zipper[Node], lastUpdate: Time, - metas: VectorCase[(ZipperPath, Time)], additionalHoles: immutable.Seq[(ZipperPath, Time)]) + private[antixml] case class Context( + parent: Zipper[Node], + lastUpdate: Time, + metas: VectorCase[(ZipperPath, Time)], + additionalHoles: immutable.Seq[(ZipperPath, Time)], + hiddenNodes: immutable.Seq[ElemsWithContextHidden]) /** The units in which time is measured in the zipper. Assumed non negative. */ private type Time = Int @@ -458,25 +498,36 @@ object Zipper { private val itemsBuilder = VectorCase.newBuilder[A] private val metasBuilder = VectorCase.newBuilder[(ZipperPath,Time)] private val additionalHolesBuilder = new AdditionalHolesBuilder() - private var size = 0 + private val hiddenNodesBuilder = VectorCase.newBuilder[ElemsWithContextHidden] + private var size = 0 //TODO is this used anywhere? private var maxTime = 0 override def += (ewc: ElemsWithContext[A]) = { - val ElemsWithContext(pseq, time, ns) = ewc - val path: ZipperPath = ZipperPath.fromSeq(pseq) - val pathTime = (path, time) - - var nsz = 0 - for(n <- ns) { - itemsBuilder += n - metasBuilder += pathTime - nsz += 1 - } - if (nsz==0) { - additionalHolesBuilder += pathTime + // keeping track of the time in the context + val time: Time = ewc match { + case ElemsWithContextVisible(pseq, t, ns) => { + val path: ZipperPath = ZipperPath.fromSeq(pseq) + val pathTime = (path, t) + + var nsz = 0 + for (n <- ns) { + itemsBuilder += n + metasBuilder += pathTime + nsz += 1 + } + if (nsz == 0) { + additionalHolesBuilder += pathTime + } + + size += nsz + t + } + case e @ ElemsWithContextHidden(_, t, _) => { + hiddenNodesBuilder += e + t + } } - size += nsz maxTime = math.max(maxTime, time) this } @@ -484,18 +535,23 @@ object Zipper { itemsBuilder.clear() metasBuilder.clear() additionalHolesBuilder.clear() + hiddenNodesBuilder.clear() size = 0 maxTime = 0 } override def result(): Zipper[A] = { - val ctx = Context(parent, maxTime, metasBuilder.result(), additionalHolesBuilder.result()) + val ctx = Context( + parent, maxTime, + metasBuilder.result(), + additionalHolesBuilder.result(), + hiddenNodesBuilder.result()) new Group[A](itemsBuilder.result()) with Zipper[A] { val context = Some(ctx) } } } - + /** Builder for the `additionalHoles` list. This builder ensures that the result has at most one * entry for any given ZipperPath. Although this isn't necessary for correctness, it ensures that * the `additionalHoles` list remains bounded in size by the total number of holes. @@ -521,4 +577,5 @@ object Zipper { if (hm.size==0) util.Vector0 else (VectorCase.newBuilder[(ZipperPath,Time)] ++= hm).result } + } \ No newline at end of file From 199d35c1824a2a8d2a490b132794af7fee0ce826 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Fri, 9 Dec 2011 03:31:09 +0200 Subject: [PATCH 02/27] Adding a class for path transformation. --- .../codecommit/antixml/PathTransformer.scala | 76 +++++++++++++++++++ .../antixml/PathTransformerSpecs.scala | 62 +++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 src/main/scala/com/codecommit/antixml/PathTransformer.scala create mode 100644 src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala diff --git a/src/main/scala/com/codecommit/antixml/PathTransformer.scala b/src/main/scala/com/codecommit/antixml/PathTransformer.scala new file mode 100644 index 0000000..f1312f0 --- /dev/null +++ b/src/main/scala/com/codecommit/antixml/PathTransformer.scala @@ -0,0 +1,76 @@ +package com.codecommit.antixml + +import scala.annotation.tailrec +import PathTransformer._ + +/** Transforms [[com.codecommit.antixml.ZipperPath]]s with predefined functions. + * + * The transformations rely on a source parent group from which all paths are + * calculated. Any paths passed to instances of this class are assumed to be valid + * paths in the source group. + * + * @param source The source for the transformed paths. + */ +private[antixml] case class PathTransformer(source: Group[Node]) { + //TODO this whole class is probably quite slow + //TODO state monad for caching? + + /** Shifts the path one step upwards, if possible. + * @param path The path to be shifted + * @return An optional path which is the parent of the original path, a new path transformer + * with an updated cache. */ + def shiftUp(path: ZipperPath): Option[ZipperPath] = if (path.isEmpty) None else Some(path.init) + + /** Shifts the path one step to the left, if possible. + * @param path The path to be shifted + * @return An optional path which is a sibling of the original path from the left, a new path transformer + * with an updated cache. */ + def shiftLeft(path: ZipperPath): Option[ZipperPath] = { + shiftSideways(path, -1) + } + + /** Shifts the path one step to the right, if possible. + * @param path The path to be shifted + * @return An optional path which is a sibling of the original path from the right, a new path transformer + * with an updated cache. */ + def shiftRight(path: ZipperPath): Option[ZipperPath] = { + shiftSideways(path, +1) + } + + /** @return An optional node at the end of the path and updated cache. */ + private def getNode(path: ZipperPath): Option[Node] = getNode(source, path) + + @tailrec + private def getNode(currLevel: Group[Node], path: ZipperPath): Option[Node] = { + if (path.isEmpty) None + else if (path.size == 1) Some(currLevel(path.head)) + else { + // the cast must succeed otherwise the path is invalid + val children = currLevel(path.head).asInstanceOf[Elem].children + getNode(children, path.tail) + } + } + + /** Tries to shift the path sideways by the given increment. */ + private def shiftSideways(path: ZipperPath, increment: Int): Option[ZipperPath] = { + assert(!path.isEmpty, "Cannot shift an empty path.") + + val currLevel = + if (path.size == 1) source + else { + val parent = getNode(path.init) // size > 1 + parent.get.asInstanceOf[Elem].children // must be an elem for a valid path + } + + val end = path.size - 1 + + val newLoc = path(end) + increment + + if (currLevel.indices.contains(newLoc)) Some(path.updated(end, newLoc)) + else None + } +} + +private[antixml] object PathTransformer { + private[antixml]type PathCache = Map[ZipperPath, Option[Node]] +} \ No newline at end of file diff --git a/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala b/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala new file mode 100644 index 0000000..e79ec93 --- /dev/null +++ b/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala @@ -0,0 +1,62 @@ +package com.codecommit.antixml + +import org.specs2.mutable._ +import XML._ +import scala.collection.immutable.HashMap + +class PathTransformerSpecs extends SpecificationWithJUnit { + + val x0 = fromString("foobaz") + val x1 = fromString("foobaz") + val x2 = fromString("foobaz") + + val group = Group(x0, x1, x2) + + val empty = ZipperPath() + val p1 = ZipperPath(0, 1, 0) + val p2 = ZipperPath(1, 1) + val p3 = ZipperPath(2) + val p4 = ZipperPath(0) + + val transformer = PathTransformer(group) + + "shifting upwards" should { + "ignore empty paths" in { + transformer.shiftUp(empty) mustEqual None + } + + "properly shift paths" in { + transformer.shiftUp(p1) mustEqual Some(ZipperPath(0, 1)) + transformer.shiftUp(p2) mustEqual Some(ZipperPath(1)) + transformer.shiftUp(p3) mustEqual Some(ZipperPath()) + } + } + + "shifting leftwards" should { + "fail on empty paths" in { + transformer.shiftLeft(empty) must throwAn[AssertionError] + } + + "properly shift paths" in { + transformer.shiftLeft(p1) mustEqual None + transformer.shiftLeft(p2) mustEqual Some(ZipperPath(1, 0)) + transformer.shiftLeft(p3) mustEqual Some(ZipperPath(1)) + transformer.shiftLeft(p4) mustEqual None + } + } + + "shifting rightwards" should { + "fail on empty paths" in { + transformer.shiftRight(empty) must throwAn[AssertionError] + } + + "properly shift paths" in { + transformer.shiftRight(p1) mustEqual None + transformer.shiftRight(p2) mustEqual Some(ZipperPath(1, 2)) + transformer.shiftRight(p3) mustEqual None + transformer.shiftRight(p4) mustEqual Some(ZipperPath(1)) + } + } + + def elem(name: String, text: String) = Elem(None, name, Attributes(), Map(), Group(Text(text))) +} \ No newline at end of file From 27b1ff57d075c0cae133088ff9b005ddecbacdc3 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Fri, 9 Dec 2011 04:24:18 +0200 Subject: [PATCH 03/27] Fixing a spelling mistake. --- src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala b/src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala index 712ad28..f039c7d 100644 --- a/src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala +++ b/src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala @@ -105,7 +105,7 @@ private[antixml] final class ZipperHoleMap[+B] private (items: Map[Int, ZipperHo } /** Returns a traversable represented the tree's contents in pre-order (lexicographically by path). - * Note that this has not been optimized, as it is only currenly used by toString and for testing. + * Note that this has not been optimized, as it is only currently used by toString and for testing. */ def depthFirst: Traversable[(ZipperPath, B)] = new Traversable[(ZipperPath, B)] { From 7fae42620c014bbec5b2cfa3cb87c2fab1fda73a Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Fri, 9 Dec 2011 06:04:06 +0200 Subject: [PATCH 04/27] Extracting node fetching into a separate object. --- .../com/codecommit/antixml/PathFetcher.scala | 25 ++++++++++++++++++ .../codecommit/antixml/PathTransformer.scala | 26 ++++--------------- 2 files changed, 30 insertions(+), 21 deletions(-) create mode 100644 src/main/scala/com/codecommit/antixml/PathFetcher.scala diff --git a/src/main/scala/com/codecommit/antixml/PathFetcher.scala b/src/main/scala/com/codecommit/antixml/PathFetcher.scala new file mode 100644 index 0000000..1602150 --- /dev/null +++ b/src/main/scala/com/codecommit/antixml/PathFetcher.scala @@ -0,0 +1,25 @@ +package com.codecommit.antixml +import scala.annotation.tailrec + +/** + * Fetches [[com.codecommit.antixml.ZipperPath]]s from within a [[com.codecommit.antixml.Group]] + */ +object PathFetcher { //TODO test + + /** + * @param source The group upon which the fetching is performed. + * @param path The path to be fetched, assumed to be valid for the given source. + * @return An optional node at the end of the path. */ + def getNode(source: Group[Node])(path: ZipperPath): Option[Node] = getNodeRec(source, path) + + @tailrec + private def getNodeRec(currLevel: Group[Node], path: ZipperPath): Option[Node] = { + if (path.isEmpty) None + else if (path.size == 1) Some(currLevel(path.head)) + else { + // the cast must succeed otherwise the path is invalid + val children = currLevel(path.head).asInstanceOf[Elem].children + getNodeRec(children, path.tail) + } + } +} \ No newline at end of file diff --git a/src/main/scala/com/codecommit/antixml/PathTransformer.scala b/src/main/scala/com/codecommit/antixml/PathTransformer.scala index f1312f0..31ef577 100644 --- a/src/main/scala/com/codecommit/antixml/PathTransformer.scala +++ b/src/main/scala/com/codecommit/antixml/PathTransformer.scala @@ -2,6 +2,7 @@ package com.codecommit.antixml import scala.annotation.tailrec import PathTransformer._ +import PathFetcher._ /** Transforms [[com.codecommit.antixml.ZipperPath]]s with predefined functions. * @@ -17,40 +18,23 @@ private[antixml] case class PathTransformer(source: Group[Node]) { /** Shifts the path one step upwards, if possible. * @param path The path to be shifted - * @return An optional path which is the parent of the original path, a new path transformer - * with an updated cache. */ + * @return An optional path which is the parent of the original path. */ def shiftUp(path: ZipperPath): Option[ZipperPath] = if (path.isEmpty) None else Some(path.init) /** Shifts the path one step to the left, if possible. * @param path The path to be shifted - * @return An optional path which is a sibling of the original path from the left, a new path transformer - * with an updated cache. */ + * @return An optional path which is a sibling of the original path from the left. */ def shiftLeft(path: ZipperPath): Option[ZipperPath] = { shiftSideways(path, -1) } /** Shifts the path one step to the right, if possible. * @param path The path to be shifted - * @return An optional path which is a sibling of the original path from the right, a new path transformer - * with an updated cache. */ + * @return An optional path which is a sibling of the original path from the right. */ def shiftRight(path: ZipperPath): Option[ZipperPath] = { shiftSideways(path, +1) } - /** @return An optional node at the end of the path and updated cache. */ - private def getNode(path: ZipperPath): Option[Node] = getNode(source, path) - - @tailrec - private def getNode(currLevel: Group[Node], path: ZipperPath): Option[Node] = { - if (path.isEmpty) None - else if (path.size == 1) Some(currLevel(path.head)) - else { - // the cast must succeed otherwise the path is invalid - val children = currLevel(path.head).asInstanceOf[Elem].children - getNode(children, path.tail) - } - } - /** Tries to shift the path sideways by the given increment. */ private def shiftSideways(path: ZipperPath, increment: Int): Option[ZipperPath] = { assert(!path.isEmpty, "Cannot shift an empty path.") @@ -58,7 +42,7 @@ private[antixml] case class PathTransformer(source: Group[Node]) { val currLevel = if (path.size == 1) source else { - val parent = getNode(path.init) // size > 1 + val parent = getNode(source)(path.init) // size > 1 parent.get.asInstanceOf[Elem].children // must be an elem for a valid path } From 06dafa009d64f04e0aa459e0b56fe4180fcc7f3a Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Fri, 9 Dec 2011 06:10:29 +0200 Subject: [PATCH 05/27] Adding Zipper shifting function. --- .../antixml/CanBuildFromWithZipper.scala | 10 ++- .../scala/com/codecommit/antixml/Zipper.scala | 65 +++++++++++++++++-- .../codecommit/antixml/ZipperHoleMap.scala | 2 + 3 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala b/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala index 0f8e078..a529012 100644 --- a/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala +++ b/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala @@ -103,27 +103,25 @@ object CanBuildFromWithZipper { * Both types are represented as concrete subclasses of this one. * * @tparam Elem the type of node that will be contained in the zipper. + * @param path A zipper path instance leading to the location of the hole. * @param updateTime the update time associated with these elements. One context is considered to have * been updated later than another if its updateTime is greater. * @see [[com.codecommit.antixml.CanBuildFromWithZipper]] */ - sealed abstract class ElemsWithContext[+Elem](updateTime: Int) + sealed abstract class ElemsWithContext[+Elem](path: ZipperPath, updateTime: Int) /** * A visible zipper element. - * @param path Identifies a location (known as a "hole") in the zipper's parent. The order of the - * path is from top to bottom (the first item specifies the index of a top-level node in the parent Group). * @param elements the actual elements to be added to the zipper. */ - case class ElemsWithContextVisible[+Elem](path: Seq[Int], updateTime: Int, elements: GenTraversableOnce[Elem]) extends ElemsWithContext[Elem](updateTime) + case class ElemsWithContextVisible[+Elem](path: ZipperPath, updateTime: Int, elements: GenTraversableOnce[Elem]) extends ElemsWithContext[Elem](path, updateTime) /** * A hidden zipper element. * @tparam Elem Dummy parameterization to satisfy the signature of methods like `flatMap`. - * @param path A zipper path instance leading to the location of the hole. * @param elements The elements to be mapped to the path contained in the context. The type * of the elements is the most general as they are not accessible through the zipper's methods * and hence do not participate in any sort of transformations. */ - case class ElemsWithContextHidden(path: ZipperPath, updateTime: Int, elements: GenTraversableOnce[Node]) extends ElemsWithContext[Nothing](updateTime) + case class ElemsWithContextHidden(path: ZipperPath, updateTime: Int, elements: GenTraversableOnce[Node]) extends ElemsWithContext[Nothing](path, updateTime) /** Implicitly lifts [[scala.collection.mutable.CanBuildFrom]] instances into instances of [[com.codecommit.antixml.CanBuildFromWithZipper]]. The resulting builders simply ignore * the extra information in `ElemsWithContext` and produce their collections as usual. diff --git a/src/main/scala/com/codecommit/antixml/Zipper.scala b/src/main/scala/com/codecommit/antixml/Zipper.scala index b0c8d62..d5bcedb 100644 --- a/src/main/scala/com/codecommit/antixml/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/Zipper.scala @@ -35,9 +35,11 @@ import scala.collection.generic.{CanBuildFrom, FilterMonadic} import scala.collection.immutable.{SortedMap, IndexedSeq} import scala.collection.mutable.Builder import Zipper._ +import Zipper.ZipperPathOrdering import CanBuildFromWithZipper.ElemsWithContext import com.codecommit.antixml.CanBuildFromWithZipper.ElemsWithContextVisible import com.codecommit.antixml.CanBuildFromWithZipper.ElemsWithContextHidden +import scala.collection.immutable.SortedSet /** * Provides an `unselect` operation which copies this Group's nodes back to the XML tree from which @@ -154,6 +156,44 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se } case None => brokenZipper(nodes.updated(index,node)) } + + /** TODO */ + private[antixml] def shiftHoles(shiftFunc: PathTransformer => ZipperPath => Seq[ZipperPath]): Zipper[Node] = context match { + case Some(context @ Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { + implicit val lexicographic = ZipperPathOrdering + + val shift = shiftFunc(PathTransformer(parent)) + + // not allowing duplicates and sorting lexicographical + val newPaths = SortedSet(metas.flatMap(m => shift(m._1)): _*) + val holeInfo = new HoleMapper(context).holeInfo + + val b = newZipperContextBuilder[Node](Some(parent)) + val pathsInit: VectorCase[ElemsWithContextVisible[Node]] = util.Vector0 + + val (unusedPaths, usedPaths) = // leaving paths that were never used before + holeInfo.depthFirst.foldLeft((newPaths, pathsInit)) { case ((nPaths, used), hole) => + val (path, (nt, time)) = hole + val (nodes, _) = nt.unzip + + if (nPaths contains path) { + (nPaths - path, used :+ ElemsWithContextVisible(path, time, nodes)) + } else { + b += ElemsWithContextHidden(path, time, nodes) + (nPaths, used) + } + } + + val initTime = 0 // these paths were never modified + val unusedElems = unusedPaths.toList map(p => ElemsWithContextVisible[Node](p, initTime, PathFetcher.getNode(parent)(p))) + + val visibleElems = SortedSet(unusedElems ++ usedPaths: _*)(Ordering.by(_.path)) + b ++= visibleElems + + b.result + } + case None => sys.error("Cannot shift root.") + } override def slice(from: Int, until: Int): Zipper[A] = context match { case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { @@ -305,12 +345,11 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se new Unselector(ctx, zms).unselect } - /** Utility class to perform unselect. */ - private[this] class Unselector(context: Context, mergeStrategy: ZipperMergeStrategy) { - - /** Each hole is associated with a list of node/time pairs as well as a master update time */ - type HoleInfo = ZipperHoleMap[(VectorCase[(Node,Time)],Time)] - + /** Each hole is associated with a list of node/time pairs as well as a master update time */ + private type HoleInfo = ZipperHoleMap[(VectorCase[(Node,Time)],Time)] + + /** A utility class to convert the contents of the zipper into a hole map. */ + private[this] class HoleMapper(context: Context) { private val initHoleInfoItem:(VectorCase[(Node,Time)],Time) = (util.Vector0,0) type HoleMapGet[A] = A => (ZipperPath, Time, GenTraversableOnce[Node]) @@ -327,7 +366,7 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se } } - private val topLevelHoleInfo: HoleInfo = { + val holeInfo: HoleInfo = { val Context(_, _, metas, additionalHoles, hiddenNodes) = context /* Getters for the different parts of the zipper. */ @@ -356,6 +395,12 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se addToHoleInfo(items, hi, get) } } + } + + /** Utility class to perform unselect. */ + private[this] class Unselector(context: Context, mergeStrategy: ZipperMergeStrategy) { + + private val topLevelHoleInfo = new HoleMapper(context).holeInfo /** Applies the node updates to the parent and returns the result. */ def unselect: Zipper[Node] = @@ -468,6 +513,12 @@ object Zipper { } } + /** Lexicographic ordering for path objects. */ + private object ZipperPathOrdering extends Ordering[ZipperPath] { + override def compare(x: ZipperPath, y: ZipperPath) = + Ordering.Iterable[Int].compare(x,y) + } + /** Returns a builder that produces a zipper without a parent */ def newBuilder[A <: Node] = VectorCase.newBuilder[A].mapResult(brokenZipper(_)) diff --git a/src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala b/src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala index f039c7d..9aeeb80 100644 --- a/src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala +++ b/src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala @@ -106,6 +106,8 @@ private[antixml] final class ZipperHoleMap[+B] private (items: Map[Int, ZipperHo /** Returns a traversable represented the tree's contents in pre-order (lexicographically by path). * Note that this has not been optimized, as it is only currently used by toString and for testing. + * + * TODO using this for zipper shifting, consider optimizing */ def depthFirst: Traversable[(ZipperPath, B)] = new Traversable[(ZipperPath, B)] { From de358f3c12009e2a97a295f0f974a373194dc571 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Fri, 9 Dec 2011 16:03:23 +0200 Subject: [PATCH 06/27] Test for PathFetcher --- .../com/codecommit/antixml/PathFetcher.scala | 4 +- .../codecommit/antixml/PathFetcherSpecs.scala | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/test/scala/com/codecommit/antixml/PathFetcherSpecs.scala diff --git a/src/main/scala/com/codecommit/antixml/PathFetcher.scala b/src/main/scala/com/codecommit/antixml/PathFetcher.scala index 1602150..07063cb 100644 --- a/src/main/scala/com/codecommit/antixml/PathFetcher.scala +++ b/src/main/scala/com/codecommit/antixml/PathFetcher.scala @@ -4,7 +4,9 @@ import scala.annotation.tailrec /** * Fetches [[com.codecommit.antixml.ZipperPath]]s from within a [[com.codecommit.antixml.Group]] */ -object PathFetcher { //TODO test +object PathFetcher { + + //TODO state monad for caching? /** * @param source The group upon which the fetching is performed. diff --git a/src/test/scala/com/codecommit/antixml/PathFetcherSpecs.scala b/src/test/scala/com/codecommit/antixml/PathFetcherSpecs.scala new file mode 100644 index 0000000..cd5566b --- /dev/null +++ b/src/test/scala/com/codecommit/antixml/PathFetcherSpecs.scala @@ -0,0 +1,37 @@ +package com.codecommit.antixml + +import org.specs2.mutable._ +import org.specs2.matcher.DataTables +import XML._ + +class PathFetcherSpecs extends SpecificationWithJUnit with DataTables { + + val x0 = fromString("foobaz") + val x1 = fromString("foobaz") + val x2 = fromString("foobaz") + + val group = Group(x0, x1, x2) + val fetch = PathFetcher.getNode(group)_ + val empty: Option[Node] = None + + "path fetching" should { + "produce correct results" in { + "path" | "result" | + ZipperPath.empty ! empty | + ZipperPath(0) ! Some(x0) | + ZipperPath(1) ! Some(x1) | + ZipperPath(2) ! Some(x2) | + ZipperPath(0, 0) ! Some(fromString("foo")) | + ZipperPath(0, 1) ! Some(fromString("baz")) | + ZipperPath(0, 2) ! Some(fromString("")) | + ZipperPath(1, 0) ! Some(fromString("foo")) | + ZipperPath(1, 1) ! Some(fromString("baz")) | + ZipperPath(1, 2) ! Some(fromString("")) | + ZipperPath(2, 0) ! Some(fromString("foo")) | + ZipperPath(2, 1) ! Some(fromString("baz")) | + ZipperPath(2, 2) ! Some(fromString("")) |> { + (path, res) => fetch(path) mustEqual res + } + } + } +} \ No newline at end of file From 6cfdd6d58bfd1c84c41ce78393c2b7dcfae5e4d8 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Fri, 9 Dec 2011 16:12:46 +0200 Subject: [PATCH 07/27] Prettified tests for and fixed docs on PathTransformer. --- .../codecommit/antixml/PathTransformer.scala | 4 +- .../antixml/PathTransformerSpecs.scala | 45 +++++++++++-------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/PathTransformer.scala b/src/main/scala/com/codecommit/antixml/PathTransformer.scala index 31ef577..834425e 100644 --- a/src/main/scala/com/codecommit/antixml/PathTransformer.scala +++ b/src/main/scala/com/codecommit/antixml/PathTransformer.scala @@ -13,8 +13,8 @@ import PathFetcher._ * @param source The source for the transformed paths. */ private[antixml] case class PathTransformer(source: Group[Node]) { - //TODO this whole class is probably quite slow - //TODO state monad for caching? + //TODO this whole class is probably quite slow, ZipperPath is not optimized for modifications + /** Shifts the path one step upwards, if possible. * @param path The path to be shifted diff --git a/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala b/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala index e79ec93..0c74992 100644 --- a/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala @@ -1,18 +1,18 @@ package com.codecommit.antixml import org.specs2.mutable._ +import org.specs2.matcher.DataTables import XML._ -import scala.collection.immutable.HashMap -class PathTransformerSpecs extends SpecificationWithJUnit { +class PathTransformerSpecs extends SpecificationWithJUnit with DataTables { val x0 = fromString("foobaz") val x1 = fromString("foobaz") val x2 = fromString("foobaz") - + val group = Group(x0, x1, x2) - val empty = ZipperPath() + val empty = ZipperPath.empty val p1 = ZipperPath(0, 1, 0) val p2 = ZipperPath(1, 1) val p3 = ZipperPath(2) @@ -26,9 +26,12 @@ class PathTransformerSpecs extends SpecificationWithJUnit { } "properly shift paths" in { - transformer.shiftUp(p1) mustEqual Some(ZipperPath(0, 1)) - transformer.shiftUp(p2) mustEqual Some(ZipperPath(1)) - transformer.shiftUp(p3) mustEqual Some(ZipperPath()) + "path" | "result" | + p1 ! Some(ZipperPath(0, 1)) | + p2 ! Some(ZipperPath(1)) | + p3 ! Some(ZipperPath()) |> { + (path, res) => transformer.shiftUp(path) mustEqual res + } } } @@ -38,10 +41,13 @@ class PathTransformerSpecs extends SpecificationWithJUnit { } "properly shift paths" in { - transformer.shiftLeft(p1) mustEqual None - transformer.shiftLeft(p2) mustEqual Some(ZipperPath(1, 0)) - transformer.shiftLeft(p3) mustEqual Some(ZipperPath(1)) - transformer.shiftLeft(p4) mustEqual None + "path" | "result" | + p1 ! None.asInstanceOf[Option[ZipperPath]] | + p2 ! Some(ZipperPath(1, 0)) | + p3 ! Some(ZipperPath(1)) | + p4 ! None |> { + (path, res) => transformer.shiftLeft(path) mustEqual res + } } } @@ -49,13 +55,16 @@ class PathTransformerSpecs extends SpecificationWithJUnit { "fail on empty paths" in { transformer.shiftRight(empty) must throwAn[AssertionError] } - - "properly shift paths" in { - transformer.shiftRight(p1) mustEqual None - transformer.shiftRight(p2) mustEqual Some(ZipperPath(1, 2)) - transformer.shiftRight(p3) mustEqual None - transformer.shiftRight(p4) mustEqual Some(ZipperPath(1)) - } + + "properly shift paths" in { + "path" | "result" | + p1 ! None.asInstanceOf[Option[ZipperPath]] | + p2 ! Some(ZipperPath(1, 2)) | + p3 ! None | + p4 ! Some(ZipperPath(1)) |> { + (path, res) => transformer.shiftRight(path) mustEqual res + } + } } def elem(name: String, text: String) = Elem(None, name, Attributes(), Map(), Group(Text(text))) From fb39958847e071c4a1f3c05459ad381f8f95f532 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Sat, 10 Dec 2011 17:47:15 +0200 Subject: [PATCH 08/27] Decoupling Zipper shifting function from PathTrasonfrmer. --- src/main/scala/com/codecommit/antixml/Zipper.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/Zipper.scala b/src/main/scala/com/codecommit/antixml/Zipper.scala index d5bcedb..9d8a2da 100644 --- a/src/main/scala/com/codecommit/antixml/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/Zipper.scala @@ -157,12 +157,12 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se case None => brokenZipper(nodes.updated(index,node)) } - /** TODO */ - private[antixml] def shiftHoles(shiftFunc: PathTransformer => ZipperPath => Seq[ZipperPath]): Zipper[Node] = context match { + /** TODO doc/test */ + private[antixml] def shiftHoles(shiftFunc: Group[Node] => ZipperPath => Seq[ZipperPath]): Zipper[Node] = context match { case Some(context @ Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { implicit val lexicographic = ZipperPathOrdering - val shift = shiftFunc(PathTransformer(parent)) + val shift = shiftFunc(parent) // not allowing duplicates and sorting lexicographical val newPaths = SortedSet(metas.flatMap(m => shift(m._1)): _*) From b7cbf390bd2e4e4933b49b43d7d04c933e8c5762 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Sat, 10 Dec 2011 19:18:12 +0200 Subject: [PATCH 09/27] Shifting preserves all times associated with holes. --- .../scala/com/codecommit/antixml/Zipper.scala | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/Zipper.scala b/src/main/scala/com/codecommit/antixml/Zipper.scala index 9d8a2da..da39d33 100644 --- a/src/main/scala/com/codecommit/antixml/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/Zipper.scala @@ -157,14 +157,34 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se case None => brokenZipper(nodes.updated(index,node)) } - /** TODO doc/test */ + /** Shifts the focus of the zipper to another set of holes. + * + * The shifting is performed using a shifting function which is applied to each + * path visible in the zipper and produces a new sequence of paths. + * These paths are sorted lexicographically and duplicates are removed. + * + * A new zipper displaying the above paths is returned while internally maintaining data + * about any previously contained paths. + * + * The values which are attached to the paths come from two sources: + * - If the zipper previously contained the path, the data attached to it is used. + * - If the path is new, the data is fetched directly from the parent of the zipper. + * + * In case a hole was previously multiplied (e.g. using flatMap) it is placed + * as is in the resulting zipper. + * + * Note: shifting is not supported for parentless (broken) zippers. + * + * @param shiftFunc A function to be supplied with the parent of the zipper + * and applied to the indexed contents of the zipper. + * Assumed to produce valid paths with regard to the supplied parent. */ private[antixml] def shiftHoles(shiftFunc: Group[Node] => ZipperPath => Seq[ZipperPath]): Zipper[Node] = context match { case Some(context @ Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { implicit val lexicographic = ZipperPathOrdering val shift = shiftFunc(parent) - // not allowing duplicates and sorting lexicographical + // not allowing duplicates and sorting lexicographically val newPaths = SortedSet(metas.flatMap(m => shift(m._1)): _*) val holeInfo = new HoleMapper(context).holeInfo @@ -173,13 +193,19 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se val (unusedPaths, usedPaths) = // leaving paths that were never used before holeInfo.depthFirst.foldLeft((newPaths, pathsInit)) { case ((nPaths, used), hole) => - val (path, (nt, time)) = hole - val (nodes, _) = nt.unzip + val (path, (nodesTimes, masterTime)) = hole + + val holes = + if (nodesTimes.isEmpty) Seq((path, masterTime, util.Vector0)) + else nodesTimes.map { case (n, t) => (path, t, Seq(n)) } + + val visible = (ElemsWithContextVisible.apply[Node] _).tupled + val hidden = (ElemsWithContextHidden.apply _).tupled if (nPaths contains path) { - (nPaths - path, used :+ ElemsWithContextVisible(path, time, nodes)) + (nPaths - path, used ++ holes.map(visible)) } else { - b += ElemsWithContextHidden(path, time, nodes) + b ++= holes.map(hidden) (nPaths, used) } } From 5c43fad4b91d5c076fdbea0784173d688f2099ae Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Sat, 10 Dec 2011 20:32:50 +0200 Subject: [PATCH 10/27] Adding tests for Zipper hole shifting. --- .../scala/com/codecommit/antixml/Zipper.scala | 9 +- .../com/codecommit/antixml/ZipperSpecs.scala | 101 ++++++++++++++---- 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/Zipper.scala b/src/main/scala/com/codecommit/antixml/Zipper.scala index da39d33..ff45f22 100644 --- a/src/main/scala/com/codecommit/antixml/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/Zipper.scala @@ -197,7 +197,7 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se val holes = if (nodesTimes.isEmpty) Seq((path, masterTime, util.Vector0)) - else nodesTimes.map { case (n, t) => (path, t, Seq(n)) } + else nodesTimes.map { case (n, t) => (path, t, Seq(n)) } // this contains duplicates for multiplied locations val visible = (ElemsWithContextVisible.apply[Node] _).tupled val hidden = (ElemsWithContextHidden.apply _).tupled @@ -211,9 +211,12 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se } val initTime = 0 // these paths were never modified - val unusedElems = unusedPaths.toList map(p => ElemsWithContextVisible[Node](p, initTime, PathFetcher.getNode(parent)(p))) + val unusedElems = unusedPaths.toList map { p => + ElemsWithContextVisible[Node](p, initTime, PathFetcher.getNode(parent)(p)) + } - val visibleElems = SortedSet(unusedElems ++ usedPaths: _*)(Ordering.by(_.path)) + // this can contain duplicate locations from the previously used paths + val visibleElems = (unusedElems ++ usedPaths).sortBy(_.path) b ++= visibleElems b.result diff --git a/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala index 26d3ef8..2c7e89d 100644 --- a/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala @@ -45,26 +45,6 @@ class ZipperSpecs extends SpecificationWithJUnit with ScalaCheck with XMLGenera // TODO This is by no means exhaustive, just showing off what's possible. "Deep Zipper selection" should { - // I'm lazy so the bookstore is hardcoded here. - // From some reason parsing indented literals gives errors when comparing results, using a single line string. - - val bookstore = fromString { - "" + - "" + - "For Whom the Bell Tolls" + - "Hemmingway" + - "" + - "" + - "I, Robot" + - "Isaac Asimov" + - "" + - "" + - "Programming Scala" + - "Dean Wampler" + - "Alex Payne" + - "" + - "" - } val bookGroup = Group(bookstore) @@ -953,7 +933,86 @@ class ZipperSpecs extends SpecificationWithJUnit with ScalaCheck with XMLGenera } } - + + "zipper shifting" should { + val x0 = fromString("foo0baz0") + val x1 = fromString("foo1baz1") + val x2 = fromString("foo2baz2") + val root = .convert + + def updateRoot(c: Group[Node]) = root.copy(children = c).toGroup + + val group = root.copy(children = Group(x0, x1, x2)).toGroup + val zipper = group \ * + + val noShift = (g: Any) => (p: ZipperPath) => Seq(p) + + def makeConstShift(paths: Seq[ZipperPath]) = (g: Any) => (p: Any) => paths + + val constantShift = makeConstShift(Seq(ZipperPath(0, 1), ZipperPath(0, 2), ZipperPath(0, 1), ZipperPath(0, 0))) + val constShiftRes = Group(x0, x1, x2) + + + + "fail on a broken zipper" in { + group.toZipper.shiftHoles(noShift) must throwA[RuntimeException] + } + + "preserve zipper when not shifting" in { + val shifted = zipper shiftHoles noShift + + shifted mustEqual zipper + shifted.unselect mustEqual group + } + + "result in a lexicographic set" in { + val shifted = zipper shiftHoles constantShift + shifted mustEqual constShiftRes + shifted.unselect mustEqual group + } + + "preserve elided holes" in { + val sliced = zipper.slice(1, 2) + val shifted = sliced shiftHoles constantShift + + shifted mustEqual Group(x1) // x0 and x2 were removed by slicing + shifted.unselect mustEqual updateRoot(Group(x1)) + } + + "preserve multiplied holes" in { + val flatMapped = zipper flatMap (n => Seq(n, n)) + val shifted = flatMapped shiftHoles constantShift + val children = Group(x0, x0, x1, x1, x2, x2) + + shifted mustEqual children + shifted.unselect mustEqual updateRoot(children) + } + + "preserve previous updates" in { + val update = .convert + val updated = zipper.updated(1, update) + val children = Group(x0, update, x2) + val shifted = updated shiftHoles constantShift + + shifted mustEqual children + shifted.unselect mustEqual updateRoot(children) + } + + "maintain hidden holes through multiple shifts (a.k.a 'all together now')" in { + val update = .convert + + val shift1 = zipper.slice(1, 2) shiftHoles constantShift + val shift2 = shift1 flatMap (n => Seq(n, n)) shiftHoles makeConstShift(Seq(ZipperPath(0, 0, 0))) + val shift3 = shift2.updated(0, update) shiftHoles makeConstShift(Seq(ZipperPath(0, 1, 2))) + val shift4 = shift3.updated(0, update) shiftHoles makeConstShift(Seq(ZipperPath(0, 0))) + + shift4 mustEqual Group() + + val child = x1.copy(children = x1.children.updated(2, update)) + shift4.unselect mustEqual updateRoot(Group(child, child)) + } + + } def validate[Expected] = new { def apply[A](a: A)(implicit evidence: A =:= Expected) = evidence must not beNull From 9021a46b12feb03b4ca73177b4cf9cd706065d6b Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Sat, 10 Dec 2011 20:33:10 +0200 Subject: [PATCH 11/27] Whitespace fix. --- .../scala/com/codecommit/antixml/CanBuildFromWithZipper.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala b/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala index a529012..5999f32 100644 --- a/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala +++ b/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala @@ -109,11 +109,13 @@ object CanBuildFromWithZipper { * @see [[com.codecommit.antixml.CanBuildFromWithZipper]] */ sealed abstract class ElemsWithContext[+Elem](path: ZipperPath, updateTime: Int) + /** * A visible zipper element. * @param elements the actual elements to be added to the zipper. */ - case class ElemsWithContextVisible[+Elem](path: ZipperPath, updateTime: Int, elements: GenTraversableOnce[Elem]) extends ElemsWithContext[Elem](path, updateTime) + case class ElemsWithContextVisible[+Elem](path: ZipperPath, updateTime: Int, elements: GenTraversableOnce[Elem]) extends ElemsWithContext[Elem](path, updateTime) + /** * A hidden zipper element. * @tparam Elem Dummy parameterization to satisfy the signature of methods like `flatMap`. From 326e18707611ceb5496fe8d2448610cddfe42044 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Sat, 10 Dec 2011 20:38:59 +0200 Subject: [PATCH 12/27] Removing irrelevant type definition. --- src/main/scala/com/codecommit/antixml/PathTransformer.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/PathTransformer.scala b/src/main/scala/com/codecommit/antixml/PathTransformer.scala index 834425e..276d872 100644 --- a/src/main/scala/com/codecommit/antixml/PathTransformer.scala +++ b/src/main/scala/com/codecommit/antixml/PathTransformer.scala @@ -53,8 +53,4 @@ private[antixml] case class PathTransformer(source: Group[Node]) { if (currLevel.indices.contains(newLoc)) Some(path.updated(end, newLoc)) else None } -} - -private[antixml] object PathTransformer { - private[antixml]type PathCache = Map[ZipperPath, Option[Node]] } \ No newline at end of file From f45e46efa44cfa7f37863eba8427b420f92e84cf Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Sat, 10 Dec 2011 21:55:49 +0200 Subject: [PATCH 13/27] Adding a test to zipper. --- src/test/scala/com/codecommit/antixml/ZipperSpecs.scala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala index 2c7e89d..64fe7cb 100644 --- a/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala @@ -965,6 +965,13 @@ class ZipperSpecs extends SpecificationWithJUnit with ScalaCheck with XMLGenera shifted.unselect mustEqual group } + "handle empty results" in { + val shifted = zipper shiftHoles makeConstShift(Seq()) + + shifted mustEqual Group() + shifted.unselect mustEqual group + } + "result in a lexicographic set" in { val shifted = zipper shiftHoles constantShift shifted mustEqual constShiftRes From 5ec69c4743a80831f368874698058355b7797ed9 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Sun, 11 Dec 2011 05:06:36 +0200 Subject: [PATCH 14/27] Zipper shifting ignores empty paths. --- .../scala/com/codecommit/antixml/PathTransformer.scala | 2 +- src/main/scala/com/codecommit/antixml/Zipper.scala | 8 ++++++-- .../com/codecommit/antixml/PathTransformerSpecs.scala | 4 ++-- src/test/scala/com/codecommit/antixml/ZipperSpecs.scala | 7 +++++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/PathTransformer.scala b/src/main/scala/com/codecommit/antixml/PathTransformer.scala index 276d872..0bca4f0 100644 --- a/src/main/scala/com/codecommit/antixml/PathTransformer.scala +++ b/src/main/scala/com/codecommit/antixml/PathTransformer.scala @@ -19,7 +19,7 @@ private[antixml] case class PathTransformer(source: Group[Node]) { /** Shifts the path one step upwards, if possible. * @param path The path to be shifted * @return An optional path which is the parent of the original path. */ - def shiftUp(path: ZipperPath): Option[ZipperPath] = if (path.isEmpty) None else Some(path.init) + def shiftUp(path: ZipperPath): Option[ZipperPath] = if (path.isEmpty || path.size == 1) None else Some(path.init) /** Shifts the path one step to the left, if possible. * @param path The path to be shifted diff --git a/src/main/scala/com/codecommit/antixml/Zipper.scala b/src/main/scala/com/codecommit/antixml/Zipper.scala index ff45f22..76319f7 100644 --- a/src/main/scala/com/codecommit/antixml/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/Zipper.scala @@ -183,9 +183,13 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se implicit val lexicographic = ZipperPathOrdering val shift = shiftFunc(parent) + val unsoretedPaths = for { + m <- metas + path <- shift(m._1) if path != ZipperPath.empty // ignoring empty paths + } yield path - // not allowing duplicates and sorting lexicographically - val newPaths = SortedSet(metas.flatMap(m => shift(m._1)): _*) + // not allowing duplicates and empty paths and sorting lexicographically + val newPaths = SortedSet(unsoretedPaths: _*) val holeInfo = new HoleMapper(context).holeInfo val b = newZipperContextBuilder[Node](Some(parent)) diff --git a/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala b/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala index 0c74992..d204991 100644 --- a/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala @@ -27,9 +27,9 @@ class PathTransformerSpecs extends SpecificationWithJUnit with DataTables { "properly shift paths" in { "path" | "result" | - p1 ! Some(ZipperPath(0, 1)) | + p1 ! Some(ZipperPath(0, 1)).asInstanceOf[Option[ZipperPath]] | p2 ! Some(ZipperPath(1)) | - p3 ! Some(ZipperPath()) |> { + p3 ! None |> { (path, res) => transformer.shiftUp(path) mustEqual res } } diff --git a/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala index 64fe7cb..c44cbf8 100644 --- a/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala @@ -972,6 +972,13 @@ class ZipperSpecs extends SpecificationWithJUnit with ScalaCheck with XMLGenera shifted.unselect mustEqual group } + "ignore empty paths" in { + val shifted = zipper shiftHoles makeConstShift(Seq(ZipperPath.empty)) + + shifted mustEqual Group() + shifted.unselect mustEqual group + } + "result in a lexicographic set" in { val shifted = zipper shiftHoles constantShift shifted mustEqual constShiftRes From 02294159d8ec2b0ea671914c5e5649f3577c7189 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Sun, 11 Dec 2011 05:47:51 +0200 Subject: [PATCH 15/27] Adding Zipper axes support. --- .../com/codecommit/antixml/ZipperAxes.scala | 71 ++++++++ .../codecommit/antixml/ZipperAxesSpecs.scala | 154 ++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/main/scala/com/codecommit/antixml/ZipperAxes.scala create mode 100644 src/test/scala/com/codecommit/antixml/ZipperAxesSpecs.scala diff --git a/src/main/scala/com/codecommit/antixml/ZipperAxes.scala b/src/main/scala/com/codecommit/antixml/ZipperAxes.scala new file mode 100644 index 0000000..78375c2 --- /dev/null +++ b/src/main/scala/com/codecommit/antixml/ZipperAxes.scala @@ -0,0 +1,71 @@ +package com.codecommit.antixml +import scala.annotation.tailrec + +/** + * Wraps [[com.codecommit.antixml.Zipper]] instances with some XPath like axes. + * + * Note1: the axes are applied to each node in a zipper individually and the result + * is a new zipper with the nodes concatenated and sorted lexicographically by + * location (removing any duplicate locations). + * + * Note2: the axes are calculated using holes in the zipper, hence for a modified + * zipper some nodes may be multiplied or elided. + */ +class ZipperAxes(zipper: Zipper[Node]) { + /** Returns the direct parent of a node. */ + def directParent = { + zipper shiftHoles (g => (PathTransformer(g).shiftUp(_)).andThen(_.toList)) + } + + /** Returns the ancestors of a node. */ + def ancestor = transFuncToShift(_.shiftUp, false) + + /** Returns the ancestors of a node including itself. */ + def ancestorOrSelf = transFuncToShift(_.shiftUp, true) + + /** Returns the following siblings of a node. */ + def followingSibling = transFuncToShift(_.shiftRight, false) + + /** Returns the following siblings of a node including itself. */ + def followingSiblingOrSelf = transFuncToShift(_.shiftRight, true) + + /** Returns the preceding siblings of a node. */ + def precedingSibling = transFuncToShift(_.shiftLeft, false) + + /** Returns the preceding siblings of a node including itself. */ + def precedingSiblingOrSelf = transFuncToShift(_.shiftLeft, true) + + /** Takes a path transformer function and converts it to a shifting function which is applied until + * the transformer return `None`. + * @param appendSource True if the initial path should be part of the result. + */ + private def transFuncToShift(func: PathTransformer => ZipperPath => Option[ZipperPath], withSource: Boolean) = { + zipper shiftHoles { g => + val pathToOpt = func(PathTransformer(g)) + + @tailrec + def traverse(path: ZipperPath, res: List[ZipperPath]): List[ZipperPath] = { + val opt = pathToOpt(path) + opt match { + case None => res + case Some(p) => traverse(p, p :: res) + } + } + + val shiftFunc = (p: ZipperPath) => { + val init = + if (withSource) List(p) + else List() + traverse(p, init) + } + shiftFunc + } + } + +} + +object ZipperAxes { + /** Pimps a plain zipper to have axes selection methods. + * TODO move to package object? */ + implicit def zipperToAxes(zipper: Zipper[Node]) = new ZipperAxes(zipper) +} \ No newline at end of file diff --git a/src/test/scala/com/codecommit/antixml/ZipperAxesSpecs.scala b/src/test/scala/com/codecommit/antixml/ZipperAxesSpecs.scala new file mode 100644 index 0000000..0101333 --- /dev/null +++ b/src/test/scala/com/codecommit/antixml/ZipperAxesSpecs.scala @@ -0,0 +1,154 @@ +package com.codecommit.antixml + +import org.specs2.mutable._ +import XML._ +import ZipperAxes._ +import scala.collection.immutable.SortedSet + +class ZipperAxesSpecs extends SpecificationWithJUnit { + val bookstore = resource("bookstore.xml").toGroup + + "Axes" should { + "preserve past modifications" in { + val zipper = (bookstore \\ 'author).updated(3, Text("foo")) + val res = zipper.unselect + + zipper.directParent.unselect mustEqual res + zipper.ancestor.unselect mustEqual res + zipper.ancestorOrSelf.unselect mustEqual res + zipper.followingSibling.unselect mustEqual res + zipper.followingSiblingOrSelf.unselect mustEqual res + zipper.precedingSibling.unselect mustEqual res + zipper.precedingSiblingOrSelf.unselect mustEqual res + } + + "fail on broken zippers" in { + bookstore.toZipper.directParent.unselect must throwA[RuntimeException] + bookstore.toZipper.ancestor.unselect must throwA[RuntimeException] + bookstore.toZipper.ancestorOrSelf.unselect must throwA[RuntimeException] + bookstore.toZipper.followingSibling.unselect must throwA[RuntimeException] + bookstore.toZipper.followingSiblingOrSelf.unselect must throwA[RuntimeException] + bookstore.toZipper.precedingSibling.unselect must throwA[RuntimeException] + bookstore.toZipper.precedingSiblingOrSelf.unselect must throwA[RuntimeException] + } + } + + "Direct parent axes" should { + "be empty for root" in { + val res = bookstore select 'bookstore directParent + + res mustEqual Group() + res.unselect mustEqual bookstore + } + + "return the direct parents of nodes" in { + val res = bookstore \\ 'author directParent; + + res mustEqual (bookstore \\ 'book) + res.unselect mustEqual bookstore + } + } + + "Ancestor axes" should { + "be empty for root" in { + val res = bookstore select 'bookstore ancestor + + res mustEqual Group() + res.unselect mustEqual bookstore + } + + "return all ancestors of nodes" in { + val res = bookstore \\ 'author ancestor; + + res mustEqual (bookstore ++ bookstore \\ 'book) + res.unselect mustEqual bookstore + } + } + + "Ancestor or self axes" should { + "return self for root" in { + val res = bookstore select 'bookstore ancestorOrSelf + + res mustEqual bookstore + res.unselect mustEqual bookstore + } + + "return all ancestors and self of nodes" in { + val books = bookstore \\ 'book + val authors = bookstore \\ 'author + + val res = authors ancestorOrSelf; + + + res mustEqual (bookstore :+ books(0) :+ authors(0) :+ books(1) :+ authors(1) :+ books(2) :+ authors(2) :+ authors(3)) + res.unselect mustEqual bookstore + } + } + + "Following sibling axes" should { + "return nothing on rightmost node" in { + val res = bookstore \\ 'author followingSibling + + res mustEqual Group((bookstore \\ 'author).apply(3)) + res.unselect mustEqual bookstore + } + + "return all following siblings of nodes" in { + val res = bookstore \\ 'title followingSibling + + res mustEqual bookstore \\ 'author + res.unselect mustEqual bookstore + } + } + + "Following sibling or self axes" should { + "return self for rightmost nodes" in { + val res = bookstore \\ 'author followingSiblingOrSelf + + res mustEqual (bookstore \\ 'author) + res.unselect mustEqual bookstore + } + + "return all following siblings and self of nodes" in { + val res = bookstore \\ 'title followingSiblingOrSelf + + res mustEqual bookstore \ 'book \ * + res.unselect mustEqual bookstore + } + } + + "Preceding sibling axes" should { + "return nothing on leftmost node" in { + val res = bookstore \\ 'title precedingSibling + + res mustEqual Group() + res.unselect mustEqual bookstore + } + + "return all preceding siblings of nodes" in { + val res = bookstore \\ 'author precedingSibling + + res mustEqual (bookstore \\ 'title) :+ (bookstore \\ 'author).apply(2) + res.unselect mustEqual bookstore + } + } + + "Preceding sibling or self axes" should { + "return self for leftmost nodes" in { + val res = bookstore \\ 'title precedingSiblingOrSelf + + res mustEqual (bookstore \\ 'title) + res.unselect mustEqual bookstore + } + + "return all preceding siblings and self of nodes" in { + val res = bookstore \\ 'author precedingSiblingOrSelf + + res mustEqual bookstore \ 'book \ * + res.unselect mustEqual bookstore + } + } + + def resource(filename: String) = + XML fromSource (scala.io.Source fromURL (getClass getResource ("/" + filename))) +} \ No newline at end of file From ee2ed5aaf42ff1d9646c9ea6965fb297c44b68e8 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Tue, 13 Dec 2011 21:11:38 +0200 Subject: [PATCH 16/27] Moving zipper related classes into a separate folder. --- .../codecommit/antixml/{ => zipper}/CanBuildFromWithZipper.scala | 0 src/main/scala/com/codecommit/antixml/{ => zipper}/Zipper.scala | 0 .../scala/com/codecommit/antixml/{ => zipper}/ZipperAxes.scala | 0 .../scala/com/codecommit/antixml/{ => zipper}/ZipperHoleMap.scala | 0 .../com/codecommit/antixml/{ => zipper}/ZipperMergeContext.scala | 0 .../com/codecommit/antixml/{ => zipper}/ZipperMergeStrategy.scala | 0 .../scala/com/codecommit/antixml/{ => zipper}/ZipperPath.scala | 0 .../com/codecommit/antixml/{ => zipper}/ZipperAxesSpecs.scala | 0 .../com/codecommit/antixml/{ => zipper}/ZipperHoleMapSpecs.scala | 0 .../antixml/{ => zipper}/ZipperMergeStrategySpecs.scala | 0 .../com/codecommit/antixml/{ => zipper}/ZipperPathSpecs.scala | 0 .../scala/com/codecommit/antixml/{ => zipper}/ZipperSpecs.scala | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename src/main/scala/com/codecommit/antixml/{ => zipper}/CanBuildFromWithZipper.scala (100%) rename src/main/scala/com/codecommit/antixml/{ => zipper}/Zipper.scala (100%) rename src/main/scala/com/codecommit/antixml/{ => zipper}/ZipperAxes.scala (100%) rename src/main/scala/com/codecommit/antixml/{ => zipper}/ZipperHoleMap.scala (100%) rename src/main/scala/com/codecommit/antixml/{ => zipper}/ZipperMergeContext.scala (100%) mode change 100755 => 100644 rename src/main/scala/com/codecommit/antixml/{ => zipper}/ZipperMergeStrategy.scala (100%) mode change 100755 => 100644 rename src/main/scala/com/codecommit/antixml/{ => zipper}/ZipperPath.scala (100%) rename src/test/scala/com/codecommit/antixml/{ => zipper}/ZipperAxesSpecs.scala (100%) rename src/test/scala/com/codecommit/antixml/{ => zipper}/ZipperHoleMapSpecs.scala (100%) rename src/test/scala/com/codecommit/antixml/{ => zipper}/ZipperMergeStrategySpecs.scala (100%) mode change 100755 => 100644 rename src/test/scala/com/codecommit/antixml/{ => zipper}/ZipperPathSpecs.scala (100%) rename src/test/scala/com/codecommit/antixml/{ => zipper}/ZipperSpecs.scala (100%) diff --git a/src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala b/src/main/scala/com/codecommit/antixml/zipper/CanBuildFromWithZipper.scala similarity index 100% rename from src/main/scala/com/codecommit/antixml/CanBuildFromWithZipper.scala rename to src/main/scala/com/codecommit/antixml/zipper/CanBuildFromWithZipper.scala diff --git a/src/main/scala/com/codecommit/antixml/Zipper.scala b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala similarity index 100% rename from src/main/scala/com/codecommit/antixml/Zipper.scala rename to src/main/scala/com/codecommit/antixml/zipper/Zipper.scala diff --git a/src/main/scala/com/codecommit/antixml/ZipperAxes.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala similarity index 100% rename from src/main/scala/com/codecommit/antixml/ZipperAxes.scala rename to src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala diff --git a/src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMap.scala similarity index 100% rename from src/main/scala/com/codecommit/antixml/ZipperHoleMap.scala rename to src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMap.scala diff --git a/src/main/scala/com/codecommit/antixml/ZipperMergeContext.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeContext.scala old mode 100755 new mode 100644 similarity index 100% rename from src/main/scala/com/codecommit/antixml/ZipperMergeContext.scala rename to src/main/scala/com/codecommit/antixml/zipper/ZipperMergeContext.scala diff --git a/src/main/scala/com/codecommit/antixml/ZipperMergeStrategy.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeStrategy.scala old mode 100755 new mode 100644 similarity index 100% rename from src/main/scala/com/codecommit/antixml/ZipperMergeStrategy.scala rename to src/main/scala/com/codecommit/antixml/zipper/ZipperMergeStrategy.scala diff --git a/src/main/scala/com/codecommit/antixml/ZipperPath.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperPath.scala similarity index 100% rename from src/main/scala/com/codecommit/antixml/ZipperPath.scala rename to src/main/scala/com/codecommit/antixml/zipper/ZipperPath.scala diff --git a/src/test/scala/com/codecommit/antixml/ZipperAxesSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala similarity index 100% rename from src/test/scala/com/codecommit/antixml/ZipperAxesSpecs.scala rename to src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala diff --git a/src/test/scala/com/codecommit/antixml/ZipperHoleMapSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperHoleMapSpecs.scala similarity index 100% rename from src/test/scala/com/codecommit/antixml/ZipperHoleMapSpecs.scala rename to src/test/scala/com/codecommit/antixml/zipper/ZipperHoleMapSpecs.scala diff --git a/src/test/scala/com/codecommit/antixml/ZipperMergeStrategySpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperMergeStrategySpecs.scala old mode 100755 new mode 100644 similarity index 100% rename from src/test/scala/com/codecommit/antixml/ZipperMergeStrategySpecs.scala rename to src/test/scala/com/codecommit/antixml/zipper/ZipperMergeStrategySpecs.scala diff --git a/src/test/scala/com/codecommit/antixml/ZipperPathSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperPathSpecs.scala similarity index 100% rename from src/test/scala/com/codecommit/antixml/ZipperPathSpecs.scala rename to src/test/scala/com/codecommit/antixml/zipper/ZipperPathSpecs.scala diff --git a/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperSpecs.scala similarity index 100% rename from src/test/scala/com/codecommit/antixml/ZipperSpecs.scala rename to src/test/scala/com/codecommit/antixml/zipper/ZipperSpecs.scala From e20bf455e91912a65b5cbf5d9359e15cf1b0f833 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Tue, 13 Dec 2011 21:51:03 +0200 Subject: [PATCH 17/27] Taking out Group methods overridden in Zipper into a separate trait. --- .../codecommit/antixml/zipper/Zipper.scala | 169 +----------------- .../antixml/zipper/ZipperGroupOverrides.scala | 167 +++++++++++++++++ 2 files changed, 174 insertions(+), 162 deletions(-) create mode 100644 src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala diff --git a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala index 76319f7..3ba6863 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala @@ -32,14 +32,11 @@ import com.codecommit.antixml.util.VectorCase import scala.annotation.tailrec import scala.collection.{immutable, mutable, IndexedSeqLike, GenTraversableOnce} import scala.collection.generic.{CanBuildFrom, FilterMonadic} -import scala.collection.immutable.{SortedMap, IndexedSeq} +import scala.collection.immutable.{SortedMap, IndexedSeq, SortedSet} import scala.collection.mutable.Builder import Zipper._ import Zipper.ZipperPathOrdering -import CanBuildFromWithZipper.ElemsWithContext -import com.codecommit.antixml.CanBuildFromWithZipper.ElemsWithContextVisible -import com.codecommit.antixml.CanBuildFromWithZipper.ElemsWithContextHidden -import scala.collection.immutable.SortedSet +import CanBuildFromWithZipper._ /** * Provides an `unselect` operation which copies this Group's nodes back to the XML tree from which @@ -124,7 +121,9 @@ import scala.collection.immutable.SortedSet * Let T be the sequence of top-level locations in G. Then `Z.unselect` is defined as `T.flatMap(pullback)`. * */ -trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { self => +trait Zipper[+A <: Node] extends Group[A] + with IndexedSeqLike[A, Zipper[A]] + with ZipperGroupOverrides[A] { self => /** * Returns the original group that was selected upon when the Zipper was created. A value of `None` indicates that @@ -141,22 +140,6 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se /** The zipper context or None if this is a broken zipper. */ private[antixml] val context: Option[Context] - override protected[this] def newBuilder = Zipper.newBuilder[A] - - override def updated[B >: A <: Node](index: Int, node: B): Zipper[B] = context match { - case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { - val updatedTime = lastUpdate + 1 - val (updatedPath,_) = metas(index) - val updatedMetas = metas.updated(index, (updatedPath, updatedTime)) - val ctx = Context(parent, updatedTime, updatedMetas, additionalHoles, hiddenNodes) - - new Group(nodes.updated(index, node)) with Zipper[B] { - val context = Some(ctx) - } - } - case None => brokenZipper(nodes.updated(index,node)) - } - /** Shifts the focus of the zipper to another set of holes. * * The shifting is performed using a shifting function which is applied to each @@ -228,150 +211,12 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] { se case None => sys.error("Cannot shift root.") } - override def slice(from: Int, until: Int): Zipper[A] = context match { - case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { - val lo = math.min(math.max(from, 0), nodes.length) - val hi = math.min(math.max(until, lo), nodes.length) - val cnt = hi - lo - - //Put the ZipperPaths from the elided `metas` entries into additionalHoles - val ahs = new AdditionalHolesBuilder() - ahs ++= additionalHoles - for(i <- 0 until lo) - ahs += ((metas(i)._1, lastUpdate + 1 + i)) - for(i <- hi until nodes.length) - ahs += ((metas(i)._1, lastUpdate + 1 + i - cnt)) - - val ctx = Context(parent, lastUpdate + length - cnt, metas.slice(from, until), ahs.result(), hiddenNodes) - - new Group(nodes.slice(from,until)) with Zipper[A] { - val context = Some(ctx) - } - } - case None => brokenZipper(nodes.slice(from,until)) - } - - override def drop(n: Int) = slice(n, size) - - override def take(n: Int) = slice(0, n) - - override def splitAt(n: Int) = (take(n), drop(n)) - - override def filter(f: A => Boolean): Zipper[A] = collect { - case e if f(e) => e - } - - override def collect[B, That](pf: PartialFunction[A, B])(implicit cbf: CanBuildFrom[Zipper[A], B, That]): That = - flatMap(pf.lift andThen { _.toTraversable }) - - override def map[B, That](f: A => B)(implicit cbf: CanBuildFrom[Zipper[A], B, That]): That = { - val liftedF = (a: A) => Seq(f(a)) - flatMap(liftedF)(cbf) - } - - override def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit cbf: CanBuildFrom[Zipper[A], B, That]): That = cbf match { - case cpz: CanProduceZipper[Zipper[A], B, That] if context.isDefined => { - val Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes) = context.get - val b = cpz.lift(Some(parent), this) - for(i <- 0 until nodes.length) { - val (path,_) = metas(i) - b += ElemsWithContextVisible(path, lastUpdate+i+1, f(nodes(i))) - } - - for ((path, time) <- additionalHoles) { - b += ElemsWithContextVisible[B](path, time, util.Vector0) - } - - b ++= hiddenNodes - - b.result() - } - case _ => { - val b = cbf(this) - for(n <- nodes) - b ++= f(n).seq - b.result() - } - } - - override def withFilter(f: A => Boolean) = new WithFilter(List(f)) - - class WithFilter(filters: List[A => Boolean]) extends FilterMonadic[A, Zipper[A]] { - private[this] def sat(a: A) = filters forall { _(a) } - - def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Zipper[A], B, That]) = - self flatMap {a => if (sat(a)) Seq(f(a)) else Nil } - - def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit bf: CanBuildFrom[Zipper[A], B, That]) = - self flatMap {a => if (sat(a)) f(a) else Nil} - - def foreach[B](f: A => B) = self foreach {a => if (sat(a)) f(a)} - - def withFilter(p: A => Boolean) = new WithFilter(p :: filters) - } - - override def toZipper = this - /** * Returns a `Group` containing the same nodes as this Zipper, but without any Zipper context, and in particular, * without any implict references to the zipper's parent. */ def stripZipper = new Group(toVectorCase) - /** - * Optionally replaces each node with 0 to many nodes. Used by `unselect`. See same-named function in `Group` for more details. - */ - private [antixml] override def conditionalFlatMapWithIndex[B >: A <: Node] (f: (A, Int) => Option[scala.collection.Seq[B]]): Zipper[B] = { - /* See the Group implementation for information about how this function is optimized. */ - context match { - case None => brokenZipper(new Group(nodes).conditionalFlatMapWithIndex(f).nodes) - case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { - //Optimistic function that uses `update` - @tailrec - def update(z: Zipper[B], index: Int): Zipper[B] = { - if (index update(z,index + 1) - case Some(r) if r.lengthCompare(1)==0 => update(z.updated(index, r.head), index + 1) - case Some(r) => build(z, r, index) - } - else - z - } - - //Fallback function that uses a builder - def build(z: Zipper[B], currentReplacements: Seq[B], index: Int): Zipper[B] = { - val b = newZipperContextBuilder[B](Some(parent)) - val zc = z.context.get - - for(i <- 0 until index) { - val (p,t) = zc.metas(i) - b += ElemsWithContextVisible(p,t,util.Vector1(z(i))) - } - b += ElemsWithContextVisible(metas(index)._1, zc.lastUpdate+1,currentReplacements) - for(i <- (index + 1) until nodes.length) { - val n = nodes(i) - val m = metas(i) - f(n,i) match { - case None => b += ElemsWithContextVisible(m._1, m._2, util.Vector1(n)) - case Some(r) => b += ElemsWithContextVisible(m._1, zc.lastUpdate + 1 + i - index, r) - } - } - - for((p,t) <- additionalHoles) { - b += ElemsWithContextVisible(p,t,util.Vector0) - } - - b ++= hiddenNodes - - b.result - } - - update(this, 0) - } - } - } - /** Applies the node updates to the parent and returns the result. */ def unselect(implicit zms: ZipperMergeStrategy): Zipper[Node] = { val ctx = context.getOrElse(sys.error("Zipper does not have a valid context")) @@ -556,7 +401,7 @@ object Zipper { def newBuilder[A <: Node] = VectorCase.newBuilder[A].mapResult(brokenZipper(_)) /** Returns a builder that produces a zipper with a full zipper context */ - private def newZipperContextBuilder[A <: Node](parent: Option[Zipper[Node]]) = parent match { + private[antixml] def newZipperContextBuilder[A <: Node](parent: Option[Zipper[Node]]) = parent match { case Some(p) => new WithZipperBuilder[A](p) case None => brokenZipperBuilder[A] } @@ -645,7 +490,7 @@ object Zipper { * probably unlikely in practice. I think it could only occur if there was a very strange sequence * of `flatMap` calls. */ - private class AdditionalHolesBuilder extends Builder[(ZipperPath,Time), immutable.Seq[(ZipperPath,Time)]] {0 + private[antixml] class AdditionalHolesBuilder extends Builder[(ZipperPath,Time), immutable.Seq[(ZipperPath,Time)]] {0 private val hm = mutable.HashMap[ZipperPath,Time]() def += (elem: (ZipperPath,Time)) = { diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala new file mode 100644 index 0000000..8e95ab8 --- /dev/null +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala @@ -0,0 +1,167 @@ +package com.codecommit.antixml + +import Zipper._ +import CanBuildFromWithZipper.ElemsWithContextVisible +import scala.collection.generic.{CanBuildFrom, FilterMonadic} +import scala.collection.GenTraversableOnce +import scala.annotation.tailrec + +/** Contains methods on [[com.codecommit.antixml.Group]] that are overridden in + * the zipper's implementation. + */ +trait ZipperGroupOverrides[+A <: Node] { self: Zipper[A] => + + override protected[this] def newBuilder = Zipper.newBuilder[A] + + override def updated[B >: A <: Node](index: Int, node: B): Zipper[B] = context match { + case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { + val updatedTime = lastUpdate + 1 + val (updatedPath, _) = metas(index) + val updatedMetas = metas.updated(index, (updatedPath, updatedTime)) + val ctx = Context(parent, updatedTime, updatedMetas, additionalHoles, hiddenNodes) + + new Group(nodes.updated(index, node)) with Zipper[B] { + val context = Some(ctx) + } + } + case None => brokenZipper(nodes.updated(index, node)) + } + + override def slice(from: Int, until: Int): Zipper[A] = context match { + case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { + val lo = math.min(math.max(from, 0), nodes.length) + val hi = math.min(math.max(until, lo), nodes.length) + val cnt = hi - lo + + //Put the ZipperPaths from the elided `metas` entries into additionalHoles + val ahs = new AdditionalHolesBuilder() + ahs ++= additionalHoles + for(i <- 0 until lo) + ahs += ((metas(i)._1, lastUpdate + 1 + i)) + for(i <- hi until nodes.length) + ahs += ((metas(i)._1, lastUpdate + 1 + i - cnt)) + + val ctx = Context(parent, lastUpdate + length - cnt, metas.slice(from, until), ahs.result(), hiddenNodes) + + new Group(nodes.slice(from,until)) with Zipper[A] { + val context = Some(ctx) + } + } + case None => brokenZipper(nodes.slice(from,until)) + } + + override def drop(n: Int) = slice(n, size) + + override def take(n: Int) = slice(0, n) + + override def splitAt(n: Int) = (take(n), drop(n)) + + override def filter(f: A => Boolean): Zipper[A] = collect { + case e if f(e) => e + } + + override def collect[B, That](pf: PartialFunction[A, B])(implicit cbf: CanBuildFrom[Zipper[A], B, That]): That = + flatMap(pf.lift andThen { _.toTraversable }) + + override def map[B, That](f: A => B)(implicit cbf: CanBuildFrom[Zipper[A], B, That]): That = { + val liftedF = (a: A) => Seq(f(a)) + flatMap(liftedF)(cbf) + } + + override def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit cbf: CanBuildFrom[Zipper[A], B, That]): That = cbf match { + case cpz: CanProduceZipper[Zipper[A], B, That] if context.isDefined => { + val Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes) = context.get + val b = cpz.lift(Some(parent), this) + for(i <- 0 until nodes.length) { + val (path,_) = metas(i) + b += ElemsWithContextVisible(path, lastUpdate+i+1, f(nodes(i))) + } + + for ((path, time) <- additionalHoles) { + b += ElemsWithContextVisible[B](path, time, util.Vector0) + } + + b ++= hiddenNodes + + b.result() + } + case _ => { + val b = cbf(this) + for(n <- nodes) + b ++= f(n).seq + b.result() + } + } + + override def withFilter(f: A => Boolean) = new WithFilter(List(f)) + + class WithFilter(filters: List[A => Boolean]) extends FilterMonadic[A, Zipper[A]] { + private[this] def sat(a: A) = filters forall { _(a) } + + def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Zipper[A], B, That]) = + self flatMap {a => if (sat(a)) Seq(f(a)) else Nil } + + def flatMap[B, That](f: A => GenTraversableOnce[B])(implicit bf: CanBuildFrom[Zipper[A], B, That]) = + self flatMap {a => if (sat(a)) f(a) else Nil} + + def foreach[B](f: A => B) = self foreach {a => if (sat(a)) f(a)} + + def withFilter(p: A => Boolean) = new WithFilter(p :: filters) + } + + override def toZipper = this + + /** + * Optionally replaces each node with 0 to many nodes. Used by `unselect`. See same-named function in `Group` for more details. + */ + private [antixml] override def conditionalFlatMapWithIndex[B >: A <: Node] (f: (A, Int) => Option[scala.collection.Seq[B]]): Zipper[B] = { + /* See the Group implementation for information about how this function is optimized. */ + context match { + case None => brokenZipper(new Group(nodes).conditionalFlatMapWithIndex(f).nodes) + case Some(Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { + //Optimistic function that uses `update` + @tailrec + def update(z: Zipper[B], index: Int): Zipper[B] = { + if (index update(z,index + 1) + case Some(r) if r.lengthCompare(1)==0 => update(z.updated(index, r.head), index + 1) + case Some(r) => build(z, r, index) + } + else + z + } + + //Fallback function that uses a builder + def build(z: Zipper[B], currentReplacements: Seq[B], index: Int): Zipper[B] = { + val b = newZipperContextBuilder[B](Some(parent)) + val zc = z.context.get + + for(i <- 0 until index) { + val (p,t) = zc.metas(i) + b += ElemsWithContextVisible(p,t,util.Vector1(z(i))) + } + b += ElemsWithContextVisible(metas(index)._1, zc.lastUpdate+1,currentReplacements) + for(i <- (index + 1) until nodes.length) { + val n = nodes(i) + val m = metas(i) + f(n,i) match { + case None => b += ElemsWithContextVisible(m._1, m._2, util.Vector1(n)) + case Some(r) => b += ElemsWithContextVisible(m._1, zc.lastUpdate + 1 + i - index, r) + } + } + + for((p,t) <- additionalHoles) { + b += ElemsWithContextVisible(p,t,util.Vector0) + } + + b ++= hiddenNodes + + b.result + } + + update(this, 0) + } + } + } +} \ No newline at end of file From f566b66cf252aa03efb64ba57f46f18339307201 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Tue, 13 Dec 2011 22:10:19 +0200 Subject: [PATCH 18/27] Pulling Zipper hole shifting and axes into separate traits. This removes the need in implicit conversion for Zipper axes support. --- .../codecommit/antixml/zipper/Zipper.scala | 89 ++---------------- .../antixml/zipper/ZipperAxes.scala | 14 +-- .../antixml/zipper/ZipperHoleShifting.scala | 92 +++++++++++++++++++ .../antixml/zipper/ZipperAxesSpecs.scala | 1 - 4 files changed, 104 insertions(+), 92 deletions(-) create mode 100644 src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala diff --git a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala index 3ba6863..c3a909c 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala @@ -35,7 +35,6 @@ import scala.collection.generic.{CanBuildFrom, FilterMonadic} import scala.collection.immutable.{SortedMap, IndexedSeq, SortedSet} import scala.collection.mutable.Builder import Zipper._ -import Zipper.ZipperPathOrdering import CanBuildFromWithZipper._ /** @@ -123,7 +122,9 @@ import CanBuildFromWithZipper._ */ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] - with ZipperGroupOverrides[A] { self => + with ZipperGroupOverrides[A] + with ZipperHoleShifting + with ZipperAxes { self => /** * Returns the original group that was selected upon when the Zipper was created. A value of `None` indicates that @@ -140,77 +141,6 @@ trait Zipper[+A <: Node] extends Group[A] /** The zipper context or None if this is a broken zipper. */ private[antixml] val context: Option[Context] - /** Shifts the focus of the zipper to another set of holes. - * - * The shifting is performed using a shifting function which is applied to each - * path visible in the zipper and produces a new sequence of paths. - * These paths are sorted lexicographically and duplicates are removed. - * - * A new zipper displaying the above paths is returned while internally maintaining data - * about any previously contained paths. - * - * The values which are attached to the paths come from two sources: - * - If the zipper previously contained the path, the data attached to it is used. - * - If the path is new, the data is fetched directly from the parent of the zipper. - * - * In case a hole was previously multiplied (e.g. using flatMap) it is placed - * as is in the resulting zipper. - * - * Note: shifting is not supported for parentless (broken) zippers. - * - * @param shiftFunc A function to be supplied with the parent of the zipper - * and applied to the indexed contents of the zipper. - * Assumed to produce valid paths with regard to the supplied parent. */ - private[antixml] def shiftHoles(shiftFunc: Group[Node] => ZipperPath => Seq[ZipperPath]): Zipper[Node] = context match { - case Some(context @ Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { - implicit val lexicographic = ZipperPathOrdering - - val shift = shiftFunc(parent) - val unsoretedPaths = for { - m <- metas - path <- shift(m._1) if path != ZipperPath.empty // ignoring empty paths - } yield path - - // not allowing duplicates and empty paths and sorting lexicographically - val newPaths = SortedSet(unsoretedPaths: _*) - val holeInfo = new HoleMapper(context).holeInfo - - val b = newZipperContextBuilder[Node](Some(parent)) - val pathsInit: VectorCase[ElemsWithContextVisible[Node]] = util.Vector0 - - val (unusedPaths, usedPaths) = // leaving paths that were never used before - holeInfo.depthFirst.foldLeft((newPaths, pathsInit)) { case ((nPaths, used), hole) => - val (path, (nodesTimes, masterTime)) = hole - - val holes = - if (nodesTimes.isEmpty) Seq((path, masterTime, util.Vector0)) - else nodesTimes.map { case (n, t) => (path, t, Seq(n)) } // this contains duplicates for multiplied locations - - val visible = (ElemsWithContextVisible.apply[Node] _).tupled - val hidden = (ElemsWithContextHidden.apply _).tupled - - if (nPaths contains path) { - (nPaths - path, used ++ holes.map(visible)) - } else { - b ++= holes.map(hidden) - (nPaths, used) - } - } - - val initTime = 0 // these paths were never modified - val unusedElems = unusedPaths.toList map { p => - ElemsWithContextVisible[Node](p, initTime, PathFetcher.getNode(parent)(p)) - } - - // this can contain duplicate locations from the previously used paths - val visibleElems = (unusedElems ++ usedPaths).sortBy(_.path) - b ++= visibleElems - - b.result - } - case None => sys.error("Cannot shift root.") - } - /** * Returns a `Group` containing the same nodes as this Zipper, but without any Zipper context, and in particular, * without any implict references to the zipper's parent. @@ -224,7 +154,10 @@ trait Zipper[+A <: Node] extends Group[A] } /** Each hole is associated with a list of node/time pairs as well as a master update time */ - private type HoleInfo = ZipperHoleMap[(VectorCase[(Node,Time)],Time)] + private[antixml] type HoleInfo = ZipperHoleMap[(VectorCase[(Node,Time)],Time)] + + /** Converts the given context object into a hole info instance. */ + private[antixml] def toHoleInfo(context: Context) = new HoleMapper(context).holeInfo /** A utility class to convert the contents of the zipper into a hole map. */ private[this] class HoleMapper(context: Context) { @@ -278,7 +211,7 @@ trait Zipper[+A <: Node] extends Group[A] /** Utility class to perform unselect. */ private[this] class Unselector(context: Context, mergeStrategy: ZipperMergeStrategy) { - private val topLevelHoleInfo = new HoleMapper(context).holeInfo + private val topLevelHoleInfo = toHoleInfo(context) /** Applies the node updates to the parent and returns the result. */ def unselect: Zipper[Node] = @@ -391,12 +324,6 @@ object Zipper { } } - /** Lexicographic ordering for path objects. */ - private object ZipperPathOrdering extends Ordering[ZipperPath] { - override def compare(x: ZipperPath, y: ZipperPath) = - Ordering.Iterable[Int].compare(x,y) - } - /** Returns a builder that produces a zipper without a parent */ def newBuilder[A <: Node] = VectorCase.newBuilder[A].mapResult(brokenZipper(_)) diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala index 78375c2..702313a 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala @@ -2,7 +2,7 @@ package com.codecommit.antixml import scala.annotation.tailrec /** - * Wraps [[com.codecommit.antixml.Zipper]] instances with some XPath like axes. + * Adds some XPath like axes to [[com.codecommit.antixml.Zipper]] instances. * * Note1: the axes are applied to each node in a zipper individually and the result * is a new zipper with the nodes concatenated and sorted lexicographically by @@ -11,10 +11,10 @@ import scala.annotation.tailrec * Note2: the axes are calculated using holes in the zipper, hence for a modified * zipper some nodes may be multiplied or elided. */ -class ZipperAxes(zipper: Zipper[Node]) { +trait ZipperAxes { self: Zipper[Node] => /** Returns the direct parent of a node. */ def directParent = { - zipper shiftHoles (g => (PathTransformer(g).shiftUp(_)).andThen(_.toList)) + shiftHoles (g => (PathTransformer(g).shiftUp(_)).andThen(_.toList)) } /** Returns the ancestors of a node. */ @@ -40,7 +40,7 @@ class ZipperAxes(zipper: Zipper[Node]) { * @param appendSource True if the initial path should be part of the result. */ private def transFuncToShift(func: PathTransformer => ZipperPath => Option[ZipperPath], withSource: Boolean) = { - zipper shiftHoles { g => + shiftHoles { g => val pathToOpt = func(PathTransformer(g)) @tailrec @@ -62,10 +62,4 @@ class ZipperAxes(zipper: Zipper[Node]) { } } -} - -object ZipperAxes { - /** Pimps a plain zipper to have axes selection methods. - * TODO move to package object? */ - implicit def zipperToAxes(zipper: Zipper[Node]) = new ZipperAxes(zipper) } \ No newline at end of file diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala new file mode 100644 index 0000000..27a4dd3 --- /dev/null +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala @@ -0,0 +1,92 @@ +package com.codecommit.antixml + +import Zipper._ +import ZipperHoleShifting._ +import util.VectorCase +import scala.collection.immutable.SortedSet +import CanBuildFromWithZipper._ + +/** Responsible for zipper hole shifting support. */ +trait ZipperHoleShifting { self: Zipper[Node] => + + /** Shifts the focus of the zipper to another set of holes. + * + * The shifting is performed using a shifting function which is applied to each + * path visible in the zipper and produces a new sequence of paths. + * These paths are sorted lexicographically and duplicates are removed. + * + * A new zipper displaying the above paths is returned while internally maintaining data + * about any previously contained paths. + * + * The values which are attached to the paths come from two sources: + * - If the zipper previously contained the path, the data attached to it is used. + * - If the path is new, the data is fetched directly from the parent of the zipper. + * + * In case a hole was previously multiplied (e.g. using flatMap) it is placed + * as is in the resulting zipper. + * + * Note: shifting is not supported for parentless (broken) zippers. + * + * @param shiftFunc A function to be supplied with the parent of the zipper + * and applied to the indexed contents of the zipper. + * Assumed to produce valid paths with regard to the supplied parent. + */ + private[antixml] def shiftHoles(shiftFunc: Group[Node] => ZipperPath => Seq[ZipperPath]): Zipper[Node] = context match { + case Some(context @ Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { + implicit val lexicographic = ZipperPathOrdering + + val shift = shiftFunc(parent) + val unsoretedPaths = for { + m <- metas + path <- shift(m._1) if path != ZipperPath.empty // ignoring empty paths + } yield path + + // not allowing duplicates and empty paths and sorting lexicographically + val newPaths = SortedSet(unsoretedPaths: _*) + val holeInfo = toHoleInfo(context) + + val b = newZipperContextBuilder[Node](Some(parent)) + val pathsInit: VectorCase[ElemsWithContextVisible[Node]] = util.Vector0 + + val (unusedPaths, usedPaths) = // leaving paths that were never used before + holeInfo.depthFirst.foldLeft((newPaths, pathsInit)) { + case ((nPaths, used), hole) => + val (path, (nodesTimes, masterTime)) = hole + + val holes = + if (nodesTimes.isEmpty) Seq((path, masterTime, util.Vector0)) + else nodesTimes.map { case (n, t) => (path, t, Seq(n)) } // this contains duplicates for multiplied locations + + val visible = (ElemsWithContextVisible.apply[Node] _).tupled + val hidden = (ElemsWithContextHidden.apply _).tupled + + if (nPaths contains path) { + (nPaths - path, used ++ holes.map(visible)) + } else { + b ++= holes.map(hidden) + (nPaths, used) + } + } + + val initTime = 0 // these paths were never modified + val unusedElems = unusedPaths.toList map { p => + ElemsWithContextVisible[Node](p, initTime, PathFetcher.getNode(parent)(p)) + } + + // this can contain duplicate locations from the previously used paths + val visibleElems = (unusedElems ++ usedPaths).sortBy(_.path) + b ++= visibleElems + + b.result + } + case None => sys.error("Cannot shift root.") + } +} + +object ZipperHoleShifting { + /** Lexicographic ordering for path objects. */ + private object ZipperPathOrdering extends Ordering[ZipperPath] { + override def compare(x: ZipperPath, y: ZipperPath) = + Ordering.Iterable[Int].compare(x,y) + } +} \ No newline at end of file diff --git a/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala index 0101333..04500cf 100644 --- a/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala @@ -2,7 +2,6 @@ package com.codecommit.antixml import org.specs2.mutable._ import XML._ -import ZipperAxes._ import scala.collection.immutable.SortedSet class ZipperAxesSpecs extends SpecificationWithJUnit { From d3773a5ee13556887ceff23c5fe2f6c70b1c6fd9 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Tue, 13 Dec 2011 22:17:47 +0200 Subject: [PATCH 19/27] Adding package private modifier to all Zipper related traits. --- src/main/scala/com/codecommit/antixml/zipper/Zipper.scala | 2 +- src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala | 2 +- .../com/codecommit/antixml/zipper/ZipperGroupOverrides.scala | 2 +- .../com/codecommit/antixml/zipper/ZipperHoleShifting.scala | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala index c3a909c..4c5ebe0 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala @@ -136,7 +136,7 @@ trait Zipper[+A <: Node] extends Group[A] * - A method such as `++`, is used to "add" nodes to a zipper without replacing existing nodes. * **/ - def parent: Option[Zipper[Node]] = context map {_.parent} + def parent: Option[Zipper[Node]] = context map {_.parent} /** The zipper context or None if this is a broken zipper. */ private[antixml] val context: Option[Context] diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala index 702313a..9c84c8c 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala @@ -11,7 +11,7 @@ import scala.annotation.tailrec * Note2: the axes are calculated using holes in the zipper, hence for a modified * zipper some nodes may be multiplied or elided. */ -trait ZipperAxes { self: Zipper[Node] => +private[antixml] trait ZipperAxes { self: Zipper[Node] => /** Returns the direct parent of a node. */ def directParent = { shiftHoles (g => (PathTransformer(g).shiftUp(_)).andThen(_.toList)) diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala index 8e95ab8..dce791d 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala @@ -9,7 +9,7 @@ import scala.annotation.tailrec /** Contains methods on [[com.codecommit.antixml.Group]] that are overridden in * the zipper's implementation. */ -trait ZipperGroupOverrides[+A <: Node] { self: Zipper[A] => +private[antixml] trait ZipperGroupOverrides[+A <: Node] { self: Zipper[A] => override protected[this] def newBuilder = Zipper.newBuilder[A] diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala index 27a4dd3..d352611 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala @@ -7,7 +7,7 @@ import scala.collection.immutable.SortedSet import CanBuildFromWithZipper._ /** Responsible for zipper hole shifting support. */ -trait ZipperHoleShifting { self: Zipper[Node] => +private[antixml] trait ZipperHoleShifting { self: Zipper[Node] => /** Shifts the focus of the zipper to another set of holes. * @@ -83,7 +83,7 @@ trait ZipperHoleShifting { self: Zipper[Node] => } } -object ZipperHoleShifting { +private object ZipperHoleShifting { /** Lexicographic ordering for path objects. */ private object ZipperPathOrdering extends Ordering[ZipperPath] { override def compare(x: ZipperPath, y: ZipperPath) = From bddf6e91cb087d794994d1b391ba5fd8b4e8cad5 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Tue, 13 Dec 2011 22:56:45 +0200 Subject: [PATCH 20/27] Pulled Zipper unselection support into a separate trait. --- .../antixml/ZipperUnselection.scala | 81 +++++++++++++++++++ .../codecommit/antixml/zipper/Zipper.scala | 81 +------------------ 2 files changed, 85 insertions(+), 77 deletions(-) create mode 100644 src/main/scala/com/codecommit/antixml/ZipperUnselection.scala diff --git a/src/main/scala/com/codecommit/antixml/ZipperUnselection.scala b/src/main/scala/com/codecommit/antixml/ZipperUnselection.scala new file mode 100644 index 0000000..9b877b0 --- /dev/null +++ b/src/main/scala/com/codecommit/antixml/ZipperUnselection.scala @@ -0,0 +1,81 @@ +package com.codecommit.antixml + +import Zipper._ +import util.VectorCase +import collection.immutable.IndexedSeq + +/** Provides unselection support for zipper. */ +private[antixml] trait ZipperUnselection { self: Zipper[Node] => + + /** Applies the node updates to the context parent and returns the result. */ + private def unselect(context: Context, mergeStrategy: ZipperMergeStrategy) = { + val topLevelHoleInfo = toHoleInfo(context) + pullBackGroup(context.parent, topLevelHoleInfo, mergeStrategy)._1.asInstanceOf[Zipper[Node]] + } + + /** Returns the pullback of the nodes in the specified group. + * @param nodes the group containing the nodes to pull back. + * @param holeInfo the HoleInfo corresponding to the group. + * @param zms The strategy to be used for conflict resolution. + * @return the pullBacks of the groups children, concatenated together, along with the latest update + * time. + */ + private def pullBackGroup(nodes: Group[Node], holeInfo: HoleInfo, zms: ZipperMergeStrategy): (Group[Node], Time) = { + var maxTime: Int = 0 //mutable for performance and to avoid further complicating`conditionalFlatMapWithIndex`. + val updatedGroup = nodes.conditionalFlatMapWithIndex[Node] { (node, index) => + node match { + case elem: Elem if (holeInfo.hasChildrenAt(index)) => { + val (newNodes, time) = pullUp(elem, index, holeInfo, zms) + maxTime = math.max(maxTime, time) + Some(newNodes) + } + case _ if holeInfo.contains(index) => { + val (newNodes, time) = holeInfo(index) + maxTime = math.max(maxTime, time) + Some(newNodes.map { _._1 }) + } + case _ => None + } + } + (updatedGroup, maxTime) + } + + /** Returns the pullback of an element that is known to be above a hole (and thus has + * child updates that need to be pulled up). + * + * @param elem the element + * @param indexInParent the index of the element in its parent + * @param holeInfo the HoleInfo corresponding to the parent group + * @param zms The strategy to be used for conflict resolution. + * @return the pulled back nodes and their combined update time + * + * @note assumes `holeInfo.hasChildrenAt(indexInParent) == true` + */ + private def pullUp(elem: Elem, indexInParent: Int, holeInfo: HoleInfo, zms: ZipperMergeStrategy): (VectorCase[Node], Time) = { + //Recursively pull back children + val (childGrp, childTime) = pullBackGroup(elem.children, holeInfo.children(indexInParent), zms) + val indirectUpdate = elem.copy(children = childGrp) + if (holeInfo.contains(indexInParent)) { + //This is a conflicted hole, so merge. + mergeConflicts(elem, holeInfo(indexInParent), (indirectUpdate, childTime), zms) + } else { + //No conflicts, just let the child updates bubble up + (VectorCase(indirectUpdate), childTime) + } + } + + /** Merges updates at a conflicted node in the tree. See the unselection algorithm, above, for more information. + * @param node the conflicted node + * @param directUpdates the direct updates to `node`. + * @param indirectUpdate the indirectUpdate to `node`. + * @param mergeStrategy The strategy to be used for conflict resolution. + * @return the sequence of nodes to replace `node`, along with an overall update time for `node`. + */ + private def mergeConflicts(node: Elem, directUpdates: (IndexedSeq[(Node, Time)], Time), indirectUpdate: (Node, Time), mergeStrategy: ZipperMergeStrategy): (VectorCase[Node], Time) = { + val mergeContext = ZipperMergeContext(original = node, lastDirectUpdate = directUpdates._2, directUpdate = directUpdates._1, + indirectUpdate = indirectUpdate) + + val result = mergeStrategy(mergeContext) + (VectorCase.fromSeq(result), math.max(directUpdates._2, indirectUpdate._2)) + } +} \ No newline at end of file diff --git a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala index 4c5ebe0..0cd4648 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala @@ -124,7 +124,8 @@ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] with ZipperGroupOverrides[A] with ZipperHoleShifting - with ZipperAxes { self => + with ZipperAxes + with ZipperUnselection { self => /** * Returns the original group that was selected upon when the Zipper was created. A value of `None` indicates that @@ -150,7 +151,7 @@ trait Zipper[+A <: Node] extends Group[A] /** Applies the node updates to the parent and returns the result. */ def unselect(implicit zms: ZipperMergeStrategy): Zipper[Node] = { val ctx = context.getOrElse(sys.error("Zipper does not have a valid context")) - new Unselector(ctx, zms).unselect + unselect(ctx, zms) } /** Each hole is associated with a list of node/time pairs as well as a master update time */ @@ -207,80 +208,6 @@ trait Zipper[+A <: Node] extends Group[A] } } } - - /** Utility class to perform unselect. */ - private[this] class Unselector(context: Context, mergeStrategy: ZipperMergeStrategy) { - - private val topLevelHoleInfo = toHoleInfo(context) - - /** Applies the node updates to the parent and returns the result. */ - def unselect: Zipper[Node] = - pullBackGroup(context.parent, topLevelHoleInfo)._1.asInstanceOf[Zipper[Node]] - - /** - * Returns the pullback of the nodes in the specified group. - * @param nodes the group containing the nodes to pull back. - * @param holeInfo the HoleInfo corresponding to the group. - * @return the pullBacks of the groups children, concatenated together, along with the latest update - * time. - */ - private[this] def pullBackGroup(nodes: Group[Node], holeInfo: HoleInfo): (Group[Node], Time) = { - var maxTime: Int = 0 //mutable for performance and to avoid further complicating`conditionalFlatMapWithIndex`. - val updatedGroup = nodes.conditionalFlatMapWithIndex[Node] { (node,index) => node match { - case elem:Elem if (holeInfo.hasChildrenAt(index)) => { - val (newNodes, time) = pullUp(elem, index, holeInfo) - maxTime = math.max(maxTime,time) - Some(newNodes) - } - case _ if holeInfo.contains(index) => { - val (newNodes, time) = holeInfo(index) - maxTime = math.max(maxTime, time) - Some(newNodes.map {_._1}) - } - case _ => None - }} - (updatedGroup,maxTime) - } - - /** - * Returns the pullback of an element that is known to be above a hole (and thus has - * child updates that need to be pulled up). - * - * @param elem the element - * @param indexInParent the index of the element in its parent - * @param holeInfo the HoleInfo corresponding to the parent group - * @return the pulled back nodes and their combined update time - * - * @note assumes `holeInfo.hasChildrenAt(indexInParent) == true` - */ - private[this] def pullUp(elem: Elem, indexInParent: Int, holeInfo: HoleInfo): (VectorCase[Node], Time) = { - //Recursively pull back children - val (childGrp, childTime) = pullBackGroup(elem.children, holeInfo.children(indexInParent)) - val indirectUpdate = elem.copy(children = childGrp) - if (holeInfo.contains(indexInParent)) { - //This is a conflicted hole, so merge. - mergeConflicts(elem, holeInfo(indexInParent), (indirectUpdate, childTime)) - } else { - //No conflicts, just let the child updates bubble up - (VectorCase(indirectUpdate), childTime) - } - } - - /** - * Merges updates at a conflicted node in the tree. See the unselection algorithm, above, for more information. - * @param node the conflicted node - * @param directUpdates the direct updates to `node`. - * @param indirectUpdate the indirectUpdate to `node`. - * @return the sequence of nodes to replace `node`, along with an overall update time for `node`. - */ - private def mergeConflicts(node: Elem, directUpdates: (IndexedSeq[(Node,Time)], Time) , indirectUpdate: (Node, Time)): (VectorCase[Node], Time) = { - val mergeContext = ZipperMergeContext(original=node, lastDirectUpdate = directUpdates._2, directUpdate = directUpdates._1, - indirectUpdate = indirectUpdate) - - val result = mergeStrategy(mergeContext) - (VectorCase.fromSeq(result), math.max(directUpdates._2, indirectUpdate._2)) - } - } } object Zipper { @@ -307,7 +234,7 @@ object Zipper { hiddenNodes: immutable.Seq[ElemsWithContextHidden]) /** The units in which time is measured in the zipper. Assumed non negative. */ - private type Time = Int + private[antixml] type Time = Int implicit def canBuildFromWithZipper[A <: Node] = { new CanBuildFromWithZipper[Traversable[_], A, Zipper[A]] { From 2b38bcd8ae494e17c1fbe56dd13f1df3c9d3dd64 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Tue, 13 Dec 2011 23:12:23 +0200 Subject: [PATCH 21/27] Extracting zipper to hole map conversion methods into a trait. --- .../antixml/ZipperUnselection.scala | 5 +- .../codecommit/antixml/zipper/Zipper.scala | 59 +--------------- .../antixml/zipper/ZipperHoleMapper.scala | 68 +++++++++++++++++++ .../antixml/zipper/ZipperHoleShifting.scala | 2 +- 4 files changed, 74 insertions(+), 60 deletions(-) create mode 100644 src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMapper.scala diff --git a/src/main/scala/com/codecommit/antixml/ZipperUnselection.scala b/src/main/scala/com/codecommit/antixml/ZipperUnselection.scala index 9b877b0..1786314 100644 --- a/src/main/scala/com/codecommit/antixml/ZipperUnselection.scala +++ b/src/main/scala/com/codecommit/antixml/ZipperUnselection.scala @@ -1,14 +1,15 @@ package com.codecommit.antixml import Zipper._ +import ZipperHoleMapper._ import util.VectorCase import collection.immutable.IndexedSeq /** Provides unselection support for zipper. */ -private[antixml] trait ZipperUnselection { self: Zipper[Node] => +private[antixml] trait ZipperUnselection extends ZipperHoleMapper { self: Zipper[Node] => /** Applies the node updates to the context parent and returns the result. */ - private def unselect(context: Context, mergeStrategy: ZipperMergeStrategy) = { + def unselect(context: Context, mergeStrategy: ZipperMergeStrategy) = { val topLevelHoleInfo = toHoleInfo(context) pullBackGroup(context.parent, topLevelHoleInfo, mergeStrategy)._1.asInstanceOf[Zipper[Node]] } diff --git a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala index 0cd4648..7ba06ce 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala @@ -123,9 +123,9 @@ import CanBuildFromWithZipper._ trait Zipper[+A <: Node] extends Group[A] with IndexedSeqLike[A, Zipper[A]] with ZipperGroupOverrides[A] + with ZipperUnselection with ZipperHoleShifting - with ZipperAxes - with ZipperUnselection { self => + with ZipperAxes { self => /** * Returns the original group that was selected upon when the Zipper was created. A value of `None` indicates that @@ -153,61 +153,6 @@ trait Zipper[+A <: Node] extends Group[A] val ctx = context.getOrElse(sys.error("Zipper does not have a valid context")) unselect(ctx, zms) } - - /** Each hole is associated with a list of node/time pairs as well as a master update time */ - private[antixml] type HoleInfo = ZipperHoleMap[(VectorCase[(Node,Time)],Time)] - - /** Converts the given context object into a hole info instance. */ - private[antixml] def toHoleInfo(context: Context) = new HoleMapper(context).holeInfo - - /** A utility class to convert the contents of the zipper into a hole map. */ - private[this] class HoleMapper(context: Context) { - private val initHoleInfoItem:(VectorCase[(Node,Time)],Time) = (util.Vector0,0) - - type HoleMapGet[A] = A => (ZipperPath, Time, GenTraversableOnce[Node]) - - /** Adding items to the hole info object using the given getter function to transform the items - * into the appropriate format. */ - private def addToHoleInfo[A](items: Seq[A], h: HoleInfo, get: HoleMapGet[A]) = { - (h /: items) { (hi, item) => - val (path, time, nodes) = get(item) - val (oldNodes, oldTime) = hi.getDeep(path).getOrElse(initHoleInfoItem) - val newItems = (oldNodes /: nodes) { case(old, node) => old :+ (node, time) } - val newTime = math.max(oldTime, time) - hi.updatedDeep(path, (newItems, newTime)) - } - } - - val holeInfo: HoleInfo = { - val Context(_, _, metas, additionalHoles, hiddenNodes) = context - - /* Getters for the different parts of the zipper. */ - - val indicesGet = (i: Int) => { - val (path, time) = metas(i) - val items = util.Vector1(self(i)) - (path, time, items) - } - - val hiddenGet = (ewc: ElemsWithContextHidden) => { - val ElemsWithContextHidden(path, time, items) = ewc - (path, time, items) - } - - val additonalGet = (pt: (ZipperPath, Time)) => { - val (path, time) = pt - (path, time, util.Vector0) - } - - case class ItemsGet[A](items: Seq[A], get: HoleMapGet[A]) - val itemsGetters = List(ItemsGet(indices, indicesGet), ItemsGet(hiddenNodes, hiddenGet), ItemsGet(additionalHoles, additonalGet)) - - val holeInit: HoleInfo = ZipperHoleMap.empty - (holeInit /: itemsGetters) { case (hi, ItemsGet(items, get)) => - addToHoleInfo(items, hi, get) - } - } - } } object Zipper { diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMapper.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMapper.scala new file mode 100644 index 0000000..de47bd2 --- /dev/null +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMapper.scala @@ -0,0 +1,68 @@ +package com.codecommit.antixml + +import Zipper._ +import ZipperHoleMapper._ +import util.VectorCase +import scala.collection.GenTraversableOnce +import CanBuildFromWithZipper._ + +/** Converts a zipper into a hole map. */ +private[antixml] trait ZipperHoleMapper { self: Zipper[Node] => + + private val initHoleInfoItem: (VectorCase[(Node, Time)], Time) = (util.Vector0, 0) + + private type HoleMapGet[A] = A => (ZipperPath, Time, GenTraversableOnce[Node]) + + /** Adding items to the hole info object using the given getter function to transform the items + * into the appropriate format. + */ + private def addToHoleInfo[A](items: Seq[A], h: HoleInfo, get: HoleMapGet[A]) = { + (h /: items) { (hi, item) => + val (path, time, nodes) = get(item) + val (oldNodes, oldTime) = hi.getDeep(path).getOrElse(initHoleInfoItem) + val newItems = (oldNodes /: nodes) { case (old, node) => old :+ (node, time) } + val newTime = math.max(oldTime, time) + hi.updatedDeep(path, (newItems, newTime)) + } + } + + /** Using the given context to convert self to a hole map. */ + def toHoleInfo(context: Context): HoleInfo = { + val Context(_, _, metas, additionalHoles, hiddenNodes) = context + + /* Getters for the different parts of the zipper. */ + + val indicesGet = (i: Int) => { + val (path, time) = metas(i) + val items = util.Vector1(self(i)) + (path, time, items) + } + + val hiddenGet = (ewc: ElemsWithContextHidden) => { + val ElemsWithContextHidden(path, time, items) = ewc + (path, time, items) + } + + val additonalGet = (pt: (ZipperPath, Time)) => { + val (path, time) = pt + (path, time, util.Vector0) + } + + case class ItemsGet[A](items: Seq[A], get: HoleMapGet[A]) + val itemsGetters = List( + ItemsGet(indices, indicesGet), + ItemsGet(hiddenNodes, hiddenGet), + ItemsGet(additionalHoles, additonalGet)) + + val holeInit: HoleInfo = ZipperHoleMap.empty + (holeInit /: itemsGetters) { + case (hi, ItemsGet(items, get)) => + addToHoleInfo(items, hi, get) + } + } +} + +object ZipperHoleMapper { + /** Each hole is associated with a list of node/time pairs as well as a master update time */ + private[antixml] type HoleInfo = ZipperHoleMap[(VectorCase[(Node,Time)],Time)] +} \ No newline at end of file diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala index d352611..f924d1c 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala @@ -7,7 +7,7 @@ import scala.collection.immutable.SortedSet import CanBuildFromWithZipper._ /** Responsible for zipper hole shifting support. */ -private[antixml] trait ZipperHoleShifting { self: Zipper[Node] => +private[antixml] trait ZipperHoleShifting extends ZipperHoleMapper { self: Zipper[Node] => /** Shifts the focus of the zipper to another set of holes. * From 9338efb0347358c64f470a02c838ceaaef595998 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Tue, 13 Dec 2011 23:15:38 +0200 Subject: [PATCH 22/27] Removing the `parent` method on Zipper, doesn't seem to be used anywhere. --- .../scala/com/codecommit/antixml/zipper/Zipper.scala | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala index 7ba06ce..609411e 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala @@ -127,19 +127,15 @@ trait Zipper[+A <: Node] extends Group[A] with ZipperHoleShifting with ZipperAxes { self => - /** - * Returns the original group that was selected upon when the Zipper was created. A value of `None` indicates that + /** The zipper context or None if this is a broken zipper. + * A value of `None` indicates that * the Zipper has no parent and `unselect` cannot be called. This possibility is an unfortunate consequence of the * fact that some operations must return the static type of Zipper even though they yield no usable context. * In practice, this situation is usually caused by one of the following operations: * * - A non-Zipper group is selected upon and then 'unselect' is used to generate an updated group. * - A method such as `++`, is used to "add" nodes to a zipper without replacing existing nodes. - * - **/ - def parent: Option[Zipper[Node]] = context map {_.parent} - - /** The zipper context or None if this is a broken zipper. */ + */ private[antixml] val context: Option[Context] /** From 31ab20a011daaaf6a6c3bec85482e59ed63a1e4d Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sun, 8 Jan 2012 16:06:53 -0700 Subject: [PATCH 23/27] Refactored all the zipper stuff into its own package --- src/main/scala/com/codecommit/antixml/Group.scala | 1 + src/main/scala/com/codecommit/antixml/Selectable.scala | 4 +++- .../scala/com/codecommit/antixml/{zipper => }/Zipper.scala | 2 ++ .../codecommit/antixml/zipper/CanBuildFromWithZipper.scala | 1 + .../com/codecommit/antixml/{ => zipper}/PathCreator.scala | 1 + .../com/codecommit/antixml/{ => zipper}/PathFetcher.scala | 2 ++ .../codecommit/antixml/{ => zipper}/PathTransformer.scala | 4 +++- .../scala/com/codecommit/antixml/zipper/ZipperAxes.scala | 2 ++ .../com/codecommit/antixml/zipper/ZipperGroupOverrides.scala | 1 + .../scala/com/codecommit/antixml/zipper/ZipperHoleMap.scala | 1 + .../com/codecommit/antixml/zipper/ZipperHoleMapper.scala | 1 + .../com/codecommit/antixml/zipper/ZipperHoleShifting.scala | 1 + .../com/codecommit/antixml/zipper/ZipperMergeContext.scala | 1 + .../com/codecommit/antixml/zipper/ZipperMergeStrategy.scala | 1 + .../scala/com/codecommit/antixml/zipper/ZipperPath.scala | 1 + .../codecommit/antixml/{ => zipper}/ZipperUnselection.scala | 1 + .../scala/com/codecommit/antixml/performance/package.scala | 2 ++ .../codecommit/antixml/{ => zipper}/PathCreatorSpecs.scala | 5 +++-- .../codecommit/antixml/{ => zipper}/PathFetcherSpecs.scala | 1 + .../antixml/{ => zipper}/PathTransformerSpecs.scala | 1 + .../com/codecommit/antixml/zipper/ZipperAxesSpecs.scala | 1 + .../com/codecommit/antixml/zipper/ZipperHoleMapSpecs.scala | 1 + .../codecommit/antixml/zipper/ZipperMergeStrategySpecs.scala | 3 ++- .../com/codecommit/antixml/zipper/ZipperPathSpecs.scala | 1 + .../scala/com/codecommit/antixml/zipper/ZipperSpecs.scala | 1 + 25 files changed, 36 insertions(+), 5 deletions(-) rename src/main/scala/com/codecommit/antixml/{zipper => }/Zipper.scala (99%) rename src/main/scala/com/codecommit/antixml/{ => zipper}/PathCreator.scala (97%) rename src/main/scala/com/codecommit/antixml/{ => zipper}/PathFetcher.scala (95%) rename src/main/scala/com/codecommit/antixml/{ => zipper}/PathTransformer.scala (95%) rename src/main/scala/com/codecommit/antixml/{ => zipper}/ZipperUnselection.scala (97%) rename src/test/scala/com/codecommit/antixml/{ => zipper}/PathCreatorSpecs.scala (96%) rename src/test/scala/com/codecommit/antixml/{ => zipper}/PathFetcherSpecs.scala (96%) rename src/test/scala/com/codecommit/antixml/{ => zipper}/PathTransformerSpecs.scala (95%) diff --git a/src/main/scala/com/codecommit/antixml/Group.scala b/src/main/scala/com/codecommit/antixml/Group.scala index 3ea727b..108debb 100644 --- a/src/main/scala/com/codecommit/antixml/Group.scala +++ b/src/main/scala/com/codecommit/antixml/Group.scala @@ -30,6 +30,7 @@ package com.codecommit package antixml import util._ +import zipper._ import scala.annotation.unchecked.uncheckedVariance import scala.annotation.tailrec diff --git a/src/main/scala/com/codecommit/antixml/Selectable.scala b/src/main/scala/com/codecommit/antixml/Selectable.scala index ccf4df1..e05a054 100644 --- a/src/main/scala/com/codecommit/antixml/Selectable.scala +++ b/src/main/scala/com/codecommit/antixml/Selectable.scala @@ -29,10 +29,12 @@ package com.codecommit package antixml +import zipper._ + import scala.collection.generic.{CanBuildFrom, HasNewBuilder} import scala.collection.immutable.VectorBuilder -import com.codecommit.antixml.CanBuildFromWithZipper.ElemsWithContextVisible +import CanBuildFromWithZipper.ElemsWithContextVisible import com.codecommit.antixml.util.VectorCase trait Selectable[+A <: Node] { diff --git a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala b/src/main/scala/com/codecommit/antixml/Zipper.scala similarity index 99% rename from src/main/scala/com/codecommit/antixml/zipper/Zipper.scala rename to src/main/scala/com/codecommit/antixml/Zipper.scala index 609411e..b0f3ec8 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/Zipper.scala @@ -28,6 +28,8 @@ package com.codecommit.antixml +import zipper._ + import com.codecommit.antixml.util.VectorCase import scala.annotation.tailrec import scala.collection.{immutable, mutable, IndexedSeqLike, GenTraversableOnce} diff --git a/src/main/scala/com/codecommit/antixml/zipper/CanBuildFromWithZipper.scala b/src/main/scala/com/codecommit/antixml/zipper/CanBuildFromWithZipper.scala index 5999f32..b89752f 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/CanBuildFromWithZipper.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/CanBuildFromWithZipper.scala @@ -27,6 +27,7 @@ */ package com.codecommit.antixml +package zipper import scala.collection.GenTraversableOnce import scala.collection.mutable.Builder diff --git a/src/main/scala/com/codecommit/antixml/PathCreator.scala b/src/main/scala/com/codecommit/antixml/zipper/PathCreator.scala similarity index 97% rename from src/main/scala/com/codecommit/antixml/PathCreator.scala rename to src/main/scala/com/codecommit/antixml/zipper/PathCreator.scala index 4d1fbc0..6cf75df 100644 --- a/src/main/scala/com/codecommit/antixml/PathCreator.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/PathCreator.scala @@ -27,6 +27,7 @@ */ package com.codecommit.antixml +package zipper import util.VectorCase diff --git a/src/main/scala/com/codecommit/antixml/PathFetcher.scala b/src/main/scala/com/codecommit/antixml/zipper/PathFetcher.scala similarity index 95% rename from src/main/scala/com/codecommit/antixml/PathFetcher.scala rename to src/main/scala/com/codecommit/antixml/zipper/PathFetcher.scala index 07063cb..9645e98 100644 --- a/src/main/scala/com/codecommit/antixml/PathFetcher.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/PathFetcher.scala @@ -1,4 +1,6 @@ package com.codecommit.antixml +package zipper + import scala.annotation.tailrec /** diff --git a/src/main/scala/com/codecommit/antixml/PathTransformer.scala b/src/main/scala/com/codecommit/antixml/zipper/PathTransformer.scala similarity index 95% rename from src/main/scala/com/codecommit/antixml/PathTransformer.scala rename to src/main/scala/com/codecommit/antixml/zipper/PathTransformer.scala index 0bca4f0..e7aab95 100644 --- a/src/main/scala/com/codecommit/antixml/PathTransformer.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/PathTransformer.scala @@ -1,7 +1,7 @@ package com.codecommit.antixml +package zipper import scala.annotation.tailrec -import PathTransformer._ import PathFetcher._ /** Transforms [[com.codecommit.antixml.ZipperPath]]s with predefined functions. @@ -13,6 +13,8 @@ import PathFetcher._ * @param source The source for the transformed paths. */ private[antixml] case class PathTransformer(source: Group[Node]) { + import PathTransformer._ + //TODO this whole class is probably quite slow, ZipperPath is not optimized for modifications diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala index 9c84c8c..d1f495c 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala @@ -1,4 +1,6 @@ package com.codecommit.antixml +package zipper + import scala.annotation.tailrec /** diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala index dce791d..ae3148f 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperGroupOverrides.scala @@ -1,4 +1,5 @@ package com.codecommit.antixml +package zipper import Zipper._ import CanBuildFromWithZipper.ElemsWithContextVisible diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMap.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMap.scala index 9aeeb80..c31a91b 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMap.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMap.scala @@ -27,6 +27,7 @@ */ package com.codecommit.antixml +package zipper import util.{VectorCase} import scala.annotation.tailrec diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMapper.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMapper.scala index de47bd2..031b072 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMapper.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleMapper.scala @@ -1,4 +1,5 @@ package com.codecommit.antixml +package zipper import Zipper._ import ZipperHoleMapper._ diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala index f924d1c..100f61e 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala @@ -1,4 +1,5 @@ package com.codecommit.antixml +package zipper import Zipper._ import ZipperHoleShifting._ diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeContext.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeContext.scala index 90a0262..3c6efa8 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeContext.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeContext.scala @@ -27,6 +27,7 @@ */ package com.codecommit.antixml +package zipper import scala.collection.immutable.IndexedSeq diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeStrategy.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeStrategy.scala index 1234b49..528e1fb 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeStrategy.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperMergeStrategy.scala @@ -27,6 +27,7 @@ */ package com.codecommit.antixml +package zipper import util.VectorCase import scala.collection.immutable.IndexedSeq diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperPath.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperPath.scala index 761b1cf..56dfcda 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperPath.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperPath.scala @@ -27,6 +27,7 @@ */ package com.codecommit.antixml +package zipper import scala.collection.{IndexedSeqOptimized,Seq, LinearSeq} import scala.collection.generic.CanBuildFrom diff --git a/src/main/scala/com/codecommit/antixml/ZipperUnselection.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperUnselection.scala similarity index 97% rename from src/main/scala/com/codecommit/antixml/ZipperUnselection.scala rename to src/main/scala/com/codecommit/antixml/zipper/ZipperUnselection.scala index 1786314..afa9e42 100644 --- a/src/main/scala/com/codecommit/antixml/ZipperUnselection.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperUnselection.scala @@ -1,4 +1,5 @@ package com.codecommit.antixml +package zipper import Zipper._ import ZipperHoleMapper._ diff --git a/src/test/scala/com/codecommit/antixml/performance/package.scala b/src/test/scala/com/codecommit/antixml/performance/package.scala index bf570b5..1dc86c7 100644 --- a/src/test/scala/com/codecommit/antixml/performance/package.scala +++ b/src/test/scala/com/codecommit/antixml/performance/package.scala @@ -31,6 +31,8 @@ package com.codecommit.antixml import scala.collection.generic.CanBuildFrom import scala.collection.mutable.Builder +import zipper._ + package object performance { def simpleNameOf(n: org.w3c.dom.Node) = diff --git a/src/test/scala/com/codecommit/antixml/PathCreatorSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/PathCreatorSpecs.scala similarity index 96% rename from src/test/scala/com/codecommit/antixml/PathCreatorSpecs.scala rename to src/test/scala/com/codecommit/antixml/zipper/PathCreatorSpecs.scala index 75cecf4..3a0b9b9 100644 --- a/src/test/scala/com/codecommit/antixml/PathCreatorSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/PathCreatorSpecs.scala @@ -27,10 +27,11 @@ */ package com.codecommit.antixml +package zipper import org.specs2.mutable._ -import com.codecommit.antixml.PathCreator._ -import com.codecommit.antixml.Zipper._ +import PathCreator._ +import Zipper._ import XML._ import scala.math.Ordering diff --git a/src/test/scala/com/codecommit/antixml/PathFetcherSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/PathFetcherSpecs.scala similarity index 96% rename from src/test/scala/com/codecommit/antixml/PathFetcherSpecs.scala rename to src/test/scala/com/codecommit/antixml/zipper/PathFetcherSpecs.scala index cd5566b..33aef9a 100644 --- a/src/test/scala/com/codecommit/antixml/PathFetcherSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/PathFetcherSpecs.scala @@ -1,4 +1,5 @@ package com.codecommit.antixml +package zipper import org.specs2.mutable._ import org.specs2.matcher.DataTables diff --git a/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/PathTransformerSpecs.scala similarity index 95% rename from src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala rename to src/test/scala/com/codecommit/antixml/zipper/PathTransformerSpecs.scala index d204991..b497489 100644 --- a/src/test/scala/com/codecommit/antixml/PathTransformerSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/PathTransformerSpecs.scala @@ -1,4 +1,5 @@ package com.codecommit.antixml +package zipper import org.specs2.mutable._ import org.specs2.matcher.DataTables diff --git a/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala index 04500cf..937bbdd 100644 --- a/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala @@ -1,4 +1,5 @@ package com.codecommit.antixml +package zipper import org.specs2.mutable._ import XML._ diff --git a/src/test/scala/com/codecommit/antixml/zipper/ZipperHoleMapSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperHoleMapSpecs.scala index 1d268ec..f9b16f2 100644 --- a/src/test/scala/com/codecommit/antixml/zipper/ZipperHoleMapSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/ZipperHoleMapSpecs.scala @@ -27,6 +27,7 @@ */ package com.codecommit.antixml +package zipper import org.specs2.mutable._ import org.specs2.ScalaCheck diff --git a/src/test/scala/com/codecommit/antixml/zipper/ZipperMergeStrategySpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperMergeStrategySpecs.scala index 267fe8d..152bc84 100644 --- a/src/test/scala/com/codecommit/antixml/zipper/ZipperMergeStrategySpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/ZipperMergeStrategySpecs.scala @@ -27,9 +27,10 @@ */ package com.codecommit.antixml +package zipper import org.specs2.mutable._ -import com.codecommit.antixml.ZipperMergeStrategy._ +import ZipperMergeStrategy._ import XML._ class ZipperMergeStrategySpecs extends SpecificationWithJUnit { diff --git a/src/test/scala/com/codecommit/antixml/zipper/ZipperPathSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperPathSpecs.scala index bdd8181..5d99900 100644 --- a/src/test/scala/com/codecommit/antixml/zipper/ZipperPathSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/ZipperPathSpecs.scala @@ -27,6 +27,7 @@ */ package com.codecommit.antixml +package zipper import org.specs2.mutable._ import org.specs2.ScalaCheck diff --git a/src/test/scala/com/codecommit/antixml/zipper/ZipperSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperSpecs.scala index c44cbf8..f7251af 100644 --- a/src/test/scala/com/codecommit/antixml/zipper/ZipperSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/ZipperSpecs.scala @@ -27,6 +27,7 @@ */ package com.codecommit.antixml +package zipper import org.specs2.mutable._ import org.specs2.ScalaCheck From 2c45ad94ca151cea9e666a5803d11f81e0da66cc Mon Sep 17 00:00:00 2001 From: Daniel Spiewak Date: Sun, 8 Jan 2012 16:19:59 -0700 Subject: [PATCH 24/27] Was over-zealous in my refactoring; ZipperSpecs should remain at root --- .../com/codecommit/antixml/{zipper => }/ZipperSpecs.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename src/test/scala/com/codecommit/antixml/{zipper => }/ZipperSpecs.scala (97%) diff --git a/src/test/scala/com/codecommit/antixml/zipper/ZipperSpecs.scala b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala similarity index 97% rename from src/test/scala/com/codecommit/antixml/zipper/ZipperSpecs.scala rename to src/test/scala/com/codecommit/antixml/ZipperSpecs.scala index f7251af..7e35611 100644 --- a/src/test/scala/com/codecommit/antixml/zipper/ZipperSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala @@ -27,13 +27,14 @@ */ package com.codecommit.antixml -package zipper + +import zipper._ import org.specs2.mutable._ import org.specs2.ScalaCheck import org.scalacheck._ import XML._ -import com.codecommit.antixml.Zipper._ +import Zipper._ import scala.io.Source class ZipperSpecs extends SpecificationWithJUnit with ScalaCheck with XMLGenerators { From b6313c3db8876de604907945c9f9fb0c31d47613 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Sun, 15 Jan 2012 23:28:17 +0200 Subject: [PATCH 25/27] Whitespace fix. --- .../com/codecommit/antixml/zipper/PathTransformer.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/zipper/PathTransformer.scala b/src/main/scala/com/codecommit/antixml/zipper/PathTransformer.scala index e7aab95..2c6de44 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/PathTransformer.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/PathTransformer.scala @@ -36,7 +36,7 @@ private[antixml] case class PathTransformer(source: Group[Node]) { def shiftRight(path: ZipperPath): Option[ZipperPath] = { shiftSideways(path, +1) } - + /** Tries to shift the path sideways by the given increment. */ private def shiftSideways(path: ZipperPath, increment: Int): Option[ZipperPath] = { assert(!path.isEmpty, "Cannot shift an empty path.") @@ -52,7 +52,7 @@ private[antixml] case class PathTransformer(source: Group[Node]) { val newLoc = path(end) + increment - if (currLevel.indices.contains(newLoc)) Some(path.updated(end, newLoc)) - else None + if (currLevel.indices.contains(newLoc)) Some(path.updated(end, newLoc)) + else None } } \ No newline at end of file From 191fd38a8a3ecd615be7e8b0a3f9bfdb99421823 Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Mon, 16 Jan 2012 00:32:11 +0200 Subject: [PATCH 26/27] Adding descendant Zipper axes. Abusing PathCreator to reuse code, probably hurting performance. --- .../antixml/zipper/ZipperAxes.scala | 24 +++++- .../antixml/zipper/ZipperHoleShifting.scala | 2 +- .../antixml/zipper/ZipperAxesSpecs.scala | 84 ++++++++++++------- 3 files changed, 78 insertions(+), 32 deletions(-) diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala index d1f495c..30aee61 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperAxes.scala @@ -2,6 +2,7 @@ package com.codecommit.antixml package zipper import scala.annotation.tailrec +import PathCreator.PathVals /** * Adds some XPath like axes to [[com.codecommit.antixml.Zipper]] instances. @@ -36,9 +37,30 @@ private[antixml] trait ZipperAxes { self: Zipper[Node] => /** Returns the preceding siblings of a node including itself. */ def precedingSiblingOrSelf = transFuncToShift(_.shiftLeft, true) + + /** Returns the descendants of a node. + * + * Note: this method is not optimized, use plain zipper selection for more efficient results. */ + def descendant = pathCreatorFuncToShift(PathCreator.allChildren) + + /** Returns the descendants of a node including itself. + * + * Note: this method is not optimized, use plain zipper selection for more efficient results. */ + def descendantOrSelf = pathCreatorFuncToShift(PathCreator.all) + + /** Takes a [[PathCreator]] function and turns it into a shifting function. */ + private def pathCreatorFuncToShift(func: Selector[Node] => Group[Node] => PathVals[Node]) = { + shiftHoles { parent => + path => + val group = Group(PathFetcher.getNode(parent)(path).toList: _*) + //TODO redundant calculation of path values + //TODO path is not efficient for this sort of operations + func(*)(group).map(path ++ _.path.tail) // converting path to original parent's "coordinates" + } + } /** Takes a path transformer function and converts it to a shifting function which is applied until - * the transformer return `None`. + * the transformer returns `None`. * @param appendSource True if the initial path should be part of the result. */ private def transFuncToShift(func: PathTransformer => ZipperPath => Option[ZipperPath], withSource: Boolean) = { diff --git a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala index 100f61e..a8fb0f3 100644 --- a/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala +++ b/src/main/scala/com/codecommit/antixml/zipper/ZipperHoleShifting.scala @@ -32,7 +32,7 @@ private[antixml] trait ZipperHoleShifting extends ZipperHoleMapper { self: Zippe * and applied to the indexed contents of the zipper. * Assumed to produce valid paths with regard to the supplied parent. */ - private[antixml] def shiftHoles(shiftFunc: Group[Node] => ZipperPath => Seq[ZipperPath]): Zipper[Node] = context match { + private[antixml] def shiftHoles(shiftFunc: Group[Node] => ZipperPath => Traversable[ZipperPath]): Zipper[Node] = context match { case Some(context @ Context(parent, lastUpdate, metas, additionalHoles, hiddenNodes)) => { implicit val lexicographic = ZipperPathOrdering diff --git a/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala b/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala index 937bbdd..fd9b22c 100644 --- a/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/zipper/ZipperAxesSpecs.scala @@ -20,8 +20,10 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { zipper.followingSiblingOrSelf.unselect mustEqual res zipper.precedingSibling.unselect mustEqual res zipper.precedingSiblingOrSelf.unselect mustEqual res + zipper.descendant.unselect mustEqual res + zipper.descendantOrSelf.unselect mustEqual res } - + "fail on broken zippers" in { bookstore.toZipper.directParent.unselect must throwA[RuntimeException] bookstore.toZipper.ancestor.unselect must throwA[RuntimeException] @@ -30,6 +32,8 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { bookstore.toZipper.followingSiblingOrSelf.unselect must throwA[RuntimeException] bookstore.toZipper.precedingSibling.unselect must throwA[RuntimeException] bookstore.toZipper.precedingSiblingOrSelf.unselect must throwA[RuntimeException] + bookstore.toZipper.descendant.unselect must throwA[RuntimeException] + bookstore.toZipper.descendantOrSelf.unselect must throwA[RuntimeException] } } @@ -37,15 +41,13 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { "be empty for root" in { val res = bookstore select 'bookstore directParent - res mustEqual Group() - res.unselect mustEqual bookstore + verify(res)(Group()) } "return the direct parents of nodes" in { val res = bookstore \\ 'author directParent; - res mustEqual (bookstore \\ 'book) - res.unselect mustEqual bookstore + verify(res)(bookstore \\ 'book) } } @@ -53,15 +55,13 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { "be empty for root" in { val res = bookstore select 'bookstore ancestor - res mustEqual Group() - res.unselect mustEqual bookstore + verify(res)(Group()) } "return all ancestors of nodes" in { val res = bookstore \\ 'author ancestor; - res mustEqual (bookstore ++ bookstore \\ 'book) - res.unselect mustEqual bookstore + verify(res)(bookstore ++ bookstore \\ 'book) } } @@ -69,8 +69,7 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { "return self for root" in { val res = bookstore select 'bookstore ancestorOrSelf - res mustEqual bookstore - res.unselect mustEqual bookstore + verify(res)(bookstore) } "return all ancestors and self of nodes" in { @@ -79,9 +78,7 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { val res = authors ancestorOrSelf; - - res mustEqual (bookstore :+ books(0) :+ authors(0) :+ books(1) :+ authors(1) :+ books(2) :+ authors(2) :+ authors(3)) - res.unselect mustEqual bookstore + verify(res)(bookstore :+ books(0) :+ authors(0) :+ books(1) :+ authors(1) :+ books(2) :+ authors(2) :+ authors(3)) } } @@ -89,15 +86,13 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { "return nothing on rightmost node" in { val res = bookstore \\ 'author followingSibling - res mustEqual Group((bookstore \\ 'author).apply(3)) - res.unselect mustEqual bookstore + verify(res)(Group((bookstore \\ 'author).apply(3))) } "return all following siblings of nodes" in { val res = bookstore \\ 'title followingSibling - res mustEqual bookstore \\ 'author - res.unselect mustEqual bookstore + verify(res)(bookstore \\ 'author) } } @@ -105,15 +100,13 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { "return self for rightmost nodes" in { val res = bookstore \\ 'author followingSiblingOrSelf - res mustEqual (bookstore \\ 'author) - res.unselect mustEqual bookstore + verify(res)(bookstore \\ 'author) } "return all following siblings and self of nodes" in { val res = bookstore \\ 'title followingSiblingOrSelf - res mustEqual bookstore \ 'book \ * - res.unselect mustEqual bookstore + verify(res)(bookstore \ 'book \ *) } } @@ -121,15 +114,13 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { "return nothing on leftmost node" in { val res = bookstore \\ 'title precedingSibling - res mustEqual Group() - res.unselect mustEqual bookstore + verify(res)(Group()) } "return all preceding siblings of nodes" in { val res = bookstore \\ 'author precedingSibling - res mustEqual (bookstore \\ 'title) :+ (bookstore \\ 'author).apply(2) - res.unselect mustEqual bookstore + verify(res)((bookstore \\ 'title) :+ (bookstore \\ 'author).apply(2)) } } @@ -137,18 +128,51 @@ class ZipperAxesSpecs extends SpecificationWithJUnit { "return self for leftmost nodes" in { val res = bookstore \\ 'title precedingSiblingOrSelf - res mustEqual (bookstore \\ 'title) - res.unselect mustEqual bookstore + verify(res)(bookstore \\ 'title) } "return all preceding siblings and self of nodes" in { val res = bookstore \\ 'author precedingSiblingOrSelf - res mustEqual bookstore \ 'book \ * - res.unselect mustEqual bookstore + verify(res)(bookstore \ 'book \ *) + } + } + + val text = Selector({ case t: Text => t }) + + "Descendant axes" should { + "return empty for leaf nodes" in { + val res = bookstore \\ text descendant + + verify(res)(Group()) + } + + "return all descendants of nodes" in { + val res = bookstore \\ 'book descendant + + verify(res)(bookstore \\ 'book \\ *) } } + "Descendant or self axes" should { + "return self for leaf nodes" in { + val res = bookstore \\ text descendantOrSelf + + verify(res)(bookstore \\ text) + } + + "return all descendants and self of nodes" in { + val res = bookstore \\ 'book descendantOrSelf + + verify(res)(bookstore \\ *) + } + } + + def verify(result: Zipper[Node])(expected: Group[Node]) = { + result mustEqual expected + result.unselect mustEqual bookstore + } + def resource(filename: String) = XML fromSource (scala.io.Source fromURL (getClass getResource ("/" + filename))) } \ No newline at end of file From e84221ba9c4329b466c96a161358bc0bc1711f9f Mon Sep 17 00:00:00 2001 From: Daniel Beskin Date: Mon, 16 Jan 2012 00:54:57 +0200 Subject: [PATCH 27/27] Adding unselectAll to Zipper. --- .../scala/com/codecommit/antixml/Zipper.scala | 10 +++++++++ .../com/codecommit/antixml/ZipperSpecs.scala | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/main/scala/com/codecommit/antixml/Zipper.scala b/src/main/scala/com/codecommit/antixml/Zipper.scala index b0f3ec8..268374f 100644 --- a/src/main/scala/com/codecommit/antixml/Zipper.scala +++ b/src/main/scala/com/codecommit/antixml/Zipper.scala @@ -151,6 +151,16 @@ trait Zipper[+A <: Node] extends Group[A] val ctx = context.getOrElse(sys.error("Zipper does not have a valid context")) unselect(ctx, zms) } + + /** Recursively applying [[Zipper#unselect]] until reaching the root. + * Guaranteed not to fail upon root unselection. */ + def unselectAll(implicit zms: ZipperMergeStrategy): Group[Node] = { + def unselectRecur(z: Zipper[Node]): Group[Node] = { + if (z.context.isDefined) unselectRecur(z.unselect) + else z.stripZipper + } + unselectRecur(this) + } } object Zipper { diff --git a/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala index 7e35611..ab95c4f 100644 --- a/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala +++ b/src/test/scala/com/codecommit/antixml/ZipperSpecs.scala @@ -918,6 +918,27 @@ class ZipperSpecs extends SpecificationWithJUnit with ScalaCheck with XMLGenera } } + "Zipper.unselectAll" should { + val xml = .convert + + "behave like unselect for single selection" in { + val z = xml \\ * + val z1 = z.updated(0, .convert) + z1.unselectAll mustEqual z1.unselect + } + + "behave like reucrsive unselect for multiple selections" in { + val z = xml \ 'a \ 'b \ 'c \ 'd + val z1 = z.updated(0, .convert) + z1.unselectAll mustEqual z1.unselect.unselect.unselect.unselect + } + + "return self group on a broken zipper" in { + val group = xml.toGroup + group.toZipper.unselectAll mustEqual group + } + } + "Zipper.flatMap" should { "increase update times front to back" in { val xml = .convert