/*
* 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.eclipse.ecr.core.storage.sql;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.collections.map.ReferenceMap;
import org.eclipse.ecr.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. */
private final FragmentsMap fragments;
/**
* 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
*/
@SuppressWarnings("unchecked")
protected Node(PersistenceContext context, FragmentGroup fragmentGroup)
throws StorageException {
this.context = context;
model = context.model;
hierFragment = fragmentGroup.hier;
if (fragmentGroup.fragments == null) {
fragments = new FragmentsMap();
} else {
fragments = fragmentGroup.fragments;
}
// 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 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 String getParentId() {
try {
return getHierFragment().getString(model.HIER_PARENT_KEY);
} catch (StorageException e) {
// do not propagate this unlikely exception as a checked one
throw new RuntimeException(e);
}
}
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() {
return getPrimaryType().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;
}
/**
* Adds a mixin.
*/
public boolean addMixinType(String mixin) {
if (model.getMixinPropertyInfos(mixin) == null) {
throw new IllegalArgumentException("No such mixin: " + mixin);
}
if (model.getDocumentTypeFacets(getPrimaryType()).contains(mixin)) {
return false; // already present in type
}
List<String> list = new ArrayList<String>(Arrays.asList(getMixinTypes()));
if (list.contains(mixin)) {
return false; // already present in node
}
list.add(mixin);
try {
String[] mixins = list.toArray(new String[list.size()]);
hierFragment.put(model.MAIN_MIXIN_TYPES_KEY, mixins);
} catch (StorageException e) {
throw new RuntimeException(e);
}
return true;
}
/**
* Removes a mixin.
*/
public boolean removeMixinType(String mixin) {
List<String> list = new ArrayList<String>(Arrays.asList(getMixinTypes()));
if (!list.remove(mixin)) {
return false; // not present in node
}
try {
String[] mixins = list.toArray(new String[list.size()]);
if (mixins.length == 0) {
mixins = null;
}
hierFragment.put(model.MAIN_MIXIN_TYPES_KEY, mixins);
clearMixinValues(mixin);
} catch (StorageException e) {
throw new RuntimeException(e);
}
return true;
}
protected void clearMixinValues(String mixin) throws StorageException {
for (Entry<String, ModelProperty> en : model.getMixinPropertyInfos(
mixin).entrySet()) {
String name = en.getKey();
if (getPropertyInfo(name) != null) {
// don't clear if still exists in primary type or other
// mixins
continue;
}
ModelProperty propertyInfo = en.getValue();
if (propertyInfo.propertyType.isArray()) {
makeCollectionProperty(name, propertyInfo).setValue(null);
} else {
makeSimpleProperty(name, propertyInfo).setValue(null);
}
}
propertyCache.clear(); // some properties have now become invalid
// TODO optim: delete rows if all null
}
// ----- 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;
RowId rowId = new RowId(fragmentName, getId());
Fragment fragment = context.get(rowId, true);
CollectionProperty property = new CollectionProperty(name,
propertyInfo.propertyType, false, (CollectionFragment) fragment);
return property;
}
public BaseProperty getProperty(String name) throws StorageException {
BaseProperty property = propertyCache.get(name);
if (property != null) {
return property;
}
ModelProperty propertyInfo = getPropertyInfo(name);
if (propertyInfo == null) {
throw new IllegalArgumentException("Unknown field: " + name);
}
if (propertyInfo.propertyType.isArray()) {
return getCollectionProperty(name);
} else {
return getSimpleProperty(name);
}
}
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;
}
}
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();
}
}