de.gold.scim.common.schemas.SchemaAttribute Maven / Gradle / Ivy
The newest version!
package de.gold.scim.common.schemas;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import de.gold.scim.common.constants.AttributeNames;
import de.gold.scim.common.constants.HttpStatus;
import de.gold.scim.common.constants.enums.Mutability;
import de.gold.scim.common.constants.enums.ReferenceTypes;
import de.gold.scim.common.constants.enums.Returned;
import de.gold.scim.common.constants.enums.Type;
import de.gold.scim.common.constants.enums.Uniqueness;
import de.gold.scim.common.exceptions.InvalidSchemaException;
import de.gold.scim.common.resources.base.ScimObjectNode;
import de.gold.scim.common.utils.JsonHelper;
import lombok.EqualsAndHashCode;
import lombok.Getter;
/**
* holds the data of an attribute definition from a schema type document
*/
@Getter
@EqualsAndHashCode(exclude = {"schema", "parent"}, callSuper = true)
public final class SchemaAttribute extends ScimObjectNode
{
/**
* a reference to the parent schema that holds this schema attribute
*/
private final Schema schema;
/**
* is used in case of subAttributes
*/
private final SchemaAttribute parent;
/**
* the uri of the resource to which this attribute belongs
*/
private final String resourceUri;
/**
* an optional attribute that is used as a workaround. For example the meta attribute definition has been
* separated from the normal resource schemata in order to prevent developers for having to define the
* meta-attribute definition for each resource separately. But if this is done the name of the attributes is
* not build correctly because meta definition is not a schema-definition and not an attribute definition
* anymore. Therefore this name prefix can be used to build the attribute name correctly.
* in case of meta the attribute "created" would only get the name "created". But if this variable is set to
* "meta" than the attribute will be accessible by the name "meta.created" instead of just "created"
*/
private final String namePrefix;
protected SchemaAttribute(Schema schema,
String resourceUri,
SchemaAttribute parent,
JsonNode jsonNode,
String namePrefix)
{
super(null);
this.schema = schema;
this.resourceUri = resourceUri;
this.namePrefix = namePrefix;
Function errorMessageBuilder = attribute -> "could not find required attribute '" + attribute
+ "' in meta-schema for attribute: "
+ getScimNodeName();
final String nameAttribute = AttributeNames.RFC7643.NAME;
final String nameErrorMessage = errorMessageBuilder.apply(nameAttribute);
setName(JsonHelper.getSimpleAttribute(jsonNode, nameAttribute)
.orElseThrow(() -> getException(nameErrorMessage, null)));
final String typeAttribute = AttributeNames.RFC7643.TYPE;
final String typeErrorMessage = errorMessageBuilder.apply(typeAttribute);
final Type type = Type.getByValue(JsonHelper.getSimpleAttribute(jsonNode, typeAttribute)
.orElseThrow(() -> getException(typeErrorMessage, null)));
setType(type);
final String descriptionAttribute = AttributeNames.RFC7643.DESCRIPTION;
final String descriptionErrorMessage = errorMessageBuilder.apply(descriptionAttribute);
setDescription(JsonHelper.getSimpleAttribute(jsonNode, descriptionAttribute)
.orElseThrow(() -> getException(descriptionErrorMessage, null)));
setMutability(Mutability.getByValue(JsonHelper.getSimpleAttribute(jsonNode, AttributeNames.RFC7643.MUTABILITY)
.orElse(null)));
setReturned(Returned.getByValue(JsonHelper.getSimpleAttribute(jsonNode, AttributeNames.RFC7643.RETURNED)
.orElse(null)));
setUniqueness(Uniqueness.getByValue(JsonHelper.getSimpleAttribute(jsonNode, AttributeNames.RFC7643.UNIQUENESS)
.orElse(Uniqueness.NONE.getValue())));
setMultiValued(JsonHelper.getSimpleAttribute(jsonNode, AttributeNames.RFC7643.MULTI_VALUED, Boolean.class)
.orElse(false));
setRequired(JsonHelper.getSimpleAttribute(jsonNode, AttributeNames.RFC7643.REQUIRED, Boolean.class).orElse(false));
setCaseExact(JsonHelper.getSimpleAttribute(jsonNode, AttributeNames.RFC7643.CASE_EXACT, Boolean.class)
.orElse(false));
setCanonicalValues(JsonHelper.getSimpleAttributeArray(jsonNode, AttributeNames.RFC7643.CANONICAL_VALUES)
.orElse(Collections.emptyList()));
setReferenceTypes(JsonHelper.getSimpleAttributeArray(jsonNode, AttributeNames.RFC7643.REFERENCE_TYPES)
.map(strings -> strings.stream()
.map(ReferenceTypes::getByValue)
.collect(Collectors.toList()))
.orElse(Type.REFERENCE.equals(type) ? Collections.singletonList(ReferenceTypes.EXTERNAL)
: Collections.emptyList()));
setSubAttributes(resolveSubAttributes(jsonNode));
this.parent = parent;
validateAttribute();
schema.addSchemaAttribute(this);
}
public SchemaAttribute(Schema schema, String resourceUri, SchemaAttribute parent, JsonNode jsonNode)
{
this(schema, resourceUri, parent, jsonNode, null);
}
/**
* @return the full resource node name e.g. User.name.givenName or Group.member.value
*/
public String getFullResourceName()
{
return getParent() == null ? getResourceUri() + ":" + getScimNodeName()
: getResourceUri() + ":" + getScimNodeName();
}
/**
* @return the name scim node name of this attribute e.g. "name.givenName"
*/
public String getScimNodeName()
{
return getParent() == null ? getNamePrefix() + getName() : getParent().getScimNodeName() + "." + getName();
}
/**
* @see #namePrefix
*/
public String getNamePrefix()
{
return namePrefix == null ? "" : namePrefix + ".";
}
/**
* The attribute's name.
*/
public String getName()
{
return getStringAttribute(AttributeNames.RFC7643.NAME).orElse(null);
}
/**
* The attribute's name.
*/
private void setName(String name)
{
setAttribute(AttributeNames.RFC7643.NAME, name);
}
// @formatter:off
/**
* The attribute's data type. Valid values are "string",
* "boolean", "decimal", "integer", "dateTime", "reference", and
* "complex". When an attribute is of type "complex", there
* SHOULD be a corresponding schema attribute "subAttributes"
* defined, listing the sub-attributes of the attribute.
*/
// @formatter:on
public Type getType()
{
return getStringAttribute(AttributeNames.RFC7643.TYPE).map(Type::getByValue).orElse(null);
}
// @formatter:off
/**
* The attribute's data type. Valid values are "string",
* "boolean", "decimal", "integer", "dateTime", "reference", and
* "complex". When an attribute is of type "complex", there
* SHOULD be a corresponding schema attribute "subAttributes"
* defined, listing the sub-attributes of the attribute.
*/
// @formatter:on
private void setType(Type type)
{
setAttribute(AttributeNames.RFC7643.TYPE, Optional.ofNullable(type).map(Type::getValue).orElse(null));
}
// @formatter:off
/**
* The attribute's human-readable description. When
* applicable, service providers MUST specify the description.
*/
// @formatter:on
public String getDescription()
{
return getStringAttribute(AttributeNames.RFC7643.DESCRIPTION).orElse(null);
}
// @formatter:off
/**
* The attribute's human-readable description. When
* applicable, service providers MUST specify the description.
*/
// @formatter:on
private void setDescription(String description)
{
setAttribute(AttributeNames.RFC7643.DESCRIPTION, description);
}
// @formatter:off
/**
* A single keyword indicating the circumstances under
* which the value of the attribute can be (re)defined:
*
* readOnly The attribute SHALL NOT be modified.
*
* readWrite The attribute MAY be updated and read at any time.
* This is the default value.
*
* immutable The attribute MAY be defined at resource creation
* (e.g., POST) or at record replacement via a request (e.g., a
* PUT). The attribute SHALL NOT be updated.
*
* writeOnly The attribute MAY be updated at any time. Attribute
* values SHALL NOT be returned (e.g., because the value is a
* stored hash). Note: An attribute with a mutability of
* "writeOnly" usually also has a returned setting of "never".
*/
// @formatter:on
public Mutability getMutability()
{
return getStringAttribute(AttributeNames.RFC7643.MUTABILITY).map(Mutability::getByValue).orElse(null);
}
// @formatter:off
/**
* A single keyword indicating the circumstances under
* which the value of the attribute can be (re)defined:
*
* readOnly The attribute SHALL NOT be modified.
*
* readWrite The attribute MAY be updated and read at any time.
* This is the default value.
*
* immutable The attribute MAY be defined at resource creation
* (e.g., POST) or at record replacement via a request (e.g., a
* PUT). The attribute SHALL NOT be updated.
*
* writeOnly The attribute MAY be updated at any time. Attribute
* values SHALL NOT be returned (e.g., because the value is a
* stored hash). Note: An attribute with a mutability of
* "writeOnly" usually also has a returned setting of "never".
*/
// @formatter:on
private void setMutability(Mutability mutability)
{
setAttribute(AttributeNames.RFC7643.MUTABILITY,
Optional.ofNullable(mutability).map(Mutability::getValue).orElse(null));
}
// @formatter:off
/**
* A single keyword that indicates when an attribute and
* associated values are returned in response to a GET request or
* in response to a PUT, POST, or PATCH request. Valid keywords
* are as follows:
*
* always The attribute is always returned, regardless of the
* contents of the "attributes" parameter. For example, "id"
* is always returned to identify a SCIM resource.
*
* never The attribute is never returned. This may occur because
* the original attribute value (e.g., a hashed value) is not
* retained by the service provider. A service provider MAY
* allow attributes to be used in a search filter.
*
* default The attribute is returned by default in all SCIM
* operation responses where attribute values are returned. If
* the GET request "attributes" parameter is specified,
* attribute values are only returned if the attribute is named
* in the "attributes" parameter. DEFAULT.
*
* request The attribute is returned in response to any PUT,
* POST, or PATCH operations if the attribute was specified by
* the client (for example, the attribute was modified). The
* attribute is returned in a SCIM query operation only if
* specified in the "attributes" parameter.
*/
// @formatter:on
public Returned getReturned()
{
return getStringAttribute(AttributeNames.RFC7643.RETURNED).map(Returned::getByValue).orElse(null);
}
// @formatter:off
/**
* A single keyword that indicates when an attribute and
* associated values are returned in response to a GET request or
* in response to a PUT, POST, or PATCH request. Valid keywords
* are as follows:
*
* always The attribute is always returned, regardless of the
* contents of the "attributes" parameter. For example, "id"
* is always returned to identify a SCIM resource.
*
* never The attribute is never returned. This may occur because
* the original attribute value (e.g., a hashed value) is not
* retained by the service provider. A service provider MAY
* allow attributes to be used in a search filter.
*
* default The attribute is returned by default in all SCIM
* operation responses where attribute values are returned. If
* the GET request "attributes" parameter is specified,
* attribute values are only returned if the attribute is named
* in the "attributes" parameter. DEFAULT.
*
* request The attribute is returned in response to any PUT,
* POST, or PATCH operations if the attribute was specified by
* the client (for example, the attribute was modified). The
* attribute is returned in a SCIM query operation only if
* specified in the "attributes" parameter.
*/
// @formatter:on
private void setReturned(Returned returned)
{
setAttribute(AttributeNames.RFC7643.RETURNED, Optional.ofNullable(returned).map(Returned::getValue).orElse(null));
}
// @formatter:off
/**
* A single keyword value that specifies how the service
* provider enforces uniqueness of attribute values. A server MAY
* reject an invalid value based on uniqueness by returning HTTP
* response code 400 (Bad Request). A client MAY enforce
* uniqueness on the client side to a greater degree than the
* service provider enforces. For example, a client could make a
* value unique while the server has uniqueness of "none". Valid
* keywords are as follows:
*
* none The values are not intended to be unique in any way.
* DEFAULT.
*
* server The value SHOULD be unique within the context of the
* current SCIM endpoint (or tenancy) and MAY be globally
* unique (e.g., a "username", email address, or other
* server-generated key or counter). No two resources on the
* same server SHOULD possess the same value.
*
* global The value SHOULD be globally unique (e.g., an email
* address, a GUID, or other value). No two resources on any
* server SHOULD possess the same value.
*/
// @formatter:on
public Uniqueness getUniqueness()
{
return getStringAttribute(AttributeNames.RFC7643.UNIQUENESS).map(Uniqueness::getByValue).orElse(null);
}
// @formatter:off
/**
* A single keyword value that specifies how the service
* provider enforces uniqueness of attribute values. A server MAY
* reject an invalid value based on uniqueness by returning HTTP
* response code 400 (Bad Request). A client MAY enforce
* uniqueness on the client side to a greater degree than the
* service provider enforces. For example, a client could make a
* value unique while the server has uniqueness of "none". Valid
* keywords are as follows:
*
* none The values are not intended to be unique in any way.
* DEFAULT.
*
* server The value SHOULD be unique within the context of the
* current SCIM endpoint (or tenancy) and MAY be globally
* unique (e.g., a "username", email address, or other
* server-generated key or counter). No two resources on the
* same server SHOULD possess the same value.
*
* global The value SHOULD be globally unique (e.g., an email
* address, a GUID, or other value). No two resources on any
* server SHOULD possess the same value.
*/
// @formatter:on
private void setUniqueness(Uniqueness uniqueness)
{
setAttribute(AttributeNames.RFC7643.UNIQUENESS,
Optional.ofNullable(uniqueness).map(Uniqueness::getValue).orElse(null));
}
/**
* A Boolean value indicating the attribute's plurality.
*/
public boolean isMultiValued()
{
return getBooleanAttribute(AttributeNames.RFC7643.MULTI_VALUED).orElse(false);
}
/**
* A Boolean value indicating the attribute's plurality.
*/
private void setMultiValued(boolean multiValued)
{
setAttribute(AttributeNames.RFC7643.MULTI_VALUED, multiValued);
}
// @formatter:off
/**
* A Boolean value that specifies whether or not the
* attribute is required.
*/
// @formatter:on
public boolean isRequired()
{
return getBooleanAttribute(AttributeNames.RFC7643.REQUIRED).orElse(false);
}
// @formatter:off
/**
* A Boolean value that specifies whether or not the
* attribute is required.
*/
// @formatter:on
private void setRequired(boolean required)
{
setAttribute(AttributeNames.RFC7643.REQUIRED, required);
}
// @formatter:off
/**
* A Boolean value that specifies whether or not a string
* attribute is case sensitive. The server SHALL use case
* sensitivity when evaluating filters. For attributes that are
* case exact, the server SHALL preserve case for any value
* submitted. If the attribute is case insensitive, the server
* MAY alter case for a submitted value. Case sensitivity also
* impacts how attribute values MAY be compared against filter
* values (see Section 3.4.2.2 of [RFC7644]).
*/
// @formatter:on
public boolean isCaseExact()
{
return getBooleanAttribute(AttributeNames.RFC7643.CASE_EXACT).orElse(false);
}
// @formatter:off
/**
* A Boolean value that specifies whether or not a string
* attribute is case sensitive. The server SHALL use case
* sensitivity when evaluating filters. For attributes that are
* case exact, the server SHALL preserve case for any value
* submitted. If the attribute is case insensitive, the server
* MAY alter case for a submitted value. Case sensitivity also
* impacts how attribute values MAY be compared against filter
* values (see Section 3.4.2.2 of [RFC7644]).
*/
// @formatter:on
private void setCaseExact(boolean caseExact)
{
setAttribute(AttributeNames.RFC7643.CASE_EXACT, caseExact);
}
// @formatter:off
/**
* A collection of suggested canonical values that
* MAY be used (e.g., "work" and "home"). In some cases, service
* providers MAY choose to ignore unsupported values. OPTIONAL.
*/
// @formatter:on
public List getCanonicalValues()
{
return getSimpleArrayAttribute(AttributeNames.RFC7643.CANONICAL_VALUES);
}
// @formatter:off
/**
* A collection of suggested canonical values that
* MAY be used (e.g., "work" and "home"). In some cases, service
* providers MAY choose to ignore unsupported values. OPTIONAL.
*/
// @formatter:on
private void setCanonicalValues(List canonicalValues)
{
setStringAttributeList(AttributeNames.RFC7643.CANONICAL_VALUES, canonicalValues);
}
// @formatter:off
/**
* A multi-valued array of JSON strings that indicate
* the SCIM resource types that may be referenced. Valid values
* are as follows:
*
* + A SCIM resource type (e.g., "User" or "Group"),
*
* + "external" - indicating that the resource is an external
* resource (e.g., a photo), or
*
* + "uri" - indicating that the reference is to a service
* endpoint or an identifier (e.g., a schema URN).
*
* This attribute is only applicable for attributes that are of
* type "reference" (Section 2.3.7).
*/
// @formatter:on
public List getReferenceTypes()
{
return getSimpleArrayAttribute(AttributeNames.RFC7643.REFERENCE_TYPES).stream()
.map(ReferenceTypes::getByValue)
.collect(Collectors.toList());
}
// @formatter:off
/**
* A multi-valued array of JSON strings that indicate
* the SCIM resource types that may be referenced. Valid values
* are as follows:
*
* + A SCIM resource type (e.g., "User" or "Group"),
*
* + "external" - indicating that the resource is an external
* resource (e.g., a photo), or
*
* + "uri" - indicating that the reference is to a service
* endpoint or an identifier (e.g., a schema URN).
*
* This attribute is only applicable for attributes that are of
* type "reference" (Section 2.3.7).
*/
// @formatter:on
private void setReferenceTypes(List referenceTypes)
{
setStringAttributeList(AttributeNames.RFC7643.REFERENCE_TYPES,
referenceTypes.stream().map(ReferenceTypes::getValue).collect(Collectors.toList()));
}
// @formatter:off
/**
* When an attribute is of type "complex",
* "subAttributes" defines a set of sub-attributes.
* "subAttributes" has the same schema sub-attributes as
* "attributes".
*/
// @formatter:on
public List getSubAttributes()
{
return getArrayAttribute(AttributeNames.RFC7643.SUB_ATTRIBUTES, SchemaAttribute.class);
}
// @formatter:off
/**
* When an attribute is of type "complex",
* "subAttributes" defines a set of sub-attributes.
* "subAttributes" has the same schema sub-attributes as
* "attributes".
*/
// @formatter:on
private void setSubAttributes(List subAttributes)
{
setAttribute(AttributeNames.RFC7643.SUB_ATTRIBUTES, subAttributes);
}
/**
* tries to parse the sub attributes of complex type definition
*
* @param jsonNode the complex type definition node
* @return a list of the aub attributes of this complex node
*/
private List resolveSubAttributes(JsonNode jsonNode)
{
if (!Type.COMPLEX.equals(this.getType()))
{
return Collections.emptyList();
}
List schemaAttributeList = new ArrayList<>();
final String subAttributeName = AttributeNames.RFC7643.SUB_ATTRIBUTES;
String errorMessage = "missing attribute '" + subAttributeName + "' on '" + getType() + "'-attribute with name: "
+ getName();
ArrayNode subAttributesArray = JsonHelper.getArrayAttribute(jsonNode, subAttributeName)
.orElseThrow(() -> getException(errorMessage, null));
Set attributeNameSet = new HashSet<>();
for ( JsonNode subAttribute : subAttributesArray )
{
SchemaAttribute schemaAttribute = new SchemaAttribute(schema, resourceUri, this, subAttribute, namePrefix);
if (attributeNameSet.contains(schemaAttribute.getScimNodeName()))
{
String duplicateNameMessage = "the attribute with the name '" + schemaAttribute.getFullResourceName()
+ "' was found twice within the given schema declaration";
throw new InvalidSchemaException(duplicateNameMessage, null, null, null);
}
attributeNameSet.add(schemaAttribute.getScimNodeName());
schemaAttributeList.add(schemaAttribute);
}
return schemaAttributeList;
}
/**
* this method will decide if the attribute definition makes sense. Some attribute combinations are simply
* senseless and might cause confusable situations that would not be easily identifiable.
* the known senseless attribute combinations are the following:
*
*
* {
* "name": "senseless",
* "type": "string",
* "description": "senseless declaration: client cannot write to it and server cannot return it",
* "mutability": "readOnly",
* "returned": "never"
* },
* {
* "name": "senseless",
* "type": "string",
* "description": "senseless declaration: writeOnly must have a returned value of 'never'.",
* "mutability": "writeOnly",
* "returned": "always"
* }
*
*
* this combination shows 3 problems but the following method will only handle two of theses problems:
*
* - mutability: readOnly
* - returned: never
* - the client can never write to this attribute and the server will never return it. The server may use
* this attribute but it simply makes no sense to declare it within the schema
* - ----------------------------
* - and
* - ----------------------------
* - mutability: writeOnly
* - returned: something else than "never"
* - This is also defined in RFC7643 chapter 7:
* writeOnly The attribute MAY be updated at any time. Attribute values SHALL NOT be returned (e.g.,
* because the value is a stored hash). Note: An attribute with a mutability of "writeOnly" usually also has a
* returned setting of "never"
*
* the last problem is that the an attribute attribute with the same name was declared twice. This problem
* will be handled in another method
*/
private void validateAttribute()
{
final Mutability mutability = getMutability();
final Returned returned = getReturned();
if (Mutability.READ_ONLY.equals(mutability) && Returned.NEVER.equals(returned))
{
String errorMessage = "the attribute with the name '" + getFullResourceName() + "' has an invalid declaration. "
+ "mutability 'readOnly' and returned 'never' are an illegal combination. The client is "
+ "not able to write to the given attribute and the server will never return it.";
throw getException(errorMessage, null);
}
else if (Mutability.WRITE_ONLY.equals(mutability) && !Returned.NEVER.equals(returned))
{
String errorMessage = "the attribute with the name '" + getFullResourceName() + "' has an invalid declaration. "
+ "mutability 'writeOnly' must have a returned value of 'never' are an illegal in "
+ "combination. The client should only write to this attribute but should never have it "
+ "returned. The mutability writeOnly makes only sense for sensitive application data "
+ "like passwords or other secrets.";
throw getException(errorMessage, null);
}
}
/**
* builds an exception
*
* @param errorMessage the error message of the exception
* @param cause the cause of this exception, may be null
* @return a new exception instance
*/
private InvalidSchemaException getException(String errorMessage, Exception cause)
{
return new InvalidSchemaException(errorMessage, cause, HttpStatus.INTERNAL_SERVER_ERROR, null);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy