Intro
When using JAXB to convert Java Objects to XML strings we almost always rely on annotations on our model that define how the entities are mapped to XML.
For example, we have Book entities that can have multiple localizations, defined as attributes en=English and de=German:
<Book en="This is the first book" de="Dies ist das erste Buch">
.
.
.
</Book>
We could achieve that by the following code:
@XmlAccessorType(XmlAccessType.FIELD)
public class Book {
@XmlAttribute
private String en;
@XmlAttribute
private String de;
}
The problem here becomes clear if we want to add further languages. We´d always have to modify our entity and add more fields and even our business logic that accesses the entity to get the new translations in and out of the object.
Dynamic approach
What we instead want to do is having a one to many relationship between the book entity and translations.
We are picking up the example of </blog/how-to-quickly-expose-a-rest-api-from-your-repository-in-spring> we´ve used to expose a Translation API and use this entity to enhance a Book Node with multiple translations that should be exported as XML attributes like:
Our Translation Entity looks like this:
@Entity
@Table(name = "translation")
public class Translation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@CreationTimestamp
@Column(name = "insertTimestamp")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updateTimestamp")
private LocalDateTime updatedAt;
@Column(name = "identifier")
private String key;
@Column(name = "en")
private String en;
@Column(name = "de")
private String de;
}
We change the class above to represent a key-value based approach - new languages won´t require additional columns anymore that way.
@Entity
@Table(name = "translation")
public class Translation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@CreationTimestamp
@Column(name = "insertTimestamp")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updateTimestamp")
private LocalDateTime updatedAt;
@Column(name = "languageCode")
private String languageCode;
@Column(name = "translationText")
private String translationText;
}
We also change our book entity we want to apply translation attributes to. It is now getting a Map<QName, String> translations field and we annotate it with @XmlAnyAttribute. This will tell JAXB to generate attributes from the map given.
@XmlAccessorType(XmlAccessType.FIELD)
public class Book {
@XmlAttribute
private String name
@XmlAnyAttribute
private Map<QName, String> translations = new HashMap<>();
public void addTranslation(Translation translation) {
translations.put(new QName(trannslation.languageCode), translation.translationText);
}
}
We are creating an additional "setter" addTranslations just to have a more convenient API allowing us to pass Translations objects directly into the Book objects.
Our XML result will be the same as in the first example, but the attributes are getting added dynamically everytime we add new Translations to it.