/*
* (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* 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.api.PropertyException;
import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
import org.nuxeo.ecm.core.storage.StateAccessor;
/**
* A {@code Node} implementation. The actual data is stored in contained objects that are {@link Fragment}s.
*/
public class Node implements StateAccessor {
/** 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) {
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() {
return getHierFragment().getString(model.HIER_CHILD_NAME_KEY);
}
public Long getPos() {
return (Long) getHierFragment().get(model.HIER_CHILD_POS_KEY);
}
public String getPrimaryType() {
return hierFragment.getString(model.MAIN_PRIMARY_TYPE_KEY);
}
public Serializable getParentId() {
return getHierFragment().get(model.HIER_PARENT_KEY);
}
/**
* 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) {
isVersion = (Boolean) getSimpleProperty(model.MAIN_IS_VERSION_PROP).getValue();
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() {
String[] value = (String[]) hierFragment.get(model.MAIN_MIXIN_TYPES_KEY);
return value == null ? NO_MIXINS : value.clone();
}
/**
* 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 PropertyNotFoundException if the name is invalid
*/
public SimpleProperty getSimpleProperty(String name) {
SimpleProperty property = (SimpleProperty) propertyCache.get(name);
if (property == null) {
ModelProperty propertyInfo = getPropertyInfo(name);
if (propertyInfo == null) {
throw new PropertyNotFoundException(name);
}
property = makeSimpleProperty(name, propertyInfo);
propertyCache.put(name, property);
}
return property;
}
protected SimpleProperty makeSimpleProperty(String name, ModelProperty propertyInfo) {
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 PropertyNotFoundException if the name is invalid
*/
public CollectionProperty getCollectionProperty(String name) {
CollectionProperty property = (CollectionProperty) propertyCache.get(name);
if (property == null) {
ModelProperty propertyInfo = getPropertyInfo(name);
if (propertyInfo == null) {
throw new PropertyNotFoundException(name);
}
property = makeCollectionProperty(name, propertyInfo);
propertyCache.put(name, property);
}
return property;
}
protected CollectionProperty makeCollectionProperty(String name, ModelProperty propertyInfo) {
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, Object value) {
SimpleProperty property = getSimpleProperty(name);
property.setValue(value);
}
public void setCollectionProperty(String name, Object[] value) {
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() + ")";
}
@Override
public Object getSingle(String name) throws PropertyException {
return getSimpleProperty(name).getValue();
}
@Override
public Object[] getArray(String name) throws PropertyException {
return getCollectionProperty(name).getValue();
}
@Override
public void setSingle(String name, Object value) throws PropertyException {
getSimpleProperty(name).setValue(value);
}
@Override
public void setArray(String name, Object[] value) throws PropertyException {
getCollectionProperty(name).setValue(value);
}
}