/* * #! * Ontopia Engine * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * !# */ package net.ontopia.persistence.proxy; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import net.ontopia.persistence.query.jdo.JDOQuery; import net.ontopia.utils.OntopiaRuntimeException; import net.ontopia.utils.StringUtils; import org.apache.commons.collections4.map.AbstractReferenceMap; import org.apache.commons.collections4.map.ReferenceMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * INTERNAL: The default proxy transaction implementation. */ public abstract class AbstractTransaction implements TransactionIF { // Define a logging category. static Logger log = LoggerFactory.getLogger(AbstractTransaction.class.getName()); protected boolean debug = log.isDebugEnabled(); protected boolean isactive; protected boolean isclosed; protected String id; protected StorageAccessIF access; protected StorageCacheIF txncache; protected AccessRegistrarIF registrar; protected ObjectAccessIF oaccess; protected ObjectRelationalMappingIF mapping; protected final Map<IdentityIF, PersistentIF> identity_map; protected Map<IdentityIF, PersistentIF> lru; protected int lrusize; protected Map<String, QueryIF> querymap; protected long timestamp; AbstractTransaction(String id, StorageAccessIF access) { this.id = id; this.access = access; this.mapping = access.getStorage().getMapping(); // Map containing named queries this.querymap = new HashMap<String, QueryIF>(); // Identity map - maintains the relationships between object // identities and the single(ton) instances used with the // transaction. This enforces the constraint that only one // instance per object identity should exist at a given time in a // transaction. // NOTE: Even though it's the keys that are garbage collected // there is a strict mapping between IdentityIF and PersistentIF // that lets us do this, i.e. the objects reference their // identities, so the identity will not be garbage collected as // long as the object is reachable. this.identity_map = new ReferenceMap<IdentityIF, PersistentIF>(AbstractReferenceMap.ReferenceStrength.HARD, AbstractReferenceMap.ReferenceStrength.SOFT); log.debug(getId() + ": Transaction created."); this.timestamp = System.currentTimeMillis(); } // ----------------------------------------------------------------------------- // TransactionIF (public) // ----------------------------------------------------------------------------- public String getId() { return id; } public StorageAccessIF getStorageAccess() { return access; } public boolean isActive() { return isactive; } public boolean validate() { if (isclosed) return false; else return access.validate(); } public synchronized void begin() { if (isclosed) throw new OntopiaRuntimeException("Cannot restart a closed transaction."); this.isactive = true; // Notify transaction cache log.debug(getId() + ": Transaction started."); } public synchronized void commit() { if (!isactive) throw new OntopiaRuntimeException("Transaction is not active."); // Store transaction changes flush(); // Before txn commit transactionPreCommit(); // Commit storage transaction access.commit(); // After txn commit transactionPostCommit(); log.debug(getId() + ": Transaction committed."); } public synchronized void abort() { if (!isactive) throw new OntopiaRuntimeException("Transaction is not active."); // Rollback all changes transactionPreAbort(); try { access.abort(); } catch (Throwable t) { // ignore, because the txn will be invalid anyway } transactionPostAbort(); log.debug(getId() + ": Transaction aborted."); } public synchronized void close() { if (isclosed) throw new OntopiaRuntimeException("Transaction is already closed."); // Note: access is closed here. access.close(); log.debug(getId() + ": Transaction closed."); this.isclosed = true; this.isactive = false; ((RDBMSStorage)access.getStorage()).transactionClosed(this); } public abstract void flush(); protected abstract void transactionPreCommit(); protected abstract void transactionPostCommit(); protected abstract void transactionPreAbort(); protected abstract void transactionPostAbort(); public ObjectAccessIF getObjectAccess() { return oaccess; } public AccessRegistrarIF getAccessRegistrar() { return registrar; } // ----------------------------------------------------------------------------- // Misc. PersistentIF callbacks // ----------------------------------------------------------------------------- public boolean isObjectLoaded(IdentityIF identity) { if (!isactive) throw new TransactionNotActiveException(); // check identity map synchronized (identity_map) { // read if (checkIdentityMapNoLRU(identity) != null) return true; } // check to see if object is registered in cache return txncache.isObjectLoaded(identity); } public boolean isFieldLoaded(IdentityIF identity, int field) { if (!isactive) throw new TransactionNotActiveException(); // check identity map synchronized (identity_map) { // read PersistentIF p = checkIdentityMapNoLRU(identity); if (p == null) return false; if (p.isLoaded(field)) return true; } // Check to see if field is registered in cache return txncache.isFieldLoaded(identity, field); } public <F> F loadField(IdentityIF identity, int field) { if (!isactive) throw new TransactionNotActiveException(); // NOTE: this methods is always called by a PersistentIF // NOTE: no need to check identity map first // get value from shared cache Object value = txncache.getValue(access, identity, field); // track all changes objectRead(identity); // look up identity value if (value != null) { if (value instanceof IdentityIF) return (F) getObject((IdentityIF)value); } return (F) value; } /** * INTERNAL: Called by other transactions to notify this transaction of * committed merges. Default implementation is empty. * @param source The identity of the object merged into target * @param target The identity of the target object that was merged * @since %NEXT% */ public void objectMerged(IdentityIF source, IdentityIF target) { // does noting by default, see RWTransaction } // ----------------------------------------------------------------------------- // Object lookup // ----------------------------------------------------------------------------- public PersistentIF getObject(IdentityIF identity) { return getObject(identity, false); } public PersistentIF getObject(IdentityIF identity, boolean acceptDeleted) { PersistentIF o = _getObject(identity); if (o != null && o.isDeleted()) return (acceptDeleted ? o : null); else return o; } public PersistentIF _getObject(IdentityIF identity) { if (!isactive) throw new TransactionNotActiveException(); if (identity == null) throw new NullPointerException("null identities should not be looked up."); // Check local identity map synchronized (identity_map) { // read // check identity map PersistentIF p = checkIdentityMap(identity); if (p != null && !p.isTransient()) { return p; } } //! // Object was not found in the identity map, so we need to store //! // transaction changes, to make sure that deleted objects are //! // deleted in the database. //! flush(); // FIXME: Is it faster to loop over deleted objects in the change set? // The instance is not in the identity map, so we need to // prepare a new instance. // Ask transaction cache to perform existence check. If the call // succeeded we know that the object exists in the data // repository. The identity will also be registered with the // appropriate access registrar. if (!txncache.exists(access, identity)) throw new IdentityNotFoundException(identity); if (log.isDebugEnabled()) log.debug(getId() + ": Identity found in data repository: " + identity); return checkIdentityMapAndCreateInstance(identity); } // ----------------------------------------------------------------------------- // Identity map management methods // ----------------------------------------------------------------------------- PersistentIF checkIdentityMapAndCreateInstance(IdentityIF identity) { // NOTE: now rechecking identity map because registrar might have // been here and created an instance for us. At this point we know // that the identity exists. // prevent somebody else tampering with the identity map synchronized (identity_map) { // read, then write // check identity map PersistentIF p = checkIdentityMapNoLRU(identity); if (p != null) { // set state if transient if (p.isTransient()) { p.setPersistent(true); p.setInDatabase(true); } return p; } // create new instance p = createInstance(identity); // set state if (!isReadOnly()) { p.setPersistent(true); p.setInDatabase(true); } return p; } } PersistentIF checkIdentityMapNoLRU(IdentityIF identity) { // WARNING: access to this method should be synchronized on identity_map // Check to see if somebody else has registered the same identity return identity_map.get(identity); } PersistentIF removeIdentityMapNoLRU(IdentityIF identity) { // WARNING: access to this method should be synchronized on identity_map // ISSUE: remove from lru as well? // Check to see if somebody else has registered the same identity return identity_map.remove(identity); } PersistentIF checkIdentityMap(IdentityIF identity) { // WARNING: access to this method should be synchronized on identity_map // Check to see if somebody else has registered the same identity PersistentIF o = identity_map.get(identity); if (o != null) { //! if (log.isDebugEnabled()) //! log.debug(getId() + ": Object found in identity map: " + identity); // Register with LRU cache lru.put(identity, o); // Return singleton object instance return o; } return null; } protected PersistentIF createInstance(IdentityIF identity) { try { // Create instance of identity type class //! PersistentIF object = (PersistentIF)identity.createInstance(); ClassInfoIF cinfo = mapping.getClassInfo(identity.getType()); PersistentIF object = (PersistentIF)cinfo.createInstance(isReadOnly()); // Register identity with persistent object object._p_setIdentity(identity); // Register transaction state object._p_setTransaction(this); // Note: The entry value must also be a soft reference, since the // object references the key (its identity)! Thus an LRU cache // might have to be used somewhere to avoid references being // evicted too often. // Register object with identity map identity_map.put(identity, object); // Register with LRU cache lru.put(identity, object); // TODO: Should register with AccessRegistrarIF here? // NOTE: No need to set loaded members because they will be // retrieved lazily when needed. // Return newly created object return object; } catch (RuntimeException e1) { throw e1; } catch (Exception e2) { throw new OntopiaRuntimeException(e2); } } // ----------------------------------------------------------------------------- // Prefetching // ----------------------------------------------------------------------------- public void prefetch(Class<?> type, int field, boolean traverse, Collection<IdentityIF> identities) { // bug #1439: do not prefetch if identity is altered by local transaction identities = extractNonDirty(identities); // prefetch field values if (log.isDebugEnabled()) log.debug("Prefetching field: " + field + " " + type + " " + identities.size()); this.txncache.prefetch(access, type, field, -1, traverse, identities); } public void prefetch(Class<?> type, int[] fields, boolean[] traverse, Collection<IdentityIF> identities) { // bug #1439: do not prefetch if identity is altered by local transaction identities = extractNonDirty(identities); if (log.isDebugEnabled()) log.debug("Prefetching fields: " + StringUtils.join(fields, ",") + " " + type + " " + identities.size()); ClassInfoIF cinfo = mapping.getClassInfo(type); for (int i=0; i < fields.length; i++) { // prefetch field values boolean moreFields = (i+1 < fields.length); //! System.out.println("PFx: " + fields[i] + " " + type + " " + identities.size()); int prefetched = this.txncache.prefetch(access, type, fields[i], (moreFields ? fields[i+1] : -1), traverse[i], identities); if (prefetched == 0) return; // get next type if (moreFields) { // extract prefetched field values identities = extractFieldValues(type, fields[i], identities); // update type information cinfo = cinfo.getValueFieldInfos()[fields[i]].getValueClassInfo(); type = cinfo.getDescriptorClass(); } } } protected Collection<IdentityIF> extractNonDirty(Collection<IdentityIF> identities) { // get rid of identities that are dirty in this transaction Collection<IdentityIF> result = new HashSet<IdentityIF>(identities.size()); for (IdentityIF identity : identities) { // bug #1439: do not prefetch if identity is altered by local transaction if (!isObjectClean(identity)) continue; result.add(identity); } return result; } protected Collection extractFieldValues(Object type, int field, Collection<IdentityIF> identities) { Collection result = new HashSet(identities.size()); for (IdentityIF identity : identities) { // bug #1439: do not prefetch if identity is altered by local transaction if (!isObjectClean(identity)) continue; // get field value from cache Object value = txncache.getValue(access, identity, field); if (value == null) continue; if (value instanceof Collection) { Collection coll = (Collection)value; if (!coll.isEmpty()) result.addAll(coll); } else { result.add(value); } } return result; } // ----------------------------------------------------------------------------- // Queries // ----------------------------------------------------------------------------- public Object executeQuery(String name, Object[] params) { if (!isactive) throw new TransactionNotActiveException(); try { // Look up query QueryIF query = getQuery(name); // store changes up to this point (conforming queries) flush(); // Execute query return query.executeQuery(params); } catch (RuntimeException e1) { throw e1; } catch (Exception e2) { throw new OntopiaRuntimeException(e2); } } public QueryIF createQuery(JDOQuery jdoquery, boolean resolve_identities) { if (!isactive) throw new TransactionNotActiveException(); // FIXME: Move this method elsewhere? return access.createQuery(jdoquery, oaccess, registrar, resolve_identities); } protected QueryIF getQuery(String name) { QueryIF query = querymap.get(name); if (query == null) { // Create and register query instance lazily query = access.createQuery(name, oaccess, registrar); registerQuery(name, query); } return query; } protected void registerQuery(String name, QueryIF query) { if (log.isDebugEnabled()) log.debug(getId() + ": Registering query '" + name + "'"); querymap.put(name, query); } public void writeIdentityMap(java.io.Writer out, boolean dump) throws java.io.IOException { out.write("<p>Cache size: " + identity_map.size() + ", LRU size: " + lru.size() + " / " + lrusize + "<br>\n"); out.write("Created: " + new Date(timestamp) + " (" + (System.currentTimeMillis()-timestamp) + " ms)</p>\n"); if (dump) { out.write("<table>\n"); for (Map.Entry<IdentityIF, PersistentIF> entry : identity_map.entrySet()) { IdentityIF key = entry.getKey(); PersistentIF val = entry.getValue(); out.write("<tr><td>"); out.write((key == null ? "null" : StringUtils.escapeHTMLEntities(key.toString()))); out.write("</td><td>"); out.write((val == null ? "null" : StringUtils.escapeHTMLEntities(val.toString()))); out.write("</td></tr>\n"); } out.write("</table><br>\n"); } } // ----------------------------------------------------------------------------- // Misc // ----------------------------------------------------------------------------- public String toString() { return "<Transaction " + getId() + ">"; } }