/*
* 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;
import static org.unitils.util.FileUtils.getUrl;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.unitils.core.UnitilsException;
import org.unitils.dbmaintainer.script.ScriptContentHandle.UrlScriptContentHandle;
import org.unitils.dbmaintainer.version.Version;
/**
* A class representing a script file and it's content.
*
* @author Filip Neven
* @author Tim Ducheyne
*/
public class Script implements Comparable<Script> {
/* The name of the script */
private String fileName;
/* The version of the script */
private Version version;
/* Timestamp that indicates when the file was last modified */
private Long fileLastModifiedAt;
/* Checksum calculated on the script contents */
private String checkSum;
/* The handle to the content of the script */
private ScriptContentHandle scriptContentHandle;
/**
* Creates a script with the given script fileName, whose content is provided by the given handle.
*
* @param fileName The name of the script file, not null
* @param fileLastModifiedAt
* @param scriptContentHandle Handle providing access to the contents of the script, not null
*/
public Script(String fileName, Long fileLastModifiedAt, ScriptContentHandle scriptContentHandle) {
this.fileName = fileName;
this.version = getVersionFromPath(fileName);
this.fileLastModifiedAt = fileLastModifiedAt;
this.scriptContentHandle = scriptContentHandle;
}
/**
* Creates a script with the given fileName and content checksum. The contents of the scripts itself
* are unknown, which makes a script that is created this way unsuitable for being executed. The reason
* that we provide this constructor is to be able to store information of the script without having to
* store it's contents. The availability of a checksum enables us to find out whether it's contents
* are equal to another script objects whose contents are provided.
*
* @param fileName The name of the script file, not null
* @param fileLastModifiedAt
* @param checkSum Checksum calculated for the content of the script
*/
public Script(String fileName, Long fileLastModifiedAt, String checkSum) {
this.fileName = fileName;
this.version = getVersionFromPath(fileName);
this.fileLastModifiedAt = fileLastModifiedAt;
this.checkSum = checkSum;
}
/**
* @return The script name, not null
*/
public String getFileName() {
return fileName;
}
/**
* @return The timestamp at which the file in which this script is stored on the filesystem was last
* modified. Be careful: This can also be the timestamp on which this file was retrieved from the sourcde
* control system. If the timestamp wasn't changed, we're almost 100% sure that this file has not been modified. If
* changed, this file is possibly modified (but the reason might also be that a fresh checkout has been made
* from the version control system, or that the script was applied from another workstation or from another copy
* of the project), not null
*/
public Long getFileLastModifiedAt() {
return fileLastModifiedAt;
}
/**
* @return Checksum calculated for the content of the script, not null
*/
public String getCheckSum() {
if (checkSum == null) {
checkSum = scriptContentHandle.getCheckSum();
}
return checkSum;
}
/**
* @return The version, not null
*/
public Version getVersion() {
return version;
}
/**
* @return Handle that provides access to the content of the script. May be null! If so, this
* object is not suitable for being executed. The checksum however cannot be null, so we can always
* verify if the contents of the script are equal to another one.
*/
public ScriptContentHandle getScriptContentHandle() {
if (scriptContentHandle == null) {
throw new UnitilsException("Script content is not available");
}
return scriptContentHandle;
}
/**
* @param other Another script, not null
* @param useLastModificationDates If true, this method first checks if the lastModifiedAt property of
* this Script is equal to the given one. If equal, we assume that the contents are also equal and we don't
* compare the checksums. If not equal, we compare the checksums to find out whether there is a difference.
* By setting this value to true, performance is heavily improved when you check for updates regularly from
* the same workstation (which is the case when you use unitils's automatic database maintenance for testing).
* This is because, to calculate a checksum, the script contents have to be read. This can take a few seconds
* to complete, which we want to avoid since a check for database updates is started every time a test is launched
* that accesses the test database.
* For applying changes to an environment that can only be updated incrementally (e.g. a database use by testers
* or even the production database), this parameter should be false, since working with last modification dates
* is not guaranteed to be 100% bulletproof (although unlikely, it is possible that a different version of
* the same file is checked out on different systems on exactly the same time).
*
* @return True if the contents of this script are equal to the given one, false otherwise
*/
public boolean isScriptContentEqualTo(Script other, boolean useLastModificationDates) {
if (useLastModificationDates && this.getFileLastModifiedAt().equals(other.getFileLastModifiedAt())) {
return true;
}
return this.getCheckSum().equals(other.getCheckSum());
}
/**
* @return True if this is an incremental script, i.e. it needs to be executed in the correct order, and it can
* be executed only once. If an incremental script is changed, the database needs to be recreated from scratch,
* or an error must be reported.
*/
public boolean isIncremental() {
return version.getScriptIndex() != null;
}
/**
* Compares the given script to this script by comparing the versions. Can be used to define
* the proper execution sequence of the scripts.
*
* @param script The other script, not null
* @return -1 when this script has a smaller version, 0 if equal, 1 when larger
*/
public int compareTo(Script script) {
return version.compareTo(script.getVersion());
}
/**
* Creates a version for the given script file. The index -1 is used for files that do not have a version in the
* file name.
*
* @param parentIndexes The indexes of the parent folders, not null
* @param scriptFile The script file, not null
* @return The version of the script file, not null
*/
protected Version createVersion(List<Long> parentIndexes, File scriptFile) {
List<Long> indexes = new ArrayList<Long>();
indexes.addAll(parentIndexes);
indexes.add(extractIndex(scriptFile.getName()));
return new Version(indexes);
}
protected Version getVersionFromPath(String relativePath) {
String[] pathParts = StringUtils.split(relativePath, '/');
List<Long> versionIndexes = new ArrayList<Long>();
for (String pathPart : pathParts) {
versionIndexes.add(extractIndex(pathPart));
}
return new Version(versionIndexes);
}
/**
* Extracts the index part out of a given file name.
*
* @param pathPart The simple (only one part of path) directory or file name, not null
* @return The index, null if there is no index
*/
protected Long extractIndex(String pathPart) {
if (StringUtils.contains(pathPart, "_")) {
try {
return Long.parseLong(StringUtils.substringBefore(pathPart, "_"));
} catch (NumberFormatException e) {
// ignore
}
}
return null;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((fileName == null) ? 0 : fileName.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Script other = (Script) obj;
if (fileName == null) {
if (other.fileName != null)
return false;
} else if (!fileName.equals(other.fileName))
return false;
return true;
}
/**
* Gets a string representation of this script.
*
* @return The name and version, not null
*/
@Override
public String toString() {
return fileName;
}
}