001/* 002 * (C) Copyright 2006-2016 Nuxeo SA (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 * Benoit Delbosc 019 */ 020package org.nuxeo.ecm.core.storage.sql.jdbc.dialect; 021 022import java.io.IOException; 023import java.io.Reader; 024import java.io.Serializable; 025import java.lang.reflect.Constructor; 026import java.lang.reflect.InvocationTargetException; 027import java.lang.reflect.Method; 028import java.sql.Array; 029import java.sql.Connection; 030import java.sql.DatabaseMetaData; 031import java.sql.PreparedStatement; 032import java.sql.ResultSet; 033import java.sql.SQLException; 034import java.sql.Statement; 035import java.sql.Types; 036import java.util.ArrayList; 037import java.util.Arrays; 038import java.util.Collections; 039import java.util.HashMap; 040import java.util.HashSet; 041import java.util.LinkedList; 042import java.util.List; 043import java.util.Locale; 044import java.util.Map; 045import java.util.Set; 046 047import javax.transaction.xa.XAException; 048 049import org.apache.commons.lang.ArrayUtils; 050import org.apache.commons.logging.Log; 051import org.apache.commons.logging.LogFactory; 052import org.nuxeo.common.utils.StringUtils; 053import org.nuxeo.ecm.core.NXCore; 054import org.nuxeo.ecm.core.api.NuxeoException; 055import org.nuxeo.ecm.core.api.security.SecurityConstants; 056import org.nuxeo.ecm.core.storage.FulltextConfiguration; 057import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer; 058import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer.FulltextQuery; 059import org.nuxeo.ecm.core.storage.sql.ColumnType; 060import org.nuxeo.ecm.core.storage.sql.Model; 061import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor; 062import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCLogger; 063import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column; 064import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database; 065import org.nuxeo.ecm.core.storage.sql.jdbc.db.Join; 066import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table; 067import org.nuxeo.runtime.datasource.ConnectionHelper; 068 069/** 070 * Oracle-specific dialect. 071 * 072 * @author Florent Guillaume 073 */ 074public class DialectOracle extends Dialect { 075 076 private static final Log log = LogFactory.getLog(DialectOracle.class); 077 078 private Constructor<?> arrayDescriptorConstructor; 079 080 private Constructor<?> arrayConstructor; 081 082 private Method arrayGetLongArrayMethod; 083 084 protected final String fulltextParameters; 085 086 protected boolean pathOptimizationsEnabled; 087 088 protected int pathOptimizationsVersion = 0; 089 090 private static final String DEFAULT_USERS_SEPARATOR = "|"; 091 092 protected String usersSeparator; 093 094 protected final DialectIdType idType; 095 096 protected String idSequenceName; 097 098 protected XAErrorLogger xaErrorLogger; 099 100 protected static class XAErrorLogger { 101 102 protected final Class<?> oracleXAExceptionClass; 103 104 protected final Method m_xaError; 105 106 protected final Method m_xaErrorMessage; 107 108 protected final Method m_oracleError; 109 110 protected final Method m_oracleSQLError; 111 112 public XAErrorLogger() throws ReflectiveOperationException { 113 oracleXAExceptionClass = Thread.currentThread() 114 .getContextClassLoader() 115 .loadClass("oracle.jdbc.xa.OracleXAException"); 116 m_xaError = oracleXAExceptionClass.getMethod("getXAError"); 117 m_xaErrorMessage = oracleXAExceptionClass.getMethod("getXAErrorMessage", m_xaError.getReturnType()); 118 m_oracleError = oracleXAExceptionClass.getMethod("getOracleError"); 119 m_oracleSQLError = oracleXAExceptionClass.getMethod("getOracleSQLError"); 120 } 121 122 public void log(XAException e) throws ReflectiveOperationException { 123 int xaError = ((Integer) m_xaError.invoke(e)).intValue(); 124 String xaErrorMessage = (String) m_xaErrorMessage.invoke(xaError); 125 int oracleError = ((Integer) m_oracleError.invoke(e)).intValue(); 126 int oracleSQLError = ((Integer) m_oracleSQLError.invoke(e)).intValue(); 127 StringBuilder builder = new StringBuilder(); 128 builder.append("Oracle XA Error : ").append(xaError).append(" (").append(xaErrorMessage).append("),"); 129 builder.append("Oracle Error : ").append(oracleError).append(","); 130 builder.append("Oracle SQL Error : ").append(oracleSQLError); 131 log.warn(builder.toString(), e); 132 } 133 134 } 135 136 public DialectOracle(DatabaseMetaData metadata, RepositoryDescriptor repositoryDescriptor) { 137 super(metadata, repositoryDescriptor); 138 fulltextParameters = repositoryDescriptor == null ? null 139 : repositoryDescriptor.getFulltextAnalyzer() == null ? "" : repositoryDescriptor.getFulltextAnalyzer(); 140 pathOptimizationsEnabled = repositoryDescriptor != null && repositoryDescriptor.getPathOptimizationsEnabled(); 141 if (pathOptimizationsEnabled) { 142 pathOptimizationsVersion = repositoryDescriptor.getPathOptimizationsVersion(); 143 } 144 usersSeparator = repositoryDescriptor == null ? null 145 : repositoryDescriptor.usersSeparatorKey == null ? DEFAULT_USERS_SEPARATOR 146 : repositoryDescriptor.usersSeparatorKey; 147 String idt = repositoryDescriptor == null ? null : repositoryDescriptor.idType; 148 if (idt == null || "".equals(idt) || "varchar".equalsIgnoreCase(idt)) { 149 idType = DialectIdType.VARCHAR; 150 } else if (idt.toLowerCase().startsWith("sequence")) { 151 idType = DialectIdType.SEQUENCE; 152 if (idt.toLowerCase().startsWith("sequence:")) { 153 String[] split = idt.split(":"); 154 idSequenceName = split[1].toUpperCase(Locale.ENGLISH); 155 } else { 156 idSequenceName = "HIERARCHY_SEQ"; 157 } 158 } else { 159 throw new NuxeoException("Unknown id type: '" + idt + "'"); 160 } 161 xaErrorLogger = newXAErrorLogger(); 162 initArrayReflection(); 163 } 164 165 protected XAErrorLogger newXAErrorLogger() { 166 try { 167 return new XAErrorLogger(); 168 } catch (ReflectiveOperationException e) { 169 log.warn("Cannot initialize xa error loggger", e); 170 return null; 171 } 172 } 173 174 // use reflection to avoid linking dependencies 175 private void initArrayReflection() { 176 try { 177 Class<?> arrayDescriptorClass = Class.forName("oracle.sql.ArrayDescriptor"); 178 arrayDescriptorConstructor = arrayDescriptorClass.getConstructor(String.class, Connection.class); 179 Class<?> arrayClass = Class.forName("oracle.sql.ARRAY"); 180 arrayConstructor = arrayClass.getConstructor(arrayDescriptorClass, Connection.class, Object.class); 181 arrayGetLongArrayMethod = arrayClass.getDeclaredMethod("getLongArray"); 182 } catch (ClassNotFoundException e) { 183 // query syntax unit test run without Oracle JDBC driver 184 return; 185 } catch (ReflectiveOperationException e) { 186 throw new NuxeoException(e); 187 } 188 } 189 190 @Override 191 public String getNoColumnsInsertString(Column idColumn) { 192 // INSERT INTO foo () VALUES () or DEFAULT VALUES is not legal for Oracle, you need at least one column 193 return String.format("(%s) VALUES (DEFAULT)", idColumn.getQuotedName()); 194 } 195 196 @Override 197 public String getConnectionSchema(Connection connection) throws SQLException { 198 Statement st = connection.createStatement(); 199 String sql = "SELECT SYS_CONTEXT('USERENV', 'SESSION_USER') FROM DUAL"; 200 log.trace("SQL: " + sql); 201 ResultSet rs = st.executeQuery(sql); 202 rs.next(); 203 String user = rs.getString(1); 204 log.trace("SQL: -> " + user); 205 rs.close(); 206 st.close(); 207 return user; 208 } 209 210 @Override 211 public String getCascadeDropConstraintsString() { 212 return " CASCADE CONSTRAINTS"; 213 } 214 215 @Override 216 public String getAddColumnString() { 217 return "ADD"; 218 } 219 220 @Override 221 public JDBCInfo getJDBCTypeAndString(ColumnType type) { 222 switch (type.spec) { 223 case STRING: 224 if (type.isUnconstrained()) { 225 return jdbcInfo("NVARCHAR2(2000)", Types.VARCHAR); 226 } else if (type.isClob() || type.length > 2000) { 227 return jdbcInfo("NCLOB", Types.CLOB); 228 } else { 229 return jdbcInfo("NVARCHAR2(%d)", type.length, Types.VARCHAR); 230 } 231 case BOOLEAN: 232 return jdbcInfo("NUMBER(1,0)", Types.BIT); 233 case LONG: 234 return jdbcInfo("NUMBER(19,0)", Types.BIGINT); 235 case DOUBLE: 236 return jdbcInfo("DOUBLE PRECISION", Types.DOUBLE); 237 case TIMESTAMP: 238 return jdbcInfo("TIMESTAMP", Types.TIMESTAMP); 239 case BLOBID: 240 return jdbcInfo("VARCHAR2(250)", Types.VARCHAR); 241 // ----- 242 case NODEID: 243 case NODEIDFK: 244 case NODEIDFKNP: 245 case NODEIDFKMUL: 246 case NODEIDFKNULL: 247 case NODEIDPK: 248 case NODEVAL: 249 switch (idType) { 250 case VARCHAR: 251 return jdbcInfo("VARCHAR2(36)", Types.VARCHAR); 252 case SEQUENCE: 253 return jdbcInfo("NUMBER(10,0)", Types.INTEGER); 254 default: 255 throw new AssertionError("Unknown id type: " + idType); 256 } 257 case SYSNAME: 258 case SYSNAMEARRAY: 259 return jdbcInfo("VARCHAR2(250)", Types.VARCHAR); 260 case TINYINT: 261 return jdbcInfo("NUMBER(3,0)", Types.TINYINT); 262 case INTEGER: 263 return jdbcInfo("NUMBER(10,0)", Types.INTEGER); 264 case AUTOINC: 265 return jdbcInfo("NUMBER(10,0)", Types.INTEGER); 266 case FTINDEXED: 267 return jdbcInfo("CLOB", Types.CLOB); 268 case FTSTORED: 269 return jdbcInfo("NCLOB", Types.CLOB); 270 case CLUSTERNODE: 271 return jdbcInfo("VARCHAR(25)", Types.VARCHAR); 272 case CLUSTERFRAGS: 273 return jdbcInfo("VARCHAR2(4000)", Types.VARCHAR); 274 } 275 throw new AssertionError(type); 276 } 277 278 @Override 279 public boolean isAllowedConversion(int expected, int actual, String actualName, int actualSize) { 280 // Oracle internal conversions 281 if (expected == Types.DOUBLE && actual == Types.FLOAT) { 282 return true; 283 } 284 if (expected == Types.VARCHAR && actual == Types.NVARCHAR) { 285 return true; 286 } 287 if (expected == Types.VARCHAR && actual == Types.OTHER && actualName.equals("NVARCHAR2")) { 288 return true; 289 } 290 if (expected == Types.CLOB && actual == Types.NCLOB) { 291 return true; 292 } 293 if (expected == Types.CLOB && actual == Types.OTHER && actualName.equals("NCLOB")) { 294 return true; 295 } 296 if (expected == Types.BIT && actual == Types.DECIMAL && actualSize == 1) { 297 return true; 298 } 299 if (expected == Types.TINYINT && actual == Types.DECIMAL && actualSize == 3) { 300 return true; 301 } 302 if (expected == Types.INTEGER && actual == Types.DECIMAL && actualSize == 10) { 303 return true; 304 } 305 if (expected == Types.BIGINT && actual == Types.DECIMAL && actualSize == 19) { 306 return true; 307 } 308 if (expected == Types.BIGINT && actual == Types.DECIMAL && actualSize == 38) { 309 return true; 310 } 311 // CLOB vs VARCHAR compatibility 312 if (expected == Types.VARCHAR && actual == Types.NCLOB) { 313 return true; 314 } 315 if (expected == Types.VARCHAR && actual == Types.OTHER && actualName.equals("NCLOB")) { 316 return true; 317 } 318 if (expected == Types.CLOB && actual == Types.VARCHAR) { 319 return true; 320 } 321 if (expected == Types.CLOB && actual == Types.NVARCHAR) { 322 return true; 323 } 324 if (expected == Types.CLOB && actual == Types.OTHER && actualName.equals("NVARCHAR2")) { 325 return true; 326 } 327 return false; 328 } 329 330 @Override 331 public Serializable getGeneratedId(Connection connection) throws SQLException { 332 if (idType != DialectIdType.SEQUENCE) { 333 return super.getGeneratedId(connection); 334 } 335 String sql = String.format("SELECT %s.NEXTVAL FROM DUAL", idSequenceName); 336 Statement s = connection.createStatement(); 337 ResultSet rs = null; 338 try { 339 rs = s.executeQuery(sql); 340 rs.next(); 341 return Long.valueOf(rs.getLong(1)); 342 } finally { 343 if (rs != null) { 344 rs.close(); 345 } 346 s.close(); 347 } 348 } 349 350 @Override 351 public void setId(PreparedStatement ps, int index, Serializable value) throws SQLException { 352 switch (idType) { 353 case VARCHAR: 354 ps.setObject(index, value); 355 break; 356 case SEQUENCE: 357 setIdLong(ps, index, value); 358 break; 359 default: 360 throw new AssertionError("Unknown id type: " + idType); 361 } 362 } 363 364 @Override 365 public void setToPreparedStatement(PreparedStatement ps, int index, Serializable value, Column column) 366 throws SQLException { 367 switch (column.getJdbcType()) { 368 case Types.VARCHAR: 369 case Types.CLOB: 370 setToPreparedStatementString(ps, index, value, column); 371 return; 372 case Types.BIT: 373 ps.setBoolean(index, ((Boolean) value).booleanValue()); 374 return; 375 case Types.TINYINT: 376 case Types.SMALLINT: 377 ps.setInt(index, ((Long) value).intValue()); 378 return; 379 case Types.INTEGER: 380 case Types.BIGINT: 381 ps.setLong(index, ((Number) value).longValue()); 382 return; 383 case Types.DOUBLE: 384 ps.setDouble(index, ((Double) value).doubleValue()); 385 return; 386 case Types.TIMESTAMP: 387 setToPreparedStatementTimestamp(ps, index, value, column); 388 return; 389 case Types.OTHER: 390 ColumnType type = column.getType(); 391 if (type.isId()) { 392 setId(ps, index, value); 393 return; 394 } else if (type == ColumnType.FTSTORED) { 395 ps.setString(index, (String) value); 396 return; 397 } 398 throw new SQLException("Unhandled type: " + column.getType()); 399 default: 400 throw new SQLException("Unhandled JDBC type: " + column.getJdbcType()); 401 } 402 } 403 404 @Override 405 @SuppressWarnings("boxing") 406 public Serializable getFromResultSet(ResultSet rs, int index, Column column) throws SQLException { 407 switch (column.getJdbcType()) { 408 case Types.VARCHAR: 409 return getFromResultSetString(rs, index, column); 410 case Types.CLOB: 411 // Oracle cannot read CLOBs using rs.getString when the ResultSet is 412 // a ScrollableResultSet (the standard OracleResultSetImpl works 413 // fine). 414 Reader r = rs.getCharacterStream(index); 415 if (r == null) { 416 return null; 417 } 418 StringBuilder sb = new StringBuilder(); 419 try { 420 int n; 421 char[] buffer = new char[4096]; 422 while ((n = r.read(buffer)) != -1) { 423 sb.append(new String(buffer, 0, n)); 424 } 425 } catch (IOException e) { 426 log.error("Cannot read CLOB", e); 427 } 428 return sb.toString(); 429 case Types.BIT: 430 return rs.getBoolean(index); 431 case Types.TINYINT: 432 case Types.SMALLINT: 433 case Types.INTEGER: 434 case Types.BIGINT: 435 return rs.getLong(index); 436 case Types.DOUBLE: 437 return rs.getDouble(index); 438 case Types.TIMESTAMP: 439 return getFromResultSetTimestamp(rs, index, column); 440 } 441 throw new SQLException("Unhandled JDBC type: " + column.getJdbcType()); 442 } 443 444 @Override 445 protected int getMaxNameSize() { 446 return 30; 447 } 448 449 @Override 450 /* Avoid DRG-11439: index name length exceeds maximum of 25 bytes */ 451 protected int getMaxIndexNameSize() { 452 return 25; 453 } 454 455 @Override 456 public String getCreateFulltextIndexSql(String indexName, String quotedIndexName, Table table, List<Column> columns, 457 Model model) { 458 return String.format( 459 "CREATE INDEX %s ON %s(%s) INDEXTYPE IS CTXSYS.CONTEXT " 460 + "PARAMETERS('%s SYNC (ON COMMIT) TRANSACTIONAL')", 461 quotedIndexName, table.getQuotedName(), columns.get(0).getQuotedName(), fulltextParameters); 462 } 463 464 protected static final String CHARS_RESERVED_STR = "%${"; 465 466 protected static final Set<Character> CHARS_RESERVED = new HashSet<>( 467 Arrays.asList(ArrayUtils.toObject(CHARS_RESERVED_STR.toCharArray()))); 468 469 @Override 470 public String getDialectFulltextQuery(String query) { 471 query = query.replace("*", "%"); // reserved, words with it not quoted 472 FulltextQuery ft = FulltextQueryAnalyzer.analyzeFulltextQuery(query); 473 if (ft == null) { 474 return "DONTMATCHANYTHINGFOREMPTYQUERY"; 475 } 476 return FulltextQueryAnalyzer.translateFulltext(ft, "OR", "AND", "NOT", "{", "}", CHARS_RESERVED, "", "", true); 477 } 478 479 // SELECT ..., (SCORE(1) / 100) AS "_nxscore" 480 // FROM ... LEFT JOIN fulltext ON fulltext.id = hierarchy.id 481 // WHERE ... AND CONTAINS(fulltext.fulltext, ?, 1) > 0 482 // ORDER BY "_nxscore" DESC 483 @Override 484 public FulltextMatchInfo getFulltextScoredMatchInfo(String fulltextQuery, String indexName, int nthMatch, 485 Column mainColumn, Model model, Database database) { 486 String indexSuffix = model.getFulltextIndexSuffix(indexName); 487 Table ft = database.getTable(Model.FULLTEXT_TABLE_NAME); 488 Column ftMain = ft.getColumn(Model.MAIN_KEY); 489 Column ftColumn = ft.getColumn(Model.FULLTEXT_FULLTEXT_KEY + indexSuffix); 490 String score = String.format("SCORE(%d)", nthMatch); 491 String nthSuffix = nthMatch == 1 ? "" : String.valueOf(nthMatch); 492 FulltextMatchInfo info = new FulltextMatchInfo(); 493 if (nthMatch == 1) { 494 // Need only one JOIN involving the fulltext table 495 info.joins = Collections.singletonList(new Join(Join.INNER, ft.getQuotedName(), null, null, 496 ftMain.getFullQuotedName(), mainColumn.getFullQuotedName())); 497 } 498 info.whereExpr = String.format("CONTAINS(%s, ?, %d) > 0", ftColumn.getFullQuotedName(), nthMatch); 499 info.whereExprParam = fulltextQuery; 500 info.scoreExpr = String.format("(%s / 100)", score); 501 info.scoreAlias = openQuote() + "_nxscore" + nthSuffix + closeQuote(); 502 info.scoreCol = new Column(mainColumn.getTable(), null, ColumnType.DOUBLE, null); 503 return info; 504 } 505 506 @Override 507 public boolean getMaterializeFulltextSyntheticColumn() { 508 return true; 509 } 510 511 @Override 512 public int getFulltextIndexedColumns() { 513 return 1; 514 } 515 516 @Override 517 public String getLikeEscaping() { 518 return " ESCAPE '\\'"; 519 } 520 521 @Override 522 public boolean supportsUpdateFrom() { 523 throw new UnsupportedOperationException(); 524 } 525 526 @Override 527 public boolean doesUpdateFromRepeatSelf() { 528 throw new UnsupportedOperationException(); 529 } 530 531 @Override 532 public boolean needsOriginalColumnInGroupBy() { 533 // http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/statements_10002.htm#i2080424 534 // The alias can be used in the order_by_clause but not other clauses in 535 // the query. 536 return true; 537 } 538 539 @Override 540 public boolean needsOracleJoins() { 541 return true; 542 } 543 544 @Override 545 public String getClobCast(boolean inOrderBy) { 546 return "CAST(%s AS NVARCHAR2(%d))"; 547 } 548 549 @Override 550 public boolean supportsReadAcl() { 551 return aclOptimizationsEnabled; 552 } 553 554 @Override 555 public String getPrepareUserReadAclsSql() { 556 return "{CALL nx_prepare_user_read_acls(?)}"; 557 } 558 559 @Override 560 public String getReadAclsCheckSql(String userIdCol) { 561 return String.format("%s = nx_hash_users(?)", userIdCol); 562 } 563 564 @Override 565 public String getUpdateReadAclsSql() { 566 return "{CALL nx_update_read_acls}"; 567 } 568 569 @Override 570 public String getRebuildReadAclsSql() { 571 return "{CALL nx_rebuild_read_acls}"; 572 } 573 574 @Override 575 public String getSecurityCheckSql(String idColumnName) { 576 return String.format("NX_ACCESS_ALLOWED(%s, ?, ?) = 1", idColumnName); 577 } 578 579 @Override 580 public boolean supportsFastDescendants() { 581 return pathOptimizationsEnabled; 582 } 583 584 @Override 585 public String getInTreeSql(String idColumnName, String id) { 586 String idParam; 587 switch (idType) { 588 case VARCHAR: 589 idParam = "?"; 590 break; 591 case SEQUENCE: 592 // check that it's really an integer 593 if (id != null && !org.apache.commons.lang.StringUtils.isNumeric(id)) { 594 return null; 595 } 596 idParam = "CAST(? AS NUMBER(10,0))"; 597 break; 598 default: 599 throw new AssertionError("Unknown id type: " + idType); 600 } 601 602 if (pathOptimizationsVersion == 2) { 603 return String.format("EXISTS(SELECT 1 FROM ancestors WHERE hierarchy_id = %s AND ancestor = %s)", 604 idColumnName, idParam); 605 } else if (pathOptimizationsVersion == 1) { 606 // using nested table optim 607 return String.format("EXISTS(SELECT 1 FROM ancestors WHERE hierarchy_id = %s AND %s MEMBER OF ancestors)", 608 idColumnName, idParam); 609 } else { 610 // no optimization 611 return String.format( 612 "%s in (SELECT id FROM hierarchy WHERE LEVEL>1 AND isproperty = 0 START WITH id = %s CONNECT BY PRIOR id = parentid)", 613 idColumnName, idParam); 614 } 615 } 616 617 @Override 618 public boolean isClusteringSupported() { 619 return true; 620 } 621 622 /* 623 * For Oracle we don't use a function to return values and delete them at the same time, because pipelined functions 624 * that need to do DML have to do it in an autonomous transaction which could cause consistency issues. 625 */ 626 @Override 627 public boolean isClusteringDeleteNeeded() { 628 return true; 629 } 630 631 @Override 632 public String getClusterInsertInvalidations() { 633 return "{CALL NX_CLUSTER_INVAL(?, ?, ?, ?)}"; 634 } 635 636 @Override 637 public String getClusterGetInvalidations() { 638 return "SELECT id, fragments, kind FROM cluster_invals WHERE nodeid = ?"; 639 } 640 641 @Override 642 public boolean supportsPaging() { 643 return true; 644 } 645 646 @Override 647 public String addPagingClause(String sql, long limit, long offset) { 648 return String.format( 649 "SELECT * FROM (SELECT /*+ FIRST_ROWS(%d) */ a.*, ROWNUM rnum FROM (%s) a WHERE ROWNUM <= %d) WHERE rnum > %d", 650 limit, sql, limit + offset, offset); 651 } 652 653 @Override 654 public boolean supportsWith() { 655 return false; 656 } 657 658 @Override 659 public boolean supportsArrays() { 660 return true; 661 } 662 663 @Override 664 public boolean supportsArraysReturnInsteadOfRows() { 665 return true; 666 } 667 668 @Override 669 public Serializable[] getArrayResult(Array array) throws SQLException { 670 Serializable[] ids; 671 if (array.getBaseType() == Types.NUMERIC) { 672 long[] longs; 673 try { 674 longs = (long[]) arrayGetLongArrayMethod.invoke(array); 675 } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { 676 throw new RuntimeException(e); 677 } 678 ids = new Serializable[longs.length]; 679 for (int i = 0; i < ids.length; i++) { 680 ids[i] = Long.valueOf(longs[i]); 681 } 682 } else { 683 ids = (Serializable[]) array.getArray(); 684 } 685 return ids; 686 } 687 688 @Override 689 public boolean hasNullEmptyString() { 690 return true; 691 } 692 693 @Override 694 public Array createArrayOf(int type, Object[] elements, Connection connection) throws SQLException { 695 if (elements == null || elements.length == 0) { 696 return null; 697 } 698 String typeName; 699 switch (type) { 700 case Types.VARCHAR: 701 typeName = "NX_STRING_TABLE"; 702 break; 703 case Types.OTHER: // id 704 switch (idType) { 705 case VARCHAR: 706 typeName = "NX_STRING_TABLE"; 707 break; 708 case SEQUENCE: 709 typeName = "NX_INT_TABLE"; 710 break; 711 default: 712 throw new AssertionError("Unknown id type: " + idType); 713 } 714 break; 715 default: 716 throw new AssertionError("Unknown type: " + type); 717 } 718 connection = ConnectionHelper.unwrap(connection); 719 try { 720 Object arrayDescriptor = arrayDescriptorConstructor.newInstance(typeName, connection); 721 return (Array) arrayConstructor.newInstance(arrayDescriptor, connection, elements); 722 } catch (ReflectiveOperationException e) { 723 throw new SQLException(e); 724 } 725 } 726 727 @Override 728 public String getSQLStatementsFilename() { 729 return "nuxeovcs/oracle.sql.txt"; 730 } 731 732 @Override 733 public String getTestSQLStatementsFilename() { 734 return "nuxeovcs/oracle.test.sql.txt"; 735 } 736 737 @Override 738 public Map<String, Serializable> getSQLStatementsProperties(Model model, Database database) { 739 Map<String, Serializable> properties = new HashMap<>(); 740 switch (idType) { 741 case VARCHAR: 742 properties.put("idType", "VARCHAR2(36)"); 743 properties.put("idTypeParam", "VARCHAR2"); 744 properties.put("idArrayType", "NX_STRING_TABLE"); 745 properties.put("idNotPresent", "'-'"); 746 properties.put("sequenceEnabled", Boolean.FALSE); 747 break; 748 case SEQUENCE: 749 properties.put("idType", "NUMBER(10,0)"); 750 properties.put("idTypeParam", "NUMBER"); 751 properties.put("idArrayType", "NX_INT_TABLE"); 752 properties.put("idNotPresent", "-1"); 753 properties.put("sequenceEnabled", Boolean.TRUE); 754 properties.put("idSequenceName", idSequenceName); 755 break; 756 default: 757 throw new AssertionError("Unknown id type: " + idType); 758 } 759 properties.put("aclOptimizationsEnabled", Boolean.valueOf(aclOptimizationsEnabled)); 760 properties.put("pathOptimizationsEnabled", Boolean.valueOf(pathOptimizationsEnabled)); 761 properties.put("pathOptimizationsVersion1", pathOptimizationsVersion == 1); 762 properties.put("pathOptimizationsVersion2", pathOptimizationsVersion == 2); 763 properties.put("fulltextEnabled", Boolean.valueOf(!fulltextDisabled)); 764 properties.put("fulltextSearchEnabled", Boolean.valueOf(!fulltextSearchDisabled)); 765 properties.put("clusteringEnabled", Boolean.valueOf(clusteringEnabled)); 766 properties.put("proxiesEnabled", Boolean.valueOf(proxiesEnabled)); 767 properties.put("softDeleteEnabled", Boolean.valueOf(softDeleteEnabled)); 768 if (!fulltextSearchDisabled) { 769 Table ft = database.getTable(Model.FULLTEXT_TABLE_NAME); 770 properties.put("fulltextTable", ft.getQuotedName()); 771 FulltextConfiguration fti = model.getFulltextConfiguration(); 772 List<String> lines = new ArrayList<>(fti.indexNames.size()); 773 for (String indexName : fti.indexNames) { 774 String suffix = model.getFulltextIndexSuffix(indexName); 775 Column ftft = ft.getColumn(Model.FULLTEXT_FULLTEXT_KEY + suffix); 776 Column ftst = ft.getColumn(Model.FULLTEXT_SIMPLETEXT_KEY + suffix); 777 Column ftbt = ft.getColumn(Model.FULLTEXT_BINARYTEXT_KEY + suffix); 778 String line = String.format(" :NEW.%s := :NEW.%s || ' ' || :NEW.%s; ", ftft.getQuotedName(), 779 ftst.getQuotedName(), ftbt.getQuotedName()); 780 lines.add(line); 781 } 782 properties.put("fulltextTriggerStatements", String.join("\n", lines)); 783 } 784 String[] permissions = NXCore.getSecurityService().getPermissionsToCheck(SecurityConstants.BROWSE); 785 List<String> permsList = new LinkedList<>(); 786 for (String perm : permissions) { 787 permsList.add(String.format(" INTO ACLR_PERMISSION VALUES ('%s')", perm)); 788 } 789 properties.put("readPermissions", String.join("\n", permsList)); 790 properties.put("usersSeparator", getUsersSeparator()); 791 properties.put("everyone", SecurityConstants.EVERYONE); 792 return properties; 793 } 794 795 protected int getOracleErrorCode(Throwable t) { 796 try { 797 Method m = t.getClass().getMethod("getOracleError"); 798 Integer oracleError = (Integer) m.invoke(t); 799 if (oracleError != null) { 800 int errorCode = oracleError.intValue(); 801 if (errorCode != 0) { 802 return errorCode; 803 } 804 } 805 } catch (ReflectiveOperationException e) { 806 // ignore 807 } 808 if (t instanceof SQLException) { 809 return ((SQLException) t).getErrorCode(); 810 } 811 return 0; 812 } 813 814 protected boolean isConnectionClosed(int oracleError) { 815 switch (oracleError) { 816 case 28: // your session has been killed. 817 case 1033: // Oracle initialization or shudown in progress. 818 case 1034: // Oracle not available 819 case 1041: // internal error. hostdef extension doesn't exist 820 case 1089: // immediate shutdown in progress - no operations are permitted 821 case 1090: // shutdown in progress - connection is not permitted 822 case 3113: // end-of-file on communication channel 823 case 3114: // not connected to ORACLE 824 case 12571: // TNS:packet writer failure 825 case 17002: // IO Exception 826 case 17008: // Closed Connection 827 case 17410: // No more data to read from socket 828 case 24768: // commit protocol error occured in the server 829 return true; 830 } 831 return false; 832 } 833 834 @Override 835 public boolean isConcurrentUpdateException(Throwable t) { 836 while (t.getCause() != null) { 837 t = t.getCause(); 838 } 839 switch (getOracleErrorCode(t)) { 840 case 1: // ORA-00001: unique constraint violated 841 case 60: // ORA-00060: deadlock detected while waiting for resource 842 case 2291: // ORA-02291: integrity constraint ... violated - parent key not found 843 return true; 844 } 845 return false; 846 } 847 848 @Override 849 public String getValidationQuery() { 850 return "SELECT 1 FROM DUAL"; 851 } 852 853 @Override 854 public String getBlobLengthFunction() { 855 return "LENGTHB"; 856 } 857 858 @Override 859 public List<String> getPostCreateIdentityColumnSql(Column column) { 860 String table = column.getTable().getPhysicalName(); 861 String col = column.getPhysicalName(); 862 String seq = table + "_IDSEQ"; 863 String trig = table + "_IDTRIG"; 864 String createSeq = String.format("CREATE SEQUENCE \"%s\"", seq); 865 String createTrig = String.format("CREATE TRIGGER \"%s\"\n" // 866 + " BEFORE INSERT ON \"%s\"\n" // 867 + " FOR EACH ROW WHEN (NEW.\"%s\" IS NULL)\n" // 868 + "BEGIN\n" // 869 + " SELECT \"%s\".NEXTVAL INTO :NEW.\"%s\" FROM DUAL;\n" // 870 + "END;", trig, table, col, seq, col); 871 return Arrays.asList(createSeq, createTrig); 872 } 873 874 @Override 875 public boolean hasIdentityGeneratedKey() { 876 return false; 877 } 878 879 @Override 880 public String getIdentityGeneratedKeySql(Column column) { 881 String table = column.getTable().getPhysicalName(); 882 String seq = table + "_IDSEQ"; 883 return String.format("SELECT \"%s\".CURRVAL FROM DUAL", seq); 884 } 885 886 @Override 887 public String getAncestorsIdsSql() { 888 return "SELECT NX_ANCESTORS(?) FROM DUAL"; 889 } 890 891 @Override 892 public boolean needsNullsLastOnDescSort() { 893 return true; 894 } 895 896 @Override 897 public String getDateCast() { 898 // CAST(%s AS DATE) doesn't work, it doesn't compare exactly to DATE 899 // literals because the internal representation seems to be a float and 900 // CAST AS DATE does not truncate it 901 return "TRUNC(%s)"; 902 } 903 904 @Override 905 public String castIdToVarchar(String expr) { 906 switch (idType) { 907 case VARCHAR: 908 return expr; 909 case SEQUENCE: 910 return "CAST(" + expr + " AS VARCHAR2(36))"; 911 default: 912 throw new AssertionError("Unknown id type: " + idType); 913 } 914 } 915 916 @Override 917 public DialectIdType getIdType() { 918 return idType; 919 } 920 921 public String getUsersSeparator() { 922 if (usersSeparator == null) { 923 return DEFAULT_USERS_SEPARATOR; 924 } 925 return usersSeparator; 926 } 927 928 @Override 929 public String getSoftDeleteSql() { 930 return "{CALL NX_DELETE(?, ?)}"; 931 } 932 933 @Override 934 public String getSoftDeleteCleanupSql() { 935 return "{CALL NX_DELETE_PURGE(?, ?, ?)}"; 936 } 937 938 @Override 939 public List<String> getStartupSqls(Model model, Database database) { 940 if (aclOptimizationsEnabled) { 941 log.info("Vacuuming tables used by optimized acls"); 942 return Collections.singletonList("{CALL nx_vacuum_read_acls}"); 943 } 944 return Collections.emptyList(); 945 } 946 947 @Override 948 public List<String> checkStoredProcedure(String procName, String procCreate, String ddlMode, Connection connection, 949 JDBCLogger logger, Map<String, Serializable> properties) throws SQLException { 950 boolean compatCheck = ddlMode.contains(RepositoryDescriptor.DDL_MODE_COMPAT); 951 if (compatCheck) { 952 procCreate = "CREATE OR REPLACE " + procCreate.substring("create ".length()); 953 return Collections.singletonList(procCreate); 954 } 955 try (Statement st = connection.createStatement()) { 956 String getBody; 957 if (procCreate.toLowerCase().startsWith("create trigger ")) { 958 getBody = "SELECT TRIGGER_BODY FROM USER_TRIGGERS WHERE TRIGGER_NAME = '" + procName + "'"; 959 } else { 960 // works for TYPE, FUNCTION, PROCEDURE 961 getBody = "SELECT TEXT FROM ALL_SOURCE WHERE NAME = '" + procName + "' ORDER BY LINE"; 962 } 963 logger.log(getBody); 964 try (ResultSet rs = st.executeQuery(getBody)) { 965 if (rs.next()) { 966 List<String> lines = new ArrayList<>(); 967 do { 968 lines.add(rs.getString(1)); 969 } while (rs.next()); 970 String body = org.apache.commons.lang.StringUtils.join(lines, ' '); 971 if (normalizeString(procCreate).contains(normalizeString(body))) { 972 logger.log(" -> exists, unchanged"); 973 return Collections.emptyList(); 974 } else { 975 logger.log(" -> exists, old"); 976 if (!procCreate.toLowerCase().startsWith("create ")) { 977 throw new NuxeoException("Should start with CREATE: " + procCreate); 978 } 979 procCreate = "CREATE OR REPLACE " + procCreate.substring("create ".length()); 980 return Collections.singletonList(procCreate); 981 } 982 } else { 983 logger.log(" -> missing"); 984 return Collections.singletonList(procCreate); 985 } 986 } 987 } 988 } 989 990 protected static String normalizeString(String string) { 991 return string.replaceAll("[ \n\r\t]+", " ").trim(); 992 } 993 994 @Override 995 public String getSQLForDump(String sql) { 996 String sqll = sql.toLowerCase(); 997 if (sqll.startsWith("{call ")) { 998 // transform something used for JDBC calls into a proper SQL*Plus dump 999 return "EXECUTE " + sql.substring("{call ".length(), sql.length() - 1); // without ; or / 1000 } 1001 if (sqll.endsWith("end")) { 1002 sql += ";"; 1003 } 1004 return sql + "\n/"; 1005 } 1006 1007}