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