sqlblock.class.inc.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. <?php
  2. // Copyright (C) 2010 Combodo SARL
  3. //
  4. // This program is free software; you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation; version 3 of the License.
  7. //
  8. // This program is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with this program; if not, write to the Free Software
  15. // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. /**
  17. * SqlBlock - display tables or charts, given an SQL query - use cautiously!
  18. *
  19. *
  20. * @author Erwan Taloc <erwan.taloc@combodo.com>
  21. * @author Romain Quetiez <romain.quetiez@combodo.com>
  22. * @author Denis Flaven <denis.flaven@combodo.com>
  23. * @license http://www.opensource.org/licenses/gpl-3.0.html LGPL
  24. */
  25. require_once(APPROOT.'/application/webpage.class.inc.php');
  26. require_once(APPROOT.'/application/utils.inc.php');
  27. require_once(APPROOT.'/pages/php-ofc-library/open-flash-chart.php');
  28. /**
  29. * Helper class to design optimized dashboards, based on an SQL query
  30. *
  31. */
  32. class SqlBlock
  33. {
  34. protected $m_sQuery;
  35. protected $m_aColumns;
  36. protected $m_sTitle;
  37. protected $m_sType;
  38. protected $m_aParams;
  39. public function __construct($sQuery, $aColumns, $sTitle, $sType, $aParams = array())
  40. {
  41. $this->m_sQuery = $sQuery;
  42. $this->m_aColumns = $aColumns;
  43. $this->m_sTitle = $sTitle;
  44. $this->m_sType = $sType;
  45. $this->m_aParams = $aParams;
  46. }
  47. /**
  48. * Constructs a SqlBlock object from an XML template
  49. /*
  50. *
  51. * <sqlblock>
  52. * <sql>SELECT date_format(start_date, '%d') AS Date, count(*) AS Count FROM ticket WHERE DATE_SUB(NOW(), INTERVAL 15 DAY) &lt; start_date AND finalclass = 'UserIssue' GROUP BY date_format(start_date, '%d') AND $CONDITION(param1, ticket.org_id)$</sql>
  53. * <type>table</type>
  54. * <title>UserRequest:Overview-Title</title>
  55. * <parameter>
  56. * <name>param1</name>
  57. * <type>context</type>
  58. * <mapping>org_id</mapping>
  59. * </parameter>
  60. * <column>
  61. * <name>Date</name>
  62. * <label>UserRequest:Overview-Date</label>
  63. * <drilldown></drilldown>
  64. * </column>
  65. * <column>
  66. * <name>Count</name>
  67. * <label>UserRequest:Overview-Count</label>
  68. * <drilldown>SELECT UserIssue WHERE date_format(start_date, '%d') = :Date</drilldown>
  69. * </column>
  70. * </sqlblock>
  71. *
  72. * Tags
  73. * - sql: a (My)SQL query. Do not forget to use html entities (e.g. &lt; for <)
  74. * - type: table (default), bars or pie. If bars or pie is selected only the two first columns are taken into account.
  75. * - title: optional title, typed in clear or given as a dictionnary entry
  76. * - parameter: specifies how to map the context parameters (namely org_id) to a given named parameter in the query.
  77. * The expression $CONDITION(<param_name>, <sql_column_name>) will be automatically replaced by:
  78. * either the string "1" if there is no restriction on the organisation in iTop
  79. * or the string "(<sql_column_name>=<value_of_org_id>)" if there is a limitation to one organizations in iTop
  80. * or the string "(<sql_column_name> IN (<values_of_org_id>))" if there is a limitation to a given set of organizations in iTop
  81. * - column: specification of a column (not displayed if omitted)
  82. * - column / name: name of the column in the SQL query (use aliases)
  83. * - column / label: label, typed in clear or given as a dictionnary entry
  84. * - column / drilldown: NOT IMPLEMENTED YET - OQL with parameters corresponding to column names (in the query)
  85. *
  86. * @param $sTemplate string The XML template
  87. * @return DisplayBlock The DisplayBlock object, or null if the template is invalid
  88. */
  89. public static function FromTemplate($sTemplate)
  90. {
  91. $oXml = simplexml_load_string('<root>'.$sTemplate.'</root>', 'SimpleXMLElement', LIBXML_NOCDATA);
  92. if (false)
  93. {
  94. // Debug
  95. echo "<pre>\n";
  96. print_r($oXml);
  97. echo "</pre>\n";
  98. }
  99. if (isset($oXml->title))
  100. {
  101. $sTitle = (string)$oXml->title;
  102. }
  103. if (isset($oXml->type))
  104. {
  105. $sType = (string)$oXml->type;
  106. }
  107. else
  108. {
  109. $sType = 'table';
  110. }
  111. if (!isset($oXml->sql))
  112. {
  113. throw new Exception('Missing tag "sql" in sqlblock');
  114. }
  115. $sQuery = (string)$oXml->sql;
  116. $aColumns = array();
  117. if (isset($oXml->column))
  118. {
  119. foreach ($oXml->column AS $oColumnData)
  120. {
  121. if (!isset($oColumnData->name))
  122. {
  123. throw new Exception("Missing tag 'name' in sqlblock/column");
  124. }
  125. $sName = (string) $oColumnData->name;
  126. if (strlen($sName) == 0)
  127. {
  128. throw new Exception("Empty tag 'name' in sqlblock/column");
  129. }
  130. $aColumns[$sName] = array();
  131. if (isset($oColumnData->label))
  132. {
  133. $sLabel = (string)$oColumnData->label;
  134. if (strlen($sLabel) > 0)
  135. {
  136. $aColumns[$sName]['label'] = Dict::S($sLabel);
  137. }
  138. }
  139. if (isset($oColumnData->drilldown))
  140. {
  141. $sDrillDown = (string)$oColumnData->drilldown;
  142. if (strlen($sDrillDown) > 0)
  143. {
  144. $aColumns[$sName]['drilldown'] = $sDrillDown;
  145. }
  146. }
  147. }
  148. }
  149. $aParams = array();
  150. if (isset($oXml->parameter))
  151. {
  152. foreach ($oXml->parameter AS $oParamData)
  153. {
  154. if (!isset($oParamData->name))
  155. {
  156. throw new Exception("Missing tag 'name' for parameter in sqlblock/column");
  157. }
  158. $sName = (string) $oParamData->name;
  159. if (strlen($sName) == 0)
  160. {
  161. throw new Exception("Empty tag 'name' for parameter in sqlblock/column");
  162. }
  163. if (!isset($oParamData->mapping))
  164. {
  165. throw new Exception("Missing tag 'mapping' for parameter in sqlblock/column");
  166. }
  167. $sMapping = (string) $oParamData->mapping;
  168. if (strlen($sMapping) == 0)
  169. {
  170. throw new Exception("Empty tag 'mapping' for parameter in sqlblock/column");
  171. }
  172. if (isset($oParamData->type))
  173. {
  174. $sParamType = $oParamData->type;
  175. }
  176. else
  177. {
  178. $sParamType = 'context';
  179. }
  180. $aParams[$sName] = array('mapping' => $sMapping, 'type' => $sParamType);
  181. }
  182. }
  183. return new SqlBlock($sQuery, $aColumns, $sTitle, $sType, $aParams);
  184. }
  185. /**
  186. * Applies the defined parameters into the SQL query
  187. * @return string the SQL query to execute
  188. */
  189. public function BuildQuery()
  190. {
  191. $oAppContext = new ApplicationContext();
  192. $sQuery = $this->m_sQuery;
  193. $sQuery = str_replace('$DB_PREFIX$', MetaModel::GetConfig()->GetDBSubname(), $sQuery); // put the tables DB prefix (if any)
  194. foreach($this->m_aParams as $sName => $aParam)
  195. {
  196. if ($aParam['type'] == 'context')
  197. {
  198. $sSearchPattern = '/\$CONDITION\('.$sName.',([^\)]+)\)\$/';
  199. $value = $oAppContext->GetCurrentValue($aParam['mapping']);
  200. if (empty($value))
  201. {
  202. $sSQLExpr = '(1)';
  203. }
  204. else
  205. {
  206. // Special case for managing the hierarchy of organizations
  207. if (($aParam['mapping'] == 'org_id') && ( MetaModel::IsValidClass('Organization')))
  208. {
  209. $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass('Organization');
  210. if ($sHierarchicalKeyCode != false)
  211. {
  212. // organizations are in hierarchy... gather all the orgs below the given one...
  213. $sOQL = "SELECT Organization AS node JOIN Organization AS root ON node.$sHierarchicalKeyCode BELOW root.id WHERE root.id = :value";
  214. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL), array(), array('value' => $value));
  215. $aOrgIds = array();
  216. while($oOrg = $oSet->Fetch())
  217. {
  218. $aOrgIds[]= $oOrg->GetKey();
  219. }
  220. $sSQLExpr = '($1 IN('.implode(',', $aOrgIds).'))';
  221. }
  222. else
  223. {
  224. $sSQLExpr = '($1 = '.CMDBSource::Quote($value).')';
  225. }
  226. }
  227. else
  228. {
  229. $sSQLExpr = '($1 = '.CMDBSource::Quote($value).')';
  230. }
  231. }
  232. $sQuery = preg_replace($sSearchPattern, $sSQLExpr, $sQuery);
  233. }
  234. }
  235. return $sQuery;
  236. }
  237. public function RenderContent(WebPage $oPage, $aExtraParams = array())
  238. {
  239. if (empty($aExtraParams['currentId']))
  240. {
  241. $sId = 'sqlblock_'.$oPage->GetUniqueId(); // Works only if the page is not an Ajax one !
  242. }
  243. else
  244. {
  245. $sId = $aExtraParams['currentId'];
  246. }
  247. // $oPage->add($this->GetRenderContent($oPage, $aExtraParams, $sId));
  248. $sQuery = $this->BuildQuery();
  249. $res = CMDBSource::Query($sQuery);
  250. $aQueryCols = CMDBSource::GetColumns($res);
  251. // Prepare column definitions (check + give default values)
  252. //
  253. foreach($this->m_aColumns as $sName => $aColumnData)
  254. {
  255. if (!in_array($sName, $aQueryCols))
  256. {
  257. throw new Exception("Unknown column name '$sName' in sqlblock column");
  258. }
  259. if (!isset($aColumnData['label']))
  260. {
  261. $this->m_aColumns[$sName]['label'] = $sName;
  262. }
  263. if (isset($aColumnData['drilldown']) && !empty($aColumnData['drilldown']))
  264. {
  265. // Check if the OQL is valid
  266. try
  267. {
  268. $this->m_aColumns[$sName]['filter'] = DBObjectSearch::FromOQL($aColumnData['drilldown']);
  269. }
  270. catch(OQLException $e)
  271. {
  272. unset($aColumnData['drilldown']);
  273. }
  274. }
  275. }
  276. if (strlen($this->m_sTitle) > 0)
  277. {
  278. $oPage->add("<h2>".Dict::S($this->m_sTitle)."</h2>\n");
  279. }
  280. switch ($this->m_sType)
  281. {
  282. case 'bars':
  283. case 'pie':
  284. $aColNames = array_keys($this->m_aColumns);
  285. $sXColName = $aColNames[0];
  286. $sYColName = $aColNames[1];
  287. $aData = array();
  288. $aRows = array();
  289. while($aRow = CMDBSource::FetchArray($res))
  290. {
  291. $aData[$aRow[$sXColName]] = $aRow[$sYColName];
  292. $aRows[$aRow[$sXColName]] = $aRow;
  293. }
  294. $this->RenderChart($oPage, $sId, $aData, $this->m_aColumns[$sYColName]['drilldown'], $aRows);
  295. break;
  296. default:
  297. case 'table':
  298. $oAppContext = new ApplicationContext();
  299. $sContext = $oAppContext->GetForLink();
  300. if (!empty($sContext))
  301. {
  302. $sContext = '&'.$sContext;
  303. }
  304. $aDisplayConfig = array();
  305. foreach($this->m_aColumns as $sName => $aColumnData)
  306. {
  307. $aDisplayConfig[$sName] = array('label' => $aColumnData['label'], 'description' => '');
  308. }
  309. $aDisplayData = array();
  310. while($aRow = CMDBSource::FetchArray($res))
  311. {
  312. $aSQLColNames = array_keys($aRow);
  313. $aDisplayRow = array();
  314. foreach($this->m_aColumns as $sName => $aColumnData)
  315. {
  316. if (isset($aColumnData['filter']))
  317. {
  318. $sFilter = $aColumnData['drilldown'];
  319. $sClass = $aColumnData['filter']->GetClass();
  320. $sFilter = str_replace('SELECT '.$sClass, '', $sFilter);
  321. foreach($aSQLColNames as $sColName)
  322. {
  323. $sFilter = str_replace(':'.$sColName, "'".addslashes( $aRow[$sColName] )."'", $sFilter);
  324. }
  325. $sURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search_oql&search_form=0&oql_class='.$sClass.'&oql_clause='.urlencode($sFilter).'&format=html'.$sContext;
  326. $aDisplayRow[$sName] = '<a href="'.$sURL.'">'.$aRow[$sName]."</a>";
  327. }
  328. else
  329. {
  330. $aDisplayRow[$sName] = $aRow[$sName];
  331. }
  332. }
  333. $aDisplayData[] = $aDisplayRow;
  334. }
  335. $oPage->table($aDisplayConfig, $aDisplayData);
  336. break;
  337. }
  338. }
  339. public function GetRenderContent(WebPage $oPage, $aExtraParams = array(), $sId)
  340. {
  341. $sHtml = '';
  342. return $sHtml;
  343. }
  344. protected function RenderChart($oPage, $sId, $aValues, $sDrillDown = '', $aRows = array())
  345. {
  346. // 1- Compute Open Flash Chart data
  347. //
  348. $aValueKeys = array();
  349. $index = 0;
  350. if ((count($aValues) > 0) && ($sDrillDown != ''))
  351. {
  352. $oFilter = DBObjectSearch::FromOQL($sDrillDown);
  353. $sClass = $oFilter->GetClass();
  354. $sOQLClause = str_replace('SELECT '.$sClass, '', $sDrillDown);
  355. $aSQLColNames = array_keys(current($aRows)); // Read the list of columns from the current (i.e. first) element of the array
  356. $oAppContext = new ApplicationContext();
  357. $sURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search_oql&search_form=0&oql_class='.$sClass.'&format=html&'.$oAppContext->GetForLink().'&oql_clause=';
  358. }
  359. $aURLs = array();
  360. foreach($aValues as $key => $value)
  361. {
  362. // Make sure that values are integers (so that max() will work....)
  363. // and build an array of STRING with the keys (numeric keys are transformed into string by PHP :-(
  364. $aValues[$key] = (int)$value;
  365. $aValueKeys[] = (string)$key;
  366. // Build the custom query for the 'drill down' on each element
  367. if ($sDrillDown != '')
  368. {
  369. $sFilter = $sOQLClause;
  370. foreach($aSQLColNames as $sColName)
  371. {
  372. $sFilter = str_replace(':'.$sColName, "'".addslashes( $aRows[$key][$sColName] )."'", $sFilter);
  373. $aURLs[$index] = $sURL.urlencode($sFilter);
  374. }
  375. }
  376. $index++;
  377. }
  378. $oChart = new open_flash_chart();
  379. if ($this->m_sType == 'bars')
  380. {
  381. $oChartElement = new bar_glass();
  382. if (count($aValues) > 0)
  383. {
  384. $maxValue = max($aValues);
  385. }
  386. else
  387. {
  388. $maxValue = 1;
  389. }
  390. $oYAxis = new y_axis();
  391. $aMagicValues = array(1,2,5,10);
  392. $iMultiplier = 1;
  393. $index = 0;
  394. $iTop = $aMagicValues[$index % count($aMagicValues)]*$iMultiplier;
  395. while($maxValue > $iTop)
  396. {
  397. $index++;
  398. $iTop = $aMagicValues[$index % count($aMagicValues)]*$iMultiplier;
  399. if (($index % count($aMagicValues)) == 0)
  400. {
  401. $iMultiplier = $iMultiplier * 10;
  402. }
  403. }
  404. //echo "oYAxis->set_range(0, $iTop, $iMultiplier);\n";
  405. $oYAxis->set_range(0, $iTop, $iMultiplier);
  406. $oChart->set_y_axis( $oYAxis );
  407. $aBarValues = array();
  408. foreach($aValues as $iValue)
  409. {
  410. $oBarValue = new bar_value($iValue);
  411. $oBarValue->on_click("ofc_drilldown_{$sId}");
  412. $aBarValues[] = $oBarValue;
  413. }
  414. $oChartElement->set_values($aBarValues);
  415. //$oChartElement->set_values(array_values($aValues));
  416. $oXAxis = new x_axis();
  417. $oXLabels = new x_axis_labels();
  418. // set them vertical
  419. $oXLabels->set_vertical();
  420. // set the label text
  421. $oXLabels->set_labels($aValueKeys);
  422. // Add the X Axis Labels to the X Axis
  423. $oXAxis->set_labels( $oXLabels );
  424. $oChart->set_x_axis( $oXAxis );
  425. }
  426. else
  427. {
  428. $oChartElement = new pie();
  429. $oChartElement->set_start_angle( 35 );
  430. $oChartElement->set_animate( true );
  431. $oChartElement->set_tooltip( '#label# - #val# (#percent#)' );
  432. $oChartElement->set_colours( array('#FF8A00', '#909980', '#2C2B33', '#CCC08D', '#596664') );
  433. $aData = array();
  434. foreach($aValues as $sValue => $iValue)
  435. {
  436. $oPieValue = new pie_value($iValue, $sValue); //@@ BUG: not passed via ajax !!!
  437. $oPieValue->on_click("ofc_drilldown_{$sId}");
  438. $aData[] = $oPieValue;
  439. }
  440. $oChartElement->set_values( $aData );
  441. $oChart->x_axis = null;
  442. }
  443. // Title given in HTML
  444. //$oTitle = new title($this->m_sTitle);
  445. //$oChart->set_title($oTitle);
  446. $oChart->set_bg_colour('#FFFFFF');
  447. $oChart->add_element( $oChartElement );
  448. $sData = $oChart->toPrettyString();
  449. $sData = json_encode($sData);
  450. // 2- Declare the Javascript function that will render the chart data\
  451. //
  452. $oPage->add_script(
  453. <<< EOF
  454. function ofc_get_data_{$sId}()
  455. {
  456. return $sData;
  457. }
  458. EOF
  459. );
  460. if (count($aURLs) > 0)
  461. {
  462. $sURLList = '';
  463. foreach($aURLs as $index => $sURL)
  464. {
  465. $sURLList .= "\taURLs[$index] = '".addslashes($sURL)."';\n";
  466. }
  467. $oPage->add_script(
  468. <<< EOF
  469. function ofc_drilldown_{$sId}(index)
  470. {
  471. var aURLs = new Array();
  472. {$sURLList}
  473. var sURL = aURLs[index];
  474. window.location.href = sURL; // Navigate !
  475. }
  476. EOF
  477. );
  478. }
  479. // 3- Insert the Open Flash chart
  480. //
  481. $oPage->add("<div id=\"$sId\"><div>\n");
  482. $oPage->add_ready_script(
  483. <<<EOF
  484. swfobject.embedSWF( "../images/open-flash-chart.swf",
  485. "{$sId}",
  486. "100%", "300","9.0.0",
  487. "expressInstall.swf",
  488. {"get-data":"ofc_get_data_{$sId}", "id":"{$sId}"},
  489. {'wmode': 'transparent'}
  490. );
  491. EOF
  492. );
  493. }
  494. }
  495. ?>