/*
* ModeShape (http://www.modeshape.org)
*
* 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 org.modeshape.jcr;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import javax.jcr.AccessDeniedException;
import javax.jcr.Credentials;
import javax.jcr.InvalidItemStateException;
import javax.jcr.InvalidSerializedDataException;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.LoginException;
import javax.jcr.NamespaceException;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.retention.RetentionManager;
import javax.jcr.security.AccessControlManager;
import javax.jcr.version.Version;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionIterator;
import org.infinispan.schematic.SchematicEntry;
import org.modeshape.common.collection.LinkedListMultimap;
import org.modeshape.common.collection.Multimap;
import org.modeshape.common.i18n.I18n;
import org.modeshape.common.logging.Logger;
import org.modeshape.common.text.TextDecoder;
import org.modeshape.common.util.CheckArg;
import org.modeshape.jcr.AbstractJcrNode.Type;
import org.modeshape.jcr.JcrContentHandler.EnclosingSAXException;
import org.modeshape.jcr.JcrNamespaceRegistry.Behavior;
import org.modeshape.jcr.JcrRepository.RunningState;
import org.modeshape.jcr.JcrSharedNodeCache.SharedSet;
import org.modeshape.jcr.NodeTypes.NodeDefinitionSet;
import org.modeshape.jcr.api.Binary;
import org.modeshape.jcr.api.ValueFactory;
import org.modeshape.jcr.api.monitor.DurationMetric;
import org.modeshape.jcr.api.monitor.ValueMetric;
import org.modeshape.jcr.api.sequencer.Sequencer;
import org.modeshape.jcr.api.value.DateTime;
import org.modeshape.jcr.cache.CachedNode;
import org.modeshape.jcr.cache.ChildReference;
import org.modeshape.jcr.cache.ChildReferences;
import org.modeshape.jcr.cache.DocumentAlreadyExistsException;
import org.modeshape.jcr.cache.DocumentNotFoundException;
import org.modeshape.jcr.cache.MutableCachedNode;
import org.modeshape.jcr.cache.NodeCache;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.NodeNotFoundException;
import org.modeshape.jcr.cache.RepositoryCache;
import org.modeshape.jcr.cache.SessionCache;
import org.modeshape.jcr.cache.SessionCache.SaveContext;
import org.modeshape.jcr.cache.SessionCacheWrapper;
import org.modeshape.jcr.cache.SiblingCounter;
import org.modeshape.jcr.cache.WorkspaceNotFoundException;
import org.modeshape.jcr.cache.WrappedException;
import org.modeshape.jcr.cache.document.WorkspaceCache;
import org.modeshape.jcr.query.BufferManager;
import org.modeshape.jcr.security.AdvancedAuthorizationProvider;
import org.modeshape.jcr.security.AuthorizationProvider;
import org.modeshape.jcr.security.SecurityContext;
import org.modeshape.jcr.value.DateTimeFactory;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NameFactory;
import org.modeshape.jcr.value.NamespaceRegistry;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.Path.Segment;
import org.modeshape.jcr.value.PathFactory;
import org.modeshape.jcr.value.PropertyFactory;
import org.modeshape.jcr.value.Reference;
import org.modeshape.jcr.value.ReferenceFactory;
import org.modeshape.jcr.value.ValueFactories;
import org.modeshape.jcr.value.basic.LocalNamespaceRegistry;
import org.modeshape.jcr.value.basic.NodeIdentifierReferenceFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
/**
*
*/
public class JcrSession implements org.modeshape.jcr.api.Session {
private static final String[] NO_ATTRIBUTES_NAMES = new String[] {};
protected final JcrRepository repository;
private final SessionCache cache;
private final JcrRootNode rootNode;
private final ConcurrentMap<NodeKey, AbstractJcrNode> jcrNodes = new ConcurrentHashMap<>();
private final Map<String, Object> sessionAttributes;
private final JcrWorkspace workspace;
private final JcrNamespaceRegistry sessionRegistry;
private final AtomicReference<Map<NodeKey, NodeKey>> baseVersionKeys = new AtomicReference<>();
private final AtomicReference<Map<NodeKey, NodeKey>> originalVersionKeys = new AtomicReference<>();
private final AtomicReference<JcrSharedNodeCache> shareableNodeCache = new AtomicReference<>();
private final AtomicLong aclChangesCount = new AtomicLong(0);
private volatile JcrValueFactory valueFactory;
private volatile boolean isLive = true;
private final long nanosCreated;
private volatile BufferManager bufferMgr;
private final boolean hasCustomAuthorizationProvider;
private ExecutionContext context;
private final AccessControlManagerImpl acm;
private final AdvancedAuthorizationProvider.Context authorizerContext = new AdvancedAuthorizationProvider.Context() {
@Override
public ExecutionContext getExecutionContext() {
return context();
}
@Override
public String getRepositoryName() {
return repository().getName();
}
@Override
public Session getSession() {
return JcrSession.this;
}
@Override
public String getWorkspaceName() {
return workspaceName();
}
};
protected JcrSession( JcrRepository repository,
String workspaceName,
ExecutionContext context,
Map<String, Object> sessionAttributes,
boolean readOnly ) {
this.repository = repository;
// Get the node key of the workspace we're going to use ...
final RepositoryCache repositoryCache = repository.repositoryCache();
WorkspaceCache workspace = repositoryCache.getWorkspaceCache(workspaceName);
NodeKey rootKey = workspace.getRootKey();
// Now create a specific reference factories that know about the root node key ...
TextDecoder decoder = context.getDecoder();
ValueFactories factories = context.getValueFactories();
ReferenceFactory rootKeyAwareStrongRefFactory = NodeIdentifierReferenceFactory.newInstance(rootKey, decoder, factories,
false, false);
ReferenceFactory rootKeyAwareWeakRefFactory = NodeIdentifierReferenceFactory.newInstance(rootKey, decoder, factories,
true, false);
ReferenceFactory rootKeyAwareSimpleRefFactory = NodeIdentifierReferenceFactory.newInstance(rootKey, decoder, factories,
true, true);
context = context.with(rootKeyAwareStrongRefFactory).with(rootKeyAwareWeakRefFactory).with(rootKeyAwareSimpleRefFactory);
// Create an execution context for this session that uses a local namespace registry ...
final NamespaceRegistry globalNamespaceRegistry = context.getNamespaceRegistry(); // thread-safe!
final LocalNamespaceRegistry localRegistry = new LocalNamespaceRegistry(globalNamespaceRegistry); // not-thread-safe!
this.context = context.with(localRegistry);
this.sessionRegistry = new JcrNamespaceRegistry(Behavior.SESSION, localRegistry, globalNamespaceRegistry, this);
this.workspace = new JcrWorkspace(this, workspaceName);
// Create the session cache ...
this.cache = repositoryCache.createSession(this.context, workspaceName, readOnly);
this.rootNode = new JcrRootNode(this, this.cache.getRootKey());
this.jcrNodes.put(this.rootNode.key(), this.rootNode);
this.sessionAttributes = sessionAttributes != null ? sessionAttributes : Collections.<String, Object>emptyMap();
// Pre-cache all of the namespaces to be a snapshot of what's in the global registry at this time.
// This behavior is specified in Section 3.5.2 of the JCR 2.0 specification.
localRegistry.getNamespaces();
// Increment the statistics ...
this.nanosCreated = System.nanoTime();
repository.statistics().increment(ValueMetric.SESSION_COUNT);
acm = new AccessControlManagerImpl(this);
SecurityContext securityContext = context.getSecurityContext();
this.hasCustomAuthorizationProvider = securityContext instanceof AuthorizationProvider
|| securityContext instanceof AdvancedAuthorizationProvider;
}
protected JcrSession( JcrSession original,
boolean readOnly ) {
// Most of the components can be reused from the original session ...
this.repository = original.repository;
this.context = original.context;
this.sessionRegistry = original.sessionRegistry;
this.valueFactory = original.valueFactory;
this.sessionAttributes = original.sessionAttributes;
this.workspace = original.workspace;
this.hasCustomAuthorizationProvider = original.hasCustomAuthorizationProvider;
// Create a new session cache and root node ...
this.cache = repository.repositoryCache().createSession(context, this.workspace.getName(), readOnly);
this.rootNode = new JcrRootNode(this, this.cache.getRootKey());
this.jcrNodes.put(this.rootNode.key(), this.rootNode);
// Increment the statistics ...
this.nanosCreated = System.nanoTime();
repository.statistics().increment(ValueMetric.SESSION_COUNT);
acm = new AccessControlManagerImpl(this);
}
final JcrWorkspace workspace() {
return workspace;
}
final JcrRepository repository() {
return repository;
}
final synchronized BufferManager bufferManager() {
if (bufferMgr == null) {
bufferMgr = new BufferManager(this.context);
}
return bufferMgr;
}
final boolean checkPermissionsWhenIteratingChildren() {
// we cannot "cache" the ACL enabled/disabled state because it's dynamic
return this.hasCustomAuthorizationProvider || repository.repositoryCache().isAccessControlEnabled();
}
/**
* This method is called by {@link #logout()} and by {@link JcrRepository#shutdown()}. It should not be called from anywhere
* else.
*
* @param removeFromActiveSession true if the session should be removed from the active session list
*/
void terminate( boolean removeFromActiveSession ) {
if (!isLive()) {
return;
}
isLive = false;
JcrObservationManager jcrObservationManager = observationManager();
if (jcrObservationManager != null) {
jcrObservationManager.removeAllEventListeners();
}
cleanLocks();
if (removeFromActiveSession) this.repository.runningState().removeSession(this);
this.context.getSecurityContext().logout();
}
private void cleanLocks() {
try {
lockManager().cleanLocks();
} catch (RepositoryException e) {
// This can only happen if the session is not live, which is checked above ...
Logger.getLogger(getClass()).error(e, JcrI18n.unexpectedException, e.getMessage());
}
}
protected SchematicEntry entryForNode( NodeKey nodeKey ) throws RepositoryException {
SchematicEntry entry = repository.documentStore().get(nodeKey.toString());
if (entry == null) {
throw new PathNotFoundException(nodeKey.toString());
}
return entry;
}
final String workspaceName() {
return workspace.getName();
}
final String sessionId() {
return context.getId();
}
public final boolean isReadOnly() {
return cache().isReadOnly();
}
/**
* Method that verifies that this session is still {@link #isLive() live}.
*
* @throws RepositoryException if session has been closed and is no longer usable.
*/
final void checkLive() throws RepositoryException {
if (!isLive()) {
throw new RepositoryException(JcrI18n.sessionIsNotActive.text(sessionId()));
}
}
NamespaceRegistry namespaces() {
return context.getNamespaceRegistry();
}
final org.modeshape.jcr.value.ValueFactory<String> stringFactory() {
return context.getValueFactories().getStringFactory();
}
final NameFactory nameFactory() {
return context.getValueFactories().getNameFactory();
}
final PathFactory pathFactory() {
return context.getValueFactories().getPathFactory();
}
final PropertyFactory propertyFactory() {
return context.getPropertyFactory();
}
final ReferenceFactory referenceFactory() {
return context.getValueFactories().getReferenceFactory();
}
final DateTimeFactory dateFactory() {
return context.getValueFactories().getDateFactory();
}
final ExecutionContext context() {
return context;
}
final JcrValueFactory valueFactory() {
if (valueFactory == null) {
// Never gets unset, and this is idempotent so okay to create without a lock
valueFactory = new JcrValueFactory(this.context);
}
return valueFactory;
}
final SessionCache cache() {
return cache;
}
final SessionCache createSystemCache( boolean readOnly ) {
// This method returns a SessionCache used by various Session-owned components that can automatically
// save system content. This session should be notified when such activities happen.
SessionCache systemCache = repository.createSystemSession(context, readOnly);
return readOnly ? systemCache : new SystemSessionCache(systemCache);
}
final JcrNodeTypeManager nodeTypeManager() {
return this.workspace.nodeTypeManager();
}
final NodeTypes nodeTypes() {
return this.repository().nodeTypeManager().getNodeTypes();
}
final JcrVersionManager versionManager() {
return this.workspace.versionManager();
}
final JcrLockManager lockManager() {
return workspace().lockManager();
}
final JcrObservationManager observationManager() {
return workspace().observationManager();
}
final void signalNamespaceChanges( boolean global ) {
nodeTypeManager().signalNamespaceChanges();
if (global) repository.nodeTypeManager().signalNamespaceChanges();
}
final void setDesiredBaseVersionKey( NodeKey nodeKey,
NodeKey baseVersionKey ) {
baseVersionKeys.get().put(nodeKey, baseVersionKey);
}
final void setOriginalVersionKey( NodeKey nodeKey,
NodeKey originalVersionKey ) {
originalVersionKeys.get().put(nodeKey, originalVersionKey);
}
final JcrSession spawnSession( boolean readOnly ) {
return new JcrSession(this, readOnly);
}
final JcrSession spawnSession( String workspaceName,
boolean readOnly ) {
return new JcrSession(repository(), workspaceName, context(), sessionAttributes, readOnly);
}
final SessionCache spawnSessionCache( boolean readOnly ) {
SessionCache cache = repository().repositoryCache().createSession(context(), workspaceName(), readOnly);
return readOnly ? cache : new SystemSessionCache(cache);
}
final void addContextData( String key,
String value ) {
this.context = context.with(key, value);
this.cache.addContextData(key, value);
}
final JcrSharedNodeCache shareableNodeCache() {
JcrSharedNodeCache result = this.shareableNodeCache.get();
if (result == null) {
this.shareableNodeCache.compareAndSet(null, new JcrSharedNodeCache(this));
result = this.shareableNodeCache.get();
}
return result;
}
protected final String readableLocation( CachedNode node ) {
try {
return stringFactory().create(node.getPath(cache));
} catch (Throwable t) {
return node.getKey().toString();
}
}
protected final long aclChangesCount() {
return aclChangesCount.longValue();
}
protected final long aclAdded( long count ) {
return aclChangesCount.addAndGet(count);
}
protected final long aclRemoved( long count ) {
return aclChangesCount.addAndGet(-count);
}
protected final String readable( Path path ) {
return stringFactory().create(path);
}
protected void releaseCachedNode( AbstractJcrNode node ) {
jcrNodes.remove(node.key(), node);
}
/**
* Obtain the {@link Node JCR Node} object for the node with the supplied key.
*
* @param nodeKey the node's key
* @param expectedType the expected implementation type for the node, or null if it is not known
* @return the JCR node; never null
* @throws ItemNotFoundException if there is no node with the supplied key
*/
AbstractJcrNode node( NodeKey nodeKey,
AbstractJcrNode.Type expectedType ) throws ItemNotFoundException {
return node(nodeKey, expectedType, null);
}
/**
* Obtain the {@link Node JCR Node} object for the node with the supplied key.
*
* @param nodeKey the node's key
* @param expectedType the expected implementation type for the node, or null if it is not known
* @param parentKey the node key for the parent node, or null if the parent is not known
* @return the JCR node; never null
* @throws ItemNotFoundException if there is no node with the supplied key
*/
AbstractJcrNode node( NodeKey nodeKey,
AbstractJcrNode.Type expectedType,
NodeKey parentKey ) throws ItemNotFoundException {
CachedNode cachedNode = cache.getNode(nodeKey);
if (cachedNode == null) {
// The node must not exist or must have been deleted ...
throw new ItemNotFoundException(nodeKey.toString());
}
AbstractJcrNode node = jcrNodes.get(nodeKey);
if (node == null) {
node = node(cachedNode, expectedType, parentKey);
} else if (parentKey != null) {
// There was an existing node found, but it might not be the node we're looking for:
// In some cases (e.g., shared nodes), the node key might be used in multiple parents,
// and we need to find the right one ...
node = node(cachedNode, expectedType, parentKey);
}
return node;
}
/**
* Obtain the {@link Node JCR Node} object for the node with the supplied key.
*
* @param cachedNode the cached node; may not be null
* @param expectedType the expected implementation type for the node, or null if it is not known
* @return the JCR node; never null
* @see #node(CachedNode, Type, NodeKey)
*/
AbstractJcrNode node( CachedNode cachedNode,
AbstractJcrNode.Type expectedType ) {
return node(cachedNode, expectedType, null);
}
/**
* Obtain the {@link Node JCR Node} object for the node with the supplied key.
*
* @param cachedNode the cached node; may not be null
* @param expectedType the expected implementation type for the node, or null if it is not known
* @param parentKey the node key for the parent node, or null if the parent is not known
* @return the JCR node; never null
*/
AbstractJcrNode node( CachedNode cachedNode,
AbstractJcrNode.Type expectedType,
NodeKey parentKey ) {
assert cachedNode != null;
NodeKey nodeKey = cachedNode.getKey();
AbstractJcrNode node = jcrNodes.get(nodeKey);
boolean mightBeShared = true;
if (node == null) {
if (expectedType == null) {
Name primaryType = cachedNode.getPrimaryType(cache);
expectedType = Type.typeForPrimaryType(primaryType);
if (expectedType == null) {
// If this node from the system workspace, then the default is Type.SYSTEM rather than Type.NODE ...
if (repository().systemWorkspaceKey().equals(nodeKey.getWorkspaceKey())) {
expectedType = Type.SYSTEM;
} else {
expectedType = Type.NODE;
}
assert expectedType != null;
}
}
// Check if the new node is in the system area ...
if (expectedType == Type.SYSTEM || expectedType == Type.VERSION || expectedType == Type.VERSION_HISTORY) {
Path path = cachedNode.getPath(cache);
assert path.size() > 0;
if (!path.getSegment(0).getName().equals(JcrLexicon.SYSTEM)) {
// It is NOT below "/jcr:system"; someone must be using a node type normally used in the system area ...
expectedType = Type.NODE;
}
}
switch (expectedType) {
case NODE:
node = new JcrNode(this, nodeKey);
break;
case VERSION:
node = new JcrVersionNode(this, nodeKey);
mightBeShared = false;
break;
case VERSION_HISTORY:
node = new JcrVersionHistoryNode(this, nodeKey);
mightBeShared = false;
break;
case SYSTEM:
node = new JcrSystemNode(this, nodeKey);
mightBeShared = false;
break;
case ROOT:
try {
return getRootNode();
} catch (RepositoryException e) {
assert false : "Should never happen: " + e.getMessage();
}
}
assert node != null;
AbstractJcrNode newNode = jcrNodes.putIfAbsent(nodeKey, node);
if (newNode != null) {
// Another thread snuck in and created the node object ...
node = newNode;
}
}
if (mightBeShared && parentKey != null && cachedNode.getMixinTypes(cache).contains(JcrMixLexicon.SHAREABLE)) {
// This is a shareable node, so we have to get the proper Node instance for the given parent ...
node = node.sharedSet().getSharedNode(cachedNode, parentKey);
}
return node;
}
final CachedNode cachedNode( Path absolutePath,
boolean checkReadPermission ) throws PathNotFoundException, RepositoryException {
return checkReadPermission ? cachedNode(cache, getRootNode().node(), absolutePath, ModeShapePermissions.READ) : cachedNode(cache,
getRootNode().node(),
absolutePath);
}
final CachedNode cachedNode( SessionCache cache,
CachedNode node,
Path path,
String... actions ) throws PathNotFoundException, AccessDeniedException, RepositoryException {
// We treat the path as a relative path, but the algorithm actually works for absolute, too. So don't enforce.
for (Segment segment : path) {
if (segment.isSelfReference()) continue;
if (segment.isParentReference()) {
node = cache.getNode(node.getParentKey(cache));
} else {
ChildReference ref = node.getChildReferences(cache).getChild(segment);
if (ref == null) {
throw new PathNotFoundException(JcrI18n.nodeNotFound.text(stringFactory().create(path), workspaceName()));
}
CachedNode child = cache.getNode(ref);
assert child != null : "Found a child reference in " + node.getPath(cache) + " to a non-existant child "
+ segment;
node = child;
}
}
// Find the absolute path, based upon the parent ...
Path absPath = path.isAbsolute() ? path : null;
if (absPath == null) {
try {
// We need to look up the absolute path ..
if (actions.length > 0) {
checkPermission(node, cache, actions);
}
} catch (NodeNotFoundException e) {
throw new PathNotFoundException(JcrI18n.nodeNotFound.text(stringFactory().create(path), workspaceName()));
}
}
return node;
}
final MutableCachedNode mutableNode( SessionCache cache,
CachedNode node,
Path path,
String... actions ) throws PathNotFoundException, RepositoryException {
return cache.mutable(cachedNode(cache, node, path, actions).getKey());
}
final AbstractJcrNode node( CachedNode node,
Path path ) throws PathNotFoundException, AccessDeniedException, RepositoryException {
CachedNode child = cachedNode(cache, node, path, ModeShapePermissions.READ);
AbstractJcrNode result = node(child, (Type)null, null);
if (result.isShareable()) {
// Find the shared node with the desired path ...
AbstractJcrNode atOrBelow = result.sharedSet().getSharedNodeAtOrBelow(path);
if (atOrBelow != null) result = atOrBelow;
}
return result;
}
final AbstractJcrNode node( Path absolutePath ) throws PathNotFoundException, AccessDeniedException, RepositoryException {
assert absolutePath.isAbsolute();
if (absolutePath.isRoot()) return getRootNode();
if (absolutePath.isIdentifier()) {
// Look up the node by identifier ...
String identifierString = stringFactory().create(absolutePath).replaceAll("\\[", "").replaceAll("\\]", "");
return getNodeByIdentifier(identifierString);
}
CachedNode node = getRootNode().node();
return node(node, absolutePath);
}
final AbstractJcrItem findItem( NodeKey nodeKey,
Path relativePath ) throws RepositoryException {
return findItem(node(nodeKey, null, null), relativePath);
}
final AbstractJcrItem findItem( AbstractJcrNode node,
Path relativePath ) throws RepositoryException {
assert !relativePath.isAbsolute();
if (relativePath.size() == 1) {
Segment last = relativePath.getLastSegment();
if (last.isSelfReference()) return node;
if (last.isParentReference()) return node.getParent();
}
// Find the path to the referenced node ...
Path nodePath = node.path();
Path absolutePath = nodePath.resolve(relativePath);
if (absolutePath.isAtOrBelow(nodePath)) {
// Find the item starting at 'node' ...
}
return getItem(absolutePath);
}
/**
* Parse the supplied string into an absolute {@link Path} representation.
*
* @param absPath the string containing an absolute path
* @return the absolute path object; never null
* @throws RepositoryException if the supplied string is not a valid absolute path
*/
Path absolutePathFor( String absPath ) throws RepositoryException {
Path path = null;
try {
path = pathFactory().create(absPath);
} catch (org.modeshape.jcr.value.ValueFormatException e) {
throw new RepositoryException(e.getMessage());
}
if (!path.isAbsolute()) {
throw new RepositoryException(JcrI18n.invalidAbsolutePath.text(absPath));
}
return path;
}
@Override
public JcrRepository getRepository() {
return repository;
}
@Override
public String getUserID() {
return context.getSecurityContext().getUserName();
}
public boolean isAnonymous() {
return context.getSecurityContext().isAnonymous();
}
@Override
public String[] getAttributeNames() {
Set<String> names = sessionAttributes.keySet();
if (names.isEmpty()) return NO_ATTRIBUTES_NAMES;
return names.toArray(new String[names.size()]);
}
@Override
public Object getAttribute( String name ) {
return this.sessionAttributes.get(name);
}
@Override
public JcrWorkspace getWorkspace() {
return workspace;
}
@Override
public JcrRootNode getRootNode() throws RepositoryException {
checkLive();
return rootNode;
}
@Override
public Session impersonate( Credentials credentials ) throws LoginException, RepositoryException {
checkLive();
return repository.login(credentials, workspaceName());
}
@Deprecated
@Override
public AbstractJcrNode getNodeByUUID( String uuid ) throws ItemNotFoundException, RepositoryException {
return getNodeByIdentifier(uuid);
}
@Override
public AbstractJcrNode getNodeByIdentifier( String id ) throws ItemNotFoundException, RepositoryException {
checkLive();
if (NodeKey.isValidFormat(id)) {
// Try the identifier as a node key ...
try {
NodeKey key = new NodeKey(id);
AbstractJcrNode node = node(key, null);
checkPermission(pathSupplierFor(node), ModeShapePermissions.READ);
return node;
} catch (ItemNotFoundException e) {
// continue ...
}
}
// First, we're given a partial key, so look first in this workspace's content ...
NodeKey key = null;
ItemNotFoundException first = null;
try {
// Try as node key identifier ...
key = this.rootNode.key.withId(id);
AbstractJcrNode node = node(key, null);
checkPermission(pathSupplierFor(node), ModeShapePermissions.READ);
return node;
} catch (ItemNotFoundException e) {
// Not found, so capture the exception (which we might use later) and continue ...
first = e;
}
// Next look for it using the same key except with the system workspace part ...
try {
String systemWorkspaceKey = this.repository().systemWorkspaceKey();
key = key.withWorkspaceKey(systemWorkspaceKey);
AbstractJcrNode systemNode = node(key, null);
if (systemNode instanceof JcrVersionHistoryNode) {
// because the version history node has the same key as the original node, we don't want to expose it to clients
// this means that if we got this far, the original hasn't been found, so neither should the version history
throw first;
}
checkPermission(pathSupplierFor(systemNode), ModeShapePermissions.READ);
return systemNode;
} catch (ItemNotFoundException e) {
// Not found, so throw the original exception ...
throw first;
}
}
/**
* A variant of the standard {@link #getNodeByIdentifier(String)} method that does <i>not</i> find nodes within the system
* area. This is often needed by the {@link JcrVersionManager} functionality.
*
* @param id the string identifier
* @return the node; never null
* @throws ItemNotFoundException if a node cannot be found in the non-system content of the repository
* @throws RepositoryException if there is another problem
* @see #getNodeByIdentifier(String)
*/
public AbstractJcrNode getNonSystemNodeByIdentifier( String id ) throws ItemNotFoundException, RepositoryException {
checkLive();
if (NodeKey.isValidFormat(id)) {
// Try the identifier as a node key ...
try {
NodeKey key = new NodeKey(id);
return node(key, null);
} catch (ItemNotFoundException e) {
// continue ...
}
}
// Try as node key identifier ...
NodeKey key = this.rootNode.key.withId(id);
return node(key, null);
}
@Override
public AbstractJcrNode getNode( String absPath ) throws PathNotFoundException, RepositoryException {
return getNode(absPath, false);
}
protected AbstractJcrNode getNode( String absPath,
boolean accessControlScope ) throws PathNotFoundException, RepositoryException {
checkLive();
CheckArg.isNotEmpty(absPath, "absolutePath");
Path path = absolutePathFor(absPath);
if (!accessControlScope) {
checkPermission(path, ModeShapePermissions.READ);
}
// Return root node if path is "/" ...
if (path.isRoot()) {
return getRootNode();
}
return node(path);
}
@Override
public AbstractJcrItem getItem( String absPath ) throws PathNotFoundException, RepositoryException {
checkLive();
CheckArg.isNotEmpty(absPath, "absPath");
Path path = absolutePathFor(absPath);
return getItem(path);
}
AbstractJcrItem getItem( Path path ) throws PathNotFoundException, RepositoryException {
assert path.isAbsolute() : "Path supplied to Session.getItem(Path) must be absolute";
// Return root node if path is "/" ...
if (path.isRoot()) {
return getRootNode();
}
// Since we don't know whether path refers to a node or a property, look to see if we can tell it's a node ...
if (path.isIdentifier() || path.getLastSegment().hasIndex()) {
return node(path);
}
// We can't tell from the name, so ask for an item.
// JSR-170 doesn't allow children and proeprties to have the same name, but this is relaxed in JSR-283.
// But JSR-283 Section 3.3.4 states "The method Session.getItem will return the item at the specified path
// if there is only one such item, if there is both a node and a property at the specified path, getItem
// will return the node." Therefore, look for a child first ...
try {
return node(path);
} catch (PathNotFoundException e) {
// Must not be any child by that name, so now look for a property on the parent node ...
AbstractJcrNode parent = node(path.getParent());
AbstractJcrProperty prop = parent.getProperty(path.getLastSegment().getName());
if (prop != null) return prop;
// Failed to find any item ...
String pathStr = stringFactory().create(path);
throw new PathNotFoundException(JcrI18n.itemNotFoundAtPath.text(pathStr, workspaceName()));
}
}
@Override
public Property getProperty( String absPath ) throws PathNotFoundException, RepositoryException {
checkLive();
CheckArg.isNotEmpty(absPath, "absPath");
// Return root node if path is "/"
Path path = absolutePathFor(absPath);
if (path.isRoot()) {
throw new PathNotFoundException(JcrI18n.rootNodeIsNotProperty.text());
}
if (path.isIdentifier()) {
throw new PathNotFoundException(JcrI18n.identifierPathNeverReferencesProperty.text());
}
Segment lastSegment = path.getLastSegment();
if (lastSegment.hasIndex()) {
throw new RepositoryException(JcrI18n.pathCannotHaveSameNameSiblingIndex.text(absPath));
}
// This will throw a PNFE if the parent path does not exist
AbstractJcrNode parentNode = node(path.getParent());
AbstractJcrProperty property = parentNode.getProperty(lastSegment.getName());
if (property == null) {
throw new PathNotFoundException(GraphI18n.pathNotFoundExceptionLowestExistingLocationFound.text(absPath,
parentNode.getPath()));
}
return property;
}
@Override
public boolean itemExists( String absPath ) throws RepositoryException {
try {
return getItem(absPath) != null;
} catch (PathNotFoundException error) {
return false;
}
}
@Override
public void removeItem( String absPath )
throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException {
getItem(absPath).remove();
}
@Override
public boolean nodeExists( String absPath ) throws RepositoryException {
// This is an optimized version of 'getNode(absPath)' ...
checkLive();
CheckArg.isNotEmpty(absPath, "absPath");
Path absolutePath = absolutePathFor(absPath);
try {
return node(absolutePath) != null;
} catch (PathNotFoundException e) {
return false;
}
}
/**
* Utility method to determine if the node with the specified key still exists within the transient & persisted state.
*
* @param key the key of the node; may not be null
* @return true if the node exists, or false if it does not
*/
protected boolean nodeExists( NodeKey key ) {
return cache.getNode(key) != null;
}
@Override
public boolean propertyExists( String absPath ) throws RepositoryException {
checkLive();
CheckArg.isNotEmpty(absPath, "absPath");
Path path = absolutePathFor(absPath);
// Check the kind of path ...
if (path.isRoot() || path.isIdentifier()) {
// These are not properties ...
return false;
}
// There is at least one segment ...
Segment lastSegment = path.getLastSegment();
if (lastSegment.hasIndex()) {
throw new RepositoryException(JcrI18n.pathCannotHaveSameNameSiblingIndex.text(absPath));
}
try {
// This will throw a PNFE if the parent path does not exist
CachedNode parentNode = cachedNode(path.getParent(), true);
return parentNode != null && parentNode.hasProperty(lastSegment.getName(), cache());
} catch (PathNotFoundException e) {
return false;
}
}
@Override
public void move( String srcAbsPath,
String destAbsPath )
throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException,
RepositoryException {
checkLive();
// Find the source path and destination path and check for permissions
Path srcPath = absolutePathFor(srcAbsPath);
checkPermission(srcPath, ModeShapePermissions.REMOVE);
Path destPath = absolutePathFor(destAbsPath);
checkPermission(destPath.getParent(), ModeShapePermissions.ADD_NODE);
if (srcPath.isRoot()) {
throw new RepositoryException(JcrI18n.unableToMoveRootNode.text(workspaceName()));
}
if (destPath.isRoot()) {
throw new RepositoryException(JcrI18n.rootNodeCannotBeDestinationOfMovedNode.text(workspaceName()));
}
if (!destPath.isIdentifier() && destAbsPath.endsWith("]")) {
// Doing a literal test here because the path factory will canonicalize "/node[1]" to "/node"
throw new RepositoryException(JcrI18n.pathCannotHaveSameNameSiblingIndex.text(destAbsPath));
}
if (srcPath.isAncestorOf(destPath)) {
String msg = JcrI18n.unableToMoveNodeToBeChildOfDecendent.text(srcAbsPath, destAbsPath, workspaceName());
throw new RepositoryException(msg);
}
if (srcPath.equals(destPath)) {
// Nothing to do ...
return;
}
// Get the node at the source path and the parent node of the destination path ...
AbstractJcrNode srcNode = node(srcPath);
AbstractJcrNode destParentNode = node(destPath.getParent());
SessionCache sessionCache = cache();
// Check whether these nodes are locked ...
if (srcNode.isLocked() && !srcNode.getLock().isLockOwningSession()) {
javax.jcr.lock.Lock sourceLock = srcNode.getLock();
if (sourceLock != null && sourceLock.getLockToken() == null) {
throw new LockException(JcrI18n.lockTokenNotHeld.text(srcAbsPath));
}
}
if (destParentNode.isLocked() && !destParentNode.getLock().isLockOwningSession()) {
javax.jcr.lock.Lock newParentLock = destParentNode.getLock();
if (newParentLock != null && newParentLock.getLockToken() == null) {
throw new LockException(JcrI18n.lockTokenNotHeld.text(destAbsPath));
}
}
// Check whether the nodes that will be modified are checked out ...
AbstractJcrNode srcParent = srcNode.getParent();
if (!srcParent.isCheckedOut()) {
throw new VersionException(JcrI18n.nodeIsCheckedIn.text(srcNode.getPath()));
}
if (!destParentNode.isCheckedOut()) {
throw new VersionException(JcrI18n.nodeIsCheckedIn.text(destParentNode.getPath()));
}
// Check whether external nodes are involved
validateMoveForExternalNodes(srcPath, destPath);
// check whether the parent definition allows children which match the source
final Name newChildName = destPath.getLastSegment().getName();
destParentNode.validateChildNodeDefinition(newChildName, srcNode.getPrimaryTypeName(), true);
// We already checked whether the supplied destination path is below the supplied source path, but this isn't
// sufficient if any of the ancestors are shared nodes. Therefore, check whether the destination node
// is actually underneath the source node by walking up the destination path to see if there are any
// shared nodes (including the shareable node) below the source path ...
AbstractJcrNode destAncestor = destParentNode;
while (!destAncestor.isRoot()) {
if (destAncestor.isShareable()) {
SharedSet sharedSet = destAncestor.sharedSet();
AbstractJcrNode sharedNodeThatCreatesCircularity = sharedSet.getSharedNodeAtOrBelow(srcPath);
if (sharedNodeThatCreatesCircularity != null) {
Path badPath = sharedNodeThatCreatesCircularity.path();
throw new RepositoryException(JcrI18n.unableToMoveNodeDueToCycle.text(srcAbsPath, destAbsPath,
readable(badPath)));
}
}
destAncestor = destAncestor.getParent();
}
try {
MutableCachedNode mutableSrcParent = srcParent.mutable();
MutableCachedNode mutableDestParent = destParentNode.mutable();
if (mutableSrcParent.equals(mutableDestParent)) {
// It's just a rename ...
mutableSrcParent.renameChild(sessionCache, srcNode.key(), destPath.getLastSegment().getName());
} else {
// It is a move from one parent to another ...
mutableSrcParent.moveChild(sessionCache, srcNode.key(), mutableDestParent, newChildName);
}
} catch (NodeNotFoundException e) {
// Not expected ...
String msg = JcrI18n.nodeNotFound.text(stringFactory().create(srcPath.getParent()), workspaceName());
throw new PathNotFoundException(msg);
}
}
private void validateMoveForExternalNodes( Path srcPath,
Path destPath ) throws RepositoryException {
AbstractJcrNode srcNode = node(srcPath);
String rootSourceKey = getRootNode().key().getSourceKey();
Set<NodeKey> sourceNodeKeys = cache().getNodeKeysAtAndBelow(srcNode.key());
boolean sourceContainsExternalNodes = false;
String externalSourceKey = null;
for (NodeKey sourceNodeKey : sourceNodeKeys) {
if (!sourceNodeKey.getSourceKey().equalsIgnoreCase(rootSourceKey)) {
externalSourceKey = sourceNodeKey.getSourceKey();
sourceContainsExternalNodes = true;
break;
}
}
AbstractJcrNode destNode = null;
try {
destNode = node(destPath);
} catch (PathNotFoundException e) {
// the destPath does not point to an existing node, so we'll use the parent
destNode = node(destPath.getParent());
}
String externalTargetKey = null;
boolean targetIsExternal = false;
if (!destNode.key().getSourceKey().equalsIgnoreCase(rootSourceKey)) {
targetIsExternal = true;
externalTargetKey = destNode.key().getSourceKey();
}
Connectors connectors = repository().runningState().connectors();
if (sourceContainsExternalNodes && !targetIsExternal) {
String sourceName = connectors.getSourceNameAtKey(externalSourceKey);
throw new RepositoryException(JcrI18n.unableToMoveSourceContainExternalNodes.text(srcPath, sourceName));
} else if (!sourceContainsExternalNodes && targetIsExternal) {
String sourceName = connectors.getSourceNameAtKey(externalTargetKey);
throw new RepositoryException(JcrI18n.unableToMoveTargetContainExternalNodes.text(srcPath, sourceName));
} else if (targetIsExternal) {
// both source and target are external nodes, but belonging to different sources
assert externalTargetKey != null;
if (!externalTargetKey.equalsIgnoreCase(srcNode.key().getSourceKey())) {
String sourceNodeSourceName = connectors.getSourceNameAtKey(srcNode.key().getSourceKey());
String targetNodeSourceName = connectors.getSourceNameAtKey(externalTargetKey);
throw new RepositoryException(JcrI18n.unableToMoveSourceTargetMismatch.text(sourceNodeSourceName,
targetNodeSourceName));
}
// both source and target belong to the same source, but one of them is a projection root
if (connectors.hasExternalProjection(srcPath.getLastSegment().getString(), srcNode.key().toString())) {
throw new RepositoryException(JcrI18n.unableToMoveProjection.text(srcPath));
}
if (connectors.hasExternalProjection(destPath.getLastSegment().getString(), destNode.key().toString())) {
throw new RepositoryException(JcrI18n.unableToMoveProjection.text(destPath));
}
}
}
@Override
public void save()
throws AccessDeniedException, ItemExistsException, ReferentialIntegrityException, ConstraintViolationException,
InvalidItemStateException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException {
checkLive();
// Perform the save, using 'JcrPreSave' operations ...
SessionCache systemCache = createSystemCache(false);
SystemContent systemContent = new SystemContent(systemCache);
Map<NodeKey, NodeKey> baseVersionKeys = this.baseVersionKeys.get();
Map<NodeKey, NodeKey> originalVersionKeys = this.originalVersionKeys.get();
try {
cache().save(systemContent.cache(),
new JcrPreSave(systemContent, baseVersionKeys, originalVersionKeys, aclChangesCount()));
this.baseVersionKeys.set(null);
this.originalVersionKeys.set(null);
this.aclChangesCount.set(0);
} catch (WrappedException e) {
Throwable cause = e.getCause();
throw (cause instanceof RepositoryException) ? (RepositoryException)cause : new RepositoryException(e.getCause());
} catch (DocumentNotFoundException e) {
throw new InvalidItemStateException(JcrI18n.nodeModifiedBySessionWasRemovedByAnotherSession.text(e.getKey()), e);
} catch (DocumentAlreadyExistsException e) {
// Try to figure out which node in this transient state was the problem ...
NodeKey key = new NodeKey(e.getKey());
AbstractJcrNode problemNode = node(key, null);
String path = problemNode.getPath();
throw new InvalidItemStateException(JcrI18n.nodeCreatedBySessionUsedExistingKey.text(path, key), e);
} catch (org.modeshape.jcr.cache.ReferentialIntegrityException e) {
throw new ReferentialIntegrityException(e);
} catch (Throwable t) {
throw new RepositoryException(t);
}
try {
// Record the save operation ...
repository().statistics().increment(ValueMetric.SESSION_SAVES);
} catch (IllegalStateException e) {
// The repository has been shutdown ...
}
}
/**
* Save a subset of the changes made within this session.
*
* @param node the node at or below which the changes are to be saved; may not be null
* @throws RepositoryException if there is a problem saving the changes
* @see AbstractJcrNode#save()
*/
void save( AbstractJcrNode node ) throws RepositoryException {
// first check the node is valid from a cache perspective
Set<NodeKey> keysToBeSaved = null;
try {
if (node.isNew()) {
// expected by TCK
throw new RepositoryException(JcrI18n.unableToSaveNodeThatWasCreatedSincePreviousSave.text(node.getPath(),
workspaceName()));
}
AtomicReference<Set<NodeKey>> refToKeys = new AtomicReference<Set<NodeKey>>();
if (node.containsChangesWithExternalDependencies(refToKeys)) {
// expected by TCK
I18n msg = JcrI18n.unableToSaveBranchBecauseChangesDependOnChangesToNodesOutsideOfBranch;
throw new ConstraintViolationException(msg.text(node.path(), workspaceName()));
}
keysToBeSaved = refToKeys.get();
} catch (ItemNotFoundException e) {
throw new InvalidItemStateException(e);
} catch (NodeNotFoundException e) {
throw new InvalidItemStateException(e);
}
assert keysToBeSaved != null;
SessionCache sessionCache = cache();
if (sessionCache.getChangedNodeKeys().size() == keysToBeSaved.size()) {
// The node is above all the other changes, so go ahead and save the whole session ...
save();
return;
}
// Perform the save, using 'JcrPreSave' operations ...
SessionCache systemCache = createSystemCache(false);
SystemContent systemContent = new SystemContent(systemCache);
Map<NodeKey, NodeKey> baseVersionKeys = this.baseVersionKeys.get();
Map<NodeKey, NodeKey> originalVersionKeys = this.originalVersionKeys.get();
try {
sessionCache.save(keysToBeSaved, systemContent.cache(), new JcrPreSave(systemContent, baseVersionKeys,
originalVersionKeys, aclChangesCount()));
} catch (WrappedException e) {
Throwable cause = e.getCause();
throw (cause instanceof RepositoryException) ? (RepositoryException)cause : new RepositoryException(e.getCause());
} catch (DocumentNotFoundException e) {
throw new InvalidItemStateException(JcrI18n.nodeModifiedBySessionWasRemovedByAnotherSession.text(e.getKey()), e);
} catch (DocumentAlreadyExistsException e) {
// Try to figure out which node in this transient state was the problem ...
NodeKey key = new NodeKey(e.getKey());
AbstractJcrNode problemNode = node(key, null);
String path = problemNode.getPath();
throw new InvalidItemStateException(JcrI18n.nodeCreatedBySessionUsedExistingKey.text(path, key), e);
} catch (org.modeshape.jcr.cache.ReferentialIntegrityException e) {
throw new ReferentialIntegrityException(e);
} catch (Throwable t) {
throw new RepositoryException(t);
}
try {
// Record the save operation ...
repository().statistics().increment(ValueMetric.SESSION_SAVES);
} catch (IllegalStateException e) {
// The repository has been shutdown ...
}
}
@Override
public void refresh( boolean keepChanges ) throws RepositoryException {
checkLive();
if (!keepChanges) {
cache.clear();
aclChangesCount.set(0);
}
// Otherwise there is nothing to do, as all persistent changes are always immediately visible to all sessions
// using that same workspace
}
@Override
public boolean hasPendingChanges() throws RepositoryException {
checkLive();
return cache().hasChanges();
}
@Override
public JcrValueFactory getValueFactory() throws RepositoryException {
checkLive();
return valueFactory();
}
private static interface PathSupplier {
/**
* Get the absolute path
*
* @return the absolute path
* @throws ItemNotFoundException if the node was deleted
*/
Path getAbsolutePath() throws ItemNotFoundException;
}
private PathSupplier pathSupplierFor( final Path path ) {
return new PathSupplier() {
@Override
public Path getAbsolutePath() {
return path;
}
};
}
private PathSupplier pathSupplierFor( final CachedNode node,
final NodeCache nodeCache ) {
return new PathSupplier() {
@Override
public Path getAbsolutePath() {
return node.getPath(nodeCache);
}
};
}
private PathSupplier pathSupplierFor( final AbstractJcrItem item ) {
return new PathSupplier() {
@Override
public Path getAbsolutePath() throws ItemNotFoundException {
try {
return item.path();
} catch (InvalidItemStateException err) {
// This happens when the session removes the node, but we know that couldn't have happened since we have the
// node instance, so ignore it ...
} catch (ItemNotFoundException err) {
throw err;
} catch (RepositoryException e) {
return null;
}
assert false;
return null;
}
};
}
/**
* Determine if the current user does not have permission for all of the named actions in the named workspace in the given
* context, otherwise returns silently.
*
* @param workspaceName the name of the workspace in which the path exists
* @param pathSupplier the supplier for the path on which the actions are occurring; may be null if the permission is on the
* whole workspace
* @param actions the list of {@link ModeShapePermissions actions} to check
* @return true if the subject has privilege to perform all of the named actions on the content at the supplied path in the
* given workspace within the repository, or false otherwise
*/
private boolean hasPermission( String workspaceName,
PathSupplier pathSupplier,
String... actions ) {
SecurityContext sec = context.getSecurityContext();
final boolean checkAcl = repository.repositoryCache().isAccessControlEnabled();
boolean hasPermission = true;
final String repositoryName = this.repository.repositoryName();
try {
if (sec instanceof AuthorizationProvider) {
// Delegate to the security context ...
AuthorizationProvider authorizer = (AuthorizationProvider)sec;
Path path = pathSupplier != null ? pathSupplier.getAbsolutePath() : null;
if (path != null) {
assert path.isAbsolute() : "The path (if provided) must be absolute";
hasPermission = authorizer.hasPermission(context, repositoryName, repositoryName, workspaceName, path,
actions);
if (checkAcl && hasPermission) {
hasPermission = acm.hasPermission(path, actions);
}
return hasPermission;
}
}
if (sec instanceof AdvancedAuthorizationProvider) {
// Delegate to the security context ...
AdvancedAuthorizationProvider authorizer = (AdvancedAuthorizationProvider)sec;
Path path = pathSupplier != null ? pathSupplier.getAbsolutePath() : null;
if (path != null) {
assert path.isAbsolute() : "The path (if provided) must be absolute";
hasPermission = authorizer.hasPermission(authorizerContext, path, actions);
if (checkAcl && hasPermission) {
hasPermission = acm.hasPermission(path, actions);
}
return hasPermission;
}
}
// It is a role-based security context, so apply role-based authorization ...
for (String action : actions) {
if (ModeShapePermissions.READ.equals(action)) {
hasPermission &= hasRole(sec, ModeShapeRoles.READONLY, repositoryName, workspaceName)
|| hasRole(sec, ModeShapeRoles.READWRITE, repositoryName, workspaceName)
|| hasRole(sec, ModeShapeRoles.ADMIN, repositoryName, workspaceName);
} else if (ModeShapePermissions.REGISTER_NAMESPACE.equals(action)
|| ModeShapePermissions.REGISTER_TYPE.equals(action) || ModeShapePermissions.UNLOCK_ANY.equals(action)
|| ModeShapePermissions.CREATE_WORKSPACE.equals(action)
|| ModeShapePermissions.DELETE_WORKSPACE.equals(action) || ModeShapePermissions.MONITOR.equals(action)
|| ModeShapePermissions.DELETE_WORKSPACE.equals(action)
|| ModeShapePermissions.INDEX_WORKSPACE.equals(action)) {
hasPermission &= hasRole(sec, ModeShapeRoles.ADMIN, repositoryName, workspaceName);
} else {
hasPermission &= hasRole(sec, ModeShapeRoles.ADMIN, repositoryName, workspaceName)
|| hasRole(sec, ModeShapeRoles.READWRITE, repositoryName, workspaceName);
}
}
if (checkAcl && hasPermission) {
Path path = pathSupplier != null ? pathSupplier.getAbsolutePath() : null;
if (path != null) {
assert path.isAbsolute() : "The path (if provided) must be absolute";
hasPermission = acm.hasPermission(path, actions);
}
}
return hasPermission;
} catch (ItemNotFoundException err) {
// The node was removed from this session
return false;
}
}
private boolean hasPermissionOnExternalPath( PathSupplier pathSupplier,
String... actions ) throws RepositoryException {
Connectors connectors = this.repository().runningState().connectors();
if (!connectors.hasConnectors() || !connectors.hasReadonlyConnectors()) {
// federation is not enabled or there are no readonly connectors
return true;
}
Path path = pathSupplier.getAbsolutePath();
if (path == null) return false;
if (connectors.isReadonlyPath(path, this)) {
// this is a readonly external path, so we need to see what the actual actions are
if (actions.length > ModeShapePermissions.READONLY_EXTERNAL_PATH_PERMISSIONS.size()) {
return false;
}
List<String> actionsList = new ArrayList<String>(Arrays.asList(actions));
for (Iterator<String> actionsIterator = actionsList.iterator(); actionsIterator.hasNext();) {
String action = actionsIterator.next();
if (!ModeShapePermissions.READONLY_EXTERNAL_PATH_PERMISSIONS.contains(action)) {
return false;
}
actionsIterator.remove();
}
return actionsList.isEmpty();
}
return true;
}
/**
* Returns whether the authenticated user has the given role.
*
* @param context the security context
* @param roleName the name of the role to check
* @param repositoryName the name of the repository
* @param workspaceName the workspace under which the user must have the role. This may be different from the current
* workspace.
* @return true if the user has the role and is logged in; false otherwise
*/
static boolean hasRole( SecurityContext context,
String roleName,
String repositoryName,
String workspaceName ) {
if (context.hasRole(roleName)) return true;
roleName = roleName + "." + repositoryName;
if (context.hasRole(roleName)) return true;
roleName = roleName + "." + workspaceName;
return context.hasRole(roleName);
}
@Override
public void checkPermission( String path,
String actions ) {
CheckArg.isNotEmpty(path, "path");
CheckArg.isNotEmpty(actions, "actions");
try {
PathSupplier supplier = pathSupplierFor(absolutePathFor(path));
String[] actionsArray = actions.split(",");
checkPermission(workspace().getName(), supplier, actionsArray);
if (!hasPermissionOnExternalPath(supplier, actionsArray)) {
String absPath = supplier.getAbsolutePath().getString(namespaces());
throw new AccessDeniedException(JcrI18n.permissionDenied.text(absPath, actions));
}
} catch (RepositoryException e) {
throw new AccessControlException(JcrI18n.permissionDenied.text(path, actions));
}
}
/**
* Throws an {@link AccessControlException} if the current user does not have permission for all of the named actions in the
* current workspace, otherwise returns silently.
* <p>
* The {@code path} parameter is included for future use and is currently ignored
* </p>
*
* @param path the absolute path on which the actions are occurring
* @param actions a comma-delimited list of actions to check
* @throws AccessDeniedException if the actions cannot be performed on the node at the specified path
*/
void checkPermission( Path path,
String... actions ) throws AccessDeniedException {
checkPermission(this.workspace().getName(), path, actions);
}
void checkPermission( PathSupplier pathSupplier,
String... actions ) throws AccessDeniedException {
checkPermission(this.workspace().getName(), pathSupplier, actions);
}
/**
* Throws an {@link AccessControlException} if the current user does not have permission for all of the named actions in the
* current workspace, otherwise returns silently.
* <p>
* The {@code path} parameter is included for future use and is currently ignored
* </p>
*
* @param item the property or node on which the actions are occurring
* @param actions a comma-delimited list of actions to check
* @throws AccessDeniedException if the actions cannot be performed on the node at the specified path
*/
void checkPermission( AbstractJcrItem item,
String... actions ) throws AccessDeniedException {
checkPermission(this.workspace().getName(), pathSupplierFor(item), actions);
}
void checkPermission( CachedNode node,
NodeCache cache,
String... actions ) throws AccessDeniedException {
checkPermission(this.workspace().getName(), pathSupplierFor(node, cache), actions);
}
/**
* Throws an {@link AccessControlException} if the current user does not have permission for all of the named actions in the
* named workspace, otherwise returns silently.
* <p>
* The {@code path} parameter is included for future use and is currently ignored
* </p>
*
* @param workspaceName the name of the workspace in which the path exists
* @param path the absolute path on which the actions are occurring
* @param actions a comma-delimited list of actions to check
* @throws AccessDeniedException if the actions cannot be performed on the node at the specified path
*/
void checkPermission( String workspaceName,
Path path,
String... actions ) throws AccessDeniedException {
checkPermission(workspaceName, pathSupplierFor(path), actions);
}
void checkPermission( String workspaceName,
PathSupplier pathSupplier,
String... actions ) throws AccessDeniedException {
CheckArg.isNotEmpty(actions, "actions");
if (hasPermission(workspaceName, pathSupplier, actions)) return;
String pathAsString = "<unknown>";
if (pathSupplier != null) {
try {
pathAsString = pathSupplier.getAbsolutePath().getString(namespaces());
} catch (ItemNotFoundException e) {
// Node was somehow removed from this session
}
}
throw new AccessDeniedException(JcrI18n.permissionDenied.text(pathAsString, actions));
}
void checkWorkspacePermission( String workspaceName,
String... actions ) throws AccessDeniedException {
checkPermission(workspaceName, (PathSupplier)null, actions);
}
@Override
public boolean hasPermission( String absPath,
String actions ) throws RepositoryException {
checkLive();
CheckArg.isNotEmpty(absPath, "absPath");
PathSupplier pathSupplier = pathSupplierFor(absolutePathFor(absPath));
String[] actionsArray = actions.split(",");
String workspaceName = this.workspace().getName();
return hasPermission(workspaceName, pathSupplier, actionsArray)
&& hasPermissionOnExternalPath(pathSupplier, actionsArray);
}
/**
* Makes a "best effort" determination of whether the given method can be successfully called on the given target with the
* given arguments. A return value of {@code false} indicates that the method would not succeed. A return value of
* {@code true} indicates that the method <i>might</i> succeed.
*
* @param methodName the method to invoke; may not be null
* @param target the object on which to invoke it; may not be null
* @param arguments the arguments to pass to the method; varies depending on the method
* @return true if the given method can be determined to be supported, or false otherwise
* @throws IllegalArgumentException
* @throws RepositoryException
*/
@Override
public boolean hasCapability( String methodName,
Object target,
Object[] arguments ) throws RepositoryException {
CheckArg.isNotEmpty(methodName, "methodName");
CheckArg.isNotNull(target, "target");
checkLive();
if (target instanceof AbstractJcrNode) {
AbstractJcrNode node = (AbstractJcrNode)target;
if ("addNode".equals(methodName)) {
CheckArg.hasSizeOfAtLeast(arguments, 1, "arguments");
CheckArg.hasSizeOfAtMost(arguments, 2, "arguments");
CheckArg.isInstanceOf(arguments[0], String.class, "arguments[0]");
String relPath = (String)arguments[0];
String primaryNodeTypeName = null;
if (arguments.length > 1) {
CheckArg.isInstanceOf(arguments[1], String.class, "arguments[1]");
primaryNodeTypeName = (String)arguments[1];
}
return node.canAddNode(relPath, primaryNodeTypeName);
}
// TODO: Should 'hasCapability' support methods other than 'addNode'?
}
return true;
}
@Override
public ContentHandler getImportContentHandler( String parentAbsPath,
int uuidBehavior )
throws PathNotFoundException, ConstraintViolationException, VersionException, LockException, RepositoryException {
checkLive();
// Find the parent path ...
AbstractJcrNode parent = getNode(parentAbsPath);
if (!parent.isCheckedOut()) {
throw new VersionException(JcrI18n.nodeIsCheckedIn.text(parent.getPath()));
}
boolean retainLifecycleInfo = getRepository().getDescriptorValue(Repository.OPTION_LIFECYCLE_SUPPORTED).getBoolean();
boolean retainRetentionInfo = getRepository().getDescriptorValue(Repository.OPTION_RETENTION_SUPPORTED).getBoolean();
return new JcrContentHandler(this, parent, uuidBehavior, false, retainRetentionInfo, retainLifecycleInfo);
}
protected void initBaseVersionKeys() {
// Since we're importing into this session, we need to capture any base version information in the imported file ...
baseVersionKeys.compareAndSet(null, new ConcurrentHashMap<NodeKey, NodeKey>());
}
protected void initOriginalVersionKeys() {
originalVersionKeys.compareAndSet(null, new ConcurrentHashMap<NodeKey, NodeKey>());
}
@Override
public void importXML( String parentAbsPath,
InputStream in,
int uuidBehavior )
throws IOException, PathNotFoundException, ItemExistsException, ConstraintViolationException, VersionException,
InvalidSerializedDataException, LockException, RepositoryException {
CheckArg.isNotNull(parentAbsPath, "parentAbsPath");
CheckArg.isNotNull(in, "in");
checkLive();
boolean error = false;
try {
XMLReader parser = XMLReaderFactory.createXMLReader();
parser.setContentHandler(getImportContentHandler(parentAbsPath, uuidBehavior));
parser.parse(new InputSource(in));
} catch (EnclosingSAXException ese) {
Exception cause = ese.getException();
if (cause instanceof RepositoryException) {
throw (RepositoryException)cause;
}
throw new RepositoryException(cause);
} catch (SAXParseException se) {
error = true;
throw new InvalidSerializedDataException(se);
} catch (SAXException se) {
error = true;
throw new RepositoryException(se);
} finally {
try {
in.close();
} catch (IOException t) {
if (!error) throw t; // throw only if no error in outer try
} catch (RuntimeException re) {
if (!error) throw re; // throw only if no error in outer try
}
}
}
@Override
public void exportSystemView( String absPath,
ContentHandler contentHandler,
boolean skipBinary,
boolean noRecurse ) throws PathNotFoundException, SAXException, RepositoryException {
CheckArg.isNotNull(absPath, "absPath");
CheckArg.isNotNull(contentHandler, "contentHandler");
Node exportRootNode = getNode(absPath);
AbstractJcrExporter exporter = new JcrSystemViewExporter(this);
exporter.exportView(exportRootNode, contentHandler, skipBinary, noRecurse);
}
@Override
public void exportSystemView( String absPath,
OutputStream out,
boolean skipBinary,
boolean noRecurse ) throws IOException, PathNotFoundException, RepositoryException {
CheckArg.isNotNull(absPath, "absPath");
CheckArg.isNotNull(out, "out");
Node exportRootNode = getNode(absPath);
AbstractJcrExporter exporter = new JcrSystemViewExporter(this);
exporter.exportView(exportRootNode, out, skipBinary, noRecurse);
}
@Override
public void exportDocumentView( String absPath,
ContentHandler contentHandler,
boolean skipBinary,
boolean noRecurse ) throws PathNotFoundException, SAXException, RepositoryException {
CheckArg.isNotNull(absPath, "absPath");
CheckArg.isNotNull(contentHandler, "contentHandler");
checkLive();
Node exportRootNode = getNode(absPath);
AbstractJcrExporter exporter = new JcrDocumentViewExporter(this);
exporter.exportView(exportRootNode, contentHandler, skipBinary, noRecurse);
}
@Override
public void exportDocumentView( String absPath,
OutputStream out,
boolean skipBinary,
boolean noRecurse ) throws IOException, PathNotFoundException, RepositoryException {
CheckArg.isNotNull(absPath, "absPath");
CheckArg.isNotNull(out, "out");
Node exportRootNode = getNode(absPath);
AbstractJcrExporter exporter = new JcrDocumentViewExporter(this);
exporter.exportView(exportRootNode, out, skipBinary, noRecurse);
}
@Override
public String getNamespacePrefix( String uri ) throws RepositoryException {
checkLive();
return sessionRegistry.getPrefix(uri);
}
@Override
public String[] getNamespacePrefixes() throws RepositoryException {
checkLive();
return sessionRegistry.getPrefixes();
}
@Override
public String getNamespaceURI( String prefix ) throws RepositoryException {
checkLive();
return sessionRegistry.getURI(prefix);
}
@Override
public void setNamespacePrefix( String newPrefix,
String existingUri ) throws NamespaceException, RepositoryException {
checkLive();
sessionRegistry.registerNamespace(newPrefix, existingUri);
}
@Override
public synchronized void logout() {
terminate(true);
try {
RunningState running = repository.runningState();
long lifetime = Math.abs(System.nanoTime() - this.nanosCreated);
Map<String, String> payload = Collections.singletonMap("userId", getUserID());
running.statistics().recordDuration(DurationMetric.SESSION_LIFETIME, lifetime, TimeUnit.NANOSECONDS, payload);
running.statistics().decrement(ValueMetric.SESSION_COUNT);
running.removeSession(this);
} catch (IllegalStateException e) {
// The repository has been shutdown
} finally {
if (bufferMgr != null) {
try {
bufferMgr.close();
} finally {
bufferMgr = null;
}
}
}
}
@Override
public boolean isLive() {
return isLive;
}
@Override
public void addLockToken( String lockToken ) {
CheckArg.isNotNull(lockToken, "lockToken");
try {
lockManager().addLockToken(lockToken);
} catch (LockException le) {
// For backwards compatibility (and API compatibility), the LockExceptions from the LockManager need to get swallowed
}
}
@Override
public String[] getLockTokens() {
if (!isLive()) return new String[] {};
return lockManager().getLockTokens();
}
@Override
public void removeLockToken( String lockToken ) {
CheckArg.isNotNull(lockToken, "lockToken");
// A LockException is thrown if the lock associated with the specified lock token is session-scoped.
try {
lockManager().removeLockToken(lockToken);
} catch (LockException le) {
// For backwards compatibility (and API compatibility), the LockExceptions from the LockManager need to get swallowed
}
}
@Override
public AccessControlManager getAccessControlManager() {
return acm;
}
@Override
public RetentionManager getRetentionManager() throws UnsupportedRepositoryOperationException, RepositoryException {
throw new UnsupportedRepositoryOperationException();
}
/**
* Returns the absolute path of the node in the specified workspace that corresponds to this node.
* <p>
* The corresponding node is defined as the node in srcWorkspace with the same UUID as this node or, if this node has no UUID,
* the same path relative to the nearest ancestor that does have a UUID, or the root node, whichever comes first. This is
* qualified by the requirement that referencable nodes only correspond with other referencables and non-referenceables with
* other non-referenceables.
* </p>
*
* @param workspaceName the name of the workspace; may not be null
* @param key the key for the node; may not be null
* @param relativePath the relative path from the referenceable node, or null if the supplied UUID identifies the
* corresponding node
* @return the absolute path to the corresponding node in the workspace; never null
* @throws NoSuchWorkspaceException if the specified workspace does not exist
* @throws ItemNotFoundException if no corresponding node exists
* @throws AccessDeniedException if the current session does not have sufficient rights to perform this operation
* @throws RepositoryException if another exception occurs
*/
Path getPathForCorrespondingNode( String workspaceName,
NodeKey key,
Path relativePath )
throws NoSuchWorkspaceException, AccessDeniedException, ItemNotFoundException, RepositoryException {
assert key != null;
try {
NodeCache cache = repository.repositoryCache().getWorkspaceCache(workspaceName);
CachedNode node = cache.getNode(key);
if (node == null) {
throw new ItemNotFoundException(JcrI18n.itemNotFoundWithUuid.text(key.toString(), workspaceName));
}
if (relativePath != null) {
for (Segment segment : relativePath) {
ChildReference child = node.getChildReferences(cache).getChild(segment);
if (child == null) {
Path path = pathFactory().create(node.getPath(cache), segment);
throw new ItemNotFoundException(JcrI18n.itemNotFoundAtPath.text(path.getString(namespaces()),
workspaceName()));
}
CachedNode childNode = cache.getNode(child);
if (childNode == null) {
Path path = pathFactory().create(node.getPath(cache), segment);
throw new ItemNotFoundException(JcrI18n.itemNotFoundAtPath.text(path.getString(namespaces()),
workspaceName()));
}
node = childNode;
}
}
return node.getPath(cache);
} catch (AccessControlException ace) {
throw new AccessDeniedException(ace);
} catch (WorkspaceNotFoundException e) {
throw new NoSuchWorkspaceException(JcrI18n.workspaceNameIsInvalid.text(repository().repositoryName(), workspaceName));
}
}
/**
* Determine if the supplied string represents just the {@link Node#getIdentifier() node's identifier} or whether it is a
* string representation of a NodeKey. If it is just the node's identifier, then the NodeKey is created by using the same
* {@link NodeKey#getSourceKey() source key} and {@link NodeKey#getWorkspaceKey() workspace key} from the supplied root node.
*
* @param identifier the identifier string; may not be null
* @param rootKey the node of the root in the workspace; may not be null
* @return the node key re-created from the supplied identifier; never null
*/
public static NodeKey createNodeKeyFromIdentifier( String identifier,
NodeKey rootKey ) {
// If this node is a random identifier, then we need to use it as a node key identifier ...
if (NodeKey.isValidRandomIdentifier(identifier)) {
return rootKey.withId(identifier);
}
return new NodeKey(identifier);
}
/**
* Checks if the node given key is foreign by comparing the source key & workspace key against the same keys from this
* session's root. This method is used for reference resolving.
*
* @param key the node key; may be null
* @param rootKey the key of the root node in the workspace; may not be null
* @return true if the node key is considered foreign, false otherwise.
*/
public static boolean isForeignKey( NodeKey key,
NodeKey rootKey ) {
if (key == null) {
return false;
}
String nodeWorkspaceKey = key.getWorkspaceKey();
boolean sameWorkspace = rootKey.getWorkspaceKey().equals(nodeWorkspaceKey);
boolean sameSource = rootKey.getSourceKey().equalsIgnoreCase(key.getSourceKey());
return !sameWorkspace || !sameSource;
}
/**
* Returns a string representing a node's identifier, based on whether the node is foreign or not.
*
* @param key the node key; may be null
* @param rootKey the key of the root node in the workspace; may not be null
* @return the identifier for the node; never null
* @see javax.jcr.Node#getIdentifier()
*/
public static String nodeIdentifier( NodeKey key,
NodeKey rootKey ) {
return isForeignKey(key, rootKey) ? key.toString() : key.getIdentifier();
}
/**
* Checks if the node given key is foreign by comparing the source key & workspace key against the same keys from this
* session's root. This method is used for reference resolving.
*
* @param key the node key; may be null
* @return true if the node key is considered foreign, false otherwise.
*/
protected final boolean isForeignKey( NodeKey key ) {
return isForeignKey(key, cache.getRootKey());
}
/**
* Returns a string representing a node's identifier, based on whether the node is foreign or not.
*
* @param key the node key; may be null
* @return the identifier for the node; never null
* @see javax.jcr.Node#getIdentifier()
*/
protected final String nodeIdentifier( NodeKey key ) {
return nodeIdentifier(key, cache.getRootKey());
}
@Override
public boolean sequence( String sequencerName,
Property inputProperty,
Node outputNode ) throws RepositoryException {
CheckArg.isSame(inputProperty.getSession(), "inputProperty", this, "this session");
CheckArg.isSame(outputNode.getSession(), "outputNode", this, "this session");
Sequencer sequencer = repository().runningState().sequencers().getSequencer(sequencerName);
if (sequencer == null) return false;
final ValueFactory values = getValueFactory();
final DateTime now = dateFactory().create();
final Sequencer.Context context = new Sequencer.Context() {
@Override
public ValueFactory valueFactory() {
return values;
}
@Override
public Calendar getTimestamp() {
return now.toCalendar();
}
};
try {
if (sequencer.hasAcceptedMimeTypes()) {
// Get the MIME type, first by looking at the changed property's parent node
// (or grand-parent node if parent is 'jcr:content') ...
String mimeType = SequencingRunner.getInputMimeType(inputProperty);
// See if the sequencer accepts the MIME type ...
if (mimeType != null && !sequencer.isAccepted(mimeType)) {
Logger.getLogger(getClass())
.debug("Skipping sequencing because input's MIME type '{0}' is not accepted by the '{1}' sequencer",
mimeType, sequencerName);
return false;
}
}
return sequencer.execute(inputProperty, outputNode, context);
} catch (RepositoryException e) {
throw e;
} catch (Exception e) {
throw new RepositoryException(e);
}
}
@Override
public String toString() {
return cache.toString();
}
@Override
public String decode( final String localName ) {
return Path.JSR283_DECODER.decode(localName);
}
@Override
public String encode( final String localName ) {
return Path.JSR283_ENCODER.encode(localName);
}
/**
* Define the operations that are to be performed on all the nodes that were created or modified within this session. This
* class was designed to be as efficient as possible for most nodes, since most nodes do not need any additional processing.
*/
protected final class JcrPreSave implements SessionCache.PreSave {
private final SessionCache cache;
private final SessionCache systemCache;
private final RepositoryNodeTypeManager nodeTypeMgr;
private final NodeTypes nodeTypeCapabilities;
private final SystemContent systemContent;
private final Map<NodeKey, NodeKey> baseVersionKeys;
private final Map<NodeKey, NodeKey> originalVersionKeys;
private boolean initialized = false;
private PropertyFactory propertyFactory;
private ReferenceFactory referenceFactory;
private JcrVersionManager versionManager;
protected JcrPreSave( SystemContent content,
Map<NodeKey, NodeKey> baseVersionKeys,
Map<NodeKey, NodeKey> originalVersionKeys,
long aclChangesCount ) {
assert content != null;
this.cache = cache();
this.systemContent = content;
this.systemCache = content.cache();
this.baseVersionKeys = baseVersionKeys;
this.originalVersionKeys = originalVersionKeys;
// Get the capabilities cache. This is immutable, so we'll use it for the entire pre-save operation ...
this.nodeTypeMgr = repository().nodeTypeManager();
this.nodeTypeCapabilities = nodeTypeMgr.getNodeTypes();
if (aclChangesCount != 0) {
aclMetadataRefresh(aclChangesCount);
}
}
private void aclMetadataRefresh( long aclChangesCount ) {
// we have a session that has added and/or removed ACLs from nodes, so we need to reflect this in the repository
// metadata
MutableCachedNode systemNode = systemContent.mutableSystemNode();
org.modeshape.jcr.value.Property aclCount = systemNode.getProperty(ModeShapeLexicon.ACL_COUNT, systemCache);
if (aclCount == null && aclChangesCount > 0) {
systemNode.setProperty(systemCache, propertyFactory().create(ModeShapeLexicon.ACL_COUNT, aclChangesCount));
repository().repositoryCache().setAccessControlEnabled(true);
} else if (aclCount != null) {
long newCount = Long.valueOf(aclCount.getFirstValue().toString()) + aclChangesCount;
if (newCount < 0) {
newCount = 0;
}
if (newCount == 0) {
repository().repositoryCache().setAccessControlEnabled(false);
}
systemNode.setProperty(systemCache, propertyFactory().create(ModeShapeLexicon.ACL_COUNT, newCount));
}
}
@Override
public void process( MutableCachedNode node,
SaveContext context ) throws Exception {
// Most nodes do not need any extra processing, so the first thing to do is figure out whether this
// node has a primary type or mixin types that need extra processing. Unfortunately, this means we always have
// to get the primary type and mixin types.
final Name primaryType = node.getPrimaryType(cache);
final Set<Name> mixinTypes = node.getMixinTypes(cache);
if (nodeTypeCapabilities.isFullyDefinedType(primaryType, mixinTypes)) {
// There is nothing to do for this node ...
return;
}
if (!initialized) {
// We're gonna need a few more objects, so create them now ...
initialized = true;
versionManager = versionManager();
propertyFactory = propertyFactory();
referenceFactory = referenceFactory();
}
AbstractJcrNode jcrNode = null;
// -----------
// mix:created
// -----------
boolean initializeVersionHistory = false;
if (node.isNew()) {
if (nodeTypeCapabilities.isCreated(primaryType, mixinTypes)) {
// Set the created by and time information if not changed explicitly
node.setPropertyIfUnchanged(cache, propertyFactory.create(JcrLexicon.CREATED, context.getTime()));
node.setPropertyIfUnchanged(cache, propertyFactory.create(JcrLexicon.CREATED_BY, context.getUserId()));
}
initializeVersionHistory = nodeTypeCapabilities.isVersionable(primaryType, mixinTypes);
} else {
// Changed nodes can only be made versionable if the primary type or mixins changed ...
if (node.hasChangedPrimaryType() || !node.getAddedMixins(cache).isEmpty()) {
initializeVersionHistory = nodeTypeCapabilities.isVersionable(primaryType, mixinTypes);
}
}
// ----------------
// mix:lastModified
// ----------------
if (nodeTypeCapabilities.isLastModified(primaryType, mixinTypes)) {
// Set the last modified by and time information if it has not been changed explicitly
node.setPropertyIfUnchanged(cache, propertyFactory.create(JcrLexicon.LAST_MODIFIED, context.getTime()));
node.setPropertyIfUnchanged(cache, propertyFactory.create(JcrLexicon.LAST_MODIFIED_BY, context.getUserId()));
}
// ---------------
// mix:versionable
// ---------------
if (initializeVersionHistory) {
// See if there is a version history for the node ...
NodeKey versionableKey = node.getKey();
if (!systemContent.hasVersionHistory(versionableKey)) {
// Initialize the version history ...
NodeKey historyKey = systemContent.versionHistoryNodeKeyFor(versionableKey);
NodeKey baseVersionKey = baseVersionKeys == null ? null : baseVersionKeys.get(versionableKey);
// it may happen during an import, that a node with version history & base version is assigned a new key and
// therefore
// the base version points to an existing version while no version history is found initially
boolean shouldCreateNewVersionHistory = true;
if (baseVersionKey != null) {
CachedNode baseVersionNode = systemCache.getNode(baseVersionKey);
if (baseVersionNode != null) {
historyKey = baseVersionNode.getParentKey(systemCache);
shouldCreateNewVersionHistory = (historyKey == null);
}
}
if (shouldCreateNewVersionHistory) {
// a new version history should be initialized
assert historyKey != null;
if (baseVersionKey == null) baseVersionKey = historyKey.withRandomId();
NodeKey originalVersionKey = originalVersionKeys != null ? originalVersionKeys.get(versionableKey) : null;
Path versionHistoryPath = versionManager.versionHistoryPathFor(versionableKey);
systemContent.initializeVersionStorage(versionableKey, historyKey, baseVersionKey, primaryType,
mixinTypes, versionHistoryPath, originalVersionKey,
context.getTime());
}
// Now update the node as if it's checked in (with the exception of the predecessors...)
Reference historyRef = referenceFactory.create(historyKey, true);
Reference baseVersionRef = referenceFactory.create(baseVersionKey, true);
node.setProperty(cache, propertyFactory.create(JcrLexicon.IS_CHECKED_OUT, Boolean.TRUE));
node.setReference(cache, propertyFactory.create(JcrLexicon.VERSION_HISTORY, historyRef), systemCache);
node.setReference(cache, propertyFactory.create(JcrLexicon.BASE_VERSION, baseVersionRef), systemCache);
// JSR 283 - 15.1
node.setReference(cache, propertyFactory.create(JcrLexicon.PREDECESSORS, new Object[] {baseVersionRef}),
systemCache);
} else {
// we're dealing with node which has a version history, check if there any versionable properties present
boolean hasVersioningProperties = node.hasProperty(JcrLexicon.IS_CHECKED_OUT, cache)
|| node.hasProperty(JcrLexicon.VERSION_HISTORY, cache)
|| node.hasProperty(JcrLexicon.BASE_VERSION, cache)
|| node.hasProperty(JcrLexicon.PREDECESSORS, cache);
if (!hasVersioningProperties) {
// the node doesn't have any versionable properties, so this is a case of mix:versionable removed at some
// point and then re-added. If it had any versioning properties, we might've been dealing with something
// else
// e.g. a restore
// Re-link the versionable properties, based on the existing version history
node.setProperty(cache, propertyFactory.create(JcrLexicon.IS_CHECKED_OUT, Boolean.TRUE));
JcrVersionHistoryNode versionHistoryNode = versionManager().getVersionHistory(node(node.getKey(), null));
Reference historyRef = referenceFactory.create(versionHistoryNode.key(), true);
node.setReference(cache, propertyFactory.create(JcrLexicon.VERSION_HISTORY, historyRef), systemCache);
// set the base version to the last existing version
JcrVersionNode baseVersion = null;
for (VersionIterator versionIterator = versionHistoryNode.getAllVersions(); versionIterator.hasNext();) {
JcrVersionNode version = (JcrVersionNode)versionIterator.nextVersion();
if (baseVersion == null || version.isLinearSuccessorOf(baseVersion)) {
baseVersion = version;
}
}
assert baseVersion != null;
Reference baseVersionRef = referenceFactory.create(baseVersion.key(), true);
node.setReference(cache, propertyFactory.create(JcrLexicon.BASE_VERSION, baseVersionRef), systemCache);
// set the predecessors to the same list as the base version's predecessors
Version[] baseVersionPredecessors = baseVersion.getPredecessors();
Reference[] predecessors = new Reference[baseVersionPredecessors.length];
for (int i = 0; i < baseVersionPredecessors.length; i++) {
predecessors[i] = referenceFactory.create(((JcrVersionNode)baseVersionPredecessors[i]).key(), true);
}
node.setReference(cache, propertyFactory.create(JcrLexicon.PREDECESSORS, predecessors), systemCache);
}
}
}
// -----------
// nt:resource
// -----------
if (nodeTypeCapabilities.isNtResource(primaryType)) {
// If there is no "jcr:mimeType" property ...
if (!node.hasProperty(JcrLexicon.MIMETYPE, cache)) {
// Try to get the MIME type for the binary value ...
org.modeshape.jcr.value.Property dataProp = node.getProperty(JcrLexicon.DATA, cache);
if (dataProp != null) {
Object dataValue = dataProp.getFirstValue();
if (dataValue instanceof Binary) {
Binary binaryValue = (Binary)dataValue;
// Get the name of this node's parent ...
String fileName = null;
NodeKey parentKey = node.getParentKey(cache);
if (parentKey != null) {
CachedNode parent = cache.getNode(parentKey);
Name parentName = parent.getName(cache);
fileName = stringFactory().create(parentName);
}
String mimeType = binaryValue.getMimeType(fileName);
if (mimeType != null) {
node.setProperty(cache, propertyFactory.create(JcrLexicon.MIMETYPE, mimeType));
}
}
}
}
}
// --------------------
// Mandatory properties
// --------------------
// Some of the version history properties are mandatory, so we need to initialize the version history first ...
Collection<JcrPropertyDefinition> mandatoryPropDefns = null;
mandatoryPropDefns = nodeTypeCapabilities.getMandatoryPropertyDefinitions(primaryType, mixinTypes);
if (!mandatoryPropDefns.isEmpty()) {
// There is at least one mandatory property on this node, so go through all of the mandatory property
// definitions and see if any do not correspond to existing properties ...
for (JcrPropertyDefinition defn : mandatoryPropDefns) {
Name propName = defn.getInternalName();
if (!node.hasProperty(propName, cache)) {
// There is no mandatory property ...
if (defn.hasDefaultValues()) {
// This may or may not be auto-created; we don't care ...
if (jcrNode == null) jcrNode = node(node, (Type)null, null);
JcrValue[] defaultValues = defn.getDefaultValues();
if (defn.isMultiple()) {
jcrNode.setProperty(propName, defaultValues, defn.getRequiredType(), false);
} else {
// don't skip constraint checks or protected checks
jcrNode.setProperty(propName, defaultValues[0], false, false, false, false);
}
} else {
// There is no default for this mandatory property, so this is a constraint violation ...
String pName = defn.getName();
String typeName = defn.getDeclaringNodeType().getName();
String loc = readableLocation(node);
throw new ConstraintViolationException(JcrI18n.missingMandatoryProperty.text(pName, typeName, loc));
}
} else {
// There is a property with the same name as the mandatory property, so verify that the
// existing property does indeed use this property definition. Use the JCR property
// since it may already cache the property definition ID or will know how to find it ...
if (jcrNode == null) jcrNode = node(node, (Type)null, null);
AbstractJcrProperty jcrProperty = jcrNode.getProperty(propName);
PropertyDefinitionId defnId = jcrProperty.propertyDefinitionId();
if (defn.getId().equals(defnId)) {
// This existing property does use the auto-created definition ...
continue;
}
// The existing property does not use the property definition, but we can't auto-create the property
// because there is already an existing one with the same name. First see if we can forcibly
// recompute the property definition ...
jcrProperty.releasePropertyDefinitionId();
defnId = jcrProperty.propertyDefinitionId();
if (defn.getId().equals(defnId)) {
// This existing property does use the auto-created definition ...
continue;
}
// Still didn't match, so this is a constraint violation of the existing property ...
String pName = defn.getName();
String typeName = defn.getDeclaringNodeType().getName();
String loc = readableLocation(node);
I18n msg = JcrI18n.propertyNoLongerSatisfiesConstraints;
throw new ConstraintViolationException(msg.text(pName, loc, defn.getName(), typeName));
}
}
}
// ---------------------
// Mandatory child nodes
// ---------------------
Collection<JcrNodeDefinition> mandatoryChildDefns = null;
mandatoryChildDefns = nodeTypeCapabilities.getMandatoryChildNodeDefinitions(primaryType, mixinTypes);
if (!mandatoryChildDefns.isEmpty()) {
Set<Name> childrenNames = new HashSet<Name>();
for (ChildReference childRef : node.getChildReferences(cache())) {
childrenNames.add(childRef.getName());
}
for (JcrNodeDefinition defn : mandatoryChildDefns) {
Name childName = defn.getInternalName();
if (!childrenNames.contains(childName)) {
throw new ConstraintViolationException(
JcrI18n.propertyNoLongerSatisfiesConstraints.text(childName,
readableLocation(node),
defn.getName(),
defn.getDeclaringNodeType()
.getName()));
}
}
}
// --------
// mix:etag
// --------
// The 'jcr:etag' property may depend on auto-created properties, so do this last ...
if (nodeTypeCapabilities.isETag(primaryType, mixinTypes)) {
// Per section 3.7.12 of JCR 2, the 'jcr:etag' property should be changed whenever BINARY properties
// are added, removed, or changed. So, go through the properties (in sorted-name order so it is repeatable)
// and create this value by simply concatenating the SHA-1 hash of each BINARY value ...
String etagValue = node.getEtag(cache);
node.setProperty(cache, propertyFactory.create(JcrLexicon.ETAG, etagValue));
}
}
@Override
public void processAfterLocking( MutableCachedNode modifiedNode,
SaveContext context,
NodeCache persistentNodeCache ) throws RepositoryException {
// We actually can avoid this altogether if certain conditions are met ...
final Name primaryType = modifiedNode.getPrimaryType(cache);
final Set<Name> mixinTypes = modifiedNode.getMixinTypes(cache);
if (!nodeTypeCapabilities.disallowsSameNameSiblings(primaryType, mixinTypes)) return;
MutableCachedNode.NodeChanges changes = modifiedNode.getNodeChanges();
Map<NodeKey, Name> appendedChildren = changes.appendedChildren();
Map<NodeKey, Name> renamedChildren = changes.renamedChildren();
Set<NodeKey> removedChildren = changes.removedChildren();
if (!appendedChildren.isEmpty() || !renamedChildren.isEmpty()) {
Multimap<Name, NodeKey> appendedOrRenamedChildrenByName = LinkedListMultimap.create();
for (Map.Entry<NodeKey, Name> appended : appendedChildren.entrySet()) {
appendedOrRenamedChildrenByName.put(appended.getValue(), appended.getKey());
}
for (Map.Entry<NodeKey, Name> renamed : renamedChildren.entrySet()) {
appendedOrRenamedChildrenByName.put(renamed.getValue(), renamed.getKey());
}
assert appendedOrRenamedChildrenByName.isEmpty() == false;
// look at the information that was already persisted to determine whether some other thread has already
// created a child with the same name
CachedNode persistentNode = persistentNodeCache.getNode(modifiedNode.getKey());
final ChildReferences persistedChildReferences = persistentNode.getChildReferences(persistentNodeCache);
final SiblingCounter siblingCounter = SiblingCounter.create(persistedChildReferences);
// process appended/renamed children
for (Name childName : appendedOrRenamedChildrenByName.keySet()) {
int existingChildrenWithSameName = persistedChildReferences.getChildCount(childName);
if (existingChildrenWithSameName == 0) {
continue;
}
if (existingChildrenWithSameName == 1) {
// See if the existing same-name sibling is removed ...
NodeKey persistedChildKey = persistedChildReferences.getChild(childName).getKey();
if (removedChildren.contains(persistedChildKey)) {
// the sole existing child with this name is being removed, so we can ignore it ...
// existingChildrenWithSameName = 0;
continue;
}
}
// There is at least one persisted child with the same name, and we're adding a new child
// or renaming an existing child to this name. Therefore, we have to find a child node definition
// that allows SNS. Look for one ignoring the child node type (this is faster than finding the
// child node primary types) ...
NodeDefinitionSet childDefns = nodeTypeCapabilities.findChildNodeDefinitions(primaryType, mixinTypes);
JcrNodeDefinition childNodeDefinition = childDefns.findBestDefinitionForChild(childName, null, true,
siblingCounter);
if (childNodeDefinition != null) {
// found the one child node definition that applies, so it's okay ...
continue;
}
// We were NOT able to find a definition that allows SNS for this name, but we need to make sure that
// the node that already exists (persisted) isn't the one that's being changed
NodeKey persistedChildKey = persistedChildReferences.getChild(childName).getKey();
if (appendedChildren.containsKey(persistedChildKey) || renamedChildren.containsKey(persistedChildKey)) {
// The persisted node is being changed, so it's okay ...
continue;
}
// We still were NOT able to find a definition that allows SNS for this name WITHOUT considering the
// specific child node type. This likely means there is either 0 or more than 1 (possibly residual)
// child node definitions. We need to find all of the added/renamed child nodes and use their specific
// primary types. The first to fail will result in an exception ...
final SessionCache session = cache();
for (NodeKey appendedOrRenamedKey : appendedOrRenamedChildrenByName.get(childName)) {
MutableCachedNode appendedOrRenamedChild = session.mutable(appendedOrRenamedKey);
if (appendedOrRenamedChild == null) continue;
Name childPrimaryType = appendedOrRenamedChild.getPrimaryType(session);
childDefns = nodeTypeCapabilities.findChildNodeDefinitions(primaryType, mixinTypes);
childNodeDefinition = childDefns.findBestDefinitionForChild(childName, childPrimaryType, true,
siblingCounter);
if (childNodeDefinition == null) {
// Could not find a valid child node definition that allows SNS given the child's primary type and
// name plus the parent's primary type and mixin types.
throw new ItemExistsException(JcrI18n.noSnsDefinitionForNode.text(childName, workspaceName()));
}
}
}
}
}
}
protected class SystemSessionCache extends SessionCacheWrapper {
protected SystemSessionCache( SessionCache delegate ) {
super(delegate);
}
@Override
public void save() {
super.save();
signalSaveOfSystemChanges();
}
@Override
public void save( SessionCache otherSession,
PreSave preSaveOperation ) {
super.save(otherSession, preSaveOperation);
signalSaveOfSystemChanges();
}
@Override
public void save( Set<NodeKey> toBeSaved,
SessionCache otherSession,
PreSave preSaveOperation ) {
super.save(toBeSaved, otherSession, preSaveOperation);
signalSaveOfSystemChanges();
}
/**
* This method can be called by workspace-write methods, which (if a transaction has started after this session was
* created) can persist changes (via their SessionCache.save())
*/
private void signalSaveOfSystemChanges() {
cache().checkForTransaction();
}
}
}