I have a situation where multiple threads load the same entity Job and then each thread adds to its child collection Set<JobError>. I can understand this exception if the parent itself was updated, but the only 'change' to the parent is the addition to the collection, even then?
Parent Entity:
#Entity
#Table(name = "JOB")
public class Job extends BaseEntity {
private Set<JobError> jobErrors = new HashSet<JobError>();
/**
* #return the jobErrors
*/
#OneToMany(mappedBy = "job", cascade = { CascadeType.PERSIST,
CascadeType.MERGE, CascadeType.REMOVE })
public Set<JobError> getJobErrors() {
return jobErrors;
}
/**
* #param jobErrors
* the jobErrors to set
*/
public void setJobErrors(Set<JobError> jobErrors) {
this.jobErrors = jobErrors;
}
/**
* Helper to take care of both sides of the association
* #param message
* #param currentProfileId
*/
public void addError(String message, Long currentProfileId,
String firstName, String lastName) {
JobError er = new JobError(message, currentProfileId, firstName,
lastName, this);
jobErrors.add(er);
}
}
Child Entity:
#Entity
#Table(name = "JOB_ERROR")
public class JobError extends BaseEntity {
private Job job;
public JobError(String description, Long profileId, String firstName,
String lastName, Job job) {
this.description = description;
this.profileId = profileId;
this.firstName = firstName;
this.lastName = lastName;
this.job = job;
}
/**
*
*/
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "JOB_ID", nullable = false)
public Job getJob() {
return job;
}
/**
* #param jobErrors
* the jobErrors to set
*/
public void setJob(Job job) {
this.job = job;
}
}
Service Code, this runs in multiple concurrent threads:
job = jobDao.findById(er.getJobId(), false);
for (Long profileId : er.getProfileIds()) {
// do stuff
try {
sendEmail(emailTemplateDto, user);
} catch (RuntimeException re) {
job.addError(re.getLocalizedMessage(), currentProfileId, profile.getPersonalData().getFirstName(), profile.getPersonalData().getLastName());
}
Once the service method returns which is annotated as #Transactional(propagation = Propagation.REQUIRED) the StaleObjectStateException is thrown:
2013-03-28 13:22:52,578 ERROR org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(324): - Could not synchronize database state with session
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.test.project.domain.Job#2]
at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1950)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2594)
at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2494)
at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2821)
at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:113)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:273)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:265)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:185)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1216)
at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:383)
at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:133)
at org.hibernate.ejb.TransactionImpl.commit(TransactionImpl.java:76)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:467)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:754)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:723)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:393)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202)
at $Proxy162.processSendEmail(Unknown Source)
at com.test.project.service.messaging.EmailRequestMessageListener.onMessage(EmailRequestMessageListener.java:57)
at org.springframework.jms.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:560)
at org.springframework.jms.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:498)
at org.springframework.jms.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:467)
at org.springframework.jms.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:439)
at org.springframework.jms.listener.SimpleMessageListenerContainer.processMessage(SimpleMessageListenerContainer.java:311)
at org.springframework.jms.listener.SimpleMessageListenerContainer$2.onMessage(SimpleMessageListenerContainer.java:287)
at org.apache.activemq.ActiveMQMessageConsumer.dispatch(ActiveMQMessageConsumer.java:1321)
at org.apache.activemq.ActiveMQSessionExecutor.dispatch(ActiveMQSessionExecutor.java:131)
at org.apache.activemq.ActiveMQSessionExecutor.iterate(ActiveMQSessionExecutor.java:202)
at org.apache.activemq.thread.PooledTaskRunner.runTask(PooledTaskRunner.java:129)
at org.apache.activemq.thread.PooledTaskRunner$1.run(PooledTaskRunner.java:47)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
I can think of nothing else except for trying to save JobError directly. Currently I first load Job, add to the collection of JobError and then merge Job and hope the cascade.merge will take care of saving the child collection.
Any pointers will be appreciated.
I don't know if this is the cause of the exception in question, but if not it is going to cause problems down the line: HashSet is not a thread-safe collection, which means that if two threads call addError at the same time then one of the errors might not make it into the set. You'll either need to add the "synchronized" keyword to the addError method, or else you'll need to replace the HashSet with a thread-safe alternative, e.g. a ConcurrentLinkedQueue or ConcurrentHashMap
Related
I wonder if there is some integration of sleuth in hazelcast. In my application I have hazelcast queue with event listeners configured for addEntity events and problem is that span seems to be broken once this listener triggeres. I know that there is integration of sleuth for ExecutorService, but is there something similar for com.hazelcast.core.ItemListener? Thanks in advance.
UPD: Giving more details.
I have some sample service that uses both spring-cloud-sleth and hazelcast queue
package com.myapp;
import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IQueue;
import com.hazelcast.core.ItemEvent;
import com.hazelcast.core.ItemListener;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.sleuth.DefaultSpanNamer;
import org.springframework.cloud.sleuth.TraceRunnable;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
#Service
#Slf4j
public class SomeService {
private HazelcastInstance hazelcastInstance =
Hazelcast.newHazelcastInstance();
private IQueue<String> queue = hazelcastInstance.getQueue("someQueue");
private Tracer tracing;
#Autowired(required = false)
public void setTracer(Tracer tracer) {
this.tracing = tracer;
}
{
queue.addItemListener(new ItemListener<String>() {
#Override
public void itemAdded(ItemEvent<String> item) {
log.info("This is span");
log.info("This is item " + item);
}
#Override
public void itemRemoved(ItemEvent<String> item) {
}
}, true);
}
#Async
public void processRequestAsync() {
log.info("Processing async");
log.info("This is span");
Executors.newSingleThreadExecutor().execute(
new TraceRunnable(tracing, new DefaultSpanNamer(), () -> log.info("Some Weird stuff")));
queue.add("some stuff");
}
}
and once I call processRequestAsync I receive following output in console:
INFO [-,792a6c3ad3e91280,792a6c3ad3e91280,false] 9996 --- [nio-8080-exec-2] com.myapp.SomeController : Incoming request!
INFO [-,792a6c3ad3e91280,792a6c3ad3e91280,false] 9996 --- [nio-8080-exec-2] com.myapp.SomeController : This is current span [Trace: 792a6c3ad3e91280, Span: 792a6c3ad3e91280, Parent: null, exportable:false]
INFO [-,792a6c3ad3e91280,7d0c06d3e24a7ba1,false] 9996 --- [cTaskExecutor-1] com.myapp.SomeService : Processing async
INFO [-,792a6c3ad3e91280,7d0c06d3e24a7ba1,false] 9996 --- [cTaskExecutor-1] com.myapp.SomeService : This is span
INFO [-,792a6c3ad3e91280,8a2f0a9028f44979,false] 9996 --- [pool-1-thread-1] com.myapp.SomeService : Some Weird stuff
INFO [-,792a6c3ad3e91280,7d0c06d3e24a7ba1,false] 9996 --- [cTaskExecutor-1] c.h.i.p.impl.PartitionStateManager : [10.236.31.22]:5701 [dev] [3.8.3] Initializing cluster partition table arrangement...
INFO [-,,,] 9996 --- [e_1_dev.event-4] com.myapp.SomeService : This is span
INFO [-,,,] 9996 --- [e_1_dev.event-4] com.myapp.SomeService : This is item ItemEvent{event=ADDED, item=some stuff, member=Member [10.236.31.22]:5701 - b830dbf0-0977-42a3-a15d-800872221c84 this}
So looks like span was broked once we go to eventListener code and I wonder how can I propagate or create new span inside hazelcast queue
Sleuth (at the time of writing) does not support Hazelcast.
The solution is more general than just Hazelcast - you need to pass Zipkin's brave.Span between the client and server, but brave.Span is not serializable.
Zipkin provides a means by which to work around this.
Given a brave.Span on the client, you can convert it to a java.util.Map:
Span span = ...
Map<String, String> map = new HashMap<>();
tracing.propagation().injector(Map<String, String>::put).inject(span.context(), map);
On the server you can convert the java.util.Map back to a brave.Span:
Span span = tracer.toSpan(tracing.propagation().extractor(Map<String, String>::get).extract(map).context())
The use of java.util.Map can obviously be replaced as need be, but the principle is the same.
I can't get it to work for ItemListeners. I think we'd need to be able to wrap Hazelcast's StripedExecutor in something like a LazyTraceThreadPoolTaskExecutor (but one that accepts a plain Executor delegate instead of a ThreadPoolTaskExecutor).
For EntryProcessors, I've hacked this together. A factory to create EntryProcessors, passing in the current span from the thread that creates the processor. When the processor runs, it uses that span as the parent span in the executor thread.
#Component
public class SleuthedEntryProcessorFactory {
private final Tracer tracer;
public SleuthedEntryProcessorFactory(Tracer tracer) {
this.tracer = tracer;
}
/**
* Create an entry processor that will continue the Sleuth span of the thread
* that invokes this method.
* Mutate the given value as required. It will then be set on the entry.
*
* #param name name of the span
* #param task task to perform on the map entry
*/
public <K, V, R> SleuthedEntryProcessor<K, V, R> create(String name, Function<V, R> task) {
return new SleuthedEntryProcessor<>(name, tracer.getCurrentSpan(), task);
}
}
/**
* Copies the MDC context (which contains Sleuth's trace ID, etc.) and the current span
* from the thread that constructs this into the thread that runs this.
* #param <K> key type
* #param <V> value type
* #param <R> return type
*/
#SpringAware
public class SleuthedEntryProcessor<K, V, R> extends AbstractEntryProcessor<K, V> {
private final Map<String, String> copyOfContextMap;
private final String name;
private final Span parentSpan;
private final Function<V, R> task;
private transient Tracer tracer;
public SleuthedEntryProcessor(String name, Span parentSpan, Function<V, R> task) {
this(name, parentSpan, task, true);
}
public SleuthedEntryProcessor(
String name, Span parentSpan, Function<V, R> task, boolean applyOnBackup) {
super(applyOnBackup);
this.name = name + "Hz";
this.parentSpan = parentSpan;
this.task = task;
copyOfContextMap = MDC.getCopyOfContextMap();
}
#Override
public final R process(Map.Entry<K, V> entry) {
if (nonNull(copyOfContextMap)) {
MDC.setContextMap(copyOfContextMap);
}
Span span = tracer.createSpan(toLowerHyphen(name), parentSpan);
try {
V value = entry.getValue();
// The task mutates the value.
R result = task.apply(value);
// Set the mutated value back onto the entry.
entry.setValue(value);
return result;
} finally {
MDC.clear();
tracer.close(span);
}
}
#Autowired
public void setTracer(Tracer tracer) {
this.tracer = tracer;
}
}
Then pass the EntryProcessor to your IMap like this:
Function<V, R> process = ...;
SleuthedEntryProcessor<K, V, R> entryProcessor = sleuthedEntryProcessorFactory.create(label, process);
Map<K, R> results = iMap.executeOnEntries(entryProcessor);
I've written most of our project's jobs/pipelines in DSL without any previous groovy experience but now I'm stuck at more advanced problem that I can't figure out.
I'm trying to implement a method that would add 1,2,n promotions to a job.
Below you can see a fully-working method that can add one promotion, and I expected it to work in such way that I'd just call the method twice if I needed another one but then I ran into my problem - only promotion that was created the latest would be generated.
/**
* #param job DSL job object
* #param promotionName Name of the promotion
* #param nextJobs Comma seperated string of jobs to trigger when promotion is executed
* #param deployers Comma seperated string IDs that can execute promotion
* #param params Array of parameters to pass to the next job [0] = key, [1] = value.
*/
static void addPromotion(def job, String promotionName, String nextJobs, String deployers, String[][] params){
job.properties {
promotions {
promotion {
name(promotionName)
icon("star-gold")
conditions {
manual(deployers)
}
actions {
downstreamParameterized {
trigger(nextJobs) {
parameters {
for (String[] param : params){
predefinedProp(param[0]+"=",param[1])
}
}
}
}
}
}
}
}
}
The way it would work, however, if I added another 'promotion' closure like this, however, this example would generate almost identical(name and name-1) promotions:
static void addPromotion(def job, String promotionName, String nextJobs, String deployers, String[][] params){
job.properties {
promotions {
promotion {
name(promotionName)
icon("star-gold")
conditions {
manual(deployers)
}
actions {
downstreamParameterized {
trigger(nextJobs) {
parameters {
for (String[] param : params){
predefinedProp(param[0]+"=",param[1])
}
}
}
}
}
}
promotion {
name("${promotionName}-1")
icon("star-gold")
conditions {
manual(deployers)
}
actions {
downstreamParameterized {
trigger(nextJobs) {
parameters {
for (String[] param : params){
predefinedProp(param[0]+"=",param[1])
}
}
}
}
}
}
}
}
}
Is it possible to re-use closures in some way and populate the variables from a different method maybe? Or any other ideas?
This is how I solved it.
Generic promotion object part:
/**
* Adds 'promoted-builds' plugin configuration to job
**/
class Promotions {
public def job
public String promotionName
public String nextJobs
public String deployers
public String [][] params
/**
* #param job DSL job object
* #param promotionName Name of the promotion
* #param nextJobs Comma seperated string of jobs to trigger when promotion is executed
* #param deployers Comma seperated string IDs that can execute promotion
* #param params Array of parameters to pass to the next job [0] = key, [1] = value.
*/
public Promotions(Object jobName, String promotionName, String nextJobs, String deployers, String[][] params){
this.job = jobName
this.promotionName = promotionName
this.nextJobs = nextJobs
this.deployers = deployers
this.params = params
}
static void addPromotions(Promotions ... jobPromotions){
// Assuming the same job is provided as arguments
jobPromotions[0].job.properties {
promotions {
for (Promotions jobPromotion : jobPromotions){
promotion {
name(jobPromotion.promotionName)
// star-gold, star-silver
icon("star-gold")
conditions {
manual(jobPromotion.deployers)
}
actions {
downstreamParameterized {
trigger(jobPromotion.nextJobs) {
parameters {
for (String[] param : jobPromotion.params){
predefinedProp(param[0],param[1])
}
}
}
}
}
}
}
}
}
}
}
And then I prepare my params and pass them to the promotion constructor, and in the end I call addPromotions() and pass all my constructed objects to it:
def nextJobs = "${Configuration.repoName}-${branchName}-deploy-to-perf"
def deployers = "developer"
def params = [["VERSION", "\${VERSION}"],
["SOURCE_GIT_COMMIT", "\${SOURCE_GIT_COMMIT}"]] as String[][]
def promo1 = new Promotions(job, "Promote to PERF", nextJobs, deployers, params)
def nextJobs2 = "../master/${Configuration.repoName}-${branchName}-to-prod-dtr"
def deployers2 = "admin"
def params2 = [["VERSION", "\${VERSION}"],
["SOURCE_GIT_COMMIT", "\${SOURCE_GIT_COMMIT}"]] as String[][]
def promo2 = new Promotions(job, "Promote to PROD", nextJobs2, deployers2, params2)
Promotions.addPromotions(promo1, promo2)
Disclaimer: I'm completely new to Event Sourcing, Axon Framework and DDD, so there is quite big possibility I'm doing something wrong.
I'm doing an application to manage events - conferences, workshops etc.
I have one aggregate root, which is EventProposal. There should be a possibility to assign a TodoList to given EventProposal. TodoList is another aggregate root, consisting of TodoItems - entities. TodoItem can be edited, mark as done etc.
There is a requirement of assigning TodoItem to TodoList, which I've implemented this way:
public class TodoList extends AbstractAnnotatedAggregateRoot {
#AggregateIdentifier
private TodoListId id;
#EventSourcedMember
private List<TodoItem> todoItems = Lists.newArrayList();
...
public void assignTodoItem(TodoItemId todoItemId, String content, LocalDate creationDate) {
if (alreadyHasTodoItemWith(todoItemId)) {
apply(new TodoItemNotAssignedToTodoList(
id, todoItemId)
);
return;
}
apply(new TodoItemAssignedToTodoListEvent(
id, todoItemId, content, creationDate)
);
}
#EventSourcingHandler
public void on(TodoItemAssignedToTodoListEvent event) {
final TodoItem item = TodoItemFactory.create(
event.todoItemId(),
event.description(),
event.createdAt()
);
todoItems.add(item);
}
Corresponding success-path command and event:
#Value
#Accessors(fluent = true)
public class AssignTodoItemToTodoListCommand {
#TargetAggregateIdentifier
private final TodoListId todoListId;
private final TodoItemId todoItemId;
private final String description;
private final LocalDate createdAt;
}
#Value
#Accessors(fluent = true)
public class TodoItemAssignedToTodoListEvent {
private final TodoListId todoListId;
private final TodoItemId todoItemId;
private final String description;
private final LocalDate createdAt;
}
This could be easly tested with Axon's BDD-way. (GivenThenFixture)
But now - there is another requirement: one should be able to create TodoList upon existing TodoListTemplate. Template is just an aggregate that wrapps a collection of TodoItemTemplates.
And there comes my implementation problem. I tried something like (in TodoList class):
public void fulfillWith(TodoListTemplate todoListTemplate, LocalDate creationDate) {
if (alreadyHasAnyTodoItem()) {
apply(new TodoListNotFulfilledWithTemplateEvent(
id,
todoListTemplate.id()
)
);
return;
}
apply(new TodoListFulfilledWithTemplateEvent(
id,
todoListTemplate.id(),
todoListTemplate.todoItemDescriptions(),
creationDate
)
);
}
#EventSourcingHandler
public void on(TodoListFulfilledWithTemplateEvent event) {
todoItems.addAll(
fromDescriptions(event.todoItemDescriptions(), event.fulfilledAt())
);
}
private Collection<TodoItem> fromDescriptions(Collection<String> descriptions, LocalDate creationDate) {
return descriptions.stream()
.map(description -> TodoItemFactory.create(description, creationDate))
.collect(Collectors.toList());
}
Again, commands and events:
#Value
#Accessors(fluent = true)
public class FulfillTodoListWithTemplateCommand {
private final TodoListId todoListId;
private final TodoListTemplateId todoListTemplateId;
private final LocalDate creationDate;
}
#Value
#Accessors(fluent = true)
public class TodoListFulfilledWithTemplateEvent {
private final TodoListId todoListId;
private final TodoListTemplateId todoListTemplateId;
private final List<String> todoItemDescriptions;
private final LocalDate fulfilledAt;
}
Problem:
As you can see - there is TodoItemFactory class involved, generating an unique ID:
public static TodoItem create(String content, LocalDate createdAt) {
return TodoItemFactory.create(nextId(), content, createdAt);
}
This way it can't be tested with axon - it gives me error org.axonframework.test.AxonAssertionError: Illegal state change detected!
This is quite obvious - working aggregate's ID is different than the one's constructed after aplying events.
Finally, my question arrives:
How should I solve this?
Generate those IDs somewhere and include them in outcoming TodoListFulfilledWithTemplateEvent? This leads to having two collections in an event - one for IDs, one for item contents/descriptions.
Assume that I got those IDs generated before and include them not only in outcoming Event, but also in incoming Command. This leads to same ugliness as before, but twice.
Do it in the way that I invoke 'assingTodoItemToTodoList' method multiple times, which will generate a lot of events and could produce reordering as events are async.
Sorry for verboseness, I tried to be as specific as possible.
My POJO:
public class Album{
private String title;
private Object tracks; // I can not change the type, beyond my control..
/** setter **/
....
/** getter **/
....
}
public class Track{
private String title;
private String singer;
/** setter **/
....
/** getter **/
....
}
Main method:
public static void main(String[] args)
{
Album album = new Album ();
album.setTitle("Thriller");
Track track = new Track();
track.setTitle("Beat It");
track.setSinger("M.J");
List<Track> trackLst = new ArrayList<Track>();
trackLst.add(track);
Album.setTracks(trackLst);
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT, As.PROPERTY);
m.writeValue(System.out, album);
}
/** console printed **/
{
"title": "Thriller";
"tracks":[
"java.util.ArrayList",
[
{
"#class":"com.hs.Track",
"title":"Beat It",
"singer":"M.J"
}
]
]
}
As you can see, tracks serialized to a jsonArray, one element is the type(ArrayList), the other is the real jsonArray. Is there any solution that just keep the real jsonArray? like this:
{
"title": "Thriller";
"tracks":
[
{
"#class":"com.hs.Track",
"title":"Beat It",
"singer":"M.J"
}
]
}
The problem is type declaration private Object tracks, which causes property to match definition and force use of type information for the list as well. If it was declared as List<?>, this would not occur.
One thing you may be able to do is to use "mix-in annotations", to associate annotation like:
public class MixIn {
#JsonDeserialize(as=List.class)
#JsonSerialize(as=List.class)
private Object tracks;
}
and register mix-in to apply to class Album. This should indicate that intended type is List, and avoid inclusion of type information
For the follwing example XML input:
<Participants course="someCourse">
<workers>
<Worker ref="p3">
<Worker ref="p2">
</workers>
<Trainer ref="p1"/>
</Participants>
<Group id="group1" name="some mixed Person group">
<trainers>
<Trainer id="p1" name="John Doe">
</trainers>
<workers>
<Worker id="p2" name="Jim Scott">
<Worker id="p3" name="Walter Peace">
</workers>
</Group>
I am trying to make sure that the PersonList in Participants points to the Persons read from group1. (see code snipptes below for the JaxB annotations used). This is just an example for the more generic
approach I am seeking. I need to be generally able to follow id="" and ref="" attributes in a way
that the list elements are correctly unmarshalled as references.
With an UnmarshalListener and Unmarshalling twice I get around the problem of the references from the ref attribute to the id attribute. In the first phase the lookup Map is filled from the id attributes. In the second phase the refs are looked up. Unfortunately this solution will create copies instead of references. I could use the parent object to fix this but I am looking for a more generic solution. What would be a good way to achieve the proper dereferencing using ref/id attributes in the manner shown?
/**
* intercept the unmarshalling
*/
public static class ModelElementMarshallerListener extends javax.xml.bind.Unmarshaller.Listener {
public Map<String,Person> lookup=new HashMap<String,Person>();
#Override
public void afterUnmarshal(java.lang.Object target, java.lang.Object parent) {
if (target instanceof Person) {
person=(Person) target;
if (person.getId()!=null) {
lookup.put(person.getId(), person);
}
if (person.getRef()!=null) {
if (lookup.containsKey(person.getRef())) {
Person personRef=lookup.get(person.getRef());
person.copyFrom(personRef);
person.setRef(null);
}
}
}
}
}
#XmlRootElement(name="Participants")
public class Participants {
private List<Worker> workers;
/**
* getter for List<Worker> workers
* #return workers
*/
#XmlElementWrapper(name="workers")
#XmlElement(name="Worker", type=Worker.class)
public List<Worker> getWorkers() {
return workers;
}
...
}
#XmlRootElement(name="Group")
public class Group {
private List<Worker> workers;
/**
* getter for List<Worker> workers
* #return workers
*/
#XmlElementWrapper(name="workers")
#XmlElement(name="Worker", type=Worker.class)
public List<Worker> getWorkers() {
return workers;
}
...
}
#XmlRootElement(name="Trainer")
public class Trainer extends Person {}
#XmlRootElement(name="Worker")
public class Worker extends Person {}
#XmlRootElement(name="Person")
public class Person {
private String name;
/**
* getter for xsd:string/String name
* #return name
*/
#XmlAttribute(name="name")
public String getName() {
return name;
}
public void setName(String name) {
this.name=name;
}
private String ref;
/**
* getter for xsd:string/String id
* #return id
*/
#XmlAttribute(name="ref")
public String getRef() {
return ref;
}
public void setRef(String ref) {
this.ref=ref;
}
private String id;
/**
* getter for xsd:string/String id
* #return id
*/
#XmlAttribute(name="id")
#XmlID
public String getId() {
this.id;
}
/**
* setter for xsd:string/String id
* #param pid - new value for id
*/
public void setId(String pid) {
this.id=pid;
}
}
To better illustrate the point I have modified the question to fit his answer. There is now a generic base class Person and I am trying to use it as per Can generic XmlAdapter be written
I solved the issue of being able to actually make sure the Adapters are used by writing specific derived Classes and using them with #XmlJavaTypeAdapter. I preregister the adapters using:
JAXBContext context = JAXBContext.newInstance(type);
Unmarshaller u = context.createUnmarshaller();
u.setAdapter(Worker.WorkerAdapter.class,new Worker.WorkerAdapter());
u.setAdapter(Trainer.TrainerAdapter.class,new Trainer.TrainerAdapter());
and then unmarshalling twice. The debug shows that the Adapter instance for both passes is the same. Still the lookup somehow seemed to fail ... The reason was the way the #XmlJavaTypeAdapter annotation works see:
What package-info do I annotate with XmlJavaTypeAdapters?
There seem to be multiple modes for #XmlJavaTypeAdapter:
it can be an annotation for a class
it can be an annotation for a field (getter)
it can be used in a package-info.java file to annotate a whole package
At this point I am using all three annotations and now have to debug which ones are necessary. I assume the global annotations (class,package) are not working as expected. The reason might be the type= usage in the #XmlElementWrapper which explicitly calls for a type. Personally I do not understand what is going on yet. At least things are now working as expected.
the local field annotation is now e.g.:
#XmlElementWrapper(name="workers")
#XmlElement(name="Worker", type=Worker.class)
#XmlJavaTypeAdapter(WorkerAdapter.class)
the package-info.java annotation is:
#XmlJavaTypeAdapters({
#XmlJavaTypeAdapter(value=WorkerAdapter.class,type=Worker.class),
#XmlJavaTypeAdapter(value=TrainerAdapter.class,type=Trainer.class),
})
package com.bitplan.jaxb.refidtest;
import javax.xml.bind.annotation.adapters.*;
the class annotation is:
#XmlJavaTypeAdapter(Worker.WorkerAdapter.class)
public class Worker extends Person {
...
/**
* Worker Adapter
* #author wf
*
*/
public static class WorkerAdapter extends PersonAdapter<Worker>{
#Override
public Worker marshal(Worker me)
throws Exception {
return super.marshal(me);
}
#Override
public Worker unmarshal(Worker me) throws Exception {
return super.unmarshal(me);
}
}
/**
* https://stackoverflow.com/questions/7587095/can-jaxb-marshal-by-containment-at-first-then-marshal-by-xmlidref-for-subsequen/7587727#7587727
* #author wf
*
*/
public class PersonAdapter<T extends Person> extends XmlAdapter<T, T>{
public boolean debug=true;
/**
* keep track of the elements already seen
*/
public Map<String,T> lookup=new HashMap<String,T>();
#Override
public T marshal(T me)
throws Exception {
return me;
}
/**
* show debug information
* #param title
* #param key
* #param me
* #param found
*/
public void showDebug(String title,String key,T me, T found) {
String deref="?";
if (found!=null)
deref="->"+found.getId()+"("+found.getClass().getSimpleName()+")";
if (debug)
System.err.println(title+": "+key+"("+me.getClass().getSimpleName()+")"+deref+" - "+this);
}
#Override
public T unmarshal(T me) throws Exception {
if (me.getId()!=null) {
showDebug("id",me.getId(),me,null);
lookup.put(me.getId(), me);
return me;
}
if (me.getRef()!=null) {
if (lookup.containsKey(me.getRef())) {
T meRef=lookup.get(me.getRef());
showDebug("ref",me.getRef(),me,meRef);
me.setRef(null);
return meRef;
} else {
if (debug)
showDebug("ref",me.getRef(),me,null);
}
}
return me;
}
}