com.memority.citadel.shared.api.im.AttributeValue Maven / Gradle / Ivy
Show all versions of citadel-api Show documentation
/*
* Copyright (c) 2016-2023 Memority. All Rights Reserved.
*
* This file is part of Memority Citadel API , a Memority project.
*
* This file is released under the Memority Public Artifacts End-User License Agreement,
* see
* Unauthorized copying of this file, via any medium is strictly prohibited.
*/
package com.memority.citadel.shared.api.im;
import com.fasterxml.jackson.annotation.*;
import lombok.EqualsAndHashCode;
import org.apache.commons.lang3.Validate;
import javax.xml.bind.annotation.*;
import java.io.Serializable;
import java.util.*;
import java.util.function.Function;
import static java.util.stream.Collectors.toList;
/**
* This represents an Attribute Value, either mono-valued or multi-valued.
*
* An attribute would typically have at least one value, although it is not mandatory, and as such an
* "empty" {@link AttributeValue} may be valid. However, all AttributeValue instances obtained from the API
* are guaranteed to contain a value and a call to {@link #hasValue()} may be omitted.
*
* Attributes cannot have null
values. Any attempt to pass a null
value
* as one of the Attribute Values will simply result in the value being filtered out.
*
* The "multi-valued" state of an {@link AttributeValue} object may be unknown in some cases, which is why
* it is encoded in the {@link MultiValuedState} enum. However, and again, all AttributeValue instances obtained from the API
* are guaranteed to contain either {@link MultiValuedState#MONO} or {@link MultiValuedState#MULTI}.
*
* An AttributeValue serializes in JSON to a simple Object with 2 properties: id
and values
.
* The multi-valued information is lost on purpose and the values
property uses the following
* semantics:
*
* - not present => forbidden
* null
=> forbidden
* - empty array => the attribute has no value (this is the canonical formalism)
* - single item => for a mono-valued attribute, this is the only attribute value
* - multiple items => these are the values of multi-valued attribute
*
*/
@EqualsAndHashCode(of = {"id", "values"})
@JsonPropertyOrder({"id", "multiValuedState", "values"})
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "AttributeValueType")
public class AttributeValue implements Serializable {
/**
* The Attribute multi-value information. In some circumstance we don't know for a fact whether or not
* an attribute is multivalued, hence the UNKNOWN state. When in doubt, treat as a multi-valued attribute.
*/
@XmlType(name = "MultiValuedStateType")
public enum MultiValuedState {
MONO, MULTI, UNKNOWN;
public static MultiValuedState get(boolean multiValued) {
return multiValued ? MULTI : MONO;
}
}
/**
* The attribute identifier.
*/
@XmlElement
private final String id;
/**
* The non null
list of non null
values for the Attribute.
* For a mono-valued Attribute, this also holds the value as a unique element..
*/
@XmlElementWrapper(name = "values")
@XmlElement(name = "value")
private final List values;
/**
* Whether this holds a mono-valued (MONO) or multi-valued (MULTI) attribute value. May also be UNKNOWN.
*/
private MultiValuedState multiValuedState;
/**
* The Attribute Value's origin. Can safely be ignored in non IDM context.
*/
private AttributeOrigin origin = AttributeOrigin.EXTERNAL;
// For JAXB
protected AttributeValue() {
this.id = null;
this.values = new ArrayList<>();
}
/**
* General constructor. Any null
value provided in the given values list is filtered out and ignored, thus
* ensuring that an Attribute never holds null
values.
*
* @param id the attribute id
* @param values the list of values, cannot be null
but may be empty
* @param multiValuedState whether the attribute is multivalued or not, or unknown
*/
@JsonCreator
public AttributeValue(@JsonProperty("id") String id,
@JsonProperty("values") List values,
@JsonProperty("multivalued") MultiValuedState multiValuedState) {
if (multiValuedState == null) {
multiValuedState = MultiValuedState.UNKNOWN;
}
Validate.notNull(id, "Attribute id must be non null!");
Validate.notNull(values, "Attribute values must be non null");
Validate.validState(multiValuedState == MultiValuedState.MULTI ||
multiValuedState == MultiValuedState.UNKNOWN ||
values.size() <= 1,
"If not multivalued attribute, list of values must contain a single element or no element at all "
+ "(no value), attribute id: '" + id + "', values: " + values);
this.id = id;
this.values = values.stream().filter(Objects::nonNull).collect(toList());
this.multiValuedState = multiValuedState;
}
/**
* Construct a new AttributeValue with the given new values.
*
* @param values the new values
* @return a new AttributeValue
*/
@SuppressWarnings("WeakerAccess") //API
public AttributeValue withValues(List values) {
AttributeValue copy = new AttributeValue<>(this.getId(), values, this.getMultiValuedState());
copy.setOrigin(this.getOrigin());
return copy;
}
/**
* Constructs a new AttributeValue by converting the values using the given converter.
*
* @param converter the converter method for the values
* @param the type of values
* @return a new AttributeValue
*/
@SuppressWarnings("WeakerAccess") //API
public AttributeValue withConversion(Function converter) {
AttributeValue copy = new AttributeValue<>(
this.getId(),
this.values.stream().map(converter).collect(toList()),
this.getMultiValuedState());
copy.setOrigin(this.getOrigin());
return copy;
}
/**
* Construct a new AttributeValue with the given new value.
*
* @param value the new values
* @return a new AttributeValue
*/
@SuppressWarnings("unused") //API
public AttributeValue withValue(V value) {
return withValues(Collections.singletonList(value));
}
/**
* @return this AttributeValue with values that are guaranteed to be distinct.
*/
public AttributeValue distinctValues() {
if (!this.hasValue()) {
return this;
}
if (this.values.size() == 1) {
return this;
}
List distinctValues = this.values.stream().distinct().collect(toList());
return withValues(distinctValues);
}
/**
* @return this AttributeValue with values that are guaranteed to be sorted.
*/
@SuppressWarnings("unused") //API
public AttributeValue sortedValues() {
if (!this.hasValue()) {
return this;
}
if (this.values.size() == 1) {
return this;
}
List sortedValues = this.values.stream().sorted().collect(toList());
return withValues(sortedValues);
}
/**
* @return this AttributeValue with values that are guaranteed to be sorted and distinct
*/
public AttributeValue sortedAndDistinctValues() {
if (!this.hasValue()) {
return this;
}
if (this.values.size() == 1) {
return this;
}
List sortedAndDistinctValues = this.values.stream().sorted().distinct().collect(toList());
return withValues(sortedAndDistinctValues);
}
/**
* Factory method to construct a new multi-valued AttributeValue instance.
*
* @param id the attribute id
* @param values the array of values, cannot be null
but may be empty
* @param the type of held values
* @return a new AttributeValue instance
*/
@SafeVarargs
public static AttributeValue multi(String id, T... values) {
return multi(id, Arrays.asList(values));
}
/**
* Factory method to construct a new multi-valued AttributeValue instance.
*
* @param id the attribute id
* @param values the list of values, cannot be null
but may be empty, null
values are filtered out
* @param the type of held values
* @return a new AttributeValue instance
*/
public static AttributeValue multi(String id, List values) {
return new AttributeValue<>(id, values, MultiValuedState.MULTI);
}
/**
* Factory method to construct a new mono-valued AttributeValue instance.
*
* @param id the attribute id
* @param value the attribute unique value, if null
then it is an empty attribute
* @param the type of held value
* @return a new AttributeValue instance
*/
public static AttributeValue mono(String id, T value) {
return new AttributeValue<>(id, Collections.singletonList(value), MultiValuedState.MONO);
}
/**
* Factory method to construct an empty AttributeValue instance. This should be used
* as a temporary or special case container only, as attributes without any value would
* otherwise simply be evicted from the list of an object's attributes.
*
* @param id the attribute id
* @param multiValued whether the attribute is multivalued
* @param the type of held value
* @return a new AttributeValue instance
*/
public static AttributeValue empty(String id, boolean multiValued) {
return new AttributeValue<>(id, Collections.emptyList(), MultiValuedState.get(multiValued));
}
/**
* Factory method to construct the correct {@link AttributeValue} (mono or multi) using the given id and
* an object value of an undefined type.
*
* Object value may be either a standard value object (string, date, boolean, etc.) or a {@link List} of such
* value objects.
*
* @param id the attribute id
* @param value the attribute value, either a single value object or a {@link List} of value objects
* @param the type of held value
* @return a new AttributeValue instance
*/
@SuppressWarnings("unchecked")
public static AttributeValue from(String id, Object value) {
if (value instanceof List) {
return multi(id, (List) value);
} else {
return mono(id, (T) value);
}
}
/**
* Same as {@link #from(String, List, MultiValuedState)}, but with a definite information about
* the multivalued characteristic.
* @param id the attribute id
* @param values the attribute values
* @param multiValued whether the attribute is multivalued
* @param the type of held value
* @return a new AttributeValue instance
*/
@SuppressWarnings("unchecked")
public static AttributeValue from(String id, List values, boolean multiValued) {
Validate.notNull(values, "values cannot be null!");
return new AttributeValue(id, values, MultiValuedState.get(multiValued));
}
/**
* General purpose Factory method that constructs either a mono or multi-valued {@link AttributeValue}
* instance depending on the multiValued
parameter. When constructing a mono-valued instance,
* the values
list must contain at most one element. An empty AttributeValue is constructed
* by passing an empty list (which would also be achieved by calling {@link #empty(String, boolean)}).
*
* @param id the attribute id
* @param values the list of values, only one single element for a mono-valued attribute
* @param multiValuedState whether or not the attribute is multivalued, or it is unknown
* @param the type of held value
* @return a new AttributeValue instance
*/
@SuppressWarnings("unchecked")
public static AttributeValue from(String id, List values, MultiValuedState multiValuedState) {
Validate.notNull(values, "values cannot be null!");
return new AttributeValue(id, values, multiValuedState);
}
/**
* @return the attribute id value
*/
@JsonProperty("id")
public String getId() {
return id;
}
/**
* Return the list of values held by this {@link AttributeValue}. It may be empty to represent an
* empty attribute (no value).
*
* Note: this method is safe to call for either mono or multi-valued attributes. For mono
* valued attributes, the list will have at most one element, that represents the attribute's single
* value.
*
* Warning: the returned list is not modifiable, this method cannot be
* used to mutate the attribute values.
*
* @return the attribute values, never null
.
*/
@JsonProperty("values")
public List getValues() {
return Collections.unmodifiableList(values);
}
/**
* Return whether or not this {@link AttributeValue} represents an attribute that has a value
* or an attribute that has no value. This needs to be checked before a call to {@link #getValue()}
* is performed in order to be absolutely safe (although, when using the API, {@link AttributeValue} instances
* are guaranteed to have a value).
*
* @return true
if the attribute has a value, false
if it does not (empty)
*/
public boolean hasValue() {
return !this.values.isEmpty();
}
/**
* Return whether or not this {@link AttributeValue} represents an attribute that has a
* non null
value.
*
* @return true
if the attribute has a non null value, false
if it does not (empty)
*/
public boolean hasNonNullValue() {
if (!hasValue()) {
return false;
}
return this.values.stream().anyMatch(Objects::nonNull);
}
/**
* Return a mono-valued attribute's value.
*
* This is a convenience method that can only be called on mono-valued attributes that have
* an actual value! In some contexts, this should be checked with {@link #hasValue()}
*
* @return the attribute's only value (may be null
)
* @throws NoSuchElementException if the attribute has no value
* @throws IllegalStateException if the attribute is multi-valued
*/
@JsonIgnore
public V getValue() {
if (isMultiValued()) {
throw new IllegalStateException(
String.format("Attribute '%s' is multi-valued or its multi-valued state is unknown: #getValue() cannot be called!", getId()));
}
if (getValues().isEmpty()) {
throw new NoSuchElementException(String.format("Attribute '%s' has no value", getId()));
}
return getValues().get(0);
}
/**
* @return true
if the attribute multi-valued characteristic is true or unknown, false
otherwise
*/
@JsonIgnore
public boolean isMultiValued() {
return this.multiValuedState == MultiValuedState.MULTI || this.multiValuedState == MultiValuedState.UNKNOWN;
}
/**
* Return the state of multi-value knowledge concerning this attribute value. Depending on the context of
* the attribute creation, this might be {@link MultiValuedState#UNKNOWN}. When accessing AttributeValue objects
* provided through the IDM conf API, however, this information is guaranteed to be known and only
* {@link MultiValuedState#MULTI} or {@link MultiValuedState#MONO} are returned.
*
* @return this object multivalued state
*/
@JsonProperty("multivalued") //for DB serialization
@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = KnownMultiValuedStateFilter.class)
public MultiValuedState getMultiValuedState() {
return this.multiValuedState;
}
public void setMultiValuedState(MultiValuedState val) {
this.multiValuedState = val;
}
@JsonIgnore
public AttributeOrigin getOrigin() {
return origin;
}
public AttributeValue setOrigin(AttributeOrigin origin) {
this.origin = origin;
return this;
}
/**
* Utility method returning the class of the attribute values. Can only work if there is a value.
*
* @return the value class, or null
if it cannot be inferred
*/
@JsonIgnore
public Class> getValueClass() {
Optional> value = getValues().stream().filter(Objects::nonNull).findFirst();
return value.map(Object::getClass).orElse(null);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(getId()).append("=");
if (!hasValue()) {
sb.append("");
} else {
sb.append(isMultiValued() ? getValues() : getValue());
}
return sb.toString();
}
/**
* Used to only output the multiValuedState when the state is not unknown.
*/
static class KnownMultiValuedStateFilter {
@Override
public boolean equals(Object obj) {
return obj == null || MultiValuedState.UNKNOWN.equals(obj);
}
}
}