ExpressionVisitor.java

package com.github.mygreen.sqlmapper.core.where.metamodel;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.springframework.util.StringUtils;

import com.github.mygreen.sqlmapper.core.dialect.Dialect;
import com.github.mygreen.sqlmapper.core.meta.EntityMeta;
import com.github.mygreen.sqlmapper.core.meta.PropertyMeta;
import com.github.mygreen.sqlmapper.core.query.FromClause;
import com.github.mygreen.sqlmapper.core.query.IllegalQueryException;
import com.github.mygreen.sqlmapper.core.query.OrderByClause;
import com.github.mygreen.sqlmapper.core.query.SelectClause;
import com.github.mygreen.sqlmapper.core.query.TableNameResolver;
import com.github.mygreen.sqlmapper.core.query.WhereClause;
import com.github.mygreen.sqlmapper.core.util.QueryUtils;
import com.github.mygreen.sqlmapper.metamodel.OrderSpecifier;
import com.github.mygreen.sqlmapper.metamodel.Path;
import com.github.mygreen.sqlmapper.metamodel.PathMeta;
import com.github.mygreen.sqlmapper.metamodel.PathType;
import com.github.mygreen.sqlmapper.metamodel.PropertyPath;
import com.github.mygreen.sqlmapper.metamodel.Visitor;
import com.github.mygreen.sqlmapper.metamodel.expression.Constant;
import com.github.mygreen.sqlmapper.metamodel.expression.SubQueryExpression;
import com.github.mygreen.sqlmapper.metamodel.operation.Operation;
import com.github.mygreen.sqlmapper.metamodel.operation.SubQueryMeta;
import com.github.mygreen.sqlmapper.metamodel.operator.ArithmeticOp;
import com.github.mygreen.sqlmapper.metamodel.operator.BooleanOp;
import com.github.mygreen.sqlmapper.metamodel.operator.ComparisionOp;
import com.github.mygreen.sqlmapper.metamodel.operator.FunctionOp;
import com.github.mygreen.sqlmapper.metamodel.operator.LikeOp;
import com.github.mygreen.sqlmapper.metamodel.operator.Operator;
import com.github.mygreen.sqlmapper.metamodel.operator.UnaryOp;

/**
 * メタモデルの式ノードを巡回するVisitorです。
 *
 *
 * @author T.TSUCHIE
 *
 */
public class ExpressionVisitor implements Visitor<VisitorContext> {

    private Map<Class<?>, OperationHandler<? extends Operator>> operationHandlerMap = new HashMap<>();

    public ExpressionVisitor() {
        register(BooleanOp.class, new BooleanOpHandler());
        register(UnaryOp.class, new UnaryOpHandler());
        register(ComparisionOp.class, new ComparisionOpHandler());
        register(LikeOp.class, new LikeOpHandler());
        register(FunctionOp.class, new FuncOpHandler());
        register(ArithmeticOp.class, new ArithmeticOpHandler());
    }

