Ver código fonte

added AclQueryFeatures, filter geom, base filters

Piotr Labudda 9 anos atrás
pai
commit
c7bc820ec4
3 arquivos alterados com 301 adições e 15 exclusões
  1. 96 15
      SE/se-lib/AclQueryBuilder.php
  2. 204 0
      SE/se-lib/AclQueryFeatures.php
  3. 1 0
      SE/se-lib/Core/AclBase.php

+ 96 - 15
SE/se-lib/AclQueryBuilder.php

@@ -35,15 +35,11 @@ class AclQueryBuilder {
     $this->isNotInstances = [];
   }
 
-  public function select($propertyName) { // TODO: ogc:propertyName, *, xpath, @instances, etc...
-    if (!empty($propertyName) && !in_array($propertyName, $this->select)) $this->select[] = $propertyName;
-    return $this;
-  }
-
   public function from($from, $prefix = 't') {
     DBG::log([
       'from instanceof Core_AclBase' => ($from instanceof Core_AclBase),
       'from instanceof AntAclBase' => ($from instanceof AntAclBase),
+      'from instanceof TableAcl' => ($from instanceof TableAcl),
     ], 'array', "\$from class(".get_class($from).")");
     if ($this->from) throw new Exception("Duplicate FROM");
     $this->from = $from;
@@ -51,13 +47,49 @@ class AclQueryBuilder {
     return $this;
   }
 
-  public function where($params) {
-    if (!empty($params['rawWhere'])) {
-      $this->where[] = $params['rawWhere'];
-    } else {
-      DBG::log($params, 'array', "Where not supported");
-      throw new Exception("Where not supported");
+  public function select($propertyName) { // TODO: ogc:propertyName, *, xpath, @instances, etc...
+    if (empty($propertyName)) return $this; // SKIP empty values, null's, etc.
+    DBG::log($propertyName, 'array', "AclQueryBuilder->select");
+    if (is_array($propertyName) && !empty($propertyName['rawSelect'])) {
+      $this->select['__rawSelect__'] = $propertyName['rawSelect'];
+      return $this;
     }
+    if (!in_array($propertyName, $this->select)) $this->select[] = $propertyName; // TODO: split by property source
+    return $this;
+  }
+
+  public function where($fieldName, $comparisonSign = null, $value = null) {
+    if (!in_array($comparisonSign, [
+      '=', '>=', '<=', '<>', '!='
+      , 'like', 'not like'
+      , 'is not null', 'is null'
+      , 'Intersects', 'GeometryType'
+    ])) {
+      throw new Exception("Not implemented comparisonSign '{$comparisonSign}'");
+    }
+    $sqlFieldName = $fieldName; // TODO: getSqlFieldName
+    switch ($comparisonSign) {
+      case 'is not null': $this->where[] = "{$this->_fromPrefix}.{$sqlFieldName} is not null"; break;
+      case 'is null': $this->where[] = "{$this->_fromPrefix}.{$sqlFieldName} is null"; break;
+      case 'Intersects': $this->where[] = "Intersects(GeomFromText('{$value}'), {$this->_fromPrefix}.`{$sqlFieldName}`)=1"; break;
+      case 'GeometryType': $this->where[] = "GeometryType({$this->_fromPrefix}.`{$sqlFieldName}`)='{$value}'"; break;
+      default: $this->where[] = "{$this->_fromPrefix}.{$sqlFieldName} {$comparisonSign} " . DB::getPDO()->quote($value);
+    }
+    return $this;
+  }
+  public function whereIsNotNull($fieldName) {
+    $this->where[] = "{$this->_fromPrefix}.{$fieldName} is not null";
+    return $this;
+  }
+  public function whereRaw($rawWhere) {
+    $this->where[] = $rawWhere;
+    return $this;
+  }
+  // public function whereFunction($column, $operator = null, $value = null) {
+  public function whereFunction($fieldName, $comparisonSign = null, $value = null) {
+    throw new Exception("whereFunction not supported");
+    $this->where[] = '';
+    return $this;
   }
 
   public function join($join, $prefix, $params) {
@@ -68,9 +100,41 @@ class AclQueryBuilder {
   }
 
   public function orderBy($orderBy) { // TODO: ogc: order by ...
-
+    if (null !== $this->orderBy) throw new Exception("OrderBy already set!");
+    $this->orderBy = [];
+    if (!$orderBy) return $this;
+    // ID A,COL_X D,COL_Y A,...
+    $sortByEx = array_map('trim', explode(',', $orderBy));
+    $sortByEx = array_filter($sortByEx, function ($part) { return !empty($part); });
+    foreach ($sortByEx as $sortPart) {
+      $sortPartEx = explode(' ', $sortPart);
+      if (count($sortPartEx) > 2) throw new Exception("SortBy parse error #" . __LINE__);
+      $fieldName = trim($sortPartEx[0]);
+      if (!$this->isFieldAllowedToOrderBy($fieldName)) throw new Exception("SortBy parse error for field '{$fieldName}' #" . __LINE__);
+      $colSortDir = 'ASC';
+      if (count($sortPartEx) == 2) {
+        if ('A' == strtoupper($sortPartEx[1]) || 'ASC' == strtoupper($sortPartEx[1])) {
+        } else if ('D' == strtoupper($sortPartEx[1]) || 'DESC' == strtoupper($sortPartEx[1])) {
+          $colSortDir = 'DESC';
+        } else throw new Exception("SortBy parse error - unknown sort order '{$sortPartEx[1]}' #" . __LINE__);
+      }
+      $this->orderBy[] = [$fieldName, $colSortDir];
+    }
     return $this;
   }
+  public function isFieldAllowedToOrderBy($fieldName) {
+    return true;
+  }
+  public function generateOrderBySql() {
+    if (empty($this->orderBy)) return '';
+    $sortByList = [];
+    foreach ($this->orderBy as $orderBy) {
+      $sortByList[] = "t.`{$orderBy[0]}` {$orderBy[1]}";
+    }
+    return (!empty($sortByList))
+      ? "order by " . implode(", ", $sortByList)
+      : '';
+  }
 
   public function limit($limit) {
     $this->limit = (int)$limit;
@@ -104,11 +168,21 @@ class AclQueryBuilder {
   }
 
   public function execute() {
+    return $this->fetchAll();
+  }
+
+  public function fetchAll() {
     $sql = $this->generateSql();
-    DBG::log((array)$this, 'array', "AclQueryBuilder");
+    DBG::log((array)$this, 'array', "AclQueryBuilder::fetchAll");
     return DB::getPDO()->fetchAll($sql);
   }
 
+  public function fetchValue() {
+    $sql = $this->generateSql();
+    DBG::log((array)$this, 'array', "AclQueryBuilder::fetchValue");
+    return DB::getPDO()->fetchValue($sql);
+  }
+
   public function generateSql() {
     if (!$this->from) throw new Exception("Missing FROM");
     $tableName = $this->_getTableName($this->from);
@@ -118,6 +192,7 @@ class AclQueryBuilder {
     if ($this->from instanceof Core_AclBase) $sqlPk = $this->from->getSqlPrimaryKeyField();
 
     $sqlJoin = [];
+    DBG::log($this, 'array', '$this');
     foreach ($this->isInstances as $k => $ns) {
       $idInstance = ACL::getInstanceId($ns);
       $instanceTable = ACL::getInstanceTable($ns);
@@ -172,8 +247,12 @@ class AclQueryBuilder {
     $sqlLimit = ($limit > 0) ? "limit {$limit} offset {$offset}" : '';
 
     $sqlSelect = [];
-    $sqlSelect[] = "{$this->_fromPrefix}.{$sqlPk}";
-    if (in_array('*', $this->select)) $sqlSelect[] = "{$this->_fromPrefix}.*";
+    if (!empty($this->select['__rawSelect__'])) {
+      $sqlSelect[] = $this->select['__rawSelect__'];
+    } else {
+      $sqlSelect[] = "{$this->_fromPrefix}.{$sqlPk}";
+      if (in_array('*', $this->select)) $sqlSelect[] = "{$this->_fromPrefix}.*";
+    }
     if (in_array('@instances', $this->select)) {
       if (!($this->from instanceof Core_AclBase)) throw new Exception("select @instances allowed only for Acl object");
       $instanceTable = ACL::getInstanceTable($this->from->getNamespace());
@@ -186,12 +265,14 @@ class AclQueryBuilder {
         ) as `@instances`
       ";
     }
+    $sqlOrderBy = $this->generateOrderBySql();
     $sqlSelect = (!empty($sqlSelect)) ? implode(", ", $sqlSelect) : "{$this->_fromPrefix}.*";
     return "
       select {$sqlSelect}
       from `{$tableName}` {$this->_fromPrefix}
         {$sqlJoin}
       {$sqlWhere}
+      {$sqlOrderBy}
       {$sqlLimit}
     ";
   }

+ 204 - 0
SE/se-lib/AclQueryFeatures.php

@@ -0,0 +1,204 @@
+<?php
+
+Lib::loadClass('ACL');
+
+// usage: (Acl class)::buildQuery($params): return new AclQueryFeatures($this, $params);
+//        (view): $queryFeatures = $acl->buildQuery($params);
+//        (view): $total = $queryFeatures->getTotal();
+//        (view): $items = $queryFeatures->getItems();
+// example: @see TableAcl, TableAjax
+class AclQueryFeatures {
+
+  public $_params;
+  public $_acl;
+  public $_query;
+  public $_total;
+  public $_legacyMode;
+
+  public function __construct($acl, $params) {
+    $this->_acl = $acl;
+    $this->_params = $params;
+    $this->_query = null;
+    $this->_total = null;
+    $this->_legacyMode = false;
+    // TODO: _legacyMode = ($from instanceof simple schema or another programmed objects)
+  }
+
+  public function parseQueryValue($searchQuery, $fieldType = 'xsd:string') {
+    if ('!NULL' === $searchQuery) return ['is not null', null];
+    if ('IS NOT NULL' === $searchQuery) return ['is not null', null];
+    if ('NULL' === $searchQuery) return ['is null', null];
+    if ('IS NULL' === $searchQuery) return ['is null', null];
+    switch ($fieldType) {
+      case 'gml:PolygonPropertyType':
+      case 'gml:PointPropertyType':
+      case 'gml:LineStringPropertyType':
+      case 'gml:GeometryPropertyType': return $this->_parseGeomQuery($searchQuery);
+      //   $sqlFilter = $this->_sqlValueForGeomField($fldName, $v, 't');
+      // if ('_CSV_NUM' == substr($fldName, -8)) { // if ($this->isCsvNumericField($fldName)) { // TODO: xsd type - p5:csv_num
+      //   $sqlFilter = $this->_sqlValueForCsvNumericField($fldName, $v, 't');
+      //   if ($sqlFilter) $sql_where_and[] = $sqlFilter;
+      //   continue;
+      // }
+    }
+
+    switch (substr($searchQuery, 0, 1)) {
+      case '=': return ['=', substr($searchQuery, 1)];
+      case '>':
+        switch (substr($searchQuery, 1, 1)) {
+          case '=': return ['>=', substr($searchQuery, 2)];
+          default:  return ['>', substr($searchQuery, 1)];
+        }
+      case '<':
+        switch (substr($searchQuery, 1, 1)) {
+          case '=': return ['<=', substr($searchQuery, 2)];
+          case '>': return ['!=', substr($searchQuery, 2)];
+          default:  return ['<', substr($searchQuery, 1)];
+        }
+      case '!':
+        switch (substr($searchQuery, 1, 1)) {
+          case '=': return ['!=', substr($searchQuery, 2)];
+          default:  return ['not like', substr($searchQuery, 1)];
+        }
+      default: {
+        if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery];
+        if ('xsd:number' === $fieldType) return ['=', $searchQuery];
+        // if ($acl->isColTypeNumber($fieldName)) return ['=', $searchQuery];
+        // else {
+        //   $queryWhereBuilder = new SqlQueryWhereBuilder();
+        //   $searchWords = $queryWhereBuilder->splitQueryToWords($v);
+        //   $sqlWhereWords = array();
+        //   if (!empty($searchWords)) {
+        //     foreach ($searchWords as $word) {
+        //       $sqlWord = $this->_db->_($word);
+        //       $sqlWhereWords[] = "t.`{$fldName}` like '%{$sqlWord}%'";
+        //     }
+        //   }
+        //   if (!empty($searchWords)) {
+        //     $sql_where_and[] = "(" . implode(" and ", $sqlWhereWords) . ")";
+        //   }
+        // }
+        return ['=', $searchQuery];
+      }
+    }
+  }
+  public function _parseGeomQuery($searchQuery) { // _sqlValueForGeomField($fldName, $fltrValue, $tblPrefix = 't')
+    // example: BBOX:54.40993961633866,18.583889010112824,54.337945760687454,18.397121431987586
+    DBG::log($searchQuery, 'string', "\$searchQuery");
+    if ('BBOX:' == substr($searchQuery, 0, 5)) {
+      $valParts = explode(',', substr($searchQuery, 5));
+      if (4 !== count($valParts)) throw new Exception("Wrong BBOX query");
+      $valParts = array_filter($valParts, 'is_numeric');
+      if (4 !== count($valParts)) throw new Exception("Wrong BBOX query - expected 4 numeric values");
+      $bounds = "POLYGON((
+        {$valParts[3]} {$valParts[2]},
+        {$valParts[3]} {$valParts[0]},
+        {$valParts[1]} {$valParts[0]},
+        {$valParts[1]} {$valParts[2]},
+        {$valParts[3]} {$valParts[2]}
+      ))";
+      // for mysql 5.6 use ST_Contains() @see http://dev.mysql.com/doc/refman/5.6/en/spatial-relation-functions.html
+      return [ 'Intersects', $bounds ];
+    }
+    else if ('GeometryType=' == substr($fltrValue, 0, 13)) {
+      return [ 'GeometryType', substr($fltrValue, 13) ];
+    }
+    throw new Exception("Not implemented geometry query string"); // TODO:? return null;
+  }
+
+  public function _sqlValueForCsvNumericField($fldName, $fltrValue, $tblPrefix = 't') {
+    $sqlFilter = false;
+    if (is_numeric($fltrValue)) {
+      $sqlFilter = "FIND_IN_SET('{$fltrValue}', `{$fldName}`)>0";
+    } else if (false !== strpos($fltrValue, ' ')) {
+      $sqlGlue = " or ";
+      $fltrValues = $fltrValue;
+      if ('&' == substr($fltrValues, 0, 1)) {
+        $fltrValues = substr($fltrValues, 1);
+        $sqlGlue = " and ";
+      }
+      $fltrValues = explode(' ', $fltrValues);
+      $sqlNumericValues = array();
+      foreach ($fltrValues as $fltrVal) {
+        if (is_numeric($fltrVal)) {
+          $sqlNumericValues[] = "FIND_IN_SET('{$fltrVal}', `{$fldName}`)>0";
+        }
+      }
+      if (!empty($sqlNumericValues)) {
+        $sqlFilter = "(" . implode($sqlGlue, $sqlNumericValues) . ")";
+      }
+    }
+    return $sqlFilter;
+  }
+
+  public function getQuery() {
+    if ($this->_query) return $this->_query;
+    // $ds = $this->_acl->getDataSource(); // TODO: only for TableAcl // TODO: move _parseSqlWhere to this class
+    $filtrIsInstance = [];
+    $filtrIsNotInstance = [];
+    $this->_query = ACL::query($this->_acl)
+      ->isInstance($filtrIsInstance)
+      ->isNotInstance($filtrIsNotInstance);
+    // ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPrimaryKey} and i.idInstance = {$idInstance}" ])
+    // $this->_query->where([
+    //   'rawWhere' => $ds->_parseSqlWhere($params)
+    // ]);
+    foreach ($this->_params as $k => $v) {
+      if ('f_' === substr($k, 0, 2) && strlen($k) > 3) {
+        $fieldName = substr($k, 2);
+        $fieldType = $this->_acl->getXsdFieldType($fieldName);
+        list($comparisonSign, $value) = $this->parseQueryValue($v, $fieldType);
+        DBG::log([ $fieldName, $fieldType, $comparisonSign, $value ], 'array', "parseQueryValue");
+        $this->_query->where($fieldName, $comparisonSign, $value);
+      } else if ('sf_' === substr($k, 0, 3) && strlen($k) > 4) {
+      } else if ('ogc:Filter' == $k) {
+      } else if ('primaryKey' == $k) {
+      }
+    }
+    return $this->_query;
+  }
+
+  public function getTotal() {
+    if ($this->_legacyMode) return $this->_acl->getTotal($this->_params);
+    if (null !== $this->_total) return $this->_total;
+    $this->_total = $this->getQuery()
+      ->select([ 'rawSelect' => "count(1) as cnt" ])
+      ->fetchValue();
+    return $this->_total;
+  }
+
+  public function hasParam($key) { return !empty($this->_params[$key]); }
+  public function getParam($key) { return V::get($key, '', $this->_params); }
+
+  public function getItems() {
+    if ($this->_legacyMode) return $this->_acl->getTotal($this->_params);
+    // 'limit' => 10,
+    //  'limitstart' => 0,
+    //  'order_by' => 'ID',
+    //  'order_dir' => 'desc',
+    // TODO: sortBy from wfs query
+    $sortBy = $this->hasParam('order_by')
+      ? ( $this->hasParam('order_dir')
+          ? $this->getParam('order_by') . " " . $this->getParam('order_dir')
+          : $this->getParam('order_by')
+        )
+      : '';
+    $limit = V::get('limit', 10, $this->_params, 'int');
+    $offset = V::get('limitstart', 0, $this->_params, 'int');
+    $ds = $this->_acl->getDataSource(); // TODO: only for TableAcl // TODO: move _parseSqlWhere to this class
+
+    DBG::log(['params' => $this->_params, 'sortBy' => $sortBy, 'limit' => $limit, 'offset' => $offset], 'array', '$this->_params');
+    return $this->getQuery()
+      ->select([
+        'rawSelect' =>   $ds->_getSqlCols()
+      ]) // TODO: fields
+      ->select(!empty($this->_params['@instances']) ? '@instances' : null)
+      ->limit($limit)
+      ->offset($offset)
+      ->orderBy($sortBy)
+      ->fetchAll();
+  }
+
+  public function getItem($primaryKey) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }
+
+}

+ 1 - 0
SE/se-lib/Core/AclBase.php

@@ -189,6 +189,7 @@ class Core_AclBase {
 
   public function getRawLabel($posLimit = 20) { return substr($this->getName(), 0, $posLimit); }
 
+  public function buildQuery($params = array()) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }// TODO: use ParseOgcQuery
   public function getItems($params = array()) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }// TODO: use ParseOgcQuery
   public function getTotal($params = array()) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }// TODO: use ParseOgcQuery
   public function getItem($primaryKey, $params = []) { throw new HttpException("Acl function " . __FUNCTION__ . " Not implemented", 501); }