/*
 * Copyright (c) 2006, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation
//
package org.eclipse.persistence.jpa.jpql.tools.model;

import static org.eclipse.persistence.jpa.jpql.parser.Expression.AND;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.AS;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.BETWEEN;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.CASE;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.COALESCE;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.CONCAT;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.DELETE;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.DISTINCT;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.DIVISION;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.ELSE;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.EMPTY;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.END;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.EQUAL;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.ESCAPE;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.EXISTS;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.FROM;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.FUNC;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.GROUP_BY;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.HAVING;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.IN;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.IS;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.LIKE;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.MEMBER;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.MINUS;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.MULTIPLICATION;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.NEW;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.NOT;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.NULL;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.OF;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.OR;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.ORDER_BY;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.PLUS;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.SELECT;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.SET;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.THEN;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.TREAT;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.UPDATE;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.WHEN;
import static org.eclipse.persistence.jpa.jpql.parser.Expression.WHERE;

import java.util.ListIterator;

import org.eclipse.persistence.jpa.jpql.ExpressionTools;
import org.eclipse.persistence.jpa.jpql.parser.AbstractDoubleEncapsulatedExpression;
import org.eclipse.persistence.jpa.jpql.parser.AbstractFromClause;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.AbstractSingleEncapsulatedExpression;
import org.eclipse.persistence.jpa.jpql.parser.AbstractTripleEncapsulatedExpression;
import org.eclipse.persistence.jpa.jpql.parser.AggregateFunction;
import org.eclipse.persistence.jpa.jpql.parser.BetweenExpression;
import org.eclipse.persistence.jpa.jpql.parser.CaseExpression;
import org.eclipse.persistence.jpa.jpql.parser.CoalesceExpression;
import org.eclipse.persistence.jpa.jpql.parser.CollectionMemberDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.CollectionMemberExpression;
import org.eclipse.persistence.jpa.jpql.parser.CompoundExpression;
import org.eclipse.persistence.jpa.jpql.parser.ConcatExpression;
import org.eclipse.persistence.jpa.jpql.parser.ConstructorExpression;
import org.eclipse.persistence.jpa.jpql.parser.DateTime;
import org.eclipse.persistence.jpa.jpql.parser.DeleteClause;
import org.eclipse.persistence.jpa.jpql.parser.EmptyCollectionComparisonExpression;
import org.eclipse.persistence.jpa.jpql.parser.EncapsulatedIdentificationVariableExpression;
import org.eclipse.persistence.jpa.jpql.parser.ExistsExpression;
import org.eclipse.persistence.jpa.jpql.parser.Expression;
import org.eclipse.persistence.jpa.jpql.parser.FunctionExpression;
import org.eclipse.persistence.jpa.jpql.parser.GroupByClause;
import org.eclipse.persistence.jpa.jpql.parser.HavingClause;
import org.eclipse.persistence.jpa.jpql.parser.IdentificationVariableDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.InExpression;
import org.eclipse.persistence.jpa.jpql.parser.Join;
import org.eclipse.persistence.jpa.jpql.parser.KeywordExpression;
import org.eclipse.persistence.jpa.jpql.parser.LikeExpression;
import org.eclipse.persistence.jpa.jpql.parser.NotExpression;
import org.eclipse.persistence.jpa.jpql.parser.NullComparisonExpression;
import org.eclipse.persistence.jpa.jpql.parser.OrderByClause;
import org.eclipse.persistence.jpa.jpql.parser.OrderByItem;
import org.eclipse.persistence.jpa.jpql.parser.RangeVariableDeclaration;
import org.eclipse.persistence.jpa.jpql.parser.ResultVariable;
import org.eclipse.persistence.jpa.jpql.parser.SelectClause;
import org.eclipse.persistence.jpa.jpql.parser.SelectStatement;
import org.eclipse.persistence.jpa.jpql.parser.SimpleSelectClause;
import org.eclipse.persistence.jpa.jpql.parser.TreatExpression;
import org.eclipse.persistence.jpa.jpql.parser.TrimExpression;
import org.eclipse.persistence.jpa.jpql.parser.UpdateClause;
import org.eclipse.persistence.jpa.jpql.parser.UpdateItem;
import org.eclipse.persistence.jpa.jpql.parser.WhenClause;
import org.eclipse.persistence.jpa.jpql.parser.WhereClause;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbsExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractDoubleEncapsulatedExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractFromClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractIdentificationVariableDeclarationStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractModifyStatementStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractPathExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractRangeVariableDeclarationStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractSchemaNameStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractSelectStatementStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractSingleEncapsulatedExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AbstractTripleEncapsulatedExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AdditionExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AggregateFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AllOrAnyExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AndExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ArithmeticFactorStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.AvgFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.BadExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.BetweenExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CaseExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CoalesceExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CollectionMemberDeclarationStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CollectionMemberExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CollectionValuedPathExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ComparisonExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CompoundExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ConcatExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ConstructorExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.CountFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.DateTimeStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.DeleteClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.DeleteStatementStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.DerivedPathIdentificationVariableDeclarationStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.DerivedPathVariableDeclarationStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.DivisionExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.EmptyCollectionComparisonExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.EncapsulatedIdentificationVariableExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.EntityTypeLiteralStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.EntryExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.EnumTypeStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ExistsExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.FromClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.FunctionExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.GroupByClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.HavingClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.IdentificationVariableDeclarationStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.IdentificationVariableStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.InExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.IndexExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.InputParameterStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.JPQLQueryStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.JoinStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.KeyExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.KeywordExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.LengthExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.LikeExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ListHolderStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.LocateExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.LowerExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.MaxFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.MinFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ModExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.MultiplicationExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.NotExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.NullComparisonExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.NullIfExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.NumericLiteralStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ObjectExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.OrExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.OrderByClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.OrderByItemStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.RangeVariableDeclarationStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ResultVariableStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SelectClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SelectStatementStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SimpleFromClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SimpleSelectClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SimpleSelectStatementStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SimpleStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SizeExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SqrtExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.StateFieldPathExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.StateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.StringLiteralStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SubExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SubstringExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SubtractionExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.SumFunctionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.TreatExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.TrimExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.TypeExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.UnknownExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.UpdateClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.UpdateItemStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.UpdateStatementStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.UpperExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.ValueExpressionStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.WhenClauseStateObject;
import org.eclipse.persistence.jpa.jpql.tools.model.query.WhereClauseStateObject;

/**
 * This {@link IJPQLQueryFormatter} is used to generate a string representation of a {@link
 * StateObject} based on how it was parsed, which means this formatter can only be used when the
 * {@link StateObject} was created by parsing a JPQL query because it needs to retrieve parsing
 * information from the corresponding {@link Expression}.
 * <p>
 * It is possible to partially match the JPQL query that was parsed, the value of <em>exactMatch</em>
 * will determine whether the string representation of any given {@link StateObject} should reflect
 * the exact string that was parsed. <code>true</code> will use every bit of information contained
 * in the corresponding {@link Expression} to perfectly match what was parsed (case of JPQL
 * identifiers and the presence of whitespace); <code>false</code> will only match the case
 * sensitivity of the JPQL identifiers.
 *
 * @version 2.5
 * @since 2.4
 * @author Pascal Filion
 */
