/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.core.persistence.bundle;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import javax.jcr.PropertyType;
import org.apache.jackrabbit.core.NamespaceRegistryImpl;
import org.apache.jackrabbit.core.NodeId;
import org.apache.jackrabbit.core.PropertyId;
import org.apache.jackrabbit.core.fs.FileSystem;
import org.apache.jackrabbit.core.fs.FileSystemResource;
import org.apache.jackrabbit.core.nodetype.PropDefId;
import org.apache.jackrabbit.core.persistence.PMContext;
import org.apache.jackrabbit.core.persistence.PersistenceManager;
import org.apache.jackrabbit.core.persistence.bundle.util.BundleCache;
import org.apache.jackrabbit.core.persistence.bundle.util.HashMapIndex;
import org.apache.jackrabbit.core.persistence.bundle.util.LRUNodeIdCache;
import org.apache.jackrabbit.core.persistence.bundle.util.NamespaceIndex;
import org.apache.jackrabbit.core.persistence.bundle.util.NodePropBundle;
import org.apache.jackrabbit.core.persistence.bundle.util.StringIndex;
import org.apache.jackrabbit.core.state.ChangeLog;
import org.apache.jackrabbit.core.state.ItemState;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.NoSuchItemStateException;
import org.apache.jackrabbit.core.state.NodeReferences;
import org.apache.jackrabbit.core.state.NodeReferencesId;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.name.QName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>AbstractBundlePersistenceManager</code> acts as base for all
* persistence managers that store the state in a {@link NodePropBundle}.
* <p/>
* The state and all property states of one node are stored together in one
* record. Property values of a certain size can be store outside of the bundle.
* This currently only works for binary properties. NodeReferences are not
* included in the bundle since they are addressed by the target id.
* <p/>
* Some strings like namespaces and local names are additionally managed by
* seperate indexes. only the index number is serialized to the records which
* reduces the amount of memory used.
* <p/>
* Special treatment is performed for the properties "jcr:uuid", "jcr:primaryType"
* and "jcr:mixinTypes". As they are also stored in the node state they are not
* included in the bundle but generated when required.
* <p/>
* In order to increase performance, there are 2 caches maintained. One ist the
* {@link BundleCache} that caches already loaded bundles. The other is the
* {@link LRUNodeIdCache} that caches non-existent bundles. This is usefull
* because a lot of {@link #exists(NodeId)} calls are issued that would result
* in a useless SQL execution if the desired bundle does not exist.
* <p/>
* Configuration:<br>
* <ul>
* <li><param name="{@link #setBundleCacheSize(String) bundleCacheSize}" value="8"/>
* </ul>
*/
abstract public class AbstractBundlePersistenceManager implements
PersistenceManager, CachingPersistenceManager {
/** the cvs/svn id */
static final String CVS_ID = "$URL: http://svn.apache.org/repos/asf/jackrabbit/tags/1.3.4/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/persistence/bundle/AbstractBundlePersistenceManager.java $ $Rev: 632996 $ $Date: 2008-03-03 11:17:36 +0200 (Mon, 03 Mar 2008) $";
/** the default logger */
private static Logger log = LoggerFactory.getLogger(AbstractBundlePersistenceManager.class);
/** the prefix of a node file */
protected static final String NODEFILENAME = "n";
/** the prefix of a node references file */
protected static final String NODEREFSFILENAME = "r";
/** the name of the names-index resource */
protected static final String RES_NAME_INDEX = "/names.properties";
/** the name of the namespace-index resource */
protected static final String RES_NS_INDEX = "/namespaces.properties";
/** the index for namespaces */
private StringIndex nsIndex;
/** the index for local names */
private StringIndex nameIndex;
/** the cache of loaded bundles */
private BundleCache bundles;
/** the cahce of non-existent bundles */
private LRUNodeIdCache missing;
/** definition id of the jcr:uuid property */
private PropDefId ID_JCR_UUID;
/** definition id of the jcr:primaryType property */
private PropDefId ID_JCR_PRIMARYTYPE;
/** definition id of the jcr:mixinTypes property */
private PropDefId ID_JCR_MIXINTYPES;
/** the persistence manager context */
protected PMContext context;
/** default size of the bunlde cache */
private long bundleCacheSize = 8 * 1024 * 1024;
/**
* Returns the size of the bundlecache in megabytes.
* @return the size of the bundlecache in megabytes.
*/
public String getBundleCacheSize() {
return String.valueOf(bundleCacheSize/(1024 * 1024));
}
/**
* Sets the size of the bundle cache in megabytes.
* the default is 8.
*
* @param bundleCacheSize the bundle cache size in megabytes.
*/
public void setBundleCacheSize(String bundleCacheSize) {
this.bundleCacheSize = Long.parseLong(bundleCacheSize)*1024*1024;
}
/**
* Creates the folder path for the given node id that is suitable for
* storing states in a filesystem.
*
* @param buf buffer to append to or <code>null</code>
* @param id the id of the node
* @return the buffer with the appended data.
*/
protected StringBuffer buildNodeFolderPath(StringBuffer buf, NodeId id) {
if (buf == null) {
buf = new StringBuffer();
}
char[] chars = id.getUUID().toString().toCharArray();
int cnt = 0;
for (int i = 0; i < chars.length; i++) {
if (chars[i] == '-') {
continue;
}
//if (cnt > 0 && cnt % 4 == 0) {
if (cnt == 2 || cnt == 4) {
buf.append(FileSystem.SEPARATOR_CHAR);
}
buf.append(chars[i]);
cnt++;
}
return buf;
}
/**
* Creates the folder path for the given property id that is suitable for
* storing states in a filesystem.
*
* @param buf buffer to append to or <code>null</code>
* @param id the id of the property
* @return the buffer with the appended data.
*/
protected StringBuffer buildPropFilePath(StringBuffer buf, PropertyId id) {
if (buf == null) {
buf = new StringBuffer();
}
buildNodeFolderPath(buf, id.getParentId());
buf.append(FileSystem.SEPARATOR);
buf.append(getNsIndex().stringToIndex(id.getName().getNamespaceURI()));
buf.append('.');
buf.append(getNameIndex().stringToIndex(id.getName().getLocalName()));
return buf;
}
/**
* Creates the file path for the given property id and value index that is
* suitable for storing property values in a filesystem.
*
* @param buf buffer to append to or <code>null</code>
* @param id the id of the property
* @param i the index of the property value
* @return the buffer with the appended data.
*/
protected StringBuffer buildBlobFilePath(StringBuffer buf, PropertyId id,
int i) {
if (buf == null) {
buf = new StringBuffer();
}
buildPropFilePath(buf, id);
buf.append('.');
buf.append(i);
return buf;
}
/**
* Creates the file path for the given node id that is
* suitable for storing node states in a filesystem.
*
* @param buf buffer to append to or <code>null</code>
* @param id the id of the node
* @return the buffer with the appended data.
*/
protected StringBuffer buildNodeFilePath(StringBuffer buf, NodeId id) {
if (buf == null) {
buf = new StringBuffer();
}
buildNodeFolderPath(buf, id);
buf.append(FileSystem.SEPARATOR);
buf.append(NODEFILENAME);
return buf;
}
/**
* Creates the file path for the given references id that is
* suitable for storing reference states in a filesystem.
*
* @param buf buffer to append to or <code>null</code>
* @param id the id of the node
* @return the buffer with the appended data.
*/
protected StringBuffer buildNodeReferencesFilePath(StringBuffer buf,
NodeReferencesId id) {
if (buf == null) {
buf = new StringBuffer();
}
buildNodeFolderPath(buf, id.getTargetId());
buf.append(FileSystem.SEPARATOR);
buf.append(NODEREFSFILENAME);
return buf;
}
/**
* Returns the namespace index
* @return the namespace index
* @throws IllegalStateException if an error occurs.
*/
public StringIndex getNsIndex() {
try {
if (nsIndex == null) {
// load name and ns index
FileSystemResource nsFile = new FileSystemResource(context.getFileSystem(), RES_NS_INDEX);
if (nsFile.exists()) {
nsIndex = new HashMapIndex(nsFile);
} else {
nsIndex = new NamespaceIndex((NamespaceRegistryImpl) context.getNamespaceRegistry());
}
}
return nsIndex;
} catch (Exception e) {
throw new IllegalStateException("Unable to create nsIndex." + e);
}
}
/**
* Returns the local name index
* @return the local name index
* @throws IllegalStateException if an error occurs.
*/
public StringIndex getNameIndex() {
try {
if (nameIndex == null) {
nameIndex = new HashMapIndex(new FileSystemResource(context.getFileSystem(), RES_NAME_INDEX));
}
return nameIndex;
} catch (Exception e) {
throw new IllegalStateException("Unable to create nsIndex." + e);
}
}
//-----------------------------------------< CacheablePersistenceManager >--
/**
* {@inheritDoc}
*/
public synchronized void onExternalUpdate(ChangeLog changes) {
Iterator iter = changes.modifiedStates();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
if (state.isNode()) {
bundles.remove((NodeId) state.getId());
} else {
bundles.remove(state.getParentId());
}
}
iter = changes.deletedStates();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
if (state.isNode()) {
bundles.remove((NodeId) state.getId());
} else {
bundles.remove(state.getParentId());
}
}
iter = changes.addedStates();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
if (state.isNode()) {
missing.remove((NodeId) state.getId());
} else {
missing.remove(state.getParentId());
}
}
}
//----------------------------------------------------------------< spi >---
/**
* Loads a bundle from the underlying system.
*
* @param id the node id of the bundle
* @return the loaded bunlde or <code>null</code> if the bundle does not
* exist.
* @throws ItemStateException if an error while loading occurs.
*/
protected abstract NodePropBundle loadBundle(NodeId id)
throws ItemStateException;
/**
* Checks if a bundle exists in the underlying system.
*
* @param id the node id of the bundle
* @return <code>true</code> if the bundle exists;
* <code>false</code> otherwise.
* @throws ItemStateException if an error while checking occurs.
*/
protected abstract boolean existsBundle(NodeId id)
throws ItemStateException;
/**
* Stores a bundle to the underlying system.
*
* @param bundle the bundle to store
* @throws ItemStateException if an error while storing occurs.
*/
protected abstract void storeBundle(NodePropBundle bundle)
throws ItemStateException;
/**
* Deletes the bundle from the underlying system.
*
* @param bundle the bundle to destroy
*
* @throws ItemStateException if an error while destroying occurs.
*/
protected abstract void destroyBundle(NodePropBundle bundle)
throws ItemStateException;
/**
* {@inheritDoc}
*/
abstract public NodeReferences load(NodeReferencesId targetId)
throws NoSuchItemStateException, ItemStateException;
/**
* Deletes the node references from the undelying system.
*
* @param refs the node references to destroy.
* @throws ItemStateException if an error while destroying occurs.
*/
protected abstract void destroy(NodeReferences refs)
throws ItemStateException;
/**
* Stores a node references to the underlying system.
*
* @param refs the node references to store.
* @throws ItemStateException if an error while storing occurs.
*/
protected abstract void store(NodeReferences refs)
throws ItemStateException;
//-------------------------------------------------< PersistenceManager >---
/**
* {@inheritDoc}
*
* Initializes the internal structures of this abstract persistence manager.
*/
public void init(PMContext context) throws Exception {
this.context = context;
// init bundle cache
bundles = new BundleCache(bundleCacheSize);
missing = new LRUNodeIdCache();
// init prop defs
if (context.getNodeTypeRegistry() != null) {
ID_JCR_UUID = context.getNodeTypeRegistry().getEffectiveNodeType(QName.MIX_REFERENCEABLE).getApplicablePropertyDef(
QName.JCR_UUID, PropertyType.STRING, false).getId();
ID_JCR_PRIMARYTYPE = context.getNodeTypeRegistry().getEffectiveNodeType(QName.NT_BASE).getApplicablePropertyDef(
QName.JCR_PRIMARYTYPE, PropertyType.NAME, false).getId();
ID_JCR_MIXINTYPES = context.getNodeTypeRegistry().getEffectiveNodeType(QName.NT_BASE).getApplicablePropertyDef(
QName.JCR_MIXINTYPES, PropertyType.NAME, true).getId();
}
}
/**
* {@inheritDoc}
*
* Loads the state via the appropriate NodePropBundle.
*/
public synchronized NodeState load(NodeId id)
throws NoSuchItemStateException, ItemStateException {
NodePropBundle bundle = getBundle(id);
if (bundle == null) {
throw new NoSuchItemStateException(id.toString());
}
return bundle.createNodeState(this);
}
/**
* {@inheritDoc}
*
* Loads the state via the appropriate NodePropBundle.
*/
public synchronized PropertyState load(PropertyId id)
throws NoSuchItemStateException, ItemStateException {
NodePropBundle bundle = getBundle(id.getParentId());
if (bundle == null) {
throw new NoSuchItemStateException(id.toString());
}
PropertyState state = bundle.createPropertyState(this, id.getName());
if (state == null) {
// check if autocreated property state
if (id.getName().equals(QName.JCR_UUID)) {
state = createNew(id);
state.setType(PropertyType.STRING);
state.setDefinitionId(ID_JCR_UUID);
state.setMultiValued(false);
state.setValues(new InternalValue[]{InternalValue.create(id.getParentId().getUUID().toString())});
} else if (id.getName().equals(QName.JCR_PRIMARYTYPE)) {
state = createNew(id);
state.setType(PropertyType.NAME);
state.setDefinitionId(ID_JCR_PRIMARYTYPE);
state.setMultiValued(false);
state.setValues(new InternalValue[]{InternalValue.create(bundle.getNodeTypeName())});
} else if (id.getName().equals(QName.JCR_MIXINTYPES)) {
Set mixins = bundle.getMixinTypeNames();
state = createNew(id);
state.setType(PropertyType.NAME);
state.setDefinitionId(ID_JCR_MIXINTYPES);
state.setMultiValued(true);
state.setValues(InternalValue.create((QName[]) mixins.toArray(new QName[mixins.size()])));
} else {
throw new NoSuchItemStateException(id.toString());
}
bundle.addProperty(state);
}
return state;
}
/**
* {@inheritDoc}
*
* Loads the state via the appropriate NodePropBundle.
*/
public synchronized boolean exists(PropertyId id) throws ItemStateException {
NodePropBundle bundle = getBundle(id.getParentId());
return bundle !=null && bundle.hasProperty(id.getName());
}
/**
* {@inheritDoc}
*
* Checks the existance via the appropriate NodePropBundle.
*/
public synchronized boolean exists(NodeId id) throws ItemStateException {
// anticipating a load followed by a exists
return getBundle(id) != null;
}
/**
* {@inheritDoc}
*/
public NodeState createNew(NodeId id) {
return new NodeState(id, null, null, NodeState.STATUS_NEW, false);
}
/**
* {@inheritDoc}
*/
public PropertyState createNew(PropertyId id) {
return new PropertyState(id, PropertyState.STATUS_NEW, false);
}
/**
* Right now, this iterates over all items in the changelog and
* calls the individual methods that handle single item states
* or node references objects. Properly implemented, this method
* should ensure that changes are either written completely to
* the underlying persistence layer, or not at all.
*
* {@inheritDoc}
*/
public synchronized void store(ChangeLog changeLog)
throws ItemStateException {
// delete bundles
HashSet deleted = new HashSet();
Iterator iter = changeLog.deletedStates();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
if (state.isNode()) {
NodePropBundle bundle = getBundle((NodeId) state.getId());
if (bundle == null) {
throw new NoSuchItemStateException(state.getId().toString());
}
deleteBundle(bundle);
deleted.add(state.getId());
}
}
// gather added node states
HashMap modified = new HashMap();
iter = changeLog.addedStates();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
if (state.isNode()) {
NodePropBundle bundle = new NodePropBundle((NodeState) state);
modified.put(state.getId(), bundle);
}
}
// gather modified node states
iter = changeLog.modifiedStates();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
if (state.isNode()) {
NodeId nodeId = (NodeId) state.getId();
NodePropBundle bundle = (NodePropBundle) modified.get(nodeId);
if (bundle == null) {
bundle = getBundle(nodeId);
if (bundle == null) {
throw new NoSuchItemStateException(nodeId.toString());
}
modified.put(nodeId, bundle);
}
bundle.update((NodeState) state);
} else {
PropertyId id = (PropertyId) state.getId();
// skip primaryType pr mixinTypes properties
if (id.getName().equals(QName.JCR_PRIMARYTYPE)
|| id.getName().equals(QName.JCR_MIXINTYPES)
|| id.getName().equals(QName.JCR_UUID)) {
continue;
}
NodeId nodeId = id.getParentId();
NodePropBundle bundle = (NodePropBundle) modified.get(nodeId);
if (bundle == null) {
bundle = getBundle(nodeId);
if (bundle == null) {
throw new NoSuchItemStateException(nodeId.toString());
}
modified.put(nodeId, bundle);
}
bundle.addProperty((PropertyState) state);
}
}
// add removed properties
iter = changeLog.deletedStates();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
if (state.isNode()) {
// check consistency
NodeId parentId = state.getParentId();
if (!modified.containsKey(parentId) && !deleted.contains(parentId)) {
log.warn("Deleted node state's parent is not modified or deleted: " + parentId + "/" + state.getId());
}
} else {
PropertyId id = (PropertyId) state.getId();
NodeId nodeId = id.getParentId();
if (!deleted.contains(nodeId)) {
NodePropBundle bundle = (NodePropBundle) modified.get(nodeId);
if (bundle == null) {
// should actually not happen
log.warn("deleted property state's parent not modified!");
bundle = getBundle(nodeId);
if (bundle == null) {
throw new NoSuchItemStateException(nodeId.toString());
}
modified.put(nodeId, bundle);
}
bundle.removeProperty(id.getName());
}
}
}
// add added properties
iter = changeLog.addedStates();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
if (!state.isNode()) {
PropertyId id = (PropertyId) state.getId();
// skip primaryType pr mixinTypes properties
if (id.getName().equals(QName.JCR_PRIMARYTYPE)
|| id.getName().equals(QName.JCR_MIXINTYPES)
|| id.getName().equals(QName.JCR_UUID)) {
continue;
}
NodeId nodeId = id.getParentId();
NodePropBundle bundle = (NodePropBundle) modified.get(nodeId);
if (bundle == null) {
// should actually not happen
log.warn("added property state's parent not modified!");
bundle = getBundle(nodeId);
if (bundle == null) {
throw new NoSuchItemStateException(nodeId.toString());
}
modified.put(nodeId, bundle);
}
bundle.addProperty((PropertyState) state);
}
}
// now store all modified bundles
iter = modified.values().iterator();
while (iter.hasNext()) {
NodePropBundle bundle = (NodePropBundle) iter.next();
putBundle(bundle);
}
// store the refs
iter = changeLog.modifiedRefs();
while (iter.hasNext()) {
NodeReferences refs = (NodeReferences) iter.next();
if (refs.hasReferences()) {
store(refs);
} else {
destroy(refs);
}
}
}
/**
* Gets the bundle for the given nodeid.
*
* @param id the id of the bundle to retrieve.
* @return the bundle or <code>null</code> if the bunlde does not exist
*
* @throws ItemStateException if an error occurs.
*/
private NodePropBundle getBundle(NodeId id) throws ItemStateException {
if (missing.contains(id)) {
return null;
}
NodePropBundle bundle = bundles.get(id);
if (bundle == null) {
bundle = loadBundle(id);
if (bundle != null) {
bundle.markOld();
bundles.put(bundle);
} else {
missing.put(id);
}
}
return bundle;
}
/**
* Deletes the bundle
*
* @param bundle the bundle to delete
* @throws ItemStateException if an error occurs
*/
private void deleteBundle(NodePropBundle bundle) throws ItemStateException {
destroyBundle(bundle);
bundles.remove(bundle.getId());
missing.put(bundle.getId());
}
/**
* Stores the bundle and puts it to the cache.
*
* @param bundle the bundle to store
* @throws ItemStateException if an error occurs
*/
private void putBundle(NodePropBundle bundle) throws ItemStateException {
storeBundle(bundle);
bundle.markOld();
log.debug("stored bundle " + bundle.getId());
missing.remove(bundle.getId());
// only put to cache if already exists. this is to ensure proper overwrite
// and not creating big contention during bulk loads
if (bundles.contains(bundle.getId())) {
bundles.put(bundle);
}
}
/**
* This implementation does nothing.
*
* {@inheritDoc}
*/
public void checkConsistency(String[] uuids, boolean recursive, boolean fix) {
}
}