<?php 
 
namespace PHPCR\Util\QOM; 
 
use InvalidArgumentException; 
use PHPCR\Query\QOM\ColumnInterface; 
use PHPCR\Query\QOM\ConstraintInterface; 
use PHPCR\Query\QOM\DynamicOperandInterface; 
use PHPCR\Query\QOM\JoinConditionInterface; 
use PHPCR\Query\QOM\OrderingInterface; 
use PHPCR\Query\QOM\QueryObjectModelConstantsInterface; 
use PHPCR\Query\QOM\QueryObjectModelFactoryInterface; 
use PHPCR\Query\QOM\QueryObjectModelInterface; 
use PHPCR\Query\QOM\SourceInterface; 
use PHPCR\Query\QueryInterface; 
use PHPCR\Query\QueryResultInterface; 
use RuntimeException; 
 
/** 
 * QueryBuilder class is responsible for dynamically create QOM queries. 
 * 
 * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004 
 * @license http://opensource.org/licenses/MIT MIT License 
 * @author      Nacho MartÃn <nitram.ohcan@gmail.com> 
 * @author      Guilherme Blanco <guilhermeblanco@hotmail.com> 
 * @author      Benjamin Eberlei <kontakt@beberlei.de> 
 */ 
class QueryBuilder 
{ 
    /** The builder states. */ 
    const STATE_DIRTY = 0; 
    const STATE_CLEAN = 1; 
 
    /** 
     * @var int The state of the query object. Can be dirty or clean. 
     */ 
    private $state = self::STATE_CLEAN; 
 
    /** 
     * @var QueryObjectModelFactoryInterface QOMFactory 
     */ 
    private $qomFactory; 
 
    /** 
     * @var int The maximum number of results to retrieve. 
     */ 
    private $firstResult = null; 
 
    /** 
     * @var int The maximum number of results to retrieve. 
     */ 
    private $maxResults = null; 
 
    /** 
     * @var array with the orderings that determine the order of the result 
     */ 
    private $orderings = []; 
 
    /** 
     * @var ConstraintInterface to apply to the query. 
     */ 
    private $constraint = null; 
 
    /** 
     * @var array with the columns to be selected. 
     */ 
    private $columns = []; 
 
    /** 
     * @var SourceInterface source of the query. 
     */ 
    private $source = null; 
 
    /** 
     * QOM tree. 
     * 
     * @var QueryObjectModelInterface 
     */ 
    private $query = null; 
 
    /** 
     * @var array The query parameters. 
     */ 
    private $params = []; 
 
    /** 
     * Initializes a new QueryBuilder. 
     * 
     * @param QueryObjectModelFactoryInterface $qomFactory 
     */ 
    public function __construct(QueryObjectModelFactoryInterface $qomFactory) 
    { 
        $this->qomFactory = $qomFactory; 
    } 
 
    /** 
     * Get a query builder instance from an existing query. 
     * 
     * @param string $statement the statement in the specified language 
     * @param string $language  the query language 
     * 
     * @throws InvalidArgumentException 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function setFromQuery($statement, $language) 
    { 
        if (QueryInterface::JCR_SQL2 === $language) { 
            $converter = new Sql2ToQomQueryConverter($this->qomFactory); 
            $statement = $converter->parse($statement); 
        } 
 
        if (!$statement instanceof QueryObjectModelInterface) { 
            throw new InvalidArgumentException("Language '$language' not supported"); 
        } 
 
        $this->state = self::STATE_DIRTY; 
        $this->source = $statement->getSource(); 
        $this->constraint = $statement->getConstraint(); 
        $this->orderings = $statement->getOrderings(); 
        $this->columns = $statement->getColumns(); 
 
        return $this; 
    } 
 
    /** 
     * Get the associated QOMFactory for this query builder. 
     * 
     * @return QueryObjectModelFactoryInterface 
     */ 
    public function getQOMFactory() 
    { 
        return $this->qomFactory; 
    } 
 
