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

com.speedment.runtime.core.internal.AbstractApplicationBuilder Maven / Gradle / Ivy

/**
 *
 * Copyright (c) 2006-2016, Speedment, Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); You may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.speedment.runtime.core.internal;

import com.speedment.common.injector.InjectBundle;
import com.speedment.common.injector.Injector;
import com.speedment.common.injector.exception.CyclicReferenceException;
import com.speedment.common.injector.internal.InjectorImpl;
import com.speedment.common.logger.Level;
import com.speedment.common.logger.Logger;
import com.speedment.common.logger.LoggerManager;
import com.speedment.common.tuple.Tuple2;
import com.speedment.common.tuple.Tuple3;
import com.speedment.common.tuple.Tuples;
import com.speedment.runtime.config.Dbms;
import com.speedment.runtime.config.Document;
import com.speedment.runtime.config.Project;
import com.speedment.runtime.config.Schema;
import com.speedment.runtime.config.trait.HasEnabled;
import com.speedment.runtime.config.trait.HasName;
import com.speedment.runtime.config.util.DocumentDbUtil;
import com.speedment.runtime.core.ApplicationBuilder;
import com.speedment.runtime.core.ApplicationMetadata;
import com.speedment.runtime.core.RuntimeBundle;
import com.speedment.runtime.core.Speedment;
import com.speedment.runtime.core.component.DbmsHandlerComponent;
import com.speedment.runtime.core.component.InfoComponent;
import com.speedment.runtime.core.component.PasswordComponent;
import com.speedment.runtime.core.component.ProjectComponent;
import com.speedment.runtime.core.db.DbmsMetadataHandler;
import com.speedment.runtime.core.db.DbmsType;
import com.speedment.runtime.core.exception.SpeedmentException;
import com.speedment.runtime.core.internal.db.AbstractDbmsOperationHandler;
import com.speedment.runtime.core.internal.db.AsynchronousQueryResultImpl;
import com.speedment.runtime.core.manager.Manager;
import com.speedment.runtime.core.util.DatabaseUtil;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.speedment.common.invariant.NullUtil.requireNonNulls;
import static com.speedment.runtime.config.util.DocumentUtil.Name.DATABASE_NAME;
import static com.speedment.runtime.config.util.DocumentUtil.relativeName;
import static java.util.Objects.requireNonNull;

/**
 * This abstract class is implemented by classes that can build a
 * {@link Speedment} application.
 *
 * @param  the type that is being built
 * @param  the (self) type of the AbstractApplicationBuilder
 *
 * @author Per Minborg
 * @author Emil Forslund
 * @since 2.0.0
 */
