Skip to content

Commit

Permalink
Add test for Schema generation from Annotations (#452)
Browse files Browse the repository at this point in the history
* Add test for Schema generation from Annotations

While working on the PatchGenerator I noticed a bug in the type detection.
This change adds tests and simplifies related logic

* Replaces ResourceReference with more specific GroupMembership/UserGroup

The attributes of each are slightly different

Fixes: #425
  • Loading branch information
bdemers authored Dec 15, 2023
1 parent cd18263 commit 8bc31b5
Show file tree
Hide file tree
Showing 12 changed files with 896 additions and 245 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
import org.apache.directory.scim.protocol.data.BulkRequest;
import org.apache.directory.scim.protocol.data.BulkResponse;
import org.apache.directory.scim.protocol.data.ErrorResponse;
import org.apache.directory.scim.spec.resources.GroupMembership;
import org.apache.directory.scim.spec.resources.ScimGroup;
import org.apache.directory.scim.spec.resources.ScimResource;
import org.apache.directory.scim.spec.resources.ScimUser;
import org.apache.directory.scim.spec.schema.ResourceReference;
import org.apache.directory.scim.core.schema.SchemaRegistry;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
Expand All @@ -60,8 +60,8 @@ public void bulkIdTemporaryIdentifiersTest() throws Exception {

ScimGroup tourGuides = new ScimGroup()
.setDisplayName("Tour Guides")
.setMembers(List.of(new ResourceReference()
.setType(ResourceReference.ReferenceType.USER)
.setMembers(List.of(new GroupMembership()
.setType(GroupMembership.Type.USER)
.setValue("bulkId:qwerty")));

BulkRequest bulkRequest = new BulkRequest()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.directory.scim.spec.resources;

import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlEnum;
import jakarta.xml.bind.annotation.XmlEnumValue;
import jakarta.xml.bind.annotation.XmlType;
import lombok.Data;
import org.apache.directory.scim.spec.annotation.ScimAttribute;
import org.apache.directory.scim.spec.annotation.ScimResourceIdReference;
import org.apache.directory.scim.spec.schema.Schema;

import java.io.Serializable;

@Data
@XmlType(propOrder = {"value","ref","display","type"})
@XmlAccessorType(XmlAccessType.NONE)
public class GroupMembership implements Serializable {

private static final long serialVersionUID = 9126588075353486789L;

@XmlEnum
public enum Type {
@XmlEnumValue("User") USER,
@XmlEnumValue("Group") GROUP;
}

@ScimAttribute(description="Identifier of the member of this Group.",
mutability = Schema.Attribute.Mutability.IMMUTABLE)
@ScimResourceIdReference
@XmlElement
String value;

@ScimAttribute(name = "$ref", description="The URI corresponding to a SCIM resource that is a member of this Group.",
referenceTypes={"User", "Group"},
mutability = Schema.Attribute.Mutability.IMMUTABLE)
@XmlElement(name = "$ref")
String ref;

@ScimAttribute(description="A human readable name, primarily used for display purposes.",
mutability = Schema.Attribute.Mutability.READ_ONLY)
@XmlElement
String display;

@ScimAttribute(description="A label indicating the type of resource, e.g., 'User' or 'Group'.",
canonicalValueList={"User", "Group"},
mutability = Schema.Attribute.Mutability.IMMUTABLE)
@XmlElement
Type type;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import org.apache.directory.scim.spec.annotation.ScimAttribute;
import org.apache.directory.scim.spec.annotation.ScimResourceType;
import org.apache.directory.scim.spec.schema.Meta;
import org.apache.directory.scim.spec.schema.ResourceReference;
import lombok.Data;
import lombok.EqualsAndHashCode;

Expand All @@ -56,13 +55,13 @@ public class ScimGroup extends ScimResource implements Serializable {

@XmlElement
@ScimAttribute(description = "A list of members of the Group.")
List<ResourceReference> members;
List<GroupMembership> members;

public ScimGroup addMember(ResourceReference resourceReference) {
public ScimGroup addMember(GroupMembership groupMembership) {
if (members == null) {
members = new ArrayList<>();
}
members.add(resourceReference);
members.add(groupMembership);

return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import org.apache.directory.scim.spec.annotation.ScimAttribute;
import org.apache.directory.scim.spec.annotation.ScimResourceType;
import org.apache.directory.scim.spec.schema.Meta;
import org.apache.directory.scim.spec.schema.ResourceReference;
import org.apache.directory.scim.spec.schema.Schema.Attribute.Returned;
import org.apache.directory.scim.spec.schema.Schema.Attribute.Uniqueness;

Expand Down Expand Up @@ -77,7 +76,7 @@ public class ScimUser extends ScimResource implements Serializable {

@XmlElement
@ScimAttribute(description="A list of groups that the user belongs to, either thorough direct membership, nested groups, or dynamically calculated")
List<ResourceReference> groups;
List<UserGroup> groups;

@XmlElement
@ScimAttribute(description="Instant messaging address for the User.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,50 +17,54 @@
* under the License.
*/

package org.apache.directory.scim.spec.schema;

import java.io.Serializable;
package org.apache.directory.scim.spec.resources;

import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlEnum;
import jakarta.xml.bind.annotation.XmlEnumValue;
import jakarta.xml.bind.annotation.XmlType;

import lombok.Data;
import org.apache.directory.scim.spec.annotation.ScimAttribute;
import org.apache.directory.scim.spec.annotation.ScimResourceIdReference;
import lombok.Data;
import org.apache.directory.scim.spec.schema.Schema;

import java.io.Serializable;

@Data
@XmlType(propOrder = {"value","ref","display","type"})
@XmlAccessorType(XmlAccessType.NONE)
public class ResourceReference implements Serializable {
public class UserGroup implements Serializable {

private static final long serialVersionUID = 9126588075353486789L;

@XmlEnum
public enum ReferenceType {
public enum Type {
@XmlEnumValue("direct") DIRECT,
@XmlEnumValue("indirect") INDIRECT,
@XmlEnumValue("User") USER,
@XmlEnumValue("Group") GROUP;
@XmlEnumValue("indirect") INDIRECT;
}

@ScimAttribute(description="Reference Element Identifier")
@ScimAttribute(description="The identifier of the User's group.",
mutability = Schema.Attribute.Mutability.READ_ONLY)
@ScimResourceIdReference
@XmlElement
String value;

@ScimAttribute(name = "$ref", description="The URI of the corresponding resource ", referenceTypes={"User", "Group"})
@ScimAttribute(name = "$ref", description="The URI of the corresponding 'Group' resource to which the user belongs.",
referenceTypes={"User", "Group"},
mutability = Schema.Attribute.Mutability.READ_ONLY)
@XmlElement(name = "$ref")
String ref;

@ScimAttribute(description="A human readable name, primarily used for display purposes. READ-ONLY.")
@ScimAttribute(description="A human-readable name, primarily used for display purposes.",
mutability = Schema.Attribute.Mutability.READ_ONLY)
@XmlElement
String display;

@ScimAttribute(description="A label indicating the attribute's function; e.g., 'direct' or 'indirect'.", canonicalValueList={"direct", "indirect"})
@ScimAttribute(description="A label indicating the attribute's function, e.g., 'direct' or 'indirect'.",
canonicalValueList={"direct", "indirect"},
mutability = Schema.Attribute.Mutability.READ_ONLY)
@XmlElement
ReferenceType type;
Type type;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,32 @@

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;

@Slf4j
public final class Schemas {

private static final String STRING_TYPE_IDENTIFIER = "class java.lang.String";
private static final String STRING_TYPE = "java.lang.String"; // TODO this is ugly
private static final String CHARACTER_ARRAY_TYPE_IDENTIFIER = "class [C";
private static final String BIG_C_CHARACTER_ARRAY_TYPE_IDENTIFIER = "class [Ljava.lang.Character;";
private static final String INT_TYPE_IDENTIFIER = "int";
private static final String INTEGER_TYPE_IDENTIFIER = "class java.lang.Integer";
private static final String FLOAT_TYPE_IDENTIFIER = "float";
private static final String BIG_F_FLOAT_TYPE_IDENTIFIER = "class java.lang.Float";
private static final String DOUBLE_TYPE_IDENTIFIER = "double";
private static final String BIG_D_DOUBLE_TYPE_IDENTIFIER = "class java.lang.Double";
private static final String BOOLEAN_TYPE_IDENTIFIER = "boolean";
private static final String BIG_B_BOOLEAN_TYPE_IDENTIFIER = "class java.lang.Boolean";
private static final String LOCAL_TIME_TYPE_IDENTIFER = "class java.time.LocalTime";
private static final String LOCAL_DATE_TYPE_IDENTIFER = "class java.time.LocalDate";
private static final String LOCAL_DATE_TIME_TYPE_IDENTIFIER = "class java.time.LocalDateTime";
private static final String DATE_TYPE_IDENTIFIER = "class java.util.Date";
private static final String BYTE_ARRAY_TYPE_IDENTIFIER = "class [B";
private static final String RESOURCE_REFERENCE_TYPE_IDENTIFIER = "class org.apache.directory.scim.spec.schema.ResourceReference$ReferenceType";
private final static Map<Class<?>, Schema.Attribute.Type> CLASS_TO_TYPE = new HashMap<>() {{
put(String.class, Schema.Attribute.Type.STRING);
put(Character.class, Schema.Attribute.Type.STRING);
put(Integer.class, Schema.Attribute.Type.INTEGER);
put(int.class, Schema.Attribute.Type.INTEGER);
put(Double.class, Schema.Attribute.Type.DECIMAL);
put(double.class, Schema.Attribute.Type.DECIMAL);
put(Float.class, Schema.Attribute.Type.DECIMAL);
put(float.class, Schema.Attribute.Type.DECIMAL);
put(Boolean.class, Schema.Attribute.Type.BOOLEAN);
put(boolean.class, Schema.Attribute.Type.BOOLEAN);
put(LocalTime.class, Schema.Attribute.Type.DATE_TIME);
put(LocalDate.class, Schema.Attribute.Type.DATE_TIME);
put(LocalDateTime.class, Schema.Attribute.Type.DATE_TIME);
put(Date.class, Schema.Attribute.Type.DATE_TIME);
put(Instant.class, Schema.Attribute.Type.DATE_TIME);
put(byte[].class, Schema.Attribute.Type.BINARY);
}};

private Schemas() {}

Expand Down Expand Up @@ -120,7 +123,7 @@ private static List<Schema.Attribute> createAttributes(String urn, List<Field> f

log.debug("++++++++++++++++++++ Processing field " + f.getName());
if (sa == null) {
log.debug("Attribute " + f.getName() + " did not have a ScimAttribute annotation");
log.debug("Attribute {} did not have a ScimAttribute annotation", f.getName());
continue;
}

Expand All @@ -146,7 +149,7 @@ private static List<Schema.Attribute> createAttributes(String urn, List<Field> f

List<String> canonicalTypes = null;
Field [] enumFields = sa.canonicalValueEnum().getFields();
log.debug("Gathered fields of off the enum, there are " + enumFields.length + " " + sa.canonicalValueEnum().getName());
log.debug("Gathered fields of off the enum, there are {} {}", enumFields.length, sa.canonicalValueEnum().getName());

if (enumFields.length != 0) {

Expand Down Expand Up @@ -174,79 +177,39 @@ private static List<Schema.Attribute> createAttributes(String urn, List<Field> f
if (canonicalTypes.isEmpty() || (canonicalTypes.size() == 1 && canonicalTypes.get(0).isEmpty())) {
attribute.setCanonicalValues(null);
} else {
attribute.setCanonicalValues(new HashSet<String>(canonicalTypes));
attribute.setCanonicalValues(new HashSet<>(canonicalTypes));
}

attribute.setCaseExact(sa.caseExact());
attribute.setDescription(sa.description());

String typeName = null;
Class<?> typeClass;
if (Collection.class.isAssignableFrom(f.getType())) {
log.debug("We have a collection");
log.debug("Attribute: '{}' is a collection", attributeName);
ParameterizedType stringListType = (ParameterizedType) f.getGenericType();
Class<?> attributeContainedClass = (Class<?>) stringListType.getActualTypeArguments()[0];
typeName = attributeContainedClass.getTypeName();
typeClass = (Class<?>) stringListType.getActualTypeArguments()[0];
attribute.setMultiValued(true);
} else if (f.getType().isArray()) {
log.debug("We have an array");
Class<?> componentType = f.getType().getComponentType();
typeName = componentType.getTypeName();
attribute.setMultiValued(true);
log.debug("Attribute: '{}' is an array", attributeName);
typeClass = f.getType().getComponentType();

// special case for byte[]
if (typeClass == byte.class) {
typeClass = byte[].class;
} else {
attribute.setMultiValued(true);
}
} else {
typeName = f.getType().toString();
typeClass = f.getType();
attribute.setMultiValued(false);
}

// attribute.setType(sa.type());
boolean attributeIsAString = false;
log.debug("Attempting to set the attribute type, raw value = " + typeName);
switch (typeName) {
case STRING_TYPE_IDENTIFIER:
case STRING_TYPE:
case CHARACTER_ARRAY_TYPE_IDENTIFIER:
case BIG_C_CHARACTER_ARRAY_TYPE_IDENTIFIER:
log.debug("Setting type to String");
attribute.setType(Schema.Attribute.Type.STRING);
attributeIsAString = true;
break;
case INT_TYPE_IDENTIFIER:
case INTEGER_TYPE_IDENTIFIER:
log.debug("Setting type to integer");
attribute.setType(Schema.Attribute.Type.INTEGER);
break;
case FLOAT_TYPE_IDENTIFIER:
case BIG_F_FLOAT_TYPE_IDENTIFIER:
case DOUBLE_TYPE_IDENTIFIER:
case BIG_D_DOUBLE_TYPE_IDENTIFIER:
log.debug("Setting type to decimal");
attribute.setType(Schema.Attribute.Type.DECIMAL);
break;
case BOOLEAN_TYPE_IDENTIFIER:
case BIG_B_BOOLEAN_TYPE_IDENTIFIER:
log.debug("Setting type to boolean");
attribute.setType(Schema.Attribute.Type.BOOLEAN);
break;
case BYTE_ARRAY_TYPE_IDENTIFIER:
log.debug("Setting type to binary");
attribute.setType(Schema.Attribute.Type.BINARY);
break;
case DATE_TYPE_IDENTIFIER:
case LOCAL_DATE_TIME_TYPE_IDENTIFIER:
case LOCAL_TIME_TYPE_IDENTIFER:
case LOCAL_DATE_TYPE_IDENTIFER:
log.debug("Setting type to date time");
attribute.setType(Schema.Attribute.Type.DATE_TIME);
break;
case RESOURCE_REFERENCE_TYPE_IDENTIFIER:
log.debug("Setting type to reference");
attribute.setType(Schema.Attribute.Type.REFERENCE);
break;
default:
log.debug("Setting type to complex");
attribute.setType(Schema.Attribute.Type.COMPLEX);
}
log.debug("Attempting to set the attribute type, raw value = {}", typeClass);
Schema.Attribute.Type type = CLASS_TO_TYPE.getOrDefault(typeClass, Schema.Attribute.Type.COMPLEX);
attribute.setType(type);

if (f.getAnnotation(ScimResourceIdReference.class) != null) {
if (attributeIsAString) {
if (type == Schema.Attribute.Type.STRING) {
attribute.setScimResourceIdReference(true);
} else {
log.warn("Field annotated with @ScimResourceIdReference must be a string: {}", f);
Expand All @@ -260,14 +223,14 @@ private static List<Schema.Attribute> createAttributes(String urn, List<Field> f
if (refType.isEmpty() || (refType.size() == 1 && refType.get(0).isEmpty())) {
attribute.setReferenceTypes(null);
} else {
attribute.setType(Schema.Attribute.Type.REFERENCE);
attribute.setReferenceTypes(Arrays.asList(sa.referenceTypes()));
}

attribute.setRequired(sa.required());
attribute.setReturned(sa.returned());
attribute.setUniqueness(sa.uniqueness());

//if (sa.type().equals(Type.COMPLEX))
ScimType st = f.getType().getAnnotation(ScimType.class);

if (attribute.getType() == Schema.Attribute.Type.COMPLEX || st != null) {
Expand All @@ -291,7 +254,7 @@ private static List<Schema.Attribute> createAttributes(String urn, List<Field> f
}

attributeList.sort(Comparator.comparing(o -> o.name));
log.debug("Returning " + attributeList.size() + " attributes");
log.debug("Returning {} attributes", attributeList.size());
return attributeList;
}

Expand Down
Loading

0 comments on commit 8bc31b5

Please sign in to comment.