/*
* Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Florent Guillaume
*/
package org.eclipse.ecr.core.storage.sql;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.collections.map.ReferenceMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.ecr.core.storage.StorageException;
import org.eclipse.ecr.core.storage.sql.Fragment.State;
import org.eclipse.ecr.core.storage.sql.Invalidations.InvalidationsPair;
import org.eclipse.ecr.core.storage.sql.RowMapper.RowBatch;
import org.eclipse.ecr.core.storage.sql.RowMapper.RowUpdate;
/**
* This class holds persistence context information.
* <p>
* All non-saved modified data is referenced here. At save time, the data is
* sent to the database by the {@link Mapper}. The database will at some time
* later be committed by the external transaction manager in effect.
* <p>
* Internally a fragment can be in at most one of the "pristine" or "modified"
* map. After a save() all the fragments are pristine, and may be partially
* invalidated after commit by other local or clustered contexts that committed
* too.
* <p>
* Depending on the table, the context may hold {@link SimpleFragment}s, which
* represent one row, {@link CollectionFragment}s, which represent several rows.
* <p>
* This class is not thread-safe, it should be tied to a single session and the
* session itself should not be used concurrently.
*/
public class PersistenceContext {
private static final Log log = LogFactory.getLog(PersistenceContext.class);
protected final Model model;
// protected because accessed by Fragment.refetch()
protected final RowMapper mapper;
private final SessionImpl session;
// public because used by unit tests
public final HierarchyContext hierContext;
/**
* The pristine fragments. All held data is identical to what is present in
* the database and could be refetched if needed.
* <p>
* This contains fragment that are {@link State#PRISTINE} or
* {@link State#ABSENT}, or in some cases {@link State#INVALIDATED_MODIFIED}
* or {@link State#INVALIDATED_DELETED}.
* <p>
* Pristine fragments must be kept here when referenced by the application,
* because the application must get the same fragment object if asking for
* it twice, even in two successive transactions.
* <p>
* This is memory-sensitive, a fragment can always be refetched if nobody
* uses it and the GC collects it. Use a weak reference for the values, we
* don't hold them longer than they need to be referenced, as the underlying
* mapper also has its own cache.
*/
protected final Map<RowId, Fragment> pristine;
/**
* The fragments changed by the session.
* <p>
* This contains fragment that are {@link State#CREATED},
* {@link State#MODIFIED} or {@link State#DELETED}.
*/
protected final Map<RowId, Fragment> modified;
/**
* Fragment ids generated but not yet saved. We know that any fragment with
* one of these ids cannot exist in the database.
*/
private final Set<Serializable> createdIds;
@SuppressWarnings("unchecked")
public PersistenceContext(Model model, RowMapper mapper, SessionImpl session)
throws StorageException {
this.model = model;
this.mapper = mapper;
this.session = session;
hierContext = new HierarchyContext(model, mapper, session, this);
// use a weak reference for the values, we don't hold them longer than
// they need to be referenced, as the underlying mapper also has its own
// cache
pristine = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK);
modified = new HashMap<RowId, Fragment>();
// this has to be linked to keep creation order, as foreign keys
// are used and need this
createdIds = new LinkedHashSet<Serializable>();
}
protected int clearCaches() {
mapper.clearCache();
hierContext.clearCaches();
// TODO there should be a synchronization here
// but this is a rare operation and we don't call
// it if a transaction is in progress
int n = pristine.size();
pristine.clear();
modified.clear(); // not empty when rolling back before save
createdIds.clear();
return n;
}
/**
* Generates a new id, or used a pre-generated one (import).
*/
protected Serializable generateNewId(Serializable id) {
if (id == null) {
id = model.generateNewId();
}
createdIds.add(id);
return id;
}
protected boolean isIdNew(Serializable id) {
return createdIds.contains(id);
}
/**
* Saves all the created, modified and deleted rows into a batch object, for
* later execution.
*/
protected RowBatch getSaveBatch() throws StorageException {
RowBatch batch = new RowBatch();
// created main rows are saved first in the batch (in their order of
// creation), because they are used as foreign keys in all other tables
for (Serializable id : createdIds) {
RowId rowId = new RowId(model.HIER_TABLE_NAME, id);
Fragment fragment = modified.remove(rowId);
if (fragment == null) {
// was created and deleted before save
continue;
}
batch.creates.add(fragment.row);
fragment.clearDirty();
fragment.setPristine();
pristine.put(rowId, fragment);
}
createdIds.clear();
// save the rest
for (Entry<RowId, Fragment> en : modified.entrySet()) {
RowId rowId = en.getKey();
Fragment fragment = en.getValue();
switch (fragment.getState()) {
case CREATED:
batch.creates.add(fragment.row);
fragment.clearDirty();
fragment.setPristine();
// modified map cleared at end of loop
pristine.put(rowId, fragment);
break;
case MODIFIED:
if (fragment.row.isCollection()) {
if (((CollectionFragment) fragment).isDirty()) {
batch.updates.add(new RowUpdate(fragment.row, null));
fragment.clearDirty();
}
} else {
Collection<String> keys = ((SimpleFragment) fragment).getDirtyKeys();
if (!keys.isEmpty()) {
batch.updates.add(new RowUpdate(fragment.row, keys));
fragment.clearDirty();
}
}
fragment.setPristine();
// modified map cleared at end of loop
pristine.put(rowId, fragment);
break;
case DELETED:
// TODO deleting non-hierarchy fragments is done by the database
// itself as their foreign key to hierarchy is ON DELETE CASCADE
batch.deletes.add(new RowId(rowId));
fragment.setDetached();
// modified map cleared at end of loop
break;
case PRISTINE:
// cannot happen, but has been observed :(
log.error("Found PRISTINE fragment in modified map: "
+ fragment);
break;
default:
throw new RuntimeException(fragment.toString());
}
}
modified.clear();
// flush children caches
hierContext.postSave();
return batch;
}
protected Serializable getContainingDocument(Serializable id)
throws StorageException {
return hierContext.getContainingDocument(id);
}
/**
* Finds the documents having dirty text or dirty binaries that have to be
* reindexed as fulltext.
*
* @param dirtyStrings set of ids, updated by this method
* @param dirtyBinaries set of ids, updated by this method
*/
protected void findDirtyDocuments(Set<Serializable> dirtyStrings,
Set<Serializable> dirtyBinaries) throws StorageException {
for (Fragment fragment : modified.values()) {
Serializable docId = null;
switch (fragment.getState()) {
case CREATED:
docId = getContainingDocument(fragment.getId());
dirtyStrings.add(docId);
dirtyBinaries.add(docId);
break;
case MODIFIED:
String tableName = fragment.row.tableName;
Collection<String> keys;
if (model.isCollectionFragment(tableName)) {
keys = Collections.singleton(null);
} else {
keys = ((SimpleFragment) fragment).getDirtyKeys();
}
for (String key : keys) {
PropertyType type = model.getFulltextFieldType(tableName,
key);
if (type == null) {
continue;
}
if (docId == null) {
docId = getContainingDocument(fragment.getId());
}
if (type == PropertyType.STRING) {
dirtyStrings.add(docId);
} else if (type == PropertyType.BINARY) {
dirtyBinaries.add(docId);
}
}
break;
case DELETED:
docId = getContainingDocument(fragment.getId());
if (!isDeleted(docId)) {
// this is a deleted fragment of a complex property from a
// document that has not been completely deleted
dirtyStrings.add(docId);
dirtyBinaries.add(docId);
}
break;
default:
}
}
}
/**
* Marks locally all the invalidations gathered by a {@link Mapper}
* operation (like a version restore).
*/
protected void markInvalidated(Invalidations invalidations) {
if (invalidations.modified != null) {
for (RowId rowId : invalidations.modified) {
Fragment fragment = getIfPresent(rowId);
if (fragment != null) {
setFragmentPristine(fragment);
fragment.setInvalidatedModified();
}
}
hierContext.markInvalidated(invalidations.modified);
}
if (invalidations.deleted != null) {
for (RowId rowId : invalidations.deleted) {
Fragment fragment = getIfPresent(rowId);
if (fragment != null) {
setFragmentPristine(fragment);
fragment.setInvalidatedDeleted();
}
}
}
// TODO XXX transactionInvalidations.add(invalidations);
}
// called from Fragment
protected void setFragmentModified(Fragment fragment) {
RowId rowId = fragment.row;
pristine.remove(rowId);
modified.put(rowId, fragment);
}
// also called from Fragment
protected void setFragmentPristine(Fragment fragment) {
RowId rowId = fragment.row;
modified.remove(rowId);
pristine.put(rowId, fragment);
}
/**
* Post-transaction invalidations notification.
* <p>
* Called post-transaction by session commit/rollback or transactionless
* save.
*/
protected void sendInvalidationsToOthers() throws StorageException {
Invalidations invalidations = new Invalidations();
hierContext.gatherInvalidations(invalidations);
mapper.sendInvalidations(invalidations);
// events sent in mapper
}
/**
* Applies all invalidations accumulated.
* <p>
* Called pre-transaction by start or transactionless save;
*/
protected void processReceivedInvalidations()
throws StorageException {
InvalidationsPair invals = mapper.receiveInvalidations();
if (invals == null) {
return;
}
processCacheInvalidations(invals.cacheInvalidations);
session.sendInvalidationEvent(invals);
}
protected void processCacheInvalidations(Invalidations invalidations)
throws StorageException {
if (invalidations == null) {
return;
}
if (invalidations.modified != null) {
for (RowId rowId : invalidations.modified) {
Fragment fragment = pristine.remove(rowId);
if (fragment != null) {
fragment.setInvalidatedModified();
}
}
hierContext.processReceivedInvalidations(invalidations.modified);
}
if (invalidations.deleted != null) {
for (RowId rowId : invalidations.deleted) {
Fragment fragment = pristine.remove(rowId);
if (fragment != null) {
fragment.setInvalidatedDeleted();
}
}
}
}
protected void checkInvalidationsConflict() {
// synchronized (receivedInvalidations) {
// if (receivedInvalidations.modified != null) {
// for (RowId rowId : receivedInvalidations.modified) {
// if (transactionInvalidations.contains(rowId)) {
// throw new ConcurrentModificationException(
// "Updating a concurrently modified value: "
// + new RowId(rowId));
// }
// }
// }
//
// if (receivedInvalidations.deleted != null) {
// for (RowId rowId : receivedInvalidations.deleted) {
// if (transactionInvalidations.contains(rowId)) {
// throw new ConcurrentModificationException(
// "Updating a concurrently deleted value: "
// + new RowId(rowId));
// }
// }
// }
// }
}
/**
* Gets a fragment, if present in the context.
* <p>
* Called by {@link #get}, and by the {@link Mapper} to reuse known
* hierarchy fragments in lists of children.
*
* @param rowId the fragment id
* @return the fragment, or {@code null} if not found
*/
protected Fragment getIfPresent(RowId rowId) {
Fragment fragment = pristine.get(rowId);
if (fragment != null) {
return fragment;
}
return modified.get(rowId);
}
/**
* Gets a fragment.
* <p>
* If it's not in the context, fetch it from the mapper. If it's not in the
* database, returns {@code null} or an absent fragment.
* <p>
* Deleted fragments may be returned.
*
* @param rowId the fragment id
* @param allowAbsent {@code true} to return an absent fragment as an object
* instead of {@code null}
* @return the fragment, or {@code null} if none is found and {@value
* allowAbsent} was {@code false}
*/
protected Fragment get(RowId rowId, boolean allowAbsent)
throws StorageException {
Fragment fragment = getIfPresent(rowId);
if (fragment == null) {
fragment = getFromMapper(rowId, allowAbsent);
}
// if (fragment != null && fragment.getState() == State.DELETED) {
// fragment = null;
// }
return fragment;
}
protected Fragment getFromMapper(RowId rowId, boolean allowAbsent)
throws StorageException {
List<Fragment> fragments = getFromMapper(Collections.singleton(rowId),
allowAbsent);
return fragments.isEmpty() ? null : fragments.get(0);
}
/**
* Gets a collection of fragments from the mapper. No order is kept between
* the inputs and outputs.
* <p>
* Fragments not found are not returned if {@code allowAbsent} is
* {@code false}.
*/
protected List<Fragment> getFromMapper(Collection<RowId> rowIds,
boolean allowAbsent) throws StorageException {
List<Fragment> res = new ArrayList<Fragment>(rowIds.size());
// find fragments we really want to fetch
List<RowId> todo = new ArrayList<RowId>(rowIds.size());
for (RowId rowId : rowIds) {
if (isIdNew(rowId.id)) {
// the id has not been saved, so nothing exists yet in the
// database
// rowId is not a row -> will use an absent fragment
Fragment fragment = getFragmentFromFetchedRow(rowId,
allowAbsent);
if (fragment != null) {
res.add(fragment);
}
} else {
todo.add(rowId);
}
}
if (todo.isEmpty()) {
return res;
}
// fetch these fragments in bulk
List<? extends RowId> rows = mapper.read(todo);
res.addAll(getFragmentsFromFetchedRows(rows, allowAbsent));
return res;
}
/**
* Gets a list of fragments.
* <p>
* If a fragment is not in the context, fetch it from the mapper. If it's
* not in the database, use an absent fragment or skip it.
* <p>
* Deleted fragments are skipped.
*
* @param id the fragment id
* @param allowAbsent {@code true} to return an absent fragment as an object
* instead of skipping it
* @return the fragments, in arbitrary order (no {@code null}s)
*/
protected List<Fragment> getMulti(Collection<RowId> rowIds,
boolean allowAbsent) throws StorageException {
if (rowIds.isEmpty()) {
return Collections.emptyList();
}
// find those already in the context
List<Fragment> res = new ArrayList<Fragment>(rowIds.size());
List<RowId> todo = new LinkedList<RowId>();
for (RowId rowId : rowIds) {
Fragment fragment = getIfPresent(rowId);
if (fragment == null) {
todo.add(rowId);
} else {
if (fragment.getState() != State.DELETED) {
res.add(fragment);
}
}
}
if (todo.isEmpty()) {
return res;
}
// fetch missing ones, return union
List<Fragment> fetched = getFromMapper(todo, allowAbsent);
res.addAll(fetched);
return res;
}
/**
* Turns the given rows (just fetched from the mapper) into fragments and
* record them in the context.
* <p>
* For each row, if the context already contains a fragment with the given
* id, it is returned instead of building a new one.
* <p>
* Deleted fragments are skipped.
* <p>
* If a simple {@link RowId} is passed, it means that an absent row was
* found by the mapper. An absent fragment will be returned, unless
* {@code allowAbsent} is {@code false} in which case it will be skipped.
*
* @param rowIds the list of rows or row ids
* @param allowAbsent {@code true} to return an absent fragment as an object
* instead of {@code null}
* @return the list of fragments
*/
protected List<Fragment> getFragmentsFromFetchedRows(
List<? extends RowId> rowIds, boolean allowAbsent)
throws StorageException {
List<Fragment> fragments = new ArrayList<Fragment>(rowIds.size());
for (RowId rowId : rowIds) {
Fragment fragment = getFragmentFromFetchedRow(rowId, allowAbsent);
if (fragment != null) {
fragments.add(fragment);
}
}
return fragments;
}
/**
* Turns the given row (just fetched from the mapper) into a fragment and
* record it in the context.
* <p>
* If the context already contains a fragment with the given id, it is
* returned instead of building a new one.
* <p>
* If the fragment was deleted, {@code null} is returned.
* <p>
* If a simple {@link RowId} is passed, it means that an absent row was
* found by the mapper. An absent fragment will be returned, unless
* {@code allowAbsent} is {@code false} in which case {@code null} will be
* returned.
*
* @param rowId the row or row id (may be {@code null})
* @param allowAbsent {@code true} to return an absent fragment as an object
* instead of {@code null}
* @return the fragment, or {@code null} if it was deleted
*/
protected Fragment getFragmentFromFetchedRow(RowId rowId,
boolean allowAbsent) throws StorageException {
if (rowId == null) {
return null;
}
Fragment fragment = getIfPresent(rowId);
if (fragment != null) {
// row is already known in the context, use it
State state = fragment.getState();
if (state == State.DELETED) {
// row has been deleted in the context, ignore it
return null;
} else if (state == State.ABSENT
|| state == State.INVALIDATED_MODIFIED
|| state == State.INVALIDATED_DELETED) {
// XXX TODO
throw new IllegalStateException(state.toString());
} else {
// keep existing fragment
return fragment;
}
}
boolean isCollection = model.isCollectionFragment(rowId.tableName);
if (rowId instanceof Row) {
Row row = (Row) rowId;
if (isCollection) {
fragment = new CollectionFragment(row, State.PRISTINE, this);
} else {
fragment = new SimpleFragment(row, State.PRISTINE, this);
}
hierContext.recordFragment(fragment);
return fragment;
} else {
if (allowAbsent) {
if (isCollection) {
Serializable[] empty = model.getCollectionFragmentType(
rowId.tableName).getEmptyArray();
Row row = new Row(rowId.tableName, rowId.id, empty);
return new CollectionFragment(row, State.ABSENT, this);
} else {
Row row = new Row(rowId.tableName, rowId.id);
return new SimpleFragment(row, State.ABSENT, this);
}
} else {
return null;
}
}
}
/**
* Creates a new fragment for a new row, not yet saved.
*
* @param row the row
* @return the created fragment
* @throws StorageException if the fragment is already in the context
*/
protected SimpleFragment createSimpleFragment(Row row)
throws StorageException {
if (pristine.containsKey(row) || modified.containsKey(row)) {
throw new StorageException("Row already registered: " + row);
}
SimpleFragment fragment = new SimpleFragment(row, State.CREATED, this);
hierContext.createdSimpleFragment(fragment);
return fragment;
}
protected void removeNode(Fragment hierFragment) throws StorageException {
hierContext.removeNode(hierFragment);
Serializable id = hierFragment.getId();
// remove the lock using the lock manager
session.removeLock(id, null, false);
// find all the fragments with this id in the maps
List<Fragment> fragments = new LinkedList<Fragment>();
for (Fragment fragment : pristine.values()) {
if (id.equals(fragment.getId())) {
fragments.add(fragment);
}
}
for (Fragment fragment : modified.values()) {
if (id.equals(fragment.getId())) {
if (fragment.getState() != State.DELETED) {
fragments.add(fragment);
}
}
}
// remove the fragments
for (Fragment fragment : fragments) {
removeFragment(fragment);
}
}
/** Deletes a fragment from the context. */
protected void removeFragment(Fragment fragment) throws StorageException {
hierContext.removeFragment(fragment);
RowId rowId = fragment.row;
switch (fragment.getState()) {
case ABSENT:
case INVALIDATED_DELETED:
pristine.remove(rowId);
break;
case CREATED:
modified.remove(rowId);
break;
case PRISTINE:
case INVALIDATED_MODIFIED:
pristine.remove(rowId);
modified.put(rowId, fragment);
break;
case MODIFIED:
// already in modified
break;
case DETACHED:
case DELETED:
break;
}
fragment.setDeleted();
}
public void recomputeVersionSeries(Serializable versionSeriesId)
throws StorageException {
hierContext.recomputeVersionSeries(versionSeriesId);
}
protected List<Serializable> getVersionIds(Serializable versionSeriesId)
throws StorageException {
List<Row> rows = mapper.getVersionRows(versionSeriesId);
List<Fragment> fragments = getFragmentsFromFetchedRows(rows, false);
return fragmentsIds(fragments);
}
protected List<Fragment> getVersionFragments(Serializable versionSeriesId)
throws StorageException {
List<Row> rows = mapper.getVersionRows(versionSeriesId);
return getFragmentsFromFetchedRows(rows, false);
}
protected List<Serializable> getProxyIds(Serializable searchId,
boolean byTarget, Serializable parentId) throws StorageException {
List<Row> rows = mapper.getProxyRows(searchId, byTarget, parentId);
List<Fragment> fragments = getFragmentsFromFetchedRows(rows, false);
return fragmentsIds(fragments);
}
private List<Serializable> fragmentsIds(List<Fragment> fragments) {
List<Serializable> ids = new ArrayList<Serializable>(fragments.size());
for (Fragment fragment : fragments) {
ids.add(fragment.getId());
}
return ids;
}
/*
* ----- Pass-through to HierarchyContext -----
*/
protected boolean isDeleted(Serializable id) throws StorageException {
return hierContext.isDeleted(id);
}
protected Long getNextPos(Serializable nodeId, boolean complexProp)
throws StorageException {
return hierContext.getNextPos(nodeId, complexProp);
}
protected void orderBefore(Serializable parentId, Serializable sourceId,
Serializable destId) throws StorageException {
hierContext.orderBefore(parentId, sourceId, destId);
}
protected SimpleFragment getChildHierByName(Serializable parentId,
String name, boolean complexProp) throws StorageException {
return hierContext.getChildHierByName(parentId, name, complexProp);
}
protected List<SimpleFragment> getChildren(Serializable parentId,
String name, boolean complexProp) throws StorageException {
return hierContext.getChildren(parentId, name, complexProp);
}
protected void move(Node source, Serializable parentId, String name)
throws StorageException {
hierContext.move(source, parentId, name);
}
protected Serializable copy(Node source, Serializable parentId, String name)
throws StorageException {
return hierContext.copy(source, parentId, name);
}
protected Serializable checkIn(Node node, String label,
String checkinComment) throws StorageException {
return hierContext.checkIn(node, label, checkinComment);
}
protected void checkOut(Node node) throws StorageException {
hierContext.checkOut(node);
}
protected void restoreVersion(Node node, Node version)
throws StorageException {
hierContext.restoreVersion(node, version);
}
}