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

Allow more flexible environment settings #146

Merged
merged 4 commits into from
Aug 20, 2019
Merged
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
144 changes: 91 additions & 53 deletions src/main/java/org/apache/ibatis/migration/Environment.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2010-2018 the original author or authors.
* Copyright 2010-2019 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.
Expand All @@ -23,9 +23,11 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.ibatis.parsing.GenericTokenParser;

public class Environment {

Expand Down Expand Up @@ -60,7 +62,7 @@ private enum SETTING_KEY {
private static final List<String> SETTING_KEYS;

static {
ArrayList<String> list = new ArrayList<String>();
ArrayList<String> list = new ArrayList<>();
SETTING_KEY[] keys = SETTING_KEY.values();
for (SETTING_KEY key : keys) {
list.add(key.name());
Expand Down Expand Up @@ -94,65 +96,101 @@ private enum SETTING_KEY {
private final String hookBeforeNew;
private final String hookAfterNew;

/**
* Prefix used to lookup environment variable or system property.
*/
private static final String PREFIX = "MIGRATIONS_";
private final Map<String, String> envVars = System.getenv();
private final Properties sysProps = System.getProperties();
private final Properties variables = new Properties();

private final GenericTokenParser parser = new GenericTokenParser("${", "}", key -> {
String value = sysProps.getProperty(key);
if (value == null) {
value = envVars.get(key);
}
if (value == null) {
value = "${" + key + "}";
}
return value;
});

public Environment(File file) {
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
Properties prop = new Properties();
prop.load(inputStream);

this.timeZone = prop.getProperty(SETTING_KEY.time_zone.name(), "GMT+0:00");
this.delimiter = prop.getProperty(SETTING_KEY.delimiter.name(), ";");
this.scriptCharset = prop.getProperty(SETTING_KEY.script_char_set.name(), Charset.defaultCharset().name());
this.fullLineDelimiter = Boolean.valueOf(prop.getProperty(SETTING_KEY.full_line_delimiter.name()));
this.sendFullScript = Boolean.valueOf(prop.getProperty(SETTING_KEY.send_full_script.name()));
this.autoCommit = Boolean.valueOf(prop.getProperty(SETTING_KEY.auto_commit.name()));
this.removeCrs = Boolean.valueOf(prop.getProperty(SETTING_KEY.remove_crs.name()));
this.ignoreWarnings = Boolean.valueOf(prop.getProperty(SETTING_KEY.ignore_warnings.name(), "true"));

this.driverPath = prop.getProperty(SETTING_KEY.driver_path.name());
this.driver = prop.getProperty(SETTING_KEY.driver.name());
this.url = prop.getProperty(SETTING_KEY.url.name());
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.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());
this.hookBeforeDown = prop.getProperty(SETTING_KEY.hook_before_down.name());
this.hookBeforeEachDown = prop.getProperty(SETTING_KEY.hook_before_each_down.name());
this.hookAfterEachDown = prop.getProperty(SETTING_KEY.hook_after_each_down.name());
this.hookAfterDown = prop.getProperty(SETTING_KEY.hook_after_down.name());

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

// User defined variables.
Set<Entry<Object, Object>> entries = prop.entrySet();
for (Entry<Object, Object> entry : entries) {
String key = (String) entry.getKey();
if (!SETTING_KEYS.contains(key)) {
variables.put(key, entry.getValue());
}
}
Properties prop = mergeProperties(file);

this.timeZone = readProperty(prop, SETTING_KEY.time_zone.name(), "GMT+0:00");
this.delimiter = readProperty(prop, SETTING_KEY.delimiter.name(), ";");
this.scriptCharset = readProperty(prop, SETTING_KEY.script_char_set.name(), Charset.defaultCharset().name());
this.fullLineDelimiter = Boolean.valueOf(readProperty(prop, SETTING_KEY.full_line_delimiter.name()));
this.sendFullScript = Boolean.valueOf(readProperty(prop, SETTING_KEY.send_full_script.name()));
this.autoCommit = Boolean.valueOf(readProperty(prop, SETTING_KEY.auto_commit.name()));
this.removeCrs = Boolean.valueOf(readProperty(prop, SETTING_KEY.remove_crs.name()));
this.ignoreWarnings = Boolean.valueOf(readProperty(prop, SETTING_KEY.ignore_warnings.name(), "true"));

this.driverPath = readProperty(prop, SETTING_KEY.driver_path.name());
this.driver = readProperty(prop, SETTING_KEY.driver.name());
this.url = readProperty(prop, SETTING_KEY.url.name());
this.username = readProperty(prop, SETTING_KEY.username.name());
this.password = readProperty(prop, SETTING_KEY.password.name());

this.hookBeforeUp = readProperty(prop, SETTING_KEY.hook_before_up.name());
this.hookBeforeEachUp = readProperty(prop, SETTING_KEY.hook_before_each_up.name());
this.hookAfterEachUp = readProperty(prop, SETTING_KEY.hook_after_each_up.name());
this.hookAfterUp = readProperty(prop, SETTING_KEY.hook_after_up.name());
this.hookBeforeDown = readProperty(prop, SETTING_KEY.hook_before_down.name());
this.hookBeforeEachDown = readProperty(prop, SETTING_KEY.hook_before_each_down.name());
this.hookAfterEachDown = readProperty(prop, SETTING_KEY.hook_after_each_down.name());
this.hookAfterDown = readProperty(prop, SETTING_KEY.hook_after_down.name());

this.hookBeforeNew = readProperty(prop, SETTING_KEY.hook_before_new.name());
this.hookAfterNew = readProperty(prop, SETTING_KEY.hook_after_new.name());

// User defined variables.
prop.entrySet().stream().filter(e -> !SETTING_KEYS.contains(e.getKey()))
.forEach(e -> variables.put(e.getKey(), parser.parse((String) e.getValue())));
}

private Properties mergeProperties(File file) {
h3adache marked this conversation as resolved.
Show resolved Hide resolved
// 1. Load from file.
Properties prop = loadPropertiesFromFile(file);
// 2. Read environment variables (existing entries are overwritten).
envVars.entrySet().stream().filter(e -> isMigrationsKey(e.getKey()))
.forEach(e -> prop.put(normalizeKey(e.getKey()), e.getValue()));
// 3. Read system properties (existing entries are overwritten).
sysProps.entrySet().stream().filter(e -> isMigrationsKey((String) e.getKey()))
.forEach(e -> prop.put(normalizeKey((String) e.getKey()), e.getValue()));
return prop;
}

private String normalizeKey(String key) {
return key.substring(PREFIX.length()).toLowerCase(Locale.ENGLISH);
}

private boolean isMigrationsKey(String key) {
return key.length() > PREFIX.length() && key.toUpperCase(Locale.ENGLISH).startsWith(PREFIX);
}

private Properties loadPropertiesFromFile(File file) {
Properties properties = new Properties();
try (FileInputStream inputStream = new FileInputStream(file)) {
properties.load(inputStream);
return properties;
} catch (FileNotFoundException e) {
throw new MigrationException("Environment file missing: " + file.getAbsolutePath());
} catch (IOException e) {
throw new MigrationException("Error loading environment properties. Cause: " + e, e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// ignore
}
}
}
}

private String readProperty(Properties properties, String propertyKey) {
return readProperty(properties, propertyKey, null);
}

private String readProperty(Properties properties, String propertyKey, String defaultValue) {
String property = properties.getProperty(propertyKey, defaultValue);
return property == null ? null : parser.parse(property);
}

public String getTimeZone() {
return timeZone;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright 2010-2019 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.system_property;

import static org.junit.Assert.*;

import java.io.File;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.migration.Migrator;
import org.apache.ibatis.migration.utils.TestUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.Assertion;
import org.junit.contrib.java.lang.system.EnvironmentVariables;
import org.junit.contrib.java.lang.system.ExpectedSystemExit;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
import org.junit.contrib.java.lang.system.SystemOutRule;

public class SystemPropertyTest {

@Rule
public final ExpectedSystemExit exit = ExpectedSystemExit.none();

@Rule
public final SystemOutRule out = new SystemOutRule().enableLog();

@Rule
public final RestoreSystemProperties restoreSysProps = new RestoreSystemProperties();

@Rule
public final EnvironmentVariables envVars = new EnvironmentVariables();

private static File dir;

@BeforeClass
public static void init() throws Exception {
dir = Resources.getResourceAsFile("org/apache/ibatis/migration/system_property/testdir");
}

@Before
public void beforeEachTest() {
exit.expectSystemExit();
exit.checkAssertionAfterwards(new Assertion() {
public void checkAssertion() {
assertEquals("", out.getLog());
}
});
out.clearLog();
}

@After
public void afterEachTest() {
out.clearLog();
System.exit(0);
}

@Test
public void testEnvironmentVariables() throws Exception {
envVars.set("MIGRATIONS_DRIVER", "org.hsqldb.jdbcDriver");
envVars.set("username", "Pocahontas");
envVars.set("var1", "Variable 1");
envVars.set("MIGRATIONS_VAR3", "Variable 3");
envVars.set("migrations_var4", "Variable 4");
envVars.set("MIGRATIONS_VAR5", "Variable 5");

assertEnvironment();
}

@Test
public void testSystemProperties() throws Exception {
System.setProperty("MIGRATIONS_DRIVER", "org.hsqldb.jdbcDriver");
System.setProperty("username", "Pocahontas");
System.setProperty("var1", "Variable 1");
System.setProperty("MIGRATIONS_VAR3", "Variable 3");
System.setProperty("migrations_var4", "Variable 4");
System.setProperty("MIGRATIONS_VAR5", "Variable 5");
// Set duplicate env vars to assert priority
envVars.set("MIGRATIONS_DRIVER", "bogus_driver");
envVars.set("MIGRATIONS_VAR3", "bogus_var3");

assertEnvironment();
}

private void assertEnvironment() {
Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "up", "1", "--trace"));

String output = out.getLog();
assertTrue(output.contains("SUCCESS"));
assertTrue(output.contains("username: Pocahontas"));
assertTrue(output.contains("var1: Variable 1"));
assertTrue(output.contains("var2: ${var2}"));
assertTrue(output.contains("var3: Variable 3"));
assertTrue(output.contains("var4: Variable 4"));
assertTrue(output.contains("var5: Variable 5"));
assertTrue(output.contains("Var5: Var5 in properties file"));

Migrator.main(TestUtil.args("--path=" + dir.getAbsolutePath(), "down", "1"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#
# Copyright 2010-2019 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.
#

driver=should_be_overwritten_by_system_property
url=jdbc:hsqldb:mem:sysprop
username=${username}
password=

changelog=CHANGELOG

var1=${var1}
var2=${var2}
var4=should be overwritten by system property
Var5=Var5 in properties file
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
--
-- Copyright 2010-2019 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.
--

-- // Create Changelog

-- Default DDL for changelog table that will keep
-- a record of the migrations that have been run.

-- You can modify this to suit your database before
-- running your first migration.

-- Be sure that ID and DESCRIPTION fields exist in
-- BigInteger and String compatible fields respectively.

CREATE TABLE ${changelog} (
ID NUMERIC(20,0) NOT NULL,
APPLIED_AT VARCHAR(25) NOT NULL,
DESCRIPTION VARCHAR(255) NOT NULL
);

ALTER TABLE ${changelog}
ADD CONSTRAINT PK_${changelog}
PRIMARY KEY (id);

SELECT 'username: ' || USER_NAME FROM INFORMATION_SCHEMA.SYSTEM_USERS;
SELECT 'var1: ' || '${var1}' FROM (VALUES(0));
SELECT 'var2: ' || '${var2}' FROM (VALUES(0));
SELECT 'var3: ' || '${var3}' FROM (VALUES(0));
SELECT 'var4: ' || '${var4}' FROM (VALUES(0));
SELECT 'var5: ' || '${var5}' FROM (VALUES(0));
SELECT 'Var5: ' || '${Var5}' FROM (VALUES(0));


-- //@UNDO

DROP TABLE ${changelog};