package er.extensions.eof;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Stack;
import org.apache.log4j.Logger;
import com.webobjects.eoaccess.EOAdaptorChannel;
import com.webobjects.eoaccess.EODatabaseContext;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EOModelGroup;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.eocontrol.EOObjectStoreCoordinator;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSForwardException;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXSystem;
import er.extensions.foundation.ERXValueUtilities;
import er.extensions.jdbc.ERXJDBCConnectionBroker;
/**
* @author david@cluster9.com<br/> <br/> Automatically generates Long primary
* keys for entities. Features a cache which reduces database roundtrips
* as well as optionally encoding Entity type in PK value.<br/> <br/>
* usage:<br/> <br/> override either the ERXGeneratesPrimaryKey
* interface like this:<br/> <code><pre>
* private NSDictionary _primaryKeyDictionary = null;
*
* public NSDictionary primaryKeyDictionary(boolean inTransaction) {
* if (_primaryKeyDictionary == null) {
* _primaryKeyDictionary = ERXLongPrimaryKeyFactory.primaryKeyDictionary(this);
* }
* return _primaryKeyDictionary;
* }
* </pre>
* </code><br/> or manually call<br/>
* <code>ERXLongPrimaryKeyFactory.primaryKeyDictionary(EOEnterpriseObject eo);</code><br/>
* <br/> the necessary database table is generated on the fly.<br/>
* <br/> <b>Encoding Entity in PK values</b><br/> If the system
* property
* <code>ERXIntegerPrimaryKeyFactory.encodeEntityInPkValue</code> is
* set to <code>true</code> then the last 6 bits from the 64 bit
* primary key is used to encode the Subentity in the pk value. This
* speeds up inheritance with multiple tables. In order to support this
* you must add an entry to the userInfo from the Subentities:<br/>
* <br/> <code>key=entityCode</code><br/>
* <code>value= %lt;%lt; an unique integer, no longer than 6 bit - 1</code><br/>
*
*/
public class ERXLongPrimaryKeyFactory {
private static final int CODE_LENGTH = 6;
private static final int HOST_CODE_LENGTH = 10;
private static final String HOST_CODE_KEY = "er.extensions.ERXLongPrimaryKeyFactory.hostCode";
private static final Logger log = Logger.getLogger(ERXLongPrimaryKeyFactory.class);
private static long MAX_PK_VALUE = (long) Math.pow(2, 48);
private Boolean encodeEntityInPkValue;
private Boolean encodeHostInPkValue;
private Integer hostCode;
private Hashtable pkCache = new Hashtable();
private Integer increaseBy;
private Long getNextPkValueForEntity(String ename) {
Long pk = cachedPkValue(ename);
if (encodeHostInPkValue()) {
long l = pk.longValue();
if (l > MAX_PK_VALUE) {
throw new IllegalStateException("max PK value reached for entity " + ename + " cannot continue!");
}
// we are assuming 64 bit int values
// and we are using the last 10 bits for
// hostCode
long realPk = l << HOST_CODE_LENGTH;
// now add the hostCode
realPk = realPk | hostCode();
if (log.isDebugEnabled()) {
log.debug("new pk value for "+ename+"("+((ERXModelGroup) EOModelGroup.defaultGroup()).entityCode(ename)+"), db value = "+pk+", new value = "+realPk);
}
pk = Long.valueOf(realPk);
}
if (encodeEntityInPkValue()) {
long l = pk.longValue();
if (l > MAX_PK_VALUE) {
throw new IllegalStateException("max PK value reached for entity " + ename + " cannot continue!");
}
// we are assuming 64 bit int values
// and we are using the last 6 bits for
// entity encoding
long realPk = l << CODE_LENGTH;
// now add the entity code
realPk = realPk | ((ERXModelGroup) EOModelGroup.defaultGroup()).entityCode(ename);
if (log.isDebugEnabled()) {
log.debug("new pk value for "+ename+"("+((ERXModelGroup) EOModelGroup.defaultGroup()).entityCode(ename)+"), db value = "+pk+", new value = "+realPk);
}
pk = Long.valueOf(realPk);
}
return pk;
}
/*
* (non-Javadoc)
*
* @see com.webobjects.eoaccess.EOModelGroup.Delegate#subEntityForEntity(com.webobjects.eoaccess.EOEntity,
* com.webobjects.foundation.NSDictionary)
*/
public EOEntity subEntityForEntity(EOEntity entity, NSDictionary pkDict) {
if (encodeEntityInPkValue()) {
//get the code, we assume that the pkDict contains only one pk value!
NSArray values = pkDict.allValues();
if (values.count() > 1) throw new IllegalArgumentException("subEntityForEntity in its default implementation"+
" works only with single pk long values " + entity.name()+ " has " + pkDict);
long pkValueWithCode;
try {
Number n = (Number) values.objectAtIndex(0);
pkValueWithCode = n.longValue();
} catch (ClassCastException e) {
throw new IllegalArgumentException("subEntityForEntity in its default implementation"+
" works only with single pk long values, expected a java.lang.Number but got a "+values.objectAtIndex(0));
}
long entityCode = pkValueWithCode & ((1 << ERXLongPrimaryKeyFactory.CODE_LENGTH) - 1);
if (entityCode == 0) return null;
for (Enumeration subEntities = entity.subEntities().objectEnumerator(); subEntities.hasMoreElements();) {
EOEntity subEntity = (EOEntity) subEntities.nextElement();
if (((ERXModelGroup) EOModelGroup.defaultGroup()).entityCode(subEntity) == entityCode) {
return subEntity;
}
}
}
return null;
}
private int hostCode() {
if (hostCode == null) {
hostCode = Integer.valueOf(ERXSystem.getProperty(HOST_CODE_KEY));
}
return hostCode.intValue();
}
private boolean encodeEntityInPkValue() {
if (encodeEntityInPkValue == null) {
boolean b = ERXValueUtilities.booleanValueWithDefault(System.getProperty("er.extensions.ERXLongPrimaryKeyFactory.encodeEntityInPkValue"),
false);
encodeEntityInPkValue = b ? Boolean.TRUE : Boolean.FALSE;
}
return encodeEntityInPkValue.booleanValue();
}
private boolean encodeHostInPkValue() {
if (encodeHostInPkValue == null) {
boolean b = ERXValueUtilities.booleanValueWithDefault(System.getProperty("er.extensions.ERXLongPrimaryKeyFactory.encodeHostInPkValue"),
false);
encodeHostInPkValue = b ? Boolean.TRUE : Boolean.FALSE;
}
return encodeHostInPkValue.booleanValue();
}
public synchronized static Object primaryKeyValue(String entityName) {
return factory().primaryKeyDictionary(entityName).objectEnumerator().nextElement();
}
public synchronized static NSDictionary primaryKeyDictionary(EOEnterpriseObject eo) {
String entityName = eo.entityName();
return factory().primaryKeyDictionary(entityName);
}
private static ERXLongPrimaryKeyFactory _factory;
private static ERXLongPrimaryKeyFactory factory() {
if(_factory == null) {
_factory = new ERXLongPrimaryKeyFactory();
if(_factory.encodeEntityInPkValue()) {
EOModelGroup.defaultGroup().setDelegate(_factory);
}
}
return _factory;
}
private NSDictionary primaryKeyDictionary(String entityName) {
EOEntity entity = EOModelGroup.defaultGroup().entityNamed(entityName);
while (entity.parentEntity() != null) {
entity = entity.parentEntity();
}
entityName = entity.name();
if(entity.primaryKeyAttributeNames().count() != 1) {
throw new IllegalArgumentException("Can handle only entities with one PK: " + entityName + " has " + entity.primaryKeyAttributeNames());
}
Long pk = getNextPkValueForEntity(entityName);
String pkName = entity.primaryKeyAttributeNames().objectAtIndex(0);
return new NSDictionary(new Object[] { pk}, new Object[] { pkName});
}
/**
* returns a new primary key for the specified entity.
*
* @param entityName
* the entity name for which this method should return a new
* primary key
* @param count
* the number of times the method should try to get a value from
* the database if something went wrong (a deadlock in the db for
* example -> high traffic with multiple instances)
* @param increasePkBy
* if > 1 then the value in the database is increased by this
* factor. This is useful to 'get' 10000 pk values at once for
* caching. Removes a lot of db roundtrips.
* @return a new pk values for the specified entity.
*/
private Long getNextPkValueForEntityIncreaseBy(String entityName, int count, int increasePkBy) {
if (increasePkBy < 1) increasePkBy = 1;
String where = "where eoentity_name = '" + entityName + "'";
if(false) {
// AK: this should actually be the correct way...
EOEditingContext ec = ERXEC.newEditingContext();
ec.lock();
try {
EODatabaseContext dbc = ERXEOAccessUtilities.databaseContextForEntityNamed((EOObjectStoreCoordinator) ec.rootObjectStore(), entityName);
dbc.lock();
try {
EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, entityName);
EOAdaptorChannel channel = (EOAdaptorChannel) dbc.adaptorContext().channels().lastObject();
NSArray result = channel.primaryKeysForNewRowsWithEntity(increasePkBy, entity);
return (Long) ((NSDictionary) result.lastObject()).allValues().lastObject();
} finally {
dbc.unlock();
}
} finally {
ec.unlock();
}
} else {
ERXJDBCConnectionBroker broker = ERXJDBCConnectionBroker.connectionBrokerForEntityNamed(entityName);
Connection con = broker.getConnection();
try {
try {
con.setAutoCommit(false);
con.setReadOnly(false);
} catch (SQLException e) {
log.error(e, e);
}
for(int tries = 0; tries < count; tries++) {
try {
ResultSet resultSet = con.createStatement().executeQuery("select pk_value from pk_table " + where);
con.commit();
boolean hasNext = resultSet.next();
long pk = 1;
if (hasNext) {
pk = resultSet.getLong("pk_value");
// now execute the update
con.createStatement().executeUpdate("update pk_table set pk_value = " + (pk+increasePkBy) + " " + where);
} else {
pk = maxIdFromTable(entityName);
// first time, we need to set i up
con.createStatement().executeUpdate("insert into pk_table (eoentity_name, pk_value) values ('" + entityName + "', " + (pk+increasePkBy) + ")");
}
con.commit();
return Long.valueOf(pk);
} catch(SQLException ex) {
String s = ex.getMessage().toLowerCase();
boolean creationError = (s.indexOf("error code 116") != -1); // frontbase?
creationError |= (s.indexOf("pk_table") != -1 && s.indexOf("does not exist") != -1); // postgres ?
creationError |= s.indexOf("ora-00942") != -1; // oracle
if (creationError) {
try {
con.rollback();
log.info("creating pk table");
con.createStatement().executeUpdate("create table pk_table (eoentity_name varchar(100) not null, pk_value integer)");
con.createStatement().executeUpdate("alter table pk_table add primary key (eoentity_name)");// NOT
// DEFERRABLE
// INITIALLY
// IMMEDIATE");
con.commit();
} catch (SQLException ee) {
throw new NSForwardException(ee, "could not create pk table");
}
} else {
throw new NSForwardException(ex, "Error fetching PK");
}
}
}
} finally {
broker.freeConnection(con);
}
}
throw new IllegalStateException("Couldn't get PK");
}
/**
* Retrieves the maxValue from id from the specified entity. If
* hosts and entities are encoded, then these values are stripped
* first
*
* @param ename
*/
private long maxIdFromTable(String ename) {
EOEntity entity = EOModelGroup.defaultGroup().entityNamed(ename);
if (entity == null) throw new NullPointerException("could not find an entity named " + ename);
String tableName = entity.externalName();
String colName = entity.primaryKeyAttributes().lastObject().columnName();
String sql = "select max(" + colName + ") from " + tableName;
ERXJDBCConnectionBroker broker = ERXJDBCConnectionBroker.connectionBrokerForEntityNamed(ename);
Connection con = broker.getConnection();
ResultSet resultSet;
try {
resultSet = con.createStatement().executeQuery(sql);
con.commit();
boolean hasNext = resultSet.next();
long v = 1l;
if (hasNext) {
v = resultSet.getLong(1);
if (log.isDebugEnabled())
log.debug("received max id from table " + tableName + ", setting value in PK_TABLE to " + v);
if(encodeEntityInPkValue()) {
v = v >> CODE_LENGTH;
}
if(encodeHostInPkValue()) {
v = v >> HOST_CODE_LENGTH;
}
}
return v + 1;
} catch (SQLException e) {
log.error("could not call database with sql " + sql, e);
throw new IllegalStateException("could not get value from " + sql);
} finally {
broker.freeConnection(con);
}
}
/**
* Returns a new integer based PkValue for the specified entity. If the
* cache is empty it is refilled again.
*
* @param ename
* the entity name for which this method should return a new
* primary key
*
* @return a new Integer based primary key for the specified entity.
*/
private Long cachedPkValue(String ename) {
Stack s = cacheStack(ename);
if (s.empty()) {
synchronized (s) {
if (s.empty()) {
fillPkCache(s, ename);
}
}
}
Long pkValue = (Long) s.pop();
return pkValue;
}
/**
* looks in the cache hashtable if there is already an Stack for the
* specified entity name. If there is no Stack a new Stack object will be
* created.
*
* @param ename
* the name of the entity for which this method should return the
* Stack
* @return the Stack with primary key values for the specified entity.
*/
private Stack cacheStack(String ename) {
Stack s = (Stack) pkCache.get(ename);
if (s == null) {
s = new Stack();
pkCache.put(ename, s);
}
return s;
}
/**
* creates x primary key values for the specified entity and updates the
* database, where x is the number specified in increaseBy
*
* @param s
* the stack into which the pk values should be inserted
* @param ename
* the entity name for which the pk values should be generated
*/
private void fillPkCache(Stack s, String ename) {
Long pkValueStart = getNextPkValueForEntityIncreaseBy(ename, 10, increaseBy());
long value = pkValueStart.longValue();
log.debug("filling pkCache for " + ename + ", starting at " + value);
for (int i = increaseBy(); i > 0; i--) {
s.push(Long.valueOf(i + value));
}
}
/**
* The amount of cached keys, set the property
* <code>er.extensions.ERXLongPrimaryKeyFactory.increaseBy</code> to the
* interval you want to use.
*
* @return the interval to use for cached keys
*/
private int increaseBy() {
if (increaseBy == null) {
increaseBy = Integer.valueOf(ERXProperties.intForKeyWithDefault("er.extensions.ERXLongPrimaryKeyFactory.increaseBy", 1000));
}
return increaseBy.intValue();
}
}