* @link http://www.pradosoft.com/ * @copyright Copyright © 2005-2012 PradoSoft * @license http://www.pradosoft.com/license/ * @version $Id$ * @package System */ /** * TComponent class * * TComponent is the base class for all PRADO components. * TComponent implements the protocol of defining, using properties and events. * * A property is defined by a getter method, and/or a setter method. * Properties can be accessed in the way like accessing normal object members. * Reading or writing a property will cause the invocation of the corresponding * getter or setter method, e.g., * * $a=$this->Text; // equivalent to $a=$this->getText(); * $this->Text='abc'; // equivalent to $this->setText('abc'); * * The signatures of getter and setter methods are as follows, * * // getter, defines a readable property 'Text' * function getText() { ... } * // setter, defines a writable property 'Text', with $value being the value to be set to the property * function setText($value) { ... } * * Property names are case-insensitive. It is recommended that they are written * in the format of concatenated words, with the first letter of each word * capitalized (e.g. DisplayMode, ItemStyle). * * Since Prado 3.2 a new class of javascript-friendly properties have been introduced * to better deal with potential security problems like cross-site scripting issues. * All the data that gets sent clientside inside a javascript block is now encoded by default. * Sometimes there's the need to bypass this encoding and be able to send raw javascript code. * This new class of javascript-friendly properties are identified by their name * starting with 'js' (case insensitive): * * // getter, defines a readable property 'Text' * function getJsText() { ... } * // setter, defines a writable property 'Text', with $value being the value to be set to the property * function setJsText(TJavaScriptLiteral $value) { ... } * * Js-friendly properties can be accessed using both their Js-less name and their Js-enabled name: * * // set some simple text as property value * $component->Text = 'text'; * // set some javascript code as property value * $component->JsText = 'raw javascript'; * * In the first case, the property value will automatically gets encoded when sent * clientside inside a javascript block. * In the second case, the property will be 'marked' as being a safe javascript * statement and will not be encoded when rendered inside a javascript block. * This special handling makes use of the {@link TJavaScriptLiteral} class. * * An event is defined by the presence of a method whose name starts with 'on'. * The event name is the method name and is thus case-insensitive. * An event can be attached with one or several methods (called event handlers). * An event can be raised by calling {@link raiseEvent} method, upon which * the attached event handlers will be invoked automatically in the order they * are attached to the event. Event handlers must have the following signature, * * function eventHandlerFuncName($sender,$param) { ... } * * where $sender refers to the object who is responsible for the raising of the event, * and $param refers to a structure that may contain event-specific information. * To raise an event (assuming named as 'Click') of a component, use * * $component->raiseEvent('OnClick'); * * To attach an event handler to an event, use one of the following ways, * * $component->OnClick=$callback; // or $component->OnClick->add($callback); * $component->attachEventHandler('OnClick',$callback); * * The first two ways make use of the fact that $component->OnClick refers to * the event handler list {@link TList} for the 'OnClick' event. * The variable $callback contains the definition of the event handler that can * be either a string referring to a global function name, or an array whose * first element refers to an object and second element a method name/path that * is reachable by the object, e.g. * - 'buttonClicked' : buttonClicked($sender,$param); * - array($object,'buttonClicked') : $object->buttonClicked($sender,$param); * - array($object,'MainContent.SubmitButton.buttonClicked') : * $object->MainContent->SubmitButton->buttonClicked($sender,$param); * * @author Qiang Xue * @version $Id$ * @package System * @since 3.0 */ class TComponent { /** * @var array event handler lists */ private $_e=array(); /** * Returns a property value or an event handler list by property or event name. * Do not call this method. This is a PHP magic method that we override * to allow using the following syntax to read a property: * * $value=$component->PropertyName; * $value=$component->jsPropertyName; // return JavaScript literal * * and to obtain the event handler list for an event, * * $eventHandlerList=$component->EventName; * * @param string the property name or the event name * @return mixed the property value or the event handler list * @throws TInvalidOperationException if the property/event is not defined. */ public function __get($name) { $getter='get'.$name; $jsgetter = 'getjs'.$name; if(method_exists($this,$getter)) { // getting a property return $this->$getter(); } else if(method_exists($this,$jsgetter)) { // getting a property return (string)$this->$jsgetter(); } else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) { // getting an event (handler list) $name=strtolower($name); if(!isset($this->_e[$name])) $this->_e[$name]=new TList; return $this->_e[$name]; } else { throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); } } /** * Sets value of a component property. * Do not call this method. This is a PHP magic method that we override * to allow using the following syntax to set a property or attach an event handler. * * $this->PropertyName=$value; * $this->jsPropertyName=$value; // $value will be treated as a JavaScript literal * $this->EventName=$handler; * * @param string the property name or event name * @param mixed the property value or event handler * @throws TInvalidOperationException If the property is not defined or read-only. */ public function __set($name,$value) { if(method_exists($this, $setter='set'.$name)) { if (strncasecmp($name,'js',2)===0 && $value && !($value instanceof TJavaScriptLiteral)) $value = new TJavaScriptLiteral($value); $this->$setter($value); } else if(method_exists($this, $jssetter = 'setjs'.$name)) { if ($value and !($value instanceof TJavaScriptString)) $value = new TJavaScriptString($value); $this->$jssetter($value); } else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) { $this->attachEventHandler($name,$value); } else if(method_exists($this,'get'.$name) || method_exists($this,'getjs'.$name)) { throw new TInvalidOperationException('component_property_readonly',get_class($this),$name); } else { throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); } } /** * Calls a method. * Do not call this method. This is a PHP magic method that we override * to allow using the following syntax to call a property setter or getter. * * $this->getPropertyName($value); // if there's a $this->getjsPropertyName() method * $this->setPropertyName($value); // if there's a $this->setjsPropertyName() method * * @param string the getter or setter method name * @param mixed method call parameters * @throws TInvalidOperationException If the property is not defined or read-only. */ public function __call($name,$params) { $getset = substr($name,0,3); if (($getset=='get') || ($getset=='set')) { $propname = substr($name,3); $jsmethod = $getset.'js'.$propname; if (method_exists($this, $jsmethod)) { if (count($params)>0) if ($params[0] && !($params[0] instanceof TJavaScriptString)) $params[0] = new TJavaScriptString($params[0]); return call_user_func_array(array($this, $jsmethod), $params); } if (($getset=='set') and method_exists($this, 'getjs'.$propname)) throw new TInvalidOperationException('component_property_readonly',get_class($this),$name); } throw new TInvalidOperationException('component_property_undefined',get_class($this),$name); } /** * Determines whether a property is defined. * A property is defined if there is a getter or setter method * defined in the class. Note, property names are case-insensitive. * @param string the property name * @return boolean whether the property is defined */ public function hasProperty($name) { return method_exists($this,'get'.$name) || method_exists($this,'set'.$name) || method_exists($this,'getjs'.$name) || method_exists($this,'setjs'.$name) ; } /** * Determines whether a property can be read. * A property can be read if the class has a getter method * for the property name. Note, property name is case-insensitive. * @param string the property name * @return boolean whether the property can be read */ public function canGetProperty($name) { return method_exists($this,'get'.$name) || method_exists($this,'getjs'.$name); } /** * Determines whether a property can be set. * A property can be written if the class has a setter method * for the property name. Note, property name is case-insensitive. * @param string the property name * @return boolean whether the property can be written */ public function canSetProperty($name) { return method_exists($this,'set'.$name) || method_exists($this,'setjs'.$name); } /** * Evaluates a property path. * A property path is a sequence of property names concatenated by '.' character. * For example, 'Parent.Page' refers to the 'Page' property of the component's * 'Parent' property value (which should be a component also). * @param string property path * @return mixed the property path value */ public function getSubProperty($path) { $object=$this; foreach(explode('.',$path) as $property) $object=$object->$property; return $object; } /** * Sets a value to a property path. * A property path is a sequence of property names concatenated by '.' character. * For example, 'Parent.Page' refers to the 'Page' property of the component's * 'Parent' property value (which should be a component also). * @param string property path * @param mixed the property path value */ public function setSubProperty($path,$value) { $object=$this; if(($pos=strrpos($path,'.'))===false) $property=$path; else { $object=$this->getSubProperty(substr($path,0,$pos)); $property=substr($path,$pos+1); } $object->$property=$value; } /** * Determines whether an event is defined. * An event is defined if the class has a method whose name is the event name prefixed with 'on'. * Note, event name is case-insensitive. * @param string the event name * @return boolean */ public function hasEvent($name) { return strncasecmp($name,'on',2)===0 && method_exists($this,$name); } /** * @return boolean whether an event has been attached one or several handlers */ public function hasEventHandler($name) { $name=strtolower($name); return isset($this->_e[$name]) && $this->_e[$name]->getCount()>0; } /** * Returns the list of attached event handlers for an event. * @return TList list of attached event handlers for an event * @throws TInvalidOperationException if the event is not defined */ public function getEventHandlers($name) { if(strncasecmp($name,'on',2)===0 && method_exists($this,$name)) { $name=strtolower($name); if(!isset($this->_e[$name])) $this->_e[$name]=new TList; return $this->_e[$name]; } else throw new TInvalidOperationException('component_event_undefined',get_class($this),$name); } /** * Attaches an event handler to an event. * * The handler must be a valid PHP callback, i.e., a string referring to * a global function name, or an array containing two elements with * the first element being an object and the second element a method name * of the object. In Prado, you can also use method path to refer to * an event handler. For example, array($object,'Parent.buttonClicked') * uses a method path that refers to the method $object->Parent->buttonClicked(...). * * The event handler must be of the following signature, * * function handlerName($sender,$param) {} * * where $sender represents the object that raises the event, * and $param is the event parameter. * * This is a convenient method to add an event handler. * It is equivalent to {@link getEventHandlers}($name)->add($handler). * For complete management of event handlers, use {@link getEventHandlers} * to get the event handler list first, and then do various * {@link TList} operations to append, insert or remove * event handlers. You may also do these operations like * getting and setting properties, e.g., * * $component->OnClick[]=array($object,'buttonClicked'); * $component->OnClick->insertAt(0,array($object,'buttonClicked')); * * which are equivalent to the following * * $component->getEventHandlers('OnClick')->add(array($object,'buttonClicked')); * $component->getEventHandlers('OnClick')->insertAt(0,array($object,'buttonClicked')); * * * @param string the event name * @param callback the event handler * @throws TInvalidOperationException if the event does not exist */ public function attachEventHandler($name,$handler) { $this->getEventHandlers($name)->add($handler); } /** * Detaches an existing event handler. * This method is the opposite of {@link attachEventHandler}. * @param string event name * @param callback the event handler to be removed * @return boolean if the removal is successful */ public function detachEventHandler($name,$handler) { if($this->hasEventHandler($name)) { try { $this->getEventHandlers($name)->remove($handler); return true; } catch(Exception $e) { } } return false; } /** * Raises an event. * This method represents the happening of an event and will * invoke all attached event handlers for the event. * @param string the event name * @param mixed the event sender object * @param TEventParameter the event parameter * @throws TInvalidOperationException if the event is undefined * @throws TInvalidDataValueException If an event handler is invalid */ public function raiseEvent($name,$sender,$param) { $name=strtolower($name); if(isset($this->_e[$name])) { foreach($this->_e[$name] as $handler) { if(is_string($handler)) { if(($pos=strrpos($handler,'.'))!==false) { $object=$this->getSubProperty(substr($handler,0,$pos)); $method=substr($handler,$pos+1); if(method_exists($object,$method)) $object->$method($sender,$param); else throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,$handler); } else call_user_func($handler,$sender,$param); } else if(is_callable($handler,true)) { // an array: 0 - object, 1 - method name/path list($object,$method)=$handler; if(is_string($object)) // static method call call_user_func($handler,$sender,$param); else { if(($pos=strrpos($method,'.'))!==false) { $object=$this->getSubProperty(substr($method,0,$pos)); $method=substr($method,$pos+1); } if(method_exists($object,$method)) $object->$method($sender,$param); else throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,$handler[1]); } } else throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,gettype($handler)); } } else if(!$this->hasEvent($name)) throw new TInvalidOperationException('component_event_undefined',get_class($this),$name); } /** * Evaluates a PHP expression in the context of this control. * @return mixed the expression result * @throws TInvalidOperationException if the expression is invalid */ public function evaluateExpression($expression) { try { if(eval("\$result=$expression;")===false) throw new Exception(''); return $result; } catch(Exception $e) { throw new TInvalidOperationException('component_expression_invalid',get_class($this),$expression,$e->getMessage()); } } /** * Evaluates a list of PHP statements. * @param string PHP statements * @return string content echoed or printed by the PHP statements * @throws TInvalidOperationException if the statements are invalid */ public function evaluateStatements($statements) { try { ob_start(); if(eval($statements)===false) throw new Exception(''); $content=ob_get_contents(); ob_end_clean(); return $content; } catch(Exception $e) { throw new TInvalidOperationException('component_statements_invalid',get_class($this),$statements,$e->getMessage()); } } /** * This method is invoked after the component is instantiated by a template. * When this method is invoked, the component's properties have been initialized. * The default implementation of this method will invoke * the potential parent component's {@link addParsedObject}. * This method can be overridden. * @param TComponent potential parent of this control * @see addParsedObject */ public function createdOnTemplate($parent) { $parent->addParsedObject($this); } /** * Processes an object that is created during parsing template. * The object can be either a component or a static text string. * This method can be overridden to customize the handling of newly created objects in template. * Only framework developers and control developers should use this method. * @param string|TComponent text string or component parsed and instantiated in template * @see createdOnTemplate */ public function addParsedObject($object) { } /** * Do not call this method. This is a PHP magic method that will be called automatically * after any unserialization; it can perform reinitialization tasks on the object. */ public function __wakeup() { if ($this->_e===null) $this->_e = array(); } /** * Returns an array with the names of all variables of that object that should be serialized. * Do not call this method. This is a PHP magic method that will be called automatically * prior to any serialization. */ public function __sleep() { $a = (array)$this; $a = array_keys($a); $exprops = array(); if ($this->_e===array()) $exprops[] = "\0TComponent\0_e"; return array_diff($a,$exprops); } } /** * TEnumerable class. * TEnumerable is the base class for all enumerable types. * To define an enumerable type, extend TEnumberable and define string constants. * Each constant represents an enumerable value. * The constant name must be the same as the constant value. * For example, * * class TTextAlign extends TEnumerable * { * const Left='Left'; * const Right='Right'; * } * * Then, one can use the enumerable values such as TTextAlign::Left and * TTextAlign::Right. * * @author Qiang Xue * @version $Id$ * @package System * @since 3.0 */ class TEnumerable implements Iterator { private $_enums = array(); public function __construct() { $reflection = new ReflectionClass($this); $this->_enums = $reflection->getConstants(); } public function current() { return current($this->_enums); } public function key() { return key($this->_enums); } public function next() { return next($this->_enums); } public function rewind() { reset($this->_enums); } public function valid() { return $this->current() !== false; } } /** * TPropertyValue class * * TPropertyValue is a utility class that provides static methods * to convert component property values to specific types. * * TPropertyValue is commonly used in component setter methods to ensure * the new property value is of specific type. * For example, a boolean-typed property setter method would be as follows, * * function setPropertyName($value) { * $value=TPropertyValue::ensureBoolean($value); * // $value is now of boolean type * } * * * Properties can be of the following types with specific type conversion rules: * - string: a boolean value will be converted to 'true' or 'false'. * - boolean: string 'true' (case-insensitive) will be converted to true, * string 'false' (case-insensitive) will be converted to false. * - integer * - float * - array: string starting with '(' and ending with ')' will be considered as * as an array expression and will be evaluated. Otherwise, an array * with the value to be ensured is returned. * - object * - enum: enumerable type, represented by an array of strings. * * @author Qiang Xue * @version $Id$ * @package System * @since 3.0 */ class TPropertyValue { /** * Converts a value to boolean type. * Note, string 'true' (case-insensitive) will be converted to true, * string 'false' (case-insensitive) will be converted to false. * If a string represents a non-zero number, it will be treated as true. * @param mixed the value to be converted. * @return boolean */ public static function ensureBoolean($value) { if (is_string($value)) return strcasecmp($value,'true')==0 || $value!=0; else return (boolean)$value; } /** * Converts a value to string type. * Note, a boolean value will be converted to 'true' if it is true * and 'false' if it is false. * @param mixed the value to be converted. * @return string */ public static function ensureString($value) { if (TJavaScript::isJsLiteral($value)) return $value; if (is_bool($value)) return $value?'true':'false'; else return (string)$value; } /** * Converts a value to integer type. * @param mixed the value to be converted. * @return integer */ public static function ensureInteger($value) { return (integer)$value; } /** * Converts a value to float type. * @param mixed the value to be converted. * @return float */ public static function ensureFloat($value) { return (float)$value; } /** * Converts a value to array type. If the value is a string and it is * in the form (a,b,c) then an array consisting of each of the elements * will be returned. If the value is a string and it is not in this form * then an array consisting of just the string will be returned. If the value * is not a string then * @param mixed the value to be converted. * @return array */ public static function ensureArray($value) { if(is_string($value)) { $value = trim($value); $len = strlen($value); if ($len >= 2 && $value[0] == '(' && $value[$len-1] == ')') { eval('$array=array'.$value.';'); return $array; } else return $len>0?array($value):array(); } else return (array)$value; } /** * Converts a value to object type. * @param mixed the value to be converted. * @return object */ public static function ensureObject($value) { return (object)$value; } /** * Converts a value to enum type. * * This method checks if the value is of the specified enumerable type. * A value is a valid enumerable value if it is equal to the name of a constant * in the specified enumerable type (class). * For more details about enumerable, see {@link TEnumerable}. * * For backward compatibility, this method also supports sanity * check of a string value to see if it is among the given list of strings. * @param mixed the value to be converted. * @param mixed class name of the enumerable type, or array of valid enumeration values. If this is not an array, * the method considers its parameters are of variable length, and the second till the last parameters are enumeration values. * @return string the valid enumeration value * @throws TInvalidDataValueException if the original value is not in the string array. */ public static function ensureEnum($value,$enums) { static $types=array(); if(func_num_args()===2 && is_string($enums)) { if(!isset($types[$enums])) $types[$enums]=new ReflectionClass($enums); if($types[$enums]->hasConstant($value)) return $value; else throw new TInvalidDataValueException( 'propertyvalue_enumvalue_invalid',$value, implode(' | ',$types[$enums]->getConstants())); } else if(!is_array($enums)) { $enums=func_get_args(); array_shift($enums); } if(in_array($value,$enums,true)) return $value; else throw new TInvalidDataValueException('propertyvalue_enumvalue_invalid',$value,implode(' | ',$enums)); } /** * Converts the value to 'null' if the given value is empty * @param mixed value to be converted * @return mixed input or NULL if input is empty */ public static function ensureNullIfEmpty($value) { return empty($value) ? null : $value; } } /** * TEventParameter class. * TEventParameter is the base class for all event parameter classes. * * @author Qiang Xue * @version $Id$ * @package System * @since 3.0 */ class TEventParameter extends TComponent { } /** * TComponentReflection class. * * TComponentReflection provides functionalities to inspect the public/protected * properties, events and methods defined in a class. * * The following code displays the properties and events defined in {@link TDataGrid}, * * $reflection=new TComponentReflection('TDataGrid'); * Prado::varDump($reflection->getProperties()); * Prado::varDump($reflection->getEvents()); * * * @author Qiang Xue * @version $Id$ * @package System * @since 3.0 */ class TComponentReflection extends TComponent { private $_className; private $_properties=array(); private $_events=array(); private $_methods=array(); /** * Constructor. * @param object|string the component instance or the class name * @throws TInvalidDataTypeException if the object is not a component */ public function __construct($component) { if(is_string($component) && class_exists($component,false)) $this->_className=$component; else if(is_object($component)) $this->_className=get_class($component); else throw new TInvalidDataTypeException('componentreflection_class_invalid'); $this->reflect(); } private function isPropertyMethod($method) { $methodName=$method->getName(); return $method->getNumberOfRequiredParameters()===0 && strncasecmp($methodName,'get',3)===0 && isset($methodName[3]); } private function isEventMethod($method) { $methodName=$method->getName(); return strncasecmp($methodName,'on',2)===0 && isset($methodName[2]); } private function reflect() { $class=new ReflectionClass($this->_className); $properties=array(); $events=array(); $methods=array(); $isComponent=is_subclass_of($this->_className,'TComponent') || strcasecmp($this->_className,'TComponent')===0; foreach($class->getMethods() as $method) { if($method->isPublic() || $method->isProtected()) { $methodName=$method->getName(); if(!$method->isStatic() && $isComponent) { if($this->isPropertyMethod($method)) $properties[substr($methodName,3)]=$method; else if($this->isEventMethod($method)) { $methodName[0]='O'; $events[$methodName]=$method; } } if(strncmp($methodName,'__',2)!==0) $methods[$methodName]=$method; } } $reserved=array(); ksort($properties); foreach($properties as $name=>$method) { $this->_properties[$name]=array( 'type'=>$this->determinePropertyType($method), 'readonly'=>!$class->hasMethod('set'.$name), 'protected'=>$method->isProtected(), 'class'=>$method->getDeclaringClass()->getName(), 'comments'=>$method->getDocComment() ); $reserved['get'.strtolower($name)]=1; $reserved['set'.strtolower($name)]=1; } ksort($events); foreach($events as $name=>$method) { $this->_events[$name]=array( 'class'=>$method->getDeclaringClass()->getName(), 'protected'=>$method->isProtected(), 'comments'=>$method->getDocComment() ); $reserved[strtolower($name)]=1; } ksort($methods); foreach($methods as $name=>$method) { if(!isset($reserved[strtolower($name)])) $this->_methods[$name]=array( 'class'=>$method->getDeclaringClass()->getName(), 'protected'=>$method->isProtected(), 'static'=>$method->isStatic(), 'comments'=>$method->getDocComment() ); } } /** * Determines the property type. * This method uses the doc comment to determine the property type. * @param ReflectionMethod * @return string the property type, '{unknown}' if type cannot be determined from comment */ protected function determinePropertyType($method) { $comment=$method->getDocComment(); if(preg_match('/@return\\s+(.*?)\\s+/',$comment,$matches)) return $matches[1]; else return '{unknown}'; } /** * @return string class name of the component */ public function getClassName() { return $this->_className; } /** * @return array list of component properties. Array keys are property names. * Each array element is of the following structure: * [type]=>property type, * [readonly]=>whether the property is read-only, * [protected]=>whether the method is protected or not * [class]=>the class where the property is inherited from, * [comments]=>comments associated with the property. */ public function getProperties() { return $this->_properties; } /** * @return array list of component events. Array keys are event names. * Each array element is of the following structure: * [protected]=>whether the event is protected or not * [class]=>the class where the event is inherited from. * [comments]=>comments associated with the event. */ public function getEvents() { return $this->_events; } /** * @return array list of public/protected methods. Array keys are method names. * Each array element is of the following structure: * [protected]=>whether the method is protected or not * [static]=>whether the method is static or not * [class]=>the class where the property is inherited from, * [comments]=>comments associated with the event. */ public function getMethods() { return $this->_methods; } } /** * TJavaScriptLiteral class that encloses string literals that are not * supposed to be escaped by {@link TJavaScript::encode() } * * Since Prado 3.2 all the data that gets sent clientside inside a javascript statement * is encoded by default to avoid any kind of injection. * Sometimes there's the need to bypass this encoding and send raw javascript code. * To ensure that a string doesn't get encoded by {@link TJavaScript::encode() }, * construct a new TJavaScriptLiteral: * * // a javascript test string * $js="alert('hello')"; * // the string in $raw will not be encoded when sent clientside inside a javascript block * $raw=new TJavaScriptLiteral($js); * // shortened form * $raw=_js($js); * * * @version $Id$ * @package System * @since prado 3.2 */ class TJavaScriptLiteral { protected $_s; public function __construct($s) { $this->_s = $s; } public function __toString() { return (string)$this->_s; } public function toJavaScriptLiteral() { return $this->__toString(); } } /** * TJavaScriptString class is an internal class that marks strings that will be * forcibly encoded when rendered inside a javascript block * * @version $Id$ * @package System * @since prado 3.2 */ class TJavaScriptString extends TJavaScriptLiteral { public function toJavaScriptLiteral() { return TJavaScript::jsonEncode((string)$this->_s,JSON_HEX_QUOT | JSON_HEX_APOS | JSON_HEX_TAG); } }