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