1 package net.sumaris.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
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
81
82 @Component
83 public class Liquibase implements InitializingBean, BeanNameAware, ResourceLoaderAware {
84
85
86 private static final org.slf4j.Logger log =
87 LoggerFactory.getLogger(Liquibase.class);
88
89
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
112
113
114
115
116 @Autowired
117 public Liquibase(DataSource dataSource, SumarisConfiguration config) {
118 this.dataSource = dataSource;
119 this.config = config;
120
121
122 LogFactory.setInstance(new LogFactory());
123 }
124
125
126
127
128
129
130 public Liquibase(SumarisConfiguration config) {
131 this.dataSource = null;
132 this.config = config;
133
134 setChangeLog(config.getLiquibaseChangeLogPath());
135 }
136
137
138
139
140
141
142 @Override
143 public void afterPropertiesSet() throws LiquibaseException {
144
145 setChangeLog(config.getLiquibaseChangeLogPath());
146
147
148 setDefaultSchema(config.getJdbcSchema());
149
150
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
167
168
169
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
196
197
198
199 public DataSource getDataSource() {
200 return dataSource;
201 }
202
203
204
205
206
207
208 public void setDataSource(DataSource dataSource) {
209 this.dataSource = dataSource;
210 }
211
212
213
214
215
216
217 public String getChangeLog() {
218 return changeLog;
219 }
220
221
222
223
224
225
226
227 public void setChangeLog(String dataModel) {
228
229 this.changeLog = dataModel;
230 }
231
232
233
234
235
236
237 public String getContexts() {
238 return contexts;
239 }
240
241
242
243
244
245
246 public void setContexts(String contexts) {
247 this.contexts = contexts;
248 }
249
250
251
252
253
254
255 public String getDefaultSchema() {
256 return defaultSchema;
257 }
258
259
260
261
262
263
264 public void setDefaultSchema(String defaultSchema) {
265 this.defaultSchema = defaultSchema;
266 }
267
268
269
270
271
272
273 public void executeUpdate() throws LiquibaseException {
274 executeUpdate(null);
275 }
276
277
278
279
280
281
282
283 public void executeUpdate(Properties connectionProperties) throws LiquibaseException {
284
285
286 Connection c = null;
287 liquibase.Liquibase liquibase;
288 try {
289
290 c = createConnection(connectionProperties);
291
292 log.info(I18n.t("sumaris.persistence.liquibase.executeUpdate", c.getMetaData().getURL()));
293
294
295 liquibase = createLiquibase(c);
296
297
298 liquibase.forceReleaseLocks();
299 performUpdate(liquibase);
300 liquibase.forceReleaseLocks();
301
302
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
316 }
317 releaseConnection(c);
318 }
319 }
320 }
321
322
323
324
325
326
327
328 protected void performUpdate(liquibase.Liquibase liquibase) throws LiquibaseException {
329 liquibase.update(getContexts());
330 }
331
332
333
334
335
336
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
345 c = createConnection();
346
347
348 liquibase = createLiquibase(c);
349
350
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
369 }
370 releaseConnection(c);
371 }
372 if (myWriter != null) {
373 try {
374 myWriter.close();
375 } catch (IOException e) {
376
377 }
378 }
379 }
380
381 }
382
383
384
385
386
387
388
389
390 protected void performReportStatus(liquibase.Liquibase liquibase, Writer writer) throws LiquibaseException {
391 liquibase.reportStatus(true, getContexts(), writer);
392 }
393
394
395
396
397
398
399
400
401 protected liquibase.Liquibase createLiquibase(Connection c) throws LiquibaseException {
402 String adjustedChangeLog = getChangeLog();
403
404 if (this.resourceLoader == null) {
405
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
421
422
423
424
425
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
438
439
440
441
442
443 protected Database createHibernateDatabase() throws DatabaseException {
444
445
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
465
466
467
468 public void setChangeLogParameters(Map<String, String> parameters) {
469 this.parameters = parameters;
470 }
471
472
473
474
475
476
477 protected ResourceAccessor createResourceAccessor() {
478
479 if (this.resourceLoader != null) {
480 return new SpringResourceOpener(getChangeLog());
481 }
482
483
484 if (isClasspathPrefixPresent(changeLog)) {
485 return new ClassLoaderResourceAccessor(this.getClass().getClassLoader());
486 }
487
488
489 return new FileSystemResourceAccessor(new File(adjustNoFilePrefix(changeLog)).getParent());
490 }
491
492
493
494
495
496
497 @Override
498 public void setBeanName(String name) {
499 this.beanName = name;
500 }
501
502
503
504
505
506
507 public String getBeanName() {
508 return beanName;
509 }
510
511
512 @Override
513 public void setResourceLoader(ResourceLoader resourceLoader) {
514 this.resourceLoader = resourceLoader;
515 }
516
517
518
519
520
521
522 public ResourceLoader getResourceLoader() {
523 return resourceLoader;
524 }
525
526
527 @Override
528 public String toString() {
529 return getClass().getName() + "(" + this.getResourceLoader().toString() + ")";
530 }
531
532
533
534
535 protected void computeMaxChangeLogFileVersion() {
536 this.maxChangeLogFileVersion = null;
537
538
539 String changeLogPath = getChangeLog();
540 if (StringUtils.isBlank(changeLogPath)) {
541 return;
542 }
543
544
545 changeLogPath = changeLogPath.replaceAll("\\\\", "/");
546
547
548 int index = changeLogPath.lastIndexOf('/');
549 if (index == -1 || index == changeLogPath.length() - 1) {
550 return;
551 }
552
553
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
564 String pathPrefix = changeLogPath.substring(0, index);
565 Resource[] resources = resolver.getResources(pathPrefix + "/**/db-changelog-*.xml");
566 if (ArrayUtils.isNotEmpty(resources)) {
567 for (Resource resource : resources) {
568 String filename = resource.getFilename();
569 Matcher matcher = changeLogWithVersionPattern.matcher(filename);
570
571
572 if (matcher.matches()) {
573 String fileVersion = matcher.group(1);
574
575 if (!fileVersion.endsWith(CHANGE_LOG_SNAPSHOT_SUFFIX)) {
576 try {
577 Version version = VersionBuilder.create(fileVersion).build();
578
579
580 if (maxVersion == null || maxVersion.before(version)) {
581 maxVersion = version;
582 }
583 } catch (IllegalArgumentException iae) {
584
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
608
609
610
611
612 public Version getMaxChangeLogFileVersion() {
613 return this.maxChangeLogFileVersion;
614 }
615
616
617
618
619
620
621
622
623
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
631 c = createConnection();
632
633
634 liquibase = createLiquibase(c);
635
636
637 liquibase.forceReleaseLocks();
638 DiffResult diffResult = performDiff(liquibase, typesToControl);
639 liquibase.forceReleaseLocks();
640
641
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
657 }
658 releaseConnection(c);
659 }
660 if (writer != null) {
661 writer.close();
662 }
663 }
664 }
665
666
667
668
669
670
671
672
673
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
681 c = createConnection();
682
683
684 liquibase = createLiquibase(c);
685
686 DiffResult diffResult;
687
688
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
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
715 }
716 releaseConnection(c);
717 }
718 if (writer != null) {
719 writer.close();
720 }
721 }
722 }
723
724
725
726
727
728
729
730
731
732
733
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
796
797
798
799 protected liquibase.logging.Logger getLog() {
800 return liquibase.logging.LogFactory.getInstance().getLog();
801 }
802
803
804
805
806
807
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
818
819
820
821
822
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
835
836
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
848
849
850
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
860
861
862
863
864 protected boolean isClasspathPrefixPresent(String file) {
865 return file.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX);
866 }
867
868
869
870
871
872
873
874 protected boolean isFilePrefixPresent(String file) {
875 return file.startsWith(ResourceUtils.FILE_URL_PREFIX);
876 }
877
878
879
880
881
882
883
884 protected String adjustNoFilePrefix(String file) {
885 return isFilePrefixPresent(file)
886 ? file.substring(ResourceUtils.FILE_URL_PREFIX.length())
887 : file;
888 }
889
890 }