Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2015-2024 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.scim2.common.utils;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.unboundid.scim2.common.annotations.NotNull;
import com.unboundid.scim2.common.annotations.Nullable;
import com.unboundid.scim2.common.types.Meta;
import com.unboundid.scim2.common.annotations.Schema;
import com.unboundid.scim2.common.annotations.Attribute;
import com.unboundid.scim2.common.types.AttributeDefinition;
import com.unboundid.scim2.common.types.SchemaResource;
import javax.lang.model.type.NullType;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.beans.Transient;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Stack;
/**
* Utility class with static methods for common schema operations.
*/
public class SchemaUtils
{
/**
* The attribute definition for the SCIM 2 standard schemas attribute.
*/
@NotNull
public static final AttributeDefinition SCHEMAS_ATTRIBUTE_DEFINITION;
/**
* The attribute definition for the SCIM 2 standard id attribute.
*/
@NotNull
public static final AttributeDefinition ID_ATTRIBUTE_DEFINITION;
/**
* The attribute definition for the SCIM 2 standard externalId attribute.
*/
@NotNull
public static final AttributeDefinition EXTERNAL_ID_ATTRIBUTE_DEFINITION;
/**
* The attribute definition for the SCIM 2 standard meta attribute.
*/
@NotNull
public static final AttributeDefinition META_ATTRIBUTE_DEFINITION;
/**
* The collection of attribute definitions for SCIM 2 standard common
* attributes: schemas, id, externalId, and meta.
*/
@NotNull
public static final Collection
COMMON_ATTRIBUTE_DEFINITIONS;
static
{
AttributeDefinition.Builder builder = new AttributeDefinition.Builder();
builder.setType(AttributeDefinition.Type.STRING);
builder.setName("schemas");
builder.setRequired(true);
builder.setCaseExact(true);
builder.setMultiValued(true);
builder.setMutability(AttributeDefinition.Mutability.READ_WRITE);
builder.setReturned(AttributeDefinition.Returned.ALWAYS);
SCHEMAS_ATTRIBUTE_DEFINITION = builder.build();
builder = new AttributeDefinition.Builder();
builder.setType(AttributeDefinition.Type.STRING);
builder.setName("id");
builder.setCaseExact(true);
builder.setMutability(AttributeDefinition.Mutability.READ_ONLY);
builder.setReturned(AttributeDefinition.Returned.ALWAYS);
ID_ATTRIBUTE_DEFINITION = builder.build();
builder = new AttributeDefinition.Builder();
builder.setType(AttributeDefinition.Type.STRING);
builder.setName("externalId");
builder.setCaseExact(true);
builder.setMutability(AttributeDefinition.Mutability.READ_WRITE);
EXTERNAL_ID_ATTRIBUTE_DEFINITION = builder.build();
builder = new AttributeDefinition.Builder();
builder.setType(AttributeDefinition.Type.COMPLEX);
builder.setName("meta");
builder.setMutability(AttributeDefinition.Mutability.READ_ONLY);
try
{
Collection subAttributes = getAttributes(Meta.class);
builder.addSubAttributes(subAttributes.toArray(
new AttributeDefinition[0]));
}
catch (IntrospectionException e)
{
throw new RuntimeException(e);
}
META_ATTRIBUTE_DEFINITION = builder.build();
COMMON_ATTRIBUTE_DEFINITIONS =
Collections.unmodifiableCollection(Arrays.asList(
SCHEMAS_ATTRIBUTE_DEFINITION, ID_ATTRIBUTE_DEFINITION,
EXTERNAL_ID_ATTRIBUTE_DEFINITION, META_ATTRIBUTE_DEFINITION));
}
/**
* Gets property descriptors for the given class.
*
* @param cls The class to get the property descriptors for.
* @return a collection of property values.
* @throws java.beans.IntrospectionException throw if there are any
* introspection errors.
*/
@NotNull
public static Collection getPropertyDescriptors(
@NotNull final Class> cls)
throws IntrospectionException
{
BeanInfo beanInfo = Introspector.getBeanInfo(cls, Object.class);
PropertyDescriptor[] propertyDescriptors =
beanInfo.getPropertyDescriptors();
return Arrays.asList(propertyDescriptors);
}
/**
* Gets schema attributes for the given class.
*
* @param cls Class to get the schema attributes for.
* @return a collection of attributes.
* @throws IntrospectionException thrown if an introspection error occurs.
*/
@Transient
@NotNull
public static Collection getAttributes(
@NotNull final Class> cls)
throws IntrospectionException
{
Stack classesProcessed = new Stack<>();
return getAttributes(classesProcessed, cls);
}
/**
* Gets SCIM schema attributes for a class.
*
* @param classesProcessed a stack containing the classes processed prior
* to this class. This is used for cycle detection.
* @param cls the class to get the attributes for.
* @return a collection of SCIM schema attributes for the class.
* @throws IntrospectionException thrown if an error occurs during
* Introspection.
*/
@NotNull
private static Collection getAttributes(
@NotNull final Stack classesProcessed,
@NotNull final Class> cls)
throws IntrospectionException
{
String className = cls.getCanonicalName();
if(!cls.isAssignableFrom(AttributeDefinition.class) &&
classesProcessed.contains(className))
{
throw new RuntimeException("Cycles detected in Schema");
}
Collection propertyDescriptors =
getPropertyDescriptors(cls);
Collection attributes =
new ArrayList();
for(PropertyDescriptor propertyDescriptor : propertyDescriptors)
{
if(propertyDescriptor.getName().equals("subAttributes") &&
cls.isAssignableFrom(AttributeDefinition.class) &&
classesProcessed.contains(className))
{
// Skip second nesting of subAttributes the second time around
// since there is no subAttributes of subAttributes in SCIM.
continue;
}
AttributeDefinition.Builder attributeBuilder =
new AttributeDefinition.Builder();
Field field = findField(cls, propertyDescriptor.getName());
if(field == null)
{
continue;
}
Attribute schemaProperty = null;
JsonProperty jsonProperty = null;
if(field.isAnnotationPresent(Attribute.class))
{
schemaProperty = field.getAnnotation(Attribute.class);
}
if(field.isAnnotationPresent(JsonProperty.class))
{
jsonProperty = field.getAnnotation(JsonProperty.class);
}
// Only generate schema for annotated fields.
if(schemaProperty == null)
{
continue;
}
addName(attributeBuilder, propertyDescriptor, jsonProperty);
addDescription(attributeBuilder, schemaProperty);
addCaseExact(attributeBuilder, schemaProperty);
addRequired(attributeBuilder, schemaProperty);
addReturned(attributeBuilder, schemaProperty);
addUniqueness(attributeBuilder, schemaProperty);
addReferenceTypes(attributeBuilder, schemaProperty);
addMutability(attributeBuilder, schemaProperty);
addMultiValued(attributeBuilder, propertyDescriptor, schemaProperty);
addCanonicalValues(attributeBuilder, schemaProperty);
Class propertyCls = propertyDescriptor.getPropertyType();
// if this is a multivalued attribute the real sub attribute class is the
// the one specified in the annotation, not the list, set, array, etc.
if((schemaProperty.multiValueClass() != NullType.class))
{
propertyCls = schemaProperty.multiValueClass();
}
AttributeDefinition.Type type = getAttributeType(propertyCls);
attributeBuilder.setType(type);
if(type == AttributeDefinition.Type.COMPLEX)
{
// Add this class to the list to allow cycle detection
classesProcessed.push(cls.getCanonicalName());
Collection subAttributes =
getAttributes(classesProcessed, propertyCls);
attributeBuilder.addSubAttributes(subAttributes.toArray(
new AttributeDefinition[subAttributes.size()]));
classesProcessed.pop();
}
attributes.add(attributeBuilder.build());
}
return attributes;
}
/**
* This method will find the name for the attribute, and add
* it to the builder.
*
* @param attributeBuilder builder for a scim attribute.
* @param propertyDescriptor property descriptor for the field to build
* the attribute for.
* @param jsonProperty the Jackson JsonProperty annotation for the field.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addName(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@NotNull final PropertyDescriptor propertyDescriptor,
@Nullable final JsonProperty jsonProperty)
{
if(jsonProperty != null &&
!jsonProperty.value().equals(JsonProperty.USE_DEFAULT_NAME))
{
attributeBuilder.setName(jsonProperty.value());
}
else
{
attributeBuilder.setName(propertyDescriptor.getName());
}
return attributeBuilder;
}
/**
* This method will determine if this attribute can have multieple
* values, and set that in the builder.
*
* @param attributeBuilder builder for a scim attribute.
* @param propertyDescriptor property descriptor for the field to build
* the attribute for.
* @param schemaProperty the schema property annotation for the field
* to build an attribute for.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addMultiValued(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@NotNull final PropertyDescriptor propertyDescriptor,
@NotNull final Attribute schemaProperty)
{
Class> multiValuedClass = schemaProperty.multiValueClass();
boolean multiValued = !multiValuedClass.equals(NullType.class);
boolean collectionOrArray =
isCollectionOrArray(propertyDescriptor.getPropertyType());
// if the multiValuedClass attribute is present in the annotation,
// make sure this is a collection or array.
if(multiValued && !collectionOrArray)
{
throw new RuntimeException("Property named " +
propertyDescriptor.getName() +
" is annotated with a multiValuedClass, " +
"but is not a Collection or an array");
}
if(!multiValued && collectionOrArray)
{
throw new RuntimeException("Property named " +
propertyDescriptor.getName() +
" is not annotated with a multiValuedClass, " +
"but is a Collection or an array");
}
attributeBuilder.setMultiValued(multiValued);
return attributeBuilder;
}
/**
* This method will find the description for the attribute, and add
* it to the builder.
*
* @param attributeBuilder builder for a scim attribute.
* @param schemaProperty the schema property annotation for the field
* to build an attribute for.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addDescription(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@Nullable final Attribute schemaProperty)
{
if(schemaProperty != null)
{
attributeBuilder.setDescription(schemaProperty.description());
}
return attributeBuilder;
}
/**
* This method will find the case exact boolean for the attribute, and add
* it to the builder.
*
* @param attributeBuilder builder for a scim attribute.
* @param schemaProperty the schema property annotation for the field
* to build an attribute for.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addCaseExact(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@Nullable final Attribute schemaProperty)
{
if(schemaProperty != null)
{
attributeBuilder.setCaseExact(schemaProperty.isCaseExact());
}
return attributeBuilder;
}
/**
* This method will find the required boolean for the attribute, and add
* it to the builder.
*
* @param attributeBuilder builder for a scim attribute.
* @param schemaProperty the schema property annotation for the field
* to build an attribute for.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addRequired(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@Nullable final Attribute schemaProperty)
{
if(schemaProperty != null)
{
attributeBuilder.setRequired(schemaProperty.isRequired());
}
return attributeBuilder;
}
/**
* This method will find the canonical values for the attribute, and add
* it to the builder.
*
* @param attributeBuilder builder for a scim attribute.
* @param schemaProperty the schema property annotation for the field
* to build an attribute for.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addCanonicalValues(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@Nullable final Attribute schemaProperty)
{
if(schemaProperty != null)
{
attributeBuilder.addCanonicalValues(schemaProperty.canonicalValues());
}
return attributeBuilder;
}
/**
* This method will find the returned constraint for the attribute, and add
* it to the builder.
*
* @param attributeBuilder builder for a scim attribute.
* @param schemaProperty the schema property annotation for the field
* to build an attribute for.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addReturned(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@Nullable final Attribute schemaProperty)
{
if(schemaProperty != null)
{
attributeBuilder.setReturned(schemaProperty.returned());
}
return attributeBuilder;
}
/**
* This method will find the uniqueness constraint for the attribute, and add
* it to the builder.
*
* @param attributeBuilder builder for a scim attribute.
* @param schemaProperty the schema property annotation for the field
* to build an attribute for.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addUniqueness(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@Nullable final Attribute schemaProperty)
{
if(schemaProperty != null)
{
attributeBuilder.setUniqueness(schemaProperty.uniqueness());
}
return attributeBuilder;
}
/**
* This method will find the reference types for the attribute, and add
* it to the builder.
*
* @param attributeBuilder builder for a scim attribute.
* @param schemaProperty the schema property annotation for the field
* to build an attribute for.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addReferenceTypes(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@Nullable final Attribute schemaProperty)
{
if(schemaProperty != null)
{
attributeBuilder.addReferenceTypes(schemaProperty.referenceTypes());
}
return attributeBuilder;
}
/**
* This method will find the mutability constraint for the attribute, and add
* it to the builder. If the schema property is {@code null}, the mutability
* will be considered "read-write".
*
* @param attributeBuilder builder for a scim attribute.
* @param schemaProperty the schema property annotation for the field
* to build an attribute for.
* @return this.
*/
@NotNull
private static AttributeDefinition.Builder addMutability(
@NotNull final AttributeDefinition.Builder attributeBuilder,
@Nullable final Attribute schemaProperty)
{
if(schemaProperty != null)
{
attributeBuilder.setMutability(schemaProperty.mutability());
}
else
{
attributeBuilder.setMutability(AttributeDefinition.Mutability.READ_WRITE);
}
return attributeBuilder;
}
/**
* Gets the attribute type for a given property descriptor. This method
* will attempt to decide what SCIM attribute type should be in the schema
* based on the java class of the attribute.
*
* @param cls java Class for an attribute of a SCIM object.
* @return an attribute type.
*/
@NotNull
private static AttributeDefinition.Type getAttributeType(
@NotNull final Class> cls)
{
if((cls == Integer.class) ||
(cls == int.class))
{
return AttributeDefinition.Type.INTEGER;
}
else if ((cls == Boolean.class) ||
(cls == boolean.class))
{
return AttributeDefinition.Type.BOOLEAN;
}
else if ((cls == Double.class) ||
(cls == double.class) ||
(cls == Float.class) ||
(cls == float.class) ||
(cls == BigDecimal.class))
{
return AttributeDefinition.Type.DECIMAL;
}
else if ((cls == String.class))
{
return AttributeDefinition.Type.STRING;
}
else if((cls == URI.class) || (cls == URL.class))
{
return AttributeDefinition.Type.REFERENCE;
}
else if((cls == Date.class) || (cls == Calendar.class))
{
return AttributeDefinition.Type.DATETIME;
}
else if((cls == byte[].class))
{
return AttributeDefinition.Type.BINARY;
}
else
{
return AttributeDefinition.Type.COMPLEX;
}
}
/**
* Gets the schema for a class. This will walk the inheritance tree looking
* for information about the SCIM schema of the objects represented. This
* information comes from annotations and introspection.
*
* @param cls the class to get the schema for.
* @return the schema.
* @throws IntrospectionException if an exception occurs during introspection.
*/
@Nullable
public static SchemaResource getSchema(@NotNull final Class> cls)
throws IntrospectionException
{
Schema schemaAnnotation = cls.getAnnotation(Schema.class);
// Only generate schema for annotated classes.
if(schemaAnnotation == null)
{
return null;
}
return new SchemaResource(schemaAnnotation.id(),
schemaAnnotation.name(), schemaAnnotation.description(),
getAttributes(cls));
}
/**
* This method will find a java Field for with a particular name. If
* needed, this method will search through super classes. The field
* does not need to be public.
*
* @param cls the java Class to search.
* @param fieldName the name of the field to find.
* @return the Java field.
*/
@Nullable
public static Field findField(@Nullable final Class> cls,
@NotNull final String fieldName)
{
Class> currentClass = cls;
while(currentClass != null)
{
Field [] fields = currentClass.getDeclaredFields();
for(Field field : fields)
{
if(field.getName().equals(fieldName))
{
return field;
}
}
currentClass = currentClass.getSuperclass();
}
return null;
}
/**
* Returns true if the supplied class is a collection or an array. This
* is primarily used to determine if it's a multivalued attribute.
*
* @param cls the class to check.
* @return true if the class is a collection or an array, or false if not.
*/
private static boolean isCollectionOrArray(@NotNull final Class> cls)
{
return (cls.isArray() && byte[].class != cls) ||
Collection.class.isAssignableFrom(cls);
}
/**
* Gets the id of the schema from the annotation of the class
* passed in.
*
* @param cls class to find the schema id property of the annotation from.
* @return the id of the schema, or {@code null} if it was not provided.
*/
@Nullable
public static String getSchemaIdFromAnnotation(@NotNull final Class> cls)
{
Schema schema = cls.getAnnotation(Schema.class);
return SchemaUtils.getSchemaIdFromAnnotation(schema);
}
/**
* Gets the id property from schema annotation. If the id
* attribute was {@code null}, a schema id is generated.
*
* @param schemaAnnotation the SCIM SchemaInfo annotation.
* @return the id of the schema, or {@code null} if it was not provided.
*/
@Nullable
private static String getSchemaIdFromAnnotation(
@Nullable final Schema schemaAnnotation)
{
if(schemaAnnotation != null)
{
return schemaAnnotation.id();
}
return null;
}
/**
* Gets the name property from the annotation of the class
* passed in.
*
* @param cls class to find the schema name property of the annotation from.
* @return the name of the schema.
*/
@Nullable
public static String getNameFromSchemaAnnotation(@NotNull final Class> cls)
{
Schema schema = (Schema)cls.getAnnotation(Schema.class);
return SchemaUtils.getNameFromSchemaAnnotation(schema);
}
/**
* Gets the name property from schema annotation.
*
* @param schemaAnnotation the SCIM SchemaInfo annotation.
* @return the name of the schema or a generated name.
*/
@Nullable
private static String getNameFromSchemaAnnotation(
@Nullable final Schema schemaAnnotation)
{
if(schemaAnnotation != null)
{
return schemaAnnotation.name();
}
return null;
}
/**
* Fetches the schema urn from a {@code Class}.
*
* @param cls The class of the object.
* @return The schema urn for the object.
*/
@NotNull
public static String getSchemaUrn(@NotNull final Class> cls)
{
// The 'schemaId' is the URN. Make sure it begins with the "urn:" prefix.
// The 'name' field is a human-friendly name for the object.
String schemaId =
SchemaUtils.getSchemaIdFromAnnotation(cls);
if ((schemaId == null) || (schemaId.isEmpty()))
{
schemaId = cls.getCanonicalName();
}
// If the schema ID doesn't appear to be a valid URN, append the data to an
// "urn:" prefix.
return forceToBeUrn(schemaId);
}
/**
* Returns true if the string passed in appears to be a urn.
* That determination is made by looking to see if the string
* starts with "{@code urn:}".
*
* @param string the string to check.
* @return true if it's a urn, or false if not.
*/
public static boolean isUrn(@NotNull final String string)
{
return StaticUtils.toLowerCase(string).startsWith("urn:") &&
string.length() > 4;
}
/**
* Will force the string passed in to look like a urn. If the
* string starts with "{@code urn:}" it will be returned as is, however
* if the string starts with anything else, this method will
* prepend "{@code urn:}". This is mainly so that if we have a class that
* will be used as an extension schema, we will ensure that its
* schema will be a urn and distinguishable from all other unmmapped
* values.
*
* @param string the string to force to be a urn.
* @return the urn.
*/
@NotNull
public static String forceToBeUrn(@NotNull final String string)
{
if(isUrn(string))
{
return string;
}
return "urn:" + string;
}
}