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