root/trunk/classes/phing/IntrospectionHelper.php

Revision 311, 22.3 kB (checked in by hans, 1 year ago)

Refs #188 - Moving some classes (File, BuildLogger, etc.)

  • Property svn:keywords set to author date id revision
Line 
1 <?php
2 /*
3  *  $Id$
4  *
5  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16  *
17  * This software consists of voluntary contributions made by many individuals
18  * and is licensed under the LGPL. For more information please see
19  * <http://phing.info>.
20  */
21
22 namespace phing;
23 use phing::util::StringHelper;
24 use phing::types::Path;
25
26 /**
27  * Helper class that collects the methods that a task or nested element
28  * holds to set attributes, create nested elements or hold PCDATA
29  * elements.
30  *
31  *<ul>
32  * <li><strong>SMART-UP INLINE DOCS</strong></li>
33  * <li><strong>POLISH-UP THIS CLASS</strong></li>
34  *</ul>
35  *
36  * @author    Andreas Aderhold <andi@binarycloud.com>
37  * @author    Hans Lellelid <hans@xmpl.org>
38  * @copyright © 2001,2002 THYRELL. All rights reserved
39  * @version   $Revision: 1.19 $
40  * @package   phing
41  */
42 class IntrospectionHelper {
43
44
45
46     /**
47      * Holds the attribute setter methods.
48      *
49      * @var array string[]
50      */
51     private $attributeSetters = array();
52
53     /** 
54      * Holds methods to create nested elements.
55      *
56      * @var array string[]
57      */
58     private $nestedCreators = array();
59
60     /**
61      * Holds methods to store configured nested elements.
62      *
63      * @var array string[]
64      */
65     private $nestedStorers = array();
66     
67     /**
68      * Map from attribute names to nested types.
69      */
70     private $nestedTypes = array();
71         
72     /**
73      * New idea in phing: any class can register certain
74      * keys -- e.g. "task.current_file" -- which can be used in
75      * task attributes, if supported.  In the build XML these
76      * are referred to like this:
77      *         <regexp pattern="\n" replace="%{task.current_file}"/>
78      * In the type/task a listener method must be defined:
79      *         function setListeningReplace($slot) {}
80      * @var array string[]
81       */
82     private $slotListeners = array();
83     
84     /**
85      * The method to add PCDATA stuff.
86      *
87      * @var string Method name of the addText (redundant?) method, if class supports it :)
88      */
89     private $methodAddText = null;
90
91     /**
92      * The Class that's been introspected.
93      *
94      * @var     object
95      * @access  private
96      */
97     private $bean;
98     
99     /**
100      * The cache of IntrospectionHelper classes instantiated by getHelper().
101      * @var array IntrospectionHelpers[]
102      */
103     private static $helpers = array();
104     
105     /**
106      * Factory method for helper objects.
107      *
108      * @param string $class The class to create a Helper for
109      */
110     public static function getHelper($class) {
111         if (!isset(self::$helpers[$class])) {
112             self::$helpers[$class] = new IntrospectionHelper($class);
113         }
114         return self::$helpers[$class];
115     }
116
117     /**
118      * This function constructs a new introspection helper for a specific class.
119      *
120      * This method loads all methods for the specified class and categorizes them
121      * as setters, creators, slot listeners, etc.  This way, the setAttribue() doesn't
122      * need to perform any introspection -- either the requested attribute setter/creator
123      * exists or it does not & a BuildException is thrown.
124      *
125      * @param string $bean The classname for this IH.
126      */
127     function __construct($class) {
128     
129         $this->bean = new ReflectionClass($class);
130         
131         //$methods = get_class_methods($bean);
132         foreach($this->bean->getMethods() as $method) {
133         
134             if ($method->isPublic()) {               
135             
136                 // We're going to keep case-insensitive method names
137                 // for as long as we're allowed :)  It makes it much
138                 // easier to map XML attributes to PHP class method names.
139                 $name = strtolower($method->getName());
140                 
141                 // There are a few "reserved" names that might look like attribute setters
142                 // but should actually just be skipped.  (Note: this means you can't ever
143                 // have an attribute named "location" or "tasktype" or a nested element named "task".)
144                 if ($name === "setlocation" || $name === "settasktype" || $name === "addtask") {
145                     continue;
146                 }
147                 
148                 if ($name === "addtext") {
149                     
150                     $this->methodAddText = $method;
151                     
152                 } elseif (strpos($name, "setlistening") === 0) {
153                     
154                     // Phing supports something unique called "RegisterSlots"
155                     // These are dynamic values that use a basic slot system so that
156                     // classes can register to listen to specific slots, and the value
157                     // will always be grabbed from the slot (and never set in the project
158                     // component).  This is useful for things like tracking the current
159                     // file being processed by a filter (e.g. AppendTask sets an append.current_file
160                     // slot, which can be ready by the XSLTParam type.)
161                     
162                     if (count($method->getParameters()) !== 1) {
163                         throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take exactly one parameter.");
164                     }
165                                                 
166                     $this->slotListeners[$name] = $method;
167                     
168                 } elseif (strpos($name, "set") === 0) {
169                     
170                     // A standard attribute setter.
171                     
172                     if (count($method->getParameters()) !== 1) {
173                         throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take exactly one parameter.");
174                     }
175                     
176                     $this->attributeSetters[$name] = $method;
177                     
178                 } elseif (strpos($name, "create") === 0) {                           
179                     
180                     if (count($method->getParameters()) > 0) {
181                         throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() may not take any parameters.");
182                     }
183                     
184                     // Because PHP doesn't support return types, we are going to do
185                     // two things here to guess return type:
186                     //     1) parse comments for an explicit value
187                     //     2) if that fails, assume that the part of the method after "create"
188                     //    is the name of the return type (in many cases it is not)
189                     
190                     // This isn't super important -- i.e. we're not instantaiting classes
191                     // based on this information.  It's more just so that IntrospectionHelper
192                     // can keep track of all the nested types -- and provide more helpful
193                     // exception messages, etc.
194                                 
195                     preg_match('/@return[\s]+([\w]+)/', $method->getDocComment(), $matches);
196                     if (!empty($matches[1]) && class_exists($matches[1], false)) {
197                         $this->nestedTypes[$name] = $matches[1];
198                     } else {                   
199                         // assume that method createEquals() creates object of type "Equals"
200                         // (that example would be false, of course)                   
201                         $this->nestedTypes[$name] = $this->getPropertyName($name, "create");
202                     }
203                     
204                     $this->nestedCreators[$name] = $method;
205                     
206                 } elseif (strpos($name, "addconfigured") === 0) {
207                     
208                     // *must* use class hints if using addConfigured ...
209                     
210                     // 1 param only
211                     $params = $method->getParameters();
212                     
213                     if (count($params) < 1) {
214                         throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take at least one parameter.");
215                     }
216                     
217                     if (count($params) > 1) {
218                         $this->warn($method->getDeclaringClass()->getName()."::".$method->getName()."() takes more than one parameter. (IH only uses the first)");
219                     }
220                     
221                     $classname = null;
222                     
223                     if (($hint = $params[0]->getClass()) !== null) {
224                         $classname = $hint->getName();   
225                     }                   
226                     
227                     if ($classname === null) {
228                         throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() method MUST use a class hint to indicate the class type of parameter.");
229                     }
230                         
231                     $this->nestedTypes[$name] = $classname;
232                 
233                     $this->nestedStorers[$name] = $method;
234                     
235                 } elseif (strpos($name, "add") === 0) {
236                     
237                     // *must* use class hints if using add ...
238                     
239                     // 1 param only
240                     $params = $method->getParameters();
241                     if (count($params) < 1) {
242                         throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take at least one parameter.");
243                     }
244                     
245                     if (count($params) > 1) {
246                         $this->warn($method->getDeclaringClass()->getName()."::".$method->getName()."() takes more than one parameter. (IH only uses the first)");
247                     }
248
249                     $classname = null;
250                     
251                     if (($hint = $params[0]->getClass()) !== null) {
252                         $classname = $hint->getName();   
253                     }                   
254                     
255                     // we don't use the classname here, but we need to make sure it exists before
256                     // we later try to instantiate a non-existant class
257                     if ($classname === null) {
258                         throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() method MUST use a class hint to indicate the class type of parameter.");
259                     }
260                 
261                     $this->nestedCreators[$name] = $method;
262                 }
263             } // if $method->isPublic()       
264         } // foreach       
265     }
266
267
268     /** Sets the named attribute. */
269     function setAttribute(Project $project, $element, $attributeName, &$value) {
270         
271         // we want to check whether the value we are setting looks like
272         // a slot-listener variable:  %{task.current_file}
273         //
274         // slot-listener variables are not like properties, in that they cannot be mixed with
275         // other text values.  The reason for this disparity is that properties are only
276         // set when first constructing objects from XML, whereas slot-listeners are always dynamic.
277         //
278         // This is made possible by PHP5 (objects automatically passed by reference) and PHP's loose
279         // typing.
280         
281         if (StringHelper::isSlotVar($value)) {
282             
283             $as = "setlistening" . strtolower($attributeName);
284
285             if (!isset($this->slotListeners[$as])) {
286                 $msg = $this->getElementName($project, $element) . " doesn't support a slot-listening '$attributeName' attribute.";
287                 throw new BuildException($msg);
288             }
289             
290             $method = $this->slotListeners[$as];
291             
292             $key = StringHelper::slotVar($value);
293             $value = Register::getSlot($key); // returns a RegisterSlot object which will hold current value of that register (accessible using getValue())
294             
295         } else {
296             
297             // Traditional value options
298             
299             $as = "set".strtolower($attributeName);
300             
301             if (!isset($this->attributeSetters[$as])) {
302                 $msg = $this->getElementName($project, $element) . " doesn't support the '$attributeName' attribute.";
303                 throw new BuildException($msg);
304             }
305             
306             $method = $this->attributeSetters[$as];           
307             
308             if ($as == "setrefid") {           
309                 $value = new Reference($value);
310             } else {
311             
312                 // decode any html entities in string
313                 $value = html_entity_decode($value);               
314                 
315                 // value is a string representation of a boolean type,
316                 // convert it to primitive
317                 if (StringHelper::isBoolean($value)) {
318
319                     $value = StringHelper::booleanValue($value);
320                 }
321                 
322                 // does method expect a File object? if so, then
323                 // pass a project-relative file.
324                 $params = $method->getParameters();
325
326                 $classname = null;
327                 
328                 if (($hint = $params[0]->getClass()) !== null) {
329                     $classname = $hint->getName();   
330                 }
331                 
332                 // there should only be one param; we'll just assume ....
333                 if ($classname !== null) {
334                     switch($classname) {
335                         case "phing::system::io::File":
336                             $value = $project->resolveFile($value);
337                             break;
338                         case "phing::types::Path":
339                             $value = new Path($project, $value);
340                             break;
341                         case "phing::types::Reference":
342                             $value = new Reference($value);
343                             break;           
344                         // any other object params we want to support should go here ...
345                     }
346                     
347                 } // if hint !== null
348                 
349             } // if not setrefid
350             
351         } // if is slot-listener
352         
353         try {
354             $project->log("    -calling setter ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);
355             $method->invoke($element, $value);
356         } catch(Exception $exc) {
357             throw new BuildException($exc);
358         }
359         
360     }
361
362     /** Adds PCDATA areas.*/
363     function addText(Project $project, $element, $text) {
364         if ($this->methodAddText === null) {
365             $msg = $this->getElementName($project, $element)." doesn't support nested text data.";
366             throw new BuildException($msg);
367         }       
368         try {
369             $method = $this->methodAddText;
370             $method->invoke($element, $text);
371         } catch (Exception $exc) {
372             throw new BuildException($exc);
373         }
374     }
375
376     /**
377      * Creates a named nested element.
378      *
379      * Valid creators can be in the form createFoo() or addFoo(Bar).
380      * @return object Returns the nested element.
381      * @throws BuildException
382      */
383     function createElement(Project $project, $element, $elementName) {
384     
385         $addMethod = "add".strtolower($elementName);
386         $createMethod = "create".strtolower($elementName);
387         $nestedElement = null;
388         
389         if (isset($this->nestedCreators[$createMethod])) {
390             
391             $method = $this->nestedCreators[$createMethod];
392              try { // try to invoke the creator method on object
393                 $project->log("    -calling creator ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);
394                 $nestedElement = $method->invoke($element);
395             } catch (Exception $exc) {
396                 throw new BuildException($exc);
397             }           
398             
399         } elseif (isset($this->nestedCreators[$addMethod])) {           
400             
401             $method = $this->nestedCreators[$addMethod];
402             
403             // project components must use class hints to support the add methods
404             
405             try { // try to invoke the adder method on object
406             
407                 $project->log("    -calling adder ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);
408                 // we've already assured that correct num of params
409                 // exist and that method is using class hints               
410                 $params = $method->getParameters();
411
412                 $classname = null;
413             
414                 if (($hint = $params[0]->getClass()) !== null) {
415                     $classname = $hint->getName();   
416                 }               
417                 
418                 // create a new instance of the object and add it via $addMethod               
419                 $nestedElement = new $classname();
420                 
421                 $method->invoke($element, $nestedElement);
422                                 
423             } catch (Exception $exc) {
424                 throw new BuildException($exc);
425             }
426         } else {
427             $msg = $this->getElementName($project, $element) . " doesn't support the '$elementName' creator/adder.";
428             throw new BuildException($msg);
429         }                               
430         
431         if ($nestedElement instanceof ProjectComponent) {
432             $nestedElement->setProject($project);
433         }
434         
435         return $nestedElement;
436     }
437
438     /**
439      * Creates a named nested element.
440      * @return void
441      * @throws BuildException
442      */
443     function storeElement($project, $element, $child, $elementName = null) {
444     
445         if ($elementName === null) {
446             return;
447         }
448         
449         $storer = "addconfigured".strtolower($elementName);
450           
451         if (isset($this->nestedStorers[$storer])) {
452             
453             $method = $this->nestedStorers[$storer];
454             
455             try {                               
456                 $project->log("    -calling storer ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);                   
457                 $method->invoke($element, $child);           
458             } catch (Exception $exc) {
459                 throw new BuildException($exc);
460             }
461         }
462         
463     }
464
465     /** Does the introspected class support PCDATA? */
466     function supportsCharacters() {
467         return ($this->methodAddText !== null);
468     }
469
470     /** Return all attribues supported by the introspected class. */
471     function getAttributes() {
472         $attribs = array();
473         foreach (array_keys($this->attributeSetters) as $setter) {
474             $attribs[] =$this->getPropertyName($setter, "set");
475         }
476         return $attribs;
477     }
478
479     /** Return all nested elements supported by the introspected class. */
480     function getNestedElements() {
481         return $this->nestedTypes;
482     }
483     
484     /**
485      * Get the the name for an element.
486      * When possible the full classnam (phing.tasks.system.PropertyTask) will
487      * be returned.  If not available (loaded in taskdefs or typedefs) then the
488      * XML element name will be returned.
489      *
490      * @param Project $project
491      * @param object $element The Task or type element.
492      * @return string Fully qualified class name of element when possible.
493      */
494     function getElementName(Project $project, $element) {
495       
496           $taskdefs = $project->getTaskDefinitions();
497         $typedefs = $project->getDataTypeDefinitions();
498         
499         // check if class of element is registered with project (tasks & types)       
500         // most element types don't have a getTag() method
501         $elClass = get_class($element);
502         
503         if (!in_array('getTag', get_class_methods($elClass))) {
504                 // loop through taskdefs and typesdefs and see if the class name
505                 // matches (case-insensitive) any of the classes in there
506                 foreach(array_merge($taskdefs, $typedefs) as $elName => $class) {
507