<?php
/**
* This class provides access to the definition for a Perforce spec type.
* This includes: field names, field types, field options, preset values,
* comments, etc.
*
* Fields with the dataType 'text' have several issues with whitespace:
* - Any trailing whitespace will be stripped
* - A trailing new-line will be added if not present
* - Any leading/intermediate lines will have trailing whitespace removed
* - Any line with non-whitespace content will preserve all trailing whitespace
*
* @copyright 2011 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
* @todo add getField, setField and addField(s) functions.
*/
class P4_Spec_Definition extends P4_ConnectedAbstract
{
protected $_data = array();
protected $_isPopulated = false;
protected static $_cache = array();
/**
* Get the type of spec that this defines.
*
* @return string the type of spec this defines.
*/
public function getType()
{
return $this->_type;
}
/**
* Set the type of spec this defines.
*
* @param string $type the type of spec to define.
* @return P4_Spec_Definition provides a fluent interface
*/
public function setType($type)
{
if (!is_string($type)) {
throw new InvalidArgumentException("Type must be a string.");
}
$this->_type = $type;
return $this;
}
/**
* Get the definition for a given spec type from Perforce.
*
* @param string $type the type of the spec to get the definition for.
* @param P4_Connection_Interface $connection optional - a specific connection to use.
* @returns P4_Spec_Definition instance containing details about this spec type.
*/
public static function fetch($type, P4_Connection_Interface $connection = null)
{
// if no connection given, use default.
$connection = $connection ?: static::getDefaultConnection();
// cache is per-server - ensure cache initialized for this server.
$port = $connection->getPort();
if (!array_key_exists($port, static::$_cache)) {
static::$_cache[$port] = array();
}
// create and populate spec definition if not cached.
if (!array_key_exists($type, static::$_cache[$port]) ||
!static::$_cache[$port][$type] instanceof P4_Spec_Definition) {
// construct spec def instance.
$definition = new static($connection);
$definition->setType($type);
// call get fields to force a populate and ensure type is valid.
$definition->getFields();
static::$_cache[$port][$type] = $definition;
}
// return cloned copy so that changes don't pollute the cache.
return clone static::$_cache[$port][$type];
}
/**
* Get multi-dimensional array of detailed field information for this spec type.
*
* @return array detailed field information for this spec type.
*/
public function getFields()
{
// only populate if fields array is unset.
if (!array_key_exists('fields', $this->_data)) {
$this->_populate();
}
return $this->_data['fields'];
}
/**
* Get array of detailed information about a particular field.
*
* @param string $field the field to get information about.
* @return array detailed field information for this spec type.
* @throws P4_Spec_Exception if the field does not exist.
*/
public function getField($field)
{
// verify field exists.
if (!$this->hasField($field)) {
throw new P4_Spec_Exception("Can't get field '$field'. Field does not exist.");
}
$fields = $this->getFields();
return $fields[$field];
}
/**
* Check if this spec definition has a particular field.
*
* @param string $field the field to check for the existence of.
* @return boolean true if the spec has the named field, false otherwise.
*/
public function hasField($field)
{
$fields = array_keys($this->getFields());
return in_array((string)$field, $fields);
}
/**
* Determine if the given field is required.
*
* @param string $field the field to check if required.
* @return bool true if the field is required, false otherwise.
*/
public function isRequiredField($field)
{
$field = $this->getField($field);
if ($field['fieldType'] === 'required' || $field['fieldType'] === 'key') {
return true;
} else {
return false;
}
}
/**
* Determine if the given field is read-only.
*
* @param string $field the field to check if required.
* @return bool true if the field is read-only, false otherwise.
*/
public function isReadOnlyField($field)
{
$field = $this->getField($field);
if ($field['fieldType'] == 'once' || $field['fieldType'] == 'always') {
return true;
} else {
return false;
}
}
/**
* Set the fields that define this specification.
*
* @param array $fields multi-dimensional array of detailed field information.
* @return P4_Spec_Definition provides a fluent interface
* @todo better validate fields array format.
*/
public function setFields($fields)
{
if (!is_array($fields)) {
throw new InvalidArgumentException("Fields must be an array.");
}
$this->_data['fields'] = $fields;
return $this;
}
/**
* Get comments for this spec type.
*
* @return string comments describing this spec type.
*/
public function getComments()
{
// only populate if comments are unset.
if (!array_key_exists('comments', $this->_data)) {
$this->_populate();
}
return $this->_data['comments'];
}
/**
* Set the comments for this specification.
*
* Comments are stored as 'text' fields which causes whitespace issues. See P4_Spec_Definition
* for details.
*
* @param string $comments comments describing this spec type.
* @return P4_Spec_Definition provides a fluent interface
* @todo validate comments format more thoroughly.
*/
public function setComments($comments)
{
if (!is_string($comments)) {
throw new InvalidArgumentException("Comments must be a string.");
}
$this->_data['comments'] = $comments;
return $this;
}
/**
* Save this spec definition to Perforce.
*
* @return P4_Spec_Definition provides a fluent interface
*/
public function save()
{
// save spec definition in Perforce.
$connection = static::getDefaultConnection();
$result = $connection->run(
'spec',
array("-i", $this->getType()),
$this->_toSpecArray($this->_data)
);
$this->clearCache();
return $this;
}
/**
* Clear the shared 'fetch' class and also clear this instances fields/comments.
*
* @todo If clearCache is called and this instance is subsequently populated, the shared
* fetch cache won't be updated. If fetch is later called an additional populate will
* be executed. This could be optimized but is a fairly narrow case.
*/
public function clearCache()
{
$type = $this->getType();
// Remove the static cache; helps with future 'fetch' calls
unset(static::$_cache[$this->getConnection()->getPort()][$type]);
// Remove our instances values
$this->_data = array();
// Ensure our instance will re-populate
$this->_isPopulated = false;
}
/**
* Expand preset values that are expected to be interpreted client side.
* For example, '$user' should be set to the name of the current Perforce
* user. '$now' should be set to the current time. See 'p4 help undoc'
* for additional details.
*
* @param string $default the default value to be expanded.
* @param P4_Connection_Interface $connection optional - a specific connection to use
* when expanding default values.
* @return string the expanded default value.
* @todo job specs have additional 'expansions'; there may be more outside jobs too.
*/
public static function expandDefault($default, P4_Connection_Interface $connection = null)
{
// if no connection given, use default.
$connection = $connection ?: static::getDefaultConnection();
switch ((string)$default) {
case '$user':
return $connection->getUser();
break;
case '$blank':
return null;
break;
default:
return $default;
break;
}
}
/**
* Get the spec definition from Perforce if not already populated.
*/
protected function _populate()
{
// only populate once.
if ($this->_isPopulated) {
return;
}
// query perforce to get spec definition.
$connection = $this->getConnection();
$result = $connection->run(
'spec',
array("-o", $this->getType())
);
// ensure all sequences are expanded into arrays
$result->expandSequences();
// ensure spec output is an array.
$spec = $result->getData(0);
if (!is_array($spec) || empty($spec)) {
throw new P4_Spec_Exception(
"Failed to populate spec definition. Perforce result invalid."
);
}
// convert spec to internal format.
$data = $this->_fromSpecArray($spec);
// don't clobber fields/comments if already set.
if (!array_key_exists('fields', $this->_data)) {
$this->_data['fields'] = $data['fields'];
}
if (!array_key_exists('comments', $this->_data)) {
$this->_data['comments'] = $data['comments'];
}
// flag as populated.
$this->_isPopulated = true;
}
/**
* Convert 'p4 spec -o' output into the format of the internal data structure
* used by this class.
*
* The data structure groups field metadata by field name and breaks multi-word
* fields into their component parts. For example:
*
* array (
* 'fields' => array (
* 'Field1' => array (
* 'code' => '310',
* 'dataType' => 'select',
* 'displayLength' => '12',
* 'fieldType' => 'optional',
* 'order' => '0',
* 'position' => 'L',
* 'options' => array (
* 0 => 'local',
* 1 => 'unix',
* )
* ),
* 'Field2' => array (
* 'code' => '311',
* 'dataType' => 'wlist',
* 'displayLength' => '64',
* 'fieldType' => 'optional',
* 'wordCount' => '2'
* )
* ),
* 'comments' => '# Comments for this spec.'
* )
*
* @param array $spec the raw output from 'p4 spec -o'
* @return array the converted spec definition data structure.
*/
protected function _fromSpecArray($spec)
{
$data = array(
'fields' => array(),
'comments' => null
);
// break apart fields word-list in spec array.
foreach ($spec['Fields'] as $fieldInfo) {
list($code, $name, $dataType, $length, $fieldType) = explode(" ", $fieldInfo);
$data['fields'][$name] = array(
'code' => $code,
'dataType' => $dataType,
'displayLength' => $length,
'fieldType' => $fieldType
);
// hack because Perforce doesn't provide word count
// for single column wlist's.
if ($dataType == 'wlist') {
$data['fields'][$name]['wordCount'] = 1;
}
}
// add word count information for multi-word fields.
if (isset($spec['Words']) && is_array($spec['Words'])) {
foreach ($spec['Words'] as $wordInfo) {
list($fieldName, $wordCount) = explode(" ", $wordInfo);
$data['fields'][$fieldName]['wordCount'] = $wordCount;
}
}
// add format information.
if (isset($spec['Formats']) && is_array($spec['Formats'])) {
foreach ($spec['Formats'] as $formatInfo) {
list($fieldName, $order, $position) = explode(" ", $formatInfo);
$data['fields'][$fieldName]['order'] = $order;
$data['fields'][$fieldName]['position'] = $position;
}
}
// add options for select fields.
if (isset($spec['Values']) && is_array($spec['Values'])) {
foreach ($spec['Values'] as $selectInfo) {
list($fieldName, $options) = explode(" ", $selectInfo);
$data['fields'][$fieldName]['options'] = explode('/', $options);
}
}
// add default field values.
if (isset($spec['Presets']) && is_array($spec['Presets'])) {
foreach ($spec['Presets'] as $defaultInfo) {
list($fieldName, $default) = explode(" ", $defaultInfo);
$data['fields'][$fieldName]['default'] = $default;
}
}
// add spec comments to data structure.
if (isset($spec['Comments']) && is_string($spec['Comments'])) {
$data['comments'] = $spec['Comments'];
}
return $data;
}
/**
* Convert the internal data structure of this class into a 'p4 spec -i'
* compatible array of comments and field details. See _fromSpecArray for
* data structure format.
*
* @param array $data an internal spec definition data structure.
* @return array spec definition array suitable for 'p4 spec -i'.
*/
protected function _toSpecArray($data)
{
if (!is_array($data) ||
!array_key_exists('fields', $data) ||
!array_key_exists('comments', $data)) {
throw InvalidArgumentException("Data must be array with fields and comments.");
}
$spec = array(
'Fields' => array(),
'Words' => array(),
'Formats' => array(),
'Values' => array(),
'Presets' => array(),
'Comments' => null
);
// convert fields back into spec array format.
foreach ($data['fields'] as $name => $field) {
$spec['Fields'][] = implode(
" ",
array(
$field['code'],
$name,
$field['dataType'],
$field['displayLength'],
$field['fieldType']
)
);
// only include word count if > 1.
if (isset($field['wordCount']) && $field['wordCount'] > 1) {
$spec['Words'][] = implode(" ", array($name, $field['wordCount']));
}
if (isset($field['order'], $field['position'])) {
$spec['Formats'][] = implode(" ", array($name, $field['order'], $field['position']));
}
if (isset($field['options']) && is_array($field['options'])) {
$spec['Values'][] = implode(" ", array($name, implode("/", $field['options'])));
}
if (isset($field['default'])) {
$spec['Presets'][] = implode(" ", array($name, $field['default']));
}
}
// remove empty elements.
foreach ($spec as $key => $value) {
if (empty($value)) {
unset($spec[$key]);
}
}
// add comments to spec array.
// Perforce will keep existing value if no comments entry is present.
// We ensure it is at least blank to force an update.
if (isset($data['comments']) && is_string($data['comments'])) {
$spec['Comments'] = $data['comments'];
}
return $spec;
}
}