Java tutorial
/* * Copyright 2015-2017 UnboundID Corp. * * 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 <http://www.gnu.org/licenses>. */ package com.unboundid.scim2.common.utils; import com.fasterxml.jackson.annotation.JsonProperty; 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. */ public static final AttributeDefinition SCHEMAS_ATTRIBUTE_DEFINITION; /** * The attribute definition for the SCIM 2 standard id attribute. */ public static final AttributeDefinition ID_ATTRIBUTE_DEFINITION; /** * The attribute definition for the SCIM 2 standard externalId attribute. */ public static final AttributeDefinition EXTERNAL_ID_ATTRIBUTE_DEFINITION; /** * The attribute definition for the SCIM 2 standard meta attribute. */ public static final AttributeDefinition META_ATTRIBUTE_DEFINITION; /** * The collection of attribute definitions for SCIM 2 standard common * attributes: schemas, id, externalId, and meta. */ public static final Collection<AttributeDefinition> 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<AttributeDefinition> subAttributes = getAttributes(Meta.class); builder.addSubAttributes(subAttributes.toArray(new AttributeDefinition[subAttributes.size()])); } 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. */ public static Collection<PropertyDescriptor> getPropertyDescriptors(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 public static Collection<AttributeDefinition> getAttributes(final Class cls) throws IntrospectionException { Stack<String> classesProcessed = new Stack<String>(); 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. */ private static Collection<AttributeDefinition> getAttributes(final Stack<String> classesProcessed, 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<PropertyDescriptor> propertyDescriptors = getPropertyDescriptors(cls); Collection<AttributeDefinition> attributes = new ArrayList<AttributeDefinition>(); 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<AttributeDefinition> 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. */ private static AttributeDefinition.Builder addName(final AttributeDefinition.Builder attributeBuilder, final PropertyDescriptor propertyDescriptor, 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. */ private static AttributeDefinition.Builder addMultiValued(final AttributeDefinition.Builder attributeBuilder, final PropertyDescriptor propertyDescriptor, 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. */ private static AttributeDefinition.Builder addDescription(final AttributeDefinition.Builder attributeBuilder, 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. */ private static AttributeDefinition.Builder addCaseExact(final AttributeDefinition.Builder attributeBuilder, 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. */ private static AttributeDefinition.Builder addRequired(final AttributeDefinition.Builder attributeBuilder, 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. */ private static AttributeDefinition.Builder addCanonicalValues( final AttributeDefinition.Builder attributeBuilder, 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. */ private static AttributeDefinition.Builder addReturned(final AttributeDefinition.Builder attributeBuilder, 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. */ private static AttributeDefinition.Builder addUniqueness(final AttributeDefinition.Builder attributeBuilder, 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. */ private static AttributeDefinition.Builder addReferenceTypes(final AttributeDefinition.Builder attributeBuilder, 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. * * @param attributeBuilder builder for a scim attribute. * @param schemaProperty the schema property annotation for the field * to build an attribute for. * @return this. */ private static AttributeDefinition.Builder addMutability(final AttributeDefinition.Builder attributeBuilder, 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. */ private static AttributeDefinition.Type getAttributeType(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) || (cls == boolean.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. */ public static SchemaResource getSchema(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. */ public static Field findField(final Class<?> cls, 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(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. */ public static String getSchemaIdFromAnnotation(final Class<?> cls) { Schema schema = cls.getAnnotation(Schema.class); return SchemaUtils.getSchemaIdFromAnnotation(schema); } /** * Gets the id property from schema annotation. If the 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. */ private static String getSchemaIdFromAnnotation(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. */ public static String getNameFromSchemaAnnotation(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. */ private static String getNameFromSchemaAnnotation(final Schema schemaAnnotation) { if (schemaAnnotation != null) { return schemaAnnotation.name(); } return null; } /** * Gets the schema urn from a java <code>Class</code>. * @param cls <code>Class</code> of the object. * @return The schema urn for the object. */ public static String getSchemaUrn(final Class cls) { // the schemaid is the urn. Just make sure it // begins with urn: ... the field called name // is just a human friendly name for the object. String schemaId = SchemaUtils.getSchemaIdFromAnnotation(cls); if ((schemaId == null) || (schemaId.isEmpty())) { schemaId = cls.getCanonicalName(); } // if this doesn't appear to be a urn, stick the "urn:" prefix // on it, and use it as a urn anyway. 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(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. */ public static String forceToBeUrn(final String string) { if (isUrn(string)) { return string; } return "urn:" + string; } }