All Downloads are FREE. Search and download functionalities are using the official Maven repository.

wtf.metio.yosql.codegen.dao.DefaultJdbcBlocks Maven / Gradle / Ivy

There is a newer version: 2023.5.3
Show newest version
/*
 * This file is part of yosql. It is subject to the license terms in the LICENSE file found in the top-level
 * directory of this distribution and at https://creativecommons.org/publicdomain/zero/1.0/. No part of yosql,
 * including this file, may be copied, modified, propagated, or distributed except according to the terms contained
 * in the LICENSE file.
 */
package wtf.metio.yosql.codegen.dao;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import wtf.metio.yosql.codegen.blocks.*;
import wtf.metio.yosql.codegen.exceptions.*;
import wtf.metio.yosql.codegen.logging.LoggingGenerator;
import wtf.metio.yosql.internals.javapoet.TypicalTypes;
import wtf.metio.yosql.internals.jdk.Buckets;
import wtf.metio.yosql.models.configuration.LoggingApis;
import wtf.metio.yosql.models.configuration.ResultRowConverter;
import wtf.metio.yosql.models.immutables.NamesConfiguration;
import wtf.metio.yosql.models.immutables.RuntimeConfiguration;
import wtf.metio.yosql.models.immutables.SqlConfiguration;
import wtf.metio.yosql.models.immutables.SqlStatement;

import java.sql.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.StreamSupport;

import static wtf.metio.yosql.codegen.blocks.CodeBlocks.code;

public final class DefaultJdbcBlocks implements JdbcBlocks {

    private final RuntimeConfiguration runtimeConfiguration;
    private final CodeBlocks blocks;
    private final ControlFlows controlFlows;
    private final NamesConfiguration names;
    private final Variables variables;
    private final JdbcMethods jdbcMethods;
    private final LoggingGenerator logging;
    private final FieldsGenerator fields;
    private final Parameters params;
    private final Methods methods;

    public DefaultJdbcBlocks(
            final RuntimeConfiguration runtimeConfiguration,
            final CodeBlocks blocks,
            final ControlFlows controlFlows,
            final Variables variables,
            final FieldsGenerator fields,
            final JdbcMethods jdbcMethods,
            final LoggingGenerator logging,
            final Parameters params,
            final Methods methods) {
        this.runtimeConfiguration = runtimeConfiguration;
        this.blocks = blocks;
        this.names = runtimeConfiguration.names();
        this.variables = variables;
        this.controlFlows = controlFlows;
        this.fields = fields;
        this.jdbcMethods = jdbcMethods;
        this.logging = logging;
        this.params = params;
        this.methods = methods;
    }

    @Override
    public CodeBlock getConnectionInline() {
        return variables.inline(Connection.class, names.connection(), jdbcMethods.dataSource().getConnection());
    }

    @Override
    public CodeBlock getConnection(final SqlConfiguration configuration) {
        return configuration.createConnection()
                .filter(Boolean.TRUE::equals)
                .map(value -> variables.statement(Connection.class, names.connection(),
                        jdbcMethods.dataSource().getConnection()))
                .orElseGet(() -> CodeBlock.builder().build());
    }

    @Override
    public CodeBlock prepareStatementInline() {
        return variables.inline(PreparedStatement.class, names.statement(),
                jdbcMethods.connection().prepareStatement());
    }

    @Override
    public CodeBlock prepareCallInline() {
        return variables.inline(CallableStatement.class, names.statement(),
                jdbcMethods.connection().prepareCall());
    }

    @Override
    public CodeBlock getMetaDataStatement() {
        return variables.statement(ResultSetMetaData.class, names.resultSetMetaData(),
                jdbcMethods.resultSet().getMetaData());
    }

    @Override
    public CodeBlock executeQueryInline() {
        return variables.inline(ResultSet.class, names.resultSet(),
                jdbcMethods.statement().executeQuery());
    }

    @Override
    public CodeBlock executeQueryStatement() {
        return variables.statement(ResultSet.class, names.resultSet(),
                jdbcMethods.statement().executeQuery());
    }

    @Override
    public CodeBlock getResultSet() {
        return controlFlows.tryWithResource(variables.inline(ResultSet.class, names.resultSet(),
                jdbcMethods.statement().getResultSet()));
    }

    @Override
    public CodeBlock returnExecuteUpdate(final SqlConfiguration configuration) {
        return configuration.writesReturnUpdateCount()
                .filter(Boolean.TRUE::equals)
                .map(value -> blocks.returnValue(jdbcMethods.statement().executeUpdate()))
                .orElseGet(() -> CodeBlock.builder()
                        .addStatement(jdbcMethods.statement().executeUpdate())
                        .build());
    }

    @Override
    public CodeBlock executeForReturning() {
        return jdbcMethods.statement().execute();
    }

