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