    /** 
     * Shortcut for getQOMFactory(). 
     */ 
    public function qomf() 
    { 
        return $this->getQOMFactory(); 
    } 
 
    /** 
     * sets the position of the first result to retrieve (the "offset"). 
     * 
     * @param int $firstResult The First result to return. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function setFirstResult($firstResult) 
    { 
        $this->firstResult = $firstResult; 
 
        return $this; 
    } 
 
    /** 
     * Gets the position of the first result the query object was set to retrieve (the "offset"). 
     * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. 
     * 
     * @return int The position of the first result. 
     */ 
    public function getFirstResult() 
    { 
        return $this->firstResult; 
    } 
 
    /** 
     * Sets the maximum number of results to retrieve (the "limit"). 
     * 
     * @param int $maxResults The maximum number of results to retrieve. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function setMaxResults($maxResults) 
    { 
        $this->maxResults = $maxResults; 
 
        return $this; 
    } 
 
    /** 
     * Gets the maximum number of results the query object was set to retrieve (the "limit"). 
     * Returns NULL if {@link setMaxResults} was not applied to this query builder. 
     * 
     * @return int Maximum number of results. 
     */ 
    public function getMaxResults() 
    { 
        return $this->maxResults; 
    } 
 
    /** 
     * Gets the array of orderings. 
     * 
     * @return OrderingInterface[] Orderings to apply. 
     */ 
    public function getOrderings() 
    { 
        return $this->orderings; 
    } 
 
    /** 
     * Adds an ordering to the query results. 
     * 
     * @param DynamicOperandInterface $sort  The ordering expression. 
     * @param string                  $order The ordering direction. 
     * 
     * @throws InvalidArgumentException 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function addOrderBy(DynamicOperandInterface $sort, $order = 'ASC') 
    { 
        $order = strtoupper($order); 
 
        if (!in_array($order, ['ASC', 'DESC'])) { 
            throw new InvalidArgumentException('Order must be one of "ASC" or "DESC"'); 
        } 
 
        $this->state = self::STATE_DIRTY; 
        if ($order === 'DESC') { 
            $ordering = $this->qomFactory->descending($sort); 
        } else { 
            $ordering = $this->qomFactory->ascending($sort); 
        } 
        $this->orderings[] = $ordering; 
 
        return $this; 
    } 
 
    /** 
     * Specifies an ordering for the query results. 
     * Replaces any previously specified orderings, if any. 
     * 
     * @param DynamicOperandInterface $sort  The ordering expression. 
     * @param string                  $order The ordering direction. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function orderBy(DynamicOperandInterface $sort, $order = 'ASC') 
    { 
        $this->orderings = []; 
        $this->addOrderBy($sort, $order); 
 
        return $this; 
    } 
 
    /** 
     * Specifies one restriction (may be simple or composed). 
     * Replaces any previously specified restrictions, if any. 
     * 
     * @param ConstraintInterface $constraint 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function where(ConstraintInterface $constraint) 
    { 
        $this->state = self::STATE_DIRTY; 
        $this->constraint = $constraint; 
 
        return $this; 
    } 
 
    /** 
     * Returns the constraint to apply. 
     * 
     * @return ConstraintInterface the constraint to be applied 
     */ 
    public function getConstraint() 
    { 
        return $this->constraint; 
    } 
 
