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);
Related
Hi my code works with multiple triggers and i am trying to pass specific parameters associated with
each trigger using jobDataMap.But when i am trying to assign the map in my config.groovy to
jobDataMap i get a nullpointerexception
**This is the Map in my Config.groovy-->**
Query
{
Map
{
time.'0/5 * * * * ?' = ['T1']
time.'0/10 * * * * ?' = ['T2']
templates.'T1' = ['Date','FinshDate','Location']
templates.'T2' = ['TableName']
parameterValues.'T1' = ['2014071600','2014072000','Path']
parameterValues.'T2' = ['AppleData']
}
}
**This is my Quartz Job Code for multiple triggers ->**
import org.quartz.*
import org.quartz.Trigger
import static org.quartz.JobBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
import static org.quartz.TriggerBuilder.*;
import org.quartz.impl.StdSchedulerFactory;
import org.codehaus.groovy.grails.commons.GrailsApplication;
public class TrialJob
{
public static void main(String[] args)
{
String JobName
String GroupName
GrailsApplication grailsApplication;
Trigger trigger
def triggerList=[]
def jobList=[]
def cronList=["0/5 * * * * ?","0/10 * * * * ?","0/15 * * * * ?"]
// here i am creating 3 triggers which works fine
for(i in 0..2)
{
JobName="trigger"+Integer.toString(i)
GroupName = "Group"+Integer.toString(i)
println cronList[i]
JobDetail job = JobBuilder.newJob(TestJob.class).withIdentity(JobName,GroupName).build();
trigger= TriggerBuilder.newTrigger().withIdentity(JobName,GroupName).withSchedule(CronScheduleBuilder.cronSchedule(cronList[i])).build();
triggerList.add(trigger)
jobList.add(job)
}
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
for(j in 0..2)
{
// here i want to put the associated parameters for each trigger in the trigger list
// For Example 1) trigger 0--> triggerList[0].jobDataMap.put(['Date','FinshDate','Location'],['2014071600','2014072000','Path'])
// 2) trigger 1--> triggerList[1].jobDataMap.put(['TableName'],['AppleData'])
scheduler.scheduleJob(jobList[j],triggerList[j]);
println "torpido"
println j
}
//while(true){};
}
public static class TestJob implements Job
{
public void execute(JobExecutionContext context) throws JobExecutionException
{
HashMap<String, String> parameter = new HashMap();
parameter=context.getMergedJobDataMap()
println "Inside Execute"
}
}
}
how do i use jobDataMap inside the above for loop (it would be more
clear by looking at the comments inside the for loop) and access them
inside the execute method ?
I'm not grails expert but it seems that grails quartz scheduler plugin should be used.
Below You can find working code:
#Grab(group='org.quartz-scheduler', module='quartz', version='2.2.1')
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class CronTriggerExample {
public static void main( String[] args ) throws Exception {
def cronExpressions = ['0/5 * * * * ?', '0/10 * * * * ?', '0/20 * * * * ?']
def triggers = cronExpressions.collect { cron ->
TriggerBuilder
.newTrigger()
.withIdentity("trigger-$cron", "trigger-$cron-group")
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.usingJobData(new JobDataMap(['cron': cron]))
.build()
}
Scheduler scheduler = new StdSchedulerFactory().getScheduler()
scheduler.start()
triggers.each { trigger ->
def job = JobBuilder.newJob(HelloJob).withIdentity("$trigger.key.name-job", "$trigger.key.name-job-group").build()
scheduler.scheduleJob(job, trigger)
}
while(true){}
}
}
public class HelloJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
println "Hello Quartz with cron: ${context.mergedJobDataMap.getString('cron')}"
}
}
Job name, job group as well trigger name, trigger group must be unique. Other objects can be passed with JobDataMap. Is that clear now?
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
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;
}
}
I am trying to create a generic convertor using Spring 3.0's Type Conversion feature for converting Strings in format "<KEY2>:<VAL>,<KEY2>:<VAL2>,<KEY3>:<VAL3>"
to a Map holding the key-value pairs where key can be an Enum type and value can be of any user-defined or Java's inbuilt type.
Below is the code I tried out.
Note: I am not good at using generics, so please bear with me if I have used generics in wrong way.
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
/**
* Converts a string in a format
* "<KEY2>:<VAL>,<KEY2>:<VAL2>,<KEY3>:<VAL3>"
* to a {#link Map} instance holding the key-value pairs <i>where key
* can be an Enum type and value can be of any type</i>.
*
* #author jigneshg
*
* #param <K>
* #param <V>
*/
public class StringToMapConvertorFactory<V> implements ConverterFactory<String, Map<Enum<?>, V>> {
private static final String COMMA = ",";
private static final String COLON = ":";
#Override
public <T extends Map<Enum<?>, V>> Converter<String, T> getConverter(
Class<T> targetType) {
return new StringToMapConverter<T>(targetType);
}
private final class StringToMapConverter<T extends Map<Enum<?>, V>> implements Converter<String,T> {
public StringToMapConverter(Class<T> targetType) {
}
#SuppressWarnings("unchecked")
#Override
public T convert(String source) {
checkArg(source);
String[] keyValuePairs = source.split(COMMA);
// value at index 0 is assumed as the key
// value at index 0 is assumed as the value
Map<Enum<?>,V> resultMap = new HashMap<Enum<?>, V>();
String[] keyValueArr = null;
String key = null;
String value = null;
for (String keyValuePair : keyValuePairs) {
keyValueArr = keyValuePair.split(COLON);
key = keyValueArr[0];
value = keyValueArr[1];
// TODO: How to specify the enumType here ??
resultMap.put(Enum.valueOf(enumType, key.trim()), (V) value);
}
return resultMap;
}
private void checkArg(String source) {
// In the spec, null input is not allowed
if (source == null) {
throw new IllegalArgumentException("null source is in allowed");
}
}
}
}
I am stuck at how to specify the enum type when putting the key-value pair in resultMap in my code.
Also have I taken the correct approach to implement my requirement or there is a better way in which this can be achieved?
Thanks,
Jignesh
You can implement GenericConverter instead of Converter, and access key type as TypeDescriptor.getMapKeyType().
Please take a look at the test class below. I am trying to do an LDAP search with Spring LDAP Template. I am able to search and produce a list of entries corresponding to the search criteria without the Spring LDAP template by using the DirContext as shown in the method searchWithoutTemplate(). But when I use a LdapTemplate, I end up with a NPE as shown further below. I am sure I must be missing something. Can someone help please?
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.springframework.ldap.core.support.LdapContextSource;
public class LDAPSearchTest {
//bind params
static String url="ldap://<IP>:<PORT>";
static String userName="cn=Directory Manager";
static String password="password123";
static String bindDN="dc=XXX,dc=com";
//search params
static String base = "ou=StandardUser,ou=XXXCustomers,ou=People,dc=XXX,dc=com";
static String filter = "(objectClass=*)";
static String[] attributeFilter = { "cn", "uid" };
static SearchControls sc = new SearchControls();
public static void main(String[] args) throws Exception {
// sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
sc.setReturningAttributes(attributeFilter);
searchWithTemplate(); //NPE
//searchWithoutTemplate(); //works fine
}
public static void searchWithTemplate() throws Exception {
DefaultDirObjectFactory factory = new DefaultDirObjectFactory();
LdapContextSource cs = new LdapContextSource();
cs.setUrl(url);
cs.setUserDn(userName);
cs.setPassword(password);
cs.setBase(bindDN);
cs.setDirObjectFactory(factory.getClass ());
LdapTemplate template = new LdapTemplate(cs);
template.afterPropertiesSet();
System.out.println((template.search(new LdapName(base), filter, sc,
new AttributesMapper() {
public Object mapFromAttributes(Attributes attrs)
throws NamingException {
System.out.println(attrs);
return attrs.get("uid").get();
}
})));
}
public static void searchWithoutTemplate() throws NamingException{
Hashtable env = new Hashtable(11);
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, url);
//env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, userName);
env.put(Context.SECURITY_CREDENTIALS, password);
DirContext dctx = new InitialDirContext(env);
NamingEnumeration results = dctx.search(base, filter, sc);
while (results.hasMore()) {
SearchResult sr = (SearchResult) results.next();
Attributes attrs = sr.getAttributes();
System.out.println(attrs);
Attribute attr = attrs.get("uid");
}
dctx.close();
}
}
Exception is:
Exception in thread "main" java.lang.NullPointerException
at org.springframework.ldap.core.support.AbstractContextSource.getReadOnlyContext(AbstractContextSource.java:125)
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:287)
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:237)
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:588)
at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:546)
at LDAPSearchTest.searchWithTemplate(LDAPSearchTest.java:47)
at LDAPSearchTest.main(LDAPSearchTest.java:33)
I am using Spring 2.5.6 and Spring LDAP 1.3.0
A quick scan showed that it's the authenticationSource field of AbstractContextSource that is the culprit. That file includes the following comment on the afterPropertiesSet() method:
/**
* Checks that all necessary data is set and that there is no compatibility
* issues, after which the instance is initialized. Note that you need to
* call this method explicitly after setting all desired properties if using
* the class outside of a Spring Context.
*/
public void afterPropertiesSet() throws Exception {
...
}
That method then goes on to create an appropriate authenticationSource if you haven't provided one.
As your test code above is most definitely not running within a Spring context, and you haven't explicitly set an authenticationSource, I think you need to edit your code as follows:
...
cs.setDirObjectFactory(factory.getClass ());
// Allow Spring to configure the Context Source:
cs.afterPropertiesSet();
LdapTemplate template = new LdapTemplate(cs);