/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jackrabbit.core.version;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.jcr.InvalidItemStateException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.version.ActivityViolationException;
import javax.jcr.version.VersionException;
import org.apache.commons.collections.map.ReferenceMap;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.cluster.UpdateEventChannel;
import org.apache.jackrabbit.core.cluster.UpdateEventListener;
import org.apache.jackrabbit.core.fs.FileSystem;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.NodeIdFactory;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.observation.DelegatingObservationDispatcher;
import org.apache.jackrabbit.core.observation.EventState;
import org.apache.jackrabbit.core.observation.EventStateCollection;
import org.apache.jackrabbit.core.observation.EventStateCollectionFactory;
import org.apache.jackrabbit.core.persistence.PersistenceManager;
import org.apache.jackrabbit.core.state.ChangeLog;
import org.apache.jackrabbit.core.state.ISMLocking;
import org.apache.jackrabbit.core.state.ItemState;
import org.apache.jackrabbit.core.state.ItemStateCacheFactory;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.ItemStateListener;
import org.apache.jackrabbit.core.state.LocalItemStateManager;
import org.apache.jackrabbit.core.state.NodeReferences;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.state.SharedItemStateManager;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.PathFactory;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This Class implements a VersionManager.
*/
public class InternalVersionManagerImpl extends InternalVersionManagerBase
implements ItemStateListener, UpdateEventListener {
/**
* the default logger
*/
private static Logger log = LoggerFactory.getLogger(InternalVersionManagerImpl.class);
/**
* The path of the jcr:system node: /jcr:system
*/
private static final Path SYSTEM_PATH;
static {
PathFactory factory = PathFactoryImpl.getInstance();
SYSTEM_PATH = factory.getRootPath().resolve(
factory.createElement(NameConstants.JCR_SYSTEM));
}
/**
* The persistence manager for the versions
*/
private final PersistenceManager pMgr;
/**
* The file system for this version manager
*/
private final FileSystem fs;
/**
* the version state manager for the version storage
*/
private VersionItemStateManager sharedStateMgr;
/**
* the virtual item state provider that exposes the version storage
*/
private final VersionItemStateProvider versProvider;
/**
* the dynamic event state collection factory
*/
private final DynamicESCFactory escFactory;
/**
* Persistent root node of the version histories.
*/
private final NodeStateEx historyRoot;
/**
* Persistent root node of the activities.
*/
private final NodeStateEx activitiesRoot;
/**
* Map of returned items. this is kept for invalidating
*/
@SuppressWarnings("unchecked")
private final Map<ItemId, InternalVersionItem> versionItems =
new ReferenceMap(ReferenceMap.HARD, ReferenceMap.WEAK);
/**
* Creates a new internal version manager
*
* @param pMgr underlying persistence manager
* @param fs workspace file system
* @param ntReg node type registry
* @param obsMgr observation manager
* @param systemId node id of the version storage parent (i.e. jcr:system)
* @param historiesId node id of the version storage (i.e. jcr:versionStorage)
* @param activitiesId node id of the activities storage (i.e. jcr:activities)
* @param cacheFactory item state cache factory
* @param ismLocking workspace item state locking
* @throws RepositoryException if an error occurs
*/
public InternalVersionManagerImpl(PersistenceManager pMgr, FileSystem fs,
NodeTypeRegistry ntReg,
DelegatingObservationDispatcher obsMgr,
NodeId systemId,
NodeId historiesId,
NodeId activitiesId,
ItemStateCacheFactory cacheFactory,
ISMLocking ismLocking,
NodeIdFactory nodeIdFactory) throws RepositoryException {
super(ntReg, historiesId, activitiesId, nodeIdFactory);
try {
this.pMgr = pMgr;
this.fs = fs;
this.escFactory = new DynamicESCFactory(obsMgr);
// need to store the version storage root directly into the persistence manager
if (!pMgr.exists(historiesId)) {
NodeState root = pMgr.createNew(historiesId);
root.setParentId(systemId);
root.setNodeTypeName(NameConstants.REP_VERSIONSTORAGE);
PropertyState pt = pMgr.createNew(new PropertyId(historiesId, NameConstants.JCR_PRIMARYTYPE));
pt.setMultiValued(false);
pt.setType(PropertyType.NAME);
pt.setValues(new InternalValue[]{InternalValue.create(NameConstants.REP_VERSIONSTORAGE)});
root.addPropertyName(pt.getName());
ChangeLog cl = new ChangeLog();
cl.added(root);
cl.added(pt);
pMgr.store(cl);
}
// check for jcr:activities
if (!pMgr.exists(activitiesId)) {
NodeState root = pMgr.createNew(activitiesId);
root.setParentId(systemId);
root.setNodeTypeName(NameConstants.REP_ACTIVITIES);
PropertyState pt = pMgr.createNew(new PropertyId(activitiesId, NameConstants.JCR_PRIMARYTYPE));
pt.setMultiValued(false);
pt.setType(PropertyType.NAME);
pt.setValues(new InternalValue[]{InternalValue.create(NameConstants.REP_ACTIVITIES)});
root.addPropertyName(pt.getName());
ChangeLog cl = new ChangeLog();
cl.added(root);
cl.added(pt);
pMgr.store(cl);
}
sharedStateMgr = createItemStateManager(pMgr, systemId, ntReg, cacheFactory, ismLocking, nodeIdFactory);
stateMgr = LocalItemStateManager.createInstance(sharedStateMgr, escFactory, cacheFactory);
stateMgr.addListener(this);
NodeState nodeState = (NodeState) stateMgr.getItemState(historiesId);
historyRoot = new NodeStateEx(stateMgr, ntReg, nodeState, NameConstants.JCR_VERSIONSTORAGE);
nodeState = (NodeState) stateMgr.getItemState(activitiesId);
activitiesRoot = new NodeStateEx(stateMgr, ntReg, nodeState, NameConstants.JCR_ACTIVITIES);
// create the virtual item state provider
versProvider = new VersionItemStateProvider(historiesId, activitiesId, sharedStateMgr);
} catch (ItemStateException e) {
throw new RepositoryException(e);
}
}
/**
* {@inheritDoc}
*/
public VirtualItemStateProvider getVirtualItemStateProvider() {
return versProvider;
}
/**
* Return the persistence manager.
*
* @return the persistence manager
*/
public PersistenceManager getPersistenceManager() {
return pMgr;
}
/**
* {@inheritDoc}
*/
public void close() throws Exception {
pMgr.close();
fs.close();
}
/**
* Returns the event state collection factory.
* @return the event state collection factory.
*/
public DynamicESCFactory getEscFactory() {
return escFactory;
}
/**
* {@inheritDoc}
* <p/>
* This method must not be synchronized since it could cause deadlocks with
* item-reading listeners in the observation thread.
*/
@Override
protected VersionHistoryInfo createVersionHistory(Session session,
final NodeState node, final NodeId copiedFrom)
throws RepositoryException {
NodeStateEx state = (NodeStateEx)
escFactory.doSourced((SessionImpl) session, new SourcedTarget() {
public Object run() throws RepositoryException {
return internalCreateVersionHistory(node, copiedFrom);
}
});
if (state == null) {
throw new InvalidItemStateException(
"History already exists for node " + node.getNodeId());
}
Name root = NameConstants.JCR_ROOTVERSION;
return new VersionHistoryInfo(
state.getNodeId(),
state.getState().getChildNodeEntry(root, 1).getId());
}
/**
* {@inheritDoc}
* <p/>
* This method must not be synchronized since it could cause deadlocks with
* item-reading listeners in the observation thread.
*/
public NodeId createActivity(Session session, final String title) throws RepositoryException {
NodeStateEx state = (NodeStateEx)
escFactory.doSourced((SessionImpl) session, new SourcedTarget() {
public Object run() throws RepositoryException {
return internalCreateActivity(title);
}
});
return state.getNodeId();
}
/**
* {@inheritDoc}
* <p/>
* This method must not be synchronized since it could cause deadlocks with
* item-reading listeners in the observation thread.
*/
public void removeActivity(Session session, final NodeId nodeId)
throws RepositoryException {
escFactory.doSourced((SessionImpl) session, new SourcedTarget() {
public Object run() throws RepositoryException {
InternalActivityImpl act = (InternalActivityImpl) getItem(nodeId);
internalRemoveActivity(act);
return null;
}
});
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasItem(NodeId id) {
VersioningLock.ReadLock lock = acquireReadLock();
try {
return stateMgr.hasItemState(id);
} finally {
lock.release();
}
}
/**
* {@inheritDoc}
*/
@Override
protected InternalVersionItem getItem(NodeId id)
throws RepositoryException {
if (id.equals(historiesId)) {
return null;
}
if (id.equals(activitiesId)) {
return null;
}
VersioningLock.ReadLock lock = acquireReadLock();
try {
synchronized (versionItems) {
InternalVersionItem item = versionItems.get(id);
if (item == null) {
item = createInternalVersionItem(id);
if (item != null) {
versionItems.put(id, item);
} else {
return null;
}
}
return item;
}
} finally {
lock.release();
}
}
/**
* {@inheritDoc}
*
* this method currently does no modifications to the persistence and just
* checks if the checkout is valid in respect to a possible activity set on
* the session
*/
public NodeId canCheckout(NodeStateEx state, NodeId activityId) throws RepositoryException {
NodeId baseId = state.getPropertyValue(NameConstants.JCR_BASEVERSION).getNodeId();
if (activityId != null) {
// If there exists another workspace with node N' where N' also has version
// history H, N' is checked out and the jcr:activity property of N'
// references A, then the checkout fails with an
// ActivityViolationException indicating which workspace currently has
// the checkout.
// we're currently leverage the fact, that only references to "real"
// workspaces are recorded. so check all references if their sources
// exist in 'this' workspace
if (stateMgr.hasNodeReferences(activityId)) {
try {
NodeReferences refs = stateMgr.getNodeReferences(activityId);
for (PropertyId id: refs.getReferences()) {
if (!state.hasNode(id.getParentId())) {
throw new ActivityViolationException("Unable to checkout. " +
"Activity is already used for the same node in " +
"another workspace.");
}
}
} catch (ItemStateException e) {
throw new RepositoryException("Error during checkout.", e);
}
}
// If there is a version in H that is not an eventual predecessor of N but
// whose jcr:activity references A, then the checkout fails with an
// ActivityViolationException
InternalActivityImpl a = (InternalActivityImpl) getItem(activityId);
NodeId historyId = state.getPropertyValue(NameConstants.JCR_VERSIONHISTORY).getNodeId();
InternalVersionHistory history = (InternalVersionHistory) getItem(historyId);
InternalVersion version = a.getLatestVersion(history);
if (version != null) {
InternalVersion baseVersion = (InternalVersion) getItem(baseId);
while (baseVersion != null && !baseVersion.getId().equals(version.getId())) {
baseVersion = baseVersion.getLinearPredecessor();
}
if (baseVersion == null) {
throw new ActivityViolationException("Unable to checkout. " +
"Activity is used by another version on a different branch: " + version.getName());
}
}
}
return baseId;
}
/**
* {@inheritDoc}
* <p/>
* This method must not be synchronized since it could cause deadlocks with
* item-reading listeners in the observation thread.
*/
public InternalVersion checkin(
Session session, final NodeStateEx node, final Calendar created)
throws RepositoryException {
return (InternalVersion) escFactory.doSourced(
(SessionImpl) session,
new SourcedTarget() {
public Object run() throws RepositoryException {
return checkin(node, created);
}
});
}
/**
* {@inheritDoc}
* <p/>
* This method must not be synchronized since it could cause deadlocks with
* item-reading listeners in the observation thread.
*/
public void removeVersion(Session session,
final InternalVersionHistory history,
final Name name)
throws VersionException, RepositoryException {
if (!history.hasVersion(name)) {
throw new VersionException("Version with name " + name.toString()
+ " does not exist in this VersionHistory");
}
escFactory.doSourced((SessionImpl) session, new SourcedTarget() {
public Object run() throws RepositoryException {
internalRemoveVersion((InternalVersionHistoryImpl) history, name);
return null;
}
});
}
/**
* {@inheritDoc}
* <p/>
* This method must not be synchronized since it could cause deadlocks with
* item-reading listeners in the observation thread.
*/
public InternalVersion setVersionLabel(Session session,
final InternalVersionHistory history,
final Name version, final Name label,
final boolean move)
throws RepositoryException {
return (InternalVersion)
escFactory.doSourced((SessionImpl) session, new SourcedTarget() {
public Object run() throws RepositoryException {
return setVersionLabel((InternalVersionHistoryImpl) history, version, label, move);
}
});
}
/**
* Invoked by some external source to indicate that some items in the
* versions tree were updated. Version histories are reloaded if possible.
* Matching items are removed from the cache.
*
* @param items items updated
*/
public void itemsUpdated(Collection<InternalVersionItem> items) {
VersioningLock.ReadLock lock = acquireReadLock();
try {
synchronized (versionItems) {
for (InternalVersionItem item : items) {
InternalVersionItem cached = versionItems.remove(item.getId());
if (cached != null) {
if (cached instanceof InternalVersionHistoryImpl) {
InternalVersionHistoryImpl vh = (InternalVersionHistoryImpl) cached;
try {
vh.reload();
versionItems.put(vh.getId(), vh);
} catch (RepositoryException e) {
log.warn("Unable to update version history: " + e.toString());
}
}
}
}
}
} finally {
lock.release();
}
}
/**
* Set an event channel to inform about updates.
*
* @param eventChannel event channel
*/
public void setEventChannel(UpdateEventChannel eventChannel) {
sharedStateMgr.setEventChannel(eventChannel);
eventChannel.setListener(this);
}
/**
* {@inheritDoc}
*/
@Override
protected void itemDiscarded(InternalVersionItem item) {
// evict removed item from cache
VersioningLock.ReadLock lock = acquireReadLock();
try {
versionItems.remove(item.getId());
} finally {
lock.release();
}
}
/**
* {@inheritDoc}
*/
@Override
protected boolean hasItemReferences(NodeId id)
throws RepositoryException {
return stateMgr.hasNodeReferences(id);
}
/**
* {@inheritDoc}
*/
@Override
protected NodeStateEx getNodeStateEx(NodeId parentNodeId)
throws RepositoryException {
try {
NodeState state = (NodeState) stateMgr.getItemState(parentNodeId);
return new NodeStateEx(stateMgr, ntReg, state, null);
} catch (ItemStateException e) {
throw new RepositoryException(e);
}
}
/**
* returns the version history root node
*
* @return the version history root node
*/
@Override
protected NodeStateEx getHistoryRoot() {
return historyRoot;
}
/**
* returns the activities root node
*
* @return the activities root node
*/
@Override
protected NodeStateEx getActivitiesRoot() {
return activitiesRoot;
}
/**
* Returns the shared item state manager.
* @return the shared item state manager.
*/
protected SharedItemStateManager getSharedStateMgr() {
return sharedStateMgr;
}
/**
* Creates a <code>VersionItemStateManager</code> or derivative.
*
* @param pMgr persistence manager
* @param rootId root node id
* @param ntReg node type registry
* @param cacheFactory cache factory
* @param ismLocking the ISM locking implementation
* @return item state manager
* @throws ItemStateException if an error occurs
*/
protected VersionItemStateManager createItemStateManager(PersistenceManager pMgr,
NodeId rootId,
NodeTypeRegistry ntReg,
ItemStateCacheFactory cacheFactory,
ISMLocking ismLocking,
NodeIdFactory nodeIdFactory)
throws ItemStateException {
return new VersionItemStateManager(pMgr, rootId, ntReg, cacheFactory, ismLocking, nodeIdFactory);
}
/**
* {@inheritDoc}
* <p/>
* Not used.
*/
public void stateCreated(ItemState created) {
}
/**
* {@inheritDoc}
* <p/>
* Not used.
*/
public void stateModified(ItemState modified) {
}
/**
* {@inheritDoc}
* <p/>
* Remove item from cache on removal.
*/
public void stateDestroyed(ItemState destroyed) {
// evict removed item from cache
VersioningLock.ReadLock lock = acquireReadLock();
try {
versionItems.remove(destroyed.getId());
} finally {
lock.release();
}
}
/**
* {@inheritDoc}
* <p/>
* Not used.
*/
public void stateDiscarded(ItemState discarded) {
}
//--------------------------------------------------< UpdateEventListener >
/**
* {@inheritDoc}
*/
public void externalUpdate(ChangeLog changes, List<EventState> events,
long timestamp, String userData)
throws RepositoryException {
EventStateCollection esc = getEscFactory().createEventStateCollection(null);
esc.addAll(events);
esc.setTimestamp(timestamp);
esc.setUserData(userData);
sharedStateMgr.externalUpdate(changes, esc);
Collection<InternalVersionItem> items =
new ArrayList<InternalVersionItem>();
synchronized (versionItems) {
for (Map.Entry<ItemId, InternalVersionItem> entry : versionItems
.entrySet()) {
if (changes.has(entry.getKey())) {
items.add(entry.getValue());
}
}
}
itemsUpdated(items);
}
//--------------------------------------------------------< inner classes >
public static final class DynamicESCFactory implements EventStateCollectionFactory {
/**
* the observation manager
*/
private final DelegatingObservationDispatcher obsMgr;
/**
* The event source of the current thread.
*/
private final ThreadLocal<SessionImpl> source =
new ThreadLocal<SessionImpl>();
/**
* Creates a new event state collection factory
* @param obsMgr dispatcher
*/
public DynamicESCFactory(DelegatingObservationDispatcher obsMgr) {
this.obsMgr = obsMgr;
}
/**
* {@inheritDoc}
* <p/>
* This object uses one instance of a <code>LocalItemStateManager</code>
* to update data on behalf of many sessions. In order to maintain the
* association between update operation and session who actually invoked
* the update, an internal event source is used.
*/
public EventStateCollection createEventStateCollection()
throws RepositoryException {
SessionImpl session = source.get();
if (session != null) {
return createEventStateCollection(session);
} else {
throw new RepositoryException("Unknown event source.");
}
}
/**
* {@inheritDoc}
* <p/>
* This object uses one instance of a <code>LocalItemStateManager</code>
* to update data on behalf of many sessions. In order to maintain the
* association between update operation and session who actually invoked
* the update, an internal event source is used.
*/
public EventStateCollection createEventStateCollection(SessionImpl source) {
return obsMgr.createEventStateCollection(source, SYSTEM_PATH);
}
/**
* Executes the given runnable using the given event source.
*
* @param eventSource event source
* @param runnable the runnable to execute
* @return the return value of the executed runnable
* @throws RepositoryException if an error occurs
*/
public Object doSourced(SessionImpl eventSource, SourcedTarget runnable)
throws RepositoryException {
source.set(eventSource);
try {
return runnable.run();
} finally {
source.remove();
}
}
}
private abstract class SourcedTarget {
public abstract Object run() throws RepositoryException;
}
}