@SuppressWarnings("null")
public abstract class AbstractActualJPQLQueryFormatter extends BaseJPQLQueryFormatter {

    /**
     * Determines whether the string representation of any given {@link StateObject} should reflect
     * the exact string that was parsed: <code>true</code> will use every bit of information
     * contained in the corresponding {@link Expression} to perfectly match what was parsed;
     * <code>false</code> will only match the case sensitivity of the JPQL identifiers.
     */
    protected final boolean exactMatch;

    /**
     * Creates a new <code>AbstractActualJPQLQueryFormatter</code>.
     *
     * @param exactMatch Determines whether the string representation of any given {@link StateObject}
     * should reflect the exact string that was parsed: <code>true</code> will use every bit of
     * information contained in the corresponding {@link Expression} to perfectly match what was
     * parsed (case of JPQL identifiers and the presence of whitespace); <code>false</code> will only
     * match the case sensitivity of the JPQL identifiers
     */
    protected AbstractActualJPQLQueryFormatter(boolean exactMatch) {
        super(IdentifierStyle.UPPERCASE);
        this.exactMatch = exactMatch;
    }

    /**
     * Creates a new <code>AbstractActualJPQLQueryFormatter</code>.
     *
     * @param exactMatch Determines whether the string representation of any given {@link StateObject}
     * should reflect the exact string that was parsed: <code>true</code> will use every bit of
     * information contained in the corresponding {@link Expression} to perfectly match what was
     * parsed (case of JPQL identifiers and the presence of whitespace); <code>false</code> will only
     * match the case sensitivity of the JPQL identifiers
     * @param style Determines how the JPQL identifiers are written out, which is used if the
     * {@link StateObject} was modified after its creation
     * @exception NullPointerException The IdentifierStyle cannot be <code>null</code>
     */
    protected AbstractActualJPQLQueryFormatter(boolean exactMatch, IdentifierStyle style) {
        super(style);
        this.exactMatch = exactMatch;
    }

    /**
     * Appends the given actual identifier if it's not an empty string, otherwise the second
     * identifier will be appended.
     *
     * @param actualIdentifier The actual JPQL identifier to append to the writer if it's not an
     * empty string
     * @param identifier The uppercase constant of the JPQL identifier to append if the actual one is
     * an empty string
     */
    protected void appendIdentifier(String actualIdentifier, String identifier) {

        if (!exactMatch || ExpressionTools.stringIsEmpty(actualIdentifier)) {
            actualIdentifier = formatIdentifier(identifier);
        }

        writer.append(actualIdentifier);
    }

    /**
     * Determines whether the string representation of any given {@link StateObject} should reflect
     * the exact string that was parsed.
     *
     * @return <code>true</code> will use every bit of information contained in the corresponding
     * {@link Expression} to perfectly match what was parsed; <code>false</code> will only match the
     * case sensitivity of the JPQL identifiers
     */
    public boolean isUsingExactMatch() {
        return exactMatch;
    }

    protected boolean shouldOutput(Expression expression) {
        return !exactMatch || (expression == null);
    }

    protected void toStringAggregateFunction(AggregateFunctionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            AggregateFunction expression = stateObject.getExpression();

            // Identifier
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : stateObject.getIdentifier(), stateObject.getIdentifier());

