5

The framework mix problem

I'm using two frameworks -- PingIdentity's SCIM 2 SDK and Spring LDAP -- to deserialise a SCIM resource (i.e. JSON) to a Java object then write it to an LDAP directory, and the other way around. The trouble is, both frameworks want to annotate the fields of my Java data class and this is getting very messy. Is there a good design to split those foreign requirements on my class -- coming from the frameworks -- so they do not mix or even conflict in one class? I imagine this case of "JSON <-> object <-> storage" transfer is rather common...

Code example

Let's make my example more concrete with some code. First, here are the requirements on my class, say AppleSauce, imposed by the frameworks:

  1. Class AppleSauce must extend com.unboundid.scim2.common.BaseScimResource.
  2. Class AppleSauce must be annotated with @com.unboundid.scim2.common.annotations.Schema.
  3. Fields of AppleSauce that should appear in the JSON serialisation must be annotated with @com.unboundid.scim2.common.annotations.Attribute.
  4. Class AppleSauce must be annotated with @org.springframework.ldap.odm.annotations.Entry (not really a issue, but mentioned for completeness' sake).
  5. Fields of AppleSauce that should be written to LDAP attributes must be annotated with @org.springframework.ldap.odm.annotations.Attribute.
  6. AppleSauce must contain a field of type javax.naming.Name annotated with @org.springframework.ldap.odm.annotations.Id.
  7. Both SCIM 2 SDK and Spring LDAP want AppleSauce to be a mutable JavaBean with getters and setters for all fields relevant to them. I want AppleSauce to be an immutable value object.

This is what AppleSauce looks like with just one interesting field email (my real class has about 45 fields):

// imports omitted

@com.unboundid.scim2.common.annotations.Schema(id = "MY_SCHEMA_URN", name = "AppleSauce", description = "delicious stuff to go with cake")
@org.springframework.ldap.odm.annotations.Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }, base = "ou=sauces,dc=example,dc=org")
public class AppleSauce extends com.unboundid.scim2.common.BaseScimResource {
  @org.springframework.ldap.odm.annotations.Id javax.naming.Name dn;

  @com.unboundid.scim2.common.annotations.Attribute(description = "email address", multiValueClass = String.class, isRequired = true)
  @org.springframework.ldap.odm.annotations.Attribute(name = "mail", syntax = "1.3.6.1.4.1.1466.115.121.1.26{256}")
  private List<String> email;

  // getters and setter omitted
}

A possible solution

My initial thought is to extract an interface containing all getters (this also has the advantage of providing a read-only view of the class) and two classes implementing it, each containing the same fields with annotations for the respective frameworks. Something like this:

public interface AppleSauce {
  List<String> getEmail();
}

@com.unboundid.scim2.common.annotations.Schema(id = "MY_SCHEMA_URN", name = "AppleSauce", description = "delicious stuff to go with cake")
public class ScimAppleSauce extends com.unboundid.scim2.common.BaseScimResource implements AppleSauce {

  @com.unboundid.scim2.common.annotations.Attribute(description = "email address", multiValueClass = String.class, isRequired = true)
  private List<String> email;

  @Override
  List<String> getEmail() {
    return this.email;
  }
}

@org.springframework.ldap.odm.annotations.Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }, base = "ou=sauces,dc=example,dc=org")
public class LdapAppleSauce implements AppleSauce {
  @org.springframework.ldap.odm.annotations.Id javax.naming.Name dn;

  @org.springframework.ldap.odm.annotations.Attribute(name = "mail", syntax = "1.3.6.1.4.1.1466.115.121.1.26{256}")
  private List<String> email;

  @Override
  List<String> getEmail() {
    return this.email;
  }
}

The downside of this approach is that it requires extra code to convert instances of ScimAppleSauce to LdapAppleSauce (copying lots of fields) and vice-versa, because neither framework will correctly work with the AppleSauce type. I am not too worried about the getters/setters boilerplate thanks to Project Lombok annotations, but manually copying 45 fields is a very boring and error-prone perspective.

Bonus difficulty

