001/*
002 * (C) Copyright 2006-2015 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 *     George Lefter
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.directory.sql;
021
022import java.sql.Connection;
023import java.sql.SQLException;
024import java.util.LinkedHashMap;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028
029import javax.naming.NamingException;
030import javax.sql.DataSource;
031import javax.transaction.Synchronization;
032import javax.transaction.SystemException;
033
034import org.apache.commons.lang.StringUtils;
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.nuxeo.common.utils.JDBCUtils;
038import org.nuxeo.ecm.core.schema.SchemaManager;
039import org.nuxeo.ecm.core.schema.types.Field;
040import org.nuxeo.ecm.core.schema.types.Schema;
041import org.nuxeo.ecm.core.storage.sql.ColumnType;
042import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
043import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
044import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
045import org.nuxeo.ecm.directory.AbstractDirectory;
046import org.nuxeo.ecm.directory.Directory;
047import org.nuxeo.ecm.directory.DirectoryException;
048import org.nuxeo.ecm.directory.DirectoryServiceImpl;
049import org.nuxeo.ecm.directory.Session;
050import org.nuxeo.ecm.directory.api.DirectoryService;
051import org.nuxeo.runtime.RuntimeService;
052import org.nuxeo.runtime.api.Framework;
053import org.nuxeo.runtime.datasource.ConnectionHelper;
054import org.nuxeo.runtime.datasource.DataSourceHelper;
055import org.nuxeo.runtime.transaction.TransactionHelper;
056
057public class SQLDirectory extends AbstractDirectory {
058
059    protected class TxSessionCleaner implements Synchronization {
060        private final SQLSession session;
061
062        Throwable initContext = captureInitContext();
063
064        protected TxSessionCleaner(SQLSession session) {
065            this.session = session;
066        }
067
068        protected Throwable captureInitContext() {
069            if (!log.isDebugEnabled()) {
070                return null;
071            }
072            return new Throwable("SQL directory session init context in " + SQLDirectory.this);
073        }
074
075        protected void checkIsNotLive() {
076            try {
077                if (!session.isLive()) {
078                    return;
079                }
080                if (initContext != null) {
081                    log.warn("Closing a sql directory session for you " + session, initContext);
082                } else {
083                    log.warn("Closing a sql directory session for you " + session);
084                }
085                if (!TransactionHelper.isTransactionActiveOrMarkedRollback()) {
086                    log.warn("Closing sql directory session outside a transaction" + session);
087                }
088                session.close();
089            } catch (DirectoryException e) {
090                log.error("Cannot state on sql directory session before commit " + SQLDirectory.this, e);
091            }
092
093        }
094
095        @Override
096        public void beforeCompletion() {
097            checkIsNotLive();
098        }
099
100        @Override
101        public void afterCompletion(int status) {
102            checkIsNotLive();
103        }
104
105    }
106
107    public static final Log log = LogFactory.getLog(SQLDirectory.class);
108
109    public static final String TENANT_ID_FIELD = "tenantId";
110
111    private final SQLDirectoryDescriptor config;
112
113    private final boolean nativeCase;
114
115    private DataSource dataSource;
116
117    private Table table;
118
119    private Schema schema;
120
121    private Map<String, Field> schemaFieldMap;
122
123    private List<String> storedFieldNames;
124
125    private volatile Dialect dialect;
126
127    public SQLDirectory(SQLDirectoryDescriptor config) {
128        super(config.name);
129        this.config = config;
130        nativeCase = Boolean.TRUE.equals(config.nativeCase);
131
132        // register the references to other directories
133        addReferences(config.getInverseReferences());
134        addReferences(config.getTableReferences());
135
136        // cache parameterization
137        cache.setEntryCacheName(config.cacheEntryName);
138        cache.setEntryCacheWithoutReferencesName(config.cacheEntryWithoutReferencesName);
139        cache.setNegativeCaching(config.negativeCaching);
140    }
141
142    /**
143     * Lazy init connection
144     *
145     * @since 6.0
146     */
147    protected void initConnection() {
148        Connection sqlConnection = getConnection();
149        try {
150            dialect = Dialect.createDialect(sqlConnection, null);
151
152            if (config.initDependencies != null) {
153                // initialize dependent directories first
154                final RuntimeService runtime = Framework.getRuntime();
155                DirectoryServiceImpl directoryService = (DirectoryServiceImpl) runtime.getComponent(DirectoryService.NAME);
156                for (String dependency : config.initDependencies) {
157                    log.debug("initializing dependencies first: " + dependency);
158                    Directory dir = directoryService.getDirectory(dependency);
159                    dir.getName();
160                }
161            }
162            // setup table and fields maps
163            table = SQLHelper.addTable(config.tableName, dialect, useNativeCase());
164            SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
165            schema = schemaManager.getSchema(config.schemaName);
166            if (schema == null) {
167                throw new DirectoryException("schema not found: " + config.schemaName);
168            }
169            schemaFieldMap = new LinkedHashMap<>();
170            storedFieldNames = new LinkedList<>();
171            boolean hasPrimary = false;
172            for (Field f : schema.getFields()) {
173                String fieldName = f.getName().getLocalName();
174                schemaFieldMap.put(fieldName, f);
175
176                if (!isReference(fieldName)) {
177                    // list of fields that are actually stored in the table of
178                    // the current directory and not read from an external
179                    // reference
180                    storedFieldNames.add(fieldName);
181
182                    boolean isId = fieldName.equals(config.getIdField());
183                    ColumnType type = ColumnType.fromField(f);
184                    if (isId && config.isAutoincrementIdField()) {
185                        type = ColumnType.AUTOINC;
186                    }
187                    Column column = SQLHelper.addColumn(table, fieldName, type, useNativeCase());
188                    if (isId) {
189                        if (config.isAutoincrementIdField()) {
190                            column.setIdentity(true);
191                        }
192                        column.setPrimary(true);
193                        column.setNullable(false);
194                        hasPrimary = true;
195                    }
196                }
197            }
198            if (!hasPrimary) {
199                throw new DirectoryException(String.format(
200                        "Directory '%s' id field '%s' is not present in schema '%s'", getName(), getIdField(),
201                        getSchema()));
202            }
203
204            SQLHelper helper = new SQLHelper(sqlConnection, table, config.dataFileName,
205                    config.getDataFileCharacterSeparator(), config.createTablePolicy);
206            helper.setupTable();
207
208        } finally {
209            try {
210                sqlConnection.close();
211            } catch (SQLException e) {
212                throw new DirectoryException(e);
213            }
214        }
215    }
216
217    public SQLDirectoryDescriptor getConfig() {
218        // utility method to simplify testing
219        return config;
220    }
221
222    /** DO NOT USE, use getConnection() instead. */
223    protected DataSource getDataSource() throws DirectoryException {
224        if (dataSource != null) {
225            return dataSource;
226        }
227        try {
228            if (!StringUtils.isEmpty(config.dataSourceName)) {
229                dataSource = DataSourceHelper.getDataSource(config.dataSourceName);
230                // InitialContext context = new InitialContext();
231                // dataSource = (DataSource)
232                // context.lookup(config.dataSourceName);
233            } else {
234                dataSource = new SimpleDataSource(config.dbUrl, config.dbDriver, config.dbUser, config.dbPassword);
235            }
236            log.trace("found datasource: " + dataSource);
237            return dataSource;
238        } catch (NamingException e) {
239            log.error("dataSource lookup failed", e);
240            throw new DirectoryException("dataSource lookup failed", e);
241        }
242    }
243
244    public Connection getConnection() throws DirectoryException {
245        try {
246            if (!StringUtils.isEmpty(config.dataSourceName)) {
247                // try single-datasource non-XA mode
248                Connection connection = ConnectionHelper.getConnection(config.dataSourceName);
249                if (connection != null) {
250                    if (ConnectionHelper.useSingleConnection(config.dataSourceName)) {
251                        connection.setAutoCommit(TransactionHelper.isNoTransaction());
252                    }
253                    return connection;
254                }
255            }
256            return getConnection(getDataSource());
257        } catch (SQLException e) {
258            throw new DirectoryException("Cannot connect to SQL directory '" + getName() + "': " + e.getMessage(), e);
259        }
260    }
261
262    /**
263     * Gets a physical connection from a datasource.
264     * <p>
265     * A few retries are done to work around databases that have problems with many open/close in a row.
266     *
267     * @param aDataSource the datasource
268     * @return the connection
269     */
270    protected Connection getConnection(DataSource aDataSource) throws SQLException {
271        return JDBCUtils.getConnection(aDataSource);
272    }
273
274    @Override
275    public String getName() {
276        return config.getName();
277    }
278
279    @Override
280    public String getSchema() {
281        return config.getSchemaName();
282    }
283
284    @Override
285    public String getParentDirectory() {
286        return config.getParentDirectory();
287    }
288
289    @Override
290    public String getIdField() {
291        return config.getIdField();
292    }
293
294    @Override
295    public String getPasswordField() {
296        return config.getPasswordField();
297    }
298
299    @Override
300    public Session getSession() throws DirectoryException {
301        checkConnection();
302        SQLSession session = new SQLSession(this, config);
303        addSession(session);
304        return session;
305    }
306
307    protected void checkConnection() {
308        // double checked locking with volatile pattern to ensure concurrent lazy init
309        if (dialect == null) {
310            synchronized (this) {
311                if (dialect == null) {
312                    initConnection();
313                }
314            }
315        }
316    }
317
318    protected void addSession(final SQLSession session) throws DirectoryException {
319        super.addSession(session);
320        registerInTx(session);
321    }
322
323    protected void registerInTx(final SQLSession session) throws DirectoryException {
324        if (!TransactionHelper.isTransactionActive()) {
325            return;
326        }
327        try {
328            ConnectionHelper.registerSynchronization(new TxSessionCleaner(session));
329        } catch (SystemException e) {
330            throw new DirectoryException("Cannot register in tx for session cleanup handling " + this, e);
331        }
332    }
333
334    public Map<String, Field> getSchemaFieldMap() {
335        return schemaFieldMap;
336    }
337
338    public List<String> getStoredFieldNames() {
339        return storedFieldNames;
340    }
341
342    public Table getTable() {
343        return table;
344    }
345
346    public Dialect getDialect() {
347        return dialect;
348    }
349
350    public boolean useNativeCase() {
351        return nativeCase;
352    }
353
354    @Override
355    public boolean isMultiTenant() {
356        return table.getColumn(TENANT_ID_FIELD) != null;
357    }
358
359    @Override
360    public String toString() {
361        return "SQLDirectory [name=" + config.name + "]";
362    }
363
364}