/*
* Copyright 2008, Unitils.org
*
* 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.unitils.dbmaintainer.script.impl;
import static org.unitils.util.PropertyUtils.getStringList;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.unitils.core.UnitilsException;
import org.unitils.core.util.BaseConfigurable;
import org.unitils.dbmaintainer.script.ExecutedScript;
import org.unitils.dbmaintainer.script.Script;
import org.unitils.dbmaintainer.script.ScriptContentHandle;
import org.unitils.dbmaintainer.script.ScriptSource;
import org.unitils.dbmaintainer.version.Version;
import org.unitils.util.FileUtils;
import org.unitils.util.PropertyUtils;
/**
* Implementation of {@link ScriptSource} that reads script files from the filesystem. <p/> Script
* files should be located in the directory configured by {@link #PROPKEY_SCRIPT_LOCATIONS}.
* Valid script files start with a version number followed by an underscore, and end with the
* extension configured by {@link #PROPKEY_SCRIPT_EXTENSIONS}.
*
* @author Filip Neven
* @author Tim Ducheyne
*/
public class DefaultScriptSource extends BaseConfigurable implements ScriptSource {
/* Logger instance for this class */
private static final Log logger = LogFactory.getLog(DefaultScriptSource.class);
/**
* Property key for the directory in which the script files are located
*/
public static final String PROPKEY_SCRIPT_LOCATIONS = "dbMaintainer.script.locations";
/**
* Property key for the extension of the script files
*/
public static final String PROPKEY_SCRIPT_EXTENSIONS = "dbMaintainer.script.fileExtensions";
/**
* Property key for the directory in which the code script files are located
*/
public static final String PROPKEY_POSTPROCESSINGSCRIPT_DIRNAME = "dbMaintainer.postProcessingScript.directoryName";
public static final String PROPKEY_USESCRIPTFILELASTMODIFICATIONDATES = "dbMaintainer.useScriptFileLastModificationDates.enabled";
public static final String PROPKEY_EXCLUDE_QUALIFIERS = "dbMaintainer.excludedQualifiers";
public static final String PROPKEY_INCLUDE_QUALIFIERS = "dbMaintainer.includedQualifiers";
public static final String PROPKEY_QUALIFIERS = "dbMaintainer.qualifiers";
protected List<Script> allUpdateScripts, allPostProcessingScripts;
/**
* Gets a list of all available update scripts. These scripts can be used to completely recreate the
* database from scratch, not null.
* <p/>
* The scripts are returned in the order in which they should be executed.
*
* @return all available database update scripts, not null
*/
public List<Script> getAllUpdateScripts(String dialect, String databaseName, boolean defaultDatabase) {
if (allUpdateScripts == null) {
loadAndOrganizeAllScripts(dialect, databaseName, defaultDatabase);
}
return allUpdateScripts;
}
/**
* @return All scripts that are incremental, i.e. non-repeatable, i.e. whose file name starts with an index
*/
protected List<Script> getIncrementalScripts(String dialect, String databaseName, boolean defaultDatabase) {
List<Script> scripts = getAllUpdateScripts(dialect, databaseName, defaultDatabase);
List<Script> indexedScripts = new ArrayList<Script>();
for (Script script : scripts) {
if (script.isIncremental()) {
indexedScripts.add(script);
}
}
return indexedScripts;
}
/**
* Asserts that, in the given list of database update scripts, there are no two indexed scripts with the same version.
*
* @param scripts The list of scripts, must be sorted by version
*/
protected void assertNoDuplicateIndexes(List<Script> scripts) {
for (int i = 0; i < scripts.size() - 1; i++) {
Script script1 = scripts.get(i);
Script script2 = scripts.get(i + 1);
if (script1.isIncremental() && script2.isIncremental() && script1.getVersion().equals(script2.getVersion())) {
throw new UnitilsException("Found 2 database scripts with the same version index: "
+ script1.getFileName() + " and " + script2.getFileName() + " both have version index "
+ script1.getVersion().getIndexesString());
}
}
}
/**
* Returns a list of scripts with a higher version or whose contents were changed.
* <p/>
* The scripts are returned in the order in which they should be executed.
*
* @param currentVersion The start version, not null
* @return The scripts that have a higher index of timestamp than the start version, not null.
*/
public List<Script> getNewScripts(Version currentVersion, Set<ExecutedScript> alreadyExecutedScripts, String dialect, String databaseName, boolean defaultDatabase) {
Map<String, Script> alreadyExecutedScriptMap = convertToScriptNameScriptMap(alreadyExecutedScripts);
List<Script> result = new ArrayList<Script>();
List<Script> allScripts = getAllUpdateScripts(dialect, databaseName, defaultDatabase);
for (Script script : allScripts) {
Script alreadyExecutedScript = alreadyExecutedScriptMap.get(script.getFileName());
// If the script is indexed and the version is higher than the highest one currently applied to the database,
// add it to the list.
if (script.isIncremental() && script.getVersion().compareTo(currentVersion) > 0) {
result.add(script);
continue;
}
// Add the script if it's not indexed and if it wasn't yet executed
if (!script.isIncremental() && alreadyExecutedScript == null) {
result.add(script);
continue;
}
// Add the script if it's not indexed and if it's contents have changed
if (!script.isIncremental() && !alreadyExecutedScript.isScriptContentEqualTo(script, useScriptFileLastModificationDates())) {
logger.info("Contents of script " + script.getFileName() + " have changed since the last database update: "
+ script.getCheckSum());
result.add(script);
}
}
return result;
}
/**
* Returns true if one or more scripts that have a version index equal to or lower than
* the index specified by the given version object has been modified since the timestamp specfied by
* the given version.
*
* @param currentVersion The current database version, not null
* @return True if an existing script has been modified, false otherwise
*/
public boolean isExistingIndexedScriptModified(Version currentVersion, Set<ExecutedScript> alreadyExecutedScripts, String dialect, String databaseName, boolean defaultDatabase) {
Map<String, Script> alreadyExecutedScriptMap = convertToScriptNameScriptMap(alreadyExecutedScripts);
List<Script> incrementalScripts = getIncrementalScripts(dialect, databaseName, defaultDatabase);
// Search for indexed scripts that have been executed but don't appear in the current indexed scripts anymore
for (ExecutedScript alreadyExecutedScript : alreadyExecutedScripts) {
if (alreadyExecutedScript.getScript().isIncremental() && Collections.binarySearch(incrementalScripts, alreadyExecutedScript.getScript()) < 0) {
logger.warn("Existing indexed script found that was executed, which has been removed: " + alreadyExecutedScript.getScript().getFileName());
return true;
}
}
// Search for indexed scripts whose version < the current version, which are new or whose contents have changed
for (Script indexedScript : incrementalScripts) {
if (indexedScript.getVersion().compareTo(currentVersion) <= 0) {
Script alreadyExecutedScript = alreadyExecutedScriptMap.get(indexedScript.getFileName());
if (alreadyExecutedScript == null) {
logger.warn("New index script has been added, with at least one already executed script having an higher index." + indexedScript.getFileName());
return true;
}
if (!alreadyExecutedScript.isScriptContentEqualTo(indexedScript, useScriptFileLastModificationDates())) {
logger.warn("Script found of which the contents have changed: " + indexedScript.getFileName());
return true;
}
}
}
return false;
}
protected boolean useScriptFileLastModificationDates() {
return PropertyUtils.getBoolean(PROPKEY_USESCRIPTFILELASTMODIFICATIONDATES, configuration);
}
/**
* Gets the configured post-processing script files and verfies that they on the file system. If one of them
* doesn't exist or is not a file, an exception is thrown.
*
* @return All the postprocessing code scripts, not null
*/
public List<Script> getPostProcessingScripts(String dialect, String databaseName, boolean defaultDatabase) {
if (allPostProcessingScripts == null) {
loadAndOrganizeAllScripts(dialect, databaseName, defaultDatabase);
}
return allPostProcessingScripts;
}
/**
* Loads all scripts and organizes them: Splits them into update and postprocessing scripts, sorts
* them in their execution order, and makes sure there are no 2 update or postprocessing scripts with
* the same index.
*/
protected void loadAndOrganizeAllScripts(String dialect, String databaseName, boolean defaultDatabase) {
List<Script> allScripts = loadAllScripts(dialect, databaseName, defaultDatabase);
allUpdateScripts = new ArrayList<Script>();
allPostProcessingScripts = new ArrayList<Script>();
for (Script script : allScripts) {
if (isPostProcessingScript(script)) {
allPostProcessingScripts.add(script);
} else {
allUpdateScripts.add(script);
}
}
Collections.sort(allUpdateScripts);
assertNoDuplicateIndexes(allUpdateScripts);
Collections.sort(allPostProcessingScripts);
assertNoDuplicateIndexes(allPostProcessingScripts);
}
/**
* @return A List containing all scripts in the given script locations, not null
*/
protected List<Script> loadAllScripts(String dialect, String databaseName, boolean defaultDatabase) {
List<String> scriptLocations = PropertyUtils.getStringList(PROPKEY_SCRIPT_LOCATIONS, configuration);
List<Script> scripts = new ArrayList<Script>();
for (String scriptLocation : scriptLocations) {
if (!new File(scriptLocation).exists()) {
throw new UnitilsException("File location " + scriptLocation + " defined in property " + PROPKEY_SCRIPT_LOCATIONS + " doesn't exist");
}
getScriptsAt(scripts, scriptLocation, "", databaseName, defaultDatabase);
}
return scripts;
}
/**
* Adds all scripts available in the given directory or one of its subdirectories to the
* given List of files
*
* @param scriptLocation The current script location, not null
* @param currentParentIndexes The indexes of the current parent folders, not null
* @param scriptFiles The list to which the available script have to be added
*/
protected void getScriptsAt(List<Script> scripts, String scriptRoot, String relativeLocation, String databaseName, boolean defaultDatabase) {
File currentLocation = new File(scriptRoot + "/" + relativeLocation);
if (currentLocation.isFile() && isScriptFile(currentLocation)) {
//check databaseName
String nameFile = currentLocation.getName();
if (checkIfScriptContainsCorrectDatabaseName(nameFile, databaseName, defaultDatabase) && containsOneOfQualifiers(nameFile)) {
Script script = createScript(currentLocation, relativeLocation);
scripts.add(script);
return;
}
}
// recursively scan sub folders for script files
if (currentLocation.isDirectory()) {
for (File subLocation : currentLocation.listFiles()) {
getScriptsAt(scripts, scriptRoot, "".equals(relativeLocation) ? subLocation.getName() : relativeLocation + "/" + subLocation.getName(), databaseName, defaultDatabase);
}
}
}
/**
* This method checks if a scriptfile is a file that should be used by every schema or if the scriptfile is a file for a specific schema.
* @param nameFile
* @param databaseName
* @return {@link Boolean}
*
* @see <a href="http://www.dbmaintain.org/tutorial.html#Multi-database__user_support">more info</a>
*/
public boolean checkIfScriptContainsCorrectDatabaseName(String nameFile, String databaseName, boolean defaultDatabase) {
String temp = nameFile.toLowerCase();
if (!temp.contains("@")) {
return (defaultDatabase ? true : false);
}
return temp.matches("(.*_)*@" + databaseName.toLowerCase()+ "_.+");
}
/**
* Checks if the name of the script contains one of the qualifiers.
* @param fileName
* @return {@link Boolean}
*/
public boolean containsOneOfQualifiers(String fileName){
List<String> excludes = PropertyUtils.getStringList(PROPKEY_EXCLUDE_QUALIFIERS, configuration, false);
List<String> includes = PropertyUtils.getStringList(PROPKEY_INCLUDE_QUALIFIERS, configuration, false);
List<String> qualifiers = PropertyUtils.getStringList(PROPKEY_QUALIFIERS, configuration, false);
if (excludes.isEmpty() && includes.isEmpty() && qualifiers.isEmpty()) {
return true;
}
if (includes.isEmpty()) {
/*
* 1. The filename can be without qualifiers.
* 2. Or the qualifier must be in the list of qualifiers and not in the exclude list.
*/
return (containsQualifier(fileName, qualifiers) && !containsQualifier(fileName, excludes)) || checkIfThereAreNoQualifiers(fileName);
} else {
return containsQualifier(fileName, includes) && !containsQualifier(fileName, excludes);
}
}
protected boolean containsQualifier(String fileName, List<String> qualifiers){
for(String qualifier: qualifiers){
if(fileName.contains("#" + qualifier + "_")){
return true;
}
}
return false;
}
protected boolean checkIfThereAreNoQualifiers(String fileName) {
return !fileName.matches(".+_#\\w+_.+");
}
/**
* @param script A database script, not null
* @return True if the given script is a post processing script according to the script source configuration
*/
protected boolean isPostProcessingScript(Script script) {
List<String> startsWiths = PropertyUtils.getStringList(PROPKEY_POSTPROCESSINGSCRIPT_DIRNAME, configuration);
for (String startsWith : startsWiths) {
if (script.getFileName().startsWith(startsWith)) {
return true;
}
}
return false;
}
/**
* Indicates if the given file is a database update script file
*
* @param file The file, not null
* @return True if the given file is a database update script file
*/
protected boolean isScriptFile(File file) {
String name = file.getName();
for (String fileExtension : getScriptExtensions()) {
if (name.endsWith(fileExtension)) {
return true;
}
}
return false;
}
/**
* Creates a script object for the given script file
*
* @param scriptFile The script file, not null
* @return The script, not null
*/
protected Script createScript(File scriptFile, String relativePath) {
return new Script(relativePath, scriptFile.lastModified(), new ScriptContentHandle.UrlScriptContentHandle(FileUtils.getUrl(scriptFile)));
}
/**
* Gets the configured extensions for the script files.
*
* @return The extensions, not null
*/
protected List<String> getScriptExtensions() {
List<String> extensions = getStringList(PROPKEY_SCRIPT_EXTENSIONS, configuration);
// check whether an extension is configured
if (extensions.isEmpty()) {
logger.warn("No extensions are specificied using the property " + PROPKEY_SCRIPT_EXTENSIONS + ". The Unitils database maintainer won't do anyting");
}
// Verify the correctness of the script extensions
for (String extension : extensions) {
if (extension.startsWith(".")) {
throw new UnitilsException("DefaultScriptSource file extension defined by " + PROPKEY_SCRIPT_EXTENSIONS + " should not start with a '.'");
}
}
return extensions;
}
protected Map<String, Script> convertToScriptNameScriptMap(Set<ExecutedScript> executedScripts) {
Map<String, Script> scriptMap = new HashMap<String, Script>();
for (ExecutedScript executedScript : executedScripts) {
scriptMap.put(executedScript.getScript().getFileName(), executedScript.getScript());
}
return scriptMap;
}
}