001/* 002 * (C) Copyright 2006-2016 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; 028import java.util.stream.Collectors; 029 030import javax.transaction.Synchronization; 031 032import org.apache.commons.lang.StringUtils; 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import org.nuxeo.ecm.core.cache.CacheService; 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.DirectoryCSVLoader; 045import org.nuxeo.ecm.directory.DirectoryException; 046import org.nuxeo.ecm.directory.Session; 047import org.nuxeo.runtime.api.Framework; 048import org.nuxeo.runtime.datasource.ConnectionHelper; 049import org.nuxeo.runtime.transaction.TransactionHelper; 050 051public class SQLDirectory extends AbstractDirectory { 052 053 protected class TxSessionCleaner implements Synchronization { 054 private final SQLSession session; 055 056 Throwable initContext = captureInitContext(); 057 058 protected TxSessionCleaner(SQLSession session) { 059 this.session = session; 060 } 061 062 protected Throwable captureInitContext() { 063 if (!log.isDebugEnabled()) { 064 return null; 065 } 066 return new Throwable("SQL directory session init context in " + SQLDirectory.this); 067 } 068 069 protected void checkIsNotLive() { 070 try { 071 if (!session.isLive()) { 072 return; 073 } 074 if (initContext != null) { 075 log.warn("Closing a sql directory session for you " + session, initContext); 076 } else { 077 log.warn("Closing a sql directory session for you " + session); 078 } 079 if (!TransactionHelper.isTransactionActiveOrMarkedRollback()) { 080 log.warn("Closing sql directory session outside a transaction" + session); 081 } 082 session.close(); 083 } catch (DirectoryException e) { 084 log.error("Cannot state on sql directory session before commit " + SQLDirectory.this, e); 085 } 086 087 } 088 089 @Override 090 public void beforeCompletion() { 091 checkIsNotLive(); 092 } 093 094 @Override 095 public void afterCompletion(int status) { 096 checkIsNotLive(); 097 } 098 099 } 100 101 public static final Log log = LogFactory.getLog(SQLDirectory.class); 102 103 public static final String TENANT_ID_FIELD = "tenantId"; 104 105 private final boolean nativeCase; 106 107 private Table table; 108 109 private Schema schema; 110 111 private Map<String, Field> schemaFieldMap; 112 113 // columns to fetch when an entry is read (with the password) 114 protected List<Column> readColumnsAll; 115 116 // columns to fetch when an entry is read (excludes the password) 117 protected List<Column> readColumns; 118 119 // columns to fetch when an entry is read (with the password), as SQL 120 protected String readColumnsAllSQL; 121 122 // columns to fetch when an entry is read (excludes the password), as SQL 123 protected String readColumnsSQL; 124 125 private volatile Dialect dialect; 126 127 public SQLDirectory(SQLDirectoryDescriptor descriptor) { 128 super(descriptor); 129 nativeCase = Boolean.TRUE.equals(descriptor.nativeCase); 130 131 // register the references to other directories 132 addReferences(descriptor.getInverseReferences()); 133 addReferences(descriptor.getTableReferences()); 134 135 // cache parameterization 136 cache.setEntryCacheName(descriptor.cacheEntryName); 137 cache.setEntryCacheWithoutReferencesName(descriptor.cacheEntryWithoutReferencesName); 138 cache.setNegativeCaching(descriptor.negativeCaching); 139 140 // Cache fallback 141 CacheService cacheService = Framework.getLocalService(CacheService.class); 142 if (cacheService != null) { 143 if (descriptor.cacheEntryName == null && descriptor.getCacheMaxSize() != 0) { 144 cache.setEntryCacheName("cache-" + getName()); 145 cacheService.registerCache("cache-" + getName(), 146 descriptor.getCacheMaxSize(), 147 descriptor.getCacheTimeout() / 60); 148 } 149 if (descriptor.cacheEntryWithoutReferencesName == null && descriptor.getCacheMaxSize() != 0) { 150 cache.setEntryCacheWithoutReferencesName( 151 "cacheWithoutReference-" + getName()); 152 cacheService.registerCache("cacheWithoutReference-" + getName(), 153 descriptor.getCacheMaxSize(), 154 descriptor.getCacheTimeout() / 60); 155 } 156 } 157 } 158 159 @Override 160 public SQLDirectoryDescriptor getDescriptor() { 161 return (SQLDirectoryDescriptor) descriptor; 162 } 163 164 /** 165 * Lazily initializes the connection. 166 * 167 * @return {@code true} if CSV data should be loaded 168 * @since 8.4 169 */ 170 protected boolean initConnectionIfNeeded() { 171 // double checked locking with volatile pattern to ensure concurrent lazy init 172 if (dialect == null) { 173 synchronized (this) { 174 if (dialect == null) { 175 return initConnection(); 176 } 177 } 178 } 179 return false; 180 } 181 182 /** 183 * Initializes the table. 184 * 185 * @return {@code true} if CSV data should be loaded 186 * @since 6.0 187 */ 188 protected boolean initConnection() { 189 SQLDirectoryDescriptor descriptor = getDescriptor(); 190 191 try (Connection sqlConnection = getConnection()) { 192 dialect = Dialect.createDialect(sqlConnection, null); 193 // setup table and fields maps 194 String tableName = descriptor.tableName == null ? descriptor.name : descriptor.tableName; 195 table = SQLHelper.addTable(tableName, dialect, useNativeCase()); 196 SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); 197 schema = schemaManager.getSchema(getSchema()); 198 if (schema == null) { 199 throw new DirectoryException("schema not found: " + getSchema()); 200 } 201 schemaFieldMap = new LinkedHashMap<>(); 202 readColumnsAll = new LinkedList<>(); 203 readColumns = new LinkedList<>(); 204 boolean hasPrimary = false; 205 for (Field f : schema.getFields()) { 206 String fieldName = f.getName().getLocalName(); 207 schemaFieldMap.put(fieldName, f); 208 209 if (!isReference(fieldName)) { 210 boolean isId = fieldName.equals(getIdField()); 211 ColumnType type = ColumnType.fromField(f); 212 if (isId && descriptor.isAutoincrementIdField()) { 213 type = ColumnType.AUTOINC; 214 } 215 Column column = SQLHelper.addColumn(table, fieldName, type, useNativeCase()); 216 if (isId) { 217 if (descriptor.isAutoincrementIdField()) { 218 column.setIdentity(true); 219 } 220 column.setPrimary(true); 221 column.setNullable(false); 222 hasPrimary = true; 223 } 224 readColumnsAll.add(column); 225 if (!fieldName.equals(descriptor.passwordField)) { 226 readColumns.add(column); 227 } 228 } 229 } 230 readColumnsAllSQL = readColumnsAll.stream().map(Column::getQuotedName).collect(Collectors.joining(", ")); 231 readColumnsSQL = readColumns.stream().map(Column::getQuotedName).collect(Collectors.joining(", ")); 232 if (!hasPrimary) { 233 throw new DirectoryException(String.format( 234 "Directory '%s' id field '%s' is not present in schema '%s'", getName(), getIdField(), 235 getSchema())); 236 } 237 238 SQLHelper helper = new SQLHelper(sqlConnection, table, descriptor.getCreateTablePolicy()); 239 boolean loadData = helper.setupTable(); 240 return loadData; 241 } catch (SQLException e) { 242 // exception on close 243 throw new DirectoryException(e); 244 } 245 } 246 247 public Connection getConnection() throws DirectoryException { 248 SQLDirectoryDescriptor descriptor = getDescriptor(); 249 if (StringUtils.isBlank(descriptor.dataSourceName)) { 250 throw new DirectoryException("Missing dataSource for SQL directory: " + getName()); 251 } 252 try { 253 return ConnectionHelper.getConnection(descriptor.dataSourceName); 254 } catch (SQLException e) { 255 throw new DirectoryException("Cannot connect to SQL directory '" + getName() + "': " + e.getMessage(), e); 256 } 257 } 258 259 @Override 260 public Session getSession() throws DirectoryException { 261 boolean loadData = initConnectionIfNeeded(); 262 SQLSession session = new SQLSession(this, getDescriptor()); 263 addSession(session); 264 if (loadData && descriptor.getDataFileName() != null) { 265 Schema schema = Framework.getService(SchemaManager.class).getSchema(getSchema()); 266 DirectoryCSVLoader.loadData(descriptor.getDataFileName(), descriptor.getDataFileCharacterSeparator(), 267 schema, session::createEntry); 268 } 269 return session; 270 } 271 272 protected void addSession(final SQLSession session) throws DirectoryException { 273 super.addSession(session); 274 registerInTx(session); 275 } 276 277 protected void registerInTx(final SQLSession session) throws DirectoryException { 278 if (!TransactionHelper.isTransactionActive()) { 279 return; 280 } 281 TransactionHelper.registerSynchronization(new TxSessionCleaner(session)); 282 } 283 284 public Map<String, Field> getSchemaFieldMap() { 285 return schemaFieldMap; 286 } 287 288 public Table getTable() { 289 return table; 290 } 291 292 public Dialect getDialect() { 293 return dialect; 294 } 295 296 public boolean useNativeCase() { 297 return nativeCase; 298 } 299 300 @Override 301 public boolean isMultiTenant() { 302 return table.getColumn(TENANT_ID_FIELD) != null; 303 } 304 305 @Override 306 public String toString() { 307 return "SQLDirectory [name=" + descriptor.name + "]"; 308 } 309 310}