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