001/*
002 * (C) Copyright 2006-2018 Nuxeo (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.Arrays;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.stream.Collectors;
028
029import javax.transaction.Synchronization;
030
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.ecm.core.schema.SchemaManager;
035import org.nuxeo.ecm.core.schema.types.Field;
036import org.nuxeo.ecm.core.schema.types.Schema;
037import org.nuxeo.ecm.core.storage.sql.ColumnType;
038import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
039import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
040import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
041import org.nuxeo.ecm.directory.AbstractDirectory;
042import org.nuxeo.ecm.directory.DirectoryException;
043import org.nuxeo.ecm.directory.Reference;
044import org.nuxeo.runtime.api.Framework;
045import org.nuxeo.runtime.datasource.ConnectionHelper;
046import org.nuxeo.runtime.transaction.TransactionHelper;
047
048public class SQLDirectory extends AbstractDirectory {
049
050    protected class TxSessionCleaner implements Synchronization {
051        private final SQLSession session;
052
053        Throwable initContext = captureInitContext();
054
055        protected TxSessionCleaner(SQLSession session) {
056            this.session = session;
057        }
058
059        protected Throwable captureInitContext() {
060            if (!log.isDebugEnabled()) {
061                return null;
062            }
063            return new Throwable("SQL directory session init context in " + SQLDirectory.this);
064        }
065
066        protected void checkIsNotLive() {
067            try {
068                if (!session.isLive()) {
069                    return;
070                }
071                if (initContext != null) {
072                    log.warn("Closing a sql directory session for you " + session, initContext);
073                } else {
074                    log.warn("Closing a sql directory session for you " + session);
075                }
076                if (!TransactionHelper.isTransactionActiveOrMarkedRollback()) {
077                    log.warn("Closing sql directory session outside a transaction" + session);
078                }
079                session.close();
080            } catch (DirectoryException e) {
081                log.error("Cannot state on sql directory session before commit " + SQLDirectory.this, e);
082            }
083
084        }
085
086        @Override
087        public void beforeCompletion() {
088            checkIsNotLive();
089        }
090
091        @Override
092        public void afterCompletion(int status) {
093            checkIsNotLive();
094        }
095
096    }
097
098    public static final Log log = LogFactory.getLog(SQLDirectory.class);
099
100    private final boolean nativeCase;
101
102    private Table table;
103
104    private Schema schema;
105
106    // columns to fetch when an entry is read (with the password)
107    protected List<Column> readColumnsAll;
108
109    // columns to fetch when an entry is read (excludes the password)
110    protected List<Column> readColumns;
111
112    // id column
113    protected Column idColumn;
114
115    // columns to fetch when an entry is read (with the password), as SQL
116    protected String readColumnsAllSQL;
117
118    // columns to fetch when an entry is read (excludes the password), as SQL
119    protected String readColumnsSQL;
120
121    private volatile Dialect dialect;
122
123    public SQLDirectory(SQLDirectoryDescriptor descriptor) {
124        super(descriptor, TableReference.class);
125
126        nativeCase = Boolean.TRUE.equals(descriptor.nativeCase);
127
128        // Cache fallback
129        fallbackOnDefaultCache();
130    }
131
132    @Override
133    public SQLDirectoryDescriptor getDescriptor() {
134        return (SQLDirectoryDescriptor) descriptor;
135    }
136
137    @Override
138    protected void addReferences() {
139        super.addReferences();
140        // add backward compat tableReferences
141        TableReferenceDescriptor[] descs = getDescriptor().getTableReferences();
142        if (descs != null) {
143            Arrays.stream(descs).map(TableReference::new).forEach(this::addReference);
144        }
145    }
146
147    @Override
148    public void initialize() {
149        super.initialize();
150        SQLDirectoryDescriptor descriptor = getDescriptor();
151        try (Connection sqlConnection = getConnection()) {
152            dialect = Dialect.createDialect(sqlConnection, null);
153            // setup table and fields maps
154            String tableName = descriptor.tableName == null ? descriptor.name : descriptor.tableName;
155            table = SQLHelper.addTable(tableName, dialect, useNativeCase());
156            SchemaManager schemaManager = Framework.getService(SchemaManager.class);
157            schema = schemaManager.getSchema(getSchema());
158            if (schema == null) {
159                throw new DirectoryException("schema not found: " + getSchema());
160            }
161            readColumnsAll = new LinkedList<>();
162            readColumns = new LinkedList<>();
163            boolean hasPrimary = false;
164            for (Field f : schema.getFields()) {
165                String fieldName = f.getName().getLocalName();
166
167                if (!isReference(fieldName)) {
168                    boolean isId = fieldName.equals(getIdField());
169                    ColumnType type = ColumnType.fromField(f);
170                    if (isId && descriptor.isAutoincrementIdField()) {
171                        type = ColumnType.AUTOINC;
172                    }
173                    Column column = SQLHelper.addColumn(table, fieldName, type, useNativeCase());
174                    if (isId) {
175                        if (descriptor.isAutoincrementIdField()) {
176                            column.setIdentity(true);
177                        }
178                        column.setPrimary(true);
179                        column.setNullable(false);
180                        idColumn = column;
181                        hasPrimary = true;
182                    }
183                    readColumnsAll.add(column);
184                    if (!fieldName.equals(descriptor.passwordField)) {
185                        readColumns.add(column);
186                    }
187                }
188            }
189            readColumnsAllSQL = readColumnsAll.stream().map(Column::getQuotedName).collect(Collectors.joining(", "));
190            readColumnsSQL = readColumns.stream().map(Column::getQuotedName).collect(Collectors.joining(", "));
191            if (!hasPrimary) {
192                throw new DirectoryException(String.format("Directory '%s' id field '%s' is not present in schema '%s'",
193                        getName(), getIdField(), getSchema()));
194            }
195
196            SQLHelper helper = new SQLHelper(sqlConnection, table, descriptor.getCreateTablePolicy());
197            boolean tableExists = !helper.setupTable();
198            // commit the transaction so that tables are committed
199            if (TransactionHelper.isTransactionActiveOrMarkedRollback()) {
200                TransactionHelper.commitOrRollbackTransaction();
201                TransactionHelper.startTransaction();
202            }
203            loadDataOnInit(tableExists);
204
205        } catch (SQLException e) {
206            // exception on close
207            throw new DirectoryException(e);
208        }
209    }
210
211    @Override
212    public void initializeReferences() {
213        try (Connection connection = getConnection()) {
214            for (Reference reference : getReferences()) {
215                if (reference instanceof TableReference) {
216                    ((TableReference) reference).initialize(connection);
217                }
218            }
219        } catch (SQLException e) {
220            throw new DirectoryException(e);
221        }
222    }
223
224    public Connection getConnection() {
225        SQLDirectoryDescriptor descriptor = getDescriptor();
226        if (StringUtils.isBlank(descriptor.dataSourceName)) {
227            throw new DirectoryException("Missing dataSource for SQL directory: " + getName());
228        }
229        try {
230            return ConnectionHelper.getConnection(descriptor.dataSourceName);
231        } catch (SQLException e) {
232            throw new DirectoryException("Cannot connect to SQL directory '" + getName() + "': " + e.getMessage(), e);
233        }
234    }
235
236    @Override
237    public SQLSession getSession() {
238        SQLSession session = new SQLSession(this, getDescriptor());
239        addSession(session);
240        return session;
241    }
242
243    protected void addSession(final SQLSession session) {
244        super.addSession(session);
245        registerInTx(session);
246    }
247
248    protected void registerInTx(final SQLSession session) {
249        if (!TransactionHelper.isTransactionActive()) {
250            return;
251        }
252        TransactionHelper.registerSynchronization(new TxSessionCleaner(session));
253    }
254
255    public Table getTable() {
256        return table;
257    }
258
259    public Dialect getDialect() {
260        return dialect;
261    }
262
263    public boolean useNativeCase() {
264        return nativeCase;
265    }
266
267    @Override
268    public boolean isMultiTenant() {
269        return table.getColumn(TENANT_ID_FIELD) != null;
270    }
271
272    @Override
273    public String toString() {
274        return "SQLDirectory [name=" + descriptor.name + "]";
275    }
276
277}