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