/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Florent Guillaume
* Benoit Delbosc
*/
package org.nuxeo.ecm.core.storage.sql.jdbc.dialect;
import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Array;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.transaction.xa.XAException;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.StringUtils;
import org.nuxeo.ecm.core.NXCore;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.storage.FulltextConfiguration;
import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer;
import org.nuxeo.ecm.core.storage.FulltextQueryAnalyzer.FulltextQuery;
import org.nuxeo.ecm.core.storage.sql.ColumnType;
import org.nuxeo.ecm.core.storage.sql.Model;
import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor;
import org.nuxeo.ecm.core.storage.sql.jdbc.JDBCLogger;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Join;
import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
import org.nuxeo.runtime.datasource.ConnectionHelper;
/**
* Oracle-specific dialect.
*
* @author Florent Guillaume
*/
public class DialectOracle extends Dialect {
private static final Log log = LogFactory.getLog(DialectOracle.class);
private Constructor<?> arrayDescriptorConstructor;
private Constructor<?> arrayConstructor;
private Method arrayGetLongArrayMethod;
protected final String fulltextParameters;
protected boolean pathOptimizationsEnabled;
protected int pathOptimizationsVersion = 0;
private static final String DEFAULT_USERS_SEPARATOR = "|";
protected String usersSeparator;
protected final DialectIdType idType;
protected String idSequenceName;
protected int majorVersion;
protected XAErrorLogger xaErrorLogger;
protected static class XAErrorLogger {
protected final Class<?> oracleXAExceptionClass;
protected final Method m_xaError;
protected final Method m_xaErrorMessage;
protected final Method m_oracleError;
protected final Method m_oracleSQLError;
public XAErrorLogger() throws ReflectiveOperationException {
oracleXAExceptionClass = Thread.currentThread()
.getContextClassLoader()
.loadClass("oracle.jdbc.xa.OracleXAException");
m_xaError = oracleXAExceptionClass.getMethod("getXAError");
m_xaErrorMessage = oracleXAExceptionClass.getMethod("getXAErrorMessage", m_xaError.getReturnType());
m_oracleError = oracleXAExceptionClass.getMethod("getOracleError");
m_oracleSQLError = oracleXAExceptionClass.getMethod("getOracleSQLError");
}
public void log(XAException e) throws ReflectiveOperationException {
int xaError = ((Integer) m_xaError.invoke(e)).intValue();
String xaErrorMessage = (String) m_xaErrorMessage.invoke(xaError);
int oracleError = ((Integer) m_oracleError.invoke(e)).intValue();
int oracleSQLError = ((Integer) m_oracleSQLError.invoke(e)).intValue();
StringBuilder builder = new StringBuilder();
builder.append("Oracle XA Error : ").append(xaError).append(" (").append(xaErrorMessage).append("),");
builder.append("Oracle Error : ").append(oracleError).append(",");
builder.append("Oracle SQL Error : ").append(oracleSQLError);
log.warn(builder.toString(), e);
}
}
public DialectOracle(DatabaseMetaData metadata, RepositoryDescriptor repositoryDescriptor) {
super(metadata, repositoryDescriptor);
try {
majorVersion = metadata.getDatabaseMajorVersion();
} catch (SQLException e) {
throw new NuxeoException(e);
}
fulltextParameters = repositoryDescriptor == null ? null
: repositoryDescriptor.getFulltextAnalyzer() == null ? "" : repositoryDescriptor.getFulltextAnalyzer();
pathOptimizationsEnabled = repositoryDescriptor != null && repositoryDescriptor.getPathOptimizationsEnabled();
if (pathOptimizationsEnabled) {
pathOptimizationsVersion = repositoryDescriptor.getPathOptimizationsVersion();
}
usersSeparator = repositoryDescriptor == null ? null
: repositoryDescriptor.usersSeparatorKey == null ? DEFAULT_USERS_SEPARATOR
: repositoryDescriptor.usersSeparatorKey;
String idt = repositoryDescriptor == null ? null : repositoryDescriptor.idType;
if (idt == null || "".equals(idt) || "varchar".equalsIgnoreCase(idt)) {
idType = DialectIdType.VARCHAR;
} else if (idt.toLowerCase().startsWith("sequence")) {
idType = DialectIdType.SEQUENCE;
if (idt.toLowerCase().startsWith("sequence:")) {
String[] split = idt.split(":");
idSequenceName = split[1].toUpperCase(Locale.ENGLISH);
} else {
idSequenceName = "HIERARCHY_SEQ";
}
} else {
throw new NuxeoException("Unknown id type: '" + idt + "'");
}
xaErrorLogger = newXAErrorLogger();
initArrayReflection();
}
protected XAErrorLogger newXAErrorLogger() {
try {
return new XAErrorLogger();
} catch (ReflectiveOperationException e) {
log.warn("Cannot initialize xa error loggger", e);
return null;
}
}
// use reflection to avoid linking dependencies
private void initArrayReflection() {
try {
Class<?> arrayDescriptorClass = Class.forName("oracle.sql.ArrayDescriptor");
arrayDescriptorConstructor = arrayDescriptorClass.getConstructor(String.class, Connection.class);
Class<?> arrayClass = Class.forName("oracle.sql.ARRAY");
arrayConstructor = arrayClass.getConstructor(arrayDescriptorClass, Connection.class, Object.class);
arrayGetLongArrayMethod = arrayClass.getDeclaredMethod("getLongArray");
} catch (ClassNotFoundException e) {
// query syntax unit test run without Oracle JDBC driver
return;
} catch (ReflectiveOperationException e) {
throw new NuxeoException(e);
}
}
@Override
public String getNoColumnsInsertString(Column idColumn) {
// INSERT INTO foo () VALUES () or DEFAULT VALUES is not legal for Oracle, you need at least one column
return String.format("(%s) VALUES (DEFAULT)", idColumn.getQuotedName());
}
@Override
public String getConnectionSchema(Connection connection) throws SQLException {
Statement st = connection.createStatement();
String sql = "SELECT SYS_CONTEXT('USERENV', 'SESSION_USER') FROM DUAL";
log.trace("SQL: " + sql);
ResultSet rs = st.executeQuery(sql);
rs.next();
String user = rs.getString(1);
log.trace("SQL: -> " + user);
rs.close();
st.close();
return user;
}
@Override
public String getCascadeDropConstraintsString() {
return " CASCADE CONSTRAINTS";
}
@Override
public String getAddColumnString() {
return "ADD";
}
@Override
public JDBCInfo getJDBCTypeAndString(ColumnType type) {
switch (type.spec) {
case STRING:
if (type.isUnconstrained()) {
return jdbcInfo("NVARCHAR2(2000)", Types.VARCHAR);
} else if (type.isClob() || type.length > 2000) {
return jdbcInfo("NCLOB", Types.CLOB);
} else {
return jdbcInfo("NVARCHAR2(%d)", type.length, Types.VARCHAR);
}
case BOOLEAN:
return jdbcInfo("NUMBER(1,0)", Types.BIT);
case LONG:
return jdbcInfo("NUMBER(19,0)", Types.BIGINT);
case DOUBLE:
return jdbcInfo("DOUBLE PRECISION", Types.DOUBLE);
case TIMESTAMP:
return jdbcInfo("TIMESTAMP", Types.TIMESTAMP);
case BLOBID:
return jdbcInfo("VARCHAR2(250)", Types.VARCHAR);
// -----
case NODEID:
case NODEIDFK:
case NODEIDFKNP:
case NODEIDFKMUL:
case NODEIDFKNULL:
case NODEIDPK:
case NODEVAL:
switch (idType) {
case VARCHAR:
return jdbcInfo("VARCHAR2(36)", Types.VARCHAR);
case SEQUENCE:
return jdbcInfo("NUMBER(10,0)", Types.INTEGER);
default:
throw new AssertionError("Unknown id type: " + idType);
}
case SYSNAME:
case SYSNAMEARRAY:
return jdbcInfo("VARCHAR2(250)", Types.VARCHAR);
case TINYINT:
return jdbcInfo("NUMBER(3,0)", Types.TINYINT);
case INTEGER:
return jdbcInfo("NUMBER(10,0)", Types.INTEGER);
case AUTOINC:
return jdbcInfo("NUMBER(10,0)", Types.INTEGER);
case FTINDEXED:
return jdbcInfo("CLOB", Types.CLOB);
case FTSTORED:
return jdbcInfo("NCLOB", Types.CLOB);
case CLUSTERNODE:
return jdbcInfo("VARCHAR(25)", Types.VARCHAR);
case CLUSTERFRAGS:
return jdbcInfo("VARCHAR2(4000)", Types.VARCHAR);
}
throw new AssertionError(type);
}
@Override
public boolean isAllowedConversion(int expected, int actual, String actualName, int actualSize) {
// Oracle internal conversions
if (expected == Types.DOUBLE && actual == Types.FLOAT) {
return true;
}
if (expected == Types.VARCHAR && actual == Types.NVARCHAR) {
return true;
}
if (expected == Types.VARCHAR && actual == Types.OTHER && actualName.equals("NVARCHAR2")) {
return true;
}
if (expected == Types.CLOB && actual == Types.NCLOB) {
return true;
}
if (expected == Types.CLOB && actual == Types.OTHER && actualName.equals("NCLOB")) {
return true;
}
if (expected == Types.BIT && actual == Types.DECIMAL && actualSize == 1) {
return true;
}
if (expected == Types.TINYINT && actual == Types.DECIMAL && actualSize == 3) {
return true;
}
if (expected == Types.INTEGER && actual == Types.DECIMAL && actualSize == 10) {
return true;
}
if (expected == Types.BIGINT && actual == Types.DECIMAL && actualSize == 19) {
return true;
}
if (expected == Types.BIGINT && actual == Types.DECIMAL && actualSize == 38) {
return true;
}
// CLOB vs VARCHAR compatibility
if (expected == Types.VARCHAR && actual == Types.NCLOB) {
return true;
}
if (expected == Types.VARCHAR && actual == Types.OTHER && actualName.equals("NCLOB")) {
return true;
}
if (expected == Types.CLOB && actual == Types.VARCHAR) {
return true;
}
if (expected == Types.CLOB && actual == Types.NVARCHAR) {
return true;
}
if (expected == Types.CLOB && actual == Types.OTHER && actualName.equals("NVARCHAR2")) {
return true;
}
return false;
}
@Override
public Serializable getGeneratedId(Connection connection) throws SQLException {
if (idType != DialectIdType.SEQUENCE) {
return super.getGeneratedId(connection);
}
String sql = String.format("SELECT %s.NEXTVAL FROM DUAL", idSequenceName);
Statement s = connection.createStatement();
ResultSet rs = null;
try {
rs = s.executeQuery(sql);
rs.next();
return Long.valueOf(rs.getLong(1));
} finally {
if (rs != null) {
rs.close();
}
s.close();
}
}
@Override
public void setId(PreparedStatement ps, int index, Serializable value) throws SQLException {
switch (idType) {
case VARCHAR:
ps.setObject(index, value);
break;
case SEQUENCE:
setIdLong(ps, index, value);
break;
default:
throw new AssertionError("Unknown id type: " + idType);
}
}
@Override
public void setToPreparedStatement(PreparedStatement ps, int index, Serializable value, Column column)
throws SQLException {
switch (column.getJdbcType()) {
case Types.VARCHAR:
case Types.CLOB:
setToPreparedStatementString(ps, index, value, column);
return;
case Types.BIT:
ps.setBoolean(index, ((Boolean) value).booleanValue());
return;
case Types.TINYINT:
case Types.SMALLINT:
ps.setInt(index, ((Long) value).intValue());
return;
case Types.INTEGER:
case Types.BIGINT:
ps.setLong(index, ((Number) value).longValue());
return;
case Types.DOUBLE:
ps.setDouble(index, ((Double) value).doubleValue());
return;
case Types.TIMESTAMP:
setToPreparedStatementTimestamp(ps, index, value, column);
return;
case Types.OTHER:
ColumnType type = column.getType();
if (type.isId()) {
setId(ps, index, value);
return;
} else if (type == ColumnType.FTSTORED) {
ps.setString(index, (String) value);
return;
}
throw new SQLException("Unhandled type: " + column.getType());
default:
throw new SQLException("Unhandled JDBC type: " + column.getJdbcType());
}
}
@Override
@SuppressWarnings("boxing")
public Serializable getFromResultSet(ResultSet rs, int index, Column column) throws SQLException {
switch (column.getJdbcType()) {
case Types.VARCHAR:
return getFromResultSetString(rs, index, column);
case Types.CLOB:
// Oracle cannot read CLOBs using rs.getString when the ResultSet is
// a ScrollableResultSet (the standard OracleResultSetImpl works
// fine).
Reader r = rs.getCharacterStream(index);
if (r == null) {
return null;
}
StringBuilder sb = new StringBuilder();
try {
int n;
char[] buffer = new char[4096];
while ((n = r.read(buffer)) != -1) {
sb.append(new String(buffer, 0, n));
}
} catch (IOException e) {
log.error("Cannot read CLOB", e);
}
return sb.toString();
case Types.BIT:
return rs.getBoolean(index);
case Types.TINYINT:
case Types.SMALLINT:
case Types.INTEGER:
case Types.BIGINT:
return rs.getLong(index);
case Types.DOUBLE:
return rs.getDouble(index);
case Types.TIMESTAMP:
return getFromResultSetTimestamp(rs, index, column);
}
throw new SQLException("Unhandled JDBC type: " + column.getJdbcType());
}
@Override
protected int getMaxNameSize() {
return 30;
}
@Override
/* Avoid DRG-11439: index name length exceeds maximum of 25 bytes */
protected int getMaxIndexNameSize() {
return 25;
}
@Override
public String getCreateFulltextIndexSql(String indexName, String quotedIndexName, Table table, List<Column> columns,
Model model) {
return String.format(
"CREATE INDEX %s ON %s(%s) INDEXTYPE IS CTXSYS.CONTEXT "
+ "PARAMETERS('%s SYNC (ON COMMIT) TRANSACTIONAL')",
quotedIndexName, table.getQuotedName(), columns.get(0).getQuotedName(), fulltextParameters);
}
protected static final String CHARS_RESERVED_STR = "%${";
protected static final Set<Character> CHARS_RESERVED = new HashSet<>(
Arrays.asList(ArrayUtils.toObject(CHARS_RESERVED_STR.toCharArray())));
@Override
public String getDialectFulltextQuery(String query) {
query = query.replace("*", "%"); // reserved, words with it not quoted
FulltextQuery ft = FulltextQueryAnalyzer.analyzeFulltextQuery(query);
if (ft == null) {
return "DONTMATCHANYTHINGFOREMPTYQUERY";
}
return FulltextQueryAnalyzer.translateFulltext(ft, "OR", "AND", "NOT", "{", "}", CHARS_RESERVED, "", "", true);
}
// SELECT ..., (SCORE(1) / 100) AS "_nxscore"
// FROM ... LEFT JOIN fulltext ON fulltext.id = hierarchy.id
// WHERE ... AND CONTAINS(fulltext.fulltext, ?, 1) > 0
// ORDER BY "_nxscore" DESC
@Override
public FulltextMatchInfo getFulltextScoredMatchInfo(String fulltextQuery, String indexName, int nthMatch,
Column mainColumn, Model model, Database database) {
String indexSuffix = model.getFulltextIndexSuffix(indexName);
Table ft = database.getTable(Model.FULLTEXT_TABLE_NAME);
Column ftMain = ft.getColumn(Model.MAIN_KEY);
Column ftColumn = ft.getColumn(Model.FULLTEXT_FULLTEXT_KEY + indexSuffix);
String score = String.format("SCORE(%d)", nthMatch);
String nthSuffix = nthMatch == 1 ? "" : String.valueOf(nthMatch);
FulltextMatchInfo info = new FulltextMatchInfo();
if (nthMatch == 1) {
// Need only one JOIN involving the fulltext table
info.joins = Collections.singletonList(new Join(Join.INNER, ft.getQuotedName(), null, null,
ftMain.getFullQuotedName(), mainColumn.getFullQuotedName()));
}
info.whereExpr = String.format("CONTAINS(%s, ?, %d) > 0", ftColumn.getFullQuotedName(), nthMatch);
info.whereExprParam = fulltextQuery;
info.scoreExpr = String.format("(%s / 100)", score);
info.scoreAlias = openQuote() + "_nxscore" + nthSuffix + closeQuote();
info.scoreCol = new Column(mainColumn.getTable(), null, ColumnType.DOUBLE, null);
return info;
}
@Override
public boolean getMaterializeFulltextSyntheticColumn() {
return true;
}
@Override
public int getFulltextIndexedColumns() {
return 1;
}
@Override
public String getLikeEscaping() {
return " ESCAPE '\\'";
}
@Override
public boolean supportsUpdateFrom() {
throw new UnsupportedOperationException();
}
@Override
public boolean doesUpdateFromRepeatSelf() {
throw new UnsupportedOperationException();
}
@Override
public boolean needsOriginalColumnInGroupBy() {
// http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/statements_10002.htm#i2080424
// The alias can be used in the order_by_clause but not other clauses in
// the query.
return true;
}
@Override
public boolean needsOracleJoins() {
return true;
}
@Override
public String getClobCast(boolean inOrderBy) {
return "CAST(%s AS NVARCHAR2(%d))";
}
@Override
public boolean supportsReadAcl() {
return aclOptimizationsEnabled;
}
@Override
public String getPrepareUserReadAclsSql() {
return "{CALL nx_prepare_user_read_acls(?)}";
}
@Override
public String getReadAclsCheckSql(String userIdCol) {
return String.format("%s = nx_hash_users(?)", userIdCol);
}
@Override
public String getUpdateReadAclsSql() {
return "{CALL nx_update_read_acls}";
}
@Override
public String getRebuildReadAclsSql() {
return "{CALL nx_rebuild_read_acls}";
}
@Override
public String getSecurityCheckSql(String idColumnName) {
return String.format("NX_ACCESS_ALLOWED(%s, ?, ?) = 1", idColumnName);
}
@Override
public boolean supportsFastDescendants() {
return pathOptimizationsEnabled;
}
@Override
public String getInTreeSql(String idColumnName, String id) {
String idParam;
switch (idType) {
case VARCHAR:
idParam = "?";
break;
case SEQUENCE:
// check that it's really an integer
if (id != null && !org.apache.commons.lang.StringUtils.isNumeric(id)) {
return null;
}
idParam = "CAST(? AS NUMBER(10,0))";
break;
default:
throw new AssertionError("Unknown id type: " + idType);
}
if (pathOptimizationsVersion == 2) {
return String.format("EXISTS(SELECT 1 FROM ancestors WHERE hierarchy_id = %s AND ancestor = %s)",
idColumnName, idParam);
} else if (pathOptimizationsVersion == 1) {
// using nested table optim
return String.format("EXISTS(SELECT 1 FROM ancestors WHERE hierarchy_id = %s AND %s MEMBER OF ancestors)",
idColumnName, idParam);
} else {
// no optimization
return String.format(
"%s in (SELECT id FROM hierarchy WHERE LEVEL>1 AND isproperty = 0 START WITH id = %s CONNECT BY PRIOR id = parentid)",
idColumnName, idParam);
}
}
@Override
public boolean isClusteringSupported() {
return true;
}
/*
* For Oracle we don't use a function to return values and delete them at the same time, because pipelined functions
* that need to do DML have to do it in an autonomous transaction which could cause consistency issues.
*/
@Override
public boolean isClusteringDeleteNeeded() {
return true;
}
@Override
public String getClusterInsertInvalidations() {
return "{CALL NX_CLUSTER_INVAL(?, ?, ?, ?)}";
}
@Override
public String getClusterGetInvalidations() {
return "SELECT id, fragments, kind FROM cluster_invals WHERE nodeid = ?";
}
@Override
public boolean supportsPaging() {
return true;
}
@Override
public String addPagingClause(String sql, long limit, long offset) {
return String.format(
"SELECT * FROM (SELECT /*+ FIRST_ROWS(%d) */ a.*, ROWNUM rnum FROM (%s) a WHERE ROWNUM <= %d) WHERE rnum > %d",
limit, sql, limit + offset, offset);
}
@Override
public boolean supportsWith() {
return false;
}
@Override
public boolean supportsArrays() {
return true;
}
@Override
public boolean supportsArraysReturnInsteadOfRows() {
return true;
}
@Override
public Serializable[] getArrayResult(Array array) throws SQLException {
Serializable[] ids;
if (array.getBaseType() == Types.NUMERIC) {
long[] longs;
try {
longs = (long[]) arrayGetLongArrayMethod.invoke(array);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e);
}
ids = new Serializable[longs.length];
for (int i = 0; i < ids.length; i++) {
ids[i] = Long.valueOf(longs[i]);
}
} else {
ids = (Serializable[]) array.getArray();
}
return ids;
}
@Override
public boolean hasNullEmptyString() {
return true;
}
@Override
public Array createArrayOf(int type, Object[] elements, Connection connection) throws SQLException {
if (elements == null || elements.length == 0) {
return null;
}
String typeName;
switch (type) {
case Types.VARCHAR:
typeName = "NX_STRING_TABLE";
break;
case Types.OTHER: // id
switch (idType) {
case VARCHAR:
typeName = "NX_STRING_TABLE";
break;
case SEQUENCE:
typeName = "NX_INT_TABLE";
break;
default:
throw new AssertionError("Unknown id type: " + idType);
}
break;
default:
throw new AssertionError("Unknown type: " + type);
}
connection = ConnectionHelper.unwrap(connection);
try {
Object arrayDescriptor = arrayDescriptorConstructor.newInstance(typeName, connection);
return (Array) arrayConstructor.newInstance(arrayDescriptor, connection, elements);
} catch (ReflectiveOperationException e) {
throw new SQLException(e);
}
}
@Override
public String getSQLStatementsFilename() {
return "nuxeovcs/oracle.sql.txt";
}
@Override
public String getTestSQLStatementsFilename() {
return "nuxeovcs/oracle.test.sql.txt";
}
@Override
public Map<String, Serializable> getSQLStatementsProperties(Model model, Database database) {
Map<String, Serializable> properties = new HashMap<>();
switch (idType) {
case VARCHAR:
properties.put("idType", "VARCHAR2(36)");
properties.put("idTypeParam", "VARCHAR2");
properties.put("idArrayType", "NX_STRING_TABLE");
properties.put("idNotPresent", "'-'");
properties.put("sequenceEnabled", Boolean.FALSE);
break;
case SEQUENCE:
properties.put("idType", "NUMBER(10,0)");
properties.put("idTypeParam", "NUMBER");
properties.put("idArrayType", "NX_INT_TABLE");
properties.put("idNotPresent", "-1");
properties.put("sequenceEnabled", Boolean.TRUE);
properties.put("idSequenceName", idSequenceName);
break;
default:
throw new AssertionError("Unknown id type: " + idType);
}
properties.put("aclOptimizationsEnabled", Boolean.valueOf(aclOptimizationsEnabled));
properties.put("pathOptimizationsEnabled", Boolean.valueOf(pathOptimizationsEnabled));
properties.put("pathOptimizationsVersion1", pathOptimizationsVersion == 1);
properties.put("pathOptimizationsVersion2", pathOptimizationsVersion == 2);
properties.put("fulltextEnabled", Boolean.valueOf(!fulltextDisabled));
properties.put("fulltextSearchEnabled", Boolean.valueOf(!fulltextSearchDisabled));
properties.put("clusteringEnabled", Boolean.valueOf(clusteringEnabled));
properties.put("proxiesEnabled", Boolean.valueOf(proxiesEnabled));
properties.put("softDeleteEnabled", Boolean.valueOf(softDeleteEnabled));
if (!fulltextSearchDisabled) {
Table ft = database.getTable(Model.FULLTEXT_TABLE_NAME);
properties.put("fulltextTable", ft.getQuotedName());
FulltextConfiguration fti = model.getFulltextConfiguration();
List<String> lines = new ArrayList<>(fti.indexNames.size());
for (String indexName : fti.indexNames) {
String suffix = model.getFulltextIndexSuffix(indexName);
Column ftft = ft.getColumn(Model.FULLTEXT_FULLTEXT_KEY + suffix);
Column ftst = ft.getColumn(Model.FULLTEXT_SIMPLETEXT_KEY + suffix);
Column ftbt = ft.getColumn(Model.FULLTEXT_BINARYTEXT_KEY + suffix);
String line = String.format(" :NEW.%s := :NEW.%s || ' ' || :NEW.%s; ", ftft.getQuotedName(),
ftst.getQuotedName(), ftbt.getQuotedName());
lines.add(line);
}
properties.put("fulltextTriggerStatements", String.join("\n", lines));
}
String[] permissions = NXCore.getSecurityService().getPermissionsToCheck(SecurityConstants.BROWSE);
List<String> permsList = new LinkedList<>();
for (String perm : permissions) {
permsList.add(String.format(" INTO ACLR_PERMISSION VALUES ('%s')", perm));
}
properties.put("readPermissions", String.join("\n", permsList));
properties.put("usersSeparator", getUsersSeparator());
properties.put("everyone", SecurityConstants.EVERYONE);
return properties;
}
protected int getOracleErrorCode(Throwable t) {
try {
Method m = t.getClass().getMethod("getOracleError");
Integer oracleError = (Integer) m.invoke(t);
if (oracleError != null) {
int errorCode = oracleError.intValue();
if (errorCode != 0) {
return errorCode;
}
}
} catch (ReflectiveOperationException e) {
// ignore
}
if (t instanceof SQLException) {
return ((SQLException) t).getErrorCode();
}
return 0;
}
protected boolean isConnectionClosed(int oracleError) {
switch (oracleError) {
case 28: // your session has been killed.
case 1033: // Oracle initialization or shudown in progress.
case 1034: // Oracle not available
case 1041: // internal error. hostdef extension doesn't exist
case 1089: // immediate shutdown in progress - no operations are permitted
case 1090: // shutdown in progress - connection is not permitted
case 3113: // end-of-file on communication channel
case 3114: // not connected to ORACLE
case 12571: // TNS:packet writer failure
case 17002: // IO Exception
case 17008: // Closed Connection
case 17410: // No more data to read from socket
case 24768: // commit protocol error occured in the server
return true;
}
return false;
}
@Override
public boolean isConcurrentUpdateException(Throwable t) {
while (t.getCause() != null) {
t = t.getCause();
}
switch (getOracleErrorCode(t)) {
case 1: // ORA-00001: unique constraint violated
case 60: // ORA-00060: deadlock detected while waiting for resource
case 2291: // ORA-02291: integrity constraint ... violated - parent key not found
return true;
}
return false;
}
@Override
public String getValidationQuery() {
return "SELECT 1 FROM DUAL";
}
@Override
public String getBlobLengthFunction() {
return "LENGTHB";
}
@Override
public List<String> getPostCreateIdentityColumnSql(Column column) {
String table = column.getTable().getPhysicalName();
String col = column.getPhysicalName();
String seq = table + "_IDSEQ";
String trig = table + "_IDTRIG";
String createSeq = String.format("CREATE SEQUENCE \"%s\"", seq);
String createTrig = String.format("CREATE TRIGGER \"%s\"\n" //
+ " BEFORE INSERT ON \"%s\"\n" //
+ " FOR EACH ROW WHEN (NEW.\"%s\" IS NULL)\n" //
+ "BEGIN\n" //
+ " SELECT \"%s\".NEXTVAL INTO :NEW.\"%s\" FROM DUAL;\n" //
+ "END;", trig, table, col, seq, col);
return Arrays.asList(createSeq, createTrig);
}
@Override
public boolean hasIdentityGeneratedKey() {
return false;
}
@Override
public String getIdentityGeneratedKeySql(Column column) {
String table = column.getTable().getPhysicalName();
String seq = table + "_IDSEQ";
return String.format("SELECT \"%s\".CURRVAL FROM DUAL", seq);
}
@Override
public String getAncestorsIdsSql() {
return "SELECT NX_ANCESTORS(?) FROM DUAL";
}
@Override
public boolean needsNullsLastOnDescSort() {
return true;
}
@Override
public String getDateCast() {
// CAST(%s AS DATE) doesn't work, it doesn't compare exactly to DATE
// literals because the internal representation seems to be a float and
// CAST AS DATE does not truncate it
return "TRUNC(%s)";
}
@Override
public String castIdToVarchar(String expr) {
switch (idType) {
case VARCHAR:
return expr;
case SEQUENCE:
return "CAST(" + expr + " AS VARCHAR2(36))";
default:
throw new AssertionError("Unknown id type: " + idType);
}
}
@Override
public DialectIdType getIdType() {
return idType;
}
public String getUsersSeparator() {
if (usersSeparator == null) {
return DEFAULT_USERS_SEPARATOR;
}
return usersSeparator;
}
@Override
public String getSoftDeleteSql() {
return "{CALL NX_DELETE(?, ?)}";
}
@Override
public String getSoftDeleteCleanupSql() {
return "{CALL NX_DELETE_PURGE(?, ?, ?)}";
}
@Override
public List<String> getStartupSqls(Model model, Database database) {
if (aclOptimizationsEnabled) {
log.info("Vacuuming tables used by optimized acls");
return Collections.singletonList("{CALL nx_vacuum_read_acls}");
}
return Collections.emptyList();
}
@Override
public List<String> checkStoredProcedure(String procName, String procCreate, String ddlMode, Connection connection,
JDBCLogger logger, Map<String, Serializable> properties) throws SQLException {
boolean compatCheck = ddlMode.contains(RepositoryDescriptor.DDL_MODE_COMPAT);
if (compatCheck) {
procCreate = "CREATE OR REPLACE " + procCreate.substring("create ".length());
return Collections.singletonList(procCreate);
}
try (Statement st = connection.createStatement()) {
String getBody;
if (procCreate.toLowerCase().startsWith("create trigger ")) {
getBody = "SELECT TRIGGER_BODY FROM USER_TRIGGERS WHERE TRIGGER_NAME = '" + procName + "'";
} else {
// works for TYPE, FUNCTION, PROCEDURE
getBody = "SELECT TEXT FROM ALL_SOURCE WHERE NAME = '" + procName + "' ORDER BY LINE";
}
logger.log(getBody);
try (ResultSet rs = st.executeQuery(getBody)) {
if (rs.next()) {
List<String> lines = new ArrayList<>();
do {
lines.add(rs.getString(1));
} while (rs.next());
String body = org.apache.commons.lang.StringUtils.join(lines, ' ');
if (normalizeString(procCreate).contains(normalizeString(body))) {
logger.log(" -> exists, unchanged");
return Collections.emptyList();
} else {
logger.log(" -> exists, old");
if (!procCreate.toLowerCase().startsWith("create ")) {
throw new NuxeoException("Should start with CREATE: " + procCreate);
}
procCreate = "CREATE OR REPLACE " + procCreate.substring("create ".length());
return Collections.singletonList(procCreate);
}
} else {
logger.log(" -> missing");
return Collections.singletonList(procCreate);
}
}
}
}
protected static String normalizeString(String string) {
return string.replaceAll("[ \n\r\t]+", " ").trim();
}
@Override
public String getSQLForDump(String sql) {
String sqll = sql.toLowerCase();
if (sqll.startsWith("{call ")) {
// transform something used for JDBC calls into a proper SQL*Plus dump
return "EXECUTE " + sql.substring("{call ".length(), sql.length() - 1); // without ; or /
}
if (sqll.endsWith("end")) {
sql += ";";
}
return sql + "\n/";
}
@Override
public boolean supportsBatchUpdateCount() {
// Oracle 11
// https://docs.oracle.com/cd/E18283_01/java.112/e16548/oraperf.htm#i1057545
// For a prepared statement batch, it is not possible to know the number of rows affected in the database by
// each individual statement in the batch. Therefore, all array elements have a value of -2. According to the
// JDBC 2.0 specification, a value of -2 indicates that the operation was successful but the number of rows
// affected is unknown
//
// Oracle 12
// https://docs.oracle.com/database/121/JJDBC/oraperf.htm#JJDBC28773
// For a prepared statement batch, the array contains the actual update counts indicating the number of rows
// affected by each operation.
return majorVersion >= 12;
}
}