/*
* 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.Collections;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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.schema.FacetNames;
import org.eclipse.ecr.core.storage.StorageException;
import org.eclipse.ecr.core.storage.sql.Fragment.State;
import org.eclipse.ecr.core.storage.sql.RowMapper.CopyHierarchyResult;
import org.eclipse.ecr.core.storage.sql.RowMapper.IdWithTypes;
import org.eclipse.ecr.core.storage.sql.SimpleFragment.PositionComparator;
/**
* This class holds cached information for children relationships in the
* hierarchy table.
*/
public class HierarchyContext {
private static final Log log = LogFactory.getLog(HierarchyContext.class);
private final Model model;
private final RowMapper mapper;
private final SessionImpl session;
private final PersistenceContext context;
private final Map<Serializable, Children> childrenRegularSoft;
// public because used from unit tests
public final Map<Serializable, Children> childrenRegularHard;
private final Map<Serializable, Children> childrenComplexPropSoft;
private final Map<Serializable, Children> childrenComplexPropHard;
private final Map<Serializable, Children>[] childrenAllMaps;
/**
* The parents modified in the transaction, that should be propagated as
* invalidations to other sessions at post-commit time.
*/
private final Set<Serializable> modifiedParentsInTransaction;
private final PositionComparator posComparator;
@SuppressWarnings("unchecked")
public HierarchyContext(Model model, RowMapper mapper, SessionImpl session,
PersistenceContext context) {
this.model = model;
this.mapper = mapper;
this.session = session;
this.context = context;
childrenRegularSoft = new ReferenceMap(ReferenceMap.HARD,
ReferenceMap.SOFT);
childrenRegularHard = new HashMap<Serializable, Children>();
childrenComplexPropSoft = new ReferenceMap(ReferenceMap.HARD,
ReferenceMap.SOFT);
childrenComplexPropHard = new HashMap<Serializable, Children>();
childrenAllMaps = new Map[] { childrenRegularSoft, childrenRegularHard,
childrenComplexPropSoft, childrenComplexPropHard };
modifiedParentsInTransaction = new HashSet<Serializable>();
posComparator = new PositionComparator(model.HIER_CHILD_POS_KEY);
}
public int clearCaches() {
// only the soft children are caches, the others hold info
int n = childrenRegularSoft.size() + childrenComplexPropSoft.size();
childrenRegularSoft.clear();
childrenComplexPropSoft.clear();
modifiedParentsInTransaction.clear();
return n;
}
/** Gets the proper children cache. Creates one if missing. */
protected Children childrenCache(Serializable parentId, boolean complexProp) {
Map<Serializable, Children> softMap;
Map<Serializable, Children> hardMap;
if (complexProp) {
softMap = childrenComplexPropSoft;
hardMap = childrenComplexPropHard;
} else {
softMap = childrenRegularSoft;
hardMap = childrenRegularHard;
}
Children children = softMap.get(parentId);
if (children == null) {
children = hardMap.get(parentId);
if (children == null) {
children = new Children(this, model.HIER_CHILD_NAME_KEY, false,
parentId, softMap, hardMap);
}
}
return children;
}
protected boolean complexProp(SimpleFragment fragment)
throws StorageException {
return ((Boolean) fragment.get(model.HIER_CHILD_ISPROPERTY_KEY)).booleanValue();
}
protected void addExistingChild(SimpleFragment fragment,
boolean complexProp, boolean invalidate) throws StorageException {
Serializable parentId = fragment.get(model.HIER_PARENT_KEY);
if (parentId == null) {
return;
}
childrenCache(parentId, complexProp).addExisting(fragment.getId());
if (invalidate) {
modifiedParentsInTransaction.add(parentId);
}
}
protected void addCreatedChild(SimpleFragment fragment, boolean complexProp)
throws StorageException {
Serializable parentId = fragment.get(model.HIER_PARENT_KEY);
if (parentId == null) {
return;
}
childrenCache(parentId, complexProp).addCreated(fragment.getId());
modifiedParentsInTransaction.add(parentId);
}
protected void removeChild(SimpleFragment fragment, boolean complexProp)
throws StorageException {
Serializable parentId = fragment.get(model.HIER_PARENT_KEY);
if (parentId == null) {
return;
}
childrenCache(parentId, complexProp).remove(fragment.getId());
modifiedParentsInTransaction.add(parentId);
}
public void createdSimpleFragment(SimpleFragment fragment)
throws StorageException {
if (!model.HIER_TABLE_NAME.equals(fragment.row.tableName)) {
return;
}
// add as a child of its parent
addCreatedChild(fragment, complexProp(fragment));
// note that this new row doesn't have children
Serializable parentId = fragment.getId();
new Children(this, model.HIER_CHILD_NAME_KEY, true, parentId,
childrenRegularSoft, childrenRegularHard);
new Children(this, model.HIER_CHILD_NAME_KEY, true, parentId,
childrenComplexPropSoft, childrenComplexPropHard);
}
/**
* Find a fragment in the hierarchy schema given its parent id and name. If
* the fragment is not in the context, fetch it from the mapper.
*
* @param parentId the parent id
* @param name the name
* @param complexProp whether to get complex properties or regular children
* @return the fragment, or {@code null} if not found
*/
public SimpleFragment getChildHierByName(Serializable parentId,
String name, boolean complexProp) throws StorageException {
SimpleFragment fragment = childrenCache(parentId, complexProp).getFragmentByValue(
name);
if (fragment == SimpleFragment.UNKNOWN) {
// read it through the mapper
Row row = mapper.readChildHierRow(parentId, name, complexProp);
fragment = (SimpleFragment) context.getFragmentFromFetchedRow(row,
false);
}
return fragment;
}
/**
* Gets the list of children main fragments for a given parent id.
* <p>
* Complex properties and children of ordered folders are returned in the
* proper order.
*
* @param parentId the parent id
* @param name the name of the children, or {@code null} for all
* @param complexProp whether to get complex properties or regular children
* @return the list of children main fragments
*/
public List<SimpleFragment> getChildren(Serializable parentId, String name,
boolean complexProp) throws StorageException {
Children children = childrenCache(parentId, complexProp);
List<SimpleFragment> fragments = children.getFragmentsByValue(name);
if (fragments == null) {
// no complete list is known
// ask the actual children to the mapper
List<Row> rows = mapper.readChildHierRows(parentId, complexProp);
List<Fragment> frags = context.getFragmentsFromFetchedRows(rows,
false);
fragments = new ArrayList<SimpleFragment>(frags.size());
List<Serializable> ids = new ArrayList<Serializable>(frags.size());
for (Fragment fragment : frags) {
fragments.add((SimpleFragment) fragment);
ids.add(fragment.getId());
}
children.addExistingComplete(ids);
// redo the query, as the children may include newly-created ones,
// and we also filter by name
fragments = children.getFragmentsByValue(name);
}
if (isOrderable(parentId, complexProp)) {
// sort children in order
Collections.sort(fragments, posComparator);
}
return fragments;
}
protected boolean isOrderable(Serializable parentId, boolean complexProp)
throws StorageException {
if (complexProp) {
return true;
}
SimpleFragment parent = getHier(parentId, true);
String typeName = parent.getString(model.MAIN_PRIMARY_TYPE_KEY);
return model.getDocumentTypeFacets(typeName).contains(
FacetNames.ORDERABLE);
}
/**
* Finds the id of the enclosing non-complex-property node.
*
* @param id the id
* @return the id of the containing document, or {@code null} if there is no
* parent or the parent has been deleted.
*/
public Serializable getContainingDocument(Serializable id)
throws StorageException {
Serializable pid = id;
while (true) {
if (pid == null) {
// no parent
return null;
}
SimpleFragment p = getHier(pid, false);
if (p == null) {
// can happen if the fragment has been deleted
return null;
}
if (!complexProp(p)) {
return pid;
}
pid = p.get(model.HIER_PARENT_KEY);
}
}
/** Checks that we don't move/copy under ourselves. */
protected void checkNotUnder(Serializable parentId, Serializable id,
String op) throws StorageException {
Serializable pid = parentId;
do {
if (pid.equals(id)) {
throw new StorageException("Cannot " + op
+ " a node under itself: " + parentId + " is under "
+ id);
}
SimpleFragment p = getHier(pid, false);
if (p == null) {
// cannot happen
throw new StorageException("No parent: " + pid);
}
pid = p.get(model.HIER_PARENT_KEY);
} while (pid != null);
}
/** Checks that a name is free. Cannot check concurrent sessions though. */
protected void checkFreeName(Serializable parentId, String name,
boolean complexProp) throws StorageException {
Fragment fragment = getChildHierByName(parentId, name, complexProp);
if (fragment != null) {
throw new StorageException("Destination name already exists: "
+ name);
}
}
/**
* Order a child before another.
*
* @param parentId the parent id
* @param sourceId the node id to move
* @param destId the node id before which to place the source node, if
* {@code null} then move the source to the end
*/
public void orderBefore(Serializable parentId, Serializable sourceId,
Serializable destId) throws StorageException {
boolean complexProp = false;
if (!isOrderable(parentId, complexProp)) {
// TODO throw exception?
return;
}
if (sourceId.equals(destId)) {
return;
}
// This is optimized by assuming the number of children is small enough
// to be manageable in-memory.
// fetch children and relevant nodes
List<SimpleFragment> fragments = getChildren(parentId, null,
complexProp);
// renumber fragments
int i = 0;
SimpleFragment source = null; // source if seen
Long destPos = null;
for (SimpleFragment fragment : fragments) {
Serializable id = fragment.getId();
if (id.equals(destId)) {
destPos = Long.valueOf(i);
i++;
if (source != null) {
source.put(model.HIER_CHILD_POS_KEY, destPos);
}
}
Long setPos;
if (id.equals(sourceId)) {
i--;
source = fragment;
setPos = destPos;
} else {
setPos = Long.valueOf(i);
}
if (setPos != null) {
if (!setPos.equals(fragment.get(model.HIER_CHILD_POS_KEY))) {
fragment.put(model.HIER_CHILD_POS_KEY, setPos);
}
}
i++;
}
if (destId == null) {
Long setPos = Long.valueOf(i);
if (!setPos.equals(source.get(model.HIER_CHILD_POS_KEY))) {
source.put(model.HIER_CHILD_POS_KEY, setPos);
}
}
}
/**
* Gets the next pos value for a new child in a folder.
*
* @param nodeId the folder node id
* @param complexProp whether to deal with complex properties or regular
* children
* @return the next pos, or {@code null} if not orderable
*/
public Long getNextPos(Serializable nodeId, boolean complexProp)
throws StorageException {
if (!isOrderable(nodeId, complexProp)) {
return null;
}
long max = -1;
for (SimpleFragment fragment : getChildren(nodeId, null, complexProp)) {
Long pos = (Long) fragment.get(model.HIER_CHILD_POS_KEY);
if (pos != null && pos.longValue() > max) {
max = pos.longValue();
}
}
return Long.valueOf(max + 1);
}
/**
* Move a child to a new parent with a new name.
*
* @param source the source
* @param parentId the destination parent id
* @param name the new name
* @throws StorageException
*/
public void move(Node source, Serializable parentId, String name)
throws StorageException {
// a save() has already been done by the caller
Serializable id = source.getId();
SimpleFragment hierFragment = source.getHierFragment();
Serializable oldParentId = hierFragment.get(model.HIER_PARENT_KEY);
String oldName = hierFragment.getString(model.HIER_CHILD_NAME_KEY);
if (!oldParentId.equals(parentId)) {
checkNotUnder(parentId, id, "move");
} else if (oldName.equals(name)) {
// null move
return;
}
boolean complexProp = complexProp(hierFragment);
checkFreeName(parentId, name, complexProp);
/*
* Do the move.
*/
if (!oldName.equals(name)) {
hierFragment.put(model.HIER_CHILD_NAME_KEY, name);
}
removeChild(hierFragment, complexProp);
hierFragment.put(model.HIER_PARENT_KEY, parentId);
addExistingChild(hierFragment, complexProp, true);
}
/**
* Copy a child to a new parent with a new name.
*
* @param source the source of the copy
* @param parentId the destination parent id
* @param name the new name
* @return the id of the copy
*/
public Serializable copy(Node source, Serializable parentId, String name)
throws StorageException {
Serializable id = source.getId();
SimpleFragment hierFragment = source.getHierFragment();
Serializable oldParentId = hierFragment.get(model.HIER_PARENT_KEY);
if (!oldParentId.equals(parentId)) {
checkNotUnder(parentId, id, "copy");
}
checkFreeName(parentId, name, complexProp(hierFragment));
// do the copy
CopyHierarchyResult res = mapper.copyHierarchy(new IdWithTypes(source),
parentId, name, null);
Serializable newId = res.copyId;
context.markInvalidated(res.invalidations);
// adds it as a new child of its parent:
getHier(newId, false);
return newId;
}
public void removeNode(Fragment hierFragment) throws StorageException {
if (hierFragment.getState() == State.CREATED) {
// only case where we can recurse in children,
// it's safe to do as they're in memory as well
Serializable id = hierFragment.getId();
for (SimpleFragment f : getChildren(id, null, true)) {
context.removeNode(f);
}
for (SimpleFragment f : getChildren(id, null, false)) {
context.removeNode(f);
}
}
// We cannot recursively delete the children from the cache as we don't
// know all their ids and it would be costly to obtain them. Instead we
// do a check on getNodeById using isDeleted() to see if there's a
// deleted parent.
}
/** Deletes a fragment from the context. */
public void removeFragment(Fragment fragment) throws StorageException {
if (!model.HIER_TABLE_NAME.equals(fragment.row.tableName)) {
return;
}
removeChild((SimpleFragment) fragment,
complexProp((SimpleFragment) fragment));
}
public void postSave() {
// flush children caches (moves from hard to soft)
for (Children children : childrenRegularHard.values()) {
children.flush(); // added to soft map
}
childrenRegularHard.clear();
for (Children children : childrenComplexPropHard.values()) {
children.flush(); // added to soft map
}
childrenComplexPropHard.clear();
}
// called by Children
public SimpleFragment getHierIfPresent(Serializable id) {
RowId rowId = new RowId(model.HIER_TABLE_NAME, id);
return (SimpleFragment) context.getIfPresent(rowId);
}
// also called by Children
public SimpleFragment getHier(Serializable id, boolean allowAbsent)
throws StorageException {
RowId rowId = new RowId(model.HIER_TABLE_NAME, id);
return (SimpleFragment) context.get(rowId, allowAbsent);
}
public void recordFragment(Fragment fragment) throws StorageException {
if (!model.HIER_TABLE_NAME.equals(fragment.row.tableName)) {
return;
}
// add as a child of its parent
addExistingChild((SimpleFragment) fragment,
complexProp((SimpleFragment) fragment), false);
}
/** Recursively checks if any of a fragment's parents has been deleted. */
// needed because we don't recursively clear caches when doing a delete
public boolean isDeleted(Serializable id) throws StorageException {
while (id != null) {
SimpleFragment fragment = getHier(id, false);
State state;
if (fragment == null
|| (state = fragment.getState()) == State.ABSENT
|| state == State.DELETED
|| state == State.INVALIDATED_DELETED) {
return true;
}
id = fragment.get(model.HIER_PARENT_KEY);
}
return false;
}
/**
* Checks in a node (creates a version).
*
* @param node the node to check in
* @param label the version label
* @param checkinComment the version description
* @return the created version id
*/
public Serializable checkIn(Node node, String label, String checkinComment)
throws StorageException {
Boolean checkedIn = (Boolean) node.hierFragment.get(model.MAIN_CHECKED_IN_KEY);
if (Boolean.TRUE.equals(checkedIn)) {
throw new StorageException("Already checked in");
}
if (label == null) {
// use version major + minor as label
try {
Serializable major = node.getSimpleProperty(
model.MAIN_MAJOR_VERSION_PROP).getValue();
Serializable minor = node.getSimpleProperty(
model.MAIN_MINOR_VERSION_PROP).getValue();
if (major == null || minor == null) {
label = "";
} else {
label = major + "." + minor;
}
} catch (StorageException e) {
log.error("Cannot get version", e);
label = "";
}
}
/*
* Do the copy without non-complex children, with null parent.
*/
Serializable id = node.getId();
CopyHierarchyResult res = mapper.copyHierarchy(new IdWithTypes(node),
null, null, null);
Serializable newId = res.copyId;
context.markInvalidated(res.invalidations);
// add version as a new child of its parent
SimpleFragment verHier = getHier(newId, false);
verHier.put(model.MAIN_IS_VERSION_KEY, Boolean.TRUE);
boolean isMajor = Long.valueOf(0).equals(
verHier.get(model.MAIN_MINOR_VERSION_KEY));
// create a "version" row for our new version
Row row = new Row(model.VERSION_TABLE_NAME, newId);
row.putNew(model.VERSION_VERSIONABLE_KEY, id);
row.putNew(model.VERSION_CREATED_KEY, new GregorianCalendar()); // now
row.putNew(model.VERSION_LABEL_KEY, label);
row.putNew(model.VERSION_DESCRIPTION_KEY, checkinComment);
row.putNew(model.VERSION_IS_LATEST_KEY, Boolean.TRUE);
row.putNew(model.VERSION_IS_LATEST_MAJOR_KEY, Boolean.valueOf(isMajor));
context.createSimpleFragment(row);
// update the original node to reflect that it's checked in
node.hierFragment.put(model.MAIN_CHECKED_IN_KEY, Boolean.TRUE);
node.hierFragment.put(model.MAIN_BASE_VERSION_KEY, newId);
recomputeVersionSeries(id);
return newId;
}
// recompute isLatest / isLatestMajor on all versions
protected void recomputeVersionSeries(Serializable versionSeriesId)
throws StorageException {
session.flush(); // needed by following search
List<Fragment> versFrags = context.getVersionFragments(versionSeriesId);
Collections.reverse(versFrags);
boolean isLatest = true;
boolean isLatestMajor = true;
for (Fragment vf : versFrags) {
SimpleFragment vsf = (SimpleFragment) vf;
// isLatestVersion
vsf.put(model.VERSION_IS_LATEST_KEY, Boolean.valueOf(isLatest));
isLatest = false;
// isLatestMajorVersion
SimpleFragment vh = getHier(vsf.getId(), true);
boolean isMajor = Long.valueOf(0).equals(
vh.get(model.MAIN_MINOR_VERSION_KEY));
vsf.put(model.VERSION_IS_LATEST_MAJOR_KEY,
Boolean.valueOf(isMajor && isLatestMajor));
if (isMajor) {
isLatestMajor = false;
}
}
}
/**
* Checks out a node.
*
* @param node the node to check out
*/
public void checkOut(Node node) throws StorageException {
Boolean checkedIn = (Boolean) node.hierFragment.get(model.MAIN_CHECKED_IN_KEY);
if (!Boolean.TRUE.equals(checkedIn)) {
throw new StorageException("Already checked out");
}
// update the node to reflect that it's checked out
node.hierFragment.put(model.MAIN_CHECKED_IN_KEY, Boolean.FALSE);
}
/**
* Restores a node to a given version.
* <p>
* The restored node is checked in.
*
* @param node the node
* @param version the version to restore on this node
*/
public void restoreVersion(Node node, Node version) throws StorageException {
Serializable versionableId = node.getId();
Serializable versionId = version.getId();
// clear complex properties
List<SimpleFragment> children = getChildren(versionableId, null, true);
// copy to avoid concurrent modifications
for (Fragment child : children.toArray(new Fragment[children.size()])) {
context.removeFragment(child); // will cascade deletes
}
session.flush(); // flush deletes
// copy the version values
Row overwriteRow = new Row(model.HIER_TABLE_NAME, versionableId);
SimpleFragment versionHier = version.getHierFragment();
for (String key : model.getFragmentKeysType(model.HIER_TABLE_NAME).keySet()) {
// keys we don't copy from version when restoring
if (key.equals(model.HIER_PARENT_KEY)
|| key.equals(model.HIER_CHILD_NAME_KEY)
|| key.equals(model.HIER_CHILD_POS_KEY)
|| key.equals(model.HIER_CHILD_ISPROPERTY_KEY)
|| key.equals(model.MAIN_PRIMARY_TYPE_KEY)
|| key.equals(model.MAIN_CHECKED_IN_KEY)
|| key.equals(model.MAIN_BASE_VERSION_KEY)
|| key.equals(model.MAIN_IS_VERSION_KEY)) {
continue;
}
overwriteRow.putNew(key, versionHier.get(key));
}
overwriteRow.putNew(model.MAIN_CHECKED_IN_KEY, Boolean.TRUE);
overwriteRow.putNew(model.MAIN_BASE_VERSION_KEY, versionId);
overwriteRow.putNew(model.MAIN_IS_VERSION_KEY, null);
CopyHierarchyResult res = mapper.copyHierarchy(
new IdWithTypes(version), node.getParentId(), null,
overwriteRow);
context.markInvalidated(res.invalidations);
}
/**
* Marks locally all the invalidations gathered by a {@link Mapper}
* operation (like a version restore).
*/
public void markInvalidated(Set<RowId> modified) {
for (RowId rowId : modified) {
if (Invalidations.PARENT.equals(rowId.tableName)) {
Serializable parentId = rowId.id;
for (Map<Serializable, Children> map : childrenAllMaps) {
Children children = map.get(parentId);
if (children != null) {
children.setIncomplete();
}
}
modifiedParentsInTransaction.add(parentId);
}
}
}
/**
* Gathers invalidations from this session.
* <p>
* Called post-transaction to gathers invalidations to be sent to others.
*/
public void gatherInvalidations(Invalidations invalidations) {
for (Serializable id : modifiedParentsInTransaction) {
invalidations.addModified(new RowId(Invalidations.PARENT, id));
}
modifiedParentsInTransaction.clear();
}
/**
* Processes all invalidations accumulated.
* <p>
* Called pre-transaction.
*/
public void processReceivedInvalidations(Set<RowId> modified)
throws StorageException {
for (RowId rowId : modified) {
if (Invalidations.PARENT.equals(rowId.tableName)) {
Serializable parentId = rowId.id;
for (Map<Serializable, Children> map : childrenAllMaps) {
map.remove(parentId);
}
}
}
}
}