Remove parsing logic from ScimResource

Moves the parsing logic of extensions from ScimResource to a Jackson module
This should remove the need for a static ScimExtensionRegistry singleton
diff --git a/scim-server/src/main/java/org/apache/directory/scim/server/rest/AttributeUtil.java b/scim-server/src/main/java/org/apache/directory/scim/server/rest/AttributeUtil.java
index 7a4b541..a22a835 100644
--- a/scim-server/src/main/java/org/apache/directory/scim/server/rest/AttributeUtil.java
+++ b/scim-server/src/main/java/org/apache/directory/scim/server/rest/AttributeUtil.java
@@ -19,15 +19,11 @@
 
 package org.apache.directory.scim.server.rest;
 
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.directory.scim.server.exception.AttributeDoesNotExistException;
 import org.apache.directory.scim.server.exception.AttributeException;
-import org.apache.directory.scim.spec.json.ObjectMapperFactory;
 import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
 import org.apache.directory.scim.spec.resources.ScimExtension;
 import org.apache.directory.scim.spec.resources.ScimGroup;
@@ -64,17 +60,9 @@
     this.schemaRegistry = schemaRegistry;
 
     // TODO move this to a CDI producer
-    objectMapper = ObjectMapperFactory.getObjectMapper();
-    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-    objectMapper.setSerializationInclusion(Include.NON_NULL);
-
-    SimpleModule module = new SimpleModule();
-    module.addDeserializer(ScimResource.class, new ScimResourceDeserializer(this.schemaRegistry, this.objectMapper));
-    objectMapper.registerModule(module);
+    objectMapper = new ObjectMapperFactory(schemaRegistry).createObjectMapper();
   }
 
-  AttributeUtil() {}
-
   public <T extends ScimResource> T keepAlwaysAttributesForDisplay(T resource) throws AttributeException {
     return setAttributesForDisplayInternal(resource, Returned.DEFAULT, Returned.REQUEST, Returned.NEVER);
   }
diff --git a/scim-server/src/main/java/org/apache/directory/scim/server/rest/ObjectMapperFactory.java b/scim-server/src/main/java/org/apache/directory/scim/server/rest/ObjectMapperFactory.java
index 04d38fb..64bb88c 100644
--- a/scim-server/src/main/java/org/apache/directory/scim/server/rest/ObjectMapperFactory.java
+++ b/scim-server/src/main/java/org/apache/directory/scim/server/rest/ObjectMapperFactory.java
@@ -20,15 +20,20 @@
 package org.apache.directory.scim.server.rest;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
 import com.fasterxml.jackson.databind.module.SimpleModule;
-import org.apache.directory.scim.spec.resources.ScimResource;
-
 import jakarta.enterprise.inject.Produces;
 import jakarta.inject.Inject;
 import jakarta.ws.rs.ext.Provider;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