    @Override
    public CodeBlock executeBatch() {
        return blocks.returnValue(jdbcMethods.statement().executeBatch());
    }

    @Override
    public CodeBlock closeResultSet() {
        return blocks.close(names.resultSet());
    }

    @Override
    public CodeBlock closePrepareStatement() {
        return blocks.close(names.statement());
    }

    @Override
    public CodeBlock closeConnection(final SqlConfiguration configuration) {
        return configuration.createConnection()
                .filter(Boolean.TRUE::equals)
                .map(value -> blocks.close(names.connection()))
                .orElseGet(() -> CodeBlock.builder().build());
    }

    @Override
    public CodeBlock executeStatement(final SqlConfiguration configuration) {
        return configuration.usePreparedStatement()
                .filter(Boolean.TRUE::equals)
                .map(value -> controlFlows.tryWithResource(executeQueryInline()))
                .orElseGet(() -> controlFlows.tryWithResource(executeStatementQueryInline()));
    }

    private CodeBlock executeStatementQueryInline() {
        return variables.inline(ResultSet.class, names.resultSet(),
                jdbcMethods.statement().executeGivenQuery());
    }

    @Override
    public CodeBlock openConnection(final SqlConfiguration configuration) {
        return configuration.createConnection()
                .filter(Boolean.TRUE::equals)
                .map(value -> controlFlows.tryWithResource(getConnectionInline()))
                .or(() -> configuration.catchAndRethrow()
                        .filter(Boolean.TRUE::equals)
                        .map(value -> controlFlows.startTryBlock()))
                .orElseGet(() -> CodeBlock.builder().build());
    }

    @Override
    public CodeBlock tryPrepareCallable() {
        return controlFlows.tryWithResource(prepareCallInline());
    }

    @Override
    public CodeBlock createStatement(final SqlConfiguration configuration) {
        return configuration.usePreparedStatement()
                .filter(Boolean.TRUE::equals)
                .map(value -> controlFlows.tryWithResource(prepareStatementInline()))
                .orElseGet(() -> controlFlows.tryWithResource(statementInline()));
    }

    private CodeBlock statementInline() {
        return variables.inline(Statement.class, names.statement(),
                jdbcMethods.connection().createStatement());
    }

    @Override
    public CodeBlock prepareBatch(final SqlConfiguration config) {
        return controlFlows.forLoop(
                code("int $N = 0; $N < $N.length; $N++",
                        names.batch(),
                        names.batch(),
                        config.parameters().get(0).name().orElseThrow(MissingParameterNameException::new),
                        names.batch()),
                CodeBlock.builder()
                        .add(setBatchParameters(config))
                        .addStatement(jdbcMethods.statement().addBatch())
                        .build());
    }

    @Override
    public CodeBlock pickVendorQuery(final List sqlStatements) {
        final var builder = CodeBlock.builder();
        if (sqlStatements.size() > 1) {
            builder.addStatement(variables.inline(DatabaseMetaData.class, names.databaseMetaData(),
                    jdbcMethods.connection().getMetaData()));
            builder.addStatement(variables.inline(String.class, names.databaseProductName(),
                            jdbcMethods.databaseMetaData().getDatabaseProductName()))
                    .add(logging.vendorDetected());
            if (logging.isEnabled()) {
                builder.addStatement("$T $N = null", String.class, names.rawQuery());
            }
            builder.addStatement("$T $N = null", String.class, names.query())
                    .addStatement("$T $N = null", TypicalTypes.MAP_OF_STRING_AND_ARRAY_OF_INTS, names.indexVariable())
                    .beginControlFlow("switch ($N)", names.databaseProductName());
            sqlStatements.stream()
                    .map(SqlStatement::getConfiguration)
                    .filter(config -> config.vendor().isPresent())
                    .forEach(config -> {
                        final var query = fields.constantSqlStatementFieldName(config);
                        builder.add("case $S:\n", config.vendor().orElseThrow(MissingSqlConfigurationVendorException::new))
                                .addStatement("$>$N = $N", names.query(), query)
                                .add(logging.vendorQueryPicked(query));
                        finalizeCase(builder, config);
                    });
            sqlStatements.stream()
                    .map(SqlStatement::getConfiguration)
                    .filter(config -> config.vendor().isEmpty())
                    .findFirst()
                    .ifPresentOrElse(config -> {
                        final var query = fields.constantSqlStatementFieldName(config);
                        builder.add("default:\n")
                                .addStatement("$>$N = $N", names.query(), query)
                                .add(logging.vendorQueryPicked(query));
                        finalizeCase(builder, config);
                    }, () -> builder.add("default:\n")
                            .addStatement("$>throw new $T($T.format($S, $N))$<", IllegalStateException.class, String.class,
                                    "No suitable query defined for vendor [%s]", names.databaseProductName()));

            builder.endControlFlow();
        } else {
            final var config = sqlStatements.get(0).getConfiguration();
            final var query = fields.constantSqlStatementFieldName(config);
            builder.addStatement(variables.inline(String.class, names.query(), "$N", query))
                    .add(logging.queryPicked(query));
            if (logging.isEnabled()) {
                final var rawQuery = fields.constantRawSqlStatementFieldName(config);
                builder.addStatement(variables.inline(String.class, names.rawQuery(), "$N", rawQuery));
            }
            if (Buckets.hasEntries(config.parameters())) {
                final var indexFieldName = fields.constantSqlStatementParameterIndexFieldName(config);
                builder.addStatement(variables.inline(TypicalTypes.MAP_OF_STRING_AND_ARRAY_OF_INTS,
                                names.indexVariable(), "$N", indexFieldName))
                        .add(logging.indexPicked(indexFieldName));
            }
        }
        return builder.build();
    }

