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?
3 Answers 3
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.
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
andLdapAppleSauce
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– Kolargol002018年08月30日 14:35:14 +00:00Commented Aug 30, 2018 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.scottysseus– scottysseus2018年08月30日 14:43:26 +00:00Commented Aug 30, 2018 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.scottysseus– scottysseus2018年08月30日 14:46:08 +00:00Commented Aug 30, 2018 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.scottysseus– scottysseus2018年08月30日 14:51:14 +00:00Commented Aug 30, 2018 at 14:51
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.
Explore related questions
See similar questions with these tags.
@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"