1 package fr.ifremer.quadrige2.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.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
78
79 @Component("liquibase")
80 @Lazy
81 public class Liquibase implements InitializingBean, BeanNameAware, ResourceLoaderAware {
82
83
84 private static final Log log =
85 LogFactory.getLog(Liquibase.class);
86
87
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
110
111
112
113
114 @Autowired
115 public Liquibase(DataSource dataSource, Quadrige2Configuration config) {
116 this.dataSource = dataSource;
117 this.config = config;
118 }
119
120
121
122
123
124
125 public Liquibase(Quadrige2Configuration config) {
126 this.dataSource = null;
127 this.config = config;
128
129 setChangeLog(config.getLiquibaseChangeLogPath());
130 }
131
132
133
134
135
136
137 @Override
138 public void afterPropertiesSet() throws LiquibaseException {
139
140 setChangeLog(config.getLiquibaseChangeLogPath());
141
142
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
159
160
161
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
190
191
192
193 public DataSource getDataSource() {
194 return dataSource;
195 }
196
197
198
199
200
201
202 public void setDataSource(DataSource dataSource) {
203 this.dataSource = dataSource;
204 }
205
206
207
208
209
210
211 public String getChangeLog() {
212 return changeLog;
213 }
214
215
216
217
218
219
220
221 public void setChangeLog(String dataModel) {
222
223 this.changeLog = dataModel;
224 }
225
226
227
228
229
230
231 public String getContexts() {
232 return contexts;
233 }
234
235
236
237
238
239
240 public void setContexts(String contexts) {
241 this.contexts = contexts;
242 }
243
244
245
246
247
248
249 public String getDefaultSchema() {
250 return defaultSchema;
251 }
252
253
254
255
256
257
258 public void setDefaultSchema(String defaultSchema) {
259 this.defaultSchema = defaultSchema;
260 }
261
262
263
264
265
266
267 public void executeUpdate() throws LiquibaseException {
268 executeUpdate(null);
269 }
270
271
272
273
274
275
276
277 public void executeUpdate(Properties connectionProperties) throws LiquibaseException {
278
279 Connection c = null;
280 liquibase.Liquibase liquibase;
281 try {
282
283 c = createConnection(connectionProperties);
284
285
286 liquibase = createLiquibase(c);
287
288
289 liquibase.forceReleaseLocks();
290 performUpdate(liquibase);
291 liquibase.forceReleaseLocks();
292
293
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
304 }
305 releaseConnection(c);
306 }
307 }
308
309 }
310
311
312
313
314
315
316
317 protected void performUpdate(liquibase.Liquibase liquibase) throws LiquibaseException {
318 liquibase.update(getContexts());
319 }
320
321
322
323
324
325
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
334 c = createConnection();
335
336
337 liquibase = createLiquibase(c);
338
339
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
358 }
359 releaseConnection(c);
360 }
361 if (myWriter != null) {
362 try {
363 myWriter.close();
364 } catch (IOException e) {
365
366 }
367 }
368 }
369
370 }
371
372
373
374
375
376
377
378
379 protected void performReportStatus(liquibase.Liquibase liquibase, Writer writer) throws LiquibaseException {
380 liquibase.reportStatus(true, getContexts(), writer);
381 }
382
383
384
385
386
387
388
389
390 protected liquibase.Liquibase createLiquibase(Connection c) throws LiquibaseException {
391 String adjustedChangeLog = getChangeLog();
392
393 if (this.resourceLoader == null) {
394
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
410
411
412
413
414
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
426
427
428
429
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
448
449
450
451 public void setChangeLogParameters(Map<String, String> parameters) {
452 this.parameters = parameters;
453 }
454
455
456
457
458
459
460 protected ResourceAccessor createResourceAccessor() {
461
462 if (this.resourceLoader != null) {
463 return new SpringResourceOpener(getChangeLog());
464 }
465
466
467 if (isClasspathPrefixPresent(changeLog)) {
468 return new ClassLoaderResourceAccessor(this.getClass().getClassLoader());
469 }
470
471
472 return new FileSystemResourceAccessor(new File(adjustNoFilePrefix(changeLog)).getParent());
473 }
474
475
476
477
478
479
480 @Override
481 public void setBeanName(String name) {
482 this.beanName = name;
483 }
484
485
486
487
488
489
490 public String getBeanName() {
491 return beanName;
492 }
493
494
495 @Override
496 public void setResourceLoader(ResourceLoader resourceLoader) {
497 this.resourceLoader = resourceLoader;
498 }
499
500
501
502
503
504
505 public ResourceLoader getResourceLoader() {
506 return resourceLoader;
507 }
508
509
510 @Override
511 public String toString() {
512 return getClass().getName() + "(" + this.getResourceLoader().toString() + ")";
513 }
514
515
516
517
518 protected void computeMaxChangeLogFileVersion() {
519 this.maxChangeLogFileVersion = null;
520
521
522 String changeLogPath = getChangeLog();
523 if (StringUtils.isBlank(changeLogPath)) {
524 return;
525 }
526
527
528 changeLogPath = changeLogPath.replaceAll("\\\\", "/");
529
530
531 int index = changeLogPath.lastIndexOf('/');
532 if (index == -1 || index == changeLogPath.length() - 1) {
533 return;
534 }
535
536
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
547 String pathPrefix = changeLogPath.substring(0, index);
548 Resource[] resources = resolver.getResources(pathPrefix + "/**/db-changelog-*.xml");
549 if (ArrayUtils.isNotEmpty(resources)) {
550 for (Resource resource : resources) {
551 String filename = resource.getFilename();
552 Matcher matcher = changeLogWithVersionPattern.matcher(filename);
553
554
555 if (matcher.matches()) {
556 String fileVersion = matcher.group(1);
557
558 if (!fileVersion.endsWith(CHANGE_LOG_SNAPSHOT_SUFFIX)) {
559 try {
560 Version version = VersionBuilder.create(fileVersion).build();
561
562
563 if (maxVersion == null || maxVersion.before(version)) {
564 maxVersion = version;
565 }
566 } catch (IllegalArgumentException iae) {
567
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
591
592
593
594
595 public Version getMaxChangeLogFileVersion() {
596 return this.maxChangeLogFileVersion;
597 }
598
599
600
601
602
603
604
605
606
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
614 c = createConnection();
615
616
617 liquibase = createLiquibase(c);
618
619
620 liquibase.forceReleaseLocks();
621 DiffResult diffResult = performDiff(liquibase, typesToControl);
622 liquibase.forceReleaseLocks();
623
624
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
640 }
641 releaseConnection(c);
642 }
643 if (writer != null) {
644 writer.close();
645 }
646 }
647 }
648
649
650
651
652
653
654
655
656
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
664 c = createConnection();
665
666
667 liquibase = createLiquibase(c);
668
669
670 liquibase.forceReleaseLocks();
671 DiffResult diffResult = performDiff(liquibase, typesToControl);
672 liquibase.forceReleaseLocks();
673
674
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
695 }
696 releaseConnection(c);
697 }
698 if (writer != null) {
699 writer.close();
700 }
701 }
702 }
703
704
705
706
707
708
709
710
711
712
713
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
776
777
778
779 protected Logger getLog() {
780 return liquibase.logging.LogFactory.getInstance().getLog();
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 }