blob: 1f40818f3b775f3e934299cc7a3962091671576b [file] [log] [blame]
/*
* 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.sis.metadata;
import java.util.Set;
import java.util.Map;
import java.util.List;
import java.util.Iterator;
import java.util.Collection;
import java.util.Objects;
import java.util.NoSuchElementException;
import java.util.ConcurrentModificationException;
import java.util.function.Function;
import org.opengis.annotation.Obligation;
import org.apache.sis.xml.NilReason;
import org.apache.sis.xml.bind.lan.LocaleAndCharset;
import org.apache.sis.util.Debug;
import org.apache.sis.util.Classes;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ObjectConverters;
import org.apache.sis.util.privy.CollectionsExt;
import org.apache.sis.util.privy.Unsafe;
import org.apache.sis.util.iso.Types;
import org.apache.sis.util.collection.TableColumn;
import org.apache.sis.util.collection.TreeTable.Node;
import org.apache.sis.util.collection.CheckedContainer;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
/**
* A node in a {@link TreeTableView} view. The {@code TreeNode} base class is used directly only for the root node,
* or for nodes containing a fixed value instead of a value fetched from the metadata object. For all other nodes,
* the actual node class shall be either {@link Element} or {@link CollectionElement}.
*
* <p>The value of a node is extracted from the {@linkplain #metadata} object by {@link #getUserObject()}.
* For each instance of {@code TreeNode}, that value is always a singleton, never a collection.
* If a metadata property is a collection, then there is an instance of the {@link CollectionElement}
* subclass for each element in the collection.</p>
*
* <p>The {@link #newChild()} operation is supported if the node is not a leaf. The user shall
* set the identifier and the value, in that order, before any other operation on the new child.
* See {@code newChild()} javadoc for an example.</p>
*
* <h2>API note</h2>
* This class is not serializable because the values of the {@link Element#indexInData}
* and {@link CollectionElement#indexInList} fields may not be stable.
* The former may be invalid if the node is serialized and deserialized by two different versions of Apache SIS
* having properties in different order. The second may be invalid if the collection is not guaranteed to preserve
* order on serialization (e.g. {@code CodeListSet} with user supplied elements, in which case the elements order
* depends on the instantiation order).</div>
*
* @author Martin Desruisseaux (Geomatys)
*/
class TreeNode implements Node {
/**
* The collection of {@linkplain #children} to return when the node does not allow children.
* This constant is also used as a sentinel value by {@link #isLeaf()}.
*
* <p>We choose an empty set instead of an empty list because {@link TreeNodeChildren}
* does not implement the {@link List} interface. So we are better to never give to the user
* a collection implementing {@code List} in order to signal incorrect casts sooner.</p>
*/
private static final Collection<Node> LEAF = Set.of();
/**
* The table for which this node is an element.
* Contains information like the metadata standard and the value existence policy.
*
* <p>All {@code TreeNode} instances in the same tree have
* a reference to the same {@code TreeTableView} instance.</p>
*/
final TreeTableView table;
/**
* The parent of this node to be returned by {@link #getParent()},
* or {@code null} if this node is the root of the tree.
*
* @see #getParent()
*/
private final TreeNode parent;
/**
* The metadata object from which the {@link #getUserObject()} method will fetch the value.
* The value is fetched in different ways, which depend on the {@code TreeNode} subclass:
*
* <ul>
* <li>For {@code TreeNode} (the root of the tree),
* the value is directly {@link #metadata}.</li>
* <li>For {@link Element} (a metadata property which is not a collection),
* the value is {@code accessor.get(indexInData, metadata)}.</li>
* <li>For {@link CollectionElement} (an element in a collection),
* another index is used for fetching the element in that collection.</li>
* </ul>
*
* This field shall never be null.
*
* @see #getUserObject()
*/
final Object metadata;
/**
* The return type of the getter method that provides the value encapsulated by this node.
* This information is used for filtering aspects when a class opportunistically implements
* many interfaces. This value is part of the {@link CacheKey} needed for invoking
* {@link MetadataStandard} methods.
*/
final Class<?> baseType;
/**
* The value of {@link TableColumn#NAME}, computed by {@link #getName()} then cached.
*
* @see #getName()
*/
private transient CharSequence name;
/**
* The children of this node, or {@code null} if not yet computed. If and only if the node
* cannot have children (i.e. {@linkplain #isLeaf() is a leaf}), then this field is set to
* {@link #LEAF}.
*
* @see #getChildren()
*/
private transient Collection<Node> children;
/**
* The value which existed when the {@link TreeNodeChildren#iterator()} traversed this node.
* This value is cached on the assumption that users will ask for value or for children soon
* after they iterated over this node. The cached value is cleared after its first use.
*
* <p>This value shall be either {@code null}, or the exact same value as what a call to
* {@link #getUserObject()} would return, assuming that the underlying {@linkplain #metadata}
* object didn't changed.</p>
*
* <p>The purpose of this cache is to avoid invoking (by reflection) the same getter methods
* twice in common situations like the {@link TreeTableView#toString()} implementation or in
* Graphical User Interface. However, we may remove this field in any future SIS version if
* experience shows that it is more problematic than helpful.</p>
*
* @see #getNonNilValue()
*/
transient Object cachedValue;
/**
* Whether {@link #cachedValue} can be used for the value of {@link TableColumn#VALUE}.
* This flag is set to {@code true} only by the {@link TreeNodeChildren} iterator,
* thus allowing the use of cached value in the {@code VALUE} column only after
* a call to {@link Iterator#next()} (for opportunistic reason), and only once.
* This restriction does not apply to {@link MetadataColumn#NIL_REASON}.
*/
transient boolean canUseCache;
/**
* Creates the root node of a new metadata tree table.
*
* @param table the table which is creating this root node.
* @param metadata the root metadata object (cannot be null).
* @param baseType the return type of the getter method that provides the value encapsulated by this node.
*/
TreeNode(final TreeTableView table, final Object metadata, final Class<?> baseType) {
this.table = table;
this.parent = null;
this.metadata = metadata;
this.baseType = baseType;
}
/**
* Creates a new child for an element of the given metadata.
* This constructor is for the {@link Element} subclass only.
*
* @param parent the parent of this node.
* @param metadata the metadata object for which this node will be a value.
* @param baseType the return type of the getter method that provides the value encapsulated by this node.
*/
private TreeNode(final TreeNode parent, final Object metadata, final Class<?> baseType) {
this.table = parent.table;
this.parent = parent;
this.metadata = metadata;
this.baseType = baseType;
if (!isMetadata(baseType)) {
children = LEAF;
}
}
/**
* Returns {@code true} if nodes for values of the given type can be expanded with more children.
* A return value of {@code false} means that values of the given type are leaves.
*/
final boolean isMetadata(final Class<?> type) {
return table.standard.isMetadata(type);
}
/**
* Returns the key to use for calls to {@link MetadataStandard} methods.
* This key is used only for some default method implementations in the root node;
* children will use the class of their node value instead.
*/
private CacheKey key() {
return new CacheKey(metadata.getClass(), baseType);
}
/**
* Appends an identifier for this node in the given buffer, for {@link #toString()} implementation.
* The appended value is similar to the value returned by {@link #getIdentifier()} (except for the
* root node), but may contains additional information like the index in a collection.
*
* <p>The default implementation is suitable only for the root node - subclasses must override.</p>
*
* @param buffer the buffer where to complete the {@link #toString()} representation.
*/
@Debug
void appendIdentifier(final StringBuilder buffer) {
buffer.append(Classes.getShortClassName(metadata));
}
/**
* Returns the UML identifier defined by the standard. The default implementation is suitable
* only for the root node, since it returns the class identifier. Subclasses must override in
* order to return the property identifier instead.
*/
String getIdentifier() {
final Class<?> type = table.standard.getInterface(key());
final String id = Types.getStandardName(type);
return (id != null) ? id : Classes.getShortName(type);
}
/**
* Returns the index in the collection if the metadata property type is a collection,
* or {@code null} otherwise. The (<var>identifier</var>, <var>index</var>) pair can
* be used as a primary key for identifying this node among its siblings.
*/
Integer getIndex() {
return null;
}
/**
* Gets the human-readable name of this node. The name shall be stable, since it will be cached
* by the caller. The name typically contains {@linkplain #getIdentifier() identifier} and
* {@linkplain #getIndex() index} information, eventually localized.
*
* <p>The default implementation is suitable only for the root node - subclasses must override.</p>
*/
CharSequence getName() {
return CharSequences.camelCaseToSentence(Classes.getShortName(
table.standard.getInterface(key()))).toString();
}
/**
* Gets whether the property is mandatory, optional or conditional, or {@code null} if unspecified.
*/
Obligation getObligation() {
return null;
}
/**
* Gets remarks about the value in this node, or {@code null} if none.
*/
CharSequence getRemarks() {
return null;
}
/**
* Gets the reason why the value is missing, or {@code null} if unspecified.
* Note that this method is expected to always return {@code null} if
* {@link ValueExistencePolicy#acceptNilValues()} is {@code false}.
*
* @see #setNilReason(NilReason)
*/
NilReason getNilReason() {
return null;
}
/**
* Returns the property value, excluding nil value and using the cached value if available.
* Nil value are excluded because the reason why they are nil is reported in a separated column.
*
* <h4>Caching</h4>
* The cached value is set by {@link TreeNodeChildren} iterator and used only once for
* the value in {@link TableColumn#VALUE}. However, the cached value may be reused for
* the value in {@link MetadataColumn#NIL_REASON}.
*/
private Object getNonNilValue() {
if (!canUseCache) {
cachedValue = getUserObject();
}
canUseCache = false; // Use the cached value only once after iteration.
if (table.valuePolicy.acceptNilValues() && NilReason.forObject(cachedValue) != null) {
return null;
}
return cachedValue;
}
/**
* The metadata value for this node, to be returned by {@code getValue(TableColumn.VALUE)}.
* The default implementation is suitable only for the root node - subclasses must override.
*/
@Override
public Object getUserObject() {
return metadata;
}
/**
* Sets the metadata value for this node. Subclasses must override this method.
*
* @throws UnsupportedOperationException if the metadata value is not writable.
*/
void setUserObject(final Object value) throws UnsupportedOperationException {
throw new UnsupportedOperationException(unmodifiableCellValue(TableColumn.VALUE));
}
/**
* Sets the value to nil with a reason explaining why the value is nil.
*
* @throws UnsupportedOperationException if the metadata value is not writable.
*/
void setNilReason(final NilReason value) {
throw new UnsupportedOperationException(unmodifiableCellValue(MetadataColumn.NIL_REASON));
}
/**
* Returns {@code true} if the metadata value can be set.
* Subclasses must override this method.
*/
boolean isWritable() {
return false;
}
/**
* Returns {@code true} if the given object is of the same class as this node and contains a reference
* to the same metadata object. Since {@code TreeNode} generates all content from the wrapped metadata,
* this condition should ensure that two equal nodes have the same values and children.
*/
@Override
public boolean equals(final Object other) {
return (other != null) && other.getClass() == getClass()
&& ((TreeNode) other).metadata == metadata
&& ((TreeNode) other).baseType == baseType;
}
/**
* Returns a hash code value for this node.
*/
@Override
public int hashCode() {
return System.identityHashCode(metadata) ^ Objects.hashCode(baseType);
}
/**
* A node for a metadata property value. This class does not store the property value directly.
* Instead, is stores a reference to the metadata object that contains the property values,
* together with the index for fetching the value in that object. That way, the real storage
* objects still the metadata object, which allow {@link TreeTableView} to be a dynamic view.
*
* <p>Instances of this class shall be instantiated only for metadata singletons. If a metadata
* property is a collection, then the {@link CollectionElement} subclass shall be instantiated
* instead.</p>
*/
static class Element extends TreeNode {
/**
* The accessor to use for fetching the property names, types and values from the {@link #metadata} object.
* Note that the reference stored in this field is the same for all siblings.
*/
private final PropertyAccessor accessor;
/**
* The reasons why some mandatory property values are missing.
* Created only if cell values in the "Nil reason" column are requested.
*
* @see #nilReasons()
*/
private transient NilReasonMap nilReasons;
/**
* Index of the value in the {@link #metadata} object to be fetched with the {@link #accessor}.
*/
private final int indexInData;
/**
* If tree node should be wrapped in another object before to be returned, the function performing that wrapping.
* This is used if we want to render a metadata property in a different way than the way implied by JavaBeans.
* The wrapping operation should be cheap because it will be applied every time the user request the node.
*
* <h4>Example</h4>
* The {@code "defaultLocale+otherLocale"} property is represented by {@code Map.Entry<Locale,Charset>} values.
* The nodes created by this class contain those {@code Map.Entry} values, but we want to show them to users as
* as a {@link java.util.Locale} node with a {@link java.nio.charset.Charset} child. This separation is done by
* {@link LocaleAndCharset}.
*/
final Function<TreeNode,Node> decorator;
/**
* Creates a new child for a property of the given metadata at the given index.
*
* @param parent the parent of this node.
* @param metadata the metadata object for which this node will be a value.
* @param accessor accessor to use for fetching the name, type and value.
* @param indexInData index to be given to the accessor for fetching the value.
*/
Element(final TreeNode parent, final Object metadata,
final PropertyAccessor accessor, final int indexInData)
{
super(parent, metadata, accessor.type(indexInData, TypeValuePolicy.ELEMENT_TYPE));
this.accessor = accessor;
this.indexInData = indexInData;
if (SpecialCases.isLocaleAndCharset(accessor, indexInData)) {
decorator = LocaleAndCharset::new;
} else {
decorator = null;
}
}
/**
* Appends an identifier for this node in the given buffer, for {@link #toString()} implementation.
* This method is mostly for debugging purposes and is not used for the tree table node values.
*/
@Debug
@Override
void appendIdentifier(final StringBuilder buffer) {
super.appendIdentifier(buffer);
buffer.append('.').append(accessor.name(indexInData, KeyNamePolicy.JAVABEANS_PROPERTY));
}
/**
* The property identifier to be returned in the {@link TableColumn#IDENTIFIER} cells.
*/
@Override
final String getIdentifier() {
return accessor.name(indexInData, KeyNamePolicy.UML_IDENTIFIER);
}
/**
* Gets the name of this node. Current implementation derives the name from the
* {@link KeyNamePolicy#UML_IDENTIFIER} instead of {@link KeyNamePolicy#JAVABEANS_PROPERTY}
* in order to get the singular form instead of the plural one, because we will create one
* node for each element in a collection.
*
* <p>If the property name is equal, ignoring case, to the simple type name, then this method
* returns the subtype name (<a href="https://issues.apache.org/jira/browse/SIS-298">SIS-298</a>).
* For example, instead of:</p>
*
* <pre class="text">
* Citation
* └─Cited responsible party
* └─Party
* └─Name ……………………………… Jon Smith</pre>
*
* we format:
*
* <pre class="text">
* Citation
* └─Cited responsible party
* └─Individual
* └─Name ……………………………… Jon Smith</pre>
*/
@Override
CharSequence getName() {
String identifier = getIdentifier();
if (identifier.equalsIgnoreCase(Classes.getShortName(baseType))) {
final Object value = getUserObject();
if (value != null) {
Class<?> type = standardSubType(Classes.getLeafInterfaces(value.getClass(), baseType));
if (type != null && type != Void.TYPE) {
identifier = Classes.getShortName(type);
}
}
}
identifier = SpecialCases.rename(identifier); // Hard-coded special case.
return CharSequences.camelCaseToSentence(identifier).toString();
}
/**
* Returns the element of the given array which is both assignable to {@link #baseType} and a member
* of the standard represented by {@link TreeTableView#standard}. If no such type is found, returns
* {@code null}. If more than one type is found, returns the {@link Void#TYPE} sentinel value.
*/
private Class<?> standardSubType(final Class<?>[] subtypes) {
Class<?> type = null;
for (Class<?> c : subtypes) {
if (baseType.isAssignableFrom(c)) {
if (!isMetadata(c)) {
c = standardSubType(c.getInterfaces());
}
if (type == null) {
type = c;
} else if (type != c) {
return Void.TYPE;
}
}
}
return type;
}
/**
* Returns the map of reasons why a mandatory value is missing.
* The map is created only when first needed.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
private NilReasonMap nilReasons() {
if (nilReasons == null) {
nilReasons = new NilReasonMap(metadata, accessor, KeyNamePolicy.UML_IDENTIFIER);
}
return nilReasons;
}
/**
* Sets the value to nil with a reason explaining why the value is nil.
*/
@Override
void setNilReason(final NilReason value) {
cachedValue = null;
canUseCache = false;
nilReasons().setReflectively(indexInData, value);
}
/**
* Gets the reason why the value is missing, or {@code null} if unspecified.
*/
@Override
NilReason getNilReason() {
// Do not check `canUseCache` because it applies to TableColumn.VALUE.
if (cachedValue == null) cachedValue = getUserObject();
return nilReasons().getNilReason(indexInData, cachedValue);
}
/**
* Gets whether the property is mandatory, optional or conditional, or {@code null} if unspecified.
*/
@Override
Obligation getObligation() {
return accessor.obligation(indexInData);
}
/**
* Gets remarks about the value in this node, or {@code null} if none.
*/
@Override
final CharSequence getRemarks() {
return accessor.remarks(indexInData, metadata);
}
/**
* Fetches the node value from the metadata object.
*/
@Override
public Object getUserObject() {
return accessor.get(indexInData, metadata);
}
/**
* Sets the property value for this node.
*/
@Override
void setUserObject(final Object value) {
cachedValue = null;
canUseCache = false;
accessor.set(indexInData, metadata, value, PropertyAccessor.RETURN_NULL);
}
/**
* Returns {@code true} if the metadata is writable.
*/
@Override
final boolean isWritable() {
return accessor.isWritable(indexInData);
}
/**
* Returns {@code true} if the value returned by {@link #getUserObject()}
* should be the same for both nodes.
*/
@Override
public boolean equals(final Object other) {
return super.equals(other) && ((Element) other).indexInData == indexInData;
}
/**
* Returns a hash code value for this node.
*/
@Override
public int hashCode() {
return super.hashCode() ^ (31 * indexInData);
}
}
/**
* A node for an element in a collection. This class needs the iteration order to be stable.
*/
static final class CollectionElement extends Element {
/**
* Index of the element in the collection, in iteration order.
*/
final int indexInList;
/**
* Creates a new node for the given collection element.
*
* @param parent the parent of this node.
* @param metadata the metadata object for which this node will be a value.
* @param accessor accessor to use for fetching the name, type and collection.
* @param indexInData index to be given to the accessor of fetching the collection.
* @param indexInList index of the element in the collection, in iteration order.
*/
CollectionElement(final TreeNode parent, final Object metadata,
final PropertyAccessor accessor, final int indexInData, final int indexInList)
{
super(parent, metadata, accessor, indexInData);
this.indexInList = indexInList;
}
/**
* Appends an identifier for this node in the given buffer, for {@link #toString()} implementation.
* This method is mostly for debugging purposes and is not used for the tree table node values.
*/
@Debug
@Override
void appendIdentifier(final StringBuilder buffer) {
super.appendIdentifier(buffer);
buffer.append('[').append(indexInList).append(']');
}
/**
* Returns the zero-based index of this node in the metadata property.
*/
@Override
Integer getIndex() {
return indexInList;
}
/**
* Appends the index of this property, if there is more than one.
* Index numbering begins at 1, since this name if for human reading.
*/
@Override
CharSequence getName() {
CharSequence name = super.getName();
final int size = CollectionsExt.size(super.getUserObject());
if (size >= 2) {
name = Vocabulary.formatInternational(Vocabulary.Keys.Of_3, name, indexInList+1, size);
}
return name;
}
/**
* Fetches the property value from the metadata object, which is expected to be a collection,
* then fetch the element at the index represented by this node.
*/
@Override
public Object getUserObject() {
final Object collection = super.getUserObject();
final Collection<?> values;
if (collection instanceof Collection<?>) {
values = (Collection<?>) collection;
} else {
/*
* ClassCastException should never happen here unless PropertyAccessor.isCollectionOrMap(…) has
* been modified, in which case there is probably many code to update (not only this method).
*/
values = ((Map<?,?>) collection).entrySet();
}
/*
* If the collection is null or empty but the value existence policy tells
* us that such elements shall be shown, behave as if the collection was a
* singleton containing a null element, in order to make the property
* visible in the tree.
*/
if (indexInList == 0 && table.valuePolicy.substituteByNullElement(values)) {
return null;
}
try {
if (values instanceof List<?>) {
return ((List<?>) values).get(indexInList);
}
final Iterator<?> it = values.iterator();
for (int i=0; i<indexInList; i++) {
it.next(); // Inefficient way to move at the desired index, but hopefully rare.
}
return it.next();
} catch (NullPointerException | IndexOutOfBoundsException | NoSuchElementException e) {
/*
* May happen if the collection for this metadata property changed after the iteration
* in the TreeNodeChildren. Users should not keep TreeNode references instances for a
* long time, but instead iterate again over TreeNodeChildren when needed.
*/
throw new ConcurrentModificationException(e);
}
}
/**
* Sets the property value for this node.
*/
@Override
void setUserObject(Object value) {
cachedValue = null;
canUseCache = false;
final Collection<?> values = (Collection<?>) super.getUserObject();
if (!(values instanceof List<?>)) {
// `setValue(…)` is the public method which invoked this one.
throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnsupportedOperation_1, "setValue"));
}
final Class<?> targetType;
if (values instanceof CheckedContainer<?>) {
/*
* Typically the same as getElementType(), but let be safe
* in case some implementations have stricter requirements.
*/
targetType = ((CheckedContainer<?>) values).getElementType();
} else {
targetType = baseType;
}
value = ObjectConverters.convert(value, targetType);
try {
/*
* Unsafe addition into a collection. In SIS implementation, the collection is
* actually an instance of CheckedCollection, so the check will be performed at
* runtime. However, other implementations could use unchecked collection. We have
* done our best for converting the type above, there is not much more we can do...
*/
Unsafe.set((List<?>) values, indexInList, value);
} catch (IndexOutOfBoundsException e) {
// Same rational as in the getUserObject() method.
throw new ConcurrentModificationException(e);
}
}
/**
* Gets the reason why the value is missing, or {@code null} if unspecified.
* Note that this method gets the nil reason of a specific collection element.
* This is a bit unusual, since nil reasons usually apply to the whole property.
*/
@Override
NilReason getNilReason() {
// Do not check `canUseCache` because it applies to TableColumn.VALUE.
if (cachedValue == null) cachedValue = getUserObject();
return NilReason.forObject(cachedValue);
}
/**
* Sets the value to nil with a reason explaining why the value is nil.
* Note that this method sets the nil reason of a specific collection element.
* This is a bit unusual, since nil reasons usually apply to the whole property.
*/
@Override
void setNilReason(final NilReason value) {
setUserObject(value != null ? value.createNilObject(baseType) : null);
}
/**
* Returns {@code true} if the value returned by {@link #getUserObject()}
* should be the same for both nodes.
*/
@Override
public boolean equals(final Object other) {
return super.equals(other) && ((CollectionElement) other).indexInList == indexInList;
}
/**
* Returns a hash code value for this node.
*/
@Override
public int hashCode() {
return super.hashCode() ^ indexInList;
}
}
// -------- Final methods (defined in terms of above methods only) ----------------------------
/**
* Returns the parent node, or {@code null} if this node is the root of the tree.
*/
@Override
public final Node getParent() {
return parent;
}
/**
* Returns {@code false} if the value is a metadata object (and consequently can have children),
* or {@code true} if the value is not a metadata object.
*/
@Override
public final boolean isLeaf() {
return (children == LEAF);
}
/**
* Returns the children of this node, or an empty set if none.
* Only metadata object can have children.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public final Collection<Node> getChildren() {
/*
* `children` is set to LEAF if an only if the node *cannot* have children,
* in which case we do not need to check for changes in the underlying metadata.
*/
if (!isLeaf()) {
Object value = getNonNilValue();
if (value == null) {
/*
* If there is no value, returns an empty set but *do not* set `children`
* to that set, in order to allow this method to check again the next time
* that this method is invoked.
*/
children = null; // Let GC do its work.
return LEAF;
}
/*
* If there is a value, check if the cached collection is still applicable.
* We verify that the collection is a wrapper for the same metadata object.
* If we need to create a new collection, we know that the property accessor
* exists otherwise the call to `isLeaf()` above would have returned `true`.
*/
if (children == null || ((TreeNodeChildren) children).metadata != value) {
PropertyAccessor accessor = table.standard.getAccessor(new CacheKey(value.getClass(), baseType), true);
children = new TreeNodeChildren(this, value, accessor);
}
}
return children;
}
/**
* Returns a proxy for a new property to be defined in the metadata object.
* The user shall set the identifier and the value, in that order, before
* any other operation on the new child. Example:
*
* {@snippet lang="java" :
* TreeTable.Node node = ...;
* TreeTable.Node child = node.newChild();
* child.setValue(TableColumn.IDENTIFIER, "title");
* child.setValue(TableColumn.VALUE, "Le petit prince");
* // Nothing else to do - node has been added.
* }
*
* Do not keep a reference to the returned node for a long time, since it is only
* a proxy toward the real node to be created once the identifier is known.
*
* @throws UnsupportedOperationException if this node {@linkplain #isLeaf() is a leaf}.
*/
@Override
public final Node newChild() throws UnsupportedOperationException {
if (isLeaf()) {
throw new UnsupportedOperationException(Errors.format(Errors.Keys.NodeIsLeaf_1, this));
}
return new NewChild();
}
/**
* The proxy to be returned by {@link TreeNode#newChild()}.
* User shall not keep a reference to this proxy for a long time.
*/
private final class NewChild implements Node {
/**
* Index in the {@link PropertyAccessor} for the property to be set.
* This index is known only after a value has been specified for the
* {@link TableColumn#IDENTIFIER}.
*/
private int indexInData = -1;
/**
* The real node created after the identifier and the value have been specified.
* All operations will be delegated to that node after it has been determined.
*/
private TreeNode delegate;
/**
* Returns the {@link #delegate} node if non-null, or throw an exception otherwise.
*
* @throws IllegalStateException if the identifier and value columns have not yet been defined.
*/
private TreeNode delegate() throws IllegalStateException {
if (delegate != null) {
return delegate;
}
throw new IllegalStateException(Errors.format(Errors.Keys.MissingValueInColumn_1,
(indexInData < 0 ? TableColumn.IDENTIFIER : TableColumn.VALUE).getHeader()));
}
/**
* Returns all children of the parent node. The new child will be added to that list.
*/
private TreeNodeChildren getSiblings() {
return (TreeNodeChildren) TreeNode.this.getChildren();
}
/**
* If the {@link #delegate} is not yet known, set the identifier or the value.
* After the identifier and value have been specified, delegates to the real node.
*/
@Override
public <V> void setValue(final TableColumn<V> column, final V value) {
if (delegate == null) {
/*
* For the given identifier, get the index in the property accessor.
* This can be done only before the `delegate` is found - after that
* point, the identifier will become unmodifiable.
*/
if (column == TableColumn.IDENTIFIER) {
ArgumentChecks.ensureNonNull("value", value);
indexInData = getSiblings().accessor.indexOf((String) value, true);
return;
}
/*
* Set the value for the property specified by the above identifier,
* then get the `delegate` on the assumption that the new value will
* be added at the end of collection (if the property is a collection).
*/
if (column == TableColumn.VALUE) {
ArgumentChecks.ensureNonNull("value", value);
if (indexInData < 0) {
throw new IllegalStateException(Errors.format(Errors.Keys.MissingValueInColumn_1,
TableColumn.IDENTIFIER.getHeader()));
}
final TreeNodeChildren siblings = getSiblings();
final int indexInList;
if (siblings.isCollectionOrMap(indexInData)) {
indexInList = CollectionsExt.size(siblings.valueAt(indexInData));
} else {
indexInList = -1;
}
if (!siblings.add(indexInData, value)) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.ElementAlreadyPresent_1, value));
}
delegate = siblings.childAt(indexInData, indexInList);
/*
* Do not set `delegate.cachedValue = value`, since `value` may
* have been converted by the setter method to another value.
*/
return;
}
}
delegate().setValue(column, value);
}
/**
* For all operations other than {@code setValue(…)}, delegates to the {@link #delegate} node
* or to some code functionally equivalent.
*
* @throws IllegalStateException if the identifier and value columns have not yet been defined.
*/
@Override public Node getParent() {return TreeNode.this;}
@Override public boolean isLeaf() {return delegate().isLeaf();}
@Override public Collection<Node> getChildren() {return delegate().getChildren();}
@Override public Node newChild() {return delegate().newChild();}
@Override public <V> V getValue(TableColumn<V> column) {return delegate().getValue(column);}
@Override public boolean isEditable(TableColumn<?> column) {return delegate().isEditable(column);}
@Override public Object getUserObject() {return delegate().getUserObject();}
}
/**
* Returns the children if the value policy is {@link ValueExistencePolicy#COMPACT}, or {@code null} otherwise.
*/
private TreeNodeChildren getCompactChildren() {
if (table.valuePolicy == ValueExistencePolicy.COMPACT) {
@SuppressWarnings("LocalVariableHidesMemberVariable")
final Collection<Node> children = getChildren();
if (children instanceof TreeNodeChildren) {
return (TreeNodeChildren) children;
}
}
return null;
}
/**
* Returns the value of this node in the given column, or {@code null} if none. This method verifies
* the {@code column} argument, then delegates to {@link #getName()}, {@link #getUserObject()} or
* other properties.
*/
@Override
public final <V> V getValue(final TableColumn<V> column) {
Object value = null;
ArgumentChecks.ensureNonNull("column", column);
if (column == TableColumn.IDENTIFIER) {
value = getIdentifier();
} else if (column == TableColumn.INDEX) {
value = getIndex();
} else if (column == TableColumn.NAME) {
if (name == null) {
name = getName();
}
value = name;
} else if (column == TableColumn.TYPE) {
@SuppressWarnings("LocalVariableHidesMemberVariable")
final TreeNodeChildren children = getCompactChildren();
if (children == null || (value = children.getParentType()) == null) {
value = baseType;
}
} else if (column == TableColumn.VALUE) {
if (isLeaf()) {
value = getNonNilValue();
} else {
@SuppressWarnings("LocalVariableHidesMemberVariable")
final TreeNodeChildren children = getCompactChildren();
if (children != null) {
value = children.getParentTitle();
}
}
} else if (column == MetadataColumn.OBLIGATION) {
value = getObligation();
} else if (column == MetadataColumn.NIL_REASON) {
value = getNilReason();
} else if (column == TableColumn.REMARKS) {
value = getRemarks();
}
return column.getElementType().cast(value);
}
/**
* Sets the value if the given column is {@link TableColumn#VALUE}. This method verifies
* the {@code column} argument, then delegates to {@link #setUserObject(Object)}.
*
* <p>This method does not accept null value, because setting a singleton property to null
* with {@link ValueExistencePolicy#NON_EMPTY} is equivalent to removing the property, and
* setting a collection element to null is not allowed. Those various behavior are at risk
* of causing confusion, so we are better to never allow null.</p>
*/
@Override
public final <V> void setValue(final TableColumn<V> column, final V value) throws UnsupportedOperationException {
ArgumentChecks.ensureNonNull("column", column);
if (column == TableColumn.VALUE) {
ArgumentChecks.ensureNonNull("value", value); // See javadoc.
@SuppressWarnings("LocalVariableHidesMemberVariable")
final TreeNodeChildren children = getCompactChildren();
if (children == null || !(children.setParentTitle(value))) {
setUserObject(value);
}
} else if (column == MetadataColumn.NIL_REASON) {
setNilReason((NilReason) value);
} else if (table.getColumns().contains(column)) {
throw new UnsupportedOperationException(unmodifiableCellValue(column));
} else {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "column", column));
}
}
/**
* Returns the error message for an unmodifiable cell value in the given column.
*/
private String unmodifiableCellValue(final TableColumn<?> column) {
return Errors.format(Errors.Keys.UnmodifiableCellValue_2, getValue(TableColumn.NAME), column.getHeader());
}
/**
* Returns {@code true} if the given column is {@link TableColumn#VALUE} and the property is writable,
* or {@code false} in all other cases. This method verifies the {@code column} argument, then delegates
* to {@link #isWritable()}.
*/
@Override
public final boolean isEditable(final TableColumn<?> column) {
ArgumentChecks.ensureNonNull("column", column);
return (column == TableColumn.VALUE) && isWritable();
}
/**
* Returns a string representation of this node for debugging purpose.
*/
@Override
public final String toString() {
final StringBuilder buffer = new StringBuilder(60);
appendStringTo(buffer);
return buffer.toString();
}
/**
* Implementation of {@link #toString()} appending the string representation in the given buffer.
* This method is mostly for debugging purposes and is not used for the tree table node values.
*/
@Debug
final void appendStringTo(final StringBuilder buffer) {
appendIdentifier(buffer.append("Node["));
buffer.append(" : ").append(Classes.getShortName(baseType)).append(']');
}
}