/* * 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(); } }