Skip to content

Commit

Permalink
Merge pull request #3006 from liquibase/added-duplicate-file-mode
Browse files Browse the repository at this point in the history
Added new liquibase.duplicateFileMode setting
  • Loading branch information
nvoxland committed Jun 30, 2022
2 parents 5273e47 + 9e1ce2a commit 4109106
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 28 deletions.
12 changes: 12 additions & 0 deletions liquibase-core/src/main/java/liquibase/GlobalConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public class GlobalConfiguration implements AutoloadedConfigurations {
public static final ConfigurationDefinition<Boolean> PRESERVE_SCHEMA_CASE;
public static final ConfigurationDefinition<Boolean> SHOW_BANNER;

public static final ConfigurationDefinition<DuplicateFileMode> DUPLICATE_FILE_MODE;

/**
* @deprecated No longer used
*/
Expand Down Expand Up @@ -198,5 +200,15 @@ public class GlobalConfiguration implements AutoloadedConfigurations {
.setDescription("If true, show a Liquibase banner on startup.")
.setDefaultValue(true)
.build();

DUPLICATE_FILE_MODE = builder.define("duplicateFileMode", DuplicateFileMode.class)
.setDescription("How to handle multiple files being found in the search path that have duplicate paths. Options are WARN (log warning and choose one at random) or ERROR (fail current operation)")
.setDefaultValue(DuplicateFileMode.ERROR)
.build();
}

public enum DuplicateFileMode {
WARN,
ERROR,
}
}
13 changes: 4 additions & 9 deletions liquibase-core/src/main/java/liquibase/Liquibase.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,7 @@
import liquibase.util.StringUtil;