    private void finalizeCase(final CodeBlock.Builder builder, final SqlConfiguration config) {
        if (logging.isEnabled()) {
            final var rawQuery = fields.constantRawSqlStatementFieldName(config);
            builder.addStatement("$N = $N", names.rawQuery(), rawQuery);
        }
        if (Buckets.hasEntries(config.parameters())) {
            final var indexName = fields.constantSqlStatementParameterIndexFieldName(config);
            builder.addStatement("$N = $N", names.indexVariable(), indexName)
                    .add(logging.vendorIndexPicked(indexName));
        }
        builder.addStatement("break$<");
    }

    @Override
    public CodeBlock logExecutedQuery(final SqlConfiguration sqlConfiguration) {
        final var builder = CodeBlock.builder();
        if (LoggingApis.NONE != runtimeConfiguration.logging().api()) {
            builder.beginControlFlow("if ($L)", logging.shouldLog());
            builder.add(variables.inline(String.class, names.executedQuery(), "$N", names.rawQuery()));
            sqlConfiguration.parameters()
                    .forEach(parameter -> {
                        final var type = parameter.typeName()
                                .orElseThrow(MissingParameterTypeNameException::new);
                        final var name = parameter.name()
                                .orElseThrow(MissingParameterNameException::new);
                        if (type.isPrimitive()) {
                            builder.add("\n$>.replace($S, $T.valueOf($N))$<", ":" + name,
                                    String.class, name);
                        } else {
                            builder.add("\n$>.replace($S, $N == null ? $S : $N.toString())$<",
                                    ":" + name, name, "null", name);
                        }
                    });
            builder.add(";\n");
            builder.add(logging.executingQuery());
            builder.endControlFlow();
        }
        return builder.build();
    }

    @Override
    public CodeBlock logExecutedBatchQuery(final SqlConfiguration sqlConfiguration) {
        final var builder = CodeBlock.builder();
        if (LoggingApis.NONE != runtimeConfiguration.logging().api()) {
            builder.beginControlFlow("if ($L)", logging.shouldLog());
            builder.add(variables.inline(String.class, names.executedQuery(), "$N", names.rawQuery()));
            sqlConfiguration.parameters()
                    .forEach(parameter -> {
                        final var type = parameter.typeName()
                                .orElseThrow(MissingParameterTypeNameException::new);
                        final var name = parameter.name()
                                .orElseThrow(MissingParameterNameException::new);
                        if (type.isPrimitive()) {
                            builder.add("\n$>.replace($S, $T.toString($N))$<", ":" + name, Arrays.class, name);
                        } else {
                            builder.add("\n$>.replace($S, $N == null ? $S : $T.toString($N))$<",
                                    ":" + name, name, "null", Arrays.class, name);
                        }
                    });
            builder.add(";\n");
            builder.add(logging.executingQuery());
            builder.endControlFlow();
        }
        return builder.build();
    }

    private CodeBlock.Builder prepareReturnList(final ParameterizedTypeName listOfResults, final ResultRowConverter converter) {
        final var java = runtimeConfiguration.java();
        CodeBlock template;
        if (java.useVar()) {
            template = CodeBlock.of("new $T()", ParameterizedTypeName.get(
                    ClassName.get(ArrayList.class), listOfResults.typeArguments.get(0)));
        } else {
            template = CodeBlock.of("new $T<>()", ArrayList.class);
        }
        return CodeBlock.builder()
                .addStatement(variables.inline(listOfResults, names.list(), template))
                .add(controlFlows.whileHasNext())
                .addStatement("$N.add($N.$N($N))",
                        names.list(),
                        converter.alias().orElseThrow(MissingConverterAliasException::new),
                        converter.methodName().orElseThrow(MissingConverterMethodNameException::new),
                        names.resultSet())
                .endControlFlow();
    }

