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

com.loopfor.zookeeper.Path.scala Maven / Gradle / Ivy

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

import scala.annotation.tailrec
import scala.collection.immutable.Stack
import scala.collection.mutable.StringBuilder
import scala.language._

/**
 * Represents an ''absolute'' or ''relative'' path to a node in ZooKeeper.
 * 
 * An ''absolute'' path starts with the `/` character. All other forms are considered ''relative''. Paths are virtually
 * identical to those on Unix file systems and may include both `.` and `..` parts, indicating the current and parent node,
 * respectively.
 * 
 * Examples:
 * {{{
 * "/"
 * "../foo"
 * "/foo/bar"
 * "./foo/../bar"
 * }}}
 */
trait Path {
  /**
   * Returns the name of the last part of the path.
   * 
   * Examples:
   * {{{
   * "" parent is ""
   * "/" parent is ""
   * "/foo" parent is "/"
   * "/foo/bar" parent is "/foo"
   * "foo/bar" parent is "foo"
   * }}}
   * 
   * @return the last part of [[path]], which is an empty string if [[path]] is either `""` or `"/"`.
   */
  def name: String

  /**
   * Returns the path.
   * 
   * @return the path
   */
  def path: String

  /**
   * Returns the parent path.
   * 
   * @return the parent of [[path]]
   * @throws NoSuchElementException if removal of [[name]] from [[path]] yields `""` or `"/"`
   */
  def parent: Path

  /**
   * Returns the parent path wrapped in an `Option`.
   * 
   * @return a `Some` containing the parent of [[path]] or `None` if removal of [[name]] from [[path]] yields `""` or `"/"`
   */
  def parentOption: Option[Path]

  /**
   * Returns a sequence containing the parts of the path.
   * 
   * Parts represent the node names sandwiched between `/` characters. An ''absolute'' path, which is prefixed with `/`, always
   * yields a sequence containing an empty string as the first element. The path `""` contains no parts, hence an empty
   * sequence is returned. In all other cases, parts are non-empty strings.
   * 
   * Examples:
   * {{{
   * "" parts ()
   * "/" parts ("/")
   * "/foo/bar" parts ("", "foo", "bar")
   * "foo/bar" parts ("foo", "bar")
   * }}}
   * 
   * @return a sequence containing the parts of [[path]]
   */
  def parts: Seq[String]

  /**
   * Resolves the given `path` relative to this path.
   * 
   * Path resolution works as follows:
   *  - if `path` is empty, return this [[path]]
   *  - if `path` starts with `/`, return `path`
   *  - if this [[path]] is empty, return `path`
   *  - otherwise, return this [[path]] + `/` + `path`
   * 
   * @param path the path to resolve against this [[path]]
   * @return a new path in which the given `path` is resolved relative to this [[path]]
   */
  def resolve(path: String): Path

  /**
   * Resolves the given `path` relative to this path.
   * 
   * @param path the path to resolve against this [[path]]
   * @return a new path in which the givne `path` is resolved relative to this [[path]]
   */
  def resolve(path: Path): Path

  /**
   * Returns the normalized form of this path.
   * 
   * The normalization process entails the removal of `.` and `..` parts where possible.
   * 
   * Examples:
   * {{{
   * "/.." normalizes to "/"
   * "/foo/.." normalizes to "/"
   * "/foo/../bar" normalizes to "/bar"
   * "./foo" normalizes to "foo"
   * "foo/." normalizes to "foo"
   * "foo/.." normalizes to ""
   * "foo/./bar" normalizes to "foo/bar"
   * "foo/../bar" normalizes to "bar"
   * }}}
   * 
   * @return the normalized form of this path
   */
  def normalize: Path

  /**
   * Returns `true` if the path is absolute.
   */
  def isAbsolute: Boolean
}

/**
 * Constructs and deconstructs [[Path]] values.
 */
object Path {
  /**
   * Constructs a new path using the given path string.
   * 
   * Path construction entails removal of extraneous `/` characters, including those at the end of `path` so long as `path`
   * itself is not equivalent to `"/"`.
   * 
   * @param path the path string
   * @return a new path with the given `path` string
   */
  def apply(path: String): Path = new Impl(compress(path))

  /**
   * Used in pattern matching to deconstruct a path.
   * 
   * @param path selector value
   * @return a `Some` containing the [[Path.parts parts]] of `path` or `None` if `path` is `null`
   */
  def unapplySeq(path: Path): Option[Seq[String]] =
    if (path == null) None else Some(path.parts)

  private def apply(parts: Seq[String]): Path = new Impl(parts mkString "/")

  private class Impl(val path: String) extends Path {
    lazy val name: String = parts.lastOption match {
      case Some(p) => p
      case _ => ""
    }

    lazy val parent: Path = parentOption match {
      case Some(p) => p
      case _ => throw new NoSuchElementException("no parent node")
    }

    lazy val parentOption: Option[Path] = {
      if (parts.size > 1) {
        val _parts = parts dropRight 1
        Some(Path(_parts.last match {
          case "" => "/"
          case _ => _parts mkString "/"
        }))
      } else
        None
    }

    lazy val parts: Seq[String] = parse(path)

    def resolve(path: String): Path = {
      Path(path.headOption match {
        case None => this.path
        case Some('/') => path
        case _ if this.path.isEmpty => path
        case _ => this.path + "/" + path
      })
    }

    def resolve(path: Path): Path = resolve(path.path)

    lazy val normalize: Path = {
      @tailrec def reduce(parts: Seq[String], stack: Stack[String]): Stack[String] = {
        parts.headOption match {
          case Some(part) =>
            reduce(parts.tail, part match {
              case ".." => stack.headOption match {
                case Some(top) => if (top == "") stack else if (top == "..") stack.push(part) else stack.pop
                case _ => stack.push(part)
              }
              case "." => stack
              case _ => stack.push(part)
            })
          case _ => stack
        }
      }
      val stack = reduce(parse(path), Stack()).reverse
      Path(stack.headOption match {
        case None => ""
        case Some("") => "/" + (stack.tail mkString "/")
        case _ => stack mkString "/"
      })
    }

    lazy val isAbsolute: Boolean = path.headOption == Some('/')

    override def toString: String = path

    override def equals(that: Any): Boolean = that match {
      case _that: Path => _that.path == path
      case _ => false
    }

    override def hashCode: Int = path.hashCode
  }

  private def compress(path: String): String = {
    @tailrec def munch(path: Seq[Char]): Seq[Char] = {
      path.headOption match {
        case Some('/') => munch(path.tail)
        case _ => path
      }
    }
    @tailrec def collapse(path: Seq[Char], to: StringBuilder): StringBuilder = {
      path.headOption match {
        case Some(c) => collapse(if (c == '/') munch(path.tail) else path.tail, to + c)
        case _ => to
      }
    }
    val to = collapse(path.seq, new StringBuilder)
    (if (to.size > 1 && to.last == '/') to.dropRight(1) else to).toString
  }

  private def parse(path: String): Seq[String] = {
    compress(path) match {
      case "" => Seq()
      case "/" => Seq("")
      case p => p split '/'
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy