Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New hooks (issue 106) #113

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/main/java/org/apache/ibatis/migration/Change.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ public Change(BigDecimal id, String appliedTimestamp, String description) {
this.description = description;
}

public Change(BigDecimal id, String appliedTimestamp, String description, String filename) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this so I could have a single constructor that takes all params and not require a setter to add the the filename (I think that was the field missing)

this.id = id;
this.appliedTimestamp = appliedTimestamp;
this.description = description;
this.filename = filename;
}

public BigDecimal getId() {
return id;
}
Expand Down Expand Up @@ -71,7 +78,8 @@ public void setFilename(String filename) {

@Override
public String toString() {
return id + " " + (appliedTimestamp == null ? " ...pending... " : appliedTimestamp) + " " + description;
String ts = appliedTimestamp == null ? " ...pending... " : appliedTimestamp;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was just easier for me to read

return String.format("%s %s %s %s", id, ts, description, filename);
}

@Override
Expand Down
19 changes: 17 additions & 2 deletions src/main/java/org/apache/ibatis/migration/Environment.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ private enum SETTING_KEY {
hook_before_down,
hook_before_each_down,
hook_after_each_down,
hook_after_down
hook_after_down,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follows the current design for hooks. Nothing new here just adding code to support "new"

hook_before_new,
hook_after_new
}

private static final List<String> SETTING_KEYS;
Expand Down Expand Up @@ -88,6 +90,8 @@ private enum SETTING_KEY {
private final String hookBeforeEachDown;
private final String hookAfterEachDown;
private final String hookAfterDown;
private final String hookBeforeNew;
private final String hookAfterNew;

private final Properties variables = new Properties();

Expand All @@ -113,7 +117,9 @@ public Environment(File file) {
this.username = prop.getProperty(SETTING_KEY.username.name());
this.password = prop.getProperty(SETTING_KEY.password.name());

this.hookBeforeUp = prop.getProperty(SETTING_KEY.hook_before_up.name());
this.hookBeforeNew = prop.getProperty(SETTING_KEY.hook_before_new.name());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follows current design. Importantly, there is no such thing as a before_each or after_each in the case of a new. So, I don't have analogs here for up/down

this.hookAfterNew = prop.getProperty(SETTING_KEY.hook_after_new.name());

this.hookBeforeEachUp = prop.getProperty(SETTING_KEY.hook_before_each_up.name());
this.hookAfterEachUp = prop.getProperty(SETTING_KEY.hook_after_each_up.name());
this.hookAfterUp = prop.getProperty(SETTING_KEY.hook_after_up.name());
Expand All @@ -122,6 +128,7 @@ public Environment(File file) {
this.hookAfterEachDown = prop.getProperty(SETTING_KEY.hook_after_each_down.name());
this.hookAfterDown = prop.getProperty(SETTING_KEY.hook_after_down.name());

this.hookBeforeUp = prop.getProperty(SETTING_KEY.hook_before_up.name());
// User defined variables.
Set<Entry<Object, Object>> entries = prop.entrySet();
for (Entry<Object, Object> entry : entries) {
Expand Down Expand Up @@ -197,6 +204,14 @@ public String getPassword() {
return password;
}

public String getBeforeNewHook() {
return hookBeforeNew;
}

public String getAfterNewHook() {
return hookAfterNew;
}

public String getHookBeforeUp() {
return hookBeforeUp;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.apache.ibatis.migration.utils.Util.file;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
Expand All @@ -35,6 +36,7 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.TimeZone;
Expand All @@ -49,10 +51,13 @@
import org.apache.ibatis.migration.FileMigrationLoaderFactory;
import org.apache.ibatis.migration.MigrationException;
import org.apache.ibatis.migration.MigrationLoader;
import org.apache.ibatis.migration.hook.FileHookScriptFactory;
import org.apache.ibatis.migration.hook.BasicHook;
import org.apache.ibatis.migration.hook.Hook;
import org.apache.ibatis.migration.hook.scripts.FileHookScriptFactory;
import org.apache.ibatis.migration.hook.FileMigrationHook;
import org.apache.ibatis.migration.hook.HookScriptFactory;
import org.apache.ibatis.migration.hook.scripts.HookScriptFactory;
import org.apache.ibatis.migration.hook.MigrationHook;
import org.apache.ibatis.migration.hook.NoOpHook;
import org.apache.ibatis.migration.io.ExternalResources;
import org.apache.ibatis.migration.options.DatabaseOperationOption;
import org.apache.ibatis.migration.options.Options;
Expand Down Expand Up @@ -106,6 +111,23 @@ public void setPrintStream(PrintStream aPrintStream) {
printStream = aPrintStream;
}

/**
* Use this to define template variables
* @return combined properties of system/environment with 'sys.' and 'env.' appended respectively
*/
protected Properties getVariables() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The templating system for new only had description available which was really limited. In the case I have in mine I need some environment variables. This falls in line with typical template substitutions. So, in your new script you can do this: insert into ${env.USER} this was previously not available. Because I put it in BaseCommand it is available to all commands now to integrate.

Properties variables = environmentProperties();
for (Entry<Object, Object> sys : System.getProperties().entrySet()) {
if (sys.getValue() != null)
variables.put("sys." + sys.getKey(), sys.getValue());
}
for (Entry<String, String> env : System.getenv().entrySet()) {
if (env.getValue() != null)
variables.put("env." + env.getKey(), env.getValue());
}
return variables;
}

protected boolean paramsEmpty(String... params) {
return params == null || params.length < 1 || params[0] == null || params[0].length() < 1;
}
Expand Down Expand Up @@ -152,7 +174,7 @@ private String generatePatternedId(String pattern) {
}
}

private String generateTimestampId() {
protected String generateTimestampId() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed so that the NewCommand subclass could gain access to a timestamp formatting that was already in existence. I was trying to maintain some kind of consistency here since since timestamp is a String and not a Date or something more appropriate.

final SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
final Date now = new Date();
dateFormat.setTimeZone(TimeZone.getTimeZone(environment().getTimeZone()));
Expand Down Expand Up @@ -238,6 +260,17 @@ protected File existingEnvironmentFile() {
return envFile;
}

protected Properties environmentProperties() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a convenience override.

File envFile = existingEnvironmentFile();
Properties props = new Properties();
try {
props.load(new FileInputStream(envFile));
} catch (IOException e) {
throw new MigrationException("Failed to load environment file " + envFile.getAbsolutePath(), e);
}
return props;
}

protected Environment environment() {
if (environment != null) {
return environment;
Expand Down Expand Up @@ -315,6 +348,15 @@ protected MigrationLoader getMigrationLoader() {
: new FileMigrationLoader(paths.getScriptPath(), env.getScriptCharset(), env.getVariables());
}

protected Hook createNewMigrationHook() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following previous code pattern.

String before = environment().getBeforeNewHook();
String after = environment().getAfterNewHook();
if (before == null && after == null) {
return NoOpHook.getInstance();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the Null Object Pattern so that null checks don't need to be carried everywhere.

}
return createBasicHook(before, after);
}

protected MigrationHook createUpHook() {
String before = environment().getHookBeforeUp();
String beforeEach = environment().getHookBeforeEachUp();
Expand Down Expand Up @@ -343,6 +385,11 @@ protected MigrationHook createFileMigrationHook(String before, String beforeEach
factory.create(after));
}

protected BasicHook createBasicHook(String before, String after) {
HookScriptFactory factory = new FileHookScriptFactory(options.getPaths(), environment(), printStream);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following previous patterns

return new BasicHook(factory.create(before), factory.create(after));
}

protected DatabaseOperationOption getDatabaseOperationOption() {
DatabaseOperationOption option = new DatabaseOperationOption();
option.setChangelogTable(changelogTable());
Expand All @@ -356,4 +403,5 @@ protected DatabaseOperationOption getDatabaseOperationOption() {
option.setDelimiter(environment().getDelimiter());
return option;
}

}
109 changes: 85 additions & 24 deletions src/main/java/org/apache/ibatis/migration/commands/NewCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@
*/
package org.apache.ibatis.migration.commands;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.migration.Change;
import org.apache.ibatis.migration.MigrationException;
import org.apache.ibatis.migration.hook.Hook;
import org.apache.ibatis.migration.options.SelectedOptions;
import org.apache.ibatis.migration.utils.Util;

public final class NewCommand extends BaseCommand {

Expand All @@ -35,35 +44,87 @@ public void execute(String... params) {
if (paramsEmpty(params)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, I really had to overhaul this function. In a nutshell, I did the following:

  1. Normalized types. At the end of all this, a Reader is required so a template can be applied. I did that by functionalizing it and still honoring options and MIGRATIONS_HOME
  2. Functionalized the bind paramaters so it would be crystal clear what was going into it. As a bonus, I also include the extra args supplied to the commandline so that new description blab foo baz could capture description blab foo baz. These get passed along and allow the new hook to operate based on commandline args.
  3. I wanted it to be dead obvious when hook.before and hook.after were being called and that they were literally before and after the template creation (which is what the original code did)

throw new MigrationException("No description specified for new migration.");
}
String description = params[0];
Properties variables = new Properties();
variables.setProperty("description", description);
existingEnvironmentFile();
String filename = getNextIDAsString() + "_" + description.replace(' ', '_') + ".sql";

if (options.getTemplate() != null) {
copyExternalResourceTo(options.getTemplate(), Util.file(paths.getScriptPath(), filename), variables);
} else {

Hook hook = createNewMigrationHook();

Properties variables = getVariables();
variables.setProperty("description", params[0]);


Map<String, Object> hookBindings = createBinding(params);
{

Reader templateReader = getTemplateReader();
Change change = (Change) hookBindings.get("change");
hook.before(hookBindings);
File changeFile = new File(change.getFilename());
try {
String customConfiguredTemplate = getPropertyOption(CUSTOM_NEW_COMMAND_TEMPLATE_PROPERTY);
if (customConfiguredTemplate != null) {
copyExternalResourceTo(migrationsHome() + "/" + customConfiguredTemplate,
Util.file(paths.getScriptPath(), filename), variables);
} else {
copyDefaultTemplate(variables, filename);
}
} catch (FileNotFoundException e) {
printStream
.append("Your migrations configuration did not find your custom template. Using the default template.");
copyDefaultTemplate(variables, filename);
copyTemplate(templateReader, changeFile, variables);
} catch (IOException e) {
throw new MigrationException("Unable to create template file " + changeFile.getAbsolutePath());
}

hook.after(hookBindings);
}
printStream.println("Done!");
printStream.println();

}

private Reader getTemplateReader() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole function is meant to normalize finding the reader for the template so that the calling code can seperate the what from the how what: I need to get a template. How: by checking a whole lot of config

String def = "org/apache/ibatis/migration/template_migration.sql";
Reader templateReader = null;
try {
templateReader = Resources.getResourceAsReader(def);
} catch (IOException e) {
throw new MigrationException(String.format("Default template %s can't be found?! ", def), e);
}
try {
String template = getTemplateFile();
if (template != null)
templateReader = new FileReader(template);

} catch (FileNotFoundException e) {
String msg = String.format(
"Your migrations configuration did not find your custom template: %s. Using the default template.",
e.getMessage());
printStream.append(msg);
}
return templateReader;
}

private String getTemplateFile() throws FileNotFoundException {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finds the location of the template file based on previous logic. This just functionalizes it for clarity

String template = null;
if (options.getTemplate() != null) {
template = options.getTemplate();
} else {
String customConfiguredTemplate = getPropertyOption(CUSTOM_NEW_COMMAND_TEMPLATE_PROPERTY);
if (customConfiguredTemplate != null) {
template = migrationsHome() + "/" + customConfiguredTemplate;
}
}
return template;
}

private void copyDefaultTemplate(Properties variables, String filename) {
copyResourceTo("org/apache/ibatis/migration/template_migration.sql", Util.file(paths.getScriptPath(), filename),
variables);
private Map<String, Object> createBinding(String[] params) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functionalizes the creation of all binding properties for clarity sake. By functionalizing it the code essentially self documents.

String nextId = getNextIDAsString();
String description = params[0];

String proposedFile = nextId + "_" + description.replace(' ', '_') + ".sql";

Map<String, Object> hookBindings = new HashMap<String, Object>();
BigDecimal id = new BigDecimal(nextId);

File f = new File(String.format("%s%s%s", paths.getScriptPath(), File.separator, proposedFile));
Change change = new Change(id, null, description, f.getAbsolutePath());
Properties props = environmentProperties();

hookBindings.put("change", change);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new hooks pass the change, environment, paths and extra CLI args

hookBindings.put("paths", paths);
hookBindings.put("environment", props);
hookBindings.put("args", Arrays.asList(params));
return hookBindings;
}

}
57 changes: 57 additions & 0 deletions src/main/java/org/apache/ibatis/migration/hook/BasicHook.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright 2010-2018 the original author or authors.
*
* 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 org.apache.ibatis.migration.hook;

import java.util.Map;
import org.apache.ibatis.migration.hook.scripts.HookScript;
import org.apache.ibatis.migration.hook.scripts.NoOpHookScript;

/**
* @author cbongiorno on 12/29/17.
*/
public class BasicHook implements Hook {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply the base implementation of a hook. There is only before/after

protected static final NoOpHookScript NO_OP = NoOpHookScript.getInstance();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null Object Pattern. I am pretty active in design patterns :)


private final HookScript beforeScript;
private final HookScript afterScript;

public BasicHook() {
this(NO_OP, NO_OP);
}

public BasicHook(HookScript beforeScript, HookScript afterScript) {
this.beforeScript = beforeScript == null ? NO_OP : beforeScript;
this.afterScript = afterScript == null ? NO_OP : afterScript;
}

@Override
public void before(Map<String, Object> bindingMap) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I debated having Hook.before and Hook.after return values. This would allow for immutable execution like:

Change c = newHook.before(params); // instead of modifying change inside the binding

Testing that now.

beforeScript.execute(bindingMap);
}

@Override
public void after(Map<String, Object> bindingMap) {
afterScript.execute(bindingMap);
}

public HookScript getBeforeScript() {
return beforeScript;
}

public HookScript getAfterScript() {
return afterScript;
}
}
Loading