    @Override
    public CodeBlock returnAsMultiple(final ResultRowConverter converter) {
        return prepareReturnList(TypicalTypes.listOf(converter.resultTypeName()
                .orElseThrow(MissingConverterResultTypeException::new)), converter)
                .addStatement("return $N", names.list())
                .build();
    }

    @Override
    public CodeBlock returnAsSingle(final SqlConfiguration configuration) {
        final var converters = runtimeConfiguration.converter();
        final var converter = configuration.converter(converters::defaultConverter);
        final var builder = prepareReturnList(TypicalTypes.listOf(converter.resultTypeName()
                .orElseThrow(MissingConverterResultTypeException::new)), converter);
        if (configuration.throwOnMultipleResults().orElse(Boolean.FALSE)) {
            builder.beginControlFlow("if ($N.size() > 1)", names.list())
                    .addStatement("throw new $T()", IllegalStateException.class)
                    .endControlFlow();
        }
        return builder.addStatement("return $N.size() > 0 ? $T.of($N.get(0)) : $T.empty()",
                        names.list(), Optional.class, names.list(), Optional.class)
                .build();
    }

    @Override
    public CodeBlock streamStateful(final SqlConfiguration configuration) {
        final var converters = runtimeConfiguration.converter();
        final var converter = configuration.converter(converters::defaultConverter);
        return CodeBlock.builder()
                .addStatement("return $T.stream($L, false).onClose($L)",
                        StreamSupport.class,
                        lazyStreamSpliterator(converter),
                        lazyStreamCloser(configuration))
                .build();
    }


    private TypeSpec lazyStreamSpliterator(final ResultRowConverter converter) {
        final var spliteratorClass = ClassName.get(Spliterators.AbstractSpliterator.class);
        final var resultType = converter.resultTypeName()
                .orElseThrow(MissingConverterResultTypeException::new);
        final var superinterface = ParameterizedTypeName.get(spliteratorClass, resultType);
        final var consumerType = TypicalTypes.consumerOf(resultType);
        return TypeSpec
                .anonymousClassBuilder("$T.MAX_VALUE, $T.ORDERED", Long.class, Spliterator.class)
                .addSuperinterface(superinterface)
                .addMethod(methods.implementation("tryAdvance")
                        .addParameter(params.parameter(consumerType, names.action()))
                        .returns(boolean.class)
                        .addCode(controlFlows.startTryBlock())
                        .addCode(controlFlows.ifHasNext())
                        .addStatement("$N.accept($N.$N($N))", names.action(), converter.alias()
                                        .orElseThrow(MissingConverterAliasException::new),
                                converter.methodName().orElseThrow(MissingConverterMethodNameException::new),
                                names.resultSet())
                        .addCode(blocks.returnTrue())
                        .addCode(controlFlows.endIf())
                        .addCode(blocks.returnFalse())
                        .addCode(controlFlows.endTryBlock())
                        .addCode(controlFlows.catchAndRethrow())
                        .build())
                .build();
    }

    private TypeSpec lazyStreamCloser(final SqlConfiguration configuration) {
        return TypeSpec.anonymousClassBuilder("")
                .addSuperinterface(Runnable.class)
                .addMethod(methods.implementation("run")
                        .returns(void.class)
                        .addCode(controlFlows.startTryBlock())
                        .addCode(closeResultSet())
                        .addCode(closePrepareStatement())
                        .addCode(closeConnection(configuration))
                        .addCode(controlFlows.endTryBlock())
                        .addCode(controlFlows.catchAndRethrow())
                        .build())
                .build();
    }

    @Override
    public CodeBlock setParameters(final SqlConfiguration config) {
        return parameterAssignment(config, "$N.setObject($N, $N)",
                parameterName -> new String[]{
                        names.statement(),
                        names.jdbcIndexVariable(),
                        parameterName});
    }

    @Override
    public CodeBlock setBatchParameters(final SqlConfiguration config) {
        return parameterAssignment(config, "$N.setObject($N, $N[$N])",
                parameterName -> new String[]{
                        names.statement(),
                        names.jdbcIndexVariable(),
                        parameterName,
                        names.batch()});
    }

    private CodeBlock parameterAssignment(
            final SqlConfiguration config,
            final String codeStatement,
            final Function parameterSetter) {
        final var builder = CodeBlock.builder();
        config.parameters().forEach(parameter -> builder.add(controlFlows.forLoop(
                code("final int $N : $N.get($S)",
                        names.jdbcIndexVariable(),
                        names.indexVariable(),
                        parameter.name().orElseThrow(MissingParameterNameException::new)),
                CodeBlock.builder()
                        .addStatement(codeStatement, parameterSetter.apply(parameter.name()
                                .orElseThrow(MissingParameterNameException::new)))
                        .build())));

        return builder.build();
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy