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