import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Writer;
import java.io.*;
import java.text.DateFormat;
import java.util.*;
import java.util.function.Supplier;
Expand Down Expand Up @@ -972,13 +969,11 @@ protected void removeRunStatus(List<ChangeSet> changeSets, Contexts contexts, La
protected void executeRollbackScript(String rollbackScript, List<ChangeSet> changeSets, Contexts contexts, LabelExpression labelExpression) throws LiquibaseException {
final Executor executor = Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", database);
String rollbackScriptContents;
try (InputStreamList streams = resourceAccessor.openStreams(null, rollbackScript)) {
if ((streams == null) || streams.isEmpty()) {
try (InputStream stream = resourceAccessor.openStream(null, rollbackScript)) {
if (stream == null) {
throw new LiquibaseException("WARNING: The rollback script '" + rollbackScript + "' was not located. Please check your parameters. No rollback was performed");
} else if (streams.size() > 1) {
throw new LiquibaseException("Found multiple rollbackScripts named " + rollbackScript);
}
rollbackScriptContents = StreamUtil.readStreamAsString(streams.iterator().next());
rollbackScriptContents = StreamUtil.readStreamAsString(stream);
} catch (IOException e) {
throw new LiquibaseException("Error reading rollbackScript " + executor + ": " + e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package liquibase.resource;

import liquibase.AbstractExtensibleObject;
import liquibase.GlobalConfiguration;
import liquibase.Scope;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.Logger;
import liquibase.util.StringUtil;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Iterator;

/**
* Convenience base class for {@link ResourceAccessor} implementations.
Expand All @@ -20,9 +25,41 @@ public InputStream openStream(String relativeTo, String streamPath) throws IOExc
if (streamList == null || streamList.size() == 0) {
return null;
} else if (streamList.size() > 1) {
streamList.close();
Scope.getCurrentScope().getLog(getClass()).warning("ResourceAccessor roots: "+Scope.getCurrentScope().getResourceAccessor().getClass().getName());
throw new IOException("Found " + streamList.size() + " files that match " + streamPath+": "+ StringUtil.join(streamList.getURIs(), ", ", new StringUtil.ToStringFormatter()));
String message = "Found " + streamList.size() + " files with the path '" + streamPath + "':" + System.lineSeparator();
for (URI uri : streamList.getURIs()) {
message += " - " + uri.toString() + System.lineSeparator();
}
message += " Search Path: " + System.lineSeparator();
for (String location : Scope.getCurrentScope().getResourceAccessor().describeLocations()) {
message += " - " + location + System.lineSeparator();
}
message += " You can limit the search path to remove duplicates with the liquibase.searchPath setting.";

final GlobalConfiguration.DuplicateFileMode mode = GlobalConfiguration.DUPLICATE_FILE_MODE.getCurrentValue();
final Logger log = Scope.getCurrentScope().getLog(getClass());

if (mode == GlobalConfiguration.DuplicateFileMode.ERROR) {
throw new IOException(message + " Or, if you KNOW these are the exact same file you can set liquibase.duplicateFileMode=WARN.");
} else if (mode == GlobalConfiguration.DuplicateFileMode.WARN) {
final String warnMessage = message + System.lineSeparator() +
" To fail when duplicates are found, set liquibase.duplicateFileMode=ERROR" + System.lineSeparator() +
" Choosing: " + streamList.getURIs().get(0);
Scope.getCurrentScope().getUI().sendMessage(warnMessage);
log.warning(warnMessage);

InputStream returnStream = null;
final Iterator<InputStream> iterator = streamList.iterator();
while (iterator.hasNext()) {
if (returnStream == null) {
returnStream = iterator.next();
} else {
iterator.next().close();
}
}
return returnStream;
} else {
throw new UnexpectedLiquibaseException("Unexpected DuplicateFileMode: " + mode);
}
} else {
return streamList.iterator().next();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public interface ResourceAccessor {

/**
* Returns a single stream matching the given path. See {@link #openStreams(String, String)} for details about path options.
* Implementations should respect {@link liquibase.GlobalConfiguration#DUPLICATE_FILE_MODE}
*
* @param relativeTo Location that streamPath should be found relative to. If null, streamPath is an absolute path
* @return null if the resource does not exist
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ private void applyColumnParameter(PreparedStatement stmt, int i, ColumnConfig co
}
}

@SuppressWarnings("java:S2583")
private LOBContent<InputStream> toBinaryStream(String valueLobFile) throws LiquibaseException, IOException {
InputStream in = getResourceAsStream(valueLobFile);

Expand Down Expand Up @@ -376,6 +377,7 @@ private LOBContent<InputStream> toBinaryStream(String valueLobFile) throws Liqui
}
}

@SuppressWarnings("java:S2583")
private LOBContent<Reader> toCharacterStream(String valueLobFile, String encoding)
throws IOException, LiquibaseException {
InputStream in = getResourceAsStream(valueLobFile);
Expand Down Expand Up @@ -419,21 +421,10 @@ private LOBContent<Reader> toCharacterStream(String valueLobFile, String encodin
}
}

@java.lang.SuppressWarnings("squid:S2095")
@SuppressWarnings("squid:S2095")
private InputStream getResourceAsStream(String valueLobFile) throws IOException, LiquibaseException {
String fileName = getFileName(valueLobFile);
InputStreamList streams = this.resourceAccessor.openStreams(null, fileName);
if ((streams == null) || streams.isEmpty()) {
return null;
}
if (streams.size() > 1) {
for (InputStream stream : streams) {
stream.close();
}

throw new IOException(streams.size() + " matched " + valueLobFile);
}
return streams.iterator().next();
return this.resourceAccessor.openStream(null, fileName);
}

private String getFileName(String fileName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package liquibase.resource

import liquibase.GlobalConfiguration
import liquibase.Scope
import spock.lang.Specification
import spock.lang.Unroll

Expand Down Expand Up @@ -102,19 +104,31 @@ class FileSystemResourceAccessorTest extends Specification {
]
}
def "openStream throws an error if multiple files match"() {
def "openStream throws an error if multiple files match and duplicateFileMode == error"() {
when:
simpleTestAccessor.openStream(null, "com/example/everywhere/file-everywhere.txt",)
Scope.child(GlobalConfiguration.DUPLICATE_FILE_MODE.getKey(), GlobalConfiguration.DuplicateFileMode.ERROR, { ->
simpleTestAccessor.openStream(null, "com/example/everywhere/file-everywhere.txt",)
} as Scope.ScopedRunner)
then:
def e = thrown(IOException)
e.message.startsWith("Found 3 files that match com/example/everywhere/file-everywhere.txt: file:")
e.message.startsWith("Found 3 files with the path 'com/example/everywhere/file-everywhere.txt':")
e.message.contains("file-everywhere.txt")
e.message.contains("test-classes")
e.message.contains("simple-files.jar")
e.message.contains("simple-files.zip")
}
def "openStream does not throw an error if multiple files match and duplicateFileMode == warn"() {
when:
Scope.child(GlobalConfiguration.DUPLICATE_FILE_MODE.getKey(), GlobalConfiguration.DuplicateFileMode.WARN, { ->
simpleTestAccessor.openStream(null, "com/example/everywhere/file-everywhere.txt",)
} as Scope.ScopedRunner)
then:
noExceptionThrown()
}
def "openStream returns null if nothing matches"() {
expect:
simpleTestAccessor.openStream(null, "com/example/invalid.txt") == null
Expand Down

0 comments on commit 4109106

Please sign in to comment.