. */ include_once 'phing/system/io/PhingFile.php'; include_once 'phing/util/FileUtils.php'; include_once 'phing/TaskAdapter.php'; include_once 'phing/util/StringHelper.php'; include_once 'phing/BuildEvent.php'; include_once 'phing/input/DefaultInputHandler.php'; /** * The Phing project class. Represents a completely configured Phing project. * The class defines the project and all tasks/targets. It also contains * methods to start a build as well as some properties and FileSystem * abstraction. * * @author Andreas Aderhold * @author Hans Lellelid * @version $Id: 7e67218e8e616860f9c746f9ab600f523089ea2e $ * @package phing */ class Project { // Logging level constants. const MSG_DEBUG = 4; const MSG_VERBOSE = 3; const MSG_INFO = 2; const MSG_WARN = 1; const MSG_ERR = 0; /** contains the targets */ private $targets = array(); /** global filterset (future use) */ private $globalFilterSet = array(); /** all globals filters (future use) */ private $globalFilters = array(); /** Project properties map (usually String to String). */ private $properties = array(); /** * Map of "user" properties (as created in the Ant task, for example). * Note that these key/value pairs are also always put into the * project properties, so only the project properties need to be queried. * Mapping is String to String. */ private $userProperties = array(); /** * Map of inherited "user" properties - that are those "user" * properties that have been created by tasks and not been set * from the command line or a GUI tool. * Mapping is String to String. */ private $inheritedProperties = array(); /** task definitions for this project*/ private $taskdefs = array(); /** type definitions for this project */ private $typedefs = array(); /** holds ref names and a reference to the referred object*/ private $references = array(); /** The InputHandler being used by this project. */ private $inputHandler; /* -- properties that come in via xml attributes -- */ /** basedir (PhingFile object) */ private $basedir; /** the default target name */ private $defaultTarget = 'all'; /** project name (required) */ private $name; /** project description */ private $description; /** require phing version */ private $phingVersion; /** a FileUtils object */ private $fileUtils; /** Build listeneers */ private $listeners = array(); /** * Constructor, sets any default vars. */ public function __construct() { $this->fileUtils = new FileUtils(); $this->inputHandler = new DefaultInputHandler(); } /** * Sets the input handler * @param InputHandler $handler */ public function setInputHandler(InputHandler $handler) { $this->inputHandler = $handler; } /** * Retrieves the current input handler. * @return InputHandler */ public function getInputHandler() { return $this->inputHandler; } /** inits the project, called from main app */ public function init() { // set builtin properties $this->setSystemProperties(); // load default tasks $taskdefs = Phing::getResourcePath("phing/tasks/defaults.properties"); try { // try to load taskdefs $props = new Properties(); $in = new PhingFile((string)$taskdefs); if ($in === null) { throw new BuildException("Can't load default task list"); } $props->load($in); $enum = $props->propertyNames(); foreach($enum as $key) { $value = $props->getProperty($key); $this->addTaskDefinition($key, $value); } } catch (IOException $ioe) { throw new BuildException("Can't load default task list"); } // load default tasks $typedefs = Phing::getResourcePath("phing/types/defaults.properties"); try { // try to load typedefs $props = new Properties(); $in = new PhingFile((string)$typedefs); if ($in === null) { throw new BuildException("Can't load default datatype list"); } $props->load($in); $enum = $props->propertyNames(); foreach($enum as $key) { $value = $props->getProperty($key); $this->addDataTypeDefinition($key, $value); } } catch(IOException $ioe) { throw new BuildException("Can't load default datatype list"); } } /** returns the global filterset (future use) */ public function getGlobalFilterSet() { return $this->globalFilterSet; } // --------------------------------------------------------- // Property methods // --------------------------------------------------------- /** * Sets a property. Any existing property of the same name * is overwritten, unless it is a user property. * @param string $name The name of property to set. * Must not be null. * @param string $value The new value of the property. * Must not be null. * @return void */ public function setProperty($name, $value) { // command line properties take precedence if (isset($this->userProperties[$name])) { $this->log("Override ignored for user property " . $name, Project::MSG_VERBOSE); return; } if (isset($this->properties[$name])) { $this->log("Overriding previous definition of property " . $name, Project::MSG_VERBOSE); } $this->log("Setting project property: " . $name . " -> " . $value, Project::MSG_DEBUG); $this->properties[$name] = $value; } /** * Sets a property if no value currently exists. If the property * exists already, a message is logged and the method returns with * no other effect. * * @param string $name The name of property to set. * Must not be null. * @param string $value The new value of the property. * Must not be null. * @since 2.0 */ public function setNewProperty($name, $value) { if (isset($this->properties[$name])) { $this->log("Override ignored for property " . $name, Project::MSG_DEBUG); return; } $this->log("Setting project property: " . $name . " -> " . $value, Project::MSG_DEBUG); $this->properties[$name] = $value; } /** * Sets a user property, which cannot be overwritten by * set/unset property calls. Any previous value is overwritten. * @param string $name The name of property to set. * Must not be null. * @param string $value The new value of the property. * Must not be null. * @see #setProperty() */ public function setUserProperty($name, $value) { $this->log("Setting ro project property: " . $name . " -> " . $value, Project::MSG_DEBUG); $this->userProperties[$name] = $value; $this->properties[$name] = $value; } /** * Sets a user property, which cannot be overwritten by set/unset * property calls. Any previous value is overwritten. Also marks * these properties as properties that have not come from the * command line. * * @param string $name The name of property to set. * Must not be null. * @param string $value The new value of the property. * Must not be null. * @see #setProperty() */ public function setInheritedProperty($name, $value) { $this->inheritedProperties[$name] = $value; $this->setUserProperty($name, $value); } /** * Sets a property unless it is already defined as a user property * (in which case the method returns silently). * * @param name The name of the property. * Must not be null. * @param value The property value. Must not be null. */ private function setPropertyInternal($name, $value) { if (isset($this->userProperties[$name])) { $this->log("Override ignored for user property " . $name, Project::MSG_VERBOSE); return; } $this->properties[$name] = $value; } /** * Returns the value of a property, if it is set. * * @param string $name The name of the property. * May be null, in which case * the return value is also null. * @return string The property value, or null for no match * or if a null name is provided. */ public function getProperty($name) { if (!isset($this->properties[$name])) { return null; } $found = $this->properties[$name]; // check to see if there are unresolved property references if (false !== strpos($found, '${')) { // attempt to resolve properties $found = $this->replaceProperties($found); // save resolved value $this->properties[$name] = $found; } return $found; } /** * Replaces ${} style constructions in the given value with the * string value of the corresponding data types. * * @param value The string to be scanned for property references. * May be null. * * @return the given string with embedded property names replaced * by values, or null if the given string is * null. * * @exception BuildException if the given value has an unclosed * property name, e.g. ${xxx */ public function replaceProperties($value) { return ProjectConfigurator::replaceProperties($this, $value, $this->properties); } /** * Returns the value of a user property, if it is set. * * @param string $name The name of the property. * May be null, in which case * the return value is also null. * @return string The property value, or null for no match * or if a null name is provided. */ public function getUserProperty($name) { if (!isset($this->userProperties[$name])) { return null; } return $this->userProperties[$name]; } /** * Returns a copy of the properties table. * @return array A hashtable containing all properties * (including user properties). */ public function getProperties() { return $this->properties; } /** * Returns a copy of the user property hashtable * @return a hashtable containing just the user properties */ public function getUserProperties() { return $this->userProperties; } /** * Copies all user properties that have been set on the command * line or a GUI tool from this instance to the Project instance * given as the argument. * *

To copy all "user" properties, you will also have to call * {@link #copyInheritedProperties copyInheritedProperties}.

* * @param Project $other the project to copy the properties to. Must not be null. * @return void * @since phing 2.0 */ public function copyUserProperties(Project $other) { foreach($this->userProperties as $arg => $value) { if (isset($this->inheritedProperties[$arg])) { continue; } $other->setUserProperty($arg, $value); } } /** * Copies all user properties that have not been set on the * command line or a GUI tool from this instance to the Project * instance given as the argument. * *

To copy all "user" properties, you will also have to call * {@link #copyUserProperties copyUserProperties}.

* * @param Project $other the project to copy the properties to. Must not be null. * * @since phing 2.0 */ public function copyInheritedProperties(Project $other) { foreach($this->userProperties as $arg => $value) { if ($other->getUserProperty($arg) !== null) { continue; } $other->setInheritedProperty($arg, $value); } } // --------------------------------------------------------- // END Properties methods // --------------------------------------------------------- /** * Sets default target * @param string $targetName */ public function setDefaultTarget($targetName) { $this->defaultTarget = (string) trim($targetName); } /** * Returns default target * @return string */ public function getDefaultTarget() { return (string) $this->defaultTarget; } /** * Sets the name of the current project * * @param string $name name of project * @return void * @access public * @author Andreas Aderhold, andi@binarycloud.com */ public function setName($name) { $this->name = (string) trim($name); $this->setProperty("phing.project.name", $this->name); } /** * Returns the name of this project * * @return string projectname * @access public * @author Andreas Aderhold, andi@binarycloud.com */ public function getName() { return (string) $this->name; } /** * Set the projects description * @param string $description */ public function setDescription($description) { $this->description = (string) trim($description); } /** * return the description, null otherwise * @return string|null */ public function getDescription() { return $this->description; } /** * Set the minimum required phing version * @param string $version */ public function setPhingVersion($version) { $version = str_replace('phing', '', strtolower($version)); $this->phingVersion = (string)trim($version); } /** * Get the minimum required phing version * @return string */ public function getPhingVersion() { if($this->phingVersion === null) { $this->setPhingVersion(Phing::getPhingVersion()); } return $this->phingVersion; } /** * Set basedir object from xm * @param PhingFile|string $dir */ public function setBasedir($dir) { if ($dir instanceof PhingFile) { $dir = $dir->getAbsolutePath(); } $dir = $this->fileUtils->normalize($dir); $dir = new PhingFile((string) $dir); if (!$dir->exists()) { throw new BuildException("Basedir ".$dir->getAbsolutePath()." does not exist"); } if (!$dir->isDirectory()) { throw new BuildException("Basedir ".$dir->getAbsolutePath()." is not a directory"); } $this->basedir = $dir; $this->setPropertyInternal("project.basedir", $this->basedir->getAbsolutePath()); $this->log("Project base dir set to: " . $this->basedir->getPath(), Project::MSG_VERBOSE); // [HL] added this so that ./ files resolve correctly. This may be a mistake ... or may be in wrong place. chdir($dir->getAbsolutePath()); } /** * Returns the basedir of this project * * @return PhingFile Basedir PhingFile object * @access public * @throws BuildException * @author Andreas Aderhold, andi@binarycloud.com */ public function getBasedir() { if ($this->basedir === null) { try { // try to set it $this->setBasedir("."); } catch (BuildException $exc) { throw new BuildException("Can not set default basedir. ".$exc->getMessage()); } } return $this->basedir; } /** * Sets system properties and the environment variables for this project. * * @return void */ public function setSystemProperties() { // first get system properties $systemP = array_merge( self::getProperties(), Phing::getProperties() ); foreach($systemP as $name => $value) { $this->setPropertyInternal($name, $value); } // and now the env vars foreach($_SERVER as $name => $value) { // skip arrays if (is_array($value)) { continue; } $this->setPropertyInternal('env.' . $name, $value); } return true; } /** * Adds a task definition. * @param string $name Name of tag. * @param string $class The class path to use. * @param string $classpath The classpat to use. */ public function addTaskDefinition($name, $class, $classpath = null) { $name = $name; $class = $class; if ($class === "") { $this->log("Task $name has no class defined.", Project::MSG_ERR); } elseif (!isset($this->taskdefs[$name])) { Phing::import($class, $classpath); $this->taskdefs[$name] = $class; $this->log(" +Task definiton: $name ($class)", Project::MSG_DEBUG); } else { $this->log("Task $name ($class) already registerd, skipping", Project::MSG_VERBOSE); } } /** * Returns the task definitions * @return array */ public function getTaskDefinitions() { return $this->taskdefs; } /** * Adds a data type definition. * @param string $typeName Name of the type. * @param string $typeClass The class to use. * @param string $classpath The classpath to use. */ public function addDataTypeDefinition($typeName, $typeClass, $classpath = null) { if (!isset($this->typedefs[$typeName])) { Phing::import($typeClass, $classpath); $this->typedefs[$typeName] = $typeClass; $this->log(" +User datatype: $typeName ($typeClass)", Project::MSG_DEBUG); } else { $this->log("Type $typeName ($typeClass) already registerd, skipping", Project::MSG_VERBOSE); } } /** * Returns the data type definitions * @return array */ public function getDataTypeDefinitions() { return $this->typedefs; } /** * Add a new target to the project * @param string $targetName * @param Target $target */ public function addTarget($targetName, &$target) { if (isset($this->targets[$targetName])) { throw new BuildException("Duplicate target: $targetName"); } $this->addOrReplaceTarget($targetName, $target); } /** * Adds or replaces a target in the project * @param string $targetName * @param Target $target */ public function addOrReplaceTarget($targetName, &$target) { $this->log(" +Target: $targetName", Project::MSG_DEBUG); $target->setProject($this); $this->targets[$targetName] = $target; $ctx = $this->getReference("phing.parsing.context"); $current = $ctx->getConfigurator()->getCurrentTargets(); $current[$targetName] = $target; } /** * Returns the available targets * @return array */ public function getTargets() { return $this->targets; } /** * Create a new task instance and return reference to it. This method is * sorta factory like. A _local_ instance is created and a reference returned to * that instance. Usually PHP destroys local variables when the function call * ends. But not if you return a reference to that variable. * This is kinda error prone, because if no reference exists to the variable * it is destroyed just like leaving the local scope with primitive vars. There's no * central place where the instance is stored as in other OOP like languages. * * [HL] Well, ZE2 is here now, and this is still working. We'll leave this alone * unless there's any good reason not to. * * @param string $taskType Task name * @return Task A task object * @throws BuildException * Exception */ public function createTask($taskType) { try { $classname = ""; $tasklwr = strtolower($taskType); foreach ($this->taskdefs as $name => $class) { if (strtolower($name) === $tasklwr) { $classname = $class; break; } } if ($classname === "") { return null; } $cls = Phing::import($classname); if (!class_exists($cls)) { throw new BuildException("Could not instantiate class $cls, even though a class was specified. (Make sure that the specified class file contains a class with the correct name.)"); } $o = new $cls(); if ($o instanceof Task) { $task = $o; } else { $this->log (" (Using TaskAdapter for: $taskType)", Project::MSG_DEBUG); // not a real task, try adapter $taskA = new TaskAdapter(); $taskA->setProxy($o); $task = $taskA; } $task->setProject($this); $task->setTaskType($taskType); // set default value, can be changed by the user $task->setTaskName($taskType); $this->log (" +Task: " . $taskType, Project::MSG_DEBUG); } catch (Exception $t) { throw new BuildException("Could not create task of type: " . $taskType, $t); } // everything fine return reference return $task; } /** * Create a datatype instance and return reference to it * See createTask() for explanation how this works * * @param string $typeName Type name * @return object A datatype object * @throws BuildException * Exception */ public function createDataType($typeName) { try { $cls = ""; $typelwr = strtolower($typeName); foreach ($this->typedefs as $name => $class) { if (strtolower($name) === $typelwr) { $cls = StringHelper::unqualify($class); break; } } if ($cls === "") { return null; } if (!class_exists($cls)) { throw new BuildException("Could not instantiate class $cls, even though a class was specified. (Make sure that the specified class file contains a class with the correct name.)"); } $type = new $cls(); $this->log(" +Type: $typeName", Project::MSG_DEBUG); if (!($type instanceof DataType)) { throw new Exception("$class is not an instance of phing.types.DataType"); } if ($type instanceof ProjectComponent) { $type->setProject($this); } } catch (Exception $t) { throw new BuildException("Could not create type: $typeName", $t); } // everything fine return reference return $type; } /** * Executes a list of targets * * @param array $targetNames List of target names to execute * @return void * @throws BuildException */ public function executeTargets($targetNames) { foreach($targetNames as $tname) { $this->executeTarget($tname); } } /** * Executes a target * * @param string $targetName Name of Target to execute * @return void * @throws BuildException */ public function executeTarget($targetName) { // complain about executing void if ($targetName === null) { throw new BuildException("No target specified"); } // invoke topological sort of the target tree and run all targets // until targetName occurs. $sortedTargets = $this->_topoSort($targetName, $this->targets); $curIndex = (int) 0; $curTarget = null; do { try { $curTarget = $sortedTargets[$curIndex++]; $curTarget->performTasks(); } catch (BuildException $exc) { $this->log("Execution of target \"".$curTarget->getName()."\" failed for the following reason: ".$exc->getMessage(), Project::MSG_ERR); throw $exc; } } while ($curTarget->getName() !== $targetName); } /** * Helper function */ public function resolveFile($fileName, $rootDir = null) { if ($rootDir === null) { return $this->fileUtils->resolveFile($this->basedir, $fileName); } else { return $this->fileUtils->resolveFile($rootDir, $fileName); } } /** * Topologically sort a set of Targets. * @param string $root is the (String) name of the root Target. The sort is * created in such a way that the sequence of Targets until the root * target is the minimum possible such sequence. * @param array $targets is a array representing a "name to Target" mapping * @return An array of Strings with the names of the targets in * sorted order. */ public function _topoSort($root, &$targets) { $root = (string) $root; $ret = array(); $state = array(); $visiting = array(); // We first run a DFS based sort using the root as the starting node. // This creates the minimum sequence of Targets to the root node. // We then do a sort on any remaining unVISITED targets. // This is unnecessary for doing our build, but it catches // circular dependencies or missing Targets on the entire // dependency tree, not just on the Targets that depend on the // build Target. $this->_tsort($root, $targets, $state, $visiting, $ret); $retHuman = ""; for ($i=0, $_i=count($ret); $i < $_i; $i++) { $retHuman .= $ret[$i]->toString()." "; } $this->log("Build sequence for target '$root' is: $retHuman", Project::MSG_VERBOSE); $keys = array_keys($targets); while($keys) { $curTargetName = (string) array_shift($keys); if (!isset($state[$curTargetName])) { $st = null; } else { $st = (string) $state[$curTargetName]; } if ($st === null) { $this->_tsort($curTargetName, $targets, $state, $visiting, $ret); } elseif ($st === "VISITING") { throw new Exception("Unexpected node in visiting state: $curTargetName"); } } $retHuman = ""; for ($i=0,$_i=count($ret); $i < $_i; $i++) { $retHuman .= $ret[$i]->toString()." "; } $this->log("Complete build sequence is: $retHuman", Project::MSG_VERBOSE); return $ret; } // one step in a recursive DFS traversal of the target dependency tree. // - The array "state" contains the state (VISITED or VISITING or null) // of all the target names. // - The stack "visiting" contains a stack of target names that are // currently on the DFS stack. (NB: the target names in "visiting" are // exactly the target names in "state" that are in the VISITING state.) // 1. Set the current target to the VISITING state, and push it onto // the "visiting" stack. // 2. Throw a BuildException if any child of the current node is // in the VISITING state (implies there is a cycle.) It uses the // "visiting" Stack to construct the cycle. // 3. If any children have not been VISITED, tsort() the child. // 4. Add the current target to the Vector "ret" after the children // have been visited. Move the current target to the VISITED state. // "ret" now contains the sorted sequence of Targets upto the current // Target. public function _tsort($root, &$targets, &$state, &$visiting, &$ret) { $state[$root] = "VISITING"; $visiting[] = $root; if (!isset($targets[$root]) || !($targets[$root] instanceof Target)) { $target = null; } else { $target = $targets[$root]; } // make sure we exist if ($target === null) { $sb = "Target '$root' does not exist in this project."; array_pop($visiting); if (!empty($visiting)) { $parent = (string) $visiting[count($visiting)-1]; $sb .= " It is a dependency of target '$parent'."; } throw new BuildException($sb); } $deps = $target->getDependencies(); while($deps) { $cur = (string) array_shift($deps); if (!isset($state[$cur])) { $m = null; } else { $m = (string) $state[$cur]; } if ($m === null) { // not been visited $this->_tsort($cur, $targets, $state, $visiting, $ret); } elseif ($m == "VISITING") { // currently visiting this node, so have a cycle throw $this->_makeCircularException($cur, $visiting); } } $p = (string) array_pop($visiting); if ($root !== $p) { throw new Exception("Unexpected internal error: expected to pop $root but got $p"); } $state[$root] = "VISITED"; $ret[] = $target; } public function _makeCircularException($end, $stk) { $sb = "Circular dependency: $end"; do { $c = (string) array_pop($stk); $sb .= " <- ".$c; } while($c != $end); return new BuildException($sb); } /** * Adds a reference to an object. This method is called when the parser * detects a id="foo" attribute. It passes the id as $name and a reference * to the object assigned to this id as $value * @param string $name * @param object $object */ public function addReference($name, $object) { if (isset($this->references[$name])) { $this->log("Overriding previous definition of reference to $name", Project::MSG_WARN); } $this->log("Adding reference: $name -> ".get_class($object), Project::MSG_DEBUG); $this->references[$name] = $object; } /** * Returns the references array. * @return array */ public function getReferences() { return $this->references; } /** * Returns a specific reference. * @param string $key The reference id/key. * @return object Reference or null if not defined */ public function getReference($key) { if (isset($this->references[$key])) { return $this->references[$key]; } return null; // just to be explicit } /** * Abstracting and simplifyling Logger calls for project messages * @param string $msg * @param int $level */ public function log($msg, $level = Project::MSG_INFO) { $this->logObject($this, $msg, $level); } function logObject($obj, $msg, $level) { $this->fireMessageLogged($obj, $msg, $level); } function addBuildListener(BuildListener $listener) { $this->listeners[] = $listener; } function removeBuildListener(BuildListener $listener) { $newarray = array(); for ($i=0, $size=count($this->listeners); $i < $size; $i++) { if ($this->listeners[$i] !== $listener) { $newarray[] = $this->listeners[$i]; } } $this->listeners = $newarray; } function getBuildListeners() { return $this->listeners; } function fireBuildStarted() { $event = new BuildEvent($this); foreach($this->listeners as $listener) { $listener->buildStarted($event); } } function fireBuildFinished($exception) { $event = new BuildEvent($this); $event->setException($exception); foreach($this->listeners as $listener) { $listener->buildFinished($event); } } function fireTargetStarted($target) { $event = new BuildEvent($target); foreach($this->listeners as $listener) { $listener->targetStarted($event); } } function fireTargetFinished($target, $exception) { $event = new BuildEvent($target); $event->setException($exception); foreach($this->listeners as $listener) { $listener->targetFinished($event); } } function fireTaskStarted($task) { $event = new BuildEvent($task); foreach($this->listeners as $listener) { $listener->taskStarted($event); } } function fireTaskFinished($task, $exception) { $event = new BuildEvent($task); $event->setException($exception); foreach($this->listeners as $listener) { $listener->taskFinished($event); } } function fireMessageLoggedEvent($event, $message, $priority) { $event->setMessage($message, $priority); foreach($this->listeners as $listener) { $listener->messageLogged($event); } } function fireMessageLogged($object, $message, $priority) { $this->fireMessageLoggedEvent(new BuildEvent($object), $message, $priority); } }