public abstract class AbstractApplicationBuilder<
        APP extends Speedment, BUILDER extends AbstractApplicationBuilder> implements ApplicationBuilder {

    private final static Logger LOGGER = LoggerManager.getLogger(AbstractApplicationBuilder.class);

    private final List, String, BiConsumer>> withsNamed;
    private final List, BiConsumer>> withsAll;
    private final Injector.Builder injector;

    private boolean skipCheckDatabaseConnectivity;
    private boolean skipValidateRuntimeConfig;
    private boolean skipLogoPrintout;

    protected AbstractApplicationBuilder(
        Class applicationImplClass,
        Class metadataClass) {

        this(Injector.builder()
            .putInBundle(RuntimeBundle.class)
            .put(applicationImplClass)
            .put(metadataClass)
        );
    }

    protected AbstractApplicationBuilder(Injector.Builder injector) {
        this.injector = requireNonNull(injector);
        this.withsNamed = new ArrayList<>();
        this.withsAll = new ArrayList<>();
        this.skipCheckDatabaseConnectivity = false;
        this.skipValidateRuntimeConfig = false;
    }

    private BUILDER self() {
        @SuppressWarnings("unchecked")
        final BUILDER builder = (BUILDER) this;
        return builder;
    }

    @Override
    public  BUILDER with(Class type, String name, BiConsumer consumer) {
        requireNonNulls(type, name, consumer);
        withsNamed.add(Tuples.of(type, name, consumer));
        return self();
    }

    @Override
    public  BUILDER with(Class type, BiConsumer consumer) {
        requireNonNulls(type, consumer);
        withsAll.add(Tuples.of(type, consumer));
        return self();
    }

    @Override
    public BUILDER withParam(String key, String value) {
        requireNonNulls(key, value);
        injector.putParam(key, value);
        return self();
    }

    @Override
    public BUILDER withPassword(char[] password) {
        // password nullable
        with(Dbms.class, (inj, dbms) -> inj.getOrThrow(PasswordComponent.class).put(dbms, password));
        return self();
    }

    @Override
    public BUILDER withPassword(String dbmsName, char[] password) {
        requireNonNull(dbmsName);
        // password nullable
        with(Dbms.class, dbmsName, (inj, dbms) -> inj.getOrThrow(PasswordComponent.class).put(dbms, password));
        return self();
    }

    @Override
    public BUILDER withPassword(String password) {
        // password nullable
        return withPassword(password == null ? null : password.toCharArray());
    }

    @Override
    public BUILDER withPassword(String dbmsName, String password) {
        requireNonNull(dbmsName);
        // password nullable
        return withPassword(dbmsName, password == null ? null : password.toCharArray());
    }

    @Override
    public BUILDER withUsername(String username) {
        // username nullable
        with(Dbms.class, dbms -> dbms.mutator().setUsername(username));
        return self();
    }

    @Override
    public BUILDER withUsername(String dbmsName, String username) {
        requireNonNull(dbmsName);
        // username nullable
        with(Dbms.class, dbmsName, d -> d.mutator().setUsername(username));
        return self();
    }

    @Override
    public BUILDER withIpAddress(String ipAddress) {
        requireNonNull(ipAddress);
        with(Dbms.class, d -> d.mutator().setIpAddress(ipAddress));
        return self();
    }

    @Override
    public BUILDER withIpAddress(String dbmsName, String ipAddress) {
        requireNonNulls(dbmsName, ipAddress);
        with(Dbms.class, dbmsName, d -> d.mutator().setIpAddress(ipAddress));
        return self();
    }

    @Override
    public BUILDER withPort(int port) {
        with(Dbms.class, d -> d.mutator().setPort(port));
        return self();
    }

    @Override
    public BUILDER withPort(String dbmsName, int port) {
        requireNonNull(dbmsName);
        with(Dbms.class, dbmsName, d -> d.mutator().setPort(port));
        return self();
    }

    @Override
    public BUILDER withSchema(String schemaName) {
        requireNonNull(schemaName);
        with(Schema.class, s -> s.mutator().setName(schemaName));
        return self();
    }

    @Override
    public BUILDER withSchema(String oldSchemaName, String schemaName) {
        requireNonNulls(oldSchemaName, schemaName);
        with(Schema.class, oldSchemaName, s -> s.mutator().setName(schemaName));
        return self();
    }

    @Override
    public BUILDER withConnectionUrl(String connectionUrl) {
        with(Dbms.class, d -> d.mutator().setConnectionUrl(connectionUrl));
        return self();
    }

    @Override
    public BUILDER withConnectionUrl(String dbmsName, String connectionUrl) {
        requireNonNull(dbmsName);
        with(Dbms.class, dbmsName, s -> s.mutator().setName(connectionUrl));
        return self();
    }

    @Override
    public > BUILDER withManager(Class managerImplType) {
        requireNonNull(managerImplType);
        withInjectable(injector, managerImplType, M::getEntityClass);
        return self();
    }

    @Override
    public BUILDER withSkipCheckDatabaseConnectivity() {
        this.skipCheckDatabaseConnectivity = true;
        return self();
    }

    @Override
    public BUILDER withSkipValidateRuntimeConfig() {
        this.skipValidateRuntimeConfig = true;
        return self();
    }

    @Override
    public BUILDER withSkipLogoPrintout() {
        this.skipLogoPrintout = true;
        return self();
    }

    @Override
    public BUILDER withBundle(Class bundleClass) {
        requireNonNull(bundleClass);
        injector.putInBundle(bundleClass);
        return self();
    }

    @Override
    public BUILDER withComponent(Class injectableClass) {
        requireNonNull(injectableClass);
        injector.put(injectableClass);
        return self();
    }

    @Override
    public BUILDER withComponent(String key, Class injectableClass) {
        requireNonNulls(key, injectableClass);
        injector.put(key, injectableClass);
        return self();
    }

    @Override
    public BUILDER withLogging(LogType logType) {
        switch (logType) {
            case STREAM: {
                LoggerManager.getLogger(AsynchronousQueryResultImpl.LOGGER_SELECT_NAME).setLevel(Level.DEBUG);
                break;
            }
            case PERSIST: {
                LoggerManager.getLogger(AbstractDbmsOperationHandler.LOGGER_INSERT_NAME).setLevel(Level.DEBUG);
                break;
            }
            case UPDATE: {
                LoggerManager.getLogger(AbstractDbmsOperationHandler.LOGGER_UPDATE_NAME).setLevel(Level.DEBUG);
                break;
            }
            case REMOVE: {
                LoggerManager.getLogger(AbstractDbmsOperationHandler.LOGGER_DELETE_NAME).setLevel(Level.DEBUG);
                break;
            }
            case APPLICATION_BUILDER: {
                LoggerManager.getLogger(InjectorImpl.class).setLevel(Level.DEBUG);
                LOGGER.setLevel(Level.DEBUG);
                break;
            }
            default: {
                LOGGER.warn("The log type " + logType.name() + " is not supported.");
            }
        }
        return self();
    }

    @Override
    public final APP build() {
        final Injector inj;

        try {
            inj = injector.build();
        } catch (final InstantiationException | CyclicReferenceException ex) {
            throw new SpeedmentException("Error in dependency injection.", ex);
        }

        loadAndSetProject(inj);

        printWelcomeMessage(inj);

        if (!skipValidateRuntimeConfig) {
            validateRuntimeConfig(inj);
        }
        if (!skipCheckDatabaseConnectivity) {
            checkDatabaseConnectivity(inj);
        }

        return build(inj);
    }

    /**
     * Builds the application using the specified injector.
     *
     * @param injector the injector to use
     * @return the built instance.
     */
    protected abstract APP build(Injector injector);

    /**
     * Builds up the complete Project meta data tree.
     *
     * @param injector the injector to use
     */
    protected void loadAndSetProject(Injector injector) {
        LOGGER.debug("Loading and Setting Project Configuration");
        final Project project = injector.getOrThrow(ProjectComponent.class).getProject();

        // Apply overidden item (if any) for all Documents of a given class
        withsAll.forEach(t2 -> {
            final Class clazz = t2.get0();

            @SuppressWarnings("unchecked")
            final BiConsumer consumer
                = (BiConsumer) t2.get1();

            DocumentDbUtil.traverseOver(project)
                .filter(clazz::isInstance)
                .map(Document.class::cast)
                .forEachOrdered(doc -> consumer.accept(injector, doc));
        });

        // Apply a named overidden item (if any) for all Entities of a given class
        withsNamed.forEach(t3 -> {
            final Class clazz = t3.get0();
            final String name = t3.get1();

            @SuppressWarnings("unchecked")
            final BiConsumer consumer
                = (BiConsumer) t3.get2();

            DocumentDbUtil.traverseOver(project)
                .filter(clazz::isInstance)
                .filter(HasName.class::isInstance)
                .map(HasName.class::cast)
                .filter(c -> name.equals(relativeName(c, Project.class, DATABASE_NAME)))
                .forEachOrdered(doc -> consumer.accept(injector, doc));
        });
    }

    protected void validateRuntimeConfig(Injector injector) {
        LOGGER.debug("Validating Runtime Configuration");
        final Project project = injector.getOrThrow(ProjectComponent.class).getProject();
        if (project == null) {
            throw new SpeedmentException("No project defined");
        }

        project.dbmses().forEach(d -> {
            final String typeName = d.getTypeName();
            final Optional oDbmsType = injector.getOrThrow(DbmsHandlerComponent.class).findByName(typeName);
            if (!oDbmsType.isPresent()) {
                throw new SpeedmentException("The database type " + typeName + " is not registered with the " + DbmsHandlerComponent.class.getSimpleName());
            }
            final String driverName = oDbmsType.get().getDriverName();
            try {
                // Make sure the driver is loaded. This is a must for some JavaEE servers.
                Class.forName(driverName);
            } catch (ClassNotFoundException cnfe) {
                LOGGER.error(cnfe, "The database driver class " + driverName + " is not available. Make sure to include it in your class path (e.g. in the POM file)");
            }
        });

    }

    protected void checkDatabaseConnectivity(Injector injector) {
        LOGGER.debug("Checking Database Connectivity");
        final Project project = injector.getOrThrow(ProjectComponent.class).getProject();
        project.dbmses().forEachOrdered(dbms -> {

            final DbmsHandlerComponent dbmsHandlerComponent = injector.getOrThrow(DbmsHandlerComponent.class);
            final DbmsType dbmsType = DatabaseUtil.dbmsTypeOf(dbmsHandlerComponent, dbms);
            final DbmsMetadataHandler handler = dbmsType.getMetadataHandler();

            try {
                LOGGER.info(handler.getDbmsInfoString(dbms));
            } catch (SQLException sqle) {
                throw new SpeedmentException("Unable to establish initial connection with the database named " + dbms.getName() + ".", sqle);
            }
        });
    }

    /**
     * Prints a welcome message to the output channel.
     *
     * @param injector the injector to use
     */
    protected void printWelcomeMessage(Injector injector) {

        final InfoComponent info = injector.getOrThrow(InfoComponent.class);
        final String title = info.getTitle();
        final String version = info.getImplementationVersion();

        if (!skipLogoPrintout) {
            final String speedmentMsg = "\n"
                + "   ____                   _                     _     \n"
                + "  / ___'_ __  __  __   __| |_ __ __    __ _ __ | |    \n"
                + "  \\___ | '_ |/  \\/  \\ / _  | '_ \\ _ \\ /  \\ '_ \\| |_   \n"
                + "   ___)| |_)| '_/ '_/| (_| | | | | | | '_/ | | |  _|  \n"
                + "  |____| .__|\\__\\\\__\\ \\____|_| |_| |_|\\__\\_| |_| '_   \n"
                + "=======|_|======================================\\__|==\n"
                + "   :: " + title + " by " + info.getVendor()
                + ":: (v" + version + ") \n";

            LOGGER.info(speedmentMsg);
        } 
        final String msg = title + " (" + info.getSubtitle()
            + ") version " + version
            + " by " + info.getVendor()
            + " Specification version "
            + info.getSpecificationVersion();
        LOGGER.info(msg);

        if (!info.isProductionMode()) {
            LOGGER.warn("This version is NOT INTENDED FOR PRODUCTION USE!");
        }

        try {
            final Package package_ = Runtime.class.getPackage();
            final String javaMsg = package_.getSpecificationTitle()
                + " " + package_.getSpecificationVersion()
                + " by " + package_.getSpecificationVendor()
                + ". Implementation "
                + package_.getImplementationVendor()
                + " " + package_.getImplementationVersion()
                + " by " + package_.getImplementationVendor();
            LOGGER.info(javaMsg);

            final String versionString = package_.getImplementationVersion();
            final Optional isVersionOk = isVersionOk(versionString);
            if (isVersionOk.isPresent()) {
                if (!isVersionOk.get()) {
                    LOGGER.warn("The current Java version (" + versionString + ") is outdated. Please upgrade to a more recent Java version.");
                }
            } else {
                LOGGER.warn("Unable to fully parse the java version. Version check skipped!");
            }
        } catch (final Exception ex) {
            LOGGER.info("Unknown Java version.");
        }

    }

    Optional isVersionOk(String versionString) {
        final Pattern pattern = Pattern.compile("(\\d+)[\\\\.](\\d+)[\\\\.](\\d+)_(\\d+)");
        final Matcher matcher = pattern.matcher(versionString);

        if (matcher.find() && matcher.groupCount() >= 4) {
            final String majorVersionString = matcher.group(1);
            final String minorVersionString = matcher.group(2);
            final String patchVersionString = matcher.group(3);
            final String microVersionString = matcher.group(4);
            try {
                final int majorVersion = Integer.parseInt(majorVersionString);
                final int minorVersion = Integer.parseInt(minorVersionString);
                final int patchVersion = Integer.parseInt(patchVersionString);
                final int microVersion = Integer.parseInt(microVersionString);
                boolean ok = true;
                if (majorVersion < 1) {
                    ok = false;
                }
                if (majorVersion == 1) {
                    if (minorVersion < 8) {
                        ok = false;
                    }
                    if (minorVersion == 8) {
                        if (patchVersion < 0) {
                            ok = false;
                        }
                        if (patchVersion == 0) {
                            if (microVersion < 40) {
                                ok = false;
                            }
                        }
                    }
                }
                return Optional.of(ok);
            } catch (NumberFormatException nfe) {
                return Optional.empty();
            }
        } else {
            return Optional.empty();
        }
    }

    private static  void withInjectable(Injector.Builder injector, Class injectableImplType, Function> keyExtractor) {
        requireNonNull(injectableImplType);

        final T injectable;
        try {
            final Constructor constructor = injectableImplType.getDeclaredConstructor();
            constructor.setAccessible(true);
            injectable = constructor.newInstance();
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException ex) {

            throw new SpeedmentException(ex);
        }

        final Class key = keyExtractor.apply(injectable);
        injector.put(key.getName(), injectableImplType);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy