001/* 002 * (C) Copyright 2006-2018 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Florent Guillaume 018 */ 019 020package org.nuxeo.ecm.core.storage.sql; 021 022import java.io.File; 023import java.io.FileOutputStream; 024import java.io.IOException; 025import java.io.OutputStream; 026import java.io.PrintStream; 027import java.io.Serializable; 028import java.security.MessageDigest; 029import java.security.NoSuchAlgorithmException; 030import java.sql.Connection; 031import java.sql.DatabaseMetaData; 032import java.sql.ResultSet; 033import java.sql.SQLException; 034import java.sql.Statement; 035import java.util.Calendar; 036import java.util.Collection; 037import java.util.HashMap; 038import java.util.HashSet; 039import java.util.LinkedList; 040import java.util.List; 041import java.util.Map; 042import java.util.Set; 043import java.util.Map.Entry; 044import java.util.concurrent.CopyOnWriteArrayList; 045 046import javax.naming.NamingException; 047 048import org.apache.commons.logging.Log; 049import org.apache.commons.logging.LogFactory; 050import org.nuxeo.common.Environment; 051import org.nuxeo.ecm.core.api.NuxeoException; 052import org.nuxeo.ecm.core.api.lock.LockManager; 053import org.nuxeo.ecm.core.api.repository.FulltextConfiguration; 054import org.nuxeo.ecm.core.storage.FulltextDescriptor; 055import org.nuxeo.ecm.core.storage.lock.LockManagerService; 056import org.nuxeo.ecm.core.storage.sql.Model.IdType; 057import org.nuxeo.ecm.core.storage.sql.Session.PathResolver; 058import org.nuxeo.ecm.core.storage.sql.coremodel.SQLSession; 059import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCConnection; 060import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCLogger; 061import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCMapper; 062import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo; 063import org.nuxeo.ecm.core.storage.sql.jdbc.TableUpgrader; 064import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column; 065import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database; 066import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table; 067import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect; 068import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.DialectOracle; 069import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.SQLStatement.ListCollector; 070import org.nuxeo.runtime.RuntimeMessage; 071import org.nuxeo.runtime.RuntimeMessage.Level; 072import org.nuxeo.runtime.RuntimeMessage.Source; 073import org.nuxeo.runtime.api.Framework; 074import org.nuxeo.runtime.cluster.ClusterService; 075import org.nuxeo.runtime.datasource.ConnectionHelper; 076import org.nuxeo.runtime.datasource.DataSourceHelper; 077import org.nuxeo.runtime.metrics.MetricsService; 078import org.nuxeo.runtime.transaction.TransactionHelper; 079 080import io.dropwizard.metrics5.Counter; 081import io.dropwizard.metrics5.Gauge; 082import io.dropwizard.metrics5.MetricName; 083import io.dropwizard.metrics5.MetricRegistry; 084import io.dropwizard.metrics5.SharedMetricRegistries; 085 086/** 087 * {@link Repository} implementation, to be extended by backend-specific initialization code. 088 */ 089public class RepositoryImpl implements Repository, org.nuxeo.ecm.core.model.Repository { 090 091 private static final Log log = LogFactory.getLog(RepositoryImpl.class); 092 093 public static final String TEST_UPGRADE = "testUpgrade"; 094 095 // property in sql.txt file 096 public static final String TEST_UPGRADE_VERSIONS = "testUpgradeVersions"; 097 098 public static final String TEST_UPGRADE_LAST_CONTRIBUTOR = "testUpgradeLastContributor"; 099 100 public static final String TEST_UPGRADE_LOCKS = "testUpgradeLocks"; 101 102 public static final String TEST_UPGRADE_SYS_CHANGE_TOKEN = "testUpgradeSysChangeToken"; 103 104 public static Map<String, Serializable> testProps = new HashMap<>(); 105 106 protected final RepositoryDescriptor repositoryDescriptor; 107 108 private final Collection<SessionImpl> sessions; 109 110 protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName()); 111 112 protected final Counter sessionCount; 113 114 private LockManager lockManager; 115 116 /** 117 * @since 7.4 : used to know if the LockManager was provided by this repository or externally 118 */ 119 protected boolean selfRegisteredLockManager = false; 120 121 /** Propagator of invalidations to all mappers' caches. */ 122 // public for tests 123 public final VCSInvalidationsPropagator invalidationsPropagator; 124 125 protected VCSClusterInvalidator clusterInvalidator; 126 127 public boolean requiresClusterSQL; 128 129 private Model model; 130 131 protected SQLInfo sqlInfo; 132 133 public RepositoryImpl(RepositoryDescriptor repositoryDescriptor) { 134 this.repositoryDescriptor = repositoryDescriptor; 135 sessions = new CopyOnWriteArrayList<>(); 136 invalidationsPropagator = new VCSInvalidationsPropagator(); 137 138 sessionCount = registry.counter(MetricName.build("nuxeo", "repositories", "repository", "sessions") 139 .tagged("repository", repositoryDescriptor.name)); 140 createMetricsGauges(); 141 142 initRepository(); 143 } 144 145 protected void createMetricsGauges() { 146 MetricName gaugeName = MetricName.build("nuxeo", "repositories", "repository", "cache", "size") 147 .tagged("repository", repositoryDescriptor.name); 148 registry.remove(gaugeName); 149 registry.register(gaugeName, new Gauge<Long>() { 150 @Override 151 public Long getValue() { 152 return getCacheSize(); 153 } 154 }); 155 gaugeName = MetricName.build("nuxeo", "repositories", "repository", "cache", "pristine") 156 .tagged("repository", repositoryDescriptor.name); 157 registry.remove(gaugeName); 158 registry.register(gaugeName, new Gauge<Long>() { 159 @Override 160 public Long getValue() { 161 return getCachePristineSize(); 162 } 163 }); 164 gaugeName = MetricName.build("nuxeo", "repositories", "repository", "cache", "selection") 165 .tagged("repository", repositoryDescriptor.name); 166 registry.remove(gaugeName); 167 registry.register(gaugeName, new Gauge<Long>() { 168 @Override 169 public Long getValue() { 170 return getCacheSelectionSize(); 171 } 172 }); 173 gaugeName = MetricName.build("nuxeo", "repositories", "repository", "cache", "mapper") 174 .tagged("repository", repositoryDescriptor.name); 175 registry.remove(gaugeName); 176 registry.register(gaugeName, new Gauge<Long>() { 177 @Override 178 public Long getValue() { 179 return getCacheMapperSize(); 180 } 181 }); 182 } 183 184 protected Mapper createCachingMapper(Model model, Mapper mapper) { 185 try { 186 Class<? extends CachingMapper> cachingMapperClass = getCachingMapperClass(); 187 if (cachingMapperClass == null) { 188 return mapper; 189 } 190 CachingMapper cachingMapper = cachingMapperClass.getDeclaredConstructor().newInstance(); 191 cachingMapper.initialize(getName(), model, mapper, invalidationsPropagator, 192 repositoryDescriptor.cachingMapperProperties); 193 return cachingMapper; 194 } catch (ReflectiveOperationException e) { 195 throw new NuxeoException(e); 196 } 197 } 198 199 protected Class<? extends CachingMapper> getCachingMapperClass() { 200 if (!repositoryDescriptor.getCachingMapperEnabled()) { 201 return null; 202 } 203 Class<? extends CachingMapper> cachingMapperClass = repositoryDescriptor.cachingMapperClass; 204 if (cachingMapperClass == null) { 205 // default cache 206 cachingMapperClass = SoftRefCachingMapper.class; 207 } 208 return cachingMapperClass; 209 } 210 211 public RepositoryDescriptor getRepositoryDescriptor() { 212 return repositoryDescriptor; 213 } 214 215 public LockManager getLockManager() { 216 return lockManager; 217 } 218 219 public Model getModel() { 220 return model; 221 } 222 223 /** @since 11.1 */ 224 public SQLInfo getSQLInfo() { 225 return sqlInfo; 226 } 227 228 public VCSInvalidationsPropagator getInvalidationsPropagator() { 229 return invalidationsPropagator; 230 } 231 232 public boolean isChangeTokenEnabled() { 233 return repositoryDescriptor.isChangeTokenEnabled(); 234 } 235 236 @Override 237 public SQLSession getSession() { 238 return new SQLSession(getConnection(), this); // NOSONAR 239 } 240 241 /** 242 * Gets a new connection. 243 * 244 * @return the session 245 */ 246 @Override 247 public synchronized SessionImpl getConnection() { 248 if (Framework.getRuntime().isShuttingDown()) { 249 throw new IllegalStateException("Cannot open connection, runtime is shutting down"); 250 } 251 SessionPathResolver pathResolver = new SessionPathResolver(); 252 Mapper mapper = new JDBCMapper(model, pathResolver, sqlInfo, clusterInvalidator, this); 253 mapper = createCachingMapper(model, mapper); 254 SessionImpl session = new SessionImpl(this, model, mapper); 255 pathResolver.setSession(session); 256 257 sessions.add(session); 258 sessionCount.inc(); 259 return session; 260 } 261 262 // callback by session at close time 263 protected void closeSession(SessionImpl session) { 264 sessions.remove(session); 265 sessionCount.dec(); 266 } 267 268 protected void initRepository() { 269 log.debug("Initializing"); 270 prepareClusterInvalidator(); // sets requiresClusterSQL used by backend init 271 272 // check datasource 273 String dataSourceName = JDBCConnection.getDataSourceName(repositoryDescriptor.name); 274 try { 275 DataSourceHelper.getDataSource(dataSourceName); 276 } catch (NamingException cause) { 277 throw new NuxeoException("Cannot acquire datasource: " + dataSourceName, cause); 278 } 279 280 // check connection and get dialect 281 Dialect dialect; 282 try (Connection connection = ConnectionHelper.getConnection(dataSourceName)) { 283 dialect = Dialect.createDialect(connection, repositoryDescriptor); 284 } catch (SQLException cause) { 285 throw new NuxeoException("Cannot get connection from datasource: " + dataSourceName, cause); 286 } 287 288 // model setup 289 ModelSetup modelSetup = new ModelSetup(); 290 modelSetup.materializeFulltextSyntheticColumn = dialect.getMaterializeFulltextSyntheticColumn(); 291 modelSetup.supportsArrayColumns = dialect.supportsArrayColumns(); 292 switch (dialect.getIdType()) { 293 case VARCHAR: 294 case UUID: 295 modelSetup.idType = IdType.STRING; 296 break; 297 case SEQUENCE: 298 modelSetup.idType = IdType.LONG; 299 break; 300 default: 301 throw new AssertionError(dialect.getIdType().toString()); 302 } 303 modelSetup.repositoryDescriptor = repositoryDescriptor; 304 305 // Model and SQLInfo 306 model = new Model(modelSetup); 307 sqlInfo = new SQLInfo(model, dialect, requiresClusterSQL); 308 309 // DDL mode 310 String ddlMode = repositoryDescriptor.getDDLMode(); 311 if (ddlMode == null) { 312 // compat 313 ddlMode = repositoryDescriptor.getNoDDL() ? RepositoryDescriptor.DDL_MODE_IGNORE 314 : RepositoryDescriptor.DDL_MODE_EXECUTE; 315 } 316 317 // create database 318 if (ddlMode.equals(RepositoryDescriptor.DDL_MODE_IGNORE)) { 319 log.info("Skipping database creation"); 320 } else { 321 createDatabase(ddlMode); 322 } 323 if (log.isDebugEnabled()) { 324 FulltextDescriptor fulltextDescriptor = repositoryDescriptor.getFulltextDescriptor(); 325 log.debug(String.format("Database ready, fulltext: disabled=%b storedInBlob=%b searchDisabled=%b.", 326 fulltextDescriptor.getFulltextDisabled(), fulltextDescriptor.getFulltextStoredInBlob(), 327 fulltextDescriptor.getFulltextSearchDisabled())); 328 } 329 330 initLockManager(); 331 initClusterInvalidator(); 332 333 // log once which mapper cache is being used 334 Class<? extends CachingMapper> cachingMapperClass = getCachingMapperClass(); 335 if (cachingMapperClass == null) { 336 log.warn("VCS Mapper cache is disabled."); 337 } else { 338 log.info("VCS Mapper cache using: " + cachingMapperClass.getName()); 339 } 340 341 initRootNode(); 342 } 343 344 protected void initRootNode() { 345 // access a session once so that SessionImpl.computeRootNode can create the root node 346 try (SessionImpl session = getConnection()) { 347 // nothing 348 } 349 } 350 351 protected String getLockManagerName() { 352 // TODO configure in repo descriptor 353 return getName(); 354 } 355 356 protected void initLockManager() { 357 String lockManagerName = getLockManagerName(); 358 LockManagerService lockManagerService = Framework.getService(LockManagerService.class); 359 lockManager = lockManagerService.getLockManager(lockManagerName); 360 if (lockManager == null) { 361 // no descriptor 362 // default to a VCSLockManager 363 lockManager = new VCSLockManager(this); 364 lockManagerService.registerLockManager(lockManagerName, lockManager); 365 selfRegisteredLockManager = true; 366 } else { 367 selfRegisteredLockManager = false; 368 } 369 log.info("Repository " + getName() + " using lock manager " + lockManager); 370 } 371 372 protected void prepareClusterInvalidator() { 373 if (Framework.getService(ClusterService.class).isEnabled()) { 374 clusterInvalidator = createClusterInvalidator(); 375 requiresClusterSQL = clusterInvalidator.requiresClusterSQL(); 376 } 377 } 378 379 protected VCSClusterInvalidator createClusterInvalidator() { 380 Class<? extends VCSClusterInvalidator> klass = repositoryDescriptor.clusterInvalidatorClass; 381 if (klass == null) { 382 klass = VCSPubSubInvalidator.class; 383 } 384 try { 385 return klass.getDeclaredConstructor().newInstance(); 386 } catch (ReflectiveOperationException e) { 387 throw new NuxeoException(e); 388 } 389 } 390 391 protected void initClusterInvalidator() { 392 if (clusterInvalidator != null) { 393 String nodeId = Framework.getService(ClusterService.class).getNodeId(); 394 clusterInvalidator.initialize(nodeId, this); 395 } 396 } 397 398 public static class SessionPathResolver implements PathResolver { 399 400 private Session session; 401 402 protected void setSession(Session session) { 403 this.session = session; 404 } 405 406 @Override 407 public Serializable getIdForPath(String path) { 408 Node node = session.getNodeByPath(path, null); 409 return node == null ? null : node.getId(); 410 } 411 } 412 413 /* 414 * ----- Repository ----- 415 */ 416 417 @Override 418 public void shutdown() { 419 close(); 420 } 421 422 @Override 423 public synchronized void close() { 424 closeAllSessions(); 425 model = null; 426 if (clusterInvalidator != null) { 427 clusterInvalidator.close(); 428 } 429 430 if (selfRegisteredLockManager) { 431 LockManagerService lms = Framework.getService(LockManagerService.class); 432 if (lms != null) { 433 lms.unregisterLockManager(getLockManagerName()); 434 } 435 } 436 } 437 438 protected synchronized void closeAllSessions() { 439 for (SessionImpl session : sessions) { 440 session.closeSession(); 441 } 442 sessions.clear(); 443 sessionCount.dec(sessionCount.getCount()); 444 if (lockManager != null) { 445 lockManager.closeLockManager(); 446 } 447 } 448 449 /* 450 * ----- RepositoryManagement ----- 451 */ 452 453 @Override 454 public String getName() { 455 return repositoryDescriptor.name; 456 } 457 458 @Override 459 public int clearCaches() { 460 int n = 0; 461 for (SessionImpl session : sessions) { 462 n += session.clearCaches(); 463 } 464 if (lockManager != null) { 465 lockManager.clearLockManagerCaches(); 466 } 467 return n; 468 } 469 470 @Override 471 public long getCacheSize() { 472 long size = 0; 473 for (SessionImpl session : sessions) { 474 size += session.getCacheSize(); 475 } 476 return size; 477 } 478 479 public long getCacheMapperSize() { 480 long size = 0; 481 for (SessionImpl session : sessions) { 482 size += session.getCacheMapperSize(); 483 } 484 return size; 485 } 486 487 @Override 488 public long getCachePristineSize() { 489 long size = 0; 490 for (SessionImpl session : sessions) { 491 size += session.getCachePristineSize(); 492 } 493 return size; 494 } 495 496 @Override 497 public long getCacheSelectionSize() { 498 long size = 0; 499 for (SessionImpl session : sessions) { 500 size += session.getCacheSelectionSize(); 501 } 502 return size; 503 } 504 505 @Override 506 public void processClusterInvalidationsNext() { 507 // TODO pass through or something 508 } 509 510 @Override 511 public void markReferencedBinaries() { 512 try (SessionImpl session = getConnection()) { 513 session.markReferencedBinaries(); 514 } 515 } 516 517 @Override 518 public int cleanupDeletedDocuments(int max, Calendar beforeTime) { 519 if (!repositoryDescriptor.getSoftDeleteEnabled()) { 520 return 0; 521 } 522 try (SessionImpl session = getConnection()) { 523 return session.cleanupDeletedDocuments(max, beforeTime); 524 } 525 } 526 527 @Override 528 public FulltextConfiguration getFulltextConfiguration() { 529 return model.getFulltextConfiguration(); 530 } 531 532 /** 533 * Creates the necessary structures in the database. 534 * 535 * @param ddlMode the DDL execution mode 536 */ 537 protected void createDatabase(String ddlMode) { 538 // some databases (SQL Server) can't create tables/indexes/etc in a transaction, so suspend it 539 runWithoutTransaction(() -> createDatabaseNoTx(ddlMode)); 540 } 541 542 protected void createDatabaseNoTx(String ddlMode) { 543 String dataSourceName = "repository_" + getName(); 544 try { 545 // open connection in noSharing mode 546 try (Connection connection = ConnectionHelper.getConnection(dataSourceName, true)) { 547 sqlInfo.dialect.performPostOpenStatements(connection); 548 if (!connection.getAutoCommit()) { 549 throw new NuxeoException("connection should not run in transactional mode for DDL operations"); 550 } 551 createTables(connection, ddlMode); 552 } 553 } catch (SQLException e) { 554 throw new NuxeoException(e); 555 } 556 } 557 558 protected String getTableName(String origName) { 559 if (sqlInfo.dialect instanceof DialectOracle) { 560 if (origName.length() > 30) { 561 StringBuilder sb = new StringBuilder(origName.length()); 562 try { 563 MessageDigest digest = MessageDigest.getInstance("MD5"); 564 sb.append(origName.substring(0, 15)); 565 sb.append('_'); 566 digest.update(origName.getBytes()); 567 sb.append(Dialect.toHexString(digest.digest()).substring(0, 12)); 568 return sb.toString(); 569 } catch (NoSuchAlgorithmException e) { 570 throw new RuntimeException("Error", e); 571 } 572 } 573 } 574 return origName; 575 } 576 577 protected void createTables(Connection connection, String ddlMode) throws SQLException { 578 JDBCLogger logger = new JDBCLogger(getName()); 579 Dialect dialect = sqlInfo.dialect; 580 ListCollector ddlCollector = new ListCollector(); 581 582 sqlInfo.executeSQLStatements(null, ddlMode, connection, logger, ddlCollector); // for missing category 583 sqlInfo.executeSQLStatements("first", ddlMode, connection, logger, ddlCollector); 584 sqlInfo.executeSQLStatements("beforeTableCreation", ddlMode, connection, logger, ddlCollector); 585 if (testProps.containsKey(TEST_UPGRADE)) { 586 // create "old" tables 587 sqlInfo.executeSQLStatements("testUpgrade", ddlMode, connection, logger, null); // do not collect 588 } 589 590 String schemaName = dialect.getConnectionSchema(connection); 591 DatabaseMetaData metadata = connection.getMetaData(); 592 Set<String> tableNames = findTableNames(metadata, schemaName); 593 Database database = sqlInfo.getDatabase(); 594 Map<String, List<Column>> added = new HashMap<>(); 595 596 for (Table table : database.getTables()) { 597 String tableName = getTableName(table.getPhysicalName()); 598 if (!tableNames.contains(tableName.toUpperCase())) { 599 /* 600 * Create missing table. 601 */ 602 ddlCollector.add(table.getCreateSql()); 603 ddlCollector.addAll(table.getPostCreateSqls(model)); 604 added.put(table.getKey(), null); // null = table created 605 sqlInfo.sqlStatementsProperties.put("create_table_" + tableName.toLowerCase(), Boolean.TRUE); 606 } else { 607 /* 608 * Get existing columns. 609 */ 610 Map<String, Integer> columnTypes = new HashMap<>(); 611 Map<String, String> columnTypeNames = new HashMap<>(); 612 Map<String, Integer> columnTypeSizes = new HashMap<>(); 613 try (ResultSet rs = metadata.getColumns(null, schemaName, tableName, "%")) { 614 while (rs.next()) { 615 String schema = rs.getString("TABLE_SCHEM"); 616 if (schema != null) { // null for MySQL, doh! 617 if ("INFORMATION_SCHEMA".equals(schema.toUpperCase())) { 618 // H2 returns some system tables (locks) 619 continue; 620 } 621 } 622 String columnName = rs.getString("COLUMN_NAME").toUpperCase(); 623 columnTypes.put(columnName, Integer.valueOf(rs.getInt("DATA_TYPE"))); 624 columnTypeNames.put(columnName, rs.getString("TYPE_NAME")); 625 columnTypeSizes.put(columnName, Integer.valueOf(rs.getInt("COLUMN_SIZE"))); 626 } 627 } 628 /* 629 * Update types and create missing columns. 630 */ 631 List<Column> addedColumns = new LinkedList<>(); 632 for (Column column : table.getColumns()) { 633 String upperName = column.getPhysicalName().toUpperCase(); 634 Integer type = columnTypes.remove(upperName); 635 if (type == null) { 636 log.warn("Adding missing column in database: " + column.getFullQuotedName()); 637 ddlCollector.add(table.getAddColumnSql(column)); 638 ddlCollector.addAll(table.getPostAddSqls(column, model)); 639 addedColumns.add(column); 640 } else { 641 String actualName = columnTypeNames.get(upperName); 642 Integer actualSize = columnTypeSizes.get(upperName); 643 String message = column.checkJdbcType(type, actualName, actualSize); 644 if (message != null) { 645 log.error(message); 646 Framework.getRuntime() 647 .getMessageHandler() 648 .addMessage(new RuntimeMessage(Level.ERROR, message, Source.CODE, 649 this.getClass().getName())); 650 } 651 } 652 } 653 for (String col : dialect.getIgnoredColumns(table)) { 654 columnTypes.remove(col.toUpperCase()); 655 } 656 if (!columnTypes.isEmpty()) { 657 log.warn("Database contains additional unused columns for table " + table.getQuotedName() + ": " 658 + String.join(", ", columnTypes.keySet())); 659 } 660 if (!addedColumns.isEmpty()) { 661 if (added.containsKey(table.getKey())) { 662 throw new AssertionError(); 663 } 664 added.put(table.getKey(), addedColumns); 665 } 666 } 667 } 668 669 if (testProps.containsKey(TEST_UPGRADE)) { 670 // create "old" content in tables 671 sqlInfo.executeSQLStatements("testUpgradeOldTables", ddlMode, connection, logger, ddlCollector); 672 } 673 674 // run upgrade for each table if added columns or test 675 if (!added.isEmpty()) { 676 TableUpgrader tableUpgrader = createTableUpgrader(connection, logger); 677 for (Entry<String, List<Column>> en : added.entrySet()) { 678 List<Column> addedColumns = en.getValue(); 679 String tableKey = en.getKey(); 680 tableUpgrader.upgrade(tableKey, addedColumns, ddlMode, ddlCollector); 681 } 682 } 683 684 sqlInfo.executeSQLStatements("afterTableCreation", ddlMode, connection, logger, ddlCollector); 685 sqlInfo.executeSQLStatements("last", ddlMode, connection, logger, ddlCollector); 686 687 // aclr_permission check for PostgreSQL 688 dialect.performAdditionalStatements(connection); 689 690 /* 691 * Execute all the collected DDL, or dump it if requested, depending on ddlMode 692 */ 693 694 // ddlMode may be: 695 // ignore (not treated here, nothing done) 696 // dump (implies execute) 697 // dump,execute 698 // dump,ignore (no execute) 699 // execute 700 // abort (implies dump) 701 // compat can be used instead of execute to always recreate stored procedures 702 703 List<String> ddl = ddlCollector.getStrings(); 704 boolean ignore = ddlMode.contains(RepositoryDescriptor.DDL_MODE_IGNORE); 705 boolean dump = ddlMode.contains(RepositoryDescriptor.DDL_MODE_DUMP); 706 boolean abort = ddlMode.contains(RepositoryDescriptor.DDL_MODE_ABORT); 707 if (dump || abort) { 708 /* 709 * Dump DDL if not empty. 710 */ 711 if (!ddl.isEmpty()) { 712 File dumpFile = new File(Environment.getDefault().getLog(), "ddl-vcs-" + getName() + ".sql"); 713 try (OutputStream out = new FileOutputStream(dumpFile); PrintStream ps = new PrintStream(out)) { 714 for (String sql : dialect.getDumpStart()) { 715 ps.println(sql); 716 } 717 for (String sql : ddl) { 718 sql = sql.trim(); 719 if (sql.endsWith(";")) { 720 sql = sql.substring(0, sql.length() - 1); 721 } 722 ps.println(dialect.getSQLForDump(sql)); 723 } 724 for (String sql : dialect.getDumpStop()) { 725 ps.println(sql); 726 } 727 } catch (IOException e) { 728 throw new NuxeoException(e); 729 } 730 /* 731 * Abort if requested. 732 */ 733 if (abort) { 734 log.error("Dumped DDL to: " + dumpFile); 735 throw new NuxeoException( 736 "Database initialization failed for: " + getName() + ", DDL must be executed: " + dumpFile); 737 } 738 } 739 } 740 if (!ignore) { 741 /* 742 * Execute DDL. 743 */ 744 try (Statement st = connection.createStatement()) { 745 for (String sql : ddl) { 746 logger.log(sql.replace("\n", "\n ")); // indented 747 try { 748 st.execute(sql); 749 } catch (SQLException e) { 750 throw new SQLException("Error executing: " + sql + " : " + e.getMessage(), e); 751 } 752 } 753 } 754 /* 755 * Execute post-DDL stuff. 756 */ 757 try (Statement st = connection.createStatement()) { 758 for (String sql : dialect.getStartupSqls(model, sqlInfo.database)) { 759 logger.log(sql.replace("\n", "\n ")); // indented 760 try { 761 st.execute(sql); 762 } catch (SQLException e) { 763 throw new SQLException("Error executing: " + sql + " : " + e.getMessage(), e); 764 } 765 } 766 } 767 } 768 } 769 770 protected TableUpgrader createTableUpgrader(Connection connection, JDBCLogger logger) { 771 TableUpgrader tableUpgrader = new TableUpgrader(sqlInfo, connection, logger); 772 tableUpgrader.add(Model.VERSION_TABLE_NAME, Model.VERSION_IS_LATEST_KEY, "upgradeVersions", 773 TEST_UPGRADE_VERSIONS); 774 tableUpgrader.add("dublincore", "lastContributor", "upgradeLastContributor", TEST_UPGRADE_LAST_CONTRIBUTOR); 775 tableUpgrader.add(Model.LOCK_TABLE_NAME, Model.LOCK_OWNER_KEY, "upgradeLocks", TEST_UPGRADE_LOCKS); 776 tableUpgrader.add(Model.HIER_TABLE_NAME, Model.MAIN_SYS_CHANGE_TOKEN_KEY, "upgradeSysChangeToken", 777 TEST_UPGRADE_SYS_CHANGE_TOKEN); 778 return tableUpgrader; 779 } 780 781 /** Finds uppercase table names. */ 782 protected static Set<String> findTableNames(DatabaseMetaData metadata, String schemaName) throws SQLException { 783 Set<String> tableNames = new HashSet<>(); 784 ResultSet rs = metadata.getTables(null, schemaName, "%", new String[] { "TABLE" }); 785 while (rs.next()) { 786 String tableName = rs.getString("TABLE_NAME"); 787 tableNames.add(tableName.toUpperCase()); 788 } 789 rs.close(); 790 return tableNames; 791 } 792 793 // completely stops the current transaction while running something 794 protected static void runWithoutTransaction(Runnable runnable) { 795 boolean rollback = TransactionHelper.isTransactionMarkedRollback(); 796 boolean hasTransaction = TransactionHelper.isTransactionActiveOrMarkedRollback(); 797 if (hasTransaction) { 798 TransactionHelper.commitOrRollbackTransaction(); 799 } 800 boolean completedAbruptly = true; 801 try { 802 runnable.run(); 803 completedAbruptly = false; 804 } finally { 805 if (hasTransaction) { 806 try { 807 TransactionHelper.startTransaction(); 808 } finally { 809 if (completedAbruptly || rollback) { 810 TransactionHelper.setTransactionRollbackOnly(); 811 } 812 } 813 } 814 } 815 } 816 817}