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:
- Class
AppleSauce
must extendcom.unboundid.scim2.common.BaseScimResource
. - Class
AppleSauce
must be annotated with@com.unboundid.scim2.common.annotations.Schema
. - Fields of
AppleSauce
that should appear in the JSON serialisation must be annotated with@com.unboundid.scim2.common.annotations.Attribute
. - Class
AppleSauce
must be annotated with@org.springframework.ldap.odm.annotations.Entry
(not really a issue, but mentioned for completeness' sake). - Fields of
AppleSauce
that should be written to LDAP attributes must be annotated with@org.springframework.ldap.odm.annotations.Attribute
. AppleSauce
must contain a field of typejavax.naming.Name
annotated with@org.springframework.ldap.odm.annotations.Id
.- 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 wantAppleSauce
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...
- Fields of class
AppleSauce
also carryjavax.validation
annotations, adding to the mess on fields. Would putting those annotations on getters in the interfaceAppleSauce
be a good idea? - What if I need to serialise
AppleSauce
into a different JSON format like plain JSON without SCIM? There could be a third implementation ofAppleSauce
,JsonAppleSauce
, but then the number of converters explodes. - 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?
@org.springframework.ldap.odm.annotations.Attribute
only having@Target(value=FIELD)
, I don't think that's possible...AppleSourceProxy
. Proxy pattern . Abstract yourself from the "implementation"