    /** 
     * Creates a new constraint formed by applying a logical AND to the 
     * existing constraint and the new one. 
     * 
     * Order of ands is important: 
     * 
     * Given $this->constraint = $constraint1 
     * running andWhere($constraint2) 
     * resulting constraint will be $constraint1 AND $constraint2 
     * 
     * If there is no previous constraint then it will simply store the 
     * provided one 
     * 
     * @param ConstraintInterface $constraint 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function andWhere(ConstraintInterface $constraint) 
    { 
        $this->state = self::STATE_DIRTY; 
 
        if ($this->constraint) { 
            $this->constraint = $this->qomFactory->andConstraint($this->constraint, $constraint); 
        } else { 
            $this->constraint = $constraint; 
        } 
 
        return $this; 
    } 
 
    /** 
     * Creates a new constraint formed by applying a logical OR to the 
     * existing constraint and the new one. 
     * 
     * Order of ands is important: 
     * 
     * Given $this->constraint = $constraint1 
     * running orWhere($constraint2) 
     * resulting constraint will be $constraint1 OR $constraint2 
     * 
     * If there is no previous constraint then it will simply store the 
     * provided one 
     * 
     * @param ConstraintInterface $constraint 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function orWhere(ConstraintInterface $constraint) 
    { 
        $this->state = self::STATE_DIRTY; 
 
        if ($this->constraint) { 
            $this->constraint = $this->qomFactory->orConstraint($this->constraint, $constraint); 
        } else { 
            $this->constraint = $constraint; 
        } 
 
        return $this; 
    } 
 
    /** 
     * Returns the columns to be selected. 
     * 
     * @return ColumnInterface[] The columns to be selected 
     */ 
    public function getColumns() 
    { 
        return $this->columns; 
    } 
 
    /** 
     * Sets the columns to be selected. 
     * 
     * @param ColumnInterface[] $columns The columns to be selected 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function setColumns(array $columns) 
    { 
        $this->columns = $columns; 
 
        return $this; 
    } 
 
    /** 
     * Identifies a property in the specified or default selector to include in the tabular view of query results. 
     * Replaces any previously specified columns to be selected if any. 
     * 
     * @param string $selectorName 
     * @param string $propertyName 
     * @param string $columnName 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function select($selectorName, $propertyName, $columnName = null) 
    { 
        $this->state = self::STATE_DIRTY; 
        $this->columns = [$this->qomFactory->column($selectorName, $propertyName, $columnName)]; 
 
        return $this; 
    } 
 
    /** 
     * Adds a property in the specified or default selector to include in the tabular view of query results. 
     * 
     * @param string $selectorName 
     * @param string $propertyName 
     * @param string $columnName 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function addSelect($selectorName, $propertyName, $columnName = null) 
    { 
        $this->state = self::STATE_DIRTY; 
 
        $this->columns[] = $this->qomFactory->column($selectorName, $propertyName, $columnName); 
 
        return $this; 
    } 
 
    /** 
     * Sets the default Selector or the node-tuple Source. Can be a selector 
     * or a join. 
     * 
     * @param SourceInterface $source 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function from(SourceInterface $source) 
    { 
        $this->state = self::STATE_DIRTY; 
        $this->source = $source; 
 
        return $this; 
    } 
 
    /** 
     * Gets the default Selector. 
     * 
     * @return SourceInterface The default selector. 
     */ 
    public function getSource() 
    { 
        return $this->source; 
    } 
 
