1 package fr.ifremer.quadrige3.core.dao.technical.liquibase;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
79
80 @Component("liquibase")
81 @Lazy
82 public class Liquibase implements InitializingBean, BeanNameAware, ResourceLoaderAware {
83
84
85 private static final Log log =
86 LogFactory.getLog(Liquibase.class);
87
88
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
111
112
113
114
115 @Autowired
116 public Liquibase(DataSource dataSource, QuadrigeConfiguration config) {
117 this.dataSource = dataSource;
118 this.config = config;
119 }
120
121
122
123
124
125
126 public Liquibase(QuadrigeConfiguration config) {
127 this.dataSource = null;
128 this.config = config;
129
130 setChangeLog(config.getLiquibaseChangeLogPath());
131 }
132
133
134
135
136
137
138 @Override
139 public void afterPropertiesSet() throws LiquibaseException {
140
141 setChangeLog(config.getLiquibaseChangeLogPath());
142
143
144 setDefaultSchema(config.getJdbcSchema());
145
146
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
163
164
165
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
192
193
194
195 public DataSource getDataSource() {
196 return dataSource;
197 }
198
199
200
201
202
203
204 public void setDataSource(DataSource dataSource) {
205 this.dataSource = dataSource;
206 }
207
208
209
210
211
212
213 public String getChangeLog() {
214 return changeLog;
215 }
216
217
218
219
220
221
222
223 public void setChangeLog(String dataModel) {
224
225 this.changeLog = dataModel;
226 }
227
228
229
230
231
232
233 public String getContexts() {
234 return contexts;
235 }
236
237
238
239
240
241
242 public void setContexts(String contexts) {
243 this.contexts = contexts;
244 }
245
246
247
248
249
250
251 public String getDefaultSchema() {
252 return defaultSchema;
253 }
254
255
256
257
258
259
260 public void setDefaultSchema(String defaultSchema) {
261 this.defaultSchema = defaultSchema;
262 }
263
264
265
266
267
268
269 public void executeUpdate() throws LiquibaseException {
270 executeUpdate(null);
271 }
272
273
274
275
276
277
278
279 public void executeUpdate(Properties connectionProperties) throws LiquibaseException {
280
281 Connection c = null;
282 liquibase.Liquibase liquibase;
283 try {
284
285 c = createConnection(connectionProperties);
286
287
288 liquibase = createLiquibase(c);
289
290
291 liquibase.forceReleaseLocks();
292 performUpdate(liquibase);
293 liquibase.forceReleaseLocks();
294
295
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
306 }
307 releaseConnection(c);
308 }
309 }
310
311 }
312
313
314
315
316
317
318
319 protected void performUpdate(liquibase.Liquibase liquibase) throws LiquibaseException {
320 liquibase.update(getContexts());
321 }
322
323
324
325
326
327
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
336 c = createConnection();
337
338
339 liquibase = createLiquibase(c);
340
341
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
360 }
361 releaseConnection(c);
362 }
363 if (myWriter != null) {
364 try {
365 myWriter.close();
366 } catch (IOException e) {
367
368 }
369 }
370 }
371
372 }
373
374
375
376
377
378
379
380
381 protected void performReportStatus(liquibase.Liquibase liquibase, Writer writer) throws LiquibaseException {
382 liquibase.reportStatus(true, getContexts(), writer);
383 }
384
385
386
387
388
389
390
391
392 protected liquibase.Liquibase createLiquibase(Connection c) throws LiquibaseException {
393 String adjustedChangeLog = getChangeLog();
394
395 if (this.resourceLoader == null) {
396
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
412
413
414
415
416
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
429
430
431
432
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
451
452
453
454 public void setChangeLogParameters(Map<String, String> parameters) {
455 this.parameters = parameters;
456 }
457
458
459
460
461
462
463 protected ResourceAccessor createResourceAccessor() {
464
465 if (this.resourceLoader != null) {
466 return new SpringResourceOpener(getChangeLog());
467 }
468
469
470 if (isClasspathPrefixPresent(changeLog)) {
471 return new ClassLoaderResourceAccessor(this.getClass().getClassLoader());
472 }
473
474
475 return new FileSystemResourceAccessor(new File(adjustNoFilePrefix(changeLog)).getParent());
476 }
477
478
479
480
481
482
483 @Override
484 public void setBeanName(String name) {
485 this.beanName = name;
486 }
487
488
489
490
491
492
493 public String getBeanName() {
494 return beanName;
495 }
496
497
498 @Override
499 public void setResourceLoader(ResourceLoader resourceLoader) {
500 this.resourceLoader = resourceLoader;
501 }
502
503
504
505
506
507
508 public ResourceLoader getResourceLoader() {
509 return resourceLoader;
510 }
511
512
513 @Override
514 public String toString() {
515 return getClass().getName() + "(" + this.getResourceLoader().toString() + ")";
516 }
517
518
519
520
521 protected void computeMaxChangeLogFileVersion() {
522 this.maxChangeLogFileVersion = null;
523
524
525 String changeLogPath = getChangeLog();
526 if (StringUtils.isBlank(changeLogPath)) {
527 return;
528 }
529
530
531 changeLogPath = changeLogPath.replaceAll("\\\\", "/");
532
533
534 int index = changeLogPath.lastIndexOf('/');
535 if (index == -1 || index == changeLogPath.length() - 1) {
536 return;
537 }
538
539
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
550 String pathPrefix = changeLogPath.substring(0, index);
551 Resource[] resources = resolver.getResources(pathPrefix + "/**/db-changelog-*.xml");
552 if (ArrayUtils.isNotEmpty(resources)) {
553 for (Resource resource : resources) {
554 String filename = resource.getFilename();
555 Matcher matcher = changeLogWithVersionPattern.matcher(filename);
556
557
558 if (matcher.matches()) {
559 String fileVersion = matcher.group(1);
560
561 if (!fileVersion.endsWith(CHANGE_LOG_SNAPSHOT_SUFFIX)) {
562 try {
563 Version version = VersionBuilder.create(fileVersion).build();
564
565
566 if (maxVersion == null || maxVersion.before(version)) {
567 maxVersion = version;
568 }
569 } catch (IllegalArgumentException iae) {
570
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
594
595
596
597
598 public Version getMaxChangeLogFileVersion() {
599 return this.maxChangeLogFileVersion;
600 }
601
602
603
604
605
606
607
608
609
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
617 c = createConnection();
618
619
620 liquibase = createLiquibase(c);
621
622
623 liquibase.forceReleaseLocks();
624 DiffResult diffResult = performDiff(liquibase, typesToControl);
625 liquibase.forceReleaseLocks();
626
627
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
643 }
644 releaseConnection(c);
645 }
646 if (writer != null) {
647 writer.close();
648 }
649 }
650 }
651
652
653
654
655
656
657
658
659
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
667 c = createConnection();
668
669
670 liquibase = createLiquibase(c);
671
672
673 liquibase.forceReleaseLocks();
674 DiffResult diffResult = performDiff(liquibase, typesToControl);
675 liquibase.forceReleaseLocks();
676
677
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
694 }
695 releaseConnection(c);
696 }
697 if (writer != null) {
698 writer.close();
699 }
700 }
701 }
702
703
704
705
706
707
708
709
710
711
712
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
775
776
777
778 protected Logger getLog() {
779
780 return new Slf4JLoggerFactory().getLog(Liquibase.class);
781 }
782
783
784
785
786
787
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
798
799
800
801
802
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
815
816
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
828
829
830
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
840
841
842
843
844 protected boolean isClasspathPrefixPresent(String file) {
845 return file.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX);
846 }
847
848
849
850
851
852
853
854 protected boolean isFilePrefixPresent(String file) {
855 return file.startsWith(ResourceUtils.FILE_URL_PREFIX);
856 }
857
858
859
860
861
862
863
864 protected String adjustNoFilePrefix(String file) {
865 return isFilePrefixPresent(file)
866 ? file.substring(ResourceUtils.FILE_URL_PREFIX.length())
867 : file;
868 }
869
870 }