Of course, there is more to this story...

  1. Fields of class AppleSauce also carry javax.validation annotations, adding to the mess on fields. Would putting those annotations on getters in the interface AppleSauce be a good idea?
  2. What if I need to serialise AppleSauce into a different JSON format like plain JSON without SCIM? There could be a third implementation of AppleSauce, JsonAppleSauce, but then the number of converters explodes.
  3. I don't like the mutability of JavaBeans and would very much prefer all objects to be immutable.

Is there a clean design to solve this "framework mix" problem?

  • Is it a hard requirement that the data stored in LDAP matches exactly 1-on-1 with the data provided in SCIM, or is there any possibility that they might be different in the future and a conversion between the two needs to be performed? – Bart van Ingen Schenau Aug 27 '18 at 13:12
  • @BartvanIngenSchenau No, they mostly but don't exactly match. For example, there are some LDAP fields that are currently not provided in the SCIM representation. – Kolargol00 Aug 27 '18 at 14:06
  • Is it possible to create inner classes that use the annotations and then have the outer class delegate necessary fields to the inner classes? – kiltek Nov 1 '18 at 9:46
  • @kiltek With @org.springframework.ldap.odm.annotations.Attribute only having @Target(value=FIELD), I don't think that's possible... – Kolargol00 Nov 13 '18 at 9:57
  • Your approach seems ok. Why don't you just implement a AppleSourceProxy. Proxy pattern . Abstract yourself from the "implementation" – Laiv Dec 12 '18 at 14:23
1

I would suggest keeping your single messy object on the API layer (maybe prefix it with API, or suffix it with JSON or JO?), and then have domain model classe separately that your system works with.

Using this approach, if you wanted to have an Email class for email fields for example, that can be done. And validation on the email could be done within the Email class.

I understand it might feel like you're repeating yourself, but you can easily use a bean mapper to seamlessly go from one object to the other, and end up with a clean validated domain object.

0

I think the best approach might be to design a domain-specific language (DSL) for describing these serializable resources. The parser for this DSL can output some intermediate representation. You can then implement some simple code generators which take the intermediate representation and generate properly-annotated Java classes:

DSL ---> IR --+-> SCIM2 Code Generator ---> SCIM2 annotated class
              |
              +-> LDAP Code Generator ---> LDAP annotated class

For example, you can use JavaCC. JavaCC will generate a parser in Java, which you can define such that it contains a parse() method that outputs List<Resource>. Resource here is an object you define, which can contain a list of fields, their types, etc. Resource just needs enough information to be serialized into the annotated Java class.

You should be able to integrate your code generation with things like Maven pretty easily, and IDEs like Intellij can be configured to recognize generated sources. This means the generated classes will be recognized once you try to use them in the rest of your code later.

  • This would make my business logic dependent on concrete types ScimAppleSauce and LdapAppleSauce which it should not have to care about. I would prefer to keep these details in their respective serialisation layers. Moreover, writing a DSL seems rather complicated... – Kolargol00 Aug 30 '18 at 14:35
  • I definitely agree that this DSL approach is not easy. However, you can have the generated annotated classes conform to an interface, and have your business logic depend on that interface instead. Thus, I am not sure dependencies would be an issue here. – scottyseus Aug 30 '18 at 14:43
  • One alternate approach I thought of that seems hacky is to create your own annotations, and then use reflection to analyze them and then generate SCIM/LDAP annotated classes at runtime. I am not sure if this is even possible, but it would allow you to only maintain one annotated 'copy' of each of your resources. – scottyseus Aug 30 '18 at 14:46
  • You could also skip annotations entirely. Define a single class for each resource, and then e.g. define Dozer mappings in XML from the resource to each of the JSON objects you need. But with this solution, you aren't necessarily saving yourself much configuration - you are just moving it around. – scottyseus Aug 30 '18 at 14:51
0

A possible solution would be to follow a Clean Architecture approach.

You would have your AppleSauce entity class free of framework annotations of any kind - i.e., a simple POJO. Then, you would have a different AppleSauce class for each framework, containing only the needed annotations for the corresponding framework, along with mapper classes that would map these to your entity class.

If you're willing to live with the fact that the number of classes might start to grow rapidly, you get the benefits of Single Responsibility classes and decoupled, cohesive code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.