View Javadoc
1   package fr.ifremer.quadrige2.core.dao.technical.liquibase;
2   
3   /*-
4    * #%L
5    * Quadrige2 Core :: Quadrige2 Core Shared
6    * $Id:$
7    * $HeadURL:$
8    * %%
9    * Copyright (C) 2017 Ifremer
10   * %%
11   * This program is free software: you can redistribute it and/or modify
12   * it under the terms of the GNU Affero General Public License as published by
13   * the Free Software Foundation, either version 3 of the License, or
14   * (at your option) any later version.
15   * 
16   * This program is distributed in the hope that it will be useful,
17   * but WITHOUT ANY WARRANTY; without even the implied warranty of
18   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19   * GNU General Public License for more details.
20   * 
21   * You should have received a copy of the GNU Affero General Public License
22   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
23   * #L%
24   */
25  
26  
27  import fr.ifremer.quadrige2.core.config.Quadrige2Configuration;
28  import fr.ifremer.quadrige2.core.config.Quadrige2ConfigurationOption;
29  import fr.ifremer.quadrige2.core.dao.technical.Daos;
30  import fr.ifremer.quadrige2.core.exception.Quadrige2TechnicalException;
31  import liquibase.database.Database;
32  import liquibase.database.DatabaseFactory;
33  import liquibase.database.jvm.JdbcConnection;
34  import liquibase.diff.DiffResult;
35  import liquibase.diff.compare.CompareControl;
36  import liquibase.diff.output.DiffOutputControl;
37  import liquibase.diff.output.changelog.DiffToChangeLog;
38  import liquibase.diff.output.report.DiffToReport;
39  import liquibase.exception.DatabaseException;
40  import liquibase.exception.LiquibaseException;
41  import liquibase.integration.commandline.CommandLineUtils;
42  import liquibase.logging.Logger;
43  import liquibase.resource.ClassLoaderResourceAccessor;
44  import liquibase.resource.FileSystemResourceAccessor;
45  import liquibase.resource.ResourceAccessor;
46  import liquibase.structure.core.DatabaseObjectFactory;
47  import org.apache.commons.lang3.ArrayUtils;
48  import org.apache.commons.lang3.StringUtils;
49  import org.apache.commons.logging.Log;
50  import org.apache.commons.logging.LogFactory;
51  import org.hibernate.cfg.Environment;
52  import org.nuiton.version.Version;
53  import org.nuiton.version.VersionBuilder;
54  import org.springframework.beans.factory.BeanNameAware;
55  import org.springframework.beans.factory.InitializingBean;
56  import org.springframework.beans.factory.annotation.Autowired;
57  import org.springframework.context.ResourceLoaderAware;
58  import org.springframework.context.annotation.Lazy;
59  import org.springframework.core.io.Resource;
60  import org.springframework.core.io.ResourceLoader;
61  import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
62  import org.springframework.core.io.support.ResourcePatternUtils;
63  import org.springframework.jdbc.datasource.DataSourceUtils;
64  import org.springframework.stereotype.Component;
65  import org.springframework.util.ResourceUtils;
66  
67  import javax.sql.DataSource;
68  import javax.xml.parsers.ParserConfigurationException;
69  import java.io.*;
70  import java.sql.Connection;
71  import java.sql.SQLException;
72  import java.util.*;
73  import java.util.regex.Matcher;
74  import java.util.regex.Pattern;
75  
76  /**
77   * <p>Liquibase class.</p>
78   */
79  @Component("liquibase")
80  @Lazy
81  public class Liquibase implements InitializingBean, BeanNameAware, ResourceLoaderAware {
82  
83      /** Logger. */
84      private static final Log log =
85              LogFactory.getLog(Liquibase.class);
86  
87      /** Constant <code>CHANGE_LOG_SNAPSHOT_SUFFIX="-SNAPSHOT.xml"</code> */
88      protected final static String CHANGE_LOG_SNAPSHOT_SUFFIX = "-SNAPSHOT.xml";
89  
90      private String beanName;
91  
92      private ResourceLoader resourceLoader;
93  
94      private DataSource dataSource;
95  
96      private Quadrige2Configuration config;
97  
98      private String changeLog;
99  
100     private String defaultSchema;
101 
102     private String contexts;
103 
104     private Map<String, String> parameters;
105 
106     protected Version maxChangeLogFileVersion;
107 
108     /**
109      * Constructor used by Spring
110      *
111      * @param dataSource a {@link javax.sql.DataSource} object.
112      * @param config a {@link fr.ifremer.quadrige2.core.config.Quadrige2Configuration} object.
113      */
114     @Autowired
115     public Liquibase(DataSource dataSource, Quadrige2Configuration config) {
116         this.dataSource = dataSource;
117         this.config = config;
118     }
119 
120     /**
121      * Constructor used when Spring is not started (no datasource, and @Resource not initialized)
122      *
123      * @param config a {@link fr.ifremer.quadrige2.core.config.Quadrige2Configuration} object.
124      */
125     public Liquibase(Quadrige2Configuration config) {
126         this.dataSource = null;
127         this.config = config;
128         // Init change log
129         setChangeLog(config.getLiquibaseChangeLogPath());
130     }
131 
132     /**
133      * {@inheritDoc}
134      *
135      * Executed automatically when the bean is initialized.
136      */
137     @Override
138     public void afterPropertiesSet() throws LiquibaseException {
139         // Update the change log path, from configuration
140         setChangeLog(config.getLiquibaseChangeLogPath());
141 
142         // Compute the max changelog file version
143         computeMaxChangeLogFileVersion();
144 
145         boolean shouldRun = Quadrige2Configuration.getInstance().useLiquibaseAutoRun();
146 
147         if (!shouldRun) {
148             getLog().debug(
149                     String.format("Liquibase did not run because properties '%s' set to false.",
150                             Quadrige2ConfigurationOption.LIQUIBASE_RUN_AUTO.getKey()));
151             return;
152         }
153 
154         executeUpdate();
155     }
156 
157     /**
158      * <p>getDatabaseProductName.</p>
159      *
160      * @return a {@link java.lang.String} object.
161      * @throws liquibase.exception.DatabaseException if any.
162      */
163     public String getDatabaseProductName() throws DatabaseException {
164         Connection connection = null;
165         String name = "unknown";
166         try {
167             connection = createConnection();
168             Database database =
169                     DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(dataSource.getConnection()));
170             name = database.getDatabaseProductName();
171         } catch (SQLException e) {
172             throw new DatabaseException(e);
173         } finally {
174             if (connection != null) {
175                 try {
176                     if (!connection.getAutoCommit()) {
177                         connection.rollback();
178                     }
179                 } catch (Exception e) {
180                     getLog().warning("Problem rollback connection", e);
181                 }
182                 releaseConnection(connection);
183             }
184         }
185         return name;
186     }
187 
188     /**
189      * <p>Getter for the field <code>dataSource</code>.</p>
190      *
191      * @return The DataSource that liquibase will use to perform the migration.
192      */
193     public DataSource getDataSource() {
194         return dataSource;
195     }
196 
197     /**
198      * The DataSource that liquibase will use to perform the migration.
199      *
200      * @param dataSource a {@link javax.sql.DataSource} object.
201      */
202     public void setDataSource(DataSource dataSource) {
203         this.dataSource = dataSource;
204     }
205 
206     /**
207      * <p>Getter for the field <code>changeLog</code>.</p>
208      *
209      * @return a Resource that is able to resolve to a file or classpath resource.
210      */
211     public String getChangeLog() {
212         return changeLog;
213     }
214 
215     /**
216      * Sets a Spring Resource that is able to resolve to a file or classpath resource.
217      * An example might be <code>classpath:db-changelog.xml</code>.
218      *
219      * @param dataModel a {@link java.lang.String} object.
220      */
221     public void setChangeLog(String dataModel) {
222 
223         this.changeLog = dataModel;
224     }
225 
226     /**
227      * <p>Getter for the field <code>contexts</code>.</p>
228      *
229      * @return a {@link java.lang.String} object.
230      */
231     public String getContexts() {
232         return contexts;
233     }
234 
235     /**
236      * <p>Setter for the field <code>contexts</code>.</p>
237      *
238      * @param contexts a {@link java.lang.String} object.
239      */
240     public void setContexts(String contexts) {
241         this.contexts = contexts;
242     }
243 
244     /**
245      * <p>Getter for the field <code>defaultSchema</code>.</p>
246      *
247      * @return a {@link java.lang.String} object.
248      */
249     public String getDefaultSchema() {
250         return defaultSchema;
251     }
252 
253     /**
254      * <p>Setter for the field <code>defaultSchema</code>.</p>
255      *
256      * @param defaultSchema a {@link java.lang.String} object.
257      */
258     public void setDefaultSchema(String defaultSchema) {
259         this.defaultSchema = defaultSchema;
260     }
261 
262     /**
263      * Execute liquibase update, using change log
264      *
265      * @throws liquibase.exception.LiquibaseException if any.
266      */
267     public void executeUpdate() throws LiquibaseException {
268         executeUpdate(null);
269     }
270 
271     /**
272      * Execute liquibase update, using change log
273      *
274      * @param connectionProperties the properties for connection
275      * @throws liquibase.exception.LiquibaseException if any.
276      */
277     public void executeUpdate(Properties connectionProperties) throws LiquibaseException {
278 
279         Connection c = null;
280         liquibase.Liquibase liquibase;
281         try {
282             // open connection
283             c = createConnection(connectionProperties);
284 
285             // create liquibase instance
286             liquibase = createLiquibase(c);
287 
288             // First, release locks, then update and release locks again
289             liquibase.forceReleaseLocks();
290             performUpdate(liquibase);
291             liquibase.forceReleaseLocks();
292 
293             // Compact database (if HsqlDB)
294             Daos.compactDatabase(c);
295 
296         } catch (SQLException e) {
297             throw new DatabaseException(e);
298         } finally {
299             if (c != null) {
300                 try {
301                     c.rollback();
302                 } catch (SQLException e) {
303                     // nothing to do
304                 }
305                 releaseConnection(c);
306             }
307         }
308 
309     }
310 
311     /**
312      * <p>performUpdate.</p>
313      *
314      * @param liquibase a {@link liquibase.Liquibase} object.
315      * @throws liquibase.exception.LiquibaseException if any.
316      */
317     protected void performUpdate(liquibase.Liquibase liquibase) throws LiquibaseException {
318         liquibase.update(getContexts());
319     }
320 
321     /**
322      * Execute liquibase status, using change log
323      *
324      * @throws liquibase.exception.LiquibaseException if any.
325      * @param writer a {@link java.io.Writer} object.
326      */
327     public void reportStatus(Writer writer) throws LiquibaseException {
328 
329         Connection c = null;
330         liquibase.Liquibase liquibase;
331         Writer myWriter = null;
332         try {
333             // open connection
334             c = createConnection();
335 
336             // create liquibase instance
337             liquibase = createLiquibase(c);
338 
339             // First, release locks, then update and release locks again
340             liquibase.forceReleaseLocks();
341             if (writer != null) {
342                 performReportStatus(liquibase, writer);
343             }
344             else {
345                 myWriter = new OutputStreamWriter(System.out);
346                 performReportStatus(liquibase, myWriter);
347             }
348             liquibase.forceReleaseLocks();
349 
350         } catch (SQLException e) {
351             throw new DatabaseException(e);
352         } finally {
353             if (c != null) {
354                 try {
355                     c.rollback();
356                 } catch (SQLException e) {
357                     // nothing to do
358                 }
359                 releaseConnection(c);
360             }
361             if (myWriter != null) {
362                 try {
363                     myWriter.close();
364                 } catch (IOException e) {
365                     // nothing to do
366                 }
367             }
368         }
369 
370     }
371 
372     /**
373      * <p>performReportStatus.</p>
374      *
375      * @param liquibase a {@link liquibase.Liquibase} object.
376      * @param writer a {@link java.io.Writer} object.
377      * @throws liquibase.exception.LiquibaseException if any.
378      */
379     protected void performReportStatus(liquibase.Liquibase liquibase, Writer writer) throws LiquibaseException {
380         liquibase.reportStatus(true, getContexts(), writer);
381     }
382 
383     /**
384      * <p>createLiquibase.</p>
385      *
386      * @param c a {@link java.sql.Connection} object.
387      * @return a {@link liquibase.Liquibase} object.
388      * @throws liquibase.exception.LiquibaseException if any.
389      */
390     protected liquibase.Liquibase createLiquibase(Connection c) throws LiquibaseException {
391         String adjustedChangeLog = getChangeLog();
392         // If Spring started, no changes
393         if (this.resourceLoader == null) {
394             // Remove 'classpath:' and 'files:' prefixes
395             adjustedChangeLog = adjustNoFilePrefix(adjustNoClasspath(adjustedChangeLog));
396         }
397 
398         liquibase.Liquibase liquibase = new liquibase.Liquibase(adjustedChangeLog, createResourceAccessor(), createDatabase(c));
399         if (parameters != null) {
400             for (Map.Entry<String, String> entry : parameters.entrySet()) {
401                 liquibase.setChangeLogParameter(entry.getKey(), entry.getValue());
402             }
403         }
404 
405         return liquibase;
406     }
407 
408     /**
409      * Subclasses may override this method add change some database settings such as
410      * default schema before returning the database object.
411      *
412      * @param c a {@link java.sql.Connection} object.
413      * @return a Database implementation retrieved from the {@link liquibase.database.DatabaseFactory}.
414      * @throws liquibase.exception.DatabaseException if any.
415      */
416     protected Database createDatabase(Connection c) throws DatabaseException {
417         Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(c));
418         if (StringUtils.trimToNull(this.defaultSchema) != null) {
419             database.setDefaultSchemaName(this.defaultSchema);
420         }
421         return database;
422     }
423 
424     /**
425      * Create a database connection to hibernate model.
426      * This is useful for diff report
427      *
428      * @throws liquibase.exception.DatabaseException if any.
429      * @return a {@link liquibase.database.Database} object.
430      */
431     protected Database createHibernateDatabase() throws DatabaseException {
432 
433         ResourceAccessor accessor = new ClassLoaderResourceAccessor(this.getClass().getClassLoader());
434 
435         return CommandLineUtils.createDatabaseObject(accessor,
436                 "hibernate:classic:hibernate.cfg.xml",
437                 null,
438                 null,
439                 null,
440                 config.getJdbcCatalog(), config.getJdbcSchema(),
441                 false, false,
442                 null, null, null, null, null, null, null
443         );
444     }
445 
446     /**
447      * <p>setChangeLogParameters.</p>
448      *
449      * @param parameters a {@link java.util.Map} object.
450      */
451     public void setChangeLogParameters(Map<String, String> parameters) {
452         this.parameters = parameters;
453     }
454 
455     /**
456      * Create a new resourceAccessor.
457      *
458      * @return a {@link liquibase.resource.ResourceAccessor} object.
459      */
460     protected ResourceAccessor createResourceAccessor() {
461         // If Spring started, resolve using Spring
462         if (this.resourceLoader != null) {
463             return new SpringResourceOpener(getChangeLog());
464         }
465 
466         // Classpath resource accessor
467         if (isClasspathPrefixPresent(changeLog)) {
468             return new ClassLoaderResourceAccessor(this.getClass().getClassLoader());
469         }
470 
471         // File resource accessor
472         return new FileSystemResourceAccessor(new File(adjustNoFilePrefix(changeLog)).getParent());
473     }
474 
475     /**
476      * {@inheritDoc}
477      *
478      * Spring sets this automatically to the instance's configured bean name.
479      */
480     @Override
481     public void setBeanName(String name) {
482         this.beanName = name;
483     }
484 
485     /**
486      * <p>Getter for the field <code>beanName</code>.</p>
487      *
488      * @return the Spring-name of this instance.
489      */
490     public String getBeanName() {
491         return beanName;
492     }
493 
494     /** {@inheritDoc} */
495     @Override
496     public void setResourceLoader(ResourceLoader resourceLoader) {
497         this.resourceLoader = resourceLoader;
498     }
499 
500     /**
501      * <p>Getter for the field <code>resourceLoader</code>.</p>
502      *
503      * @return a {@link org.springframework.core.io.ResourceLoader} object.
504      */
505     public ResourceLoader getResourceLoader() {
506         return resourceLoader;
507     }
508 
509     /** {@inheritDoc} */
510     @Override
511     public String toString() {
512         return getClass().getName() + "(" + this.getResourceLoader().toString() + ")";
513     }
514 
515     /**
516      * <p>computeMaxChangeLogFileVersion.</p>
517      */
518     protected void computeMaxChangeLogFileVersion() {
519         this.maxChangeLogFileVersion = null;
520 
521         // Get the changelog path
522         String changeLogPath = getChangeLog();
523         if (StringUtils.isBlank(changeLogPath)) {
524             return;
525         }
526 
527         // Secure all separator (need for regex)
528         changeLogPath = changeLogPath.replaceAll("\\\\", "/");
529 
530         // Get the parent folder path
531         int index = changeLogPath.lastIndexOf('/');
532         if (index == -1 || index == changeLogPath.length() - 1) {
533             return;
534         }
535 
536         // Compute a regex (based from changelog master file)
537         String changeLogWithVersionRegex = changeLogPath.substring(index + 1);
538         changeLogWithVersionRegex = changeLogWithVersionRegex.replaceAll("master\\.xml", "([0-9]\\\\.[.-_a-zA-Z]+)\\\\.xml");
539         Pattern changeLogWithVersionPattern = Pattern.compile(changeLogWithVersionRegex);
540 
541         Version maxVersion = null;
542 
543         PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(resourceLoader);
544 
545         try {
546             // Get resources from classpath
547             String pathPrefix = changeLogPath.substring(0, index);
548             Resource[] resources = resolver.getResources(pathPrefix + "/**/db-changelog-*.xml"); // WARNING: '**/' is mandatory, for multi-dbms (e.g. quadrige2-core-server)
549             if (ArrayUtils.isNotEmpty(resources)) {
550                 for (Resource resource : resources) {
551                     String filename = resource.getFilename();
552                     Matcher matcher = changeLogWithVersionPattern.matcher(filename);
553 
554                     // If the filename match the changelog with version pattern
555                     if (matcher.matches()) {
556                         String fileVersion = matcher.group(1);
557                         // Skip SNAPSHOT versions
558                         if (!fileVersion.endsWith(CHANGE_LOG_SNAPSHOT_SUFFIX)) {
559                             try {
560                                 Version version = VersionBuilder.create(fileVersion).build();
561 
562                                 // Store a version has max if need
563                                 if (maxVersion == null || maxVersion.before(version)) {
564                                     maxVersion = version;
565                                 }
566                             } catch (IllegalArgumentException iae) {
567                                 // Bad version format : log but continue
568                                 getLog().warning(
569                                         String.format(
570                                                 "Bad format version found in file: %s/%s. Ignoring this file when computing the max schema version.",
571                                                 changeLogPath, filename));
572                             }
573                         }
574                     }
575                 }
576             }
577             else {
578                 log.warn(String.format("No changelog files with version found. Please check master changelog file exists at [%s]", changeLogPath));
579             }
580         } catch (IOException e) {
581             throw new RuntimeException("Could not get changelog files", e);
582         }
583 
584         if (maxVersion != null) {
585             this.maxChangeLogFileVersion = maxVersion;
586         }
587     }
588 
589     /**
590      * Get the max version from all change log files.
591      * change log file with version must have a same pattern as the master changelog
592      *
593      * @return the max version founded in files, or null if version found
594      */
595     public Version getMaxChangeLogFileVersion() {
596         return this.maxChangeLogFileVersion;
597     }
598 
599     /**
600      * Generate a diff report (using text format)
601      *
602      * @param outputFile a {@link java.io.File} object.
603      * @param typesToControl
604      *            a comma separated database object to check (i.e Table, View, Column...). If null, all types are
605      *            checked
606      * @throws liquibase.exception.LiquibaseException if any.
607      */
608     public void reportDiff(File outputFile, String typesToControl) throws LiquibaseException {
609         Connection c = null;
610         liquibase.Liquibase liquibase;
611         PrintStream writer = null;
612         try {
613             // open connection
614             c = createConnection();
615 
616             // create liquibase instance
617             liquibase = createLiquibase(c);
618 
619             // First, release locks, then update and release locks again
620             liquibase.forceReleaseLocks();
621             DiffResult diffResult = performDiff(liquibase, typesToControl);
622             liquibase.forceReleaseLocks();
623 
624             // Write the result into report file
625             writer = outputFile != null ? new PrintStream(outputFile) : null;
626 
627             new DiffToReport(diffResult, writer != null ? writer : System.out)
628                     .print();
629 
630         } catch (SQLException e) {
631             throw new DatabaseException(e);
632         } catch (FileNotFoundException e) {
633             throw new Quadrige2TechnicalException("Could not write diff report file.", e);
634         } finally {
635             if (c != null) {
636                 try {
637                     c.rollback();
638                 } catch (SQLException e) {
639                     // nothing to do
640                 }
641                 releaseConnection(c);
642             }
643             if (writer != null) {
644                 writer.close();
645             }
646         }
647     }
648 
649     /**
650      * Generate a changelog file, with all diff found
651      *
652      * @param changeLogFile a {@link java.io.File} object.
653      * @param typesToControl
654      *            a comma separated database object to check (i.e Table, View, Column...). If null, all types are
655      *            checked
656      * @throws liquibase.exception.LiquibaseException if any.
657      */
658     public void generateDiffChangelog(File changeLogFile, String typesToControl) throws LiquibaseException {
659         Connection c = null;
660         liquibase.Liquibase liquibase;
661         PrintStream writer = null;
662         try {
663             // open connection
664             c = createConnection();
665 
666             // create liquibase instance
667             liquibase = createLiquibase(c);
668 
669             // First, release locks, then update and release locks again
670             liquibase.forceReleaseLocks();
671             DiffResult diffResult = performDiff(liquibase, typesToControl);
672             liquibase.forceReleaseLocks();
673 
674             // Write the result into report file
675             writer = changeLogFile != null ? new PrintStream(changeLogFile) : null;
676 
677             DiffOutputControl diffOutputControl = new DiffOutputControl(false, false, false, null);
678             new DiffToChangeLog(diffResult, diffOutputControl)
679                     .print(writer != null ? writer : System.out);
680 
681         } catch (SQLException e) {
682             throw new DatabaseException(e);
683         } catch (FileNotFoundException e) {
684             throw new Quadrige2TechnicalException("Could not generate changelog file.", e);
685         } catch (ParserConfigurationException e) {
686             throw new Quadrige2TechnicalException("Could not generate changelog file.", e);
687         } catch (IOException e) {
688             throw new Quadrige2TechnicalException("Could not generate changelog file.", e);
689         } finally {
690             if (c != null) {
691                 try {
692                     c.rollback();
693                 } catch (SQLException e) {
694                     // nothing to do
695                 }
696                 releaseConnection(c);
697             }
698             if (writer != null) {
699                 writer.close();
700             }
701         }
702     }
703 
704     /**
705      * <p>performDiff.</p>
706      *
707      * @param liquibase
708      *            the connection to the target database
709      * @param typesToControl
710      *            a comma separated database object to check (i.e Table, View, Column...). If null, all types are
711      *            checked
712      * @return the diff result
713      * @throws liquibase.exception.LiquibaseException if any.
714      */
715     protected DiffResult performDiff(liquibase.Liquibase liquibase, String typesToControl) throws LiquibaseException {
716         Database referenceDatabase = createHibernateDatabase();
717         CompareControl compareControl = new CompareControl(DatabaseObjectFactory.getInstance().parseTypes(typesToControl));
718         return liquibase.diff(referenceDatabase, liquibase.getDatabase(), compareControl);
719     }
720 
721     public class SpringResourceOpener implements ResourceAccessor {
722 
723         private String parentFile;
724         public SpringResourceOpener(String parentFile) {
725             this.parentFile = parentFile;
726         }
727 
728         @Override
729         public Set<String> list(String relativeTo, String path, boolean includeFiles, boolean includeDirectories, boolean recursive) throws IOException {
730             Set<String> returnSet = new HashSet<String>();
731 
732             Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(getResourceLoader()).getResources(adjustClasspath(path));
733 
734             for (Resource res : resources) {
735                 returnSet.add(res.getURL().toExternalForm());
736             }
737 
738             return returnSet;
739         }
740 
741         @Override
742         public Set<InputStream> getResourcesAsStream(String path) throws IOException {
743             Set<InputStream> returnSet = new HashSet<InputStream>();
744             Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(getResourceLoader()).getResources(adjustClasspath(path));
745 
746             if (resources == null || resources.length == 0) {
747                 return null;
748             }
749             for (Resource resource : resources) {
750                 returnSet.add(resource.getURL().openStream());
751             }
752 
753             return returnSet;
754         }
755 
756         public Resource getResource(String file) {
757             return getResourceLoader().getResource(adjustClasspath(file));
758         }
759 
760         private String adjustClasspath(String file) {
761             return isPrefixPresent(parentFile) && !isPrefixPresent(file) ? ResourceLoader.CLASSPATH_URL_PREFIX + file : file;
762         }
763 
764         public boolean isPrefixPresent(String file) {
765             return file.startsWith("classpath") || file.startsWith("file:") || file.startsWith("url:");
766         }
767 
768         @Override
769         public ClassLoader toClassLoader() {
770             return getResourceLoader().getClassLoader();
771         }
772     }
773 
774     /**
775      * <p>getLog.</p>
776      *
777      * @return a {@link liquibase.logging.Logger} object.
778      */
779     protected Logger getLog() {
780         return liquibase.logging.LogFactory.getInstance().getLog();
781     }
782 
783     /**
784      * <p>createConnection.</p>
785      *
786      * @return a {@link java.sql.Connection} object.
787      * @throws java.sql.SQLException if any.
788      */
789     protected Connection createConnection() throws SQLException {
790         if (dataSource != null) {
791             return DataSourceUtils.getConnection(dataSource);
792         }
793         return Daos.createConnection(config.getConnectionProperties());
794     }
795 
796     /**
797      * Create a connection from the given properties.<p/>
798      * If JDBC Url is equals to the datasource, use the datsource to create the connection
799      *
800      * @param connectionProperties a {@link java.util.Properties} object.
801      * @throws java.sql.SQLException if any.
802      * @return a {@link java.sql.Connection} object.
803      */
804     protected Connection createConnection(Properties connectionProperties) throws SQLException {
805         Properties targetConnectionProperties = (connectionProperties != null) ? connectionProperties : config.getConnectionProperties();
806         String jdbcUrl = targetConnectionProperties.getProperty(Environment.URL);
807         if (Objects.equals(config.getJdbcURL(), jdbcUrl) && dataSource != null) {
808             return DataSourceUtils.getConnection(dataSource);
809         }
810         return Daos.createConnection(targetConnectionProperties);
811     }
812 
813     /**
814      * <p>releaseConnection.</p>
815      *
816      * @param conn a {@link java.sql.Connection} object.
817      */
818     protected void releaseConnection(Connection conn) {
819         if (dataSource != null) {
820             DataSourceUtils.releaseConnection(conn, dataSource);
821             return;
822         }
823         Daos.closeSilently(conn);
824     }
825 
826     /**
827      * <p>adjustNoClasspath.</p>
828      *
829      * @param file a {@link java.lang.String} object.
830      * @return a {@link java.lang.String} object.
831      */
832     protected String adjustNoClasspath(String file) {
833         return isClasspathPrefixPresent(file)
834                 ? file.substring(ResourceLoader.CLASSPATH_URL_PREFIX.length())
835                 : file;
836     }
837 
838     /**
839      * <p>isClasspathPrefixPresent.</p>
840      *
841      * @param file a {@link java.lang.String} object.
842      * @return a boolean.
843      */
844     protected boolean isClasspathPrefixPresent(String file) {
845         return file.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX);
846     }
847 
848     /**
849      * <p>isFilePrefixPresent.</p>
850      *
851      * @param file a {@link java.lang.String} object.
852      * @return a boolean.
853      */
854     protected boolean isFilePrefixPresent(String file) {
855         return file.startsWith(ResourceUtils.FILE_URL_PREFIX);
856     }
857 
858     /**
859      * <p>adjustNoFilePrefix.</p>
860      *
861      * @param file a {@link java.lang.String} object.
862      * @return a {@link java.lang.String} object.
863      */
864     protected String adjustNoFilePrefix(String file) {
865         return isFilePrefixPresent(file)
866                 ? file.substring(ResourceUtils.FILE_URL_PREFIX.length())
867                 : file;
868     }
869 
870 }