    /** 
     * Performs an inner join between the stored source and the supplied source. 
     * 
     * @param SourceInterface        $rightSource 
     * @param JoinConditionInterface $joinCondition 
     * 
     * @throws RuntimeException if there is not an existing source. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function join(SourceInterface $rightSource, JoinConditionInterface $joinCondition) 
    { 
        return $this->innerJoin($rightSource, $joinCondition); 
    } 
 
    /** 
     * Performs an inner join between the stored source and the supplied source. 
     * 
     * @param SourceInterface        $rightSource 
     * @param JoinConditionInterface $joinCondition 
     * 
     * @throws RuntimeException if there is not an existing source. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function innerJoin(SourceInterface $rightSource, JoinConditionInterface $joinCondition) 
    { 
        return $this->joinWithType($rightSource, QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_INNER, $joinCondition); 
    } 
 
    /** 
     * Performs an left outer join between the stored source and the supplied source. 
     * 
     * @param SourceInterface        $rightSource 
     * @param JoinConditionInterface $joinCondition 
     * 
     * @throws RuntimeException if there is not an existing source. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function leftJoin(SourceInterface $rightSource, JoinConditionInterface $joinCondition) 
    { 
        return $this->joinWithType($rightSource, QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_LEFT_OUTER, $joinCondition); 
    } 
 
    /** 
     * Performs a right outer join between the stored source and the supplied source. 
     * 
     * @param SourceInterface        $rightSource 
     * @param JoinConditionInterface $joinCondition 
     * 
     * @throws RuntimeException if there is not an existing source. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function rightJoin(SourceInterface $rightSource, JoinConditionInterface $joinCondition) 
    { 
        return $this->joinWithType($rightSource, QueryObjectModelConstantsInterface::JCR_JOIN_TYPE_RIGHT_OUTER, $joinCondition); 
    } 
 
    /** 
     * Performs an join between the stored source and the supplied source. 
     * 
     * @param SourceInterface        $rightSource 
     * @param string                 $joinType      as specified in PHPCR\Query\QOM\QueryObjectModelConstantsInterface 
     * @param JoinConditionInterface $joinCondition 
     * 
     * @throws RuntimeException if there is not an existing source. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function joinWithType(SourceInterface $rightSource, $joinType, JoinConditionInterface $joinCondition) 
    { 
        if (!$this->source) { 
            throw new RuntimeException('Cannot perform a join without a previous call to from'); 
        } 
 
        $this->state = self::STATE_DIRTY; 
        $this->source = $this->qomFactory->join($this->source, $rightSource, $joinType, $joinCondition); 
 
        return $this; 
    } 
 
    /** 
     * Gets the query built. 
     * 
     * @return QueryObjectModelInterface 
     */ 
    public function getQuery() 
    { 
        if ($this->query !== null && $this->state === self::STATE_CLEAN) { 
            return $this->query; 
        } 
 
        $this->state = self::STATE_CLEAN; 
        $this->query = $this->qomFactory->createQuery($this->source, $this->constraint, $this->orderings, $this->columns); 
 
        if ($this->firstResult) { 
            $this->query->setOffset($this->firstResult); 
        } 
 
        if ($this->maxResults) { 
            $this->query->setLimit($this->maxResults); 
        } 
 
        return $this->query; 
    } 
 
    /** 
     * Executes the query setting firstResult and maxResults. 
     * 
     * @return QueryResultInterface 
     */ 
    public function execute() 
    { 
        if ($this->query === null || $this->state === self::STATE_DIRTY) { 
            $this->query = $this->getQuery(); 
        } 
 
        foreach ($this->params as $key => $value) { 
            $this->query->bindValue($key, $value); 
        } 
 
        return $this->query->execute(); 
    } 
 
    /** 
     * Sets a query parameter for the query being constructed. 
     * 
     * @param string $key   The parameter name. 
     * @param mixed  $value The parameter value. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function setParameter($key, $value) 
    { 
        $this->params[$key] = $value; 
 
        return $this; 
    } 
 
    /** 
     * Gets a (previously set) query parameter of the query being constructed. 
     * 
     * @param string $key The key (name) of the bound parameter. 
     * 
     * @return mixed The value of the bound parameter. 
     */ 
    public function getParameter($key) 
    { 
        return isset($this->params[$key]) ? $this->params[$key] : null; 
    } 
 
    /** 
     * Sets a collection of query parameters for the query being constructed. 
     * 
     * @param array $params The query parameters to set. 
     * 
     * @return QueryBuilder This QueryBuilder instance. 
     */ 
    public function setParameters(array $params) 
    { 
        $this->params = $params; 
 
        return $this; 
    } 
 
    /** 
     * Gets all defined query parameters for the query being constructed. 
     * 
     * @return array The currently defined query parameters. 
     */ 
    public function getParameters() 
    { 
        return $this->params; 
    } 
}