            // '('
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(formatIdentifier(LEFT_PARENTHESIS));
            }

            // 'DISTINCT'
            if (stateObject.hasDistinct()) {
                appendIdentifier((expression != null) ? expression.getActualDistinctIdentifier() : DISTINCT, DISTINCT);

                if (shouldOutput(expression) || expression.hasSpaceAfterDistinct()) {
                    writer.append(SPACE);
                }
            }

            // Encapsulated expression
            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
            }

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(formatIdentifier(RIGHT_PARENTHESIS));
            }
        }
    }

    protected void toStringChildren(ListHolderStateObject<? extends StateObject> stateObject,
                                    boolean comma) {

        for (ListIterator<? extends StateObject> iter = stateObject.items().iterator(); iter.hasNext(); ) {
            iter.next().accept(this);
            if (iter.hasNext()) {
                writer.append(comma ? COMMA_SPACE : SPACE);
            }
        }
    }

    protected void toStringCompound(CompoundExpressionStateObject stateObject, String identifier) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            CompoundExpression expression = stateObject.getExpression();

            // Left expression
            if (stateObject.hasLeft()) {
                stateObject.getLeft().accept(this);
                writer.append(SPACE);
            }

            // Identifier
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : identifier, identifier);

            if (shouldOutput(expression) || expression.hasSpaceAfterIdentifier()) {
                writer.append(SPACE);
            }

            // Right expression
            if (stateObject.hasRight()) {
                stateObject.getRight().accept(this);
            }
        }
    }

    protected void toStringDoubleEncapsulated(AbstractDoubleEncapsulatedExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            AbstractDoubleEncapsulatedExpression expression = stateObject.getExpression();

            // Identifier
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : stateObject.getIdentifier(), stateObject.getIdentifier());

            // '('
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(LEFT_PARENTHESIS);
            }
            else if (expression.hasSpaceAfterIdentifier()) {
                writer.append(COMMA);
            }

            // First expression
            if (stateObject.hasFirst()) {
                stateObject.getFirst().accept(this);
            }

            // ','
            if (shouldOutput(expression) || expression.hasComma()) {
                writer.append(COMMA);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterComma()) {
                writer.append(SPACE);
            }

            // Second expression
            if (stateObject.hasSecond()) {
                stateObject.getSecond().accept(this);
            }

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    protected void toStringEncapsulatedIdentificationVariable(EncapsulatedIdentificationVariableExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            EncapsulatedIdentificationVariableExpression expression = stateObject.getExpression();

            // Identifier
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : stateObject.getIdentifier(), stateObject.getIdentifier());

            // '('
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(LEFT_PARENTHESIS);
            }

            // Identification variable
            if (stateObject.hasIdentificationVariable()) {
                writer.append(stateObject.getIdentificationVariable());
            }

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    protected void toStringFromClause(AbstractFromClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            AbstractFromClause expression = stateObject.getExpression();

            // 'FROM'
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : FROM, FROM);

            if (shouldOutput(expression) || expression.hasSpaceAfterFrom()) {
                writer.append(SPACE);
            }

            // declaration
            if (stateObject.hasItems()) {
                toStringChildren(stateObject, true);
            }
        }
    }

    protected void toStringIdentificationVariableDeclaration(AbstractIdentificationVariableDeclarationStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            IdentificationVariableDeclaration expression = stateObject.getExpression();

            // Range variable declaration
            stateObject.getRangeVariableDeclaration().accept(this);

            // Join | Join Fetch
            if (stateObject.hasItems()) {

                if (shouldOutput(expression) || expression.hasSpace()) {
                    writer.append(SPACE);
                }

                toStringChildren(stateObject, false);
            }
        }
    }

    protected void toStringModifyStatement(AbstractModifyStatementStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            stateObject.getModifyClause().accept(this);

            if (stateObject.hasWhereClause()) {
                // TODO: HANDLE SPACE
                writer.append(SPACE);
                stateObject.getWhereClause().accept(this);
            }
        }
    }

    protected void toStringPathExpression(AbstractPathExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            stateObject.toText(writer);
        }
    }

    protected void toStringRangeVariableDeclaration(AbstractRangeVariableDeclarationStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            RangeVariableDeclaration expression = stateObject.getExpression();

            // "Root" object (abstract schema name or derived declaration)
            stateObject.getRootStateObject().accept(this);

            if (exactMatch && (expression != null) && expression.hasSpaceAfterRootObject()) {
                writer.append(SPACE);
            }

            // 'AS'
            if (stateObject.hasAs()) {

                if (!exactMatch || (expression == null)) {
                    writer.append(SPACE);
                }

                appendIdentifier((expression != null) ? expression.getActualAsIdentifier() : AS, AS);
            }

            if (exactMatch && (expression != null) && expression.hasSpaceAfterAs()) {
                writer.append(SPACE);
            }

            // Identification variable
            if (stateObject.hasIdentificationVariable() &&
               !stateObject.isIdentificationVariableVirtual()) {

                if (!exactMatch || (expression == null)) {
                    writer.append(SPACE);
                }

                writer.append(stateObject.getIdentificationVariable());
            }
        }
    }

    protected boolean toStringSelectStatement(AbstractSelectStatementStateObject stateObject) {

        AbstractSelectStatement expression = stateObject.getExpression();
        boolean spaceAdded = false;

        // SELECT clause
        stateObject.getSelectClause().accept(this);

        // If no select items were parsed but they got added later, make sure a space is added
        if (shouldOutput(expression) ||
            expression.hasSpaceAfterSelect() ||
            // TODO
            (/*!expression.getSelectClause().hasSelectExpression() &&*/
              stateObject.getSelectClause().hasSelectItem())) {

            writer.append(SPACE);
        }

        // FROM clause
        stateObject.getFromClause().accept(this);

        // If no WHERE clause was parsed but was added later, make sure a space is added
        if (exactMatch && (expression != null) && expression.hasSpaceAfterFrom() ||
            stateObject.hasWhereClause()) {

            writer.append(SPACE);
            spaceAdded = true;
        }

        // WHERE clause
        if (stateObject.hasWhereClause()) {
            stateObject.getWhereClause().accept(this);
            spaceAdded = false;
        }

        // If no GROUP BY clause was parsed but was added later, make sure a space is added
        if (exactMatch && (expression != null) && expression.hasSpaceAfterWhere() ||
            stateObject.hasGroupByClause()) {

            if (!spaceAdded) {
                writer.append(SPACE);
                spaceAdded = true;
            }
        }

        // GROUP BY clause
        if (stateObject.hasGroupByClause()) {
            stateObject.getGroupByClause().accept(this);
            spaceAdded = false;
        }

        // If no HAVING clause was parsed but was added later, make sure a space is added
        if (exactMatch && (expression != null) && expression.hasSpaceAfterGroupBy() ||
            stateObject.hasHavingClause()) {

            if (!spaceAdded) {
                writer.append(SPACE);
                spaceAdded = true;
            }
        }

        // HAVING clause
        if (stateObject.hasHavingClause()) {
            stateObject.getHavingClause().accept(this);
            spaceAdded = true;
        }

        return spaceAdded;
    }

    protected void toStringSimpleStateObject(SimpleStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else if (stateObject.hasText()) {
            writer.append(stateObject.getText());
        }
    }

    protected void toStringSingleEncapsulated(AbstractSingleEncapsulatedExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            AbstractSingleEncapsulatedExpression expression = stateObject.getExpression();

            // Identifier
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : stateObject.getIdentifier(), stateObject.getIdentifier());

            // '('
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(LEFT_PARENTHESIS);
            }

            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
            }

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    protected void toStringTripleEncapsulated(AbstractTripleEncapsulatedExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            AbstractTripleEncapsulatedExpression expression = stateObject.getExpression();

            // Identifier
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : stateObject.getIdentifier(), stateObject.getIdentifier());

            // '('
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(LEFT_PARENTHESIS);
            }

            // First expression
            if (stateObject.hasFirst()) {
                stateObject.getFirst().accept(this);
            }

            if (shouldOutput(expression) || expression.hasFirstComma()) {
                writer.append(COMMA);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterFirstComma()) {
                writer.append(SPACE);
            }

            // Second expression
            if (stateObject.hasSecond()) {
                stateObject.getSecond().accept(this);
            }

            // Third expression
            if (stateObject.hasThird()) {

                if (shouldOutput(expression) || expression.hasSecondComma()) {
                    writer.append(COMMA);
                }

                if (shouldOutput(expression) || expression.hasSpaceAfterSecondComma()) {
                    writer.append(SPACE);
                }

                stateObject.getThird().accept(this);
            }

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AbsExpressionStateObject stateObject) {
        toStringSingleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AbstractSchemaNameStateObject stateObject) {
        toStringSimpleStateObject(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AdditionExpressionStateObject stateObject) {
        toStringCompound(stateObject, PLUS);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AllOrAnyExpressionStateObject stateObject) {
        toStringSingleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AndExpressionStateObject stateObject) {
        toStringCompound(stateObject, AND);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ArithmeticFactorStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            writer.append(stateObject.getArithmeticSign());

            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(AvgFunctionStateObject stateObject) {
        toStringAggregateFunction(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(BadExpressionStateObject stateObject) {
        toStringSimpleStateObject(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(BetweenExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            BetweenExpression expression = stateObject.getExpression();

            // Expression
            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
                writer.append(SPACE);
            }

            // 'NOT
            if (stateObject.hasNot()) {
                appendIdentifier((expression != null) ? expression.getActualNotIdentifier() : NOT, NOT);
                writer.append(SPACE);
            }

            // 'BETWEEN'
            appendIdentifier((expression != null) ? expression.getActualBetweenIdentifier() : BETWEEN, BETWEEN);

            if (shouldOutput(expression) || expression.hasSpaceAfterBetween()) {
                writer.append(SPACE);
            }

            // Lower bound expression
            if (stateObject.hasLowerBound()) {
                stateObject.getLowerBound().accept(this);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterLowerBound()) {
                writer.append(SPACE);
            }

            // 'AND'
            if (shouldOutput(expression) || expression.hasAnd()) {
                appendIdentifier((expression != null) ? expression.getActualAndIdentifier() : AND, AND);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterAnd()) {
                writer.append(SPACE);
            }

            // Upper bound expression
            if (stateObject.hasUpperBound()) {
                stateObject.getUpperBound().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CaseExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            CaseExpression expression = stateObject.getExpression();

            // 'CASE'
            appendIdentifier((expression != null) ? expression.getActualCaseIdentifier() : CASE, CASE);

            if (shouldOutput(expression) || expression.hasSpaceAfterCase()) {
                writer.append(SPACE);
            }

            // Case operand
            if (stateObject.hasCaseOperand()) {
                stateObject.getCaseOperand().accept(this);

                if (shouldOutput(expression) || expression.hasSpaceAfterCaseOperand()) {
                    writer.append(SPACE);
                }
            }

            // WHEN clauses
            if (stateObject.hasItems()) {
                toStringChildren(stateObject, false);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterWhenClauses()) {
                writer.append(SPACE);
            }

            // 'ELSE'
            if (shouldOutput(expression) || expression.hasElse()) {
                appendIdentifier((expression != null) ? expression.getActualElseIdentifier() : ELSE, ELSE);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterElse()) {
                writer.append(SPACE);
            }

            // Else expression
            if (stateObject.hasElse()) {
                stateObject.getElse().accept(this);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterElseExpression()) {
                writer.append(SPACE);
            }

            // END
            if (shouldOutput(expression) || expression.hasEnd()) {
                appendIdentifier((expression != null) ? expression.getActualEndIdentifier() : END, END);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CoalesceExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            CoalesceExpression expression = stateObject.getExpression();

            // 'COALESCE'
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : COALESCE, COALESCE);

            // '('
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(LEFT_PARENTHESIS);
            }
            else if (expression.hasSpaceAfterIdentifier()) {
                writer.append(SPACE);
            }

            toStringChildren(stateObject, true);

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CollectionMemberDeclarationStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            CollectionMemberDeclaration expression = stateObject.getExpression();

            // 'IN'
            appendIdentifier((expression != null) ? expression.getActualInIdentifier() : IN, IN);

            // '('
            if (!stateObject.isDerived() && (shouldOutput(expression) || expression.hasLeftParenthesis())) {
                writer.append(LEFT_PARENTHESIS);
            }
            else if (stateObject.isDerived() && (shouldOutput(expression) || expression.hasSpaceAfterIn())) {
                writer.append(SPACE);
            }

            // Collection-valued path expression
            stateObject.getCollectionValuedPath().accept(this);

            // ')'
            if (!stateObject.isDerived() && (shouldOutput(expression) || expression.hasRightParenthesis())) {
                writer.append(RIGHT_PARENTHESIS);

                if (shouldOutput(expression) || expression.hasSpaceAfterRightParenthesis()) {
                    writer.append(SPACE);
                }
            }
            else if (stateObject.isDerived() &&
                     stateObject.hasAs() &&
                     (shouldOutput(expression) || expression.hasSpaceAfterRightParenthesis())) {

                writer.append(SPACE);
            }

            // 'AS'
            if (stateObject.hasAs()) {
                appendIdentifier((expression != null) ? expression.getActualAsIdentifier() : AS, AS);

                if (shouldOutput(expression) || expression.hasSpaceAfterAs()) {
                    writer.append(SPACE);
                }
            }

            // Identification variable
            if (stateObject.hasIdentificationVariable()) {
                stateObject.getIdentificationVariable().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CollectionMemberExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            CollectionMemberExpression expression = stateObject.getExpression();

            // Entity or value expression
            if (stateObject.hasEntityStateObject()) {
                stateObject.getEntityStateObject().accept(this);
                writer.append(SPACE);
            }

            // 'NOT'
            if (stateObject.hasNot()) {
                appendIdentifier((expression != null) ? expression.getActualNotIdentifier() : NOT, NOT);
                writer.append(SPACE);
            }

            // 'MEMBER'
            appendIdentifier((expression != null) ? expression.getActualMemberIdentifier() : MEMBER, MEMBER);

            if (shouldOutput(expression) || expression.hasSpaceAfterMember()) {
                writer.append(SPACE);
            }

            // 'OF'
            if (stateObject.hasOf()) {
                appendIdentifier((expression != null) ? expression.getActualOfIdentifier() : OF, OF);

                if (shouldOutput(expression) || expression.hasSpaceAfterOf()) {
                    writer.append(SPACE);
                }
            }

            // Collection-valued path expression
            stateObject.getCollectionValuedPath().accept(this);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CollectionValuedPathExpressionStateObject stateObject) {
        toStringPathExpression(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ComparisonExpressionStateObject stateObject) {
        toStringCompound(stateObject, stateObject.getIdentifier());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ConcatExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            ConcatExpression expression = stateObject.getExpression();

            // 'CONCAT'
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : CONCAT, CONCAT);

            // '('
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(LEFT_PARENTHESIS);
            }

            toStringChildren(stateObject, true);

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ConstructorExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            ConstructorExpression expression = stateObject.getExpression();

            // 'NEW'
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : NEW, NEW);

            if (shouldOutput(expression) || expression.hasSpaceAfterNew()) {
                writer.append(SPACE);
            }

            // Class name
            writer.append(stateObject.getClassName());

            // '('
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(LEFT_PARENTHESIS);
            }

            toStringChildren(stateObject, true);

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(CountFunctionStateObject stateObject) {
        toStringAggregateFunction(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DateTimeStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            DateTime expression = stateObject.getExpression();
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : stateObject.getText(), stateObject.getText());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DeleteClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            DeleteClause expression = stateObject.getExpression();

            // 'DELETE'
            appendIdentifier((expression != null) ? expression.getActualDeleteIdentifier() : DELETE, DELETE);

            if (shouldOutput(expression) || expression.hasSpaceAfterDelete()) {
                writer.append(SPACE);
            }

            // 'FROM'
            if (shouldOutput(expression) || expression.hasFrom()) {
                appendIdentifier((expression != null) ? expression.getActualFromIdentifier() : FROM, FROM);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterFrom()) {
                writer.append(SPACE);
            }

            // Range variable declaration
            stateObject.getRangeVariableDeclaration().accept(this);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DeleteStatementStateObject stateObject) {
        toStringModifyStatement(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DerivedPathIdentificationVariableDeclarationStateObject stateObject) {
        toStringIdentificationVariableDeclaration(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DerivedPathVariableDeclarationStateObject stateObject) {
        toStringRangeVariableDeclaration(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(DivisionExpressionStateObject stateObject) {
        toStringCompound(stateObject, DIVISION);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(EmptyCollectionComparisonExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            EmptyCollectionComparisonExpression expression = stateObject.getExpression();

            stateObject.getStateObject().accept(this);
            writer.append(SPACE);

            // 'IS'
            appendIdentifier((expression != null) ? expression.getActualIsIdentifier() : IS, IS);
            writer.append(SPACE);

            // 'NOT'
            if (stateObject.hasNot()) {
                appendIdentifier((expression != null) ? expression.getActualNotIdentifier() : NOT, NOT);
                writer.append(SPACE);
            }

            // 'EMPTY'
            appendIdentifier((expression != null) ? expression.getActualEmptyIdentifier() : EMPTY, EMPTY);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(EntityTypeLiteralStateObject stateObject) {
        toStringSimpleStateObject(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(EntryExpressionStateObject stateObject) {
        toStringEncapsulatedIdentificationVariable(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(EnumTypeStateObject stateObject) {
        toStringSimpleStateObject(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ExistsExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            ExistsExpression expression = stateObject.getExpression();

            // 'NOT'
            if (stateObject.hasNot()) {
                appendIdentifier((expression != null) ? expression.getActualNotIdentifier() : NOT, NOT);
                writer.append(SPACE);
            }

            // 'EXISTS'
            String actualIdentifier = (expression != null) ? expression.getActualIdentifier() : null;

            if ((actualIdentifier != null) && actualIdentifier.startsWith(NOT)) {
                actualIdentifier = actualIdentifier.substring(4);
            }

            appendIdentifier(actualIdentifier, EXISTS);

            // '('
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(formatIdentifier(LEFT_PARENTHESIS));
            }

            // Subquery
            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
            }

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(formatIdentifier(RIGHT_PARENTHESIS));
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(FromClauseStateObject stateObject) {
        toStringFromClause(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(FunctionExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            FunctionExpression expression = stateObject.getExpression();

            // FUNC
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : FUNC, FUNC);

            // (
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(LEFT_PARENTHESIS);
            }
            else if (exactMatch && expression.hasSpaceAfterIdentifier()) {
                writer.append(SPACE);
            }

            // Function name
            if (stateObject.hasFunctionName()) {

                writer.append(stateObject.getQuotedFunctionName());

                if (shouldOutput(expression) || expression.hasComma()) {
                    writer.append(COMMA);
                }

                if (shouldOutput(expression) || expression.hasSpaceAfterComma()) {
                    writer.append(SPACE);
                }
            }

            // Arguments
            toStringChildren(stateObject, true);

            // )
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(GroupByClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            GroupByClause expression = stateObject.getExpression();

            // 'GROUP BY'
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : GROUP_BY, GROUP_BY);

            if (shouldOutput(expression) || expression.hasSpaceAfterGroupBy()) {
                writer.append(SPACE);
            }

            // Group by items
            if (stateObject.hasItems()) {
                toStringChildren(stateObject, true);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(HavingClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            HavingClause expression = stateObject.getExpression();

            // 'HAVING'
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : HAVING, HAVING);

            if (exactMatch && (expression != null) && expression.hasSpaceAfterIdentifier() ||
                stateObject.hasConditional()) {

                writer.append(SPACE);
            }

            // Conditional expression
            if (stateObject.hasConditional()) {
                stateObject.getConditional().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(IdentificationVariableDeclarationStateObject stateObject) {
        toStringIdentificationVariableDeclaration(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(IdentificationVariableStateObject stateObject) {
        toStringSimpleStateObject(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(IndexExpressionStateObject stateObject) {
        toStringEncapsulatedIdentificationVariable(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(InExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            InExpression expression = stateObject.getExpression();

            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
                writer.append(SPACE);
            }

            // 'NOT'
            if (stateObject.hasNot()) {
                appendIdentifier((expression != null) ? expression.getActualNotIdentifier() : NOT, NOT);
                writer.append(SPACE);
            }

            // 'IN'
            appendIdentifier((expression != null) ? expression.getActualInIdentifier() : IN, IN);

            if (!stateObject.isSingleInputParameter()) {
                writer.append(LEFT_PARENTHESIS);
            }
            else if (shouldOutput(expression) || expression.hasSpaceAfterIn()) {
                writer.append(SPACE);
            }

            if (stateObject.hasItems()) {
                toStringChildren(stateObject, true);
            }

            if (!stateObject.isSingleInputParameter()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(InputParameterStateObject stateObject) {
        toStringSimpleStateObject(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(JoinStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            Join expression = stateObject.getExpression();

            // JOIN
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : stateObject.getJoinType(), stateObject.getJoinType());

            if (shouldOutput(expression) || expression.hasSpaceAfterJoin()) {
                writer.append(SPACE);
            }

            // Join association path
            stateObject.getJoinAssociationPathStateObject().accept(this);

            // Check first if the JOIN FETCH is allowed to have an identification variable
            if (stateObject.hasFetch()) {
                if (expression.hasAs()) {
                    writer.append(SPACE);
                }
            }
            // JOIN always needs a whitespace
            else {
                if (shouldOutput(expression) || expression.hasSpaceAfterJoinAssociation()) {
                    writer.append(SPACE);
                }
            }

            // AS
            if (stateObject.hasAs()) {

                appendIdentifier((expression != null) ? expression.getActualAsIdentifier() : AS, AS);

                if (shouldOutput(expression) || expression.hasSpaceAfterAs()) {
                    writer.append(SPACE);
                }
            }

            // Identification variable
            stateObject.getIdentificationVariableStateObject().accept(this);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(JPQLQueryStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else if (stateObject.hasQueryStatement()) {
            stateObject.getQueryStatement().accept(this);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(KeyExpressionStateObject stateObject) {
        toStringEncapsulatedIdentificationVariable(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(KeywordExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            KeywordExpression expression = stateObject.getExpression();
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : stateObject.getText(), stateObject.getText());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(LengthExpressionStateObject stateObject) {
        toStringSingleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(LikeExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            LikeExpression expression = stateObject.getExpression();

            // String expression
            if (stateObject.hasStringStateObject()) {
                stateObject.getStringStateObject().accept(this);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterStringExpression()) {
                writer.append(SPACE);
            }

            // 'NOT'
            if (stateObject.hasNot()) {
                appendIdentifier((expression != null) ? expression.getActualNotIdentifier() : NOT, NOT);
                writer.append(SPACE);
            }

            // 'LIKE'
            appendIdentifier((expression != null) ? expression.getActualLikeIdentifier() : LIKE, LIKE);

            if (shouldOutput(expression) || expression.hasSpaceAfterLike()) {
                writer.append(SPACE);
            }

            // Pattern value
            if (stateObject.hasPatternValue()) {
                stateObject.getPatternValue().accept(this);
            }

            if (exactMatch && (expression != null) && expression.hasSpaceAfterPatternValue()) {
                writer.append(SPACE);
            }

            // Escape character
            if (stateObject.hasEscapeCharacter()) {

                if (!exactMatch) {
                    writer.append(SPACE);
                }

                appendIdentifier((expression != null) ? expression.getActualEscapeIdentifier() : ESCAPE, ESCAPE);

                if (shouldOutput(expression) || expression.hasSpaceAfterEscape()) {
                    writer.append(SPACE);
                }

                writer.append(stateObject.getEscapeCharacter());
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(LocateExpressionStateObject stateObject) {
        toStringTripleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(LowerExpressionStateObject stateObject) {
        toStringSingleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(MaxFunctionStateObject stateObject) {
        toStringAggregateFunction(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(MinFunctionStateObject stateObject) {
        toStringAggregateFunction(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ModExpressionStateObject stateObject) {
        toStringDoubleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(MultiplicationExpressionStateObject stateObject) {
        toStringCompound(stateObject, MULTIPLICATION);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(NotExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            NotExpression expression = stateObject.getExpression();

            // 'NOT'
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : NOT, NOT);

            if (shouldOutput(expression) || expression.hasSpaceAfterNot()) {
                writer.append(SPACE);
            }

            // Expression
            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(NullComparisonExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            NullComparisonExpression expression = stateObject.getExpression();

            // Expression
            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
                writer.append(SPACE);
            }

            // 'IS'
            appendIdentifier((expression != null) ? expression.getActualIsIdentifier() : IS, IS);
            writer.append(SPACE);

            // 'NOT'
            if (stateObject.hasNot()) {
                appendIdentifier((expression != null) ? expression.getActualNotIdentifier() : NOT, NOT);
                writer.append(SPACE);
            }

            // 'NULL'
            appendIdentifier((expression != null) ? expression.getActualNullIdentifier() : NULL, NULL);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(NullIfExpressionStateObject stateObject) {
        toStringDoubleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(NumericLiteralStateObject stateObject) {
        toStringSimpleStateObject(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ObjectExpressionStateObject stateObject) {
        toStringEncapsulatedIdentificationVariable(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(OrderByClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            OrderByClause expression = stateObject.getExpression();

            // 'ORDER BY'
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : ORDER_BY, ORDER_BY);

            if (shouldOutput(expression) || expression.hasSpaceAfterIdentifier()) {
                writer.append(SPACE);
            }

            // Order by items
            if (stateObject.hasItems()) {
                toStringChildren(stateObject, true);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(OrderByItemStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            OrderByItem expression = stateObject.getExpression();

            // Order by item
            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
            }

            // ASC/DESC
            if (!stateObject.isDefault()) {

                if (shouldOutput(expression) || expression.hasSpaceAfterExpression()) {
                    writer.append(SPACE);
                }

                String ordering = stateObject.getOrdering().name();
                String actualOrdering = (expression != null) ? expression.getActualOrdering() : null;

                if (!ordering.equalsIgnoreCase(actualOrdering)) {
                    actualOrdering = ordering;
                }

                appendIdentifier(actualOrdering, ordering);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(OrExpressionStateObject stateObject) {
        toStringCompound(stateObject, OR);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(RangeVariableDeclarationStateObject stateObject) {
        toStringRangeVariableDeclaration(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ResultVariableStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            ResultVariable expression = stateObject.getExpression();

            // Select expression
            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
            }

            if (exactMatch && (expression != null) && expression.hasSelectExpression()) {
                writer.append(SPACE);
            }

            // 'AS'
            if (stateObject.hasAs()) {
                if (!exactMatch || (expression == null)) {
                    writer.append(SPACE);
                }
                appendIdentifier((expression != null) ? expression.getActualAsIdentifier() : AS, AS);
            }

            if (exactMatch && (expression != null) && expression.hasSpaceAfterAs()) {
                writer.append(SPACE);
            }

            // Result variable
            if (stateObject.hasResultVariable()) {
                if (!exactMatch || (expression == null)) {
                    writer.append(SPACE);
                }
                writer.append(stateObject.getResultVariable());
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SelectClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            SelectClause expression = stateObject.getExpression();

            // SELECT
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : SELECT, SELECT);

            if (shouldOutput(expression) || expression.hasSpaceAfterSelect()) {
                writer.append(SPACE);
            }

            // DISTINCT
            if (stateObject.hasDistinct()) {
                appendIdentifier((expression != null) ? expression.getActualDistinctIdentifier() : DISTINCT, DISTINCT);

                if (shouldOutput(expression) || expression.hasSpaceAfterDistinct()) {
                    writer.append(SPACE);
                }
            }

            // Select expressions
            if (stateObject.hasItems()) {
                toStringChildren(stateObject, true);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SelectStatementStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            boolean endWithSpace = toStringSelectStatement(stateObject);
            SelectStatement expression = stateObject.getExpression();

            // If no ORDER BY clause was parsed but was added later, make sure a space is added
            if (exactMatch && (expression != null) && expression.hasSpaceBeforeOrderBy() ||
                stateObject.hasOrderByClause()) {

                if (!endWithSpace) {
                    writer.append(SPACE);
                }
            }

            // ORDER BY clause
            if (stateObject.hasOrderByClause()) {
                stateObject.getOrderByClause().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SimpleFromClauseStateObject stateObject) {
        toStringFromClause(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SimpleSelectClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            SimpleSelectClause expression = stateObject.getExpression();

            // SELECT
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : SELECT, SELECT);

            if (shouldOutput(expression) || expression.hasSpaceAfterSelect()) {
                writer.append(SPACE);
            }

            // DISTINCT
            if (stateObject.hasDistinct()) {
                appendIdentifier((expression != null) ? expression.getActualDistinctIdentifier() : DISTINCT, DISTINCT);

                if (shouldOutput(expression) || expression.hasSpaceAfterDistinct()) {
                    writer.append(SPACE);
                }
            }

            // Select expression
            if (stateObject.hasSelectItem()) {
                stateObject.getSelectItem().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SimpleSelectStatementStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            toStringSelectStatement(stateObject);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SizeExpressionStateObject stateObject) {
        toStringSingleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SqrtExpressionStateObject stateObject) {
        toStringSingleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(StateFieldPathExpressionStateObject stateObject) {
        toStringPathExpression(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(StringLiteralStateObject stateObject) {
        toStringSimpleStateObject(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SubExpressionStateObject stateObject) {
        toStringSingleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SubstringExpressionStateObject stateObject) {
        toStringTripleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SubtractionExpressionStateObject stateObject) {
        toStringCompound(stateObject, MINUS);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(SumFunctionStateObject stateObject) {
        toStringAggregateFunction(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(TreatExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            TreatExpression expression = stateObject.getExpression();

            // TREAT
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : TREAT, TREAT);

            // (
            if (shouldOutput(expression) || expression.hasLeftParenthesis()) {
                writer.append(LEFT_PARENTHESIS);
            }

            // Join association path expression
            stateObject.getJoinAssociationPathStateObject().accept(this);

            if (shouldOutput(expression) || expression.hasSpaceAfterCollectionValuedPathExpression()) {
                writer.append(SPACE);
            }

            // AS
            if (stateObject.hasAs()) {

                appendIdentifier((expression != null) ? expression.getActualAsIdentifier() : AS, AS);

                if (shouldOutput(expression) || expression.hasSpaceAfterAs()) {
                    writer.append(SPACE);
                }
            }

            // Entity type name
            writer.append(stateObject.getEntityTypeName());

            // )
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(TrimExpressionStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            TrimExpression expression = stateObject.getExpression();

            // 'TRIM'
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : stateObject.getIdentifier(), stateObject.getIdentifier());

            // '('
            if (shouldOutput(expression) || ( expression != null && expression.hasLeftParenthesis())) {
                writer.append(LEFT_PARENTHESIS);
            }
            else if ((expression != null) && expression.hasSpaceAfterIdentifier()) {
                writer.append(SPACE);
            }

            // Trim specification
            if (stateObject.hasSpecification()) {
                String specification = stateObject.getSpecification().name();
                String actualSpecification = (expression != null) ? expression.getActualSpecificationIdentifier() : null;

                if (!specification.equalsIgnoreCase(actualSpecification)) {
                    actualSpecification = specification;
                }

                appendIdentifier(actualSpecification, specification);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterSpecification()) {
                writer.append(SPACE);
            }

            // Trim character
            if (stateObject.hasTrimCharacter()) {
                stateObject.getTrimCharacter().accept(this);

                if (shouldOutput(expression) || expression.hasSpaceAfterTrimCharacter()) {
                    writer.append(SPACE);
                }
            }

            // 'FROM'
            if (stateObject.hasSpecification() ||
                stateObject.hasTrimCharacter()) {

                appendIdentifier((expression != null) ? expression.getActualFromIdentifier() : FROM, FROM);

                if (shouldOutput(expression) || expression.hasSpaceAfterFrom()) {
                    writer.append(SPACE);
                }
            }

            // String primary
            if (stateObject.hasStateObject()) {
                stateObject.getStateObject().accept(this);
            }

            // ')'
            if (shouldOutput(expression) || expression.hasRightParenthesis()) {
                writer.append(RIGHT_PARENTHESIS);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(TypeExpressionStateObject stateObject) {
        toStringSingleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UnknownExpressionStateObject stateObject) {
        toStringSimpleStateObject(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UpdateClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            UpdateClause expression = stateObject.getExpression();

            // 'UPDATE'
            appendIdentifier((expression != null) ? expression.getActualUpdateIdentifier() : UPDATE, UPDATE);

            if (shouldOutput(expression) || expression.hasSpaceAfterUpdate()) {
                writer.append(SPACE);
            }

            // Range variable declaration
            stateObject.getRangeVariableDeclaration().accept(this);

            if (shouldOutput(expression) || expression.hasSpaceAfterRangeVariableDeclaration()) {
                writer.append(SPACE);
            }

            // 'SET'
            if (shouldOutput(expression) || expression.hasSet()) {
                appendIdentifier((expression != null) ? expression.getActualSetIdentifier() : SET, SET);

                if (shouldOutput(expression) || expression.hasSpaceAfterSet()) {
                    writer.append(SPACE);
                }
            }

            // Update items
            if (stateObject.hasItems()) {
                toStringChildren(stateObject, true);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UpdateItemStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            UpdateItem expression = stateObject.getExpression();

            // Update item
            stateObject.getStateFieldPath().accept(this);

            if (shouldOutput(expression) || expression.hasSpaceAfterStateFieldPathExpression()) {
                writer.append(SPACE);
            }

            // '='
            if (shouldOutput(expression) || expression.hasEqualSign()) {
                writer.append(EQUAL);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterEqualSign()) {
                writer.append(SPACE);
            }

            // New value
            if (stateObject.hasNewValue()) {
                stateObject.getNewValue().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UpdateStatementStateObject stateObject) {
        toStringModifyStatement(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(UpperExpressionStateObject stateObject) {
        toStringSingleEncapsulated(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(ValueExpressionStateObject stateObject) {
        toStringEncapsulatedIdentificationVariable(stateObject);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(WhenClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            WhenClause expression = stateObject.getExpression();

            // 'WHEN'
            appendIdentifier((expression != null) ? expression.getActualWhenIdentifier() : WHEN, WHEN);

            if (shouldOutput(expression) || expression.hasSpaceAfterWhen()) {
                writer.append(SPACE);
            }

            // WHEN expression
            if (stateObject.hasConditional()) {
                stateObject.getConditional().accept(this);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterWhenExpression()) {
                writer.append(SPACE);
            }

            // 'THEN'
            if (shouldOutput(expression) || expression.hasThen()) {
                appendIdentifier((expression != null) ? expression.getActualThenIdentifier() : THEN, THEN);
            }

            if (shouldOutput(expression) || expression.hasSpaceAfterThen()) {
                writer.append(SPACE);
            }

            // THEN expression
            if (stateObject.hasThen()) {
                stateObject.getThen().accept(this);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void visit(WhereClauseStateObject stateObject) {

        if (stateObject.isDecorated()) {
            toText(stateObject);
        }
        else {
            WhereClause expression = stateObject.getExpression();

            // 'WHERE
            appendIdentifier((expression != null) ? expression.getActualIdentifier() : WHERE, WHERE);

            if (exactMatch && (expression != null) && expression.hasSpaceAfterIdentifier() ||
                stateObject.hasConditional()) {

                writer.append(SPACE);
            }

            // Conditional expression
            if (stateObject.hasConditional()) {
                stateObject.getConditional().accept(this);
            }
        }
    }
}
