/*
* Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.storage.sql;
import java.io.Serializable;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections.map.ReferenceMap;
import org.nuxeo.ecm.core.storage.StorageException;
/**
* A {@code Node} implementation. The actual data is stored in contained objects
* that are {@link Fragment}s.
*/
public class Node {
/** The persistence context used. */
private final PersistenceContext context;
private final Model model;
/** The hierarchy/main fragment. */
protected final SimpleFragment hierFragment;
/** Fragment information for each additional mixin or inherited fragment. */
protected final FragmentsMap fragments;
/**
* Path, only for immediate consumption after construction (will be reset to
* null afterwards).
*/
protected String path;
/**
* Cache of property objects already retrieved. They are dumb objects, just
* providing an indirection to an underlying {@link Fragment}.
*/
private final Map<String, BaseProperty> propertyCache;
private Boolean isVersion;
/**
* Creates a Node.
*
* @param context the persistence context
* @param fragmentGroup the group of fragments for the node
* @param path the path, if known at construction time
*/
@SuppressWarnings("unchecked")
protected Node(PersistenceContext context, FragmentGroup fragmentGroup,
String path) throws StorageException {
this.context = context;
model = context.model;
hierFragment = fragmentGroup.hier;
if (fragmentGroup.fragments == null) {
fragments = new FragmentsMap();
} else {
fragments = fragmentGroup.fragments;
}
this.path = path;
// memory-sensitive
propertyCache = new ReferenceMap(ReferenceMap.HARD, ReferenceMap.SOFT);
}
// ----- basics -----
/**
* Gets the node unique id, usually a Long or a String.
*
* @return the node id
*/
public Serializable getId() {
/*
* We don't cache the id as it changes between the initial creation and
* the first save.
*/
return hierFragment.getId();
}
public String getName() {
try {
return getHierFragment().getString(model.HIER_CHILD_NAME_KEY);
} catch (StorageException e) {
// do not propagate this unlikely exception as a checked one
throw new RuntimeException(e);
}
}
public Long getPos() {
try {
return (Long) getHierFragment().get(model.HIER_CHILD_POS_KEY);
} catch (StorageException e) {
// do not propagate this unlikely exception as a checked one
throw new RuntimeException(e);
}
}
public String getPrimaryType() {
try {
return hierFragment.getString(model.MAIN_PRIMARY_TYPE_KEY);
} catch (StorageException e) {
// do not propagate this unlikely exception as a checked one
throw new RuntimeException(e);
}
}
public Serializable getParentId() {
try {
return getHierFragment().get(model.HIER_PARENT_KEY);
} catch (StorageException e) {
// do not propagate this unlikely exception as a checked one
throw new RuntimeException(e);
}
}
/**
* Gets the path that was assigned at {@link Node} construction time. Then
* it's reset to {@code null}. Should only be used once.
*
* @return the path, or {@code null} for unknown
*/
public String getPath() {
String p = path;
if (p != null) {
path = null;
}
return p;
}
protected SimpleFragment getHierFragment() {
return hierFragment;
}
// cache the isVersion computation
public boolean isVersion() {
if (isVersion == null) {
try {
isVersion = (Boolean) getSimpleProperty(
model.MAIN_IS_VERSION_PROP).getValue();
} catch (StorageException e) {
throw new RuntimeException(e);
}
if (isVersion == null) {
isVersion = Boolean.FALSE;
}
}
return isVersion.booleanValue();
}
public boolean isProxy() {
String primaryType = getPrimaryType();
if (primaryType == null) {
throw new NullPointerException(this.toString());
}
return primaryType.equals(model.PROXY_TYPE);
}
private static final String[] NO_MIXINS = {};
/**
* Gets the instance mixins. Mixins from the type are not returned.
* <p>
* Never returns {@code null}.
*/
public String[] getMixinTypes() {
try {
String[] value = (String[]) hierFragment.get(model.MAIN_MIXIN_TYPES_KEY);
return value == null ? NO_MIXINS : value.clone();
} catch (StorageException e) {
throw new RuntimeException(e);
}
}
/**
* Gets the mixins. Includes mixins from the type. Returns a fresh set.
*/
public Set<String> getAllMixinTypes() {
// linked for deterministic result
Set<String> mixins = new LinkedHashSet<String>(
model.getDocumentTypeFacets(getPrimaryType()));
mixins.addAll(Arrays.asList(getMixinTypes()));
return mixins;
}
/**
* Checks the mixins. Includes mixins from the type.
*/
public boolean hasMixinType(String mixin) {
if (model.getDocumentTypeFacets(getPrimaryType()).contains(mixin)) {
return true; // present in type
}
for (String m : getMixinTypes()) {
if (m.equals(mixin)) {
return true; // present in node
}
}
return false;
}
/**
* Clears the properties cache, used when removing mixins.
*/
protected void clearCache() {
// some properties have now become invalid
propertyCache.clear();
}
// ----- properties -----
/**
* Gets a simple property from the node, given its name.
*
* @param name the property name
* @return the property
* @throws IllegalArgumentException if the name is invalid
*/
public SimpleProperty getSimpleProperty(String name)
throws StorageException {
SimpleProperty property = (SimpleProperty) propertyCache.get(name);
if (property == null) {
ModelProperty propertyInfo = getPropertyInfo(name);
if (propertyInfo == null) {
throw new IllegalArgumentException("Unknown field: " + name);
}
property = makeSimpleProperty(name, propertyInfo);
propertyCache.put(name, property);
}
return property;
}
protected SimpleProperty makeSimpleProperty(String name,
ModelProperty propertyInfo) throws StorageException {
String fragmentName = propertyInfo.fragmentName;
Fragment fragment = fragments.get(fragmentName);
if (fragment == null) {
// lazy fragment, fetch from session
RowId rowId = new RowId(fragmentName, getId());
fragment = context.get(rowId, true);
fragments.put(fragmentName, fragment);
}
return new SimpleProperty(name, propertyInfo.propertyType,
propertyInfo.readonly, (SimpleFragment) fragment,
propertyInfo.fragmentKey);
}
/**
* Gets a collection property from the node, given its name.
*
* @param name the property name
* @return the property
* @throws IllegalArgumentException if the name is invalid
*/
public CollectionProperty getCollectionProperty(String name)
throws StorageException {
CollectionProperty property = (CollectionProperty) propertyCache.get(name);
if (property == null) {
ModelProperty propertyInfo = getPropertyInfo(name);
if (propertyInfo == null) {
throw new IllegalArgumentException("Unknown field: " + name);
}
property = makeCollectionProperty(name, propertyInfo);
propertyCache.put(name, property);
}
return property;
}
protected CollectionProperty makeCollectionProperty(String name,
ModelProperty propertyInfo) throws StorageException {
String fragmentName = propertyInfo.fragmentName;
Fragment fragment = fragments.get(fragmentName);
if (fragment == null) {
// lazy fragment, fetch from session
RowId rowId = new RowId(fragmentName, getId());
fragment = context.get(rowId, true);
}
if (fragment instanceof CollectionFragment) {
return new CollectionProperty(name, propertyInfo.propertyType,
propertyInfo.readonly, (CollectionFragment) fragment);
} else {
fragments.put(fragmentName, fragment);
return new CollectionProperty(name, propertyInfo.propertyType,
propertyInfo.readonly, (SimpleFragment) fragment,
propertyInfo.fragmentKey);
}
}
protected ModelProperty getPropertyInfo(String name) {
// check primary type
ModelProperty propertyInfo = model.getPropertyInfo(getPrimaryType(),
name);
if (propertyInfo != null) {
return propertyInfo;
}
// check mixins
for (String mixin : getMixinTypes()) {
propertyInfo = model.getMixinPropertyInfo(mixin, name);
if (propertyInfo != null) {
return propertyInfo;
}
}
// check proxy schemas
if (isProxy()) {
propertyInfo = model.getProxySchemasPropertyInfo(name);
if (propertyInfo != null) {
return propertyInfo;
}
}
return null;
}
public void setSimpleProperty(String name, Serializable value)
throws StorageException {
SimpleProperty property = getSimpleProperty(name);
property.setValue(value);
}
public void setCollectionProperty(String name, Serializable[] value)
throws StorageException {
CollectionProperty property = getCollectionProperty(name);
property.setValue(value);
}
// ----- locking -----
// ----- lifecycle -----
// ----- versioning -----
// ----- activities, baselines, configurations -----
// ----- shared nodes -----
// ----- retention -----
/*
* ----- equals/hashcode -----
*/
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (other instanceof Node) {
return equals((Node) other);
}
return false;
}
private boolean equals(Node other) {
return getId() == other.getId();
}
@Override
public int hashCode() {
return getId().hashCode();
}
@Override
public String toString() {
return getClass().getSimpleName() + "(uuid=" + getId() + ", name="
+ getName() + ", primaryType=" + getPrimaryType()
+ ", parentId=" + getParentId() + ")";
}
}