fix #1694: Add ability to have cascading liquibase.properties files#7717
fix #1694: Add ability to have cascading liquibase.properties files#7717MichaelKern-IVV wants to merge 44 commits into
Conversation
…haelKern-IVV/liquibase into feature/cascading_properties
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughRefactors defaults-file loading to enumerate all matching resources and register a DefaultsFileValueProvider per resource with explicit precedence; InputStreamList exposes URI+InputStream entries; CLI and Maven plugin points updated; tests and helpers adjusted. ChangesMultiple Defaults Files with Precedence
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java (1)
815-825:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInclude
propertyFilein the failure message and drop the unreachable null guards in callers.
handlePropertyFileInputStreamsnow throws on missing/empty resolution, but the message"Failed to resolve the properties file: Not found"does not name the file, which is a regression vs the priorFileUtil.getFileNotFoundMessage(this.propertyFile)UX. Additionally, since this helper either returns a non-null list or throws, theif (isl == null) { throw new MojoExecutionException(FileUtil.getFileNotFoundMessage(this.propertyFile)); }blocks at lines 771-773 and 781-783 are dead code — the friendly message is wrapped inUnexpectedLiquibaseExceptionby the surrounding catch instead.🛠️ Proposed fix
private static InputStreamList handlePropertyFileInputStreams(String propertyFile) throws MojoFailureException { try { InputStreamList inputStreamList = Scope.getCurrentScope().getResourceAccessor().openStreams(null, propertyFile); if (inputStreamList == null || inputStreamList.isEmpty()) { - throw new MojoFailureException("Failed to resolve the properties file: Not found"); + throw new MojoFailureException(FileUtil.getFileNotFoundMessage(propertyFile)); } return inputStreamList; } catch (IOException e) { - throw new MojoFailureException("Failed to resolve the properties file.", e); + throw new MojoFailureException("Failed to resolve the properties file: " + propertyFile, e); } }And in
configureFieldsAndValues, remove the redundant null guards:try (InputStreamList isl = handlePropertyFileInputStreams(propertyFile)) { - if (isl == null) { - throw new MojoExecutionException(FileUtil.getFileNotFoundMessage(this.propertyFile)); - } - parsePropertiesFiles(isl); getLog().info(MavenUtils.LOG_SEPARATOR); } catch (IOException | MojoFailureException e) { throw new UnexpectedLiquibaseException(e); } try (InputStreamList isl = handlePropertyFileInputStreams(this.propertyFile)) { - if (isl == null) { - throw new MojoExecutionException(FileUtil.getFileNotFoundMessage(this.propertyFile)); - } - LiquibaseConfiguration liquibaseConfiguration = Scope.getCurrentScope().getSingleton(LiquibaseConfiguration.class);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java` around lines 815 - 825, Update handlePropertyFileInputStreams to include the resolved propertyFile name in its failure message (e.g., use FileUtil.getFileNotFoundMessage(propertyFile) or append the propertyFile variable) so the thrown MojoFailureException identifies which file failed; then remove the now-unreachable null checks that throw MojoExecutionException around calls to handlePropertyFileInputStreams in configureFieldsAndValues (the blocks that check isl == null and throw with FileUtil.getFileNotFoundMessage(this.propertyFile)), since handlePropertyFileInputStreams either returns a non-null, non-empty InputStreamList or throws.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@liquibase-cli/src/main/java/liquibase/integration/commandline/LiquibaseCommandLine.java`:
- Around line 752-761: The classpath lookup is being applied unconditionally and
opens streams without closing them; guard the
ClassLoaderResourceAccessor.getAll(defaultsFileConfigValue) loop so it only runs
if no on-disk defaults provider was already added (e.g., if returnList.isEmpty()
or by checking existing providers), and change the creation of
DefaultsFileValueProvider to use a try-with-resources around
res.openInputStream() so the InputStream is closed after constructing the
provider; then call liquibaseConfiguration.registerProvider(fileProvider) and
returnList.add(fileProvider) inside that guarded block.
In
`@liquibase-cli/src/test/groovy/liquibase/integration/commandline/MultipleDefaultsFilesOverlayEachOtherTest.groovy`:
- Around line 18-22: The test's classpath setup is non-deterministic because it
assumes the first java.class.path entry (variable classPath) contains test
resources; replace that logic by resolving the test resource URL
deterministically (e.g., using
MultipleDefaultsFilesOverlayEachOtherTest.class.getResource("/subfolder/") or
this.getClass().getResource("/subfolder/") ) and build the URLClassLoader from
that URL list instead of constructing urls from classPath[0]; keep using
contextClassLoader and Thread.currentThread().setContextClassLoader(classLoader)
but remove the reliance on System.getProperty("java.class.path") and the
classPath variable so the test consistently points to the intended test
resources.
In
`@liquibase-standard/src/main/java/liquibase/configuration/core/DefaultsFileValueProvider.java`:
- Around line 42-48: The public constructors DefaultsFileValueProvider(Map<?,?>
properties, int precedenceOffset) and DefaultsFileValueProvider(Map<?,?>
properties) accept non-string keys but later cast keys to String in validate(),
so either tighten the API or validate at copy time; change the constructor
signatures to use Map<String,?> (and update any callers) or, if signatures must
remain generic, add runtime validation in propertiesFromMap: iterate the input
Map entries, if any key is not a String throw an IllegalArgumentException with a
clear message (or coerce via toString() if acceptable), then copy entries into
Properties so validate() no longer risks a ClassCastException.
In `@liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java`:
- Line 15: The change makes InputStreamList iterate and expose getFirst() as
Map.Entry<URI,InputStream>, breaking existing extensions that expect
Iterable<InputStream> and an InputStream getFirst(); restore the old contract by
adding back an iterator() that yields InputStream (or make the class implement
Iterable<InputStream> again) and a getFirst() that returns InputStream
(delegating to the entry-based implementation), while keeping the new
entry-based methods under distinct names (e.g., entryIterator(), entries(), or
getFirstEntry()) so both APIs coexist; update InputStreamList to provide both
InputStream-based and Map.Entry<URI,InputStream>-based accessors (preserve
AutoCloseable behavior and close semantics).
---
Outside diff comments:
In
`@liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java`:
- Around line 815-825: Update handlePropertyFileInputStreams to include the
resolved propertyFile name in its failure message (e.g., use
FileUtil.getFileNotFoundMessage(propertyFile) or append the propertyFile
variable) so the thrown MojoFailureException identifies which file failed; then
remove the now-unreachable null checks that throw MojoExecutionException around
calls to handlePropertyFileInputStreams in configureFieldsAndValues (the blocks
that check isl == null and throw with
FileUtil.getFileNotFoundMessage(this.propertyFile)), since
handlePropertyFileInputStreams either returns a non-null, non-empty
InputStreamList or throws.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 16f31eb3-c87b-4d22-8e92-888ae68eaa99
📒 Files selected for processing (12)
liquibase-cli/src/main/java/liquibase/integration/commandline/LiquibaseCommandLine.javaliquibase-cli/src/test/groovy/liquibase/integration/commandline/MultipleDefaultsFilesOverlayEachOtherTest.groovyliquibase-cli/src/test/resources/subfolder/test.propertiesliquibase-cli/src/test/resources/test.propertiesliquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.javaliquibase-standard/src/main/java/liquibase/changelog/ChangeSet.javaliquibase-standard/src/main/java/liquibase/configuration/ConfigurationDefinition.javaliquibase-standard/src/main/java/liquibase/configuration/core/DefaultsFileValueProvider.javaliquibase-standard/src/main/java/liquibase/resource/InputStreamList.javaliquibase-standard/src/test/groovy/liquibase/changelog/ChangeLogParametersTest.groovyliquibase-standard/src/test/groovy/liquibase/resource/DirectoryResourceAccessorTest.groovyliquibase-standard/src/test/groovy/liquibase/resource/FileSystemResourceAccessorTest.groovy
💤 Files with no reviewable changes (1)
- liquibase-standard/src/test/groovy/liquibase/changelog/ChangeLogParametersTest.groovy
| } | ||
|
|
||
| ClassLoaderResourceAccessor classLoaderResourceAccessor = new ClassLoaderResourceAccessor(); | ||
| List<Resource> resources = classLoaderResourceAccessor.getAll(defaultsFileConfigValue); | ||
| if (resources != null) { | ||
| for (Resource res : resources) { | ||
| if (res.exists()) { | ||
| final DefaultsFileValueProvider fileProvider = new DefaultsFileValueProvider(res.openInputStream(), "File in classpath " + res.getUri(), returnList.size()); | ||
| liquibaseConfiguration.registerProvider(fileProvider); | ||
| returnList.add(fileProvider); |
There was a problem hiding this comment.
Avoid unconditional classpath overlay and close classpath streams explicitly.
Line 754 currently runs even when Line 744 already resolved an on-disk defaults file, so explicit file config can be unintentionally overlaid by classpath matches. Also, Line 759 opens streams without an explicit close scope.
Proposed fix
- ClassLoaderResourceAccessor classLoaderResourceAccessor = new ClassLoaderResourceAccessor();
- List<Resource> resources = classLoaderResourceAccessor.getAll(defaultsFileConfigValue);
- if (resources != null) {
- for (Resource res : resources) {
- if (res.exists()) {
- final DefaultsFileValueProvider fileProvider = new DefaultsFileValueProvider(res.openInputStream(), "File in classpath " + res.getUri(), returnList.size());
- liquibaseConfiguration.registerProvider(fileProvider);
- returnList.add(fileProvider);
- }
- }
- }
+ else {
+ ClassLoaderResourceAccessor classLoaderResourceAccessor = new ClassLoaderResourceAccessor();
+ List<Resource> resources = classLoaderResourceAccessor.getAll(defaultsFileConfigValue);
+ if (resources != null) {
+ for (Resource res : resources) {
+ if (res.exists()) {
+ try (InputStream classpathStream = res.openInputStream()) {
+ final DefaultsFileValueProvider fileProvider =
+ new DefaultsFileValueProvider(classpathStream, "File in classpath " + res.getUri(), returnList.size());
+ liquibaseConfiguration.registerProvider(fileProvider);
+ returnList.add(fileProvider);
+ }
+ }
+ }
+ }
+ }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@liquibase-cli/src/main/java/liquibase/integration/commandline/LiquibaseCommandLine.java`
around lines 752 - 761, The classpath lookup is being applied unconditionally
and opens streams without closing them; guard the
ClassLoaderResourceAccessor.getAll(defaultsFileConfigValue) loop so it only runs
if no on-disk defaults provider was already added (e.g., if returnList.isEmpty()
or by checking existing providers), and change the creation of
DefaultsFileValueProvider to use a try-with-resources around
res.openInputStream() so the InputStream is closed after constructing the
provider; then call liquibaseConfiguration.registerProvider(fileProvider) and
returnList.add(fileProvider) inside that guarded block.
…haelKern-IVV/liquibase into feature/cascading_properties
There was a problem hiding this comment.
♻️ Duplicate comments (1)
liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java (1)
126-127:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftPublic
getFirst()signature change is a breaking API change.Line 126 now returns
Map.Entry<URI, InputStream>fromgetFirst(). If previous versions exposedInputStream getFirst(), this breaks existing extensions at compile time. Please preserve the old stream-returning contract and introduce a distinct entry-based accessor (for examplegetFirstEntry()), then migrate internal callers incrementally.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java` around lines 126 - 127, getFirst() was changed to return Map.Entry<URI, InputStream> which is a breaking API change; restore the original public signature InputStream getFirst() and add a new method Map.Entry<URI, InputStream> getFirstEntry() that returns the entry (use the current implementation for getFirstEntry()); update internal callers in this class to call getFirstEntry() if they need the URI, otherwise call getFirst() to preserve compatibility, and keep the same empty/exception behavior (e.g., handle streams.isEmpty() or iterator().next() semantics) to avoid changing runtime behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@liquibase-standard/src/main/java/liquibase/resource/InputStreamList.java`:
- Around line 126-127: getFirst() was changed to return Map.Entry<URI,
InputStream> which is a breaking API change; restore the original public
signature InputStream getFirst() and add a new method Map.Entry<URI,
InputStream> getFirstEntry() that returns the entry (use the current
implementation for getFirstEntry()); update internal callers in this class to
call getFirstEntry() if they need the URI, otherwise call getFirst() to preserve
compatibility, and keep the same empty/exception behavior (e.g., handle
streams.isEmpty() or iterator().next() semantics) to avoid changing runtime
behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 29f0eed9-e787-4c2c-b33f-29954acfb97d
📒 Files selected for processing (2)
liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.javaliquibase-standard/src/main/java/liquibase/resource/InputStreamList.java
🚧 Files skipped from review as they are similar to previous changes (1)
- liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseMojo.java
…haelKern-IVV/liquibase into feature/cascading_properties
ccf0685 to
cf2acdb
Compare
Impact
Description
Currently, we support a single "defaultsFile" (aka liquibase.properties) which can set liquibase configuration values. However, often times people have some of those values that change from environment to environment and it's a pain to have to keep multiple versions of those file with only slight differences.
In addition, there may be sections of the defaultsFile that are maintained by different groups of people and that not everyone involved is authorized to view, such as login credentials.
Unlike requirement #1694 (which should be reopened), this is not achieved by using an additional file whose name includes the stage, but rather by using files with the same name located in different places in the classpath. It is up to the user to configure the classpath according to their own requirements.
Technically, this means that the defaultsFile is read not by an InputStream but by an InputStreamList. For each InputStream contained within it, a separate DefaultsFileValueProvider is created, whose precedence depends on its position in the classpath.
Things to be aware of
Things to worry about
Additional Context