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}