+import org.apache.directory.scim.spec.extension.ScimExtensionRegistry;
+import org.apache.directory.scim.spec.resources.ScimExtension;
+import org.apache.directory.scim.spec.resources.ScimResource;
+
+import java.io.IOException;
 
 /**
  * Creates and configures an {@link ObjectMapper} used for {@code application/scim+json} parsing.
@@ -46,14 +51,46 @@
   @Produces
   public ObjectMapper createObjectMapper() {
 
-    ObjectMapper objectMapper = org.apache.directory.scim.spec.json.ObjectMapperFactory.getObjectMapper();
+    ObjectMapper objectMapper = org.apache.directory.scim.spec.json.ObjectMapperFactory.getObjectMapper().copy();
     objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
     objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
 
-    SimpleModule module = new SimpleModule();
-    module.addDeserializer(ScimResource.class, new ScimResourceDeserializer(schemaRegistry, objectMapper));
-    objectMapper.registerModule(module);
-
+    objectMapper.registerModule(new ScimResourceModule(schemaRegistry));
     return objectMapper;
   }
+
+  static class ScimResourceModule extends SimpleModule {
+
+    public ScimResourceModule(SchemaRegistry schemaRegistry) {
+      super("scim-resources", Version.unknownVersion());
+      addDeserializer(ScimResource.class, new ScimResourceDeserializer(schemaRegistry));
+    }
+
+    @Override
+    public void setupModule(SetupContext context) {
+      super.setupModule(context);
+      context.addDeserializationProblemHandler(new UnknownPropertyHandler());
+    }
+  }
+
+  static class UnknownPropertyHandler extends DeserializationProblemHandler {
+    @Override
+    public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser p, JsonDeserializer<?> deserializer, Object beanOrClass, String propertyName) throws IOException {
+
+      if (beanOrClass instanceof ScimResource) {
+        ScimResource scimResource = (ScimResource) beanOrClass;
+        Class<? extends ScimResource> resourceClass = scimResource.getClass();
+        Class<? extends ScimExtension> extensionClass = ScimExtensionRegistry.getInstance().getExtensionClass(resourceClass, propertyName);
+
+        if (extensionClass != null) {
+          ScimExtension ext = ctxt.readPropertyValue(p, null, extensionClass);
+          if (ext != null) {
+            scimResource.addExtension(ext);
+          }
+        }
+      }
+      return super.handleUnknownProperty(ctxt, p, deserializer, beanOrClass, propertyName);
+    }
+  }
+
 }
diff --git a/scim-server/src/main/java/org/apache/directory/scim/server/rest/ScimResourceDeserializer.java b/scim-server/src/main/java/org/apache/directory/scim/server/rest/ScimResourceDeserializer.java
index 13280ac..16de41d 100644
--- a/scim-server/src/main/java/org/apache/directory/scim/server/rest/ScimResourceDeserializer.java
+++ b/scim-server/src/main/java/org/apache/directory/scim/server/rest/ScimResourceDeserializer.java
@@ -19,52 +19,39 @@
 
 package org.apache.directory.scim.server.rest;
 
-import java.io.IOException;
-
-import com.fasterxml.jackson.core.JsonLocation;
-import com.fasterxml.jackson.core.JsonParseException;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.TreeNode;
+import com.fasterxml.jackson.core.*;
 import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
 import com.fasterxml.jackson.databind.node.ArrayNode;
-
-import org.apache.directory.scim.spec.resources.ScimResource;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
+import org.apache.directory.scim.spec.resources.ScimResource;
 
-public class ScimResourceDeserializer extends JsonDeserializer<ScimResource> {
+import java.io.IOException;
+import java.util.Objects;
+import java.util.stream.StreamSupport;
+
+public class ScimResourceDeserializer extends StdDeserializer<ScimResource> {
   private final SchemaRegistry schemaRegistry;
-  private final ObjectMapper objectMapper;
 
-  public ScimResourceDeserializer(SchemaRegistry schemaRegistry, ObjectMapper objectMapper) {
+  public ScimResourceDeserializer(SchemaRegistry schemaRegistry) {
+    super(ScimResource.class);
     this.schemaRegistry = schemaRegistry;
-    this.objectMapper = objectMapper;
   }
 
   @Override
-  public ScimResource deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
-    ScimResource scimResource;
+  public ScimResource deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
     JsonLocation location = jsonParser.getCurrentLocation();
     TreeNode node = jsonParser.getCodec().readTree(jsonParser);
     ArrayNode schemas = (ArrayNode) node.get("schemas");
-    Class<? extends ScimResource> scimResourceClass = null;
 
-    for (JsonNode schemaUrnNode : schemas) {
-      String schemaUrn = schemaUrnNode.textValue();
-      scimResourceClass = schemaRegistry.findScimResourceClass(schemaUrn);
+    Class<? extends ScimResource> scimResourceClass = StreamSupport.stream(schemas.spliterator(), false)
+      .map(JsonNode::textValue)
+      .map(schemaRegistry::findScimResourceClass)
+      .filter(Objects::nonNull)
+      .findFirst()
+      .orElseThrow(() -> new JsonParseException(jsonParser, "Could not find a valid schema in: " + schemas + ", valid schemas are: " + schemaRegistry.getAllSchemaUrns(), location));
 
-      if (scimResourceClass != null) {
-        break;
-      }
-    }
-    if (scimResourceClass == null) {
-      throw new JsonParseException("Could not find a valid schema in: " + schemas + ", valid schemas are: " + schemaRegistry.getAllSchemaUrns(), location);
-    }
-    scimResource = objectMapper.readValue(node.toString(), scimResourceClass);
-
-    return scimResource;
+    return jsonParser.getCodec().treeToValue(node, scimResourceClass);
   }
 }
diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/rest/ObjectMapperFactoryTest.java b/scim-server/src/test/java/org/apache/directory/scim/server/rest/ObjectMapperFactoryTest.java
new file mode 100644
index 0000000..d43afc8
--- /dev/null
+++ b/scim-server/src/test/java/org/apache/directory/scim/server/rest/ObjectMapperFactoryTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.server.rest;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.directory.scim.core.schema.SchemaRegistry;
+import org.apache.directory.scim.server.utility.ExampleObjectExtension;
+import org.apache.directory.scim.spec.extension.ScimExtensionRegistry;
+import org.apache.directory.scim.spec.resources.ScimResource;
+import org.apache.directory.scim.spec.resources.ScimUser;
+import org.apache.directory.scim.spec.schema.ResourceType;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+public class ObjectMapperFactoryTest {
+
+  @Test
+  public void serialize() throws JsonProcessingException {
+
+    ScimExtensionRegistry.getInstance().registerExtension(ScimUser.class, ExampleObjectExtension.class);
+
+    ResourceType userType = new ResourceType();
+    userType.setId("user");
+    userType.setSchemaUrn(ScimUser.SCHEMA_URI);
+    userType.setName(ScimUser.RESOURCE_NAME);
+    SchemaRegistry schemaRegistry = new SchemaRegistry();
+    schemaRegistry.addSchema(ScimUser.class, userType, List.of(ExampleObjectExtension.class));
+
+    ScimResource resource = new ScimUser().setId("test1");
+    ExampleObjectExtension extension = new ExampleObjectExtension().setValueDefault("test-value");
+    resource.addExtension(extension);
+
+    ObjectMapper objectMapper = new ObjectMapperFactory(schemaRegistry).createObjectMapper();
+    String json = objectMapper.writeValueAsString(resource);
+
+    ScimResource actual = objectMapper.readValue(json, ScimResource.class);
+
+    Assertions.assertThat(actual).isEqualTo(resource);
+  }
+}
diff --git a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/resources/ScimResource.java b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/resources/ScimResource.java
index c84d9ca..4d54c72 100644
--- a/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/resources/ScimResource.java
+++ b/scim-spec/scim-spec-schema/src/main/java/org/apache/directory/scim/spec/resources/ScimResource.java
@@ -20,7 +20,6 @@
 package org.apache.directory.scim.spec.resources;
 
 import com.fasterxml.jackson.annotation.JsonAnyGetter;
-import com.fasterxml.jackson.annotation.JsonAnySetter;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -28,7 +27,6 @@
 import org.apache.directory.scim.spec.annotation.ScimExtensionType;
 import org.apache.directory.scim.spec.annotation.ScimResourceType;
 import org.apache.directory.scim.spec.exception.InvalidExtensionException;
-import org.apache.directory.scim.spec.extension.ScimExtensionRegistry;
 import org.apache.directory.scim.spec.json.ObjectMapperFactory;
 import org.apache.directory.scim.spec.schema.Meta;
 import org.apache.directory.scim.spec.schema.Schema.Attribute.Returned;
@@ -99,7 +97,7 @@
   public void addExtension(ScimExtension extension) {
     ScimExtensionType[] se = extension.getClass().getAnnotationsByType(ScimExtensionType.class);
 
-    if (se.length == 0 || se.length > 1) {
+    if (se.length != 1) {
       throw new InvalidExtensionException("Registered extensions must have an ScimExtensionType annotation");
     }
 
@@ -129,7 +127,7 @@
   private <T> ScimExtensionType lookupScimExtensionType(Class<T> extensionClass) {
     ScimExtensionType[] se = extensionClass.getAnnotationsByType(ScimExtensionType.class);
 
-    if (se.length == 0 || se.length > 1) {
+    if (se.length != 1) {
       throw new InvalidExtensionException("Registered extensions must have an ScimExtensionType annotation");
     }
 
@@ -147,28 +145,6 @@
     return extensions;
   }
 
-  @JsonAnySetter
-  public void setExtensions(String key, Object value) {
-    LOG.debug("Found a ScimExtension");
-    LOG.debug("Extension's URN: " + key);
-    LOG.debug("Extension's string representation: " + value);
-
-    Class<? extends ScimResource> resourceClass = getClass();
-    LOG.debug("Resource class: " + resourceClass.getSimpleName());
-
-    Class<? extends ScimExtension> extensionClass = ScimExtensionRegistry.getInstance().getExtensionClass(resourceClass, key);
-
-    if (extensionClass != null) {
-      LOG.debug("Extension class: " + extensionClass.getSimpleName());
-
-      ScimExtension extension = objectMapper.convertValue(value, extensionClass);
-      if (extension != null) {
-        LOG.debug("    ***** Added extension to the resource *****");
-        extensions.put(key, extension);
-      }
-    }
-  }
-  
   public ScimExtension removeExtension(String urn) {
     return extensions.remove(urn);
   }
@@ -179,5 +155,4 @@
     
     return (T) extensions.remove(se.id());
   }
-
 }