/*
* 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.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Collections;
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.IdFactory;
import org.apache.jackrabbit.spi.ItemInfo;
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.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <code>WorkspaceItemStateFactory</code>...
*/
public class WorkspaceItemStateFactory extends AbstractItemStateFactory implements ItemStateFactory {
private static Logger log = LoggerFactory.getLogger(WorkspaceItemStateFactory.class);
private final RepositoryService service;
private final SessionInfo sessionInfo;
private final ItemDefinitionProvider definitionProvider;
public WorkspaceItemStateFactory(RepositoryService service, SessionInfo sessionInfo,
ItemDefinitionProvider definitionProvider) {
this.service = service;
this.sessionInfo = sessionInfo;
this.definitionProvider = definitionProvider;
}
/**
* @inheritDoc
* @see ItemStateFactory#createRootState(NodeEntry)
*/
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>.
*
* @inheritDoc
* @see ItemStateFactory#createNodeState(NodeId,NodeEntry)
*/
public NodeState createNodeState(NodeId nodeId, NodeEntry entry)
throws ItemNotFoundException, RepositoryException {
// build new node state from server information
try {
Iterator infos = service.getItemInfos(sessionInfo, nodeId);
NodeState nodeState = createItemStates(nodeId, infos, entry, false);
if (nodeState == null) {
throw new ItemNotFoundException("HierarchyEntry does not belong to any existing ItemInfo.");
}
return nodeState;
} catch (PathNotFoundException e) {
throw new ItemNotFoundException(e.getMessage());
}
}
/**
* @inheritDoc
* @see ItemStateFactory#createDeepNodeState(NodeId,NodeEntry)
*/
public NodeState createDeepNodeState(NodeId nodeId, NodeEntry anyParent) throws ItemNotFoundException, RepositoryException {
try {
Iterator infos = service.getItemInfos(sessionInfo, nodeId);
return createItemStates(nodeId, infos, anyParent, true);
} catch (PathNotFoundException e) {
throw new ItemNotFoundException(e.getMessage());
}
}
/**
* Creates the PropertyState with information retrieved from the
* <code>RepositoryService</code>.
*
* @inheritDoc
* @see ItemStateFactory#createPropertyState(PropertyId,PropertyEntry)
*/
public PropertyState createPropertyState(PropertyId propertyId,
PropertyEntry entry)
throws ItemNotFoundException, RepositoryException {
try {
PropertyInfo info = service.getPropertyInfo(sessionInfo, propertyId);
assertMatchingPath(info, entry);
return createPropertyState(info, entry);
} catch (PathNotFoundException e) {
throw new ItemNotFoundException(e.getMessage());
}
}
/**
* @inheritDoc
* @see ItemStateFactory#createDeepPropertyState(PropertyId,NodeEntry)
*/
public PropertyState createDeepPropertyState(PropertyId propertyId, NodeEntry anyParent) throws ItemNotFoundException, RepositoryException {
try {
PropertyInfo info = service.getPropertyInfo(sessionInfo, propertyId);
PropertyState propState = createDeepPropertyState(info, anyParent, null);
assertValidState(propState, info);
return propState;
} catch (PathNotFoundException e) {
throw new ItemNotFoundException(e.getMessage());
}
}
/**
* @inheritDoc
* @see ItemStateFactory#getChildNodeInfos(NodeId)
* @param nodeId
*/
public Iterator getChildNodeInfos(NodeId nodeId)
throws ItemNotFoundException, RepositoryException {
return service.getChildInfos(sessionInfo, nodeId);
}
/**
* @inheritDoc
* @see ItemStateFactory#getNodeReferences(NodeState,org.apache.jackrabbit.spi.Name,boolean)
*/
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
return Collections.EMPTY_SET.iterator();
}
// nodestate 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);
return Collections.EMPTY_SET.iterator();
}
}
//------------------------------------------------------------< private >---
/**
*
* @param nodeId
* @param itemInfos
* @param entry
* @param isDeep
* @return
* @throws ItemNotFoundException
* @throws RepositoryException
*/
private synchronized NodeState createItemStates(NodeId nodeId, Iterator itemInfos,
NodeEntry entry, boolean isDeep)
throws ItemNotFoundException, RepositoryException {
NodeState nodeState;
ItemInfos infos = new ItemInfos(itemInfos);
// first entry in the iterator is the originally requested Node.
if (infos.hasNext()) {
NodeInfo first = (NodeInfo) infos.next();
if (isDeep) {
// for a deep state, the hierarchy entry does not correspond to
// the given NodeEntry -> retrieve NodeState before executing
// validation check.
nodeState = createDeepNodeState(first, entry, infos);
assertValidState(nodeState, first);
} else {
// 'isDeep' == false -> the given NodeEntry must match to the
// first ItemInfo retrieved from the iterator.
assertMatchingPath(first, entry);
nodeState = createNodeState(first, entry);
}
} else {
// empty iterator
throw new ItemNotFoundException("Node with id " + nodeId + " could not be found.");
}
// deal with all additional ItemInfos that may be present.
NodeEntry parentEntry = nodeState.getNodeEntry();
while (infos.hasNext()) {
ItemInfo info = (ItemInfo) infos.next();
if (info.denotesNode()) {
createDeepNodeState((NodeInfo) info, parentEntry, infos);
} else {
createDeepPropertyState((PropertyInfo) info, parentEntry, infos);
}
}
return nodeState;
}
/**
* Creates the node with information retrieved from <code>info</code>.
*
* @param info the <code>NodeInfo</code> to use to create the <code>NodeState</code>.
* @param entry
* @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 make 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 propNames = new ArrayList();
for (Iterator it = info.getPropertyIds(); it.hasNext(); ) {
PropertyId pId = (PropertyId) it.next();
Name propertyName = pId.getName();
propNames.add(propertyName);
}
try {
entry.setPropertyEntries(propNames);
} catch (ItemExistsException e) {
// should not get here
log.warn("Internal error", e);
}
// unless the child-info are omitted by the SPI impl -> make sure
// the childentries the nodeentry are initialized or updated.
Iterator 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;
}
/**
* Creates the property with information retrieved from <code>info</code>.
*
* @param info the <code>PropertyInfo</code> to use to create the
* <code>PropertyState</code>.
* @param entry
* @return the new <code>PropertyState</code>.
*/
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;
}
/**
*
* @param info
* @param anyParent
* @return
* @throws RepositoryException
*/
private NodeState createDeepNodeState(NodeInfo info, NodeEntry anyParent, ItemInfos infos) throws RepositoryException {
try {
// node for nodeId exists -> build missing entries in hierarchy
// Note, that the path contained in NodeId does not reveal which
// entries are missing -> calculate relative path.
Path anyParentPath = anyParent.getWorkspacePath();
Path relPath = anyParentPath.computeRelativePath(info.getPath());
Path.Element[] missingElems = relPath.getElements();
if (startsWithIllegalElement(missingElems)) {
log.error("Relative path to NodeEntry starts with illegal element -> ignore NodeInfo with path " + info.getPath());
return null;
}
NodeEntry entry = anyParent;
for (int i = 0; i < missingElems.length; i++) {
Name name = missingElems[i].getName();
int index = missingElems[i].getNormalizedIndex();
entry = createIntermediateNodeEntry(entry, name, index, infos);
}
if (entry == anyParent) {
throw new RepositoryException("Internal error while getting deep itemState");
}
return createNodeState(info, entry);
} catch (PathNotFoundException e) {
throw new ItemNotFoundException(e.getMessage());
}
}
/**
*
* @param info
* @param anyParent
* @return
* @throws RepositoryException
*/
private PropertyState createDeepPropertyState(PropertyInfo info, NodeEntry anyParent, ItemInfos infos) throws RepositoryException {
try {
// prop for propertyId exists -> build missing entries in hierarchy
// Note, that the path contained in PropertyId does not reveal which
// entries are missing -> calculate relative path.
Path anyParentPath = anyParent.getWorkspacePath();
Path relPath = anyParentPath.computeRelativePath(info.getPath());
Path.Element[] missingElems = relPath.getElements();
// make sure the missing elements don't start with . or .. in which
// case the info is not within the tree as it is expected
// (see also JCR-1797)
if (startsWithIllegalElement(missingElems)) {
log.error("Relative path to PropertyEntry starts with illegal element -> ignore PropertyInfo with path " + info.getPath());
return null;
}
NodeEntry entry = anyParent;
int i = 0;
// NodeEntries except for the very last 'missingElem'
while (i < missingElems.length - 1) {
Name name = missingElems[i].getName();
int index = missingElems[i].getNormalizedIndex();
entry = createIntermediateNodeEntry(entry, name, index, infos);
i++;
}
// create PropertyEntry for the last element if not existing yet
Name propName = missingElems[i].getName();
PropertyEntry propEntry = entry.getOrAddPropertyEntry(propName);
return createPropertyState(info, propEntry);
} catch (PathNotFoundException e) {
throw new ItemNotFoundException(e.getMessage());
}
}
/**
*
* @param parentEntry
* @param name
* @param index
* @return
* @throws RepositoryException
*/
private static NodeEntry createIntermediateNodeEntry(NodeEntry parentEntry,
Name name, int index,
ItemInfos infos) throws RepositoryException {
if (infos != null) {
Iterator childInfos = infos.getChildInfos(parentEntry.getWorkspaceId());
if (childInfos != null) {
parentEntry.setNodeEntries(childInfos);
}
}
NodeEntry entry = parentEntry.getOrAddNodeEntry(name, index, null);
return entry;
}
/**
* Validation check: make sure the state is not null (was really created)
* and matches with the specified ItemInfo (path).
*
* @param state
* @param info
* @throws ItemNotFoundException
* @throws RepositoryException
*/
private static void assertValidState(ItemState state, ItemInfo info)
throws ItemNotFoundException, RepositoryException {
if (state == null) {
throw new ItemNotFoundException("HierarchyEntry does not belong to any existing ItemInfo. No ItemState was created.");
}
assertMatchingPath(info, state.getHierarchyEntry());
}
/**
* 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 ItemNotFoundException
* @throws RepositoryException
*/
private static void assertMatchingPath(ItemInfo info, HierarchyEntry entry)
throws ItemNotFoundException, RepositoryException {
Path infoPath = info.getPath();
if (!infoPath.equals(entry.getWorkspacePath())) {
// TODO: handle external move of nodes (parents) identified by uniqueID
throw new ItemNotFoundException("HierarchyEntry does not belong the given ItemInfo.");
}
}
/**
* Returns true if the given <code>missingElems</code> start with a parent (..),
* a current (.) or the root element, in which case the info is not within
* the tree as it is expected.
* See also #JCR-1797 for the corresponding enhancement request.
*
* @param missingElems
* @return true if the first element doesn't denote a named element.
*/
private static boolean startsWithIllegalElement(Path.Element[] missingElems) {
if (missingElems.length > 0) {
return !missingElems[0].denotesName();
}
return false;
}
/**
* @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) {
throw new IllegalArgumentException();
}
return parent;
}
//--------------------------------------------------------------------------
/**
* Iterator
*/
private class ItemInfos implements Iterator {
private final List prefetchQueue = new ArrayList();
private final Map nodeInfos = new HashMap();
private final Iterator infos;
private ItemInfos(Iterator infos) {
super();
this.infos = infos;
}
// ------------------------------------------------------< Iterator >---
/**
* @see Iterator#hasNext()
*/
public boolean hasNext() {
if (!prefetchQueue.isEmpty()) {
return true;
} else {
return prefetch();
}
}
/**
* @see Iterator#next()
*/
public Object next() {
if (prefetchQueue.isEmpty()) {
throw new NoSuchElementException();
} else {
Object next = prefetchQueue.remove(0);
if (next instanceof NodeInfo) {
nodeInfos.remove(((NodeInfo) next).getId());
}
return next;
}
}
/**
* @see Iterator#remove()
*/
public void remove() {
throw new UnsupportedOperationException();
}
// -------------------------------------------------------< private >---
/**
* @param parentId
* @return The children <code>NodeInfo</code>s for the parent identified
* by the given <code>parentId</code> or <code>null</code> if the parent
* has not been read yet, has already been processed (childInfo is up
* to date) or does not provide child infos.
*/
private Iterator getChildInfos(NodeId parentId) {
NodeInfo nodeInfo = (NodeInfo) nodeInfos.get(parentId);
while (nodeInfo == null && prefetch()) {
nodeInfo = (NodeInfo) nodeInfos.get(parentId);
}
return nodeInfo == null? null : nodeInfo.getChildInfos();
}
/**
* @return <code>true</code> if the next info could be retrieved.
*/
private boolean prefetch() {
if (infos.hasNext()) {
ItemInfo info = (ItemInfo) infos.next();
prefetchQueue.add(info);
if (info.denotesNode()) {
NodeInfo nodeInfo = (NodeInfo) info;
nodeInfos.put(nodeInfo.getId(), nodeInfo);
}
return true;
} else {
return false;
}
}
}
}