PageRenderTime 40ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/core/src/main/php/xp/command/Runner.class.php

https://github.com/ghiata/xp-framework
PHP | 429 lines | 293 code | 47 blank | 89 comment | 58 complexity | fca8bde2efeccd492503008e07bbba41 MD5 | raw file
  1. <?php namespace xp\command;
  2. use util\cmd\ParamString;
  3. use io\streams\StringReader;
  4. use io\streams\StringWriter;
  5. use io\streams\ConsoleInputStream;
  6. use io\streams\ConsoleOutputStream;
  7. use util\log\Logger;
  8. use util\log\context\EnvironmentAware;
  9. use util\PropertyManager;
  10. use util\FilesystemPropertySource;
  11. use util\ResourcePropertySource;
  12. use rdbms\ConnectionManager;
  13. /**
  14. * Runs util.cmd.Command subclasses on the command line.
  15. *
  16. * Usage:
  17. * <pre>
  18. * $ xpcli [options] fully.qualified.class.Name [classoptions]
  19. * </pre>
  20. *
  21. * Options includes one of the following:
  22. * <pre>
  23. * -c:
  24. * Add the path to the PropertyManager sources. The PropertyManager
  25. * is used for dependency injection. If files called log.ini exists
  26. * in this paths, the Logger will be configured with. If any
  27. * database.ini are present there, the ConnectionManager will be
  28. * configured with it. (If not given etc is used as default path)
  29. *
  30. * -cp:
  31. * Add the path value to the class path.
  32. *
  33. * -v:
  34. * Enable verbosity (show complete stack trace when exceptions
  35. * occurred)
  36. * </pre>
  37. *
  38. * @test xp://net.xp_framework.unittest.util.cmd.RunnerTest
  39. * @see xp://util.cmd.Command
  40. * @purpose Runner
  41. */
  42. class Runner extends \lang\Object {
  43. private static
  44. $in = null,
  45. $out = null,
  46. $err = null;
  47. private
  48. $verbose= false;
  49. const DEFAULT_CONFIG_PATH = 'etc';
  50. static function __static() {
  51. self::$in= new StringReader(new ConsoleInputStream(STDIN));
  52. self::$out= new StringWriter(new ConsoleOutputStream(STDOUT));
  53. self::$err= new StringWriter(new ConsoleOutputStream(STDERR));
  54. }
  55. /**
  56. * Converts api-doc "markup" to plain text w/ ASCII "art"
  57. *
  58. * @param string markup
  59. * @return string text
  60. */
  61. protected static function textOf($markup) {
  62. $line= str_repeat('=', 72);
  63. return strip_tags(preg_replace(array(
  64. '#<pre>#', '#</pre>#', '#<li>#',
  65. ), array(
  66. $line, $line, '* ',
  67. ), trim($markup)));
  68. }
  69. /**
  70. * Show usage
  71. *
  72. * @param lang.XPClass class
  73. */
  74. public static function showUsage(\lang\XPClass $class) {
  75. // Description
  76. if (null !== ($comment= $class->getComment())) {
  77. self::$err->writeLine(self::textOf($comment));
  78. self::$err->writeLine(str_repeat('=', 72));
  79. }
  80. $extra= $details= $positional= array();
  81. foreach ($class->getMethods() as $method) {
  82. if (!$method->hasAnnotation('arg')) continue;
  83. $arg= $method->getAnnotation('arg');
  84. $name= strtolower(preg_replace('/^set/', '', $method->getName()));;
  85. $comment= self::textOf($method->getComment());
  86. if (0 == $method->numParameters()) {
  87. $optional= true;
  88. } else {
  89. list($first, )= $method->getParameters();
  90. $optional= $first->isOptional();
  91. }
  92. if (isset($arg['position'])) {
  93. $details['#'.($arg['position'] + 1)]= $comment;
  94. $positional[$arg['position']]= $name;
  95. } else if (isset($arg['name'])) {
  96. $details['--'.$arg['name'].' | -'.(isset($arg['short']) ? $arg['short'] : $arg['name']{0})]= $comment;
  97. $extra[$arg['name']]= $optional;
  98. } else {
  99. $details['--'.$name.' | -'.(isset($arg['short']) ? $arg['short'] : $name{0})]= $comment;
  100. $extra[$name]= $optional;
  101. }
  102. }
  103. // Usage
  104. asort($positional);
  105. self::$err->write('Usage: $ xpcli ', $class->getName(), ' ');
  106. foreach ($positional as $name) {
  107. self::$err->write('<', $name, '> ');
  108. }
  109. foreach ($extra as $name => $optional) {
  110. self::$err->write(($optional ? '[' : ''), '--', $name, ($optional ? '] ' : ' '));
  111. }
  112. self::$err->writeLine();
  113. // Argument details
  114. self::$err->writeLine('Arguments:');
  115. foreach ($details as $which => $comment) {
  116. self::$err->writeLine('* ', $which, "\n ", str_replace("\n", "\n ", $comment), "\n");
  117. }
  118. }
  119. /**
  120. * Main method
  121. *
  122. * @param string[] args
  123. * @return int
  124. */
  125. public static function main(array $args) {
  126. return create(new self())->run(new ParamString($args));
  127. }
  128. /**
  129. * Reassigns standard input stream
  130. *
  131. * @param io.streams.InputStream in
  132. * @return io.streams.InputStream the given output stream
  133. */
  134. public function setIn(\io\streams\InputStream $in) {
  135. self::$in= new StringReader($in);
  136. return $in;
  137. }
  138. /**
  139. * Reassigns standard output stream
  140. *
  141. * @param io.streams.OutputStream out
  142. * @return io.streams.OutputStream the given output stream
  143. */
  144. public function setOut(\io\streams\OutputStream $out) {
  145. self::$out= new StringWriter($out);
  146. return $out;
  147. }
  148. /**
  149. * Reassigns standard error stream
  150. *
  151. * @param io.streams.OutputStream error
  152. * @return io.streams.OutputStream the given output stream
  153. */
  154. public function setErr(\io\streams\OutputStream $err) {
  155. self::$err= new StringWriter($err);
  156. return $err;
  157. }
  158. /**
  159. * Main method
  160. *
  161. * @param util.cmd.ParamString params
  162. * @return int
  163. */
  164. public function run(ParamString $params) {
  165. // No arguments given - show our own usage
  166. if ($params->count < 1) {
  167. self::$err->writeLine(self::textOf(\lang\XPClass::forName(\xp::nameOf(__CLASS__))->getComment()));
  168. return 1;
  169. }
  170. // Configure properties
  171. $pm= PropertyManager::getInstance();
  172. // Separate runner options from class options
  173. for ($offset= 0, $i= 0; $i < $params->count; $i++) switch ($params->list[$i]) {
  174. case '-c':
  175. if (0 == strncmp('res://', $params->list[$i+ 1], 6)) {
  176. $pm->appendSource(new ResourcePropertySource(substr($params->list[$i+ 1], 6)));
  177. } else {
  178. $pm->appendSource(new FilesystemPropertySource($params->list[$i+ 1]));
  179. }
  180. $offset+= 2; $i++;
  181. break;
  182. case '-cp':
  183. \lang\ClassLoader::registerPath($params->list[$i+ 1], null);
  184. $offset+= 2; $i++;
  185. break;
  186. case '-v':
  187. $this->verbose= true;
  188. $offset+= 1; $i++;
  189. break;
  190. default:
  191. break 2;
  192. }
  193. // Sanity check
  194. if (!$params->exists($offset)) {
  195. self::$err->writeLine('*** Missing classname');
  196. return 1;
  197. }
  198. // Use default path for PropertyManager if no sources set
  199. if (!$pm->getSources()) {
  200. $pm->configure(self::DEFAULT_CONFIG_PATH);
  201. }
  202. unset($params->list[-1]);
  203. $classname= $params->value($offset);
  204. $classparams= new ParamString(array_slice($params->list, $offset+ 1));
  205. // Class file or class name
  206. if (strstr($classname, \xp::CLASS_FILE_EXT)) {
  207. $file= new \io\File($classname);
  208. if (!$file->exists()) {
  209. self::$err->writeLine('*** Cannot load class from non-existant file ', $classname);
  210. return 1;
  211. }
  212. try {
  213. $class= \lang\ClassLoader::getDefault()->loadUri($file->getURI());
  214. } catch (\lang\ClassNotFoundException $e) {
  215. self::$err->writeLine('*** ', $this->verbose ? $e : $e->getMessage());
  216. return 1;
  217. }
  218. } else {
  219. try {
  220. $class= \lang\XPClass::forName($classname);
  221. } catch (\lang\ClassNotFoundException $e) {
  222. self::$err->writeLine('*** ', $this->verbose ? $e : $e->getMessage());
  223. return 1;
  224. }
  225. }
  226. // Check whether class is runnable
  227. if (!$class->isSubclassOf('lang.Runnable')) {
  228. self::$err->writeLine('*** ', $class->getName(), ' is not runnable');
  229. return 1;
  230. }
  231. // Usage
  232. if ($classparams->exists('help', '?')) {
  233. self::showUsage($class);
  234. return 0;
  235. }
  236. // Load, instantiate and initialize
  237. $l= Logger::getInstance();
  238. $pm->hasProperties('log') && $l->configure($pm->getProperties('log'));
  239. $cm= ConnectionManager::getInstance();
  240. $pm->hasProperties('database') && $cm->configure($pm->getProperties('database'));
  241. // Setup logger context for all registered log categories
  242. foreach (Logger::getInstance()->getCategories() as $category) {
  243. if (null === ($context= $category->getContext()) || !($context instanceof EnvironmentAware)) continue;
  244. $context->setHostname(\lang\System::getProperty('host.name'));
  245. $context->setRunner($this->getClassName());
  246. $context->setInstance($class->getName());
  247. $context->setResource(null);
  248. $context->setParams($params->string);
  249. }
  250. $instance= $class->newInstance();
  251. $instance->in= self::$in;
  252. $instance->out= self::$out;
  253. $instance->err= self::$err;
  254. $methods= $class->getMethods();
  255. // Injection
  256. foreach ($methods as $method) {
  257. if (!$method->hasAnnotation('inject')) continue;
  258. $inject= $method->getAnnotation('inject');
  259. if (isset($inject['type'])) {
  260. $type= $inject['type'];
  261. } else if ($restriction= $method->getParameter(0)->getTypeRestriction()) {
  262. $type= $restriction->getName();
  263. } else {
  264. $type= $method->getParameter(0)->getType()->getName();
  265. }
  266. try {
  267. switch ($type) {
  268. case 'rdbms.DBConnection': {
  269. $args= array($cm->getByHost($inject['name'], 0));
  270. break;
  271. }
  272. case 'util.Properties': {
  273. $p= $pm->getProperties($inject['name']);
  274. // If a PropertyAccess is retrieved which is not a util.Properties,
  275. // then, for BC sake, convert it into a util.Properties
  276. if (
  277. $p instanceof \util\PropertyAccess &&
  278. !$p instanceof \util\Properties
  279. ) {
  280. $convert= \util\Properties::fromString('');
  281. $section= $p->getFirstSection();
  282. while ($section) {
  283. // HACK: Properties::writeSection() would first attempts to
  284. // read the whole file, we cannot make use of it.
  285. $convert->_data[$section]= $p->readSection($section);
  286. $section= $p->getNextSection();
  287. }
  288. $args= array($convert);
  289. } else {
  290. $args= array($p);
  291. }
  292. break;
  293. }
  294. case 'util.log.LogCategory': {
  295. $args= array($l->getCategory($inject['name']));
  296. break;
  297. }
  298. default: {
  299. self::$err->writeLine('*** Unknown injection type "'.$type.'" at method "'.$method->getName().'"');
  300. return 2;
  301. }
  302. }
  303. $method->invoke($instance, $args);
  304. } catch (\lang\reflect\TargetInvocationException $e) {
  305. self::$err->writeLine('*** Error injecting '.$type.' '.$inject['name'].': '.$e->getCause()->compoundMessage());
  306. return 2;
  307. } catch (\lang\Throwable $e) {
  308. self::$err->writeLine('*** Error injecting '.$type.' '.$inject['name'].': '.$e->compoundMessage());
  309. return 2;
  310. }
  311. }
  312. // Arguments
  313. foreach ($methods as $method) {
  314. if ($method->hasAnnotation('args')) { // Pass all arguments
  315. if (!$method->hasAnnotation('args', 'select')) {
  316. $begin= 0;
  317. $end= $classparams->count;
  318. $pass= array_slice($classparams->list, 0, $end);
  319. } else {
  320. $pass= array();
  321. foreach (preg_split('/, ?/', $method->getAnnotation('args', 'select')) as $def) {
  322. if (is_numeric($def) || '-' == $def{0}) {
  323. $pass[]= $classparams->value((int)$def);
  324. } else {
  325. sscanf($def, '[%d..%d]', $begin, $end);
  326. isset($begin) || $begin= 0;
  327. isset($end) || $end= $classparams->count- 1;
  328. while ($begin <= $end) {
  329. $pass[]= $classparams->value($begin++);
  330. }
  331. }
  332. }
  333. }
  334. try {
  335. $method->invoke($instance, array($pass));
  336. } catch (\lang\Throwable $e) {
  337. self::$err->writeLine('*** Error for arguments '.$begin.'..'.$end.': ', $this->verbose ? $e : $e->getMessage());
  338. return 2;
  339. }
  340. } else if ($method->hasAnnotation('arg')) { // Pass arguments
  341. $arg= $method->getAnnotation('arg');
  342. if (isset($arg['position'])) {
  343. $name= '#'.($arg['position']+ 1);
  344. $select= intval($arg['position']);
  345. $short= null;
  346. } else if (isset($arg['name'])) {
  347. $name= $select= $arg['name'];
  348. $short= isset($arg['short']) ? $arg['short'] : null;
  349. } else {
  350. $name= $select= strtolower(preg_replace('/^set/', '', $method->getName()));
  351. $short= isset($arg['short']) ? $arg['short'] : null;
  352. }
  353. if (0 == $method->numParameters()) {
  354. if (!$classparams->exists($select, $short)) continue;
  355. $args= array();
  356. } else if (!$classparams->exists($select, $short)) {
  357. list($first, )= $method->getParameters();
  358. if (!$first->isOptional()) {
  359. self::$err->writeLine('*** Argument '.$name.' does not exist!');
  360. return 2;
  361. }
  362. $args= array();
  363. } else {
  364. $args= array($classparams->value($select, $short));
  365. }
  366. try {
  367. $method->invoke($instance, $args);
  368. } catch (\lang\reflect\TargetInvocationException $e) {
  369. self::$err->writeLine('*** Error for argument '.$name.': ', $this->verbose ? $e->getCause() : $e->getCause()->compoundMessage());
  370. return 2;
  371. }
  372. }
  373. }
  374. try {
  375. $instance->run();
  376. } catch (\lang\Throwable $t) {
  377. self::$err->writeLine('*** ', $t->toString());
  378. return 70; // EX_SOFTWARE according to sysexits.h
  379. }
  380. return 0;
  381. }
  382. }