package com.github.mongobee; import static com.mongodb.ServerAddress.defaultHost; import static com.mongodb.ServerAddress.defaultPort; import static org.springframework.util.StringUtils.hasText; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; import org.jongo.Jongo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.env.Environment; import org.springframework.data.mongodb.core.MongoTemplate; import com.github.mongobee.changeset.ChangeEntry; import com.github.mongobee.dao.ChangeEntryDao; import com.github.mongobee.exception.MongobeeChangeSetException; import com.github.mongobee.exception.MongobeeConfigurationException; import com.github.mongobee.exception.MongobeeConnectionException; import com.github.mongobee.exception.MongobeeException; import com.github.mongobee.utils.ChangeService; import com.mongodb.DB; import com.mongodb.MongoClient; import com.mongodb.MongoClientURI; import com.mongodb.client.MongoDatabase; /** * Mongobee runner * * @author lstolowski * @since 26/07/2014 */ public class Mongobee implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(Mongobee.class); private ChangeEntryDao dao; private boolean enabled = true; private String changeLogsScanPackage; private MongoClientURI mongoClientURI; private MongoClient mongoClient; private String dbName; private Environment springEnvironment; private MongoTemplate mongoTemplate; private Jongo jongo; /** * <p>Simple constructor with default configuration of host (localhost) and port (27017). Although * <b>the database name need to be provided</b> using {@link Mongobee#setDbName(String)} setter.</p> * <p>It is recommended to use constructors with MongoURI</p> */ public Mongobee() { this(new MongoClientURI("mongodb://" + defaultHost() + ":" + defaultPort() + "/")); } /** * <p>Constructor takes db.mongodb.MongoClientURI object as a parameter. * </p><p>For more details about MongoClientURI please see com.mongodb.MongoClientURI docs * </p> * * @param mongoClientURI uri to your db * @see MongoClientURI */ public Mongobee(MongoClientURI mongoClientURI) { this.mongoClientURI = mongoClientURI; this.setDbName(mongoClientURI.getDatabase()); this.dao = new ChangeEntryDao(); } /** * <p>Constructor takes db.mongodb.MongoClient object as a parameter. * </p><p>For more details about <tt>MongoClient</tt> please see com.mongodb.MongoClient docs * </p> * * @param mongoClient database connection client * @see MongoClient */ public Mongobee(MongoClient mongoClient) { this.mongoClient = mongoClient; this.dao = new ChangeEntryDao(); } /** * <p>Mongobee runner. Correct MongoDB URI should be provided.</p> * <p>The format of the URI is: * <pre> * mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database[.collection]][?options]] * </pre> * <ul> * <li>{@code mongodb://} Required prefix</li> * <li>{@code username:password@} are optional. If given, the driver will attempt to login to a database after * connecting to a database server. For some authentication mechanisms, only the username is specified and the password is not, * in which case the ":" after the username is left off as well.</li> * <li>{@code host1} Required. It identifies a server address to connect to. More than one host can be provided.</li> * <li>{@code :portX} is optional and defaults to :27017 if not provided.</li> * <li>{@code /database} the name of the database to login to and thus is only relevant if the * {@code username:password@} syntax is used. If not specified the "admin" database will be used by default. * <b>Mongobee will operate on the database provided here or on the database overriden by setter setDbName(String).</b> * </li> * <li>{@code ?options} are connection options. For list of options please see com.mongodb.MongoClientURI docs</li> * </ul> * <p>For details, please see com.mongodb.MongoClientURI * * @param mongoURI with correct format * @see com.mongodb.MongoClientURI */ public Mongobee(String mongoURI) { this(new MongoClientURI(mongoURI)); } /** * For Spring users: executing mongobee after bean is created in the Spring context * * @throws Exception exception */ @Override public void afterPropertiesSet() throws Exception { execute(); } /** * Executing migration * * @throws MongobeeException exception */ public void execute() throws MongobeeException { if (!isEnabled()) { logger.info("Mongobee is disabled. Exiting."); return; } validateConfig(); if (this.mongoClient != null) { dao.connectMongoDb(this.mongoClient, dbName); } else { dao.connectMongoDb(this.mongoClientURI, dbName); } if (!dao.acquireProcessLock()) { logger.info("Mongobee did not aqcuire process lock. Exiting."); return; } logger.info("Mongobee aqcuired process lock, starting the data migration sequence.."); try { executeMigration(); } finally { logger.info("Mongobee is releasing process lock."); dao.releaseProcessLock(); } logger.info("Mongobee has finished his job."); } private void executeMigration() throws MongobeeConnectionException, MongobeeException { ChangeService service = new ChangeService(changeLogsScanPackage, springEnvironment); for (Class<?> changelogClass : service.fetchChangeLogs()) { Object changelogInstance = null; try { changelogInstance = changelogClass.getConstructor().newInstance(); List<Method> changesetMethods = service.fetchChangeSets(changelogInstance.getClass()); for (Method changesetMethod : changesetMethods) { ChangeEntry changeEntry = service.createChangeEntry(changesetMethod); try { if (dao.isNewChange(changeEntry)) { executeChangeSetMethod(changesetMethod, changelogInstance, dao.getDb(), dao.getMongoDatabase()); dao.save(changeEntry); logger.info(changeEntry + " applied"); } else if (service.isRunAlwaysChangeSet(changesetMethod)) { executeChangeSetMethod(changesetMethod, changelogInstance, dao.getDb(), dao.getMongoDatabase()); logger.info(changeEntry + " reapplied"); } else { logger.info(changeEntry + " passed over"); } } catch (MongobeeChangeSetException e) { logger.error(e.getMessage()); } } } catch (NoSuchMethodException e) { throw new MongobeeException(e.getMessage(), e); } catch (IllegalAccessException e) { throw new MongobeeException(e.getMessage(), e); } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); throw new MongobeeException(targetException.getMessage(), e); } catch (InstantiationException e) { throw new MongobeeException(e.getMessage(), e); } } } private Object executeChangeSetMethod(Method changeSetMethod, Object changeLogInstance, DB db, MongoDatabase mongoDatabase) throws IllegalAccessException, InvocationTargetException, MongobeeChangeSetException { if (changeSetMethod.getParameterTypes().length == 1 && changeSetMethod.getParameterTypes()[0].equals(DB.class)) { logger.debug("method with DB argument"); return changeSetMethod.invoke(changeLogInstance, db); } else if (changeSetMethod.getParameterTypes().length == 1 && changeSetMethod.getParameterTypes()[0].equals(Jongo.class)) { logger.debug("method with Jongo argument"); return changeSetMethod.invoke(changeLogInstance, jongo != null ? jongo : new Jongo(db)); } else if (changeSetMethod.getParameterTypes().length == 1 && changeSetMethod.getParameterTypes()[0].equals(MongoTemplate.class)) { logger.debug("method with MongoTemplate argument"); return changeSetMethod.invoke(changeLogInstance, mongoTemplate != null ? mongoTemplate : new MongoTemplate(db.getMongo(), dbName)); } else if (changeSetMethod.getParameterTypes().length == 1 && changeSetMethod.getParameterTypes()[0].equals(MongoDatabase.class)) { logger.debug("method with DB argument"); return changeSetMethod.invoke(changeLogInstance, mongoDatabase); } else if (changeSetMethod.getParameterTypes().length == 0) { logger.debug("method with no params"); return changeSetMethod.invoke(changeLogInstance); } else { throw new MongobeeChangeSetException("ChangeSet method " + changeSetMethod.getName() + " has wrong arguments list. Please see docs for more info!"); } } private void validateConfig() throws MongobeeConfigurationException { if (!hasText(dbName)) { throw new MongobeeConfigurationException("DB name is not set. It should be defined in MongoDB URI or via setter"); } if (!hasText(changeLogsScanPackage)) { throw new MongobeeConfigurationException("Scan package for changelogs is not set: use appropriate setter"); } } /** * @return true if an execution is in progress, in any process. * @throws MongobeeConnectionException exception */ public boolean isExecutionInProgress() throws MongobeeConnectionException { return dao.isProccessLockHeld(); } /** * Used DB name should be set here or via MongoDB URI (in a constructor) * * @param dbName database name * @return Mongobee object for fluent interface */ public Mongobee setDbName(String dbName) { this.dbName = dbName; return this; } /** * Sets uri to MongoDB * * @param mongoClientURI object with defined mongo uri * @return Mongobee object for fluent interface */ public Mongobee setMongoClientURI(MongoClientURI mongoClientURI) { this.mongoClientURI = mongoClientURI; return this; } /** * Package name where @ChangeLog-annotated classes are kept. * * @param changeLogsScanPackage package where your changelogs are * @return Mongobee object for fluent interface */ public Mongobee setChangeLogsScanPackage(String changeLogsScanPackage) { this.changeLogsScanPackage = changeLogsScanPackage; return this; } /** * @return true if Mongobee runner is enabled and able to run, otherwise false */ public boolean isEnabled() { return enabled; } /** * Feature which enables/disables Mongobee runner execution * * @param enabled MOngobee will run only if this option is set to true * @return Mongobee object for fluent interface */ public Mongobee setEnabled(boolean enabled) { this.enabled = enabled; return this; } /** * Set Environment object for Spring Profiles (@Profile) integration * * @param environment org.springframework.core.env.Environment object to inject * @return Mongobee object for fluent interface */ public Mongobee setSpringEnvironment(Environment environment) { this.springEnvironment = environment; return this; } /** * Sets pre-configured {@link MongoTemplate} instance to use by the Mongobee * * @param mongoTemplate instance of the {@link MongoTemplate} * @return Mongobee object for fluent interface */ public Mongobee setMongoTemplate(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; return this; } /** * Sets pre-configured {@link MongoTemplate} instance to use by the Mongobee * * @param jongo {@link Jongo} instance * @return Mongobee object for fluent interface */ public Mongobee setJongo(Jongo jongo) { this.jongo = jongo; return this; } /** * Closes the Mongo instance used by Mongobee. * This will close either the connection Mongobee was initiated with or that which was internally created. */ public void close() { dao.close(); } }