    /**
     * 演算子に対する処理を登録します。
     * <p>登録する際に、{@literal OperationHandler#init()}を実行します。
     *
     * @param <T> 演算子の種別
     * @param operatorClass 演算子種別のクラス
     * @param handler 演算子に対する処理
     */
    public <T extends Operator> void register(Class<T> operatorClass,  OperationHandler<T> handler) {
        handler.init();
        this.operationHandlerMap.put(operatorClass, handler);
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Override
    public void visit(final Operation<?> expr, final VisitorContext context) {
        final Operator operator = expr.getOperator();

        OperationHandler handler = operationHandlerMap.get(operator.getClass());
        handler.handle(operator, expr, this, context);

    }

    @Override
    public void visit(final Constant<?> expr, final VisitorContext context) {
        // 値はプレースホルダーを追加
        if(expr.isExpandable()) {
            // IN句などの展開可能な複数要素の場合
            Collection<?> values = (Collection<?>)expr.getValue();
            context.addParamValues(values);
            context.appendSql("(")
                .append(QueryUtils.repeat("?", ", ", values.size()))
                .append(")");
        } else {
            context.addParamValue(expr.getValue());
            context.appendSql("?");

        }

    }


    @Override
    public void visit(final Path<?> expr, final VisitorContext context) {

        final PathMeta pathMeta = expr.getPathMeta();
        if(pathMeta.getType() == PathType.PROPERTY) {
            Path<?> rootPath = pathMeta.findRootPath();
            Class<?> rootClassType = rootPath.getType();
            String propertyName = pathMeta.getElement();
            Optional<PropertyMeta> propertyMeta = context.getEntityMetaMap().get(rootClassType).findPropertyMeta(propertyName);
            if(propertyMeta.isEmpty()) {
                throw new IllegalQueryException("unknwon property : " + propertyName);
            }

            final String tableName = context.getTableNameResolver().getTableAlias(rootPath);
            final String columnName;
            // SQL - カラム名を追加
            if(tableName != null) {
                columnName = tableName + "." + propertyMeta.get().getColumnMeta().getName();
            } else {
                columnName = propertyMeta.get().getColumnMeta().getName();;
            }
            context.appendSql(columnName);

        } else {
            throw new IllegalArgumentException("not support pathType=" + pathMeta.getType());
        }

    }

    @Override
    public void visit(final SubQueryExpression<?> expr, final VisitorContext context) {
        final SubQueryMeta queryMeta = expr.getQueryMeta();
        final Dialect dialect = context.getDialect();
        final TableNameResolver tableNameResolver = context.getTableNameResolver();

        // テーブル名の組み立て
        //TODO: テーブル名は新規に割り当てる必要があるかどうか。
        final FromClause fromClause = new FromClause();
        tableNameResolver.prepareTableAlias(queryMeta.getEntityPath());
        EntityMeta entityMeta = context.getEntityMetaFactory().create(queryMeta.getEntityPath().getType());
        fromClause.addSql(entityMeta.getTableMeta().getFullName(), tableNameResolver.getTableAlias(queryMeta.getEntityPath()));

        // 抽出カラムの組み立て
        final SelectClause selectClause = new SelectClause();
        for(PropertyMeta propertyMeta : entityMeta.getAllColumnPropertyMeta()) {
            final String propertyName = propertyMeta.getName();
            final PropertyPath<?> propertyPath = queryMeta.getEntityPath().findPropertyPath(propertyName);

            if(propertyMeta.isTransient()) {
                continue;
            }

            if(!queryMeta.getIncludesProperties().isEmpty()
                    && !queryMeta.getIncludesProperties().contains(propertyPath)) {
                continue;
            }

            String tableAlias = tableNameResolver.getTableAlias(queryMeta.getEntityPath());
            selectClause.addSql(tableAlias, propertyMeta.getColumnMeta().getName());
        }

        // 条件の組み立て
        final WhereClause whereClause = new WhereClause();
        final List<Object> paramValues = new ArrayList<>();
        if(queryMeta.getWhere() != null) {
            MetamodelWhereVisitor visitor = new MetamodelWhereVisitor(context.getEntityMetaMap(), context.getDialect(),
                    context.getEntityMetaFactory(), tableNameResolver);
            visitor.visit(new MetamodelWhere(queryMeta.getWhere()));

            whereClause.addSql(visitor.getCriteria());
            paramValues.addAll(visitor.getParamValues());
        }

        // 並び順の組み立て
        final OrderByClause orderByClause = new OrderByClause();
        for(OrderSpecifier order : queryMeta.getOrders()) {
            String propertyName = order.getPath().getPathMeta().getElement();
            Optional<PropertyMeta> propertyMeta = entityMeta.findPropertyMeta(propertyName);

            String tableAlias = tableNameResolver.getTableAlias(order.getPath().getPathMeta().getParent());
            if(!StringUtils.hasLength(tableAlias)) {
                //TODO: 例外処理

            }

            propertyMeta.ifPresent(p -> {
                String orderBy = String.format("%s.%s %s", tableAlias, p.getColumnMeta().getName(), order.getOrder().name());
                orderByClause.addSql(orderBy);
            });
        }

        String sql = "select "
                + selectClause.toSql()
                + fromClause.toSql()
                + whereClause.toSql()
                + orderByClause.toSql();

        // limit句の組み立て
        if(queryMeta.getLimit() > 0 || queryMeta.getLimit() == 0 && queryMeta.getOffset() > 0) {
            sql = dialect.convertLimitSql(sql, queryMeta.getOffset(), queryMeta.getLimit());
        }

        // 組み立てたSQLとパラメータを格納する
        context.addParamValues(paramValues);
        context.appendSql(sql);
    }

}