/*
* 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.jcr2spi.state;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.PathNotFoundException;
import javax.jcr.RepositoryException;
import org.apache.jackrabbit.jcr2spi.hierarchy.HierarchyEntry;
import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry;
import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry;
import org.apache.jackrabbit.jcr2spi.nodetype.ItemDefinitionProvider;
import org.apache.jackrabbit.spi.ChildInfo;
import org.apache.jackrabbit.spi.IdFactory;
import org.apache.jackrabbit.spi.ItemInfo;
import org.apache.jackrabbit.spi.ItemInfoCache;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.NodeId;
import org.apache.jackrabbit.spi.NodeInfo;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.PathFactory;
import org.apache.jackrabbit.spi.PropertyId;
import org.apache.jackrabbit.spi.PropertyInfo;
import org.apache.jackrabbit.spi.RepositoryService;
import org.apache.jackrabbit.spi.SessionInfo;
import org.apache.jackrabbit.spi.ItemInfoCache.Entry;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <code>WorkspaceItemStateFactory</code>...
*/
public class WorkspaceItemStateFactory extends AbstractItemStateFactory {
private static Logger log = LoggerFactory.getLogger(WorkspaceItemStateFactory.class);
private final RepositoryService service;
private final SessionInfo sessionInfo;
private final ItemDefinitionProvider definitionProvider;
public final ItemInfoCache cache;
public WorkspaceItemStateFactory(RepositoryService service, SessionInfo sessionInfo,
ItemDefinitionProvider definitionProvider, ItemInfoCache cache) {
this.service = service;
this.sessionInfo = sessionInfo;
this.definitionProvider = definitionProvider;
this.cache = cache;
}
public NodeState createRootState(NodeEntry entry) throws ItemNotFoundException, RepositoryException {
IdFactory idFactory = service.getIdFactory();
PathFactory pf = service.getPathFactory();
return createNodeState(idFactory.createNodeId((String) null, pf.getRootPath()), entry);
}
/**
* Creates the node with information retrieved from the <code>RepositoryService</code>.
*/
public NodeState createNodeState(NodeId nodeId, NodeEntry entry) throws ItemNotFoundException,
RepositoryException {
try {
Entry<NodeInfo> cached = cache.getNodeInfo(nodeId);
NodeInfo info;
if (isUpToDate(cached, entry)) {
info = cached.info;
} else {
// otherwise retreive item info from service and cache the whole batch
Iterator<? extends ItemInfo> infos = service.getItemInfos(sessionInfo, nodeId);
info = first(infos, cache, entry.getGeneration());
if (info == null) {
throw new ItemNotFoundException("NodeId: " + nodeId);
}
}
assertMatchingPath(info, entry);
return createNodeState(info, entry);
}
catch (PathNotFoundException e) {
throw new ItemNotFoundException(e);
}
}
/**
* Creates the node with information retrieved from the <code>RepositoryService</code>.
* Intermediate entries are created as needed.
*/
public NodeState createDeepNodeState(NodeId nodeId, NodeEntry anyParent) throws ItemNotFoundException,
RepositoryException {
try {
// Get item info from cache
Iterator<? extends ItemInfo> infos = null;
Entry<NodeInfo> cached = cache.getNodeInfo(nodeId);
NodeInfo info;
if (cached == null) {
// or from service if not in cache
infos = service.getItemInfos(sessionInfo, nodeId);
info = first(infos, null, 0);
if (info == null) {
throw new ItemNotFoundException("NodeId: " + nodeId);
}
} else {
info = cached.info;
}
// Build the hierarchy entry for the item info
HierarchyEntry entry = createHierarchyEntries(info, anyParent);
if (entry == null || !entry.denotesNode()) {
throw new ItemNotFoundException(
"HierarchyEntry does not belong to any existing ItemInfo. No ItemState was created.");
} else {
// Now we can check whether the item info from the cache is up to date
long generation = entry.getGeneration();
if (isOutdated(cached, entry)) {
// if not, retrieve the item info from the service and put the whole batch into the cache
infos = service.getItemInfos(sessionInfo, nodeId);
info = first(infos, cache, generation);
} else if (infos != null) {
// Otherwise put the whole batch retrieved from the service earlier into the cache
cache.put(info, generation);
first(infos, cache, generation);
}
assertMatchingPath(info, entry);
return createNodeState(info, (NodeEntry) entry);
}
}
catch (PathNotFoundException e) {
throw new ItemNotFoundException(e);
}
}
/**
* Creates the PropertyState with information retrieved from the <code>RepositoryService</code>.
*/
public PropertyState createPropertyState(PropertyId propertyId, PropertyEntry entry)
throws ItemNotFoundException, RepositoryException {
try {
// Get item info from cache and use it if up to date
Entry<PropertyInfo> cached = cache.getPropertyInfo(propertyId);
PropertyInfo info;
if (isUpToDate(cached, entry)) {
info = cached.info;
} else {
// otherwise retreive item info from service and cache the whole batch
info = service.getPropertyInfo(sessionInfo, propertyId);
cache.put(info, entry.getGeneration());
}
assertMatchingPath(info, entry);
return createPropertyState(info, entry);
}
catch (PathNotFoundException e) {
throw new ItemNotFoundException(e);
}
}
/**
* Creates the PropertyState with information retrieved from the <code>RepositoryService</code>.
* Intermediate entries are created as needed.
*/
public PropertyState createDeepPropertyState(PropertyId propertyId, NodeEntry anyParent)
throws RepositoryException {
try {
// Get item info from cache
Entry<PropertyInfo> cached = cache.getPropertyInfo(propertyId);
PropertyInfo info;
if (cached == null) {
// or from service if not in cache
info = service.getPropertyInfo(sessionInfo, propertyId);
} else {
info = cached.info;
}
// Build the hierarchy entry for the item info
HierarchyEntry entry = createHierarchyEntries(info, anyParent);
if (entry == null || entry.denotesNode()) {
throw new ItemNotFoundException(
"HierarchyEntry does not belong to any existing ItemInfo. No ItemState was created.");
} else {
if (isOutdated(cached, entry)) {
// if not, retreive the item info from the service and put the whole batch into the cache
info = service.getPropertyInfo(sessionInfo, propertyId);
cache.put(info, entry.getGeneration());
}
assertMatchingPath(info, entry);
return createPropertyState(info, (PropertyEntry) entry);
}
} catch (PathNotFoundException e) {
throw new ItemNotFoundException(e);
}
}
public Iterator<ChildInfo> getChildNodeInfos(NodeId nodeId) throws ItemNotFoundException,
RepositoryException {
return service.getChildInfos(sessionInfo, nodeId);
}
public Iterator<PropertyId> getNodeReferences(NodeState nodeState, Name propertyName, boolean weak) {
NodeEntry entry = nodeState.getNodeEntry();
// Shortcut
if (entry.getUniqueID() == null || !entry.hasPropertyEntry(NameConstants.JCR_UUID)) {
// for sure not referenceable
Set<PropertyId> t = Collections.emptySet();
return t.iterator();
}
// Has a unique ID and is potentially mix:referenceable. Try to retrieve references
try {
return service.getReferences(sessionInfo, entry.getWorkspaceId(), propertyName, weak);
} catch (RepositoryException e) {
log.debug("Unable to determine references to {}", nodeState);
Set<PropertyId> t = Collections.emptySet();
return t.iterator();
}
}
//------------------------------------------------------------< private >---
/**
* Returns the first item in the iterator if it exists and denotes a node.
* Otherwise returns <code>null</code>. If <code>cache</code> is not
* <code>null</code>, caches all items by the given <code>generation</code>.
* @param generation
*/
private static NodeInfo first(Iterator<? extends ItemInfo> infos, ItemInfoCache cache, long generation) {
ItemInfo first = null;
if (infos.hasNext()) {
first = infos.next();
if (cache != null) {
cache.put(first, generation);
}
if (!first.denotesNode()) {
first = null;
}
}
if (cache != null) {
while (infos.hasNext()) {
cache.put(infos.next(), generation);
}
}
return (NodeInfo) first;
}
/**
* Create the node state with the information from <code>info</code>.
*
* @param info the <code>NodeInfo</code> to use to create the <code>NodeState</code>.
* @param entry the hierarchy entry for of this state
* @return the new <code>NodeState</code>.
* @throws ItemNotFoundException
* @throws RepositoryException
*/
private NodeState createNodeState(NodeInfo info, NodeEntry entry) throws ItemNotFoundException,
RepositoryException {
// Make sure the entry has the correct ItemId
// this may not be the case, if the hierarchy has not been completely
// resolved yet -> if uniqueID is present, set it on this entry or on
// the appropriate parent entry
String uniqueID = info.getId().getUniqueID();
Path path = info.getId().getPath();
if (path == null) {
entry.setUniqueID(uniqueID);
} else if (uniqueID != null) {
// uniqueID that applies to a parent NodeEntry -> get parentEntry
NodeEntry parent = getAncestor(entry, path.getLength());
parent.setUniqueID(uniqueID);
}
int previousStatus = entry.getStatus();
if (Status.isTransient(previousStatus) || Status.isStale(previousStatus)) {
log.debug("Node has pending changes; omit resetting the state.");
return entry.getNodeState();
}
// update NodeEntry from the information present in the NodeInfo (prop entries)
List<Name> propNames = new ArrayList<Name>();
for (Iterator<PropertyId> it = info.getPropertyIds(); it.hasNext(); ) {
PropertyId pId = it.next();
Name propertyName = pId.getName();
propNames.add(propertyName);
}
try {
entry.setPropertyEntries(propNames);
} catch (ItemExistsException e) {
// should not get here
log.error("Internal error", e);
}
// unless the child-info are omitted by the SPI impl -> make sure
// the child entries the node entry are initialized or updated.
Iterator<ChildInfo> childInfos = info.getChildInfos();
if (childInfos != null) {
entry.setNodeEntries(childInfos);
}
// now build or update the nodestate itself
NodeState tmp = new NodeState(entry, info, this, definitionProvider);
entry.setItemState(tmp);
NodeState nState = entry.getNodeState();
if (previousStatus == Status._UNDEFINED_) {
// tmp state was used as resolution for the given entry i.e. the
// entry was not available before. otherwise the 2 states were
// merged. see HierarchyEntryImpl#setItemState
notifyCreated(nState);
} else {
notifyUpdated(nState, previousStatus);
}
return nState;
}
/**
* Create the property state with the information from <code>info</code>.
*
* @param info the <code>PropertyInfo</code> to use to create the <code>PropertyState</code>.
* @param entry the hierarchy entry for of this state
* @return the new <code>PropertyState</code>.
* @throws RepositoryException
*/
private PropertyState createPropertyState(PropertyInfo info, PropertyEntry entry)
throws RepositoryException {
// make sure uuid part of id is correct
String uniqueID = info.getId().getUniqueID();
if (uniqueID != null) {
// uniqueID always applies to a parent NodeEntry -> get parentEntry
NodeEntry parent = getAncestor(entry, info.getId().getPath().getLength());
parent.setUniqueID(uniqueID);
}
int previousStatus = entry.getStatus();
if (Status.isTransient(previousStatus) || Status.isStale(previousStatus)) {
log.debug("Property has pending changes; omit resetting the state.");
return entry.getPropertyState();
}
// now build or update the nodestate itself
PropertyState tmp = new PropertyState(entry, info, this, definitionProvider);
entry.setItemState(tmp);
PropertyState pState = entry.getPropertyState();
if (previousStatus == Status._UNDEFINED_) {
// tmp state was used as resolution for the given entry i.e. the
// entry was not available before. otherwise the 2 states were
// merged. see HierarchyEntryImpl#setItemState
notifyCreated(pState);
} else {
notifyUpdated(pState, previousStatus);
}
return pState;
}
/**
* Create missing hierarchy entries on the path from <code>anyParent</code> to the path
* of the <code>itemInfo</code>.
*
* @param info
* @param anyParent
* @return the hierarchy entry for <code>info</code>
* @throws RepositoryException
*/
private HierarchyEntry createHierarchyEntries(ItemInfo info, NodeEntry anyParent)
throws RepositoryException {
// Calculate relative path of missing entries
Path anyParentPath = anyParent.getWorkspacePath();
Path relPath = anyParentPath.computeRelativePath(info.getPath());
Path.Element[] missingElems = relPath.getElements();
NodeEntry entry = anyParent;
int last = missingElems.length - 1;
for (int i = 0; i <= last; i++) {
if (missingElems[i].denotesParent()) {
// Walk up the hierarchy for 'negative' paths
// until the smallest common root is found
entry = entry.getParent();
} else if (missingElems[i].denotesName()) {
// Add missing elements starting from the smallest common root
Name name = missingElems[i].getName();
int index = missingElems[i].getNormalizedIndex();
if (i == last && !info.denotesNode()) {
return entry.getOrAddPropertyEntry(name);
} else {
entry = createNodeEntry(entry, name, index);
}
}
}
return entry;
}
private NodeEntry createNodeEntry(NodeEntry parentEntry, Name name, int index) throws RepositoryException {
Entry<NodeInfo> cached = cache.getNodeInfo(parentEntry.getWorkspaceId());
if (isUpToDate(cached, parentEntry)) {
Iterator<ChildInfo> childInfos = cached.info.getChildInfos();
if (childInfos != null) {
parentEntry.setNodeEntries(childInfos);
}
}
return parentEntry.getOrAddNodeEntry(name, index, null);
}
/**
* Returns true if <code>cache</code> is not <code>null</code> and
* the cached entry is up to date.
* @param cacheEntry
* @param entry
* @return
* @throws RepositoryException
*/
private static boolean isUpToDate(Entry<?> cacheEntry, HierarchyEntry entry) throws RepositoryException {
return cacheEntry != null &&
cacheEntry.generation >= entry.getGeneration() &&
isMatchingPath(cacheEntry.info, entry);
}
/**
* Returns true if <code>cache</code> is not <code>null</code> and
* the cached entry is not up to date.
* @param cacheEntry
* @param entry
* @return
* @throws RepositoryException
*/
private static boolean isOutdated(Entry<?> cacheEntry, HierarchyEntry entry) throws RepositoryException {
return cacheEntry != null &&
(cacheEntry.generation < entry.getGeneration() ||
!isMatchingPath(cacheEntry.info, entry));
}
private static boolean isMatchingPath(ItemInfo info, HierarchyEntry entry) throws RepositoryException {
Path infoPath = info.getPath();
Path wspPath = entry.getWorkspacePath();
return infoPath.equals(wspPath);
}
/**
* Validation check: Path of the given ItemInfo must match to the Path of
* the HierarchyEntry. This is required for Items that are identified by
* a uniqueID that may move within the hierarchy upon restore or clone.
*
* @param info
* @param entry
* @throws RepositoryException
*/
private static void assertMatchingPath(ItemInfo info, HierarchyEntry entry) throws RepositoryException {
if (!isMatchingPath(info, entry)) {
// TODO: handle external move of nodes (parents) identified by uniqueID
throw new ItemNotFoundException("HierarchyEntry " + entry.getWorkspacePath() + " does not match ItemInfo " + info.getPath());
}
}
/**
* @param entry
* @param degree
* @return the ancestor entry at the specified degree.
*/
private static NodeEntry getAncestor(HierarchyEntry entry, int degree) {
NodeEntry parent = entry.getParent();
degree--;
while (parent != null && degree > 0) {
parent = parent.getParent();
degree--;
}
if (degree != 0) {
log.error("Parent of degree {} does not exist.", degree);
throw new IllegalArgumentException();
}
return parent;
}
}