<?php /** * Abstracts operations against Perforce streams. * * @copyright 2011 Perforce Software. All rights reserved. * @license Please see LICENSE.txt in top-level folder of this distribution. * @version <release>/<patch> * @todo add accessor/mutator for 'Paths' */ namespace P4\Spec; use P4\Validate; use P4\Connection\ConnectionInterface; use P4\Connection\Exception\CommandException; use P4\Model\Fielded\Iterator as FieldedIterator; class Stream extends PluralAbstract { const SPEC_TYPE = 'stream'; const ID_FIELD = 'Stream'; const FETCH_BY_PATH = 'path'; const FETCH_BY_FILTER = 'filter'; const SORT_RECURSIVE = 'sort'; protected $cache = array(); protected $fields = array( 'Update' => array( 'accessor' => 'getUpdateDateTime' ), 'Access' => array( 'accessor' => 'getAccessDateTime' ), 'Owner' => array( 'accessor' => 'getOwner', 'mutator' => 'setOwner' ), 'Name' => array( 'accessor' => 'getName', 'mutator' => 'setName' ), 'Parent' => array( 'accessor' => 'getParent', 'mutator' => 'setParent' ), 'Type' => array( 'accessor' => 'getType', 'mutator' => 'setType' ), 'Description' => array( 'accessor' => 'getDescription', 'mutator' => 'setDescription' ), 'Options' => array( 'accessor' => 'getOptions', 'mutator' => 'setOptions' ), 'Paths' => array( 'accessor' => 'getPaths', 'mutator' => 'setPaths' ) ); /** * Get all Streams from Perforce. Adds the following filter options: * FETCH_BY_PATH - limits results to streams matching path (can include wildcards). * FETCH_BY_FILTER - a 'jobview' style expression to limit results. * SORT_RECURSIVE - optionally sort the results recusively * * @param array $options optional - options to augment fetch behavior. * @param ConnectionInterface $connection optional - a specific connection to use. * @return FieldedIterator all records of this type matching options. */ public static function fetchAll($options = array(), ConnectionInterface $connection = null) { // try/catch parent to deal with the exception we get on non-existend depots try { $streams = parent::fetchAll($options, $connection); // set the 'parent' stream on each entry if we have a copy foreach ($streams as $stream) { // skip any streams without parents if (!$stream->getParent()) { continue; } // attempt to locate the parent object in our result set and set it $parent = $streams->filter('Stream', $stream->getParent(), FieldedIterator::FILTER_COPY) ->first(); if ($parent) { $stream->cache['parentObject'] = $parent; } } // apply sorting if it has been requested if (isset($options[static::SORT_RECURSIVE]) && $options[static::SORT_RECURSIVE]) { $streams = static::sortRecursively($streams); } return $streams; } catch (CommandException $e) { // if the 'depot' has been interpreted as an invalid client, just return no matches if (preg_match("/Command failed: .+ - must refer to client/", $e->getMessage())) { return new FieldedIterator; } // unexpected error; rethrow it throw $e; } } /** * Determine if the given stream id exists. * * @param string $id the id to check for. * @param ConnectionInterface $connection optional - a specific connection to use. * @return bool true if the given id matches an existing stream. */ public static function exists($id, ConnectionInterface $connection = null) { // check id for valid format if (!static::isValidId($id)) { return false; } $streams = static::fetchAll(array(static::FETCH_BY_FILTER => "Stream=$id"), $connection); return (bool) count($streams); } /** * Get the last update time for this stream. * This value is read only, no setUpdateDateTime function is provided. * * If this is a brand new stream, null will be returned in lieu of a time. * * @return string|null Date/Time of last update, formatted "2009/11/23 12:57:06" or null */ public function getUpdateDateTime() { return $this->getRawValue('Update'); } /** * Get the last access time for this stream. * This value is read only, no setAccessDateTime function is provided. * * If this is a brand new spec, null will be returned in lieu of a time. * * @return string|null Date/Time of last access, formatted "2009/11/23 12:57:06" or null */ public function getAccessDateTime() { return $this->getRawValue('Access'); } /** * Get the owner of this stream. * * @return string|null User who owns this record. */ public function getOwner() { return $this->getRawValue('Owner'); } /** * Set the owner of this stream to passed value. * * @param string|null $owner A string containing username * @return Stream provides a fluent interface. * @throws \InvalidArgumentException Owner is incorrect type. */ public function setOwner($owner) { if (!is_string($owner) && !is_null($owner)) { throw new \InvalidArgumentException('Owner must be a string or null.'); } return $this->setRawValue('Owner', $owner); } /** * Get the name setting for this stream. * * @return string|null Name set for this stream or null. */ public function getName() { return $this->getRawValue('Name'); } /** * Set the name for this stream. * * @param string|null $name Name for this stream or null * @return Stream provides a fluent interface. * @throws \InvalidArgumentException Name is incorrect type. */ public function setName($name) { if (!is_string($name) && !is_null($name)) { throw new \InvalidArgumentException('Name must be a string or null.'); } return $this->setRawValue('Name', $name); } /** * Get the parent setting for this stream. * * @return string|null Parent set for this stream. */ public function getParent() { $parent = $this->getRawValue('Parent'); return $parent == 'none' ? null : $parent; } /** * Set the parent for this stream. * * @param string|null $parent Parent for this stream or null * @return Stream provides a fluent interface. * @throws \InvalidArgumentException Parent is incorrect type. */ public function setParent($parent) { if (!is_string($parent) && !is_null($parent)) { throw new \InvalidArgumentException('Parent must be a string or null.'); } // clear cache as parent may have changed $this->cache = array(); return $this->setRawValue('Parent', $parent); } /** * Get the parent asociated with this stream in Stream format. * * @return Stream|null this streams parent object or null if none */ public function getParentObject() { if (!$this->getParent()) { return null; } if (!isset($this->cache['parentObject']) || !$this->cache['parentObject'] instanceof Stream ) { $this->cache['parentObject'] = Stream::fetch( $this->getParent(), $this->getConnection() ); } return clone $this->cache['parentObject']; } /** * Returns the depth of this stream. Assumes all parent objects * are returnable. * * @return int the depth of this stream. */ public function getDepth() { $stream = $this; for ($i = 0; $stream = $stream->getParentObject(); $i++) { } return $i; } /** * Get the type setting for this stream. * * @return string|null Type set for this stream. */ public function getType() { return $this->getRawValue('Type'); } /** * Set the type for this stream. * * @param string|null $type Type for this stream or null * @return Stream provides a fluent interface. * @throws \InvalidArgumentException Type is incorrect type. */ public function setType($type) { if (!is_string($type) && !is_null($type)) { throw new \InvalidArgumentException('Type must be a string or null.'); } return $this->setRawValue('Type', $type); } /** * Get the description for this stream. * * @return string|null description for this stream. */ public function getDescription() { return $this->getRawValue('Description'); } /** * Set a description for this stream. * * @param string|null $description description for this stream. * @return Stream provides a fluent interface. * @throws \InvalidArgumentException Description is incorrect type. */ public function setDescription($description) { if (!is_string($description) && !is_null($description)) { throw new \InvalidArgumentException('Description must be a string or null.'); } return $this->setRawValue('Description', $description); } /** * Get options for this stream. * Returned array will contain one option per element e.g.: * array ( * 0 => 'allsubmit', * 1 => 'toparent', * 2 => 'unlocked' * ) * * @return array options which are set on this stream. */ public function getOptions() { $options = $this->getRawValue('Options'); $options = explode(' ', $options); // Explode will set key 0 to null for empty input; clean it up. if (count($options) == 1 && empty($options[0])) { $options = array(); } return $options; } /** * Set the options for this stream. * Accepts an array, format detailed in getOptions, or a single string containing * a space seperated list of options. * * @param array|string $options options to set on this stream in array or string. * @return Steam provides a fluent interface. * @throws \InvalidArgumentException Options are incorrect type. */ public function setOptions($options) { if (is_array($options)) { $options = implode(' ', $options); } if (!is_string($options)) { throw new \InvalidArgumentException('Options must be an array or string'); } return $this->setRawValue('Options', $options); } /** * Get the paths for this stream. * Path entries will be returned as an array with 'type', 'view' and 'depot' entries, e.g.: * array ( * 0 => array ( * 'type' => 'share', * 'view' => 'src/...', * 'depot' => null * ) * 1 => array ( * 'type' => 'import', * 'view' => 'src/...', * 'depot' => '//over/there/src/...' * ) * ) * * @return array list path entries for this stream. */ public function getPaths() { // The raw path data is formatted as: // array ( // 0 => 'share ...', // 1 => 'import imp/ //depot/other/local/...' // ) $paths = array(); foreach ($this->getRawValue('Paths') ?: array() as $entry) { $entry = str_getcsv($entry, ' '); $paths[] = array_combine( array('type', 'view', 'depot'), $entry + array(null, null, null) ); } return $paths; } /** * Set the paths for this stream. * Paths are passed as an array of path entries. Each patj entry can be an array with * 'type', 'view' and, optionally, 'depot' entries or a raw string. * * @param array|string $paths Path entries, formatted as sub-arrays or strings. * @return Stream provides a fluent interface. * @throws \InvalidArgumentException Paths array, or a path entry, is incorrect type. */ public function setPaths($paths) { // we let the caller pass in a single path and normalize it below if (is_string($paths) || isset($paths['type'], $paths['view'])) { $paths = array($paths); } if (!is_array($paths)) { throw new \InvalidArgumentException('Paths must be passed as array or string.'); } // The Paths array contains either: // - Child arrays keyed on type/view/[depot] which we glue together // - Raw strings which we simply leave as is // The below foreach run will normalize the whole thing for storage $parsedPaths = array(); foreach ($paths as $path) { if (is_array($path) && isset($path['type'], $path['view']) && is_string($path['type']) && is_string($path['view']) && (!isset($path['depot']) || is_string($path['depot'])) ) { // stringify the path quoting paths to be safe $string = $path['type'] . ' "' . $path['view'] . '"'; if (isset($path['depot']) && strlen($path['depot'])) { $string .= ' "' . $path['depot'] . '"'; } $path = $string; } if (!is_string($path)) { throw new \InvalidArgumentException( "Each path entry must be an array with type/view (and optionally depot) or a string." ); } $validate = str_getcsv($path, ' '); if (count($validate) < 2 || count($validate) > 3 || trim($validate[0]) === '' || trim($validate[1]) === '' ) { throw new \InvalidArgumentException( "Each path entry must contain between two and three entries." ); } $parsedPaths[] = $path; }; return $this->setRawValue('Paths', $parsedPaths); } /** * Add a path to this stream. * * @param string $type the path type (share/isolate/import/exclude) * @param string $view the view path * @param string|null $depot the depot path (only used for import type) * @return Stream provides a fluent interface. */ public function addPath($type, $view, $depot = null) { $paths = $this->getPaths(); $paths[] = array("type" => $type, "view" => $view, "depot" => $depot); return $this->setPaths($paths); } /** * Save this spec to Perforce. * Extends parent to provide a default value of 'none' for parent. * * @return Stream provides a fluent interface */ public function save() { if (!$this->get('Parent')) { $this->set('Parent', 'none'); } return parent::save(); } /** * Remove this stream. Extend parent to remove all clients dedicated to this * stream first. * * @param boolean $force pass true to force delete this stream, additionally * attempts to delete any clients using this stream * by default stream will only be deleted if there are * no clients current using the stream and current user * is the stream owner or the stream is unlocked. * @return Stream provides fluent interface. */ public function delete($force = false) { // remove clients dedicated to this stream if force is true if ($force) { Client::fetchAll( array(Client::FETCH_BY_STREAM => $this->getId()) )->invoke('delete', array($force)); } return parent::delete($force ? array('-f') : null); } /** * Check if the given id is in a valid format for this spec type. * * @param string $id the id to check * @return bool true if id is valid, false otherwise */ protected static function isValidId($id) { $validator = new Validate\StreamName; return $validator->isValid($id); } /** * Produce set of flags for the spec list command, given fetch all options array. * Extends parent to add support for filter option. * * @param array $options array of options to augment fetch behavior. * see fetchAll for documented options. * @return array set of flags suitable for passing to spec list command. */ protected static function getFetchAllFlags($options) { $flags = parent::getFetchAllFlags($options); if (isset($options[static::FETCH_BY_FILTER])) { $filter = $options[static::FETCH_BY_FILTER]; if (!is_string($filter) || trim($filter) === '') { throw new \InvalidArgumentException( 'Filter expects a non-empty string as input' ); } $flags[] = '-F'; $flags[] = $filter; } if (isset($options[static::FETCH_BY_PATH])) { $flags[] = $options[static::FETCH_BY_PATH]; } return $flags; } /** * Given a spec entry from spec list output (p4 streams), produce * an instance of this spec with field values set where possible. * * @param array $listEntry a single spec entry from spec list output. * @param array $flags the flags that were used for this 'fetchAll' run. * @param ConnectionInterface $connection a specific connection to use. * @return Stream a (partially) populated instance of this spec class. */ protected static function fromSpecListEntry($listEntry, $flags, ConnectionInterface $connection) { // move the description into place $listEntry['Description'] = $listEntry['desc']; unset($listEntry['desc']); return parent::fromSpecListEntry($listEntry, $flags, $connection); } /** * This method will ensure the list of streams is in the proper order * with children listed after their associated parents. * * @param FieldedIterator $streams the streams to sort * @param string|null $parent the parent id (used for recursion) * @return FieldedIterator The recursively sorted result */ protected static function sortRecursively($streams, $parent = null) { // get branches with given parent and sort them $children = $streams->filter('Parent', array($parent), FieldedIterator::FILTER_COPY) ->sortBy('Name', array(FieldedIterator::SORT_NATURAL)); // assemble list and append sorted sub-entries below their parent $sorted = new FieldedIterator; foreach ($children as $stream) { $sorted[] = $stream; foreach (static::sortRecursively($streams, $stream->getId()) as $sub) { $sorted[] = $sub; } } return $sorted; } }