WebFlux functional endpoint, how to return a ServerResponse with retrieved data and no unnecessary block/wait? - spring-boot

I am totally new to reactive code and after a number of tutorials and youtube videos I am trying to setup a tiny test application using functional endpoints; a simple RouterFunction, RouterHandler and Repository. The problem is how to return an object in the ServerResponse from the respository to the caller, without causing any unnecessary blocking?
I am using Postman for testing. Here's the interesting parts of my test application:
#Configuration
public class BookRouter {
#Autowired
BookRepositoryImpl bookRepository;
#Bean
public RouterFunction<ServerResponse> bookRoutes() {
BookHandler bookHandler = new BookHandler(bookRepository);
return RouterFunctions
.nest(path("/api/books"),
route(GET("/{group}/{name}").and(accept(ALL)), bookHandler::getBook)
);
}
}
#Repository
public class BookRepositoryImpl implements BookRepository {
private final ReactiveMongoTemplate mongoTemplate;
#Autowired
public BookRepositoryImpl(ReactiveMongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
#Override
public Mono<Book> findByName(String group, String name) {
Query query = new Query(Criteria.where("group").is(group).and("name").is(name));
return mongoTemplate.findOne(query, Book.class);
}
}
public class BookHandler {
public Mono<ServerResponse> getBook(ServerRequest request) {
String group = request.pathVariable("group");
String name = request.pathVariable("name");
bookRepository
.findByName(group, name)
.subscribe(
ok -> System.out.println("findByName " + ok),
error -> System.err.println("Error: " + error));
return ServerResponse
.accepted()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue("Request queued");
}
}
When I have the code as shown above, the expected data is printed out in subscribe(ok -> ...), but I haven't figured out how to return this data in the ServerResponse.
If I change the code in getBook() to
return setting
.flatMap(s -> ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(s))
.switchIfEmpty(NOT_FOUND);
the returned bodyValue is empty, although I can see that it was retrieved from the database.
Any advice on what I am missing is most appreciated.
Update
I am using MongoDB Compass to view and verify the content of the database.
Debug logging is enabled in application.properties with logging.level.root=DEBUGso the Spring classes write some info in the terminal window. Parts of the somewhat anonymized log is as follows:
2020-09-05 21:37:02.688 DEBUG 32720 --- [ctor-http-nio-3] o.s.w.r.f.s.s.RouterFunctionMapping : [96ef6152-1] Mapped to com.sample.book.BookRouter$$Lambda$586/0x0000000800540040#3b0bf8e0
2020-09-05 21:37:02.717 DEBUG 32720 --- [ctor-http-nio-3] o.s.d.m.core.ReactiveMongoTemplate : findOne using query: { "group" : "Thriller", "name" : "The Shining"} fields: Document{{}} for class: class com.sample.book.Book in collection: book
2020-09-05 21:37:02.734 DEBUG 32720 --- [ctor-http-nio-3] o.s.d.m.core.ReactiveMongoTemplate : findOne using query: { "group" : "Thriller", "name" : "The Shining"} fields: {} in db.collection: book.book
2020-09-05 21:37:02.751 DEBUG 32720 --- [ctor-http-nio-3] org.mongodb.driver.protocol.command : Sending command '{"find": "book", "filter": {"group": "Thriller", "name": "The Shining"}, "limit": 1, "singleBatch": true, "$db": "book"}' with request id 7 to database book on connection [connectionId{localValue:2, serverValue:217}] to server localhost:27017
2020-09-05 21:37:02.766 DEBUG 32720 --- [ntLoopGroup-3-2] org.mongodb.driver.protocol.command : Execution of command with request id 7 completed successfully in 16.24 ms on connection [connectionId{localValue:2, serverValue:217}] to server localhost:27017
2020-09-05 21:37:02.837 DEBUG 32720 --- [ntLoopGroup-3-2] o.s.http.codec.json.Jackson2JsonEncoder : [96ef6152-1] Encoding [_id=5f53692af0a02d3af8a7fed9, group=Thriller, name=The Shining, value=in]]
2020-09-05 21:37:02.853 DEBUG 32720 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations : [id: 0x96ef6152, L:/0:0:0:0:0:0:0:1:8088 - R:/0:0:0:0:0:0:0:1:50248] Decreasing pending responses, now 0
2020-09-05 21:37:02.879 DEBUG 32720 --- [ctor-http-nio-3] o.s.w.s.adapter.HttpWebHandlerAdapter : [96ef6152-1] Completed 200 OK
2020-09-05 21:37:02.905 DEBUG 32720 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations : [id: 0x96ef6152, L:/0:0:0:0:0:0:0:1:8088 - R:/0:0:0:0:0:0:0:1:50248] Last HTTP response frame
2020-09-05 21:37:02.931 DEBUG 32720 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations : [id: 0x96ef6152, L:/0:0:0:0:0:0:0:1:8088 - R:/0:0:0:0:0:0:0:1:50248] Last HTTP packet was sent, terminating the channel

I found the problem. I had forgot to implement getters in the Book class holding the #Document. I am surprised that there were no error message or warning when they were missing.
As soon as I inserted them, the result was returned as expected from this code:
return setting
.flatMap(s -> ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(s))
.switchIfEmpty(NOT_FOUND);
Here is the data as returned to Postman after the fix:
{
"_id": "5f53692af0a02d3af8a7fed9",
"group": "Thrillers",
"name": "The Shining",
"value": "in"
}
Thank you to #caco3 for helping me find the problem!
Here is my updated Book.java.
#Document
#CompoundIndex(name = "group-name", def = "{'group':1, 'name':1}", unique = true) // Requires auto-index-creation in application.properties
public class Book {
#Id
private String _id;
private String group;
private String name;
private String value;
public Book() {
}
public Book(String group, String name, String value) {
this.group = group;
this.name = name;
this.value = value;
}
#Override
public String toString() {
StringBuilder s = new StringBuilder('[');
s.append("_id=").append(_id);
s.append(", group=").append(group);
s.append(", name=").append(name);
s.append(", value=").append(value);
s.append(']');
return s.toString();
}
public String get_id() {
return _id;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}

Related

Getting invalid SpanContext in mongo extension for java OpenTelemetry automatic instrumentation

I am writing an extension of OpenTelemetry to input traceId as comment in mongo query. For this I have put an Advice on the find method in com.mongodb.client.MongoCollection. Inside the Advice when I call spanContext.isValid() it returns false, also spanId and traceId have all zeros in them.
MongoQueryInstrumentation.java
public class MongoQueryInstrumentation implements TypeInstrumentation {
#Override
public ElementMatcher<TypeDescription> typeMatcher() {
return implementsInterface(named("com.mongodb.client.MongoCollection"));
}
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("find").and(takesArgument(0, Bson.class)).and(ElementMatchers.isPublic()),
AdvicesFind.class.getName());
}
#SuppressWarnings("unused")
public static class AdvicesFind {
#Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(#Advice.Return(readOnly = false) FindIterable<?> result) {
SpanContext spanContext = Java8BytecodeBridge.currentSpan().getSpanContext();
System.out.println("traceId:" + spanContext.getTraceId());
System.out.println("VALID :" + spanContext.isValid());
result.comment(spanContext.getTraceId());
}
}
}
MongoInstrumentationModule.java
#AutoService(InstrumentationModule.class)
public final class MongoInstrumentationModule extends InstrumentationModule {
public MongoInstrumentationModule() {
super("mongo-ext", "mongo-4.0");
}
#Override
public int order() {
return 1;
}
#Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return hasClassesNamed("com.mongodb.internal.async.SingleResultCallback");
}
#Override
public List<TypeInstrumentation> typeInstrumentations() {
return Collections.singletonList(new MongoQueryInstrumentation());
}
}
OUTPUT:
traceId:00000000000000000000000000000000
VALID :false
I am exporting the logs to zipkin, there db.statement is
{"find": "sampleCollection", "filter": {"title": "MongoDB"}, "comment": "00000000000000000000000000000000", "$db": "myDb", "lsid": {"id": {"$binary": {"base64": "nOpqMCwHRWe2h+qmVEgGIQ==", "subType": "04"}}}}
I tried doing similar thing in JDBC by adding a comment containing traceId and spanId and there it worked as expected. There I patched sendQueryString method of NativeProtocolInstrumentation.
There Java8BytecodeBridge.currentSpan().getSpanContext() returned a valid spanId but in mongo it does not.

Preserving hostname on HATEOAS Resource with OpenFeign

I'm trying to add a URI to a resource located in a different microservice using OpenFeign and a ResourceAssembler, while preserving the hostname from the original request.
When making a REST request to a HATEOAS resource in another microservice, the resource.getId() method returns a link where the hostname is the Docker container hash instead of the original hostname used to make the request.
Controller
#RestController
#RequestMapping("/bulletins")
public class BulletinController {
// Autowired dependencies
#GetMapping(produces = MediaTypes.HAL_JSON_VALUE)
public ResponseEntity<PagedResources<BulletinResource>> getBulletins(Pageable pageable) {
Page<Bulletin> bulletins = bulletinRepository.findAll(pageable);
return ResponseEntity.ok(pagedResourceAssembler.toResource(bulletins, bulletinResourceAssembler));
}
}
Assembler
#Component
public class BulletinResourceAssembler extends ResourceAssemblerSupport<Bulletin, BulletinResource> {
private final AdministrationService administrationService;
#Autowired
public BulletinResourceAssembler(AdministrationService administrationService) {
super(BulletinController.class, BulletinResource.class);
this.administrationService = administrationService;
}
#Override
public BulletinResource toResource(Bulletin entity) {
Resource<Site> siteRessource = administrationService.getSiteBySiteCode(entity.getSiteCode());
\\ Set other fields ...
bulletinRessource.add(siteRessource.getId().withRel("site"));
return bulletinRessource;
}
}
Feign Client
#FeignClient(name = "${feign.administration.serviceId}", path = "/api")
public interface AdministrationService {
#GetMapping(value = "/sites/{siteCode}")
Resource<Site> getSiteBySiteCode(#PathVariable("siteCode") String siteCode);
}
Bulletin Resource
#Data
public class BulletinResource extends ResourceSupport {
// fields
}
Expected result
curl http://myhost/api/bulletins
{
"_embedded" : {
"bulletinResources" : [ {
"entityId" : 1,
"_links" : {
"self" : {
"href" : "http://myhost/api/bulletins/1"
},
"site" : {
"href" : "http://myhost/api/sites/000"
}
}
} ]
},
[...]
}
Actual result
curl http://myhost/api/bulletins
{
"_embedded" : {
"bulletinResources" : [ {
"entityId" : 1,
"_links" : {
"self" : {
"href" : "http://myhost/api/bulletins/1"
},
"site" : {
"href" : "http://b4dc1a02586c:8080/api/sites/000"
}
}
} ]
},
[...]
}
Notice the site href is b4dc1a02586c, which is the Docker container id.
The solution was to manually define a RequestInterceptor for the FeignClient and manually add the X-Forwarded-Host header, as well as define a ForwardedHeaderFilter bean in the service the request was made to.
Client Side
public class ForwardHostRequestInterceptor implements RequestInterceptor {
private static final String HOST_HEADER = "Host";
private static final String X_FORWARDED_HOST = "X-Forwarded-Host";
#Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (requestAttributes == null) {
return;
}
HttpServletRequest request = requestAttributes.getRequest();
String host = request.getHeader(X_FORWARDED_HOST);
if (host == null) {
host = request.getHeader(HOST_HEADER);
}
requestTemplate.header(X_FORWARDED_HOST, host);
}
}
Producer side
The producer side also required modification as per the discussion on
https://github.com/spring-projects/spring-hateoas/issues/862
which refers to the following documentation
https://docs.spring.io/spring-hateoas/docs/current-SNAPSHOT/reference/html/#server.link-builder.forwarded-headers
which states to add the following bean in order to use forward headers.
#Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}

Spring Integration MongoDbStoringMessageHandler ClassCastException: BasicDBObject cannot be cast to BasicDBList

I have developed an integration flow where I get the users from a MongoDbMessageSource and for each social medium associated with the user I get the comments addressed to him.
Those comments I want to persist them in MongoDB with help of MongoDbStoringMessageHandler linked to channel storeChannel.
The flow is as follows:
#Configuration
#IntegrationComponentScan
public class InfrastructureConfiguration {
private static Logger logger = LoggerFactory.getLogger(InfrastructureConfiguration.class);
/**
* The Pollers builder factory can be used to configure common bean definitions or
* those created from IntegrationFlowBuilder EIP-methods
*/
#Bean(name = PollerMetadata.DEFAULT_POLLER)
public PollerMetadata poller() {
return Pollers.fixedDelay(10, TimeUnit.SECONDS).get();
}
#Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
return executor;
}
/**
*
* MongoDbMessageSource is an instance of MessageSource which returns a Message with a payload
* which is the result of execution of a Query
*/
#Bean
#Autowired
public MessageSource<Object> mongoMessageSource(MongoDbFactory mongo) {
MongoDbMessageSource messageSource = new MongoDbMessageSource(mongo, new LiteralExpression("{}"));
messageSource.setExpectSingleResult(false);
messageSource.setEntityClass(UserEntity.class);
messageSource.setCollectionNameExpression(new LiteralExpression("users"));
return messageSource;
}
#Bean
#ServiceActivator(inputChannel = "storeChannel")
public MessageHandler mongodbAdapter(MongoDbFactory mongo) throws Exception {
MongoDbStoringMessageHandler adapter = new MongoDbStoringMessageHandler(mongo);
adapter.setCollectionNameExpression(new LiteralExpression("comments"));
return adapter;
}
#Bean
#Autowired
public IntegrationFlow processUsers(MongoDbFactory mongo, PollerMetadata poller) {
return IntegrationFlows.from(mongoMessageSource(mongo), c -> c.poller(poller))
.<List<UserEntity>, Map<ObjectId, List<SocialMediaEntity>>>transform(userEntitiesList
-> userEntitiesList.stream().collect(Collectors.toMap(UserEntity::getId, UserEntity::getSocialMedia))
)
.split(new AbstractMessageSplitter() {
#Override
protected Object splitMessage(Message<?> msg) {
return ((Map<ObjectId, List<SocialMediaEntity>>) msg.getPayload()).entrySet();
}
})
.channel("directChannel_1")
.enrichHeaders(s -> s.headerExpressions(h -> h.put("user-id", "payload.key")))
.split(new AbstractMessageSplitter() {
#Override
protected Object splitMessage(Message<?> msg) {
return ((Entry<ObjectId, List<SocialMediaEntity>>) msg.getPayload()).getValue();
}
})
.channel(MessageChannels.executor("executorChannel", this.taskExecutor()))
.<SocialMediaEntity, SocialMediaTypeEnum>route(p -> p.getType(),
m
-> m.subFlowMapping(SocialMediaTypeEnum.FACEBOOK, sf -> sf.handle(new GenericHandler<SocialMediaEntity>() {
#Override
public Object handle(SocialMediaEntity payload, Map<String, Object> headers) {
ObjectId userId = (ObjectId)headers.get("user-id");
logger.info("TEST FACEBOOK Channel for user id: " + userId);
return Arrays.asList(new CommentEntity[] {
new CommentEntity("Comentario 1 from facebook dirigido a " + userId, userId),
new CommentEntity("Comentario 2 from facebook dirigido a " + userId, userId)
});
}
}))
.subFlowMapping(SocialMediaTypeEnum.YOUTUBE, sf -> sf.handle(new GenericHandler<SocialMediaEntity>() {
#Override
public Object handle(SocialMediaEntity payload, Map<String, Object> headers) {
ObjectId userId = (ObjectId)headers.get("user-id");
logger.info("TEST YOUTUBE Channel for user id: " + userId);
return Arrays.asList(new CommentEntity[] {
new CommentEntity("Comentario 1 from youtube dirigido a " + userId, userId),
new CommentEntity("Comentario 2 from youtube dirigido a " + userId, userId)
});
}
}))
.subFlowMapping(SocialMediaTypeEnum.INSTAGRAM, sf -> sf.handle(new GenericHandler<SocialMediaEntity>() {
#Override
public Object handle(SocialMediaEntity payload, Map<String, Object> headers) {
ObjectId userId = (ObjectId)headers.get("user-id");
logger.info("TEST INSTAGRAM Channel for user id: " + userId);
return Arrays.asList(new CommentEntity[] {
new CommentEntity("Comentario 1 from instagram dirigido a " + userId, userId),
new CommentEntity("Comentario 2 from instagram dirigido a " + userId, userId)
});
}
}))
)
.channel("directChannel_2")
.aggregate()
.channel("directChannel_3")
.<List<List<CommentEntity>>, List<CommentEntity>>transform(comments ->
comments.stream().flatMap(List::stream).collect(Collectors.toList()))
.aggregate()
.channel("directChannel_4")
.<List<List<CommentEntity>>, List<CommentEntity>>transform(comments ->
comments.stream().flatMap(List::stream).collect(Collectors.toList()))
.channel("storeChannel")
.get();
}
}
The debug messages before the error are these:
2017-07-24 15:43:03.265 DEBUG 15152 --- [ taskExecutor-3] o.s.integration.channel.DirectChannel : preSend on channel 'storeChannel', message: GenericMessage [payload=[sanchez.sanchez.sergio.persistence.entity.CommentEntity#4de61faa, sanchez.sanchez.sergio.persistence.entity.CommentEntity#587d9f81, sanchez.sanchez.sergio.persistence.entity.CommentEntity#21075b47, sanchez.sanchez.sergio.persistence.entity.CommentEntity#653d282, sanchez.sanchez.sergio.persistence.entity.CommentEntity#4b790cef, sanchez.sanchez.sergio.persistence.entity.CommentEntity#662a5dcd, sanchez.sanchez.sergio.persistence.entity.CommentEntity#1a82309c, sanchez.sanchez.sergio.persistence.entity.CommentEntity#1b99ebf2, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6d1a6380, sanchez.sanchez.sergio.persistence.entity.CommentEntity#13b4363c, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6c5952d0, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3b3e7b7d, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3859229, sanchez.sanchez.sergio.persistence.entity.CommentEntity#786af66, sanchez.sanchez.sergio.persistence.entity.CommentEntity#271b5a0e, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3e45e786, sanchez.sanchez.sergio.persistence.entity.CommentEntity#ae0edfb, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6955ab16, sanchez.sanchez.sergio.persistence.entity.CommentEntity#7ae0fb73, sanchez.sanchez.sergio.persistence.entity.CommentEntity#4ed5e239, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6da79744, sanchez.sanchez.sergio.persistence.entity.CommentEntity#39352779, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3a12507d, sanchez.sanchez.sergio.persistence.entity.CommentEntity#51345bc3, sanchez.sanchez.sergio.persistence.entity.CommentEntity#7d95ad, sanchez.sanchez.sergio.persistence.entity.CommentEntity#32ca5648, sanchez.sanchez.sergio.persistence.entity.CommentEntity#616e3510, sanchez.sanchez.sergio.persistence.entity.CommentEntity#53a15bc4, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3aa84ac4, sanchez.sanchez.sergio.persistence.entity.CommentEntity#ed8ac69], headers={sequenceNumber=5, sequenceDetails=[[eda06022-4472-76b2-4ab5-1b24c1929cc2, 5, 5]], mongo_collectionName=users, sequenceSize=5, user-id=5975f9523681ac3b30e547c8, correlationId=eda06022-4472-76b2-4ab5-1b24c1929cc2, id=644a7577-7033-e669-505a-901172364790, timestamp=1500903783265}]
2017-07-24 15:43:03.267 DEBUG 15152 --- [ taskExecutor-3] ssor$ReplyProducingMessageHandlerWrapper : infrastructureConfiguration.mongodbAdapter.serviceActivator.handler received message: GenericMessage [payload=[sanchez.sanchez.sergio.persistence.entity.CommentEntity#4de61faa, sanchez.sanchez.sergio.persistence.entity.CommentEntity#587d9f81, sanchez.sanchez.sergio.persistence.entity.CommentEntity#21075b47, sanchez.sanchez.sergio.persistence.entity.CommentEntity#653d282, sanchez.sanchez.sergio.persistence.entity.CommentEntity#4b790cef, sanchez.sanchez.sergio.persistence.entity.CommentEntity#662a5dcd, sanchez.sanchez.sergio.persistence.entity.CommentEntity#1a82309c, sanchez.sanchez.sergio.persistence.entity.CommentEntity#1b99ebf2, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6d1a6380, sanchez.sanchez.sergio.persistence.entity.CommentEntity#13b4363c, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6c5952d0, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3b3e7b7d, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3859229, sanchez.sanchez.sergio.persistence.entity.CommentEntity#786af66, sanchez.sanchez.sergio.persistence.entity.CommentEntity#271b5a0e, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3e45e786, sanchez.sanchez.sergio.persistence.entity.CommentEntity#ae0edfb, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6955ab16, sanchez.sanchez.sergio.persistence.entity.CommentEntity#7ae0fb73, sanchez.sanchez.sergio.persistence.entity.CommentEntity#4ed5e239, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6da79744, sanchez.sanchez.sergio.persistence.entity.CommentEntity#39352779, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3a12507d, sanchez.sanchez.sergio.persistence.entity.CommentEntity#51345bc3, sanchez.sanchez.sergio.persistence.entity.CommentEntity#7d95ad, sanchez.sanchez.sergio.persistence.entity.CommentEntity#32ca5648, sanchez.sanchez.sergio.persistence.entity.CommentEntity#616e3510, sanchez.sanchez.sergio.persistence.entity.CommentEntity#53a15bc4, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3aa84ac4, sanchez.sanchez.sergio.persistence.entity.CommentEntity#ed8ac69], headers={sequenceNumber=5, sequenceDetails=[[eda06022-4472-76b2-4ab5-1b24c1929cc2, 5, 5]], mongo_collectionName=users, sequenceSize=5, user-id=5975f9523681ac3b30e547c8, correlationId=eda06022-4472-76b2-4ab5-1b24c1929cc2, id=644a7577-7033-e669-505a-901172364790, timestamp=1500903783265}]
2017-07-24 15:43:03.267 DEBUG 15152 --- [ taskExecutor-3] o.s.i.m.o.MongoDbStoringMessageHandler : mongodbAdapter received message: GenericMessage [payload=[sanchez.sanchez.sergio.persistence.entity.CommentEntity#4de61faa, sanchez.sanchez.sergio.persistence.entity.CommentEntity#587d9f81, sanchez.sanchez.sergio.persistence.entity.CommentEntity#21075b47, sanchez.sanchez.sergio.persistence.entity.CommentEntity#653d282, sanchez.sanchez.sergio.persistence.entity.CommentEntity#4b790cef, sanchez.sanchez.sergio.persistence.entity.CommentEntity#662a5dcd, sanchez.sanchez.sergio.persistence.entity.CommentEntity#1a82309c, sanchez.sanchez.sergio.persistence.entity.CommentEntity#1b99ebf2, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6d1a6380, sanchez.sanchez.sergio.persistence.entity.CommentEntity#13b4363c, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6c5952d0, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3b3e7b7d, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3859229, sanchez.sanchez.sergio.persistence.entity.CommentEntity#786af66, sanchez.sanchez.sergio.persistence.entity.CommentEntity#271b5a0e, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3e45e786, sanchez.sanchez.sergio.persistence.entity.CommentEntity#ae0edfb, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6955ab16, sanchez.sanchez.sergio.persistence.entity.CommentEntity#7ae0fb73, sanchez.sanchez.sergio.persistence.entity.CommentEntity#4ed5e239, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6da79744, sanchez.sanchez.sergio.persistence.entity.CommentEntity#39352779, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3a12507d, sanchez.sanchez.sergio.persistence.entity.CommentEntity#51345bc3, sanchez.sanchez.sergio.persistence.entity.CommentEntity#7d95ad, sanchez.sanchez.sergio.persistence.entity.CommentEntity#32ca5648, sanchez.sanchez.sergio.persistence.entity.CommentEntity#616e3510, sanchez.sanchez.sergio.persistence.entity.CommentEntity#53a15bc4, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3aa84ac4, sanchez.sanchez.sergio.persistence.entity.CommentEntity#ed8ac69], headers={sequenceNumber=5, sequenceDetails=[[eda06022-4472-76b2-4ab5-1b24c1929cc2, 5, 5]], mongo_collectionName=users, sequenceSize=5, user-id=5975f9523681ac3b30e547c8, correlationId=eda06022-4472-76b2-4ab5-1b24c1929cc2, id=644a7577-7033-e669-505a-901172364790, timestamp=1500903783265}]
Where it is clear that the channel "storeChannel" comes a list of "CommentEntity"
#Document(collection="comments")
public class CommentEntity {
#Id
private ObjectId id;
#Field("message")
private String message;
private ObjectId user;
#PersistenceConstructor
public CommentEntity(String message, ObjectId user) {
this.message = message;
this.user = user;
}
public ObjectId getId() {
return id;
}
public void setId(ObjectId id) {
this.id = id;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public ObjectId getUser() {
return user;
}
public void setUser(ObjectId user) {
this.user = user;
}
}
This exception then occurs:
2017-07-24 15:43:03.271 ERROR 15152 --- [ taskExecutor-3] o.s.integration.handler.LoggingHandler : org.springframework.messaging.MessageHandlingException: error occurred in message handler [mongodbAdapter]; nested exception is java.lang.ClassCastException: com.mongodb.BasicDBObject cannot be cast to com.mongodb.BasicDBList, failedMessage=GenericMessage [payload=[sanchez.sanchez.sergio.persistence.entity.CommentEntity#4de61faa, sanchez.sanchez.sergio.persistence.entity.CommentEntity#587d9f81, sanchez.sanchez.sergio.persistence.entity.CommentEntity#21075b47, sanchez.sanchez.sergio.persistence.entity.CommentEntity#653d282, sanchez.sanchez.sergio.persistence.entity.CommentEntity#4b790cef, sanchez.sanchez.sergio.persistence.entity.CommentEntity#662a5dcd, sanchez.sanchez.sergio.persistence.entity.CommentEntity#1a82309c, sanchez.sanchez.sergio.persistence.entity.CommentEntity#1b99ebf2, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6d1a6380, sanchez.sanchez.sergio.persistence.entity.CommentEntity#13b4363c, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6c5952d0, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3b3e7b7d, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3859229, sanchez.sanchez.sergio.persistence.entity.CommentEntity#786af66, sanchez.sanchez.sergio.persistence.entity.CommentEntity#271b5a0e, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3e45e786, sanchez.sanchez.sergio.persistence.entity.CommentEntity#ae0edfb, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6955ab16, sanchez.sanchez.sergio.persistence.entity.CommentEntity#7ae0fb73, sanchez.sanchez.sergio.persistence.entity.CommentEntity#4ed5e239, sanchez.sanchez.sergio.persistence.entity.CommentEntity#6da79744, sanchez.sanchez.sergio.persistence.entity.CommentEntity#39352779, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3a12507d, sanchez.sanchez.sergio.persistence.entity.CommentEntity#51345bc3, sanchez.sanchez.sergio.persistence.entity.CommentEntity#7d95ad, sanchez.sanchez.sergio.persistence.entity.CommentEntity#32ca5648, sanchez.sanchez.sergio.persistence.entity.CommentEntity#616e3510, sanchez.sanchez.sergio.persistence.entity.CommentEntity#53a15bc4, sanchez.sanchez.sergio.persistence.entity.CommentEntity#3aa84ac4, sanchez.sanchez.sergio.persistence.entity.CommentEntity#ed8ac69], headers={sequenceNumber=5, sequenceDetails=[[eda06022-4472-76b2-4ab5-1b24c1929cc2, 5, 5]], mongo_collectionName=users, sequenceSize=5, user-id=5975f9523681ac3b30e547c8, correlationId=eda06022-4472-76b2-4ab5-1b24c1929cc2, id=644a7577-7033-e669-505a-901172364790, timestamp=1500903783265}]
I am currently using an embedded MongoDB:
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
Does anyone know I'm doing wrong? Thanks in advance.
Well, that exception clearly says that MongoDbStoringMessageHandler doesn't support collection for saving:
protected void handleMessageInternal(Message<?> message) throws Exception {
Assert.isTrue(this.initialized, "This class is not yet initialized. Invoke its afterPropertiesSet() method");
String collectionName = this.collectionNameExpression.getValue(this.evaluationContext, message, String.class);
Assert.notNull(collectionName, "'collectionNameExpression' must not evaluate to null");
Object payload = message.getPayload();
this.mongoTemplate.save(payload, collectionName);
}
You don't need to .aggregate() to build collections to save. You only can save them with that component only one by one.
I think that should be a good addition to let that component to perform:
/**
* Insert a mixed Collection of objects into a database collection determining the collection name to use based on the
* class.
*
* #param collectionToSave the list of objects to save.
*/
void insertAll(Collection<? extends Object> objectsToSave);
Please, raise a JIRA on the matter and don't hesitate in contribution!

Java XML, The number of formal and actual parameters differs, or an unwrapping conversion has failed

When requesting data from endpoint with accept: application/xml I keep getting the following error:
javax.xml.bind.MarshalException
- with linked exception: [Exception [EclipseLink-27] (Eclipse Persistence Services - 2.6.1.v20150916-55dc7c3):
org.eclipse.persistence.exceptions.DescriptorException Exception
Description: Trying to invoke the method [getSurveyid] on the object
[com.on24.ejb.mapping.SurveyQuestion]. The number of actual and
formal parameters differs, or an unwrapping conversion has failed.
Internal Exception: java.lang.IllegalArgumentException: object is not
an instance of declaring class Mapping:
org.eclipse.persistence.oxm.mappings.XMLDirectMapping[surveyid-->surveyid/text()]
Descriptor: XMLDescriptor(com.on24.ejb.mapping.Survey -->
[DatabaseTable(survey)])]
The response works fine when accept: application/json so I know it can't be a problem extracting the info from DB; I just haven't been able to solve this issue so any help will be greatly appreciated.
DTOs involved:
#XmlRootElement
#XmlType (propOrder={"surveyid",
"surveyquestions"})
#XmlAccessorType(XmlAccessType.PUBLIC_MEMBER)
public class Survey {
private Long surveyid;
private List<SurveyQuestion> surveyquestions;
public Survey(){}
public Long getSurveyid() {
return surveyid;
}
public void setSurveyid(Long surveyid) {
this.surveyid = surveyid;
}
#XmlElementWrapper(name="surveyquestionslist")
#XmlElement(name="surveyquestion")
public List<SurveyQuestion> getSurveyquestions() {
return surveyquestions;
}
public void setSurveyquestions(List<SurveyQuestion> surveyquestions) {
this.surveyquestions = surveyquestions;
}
}
And
#XmlRootElement
#XmlType (propOrder={"surveyquestionid",
"surveyquestion",
"surveyanswers"})
#XmlAccessorType(XmlAccessType.PUBLIC_MEMBER)
public class SurveyQuestion {
private Long surveyquestionid;
private String surveyquestion;
private List<String> surveyanswers;
public SurveyQuestion(){}
public Long getSurveyquestionid() {
return surveyquestionid;
}
public void setSurveyquestionid(Long surveyquestionid) {
this.surveyquestionid = surveyquestionid;
}
public String getSurveyquestion() {
return surveyquestion;
}
public void setSurveyquestion(String surveyquestion) {
this.surveyquestion = surveyquestion;
}
#XmlElementWrapper(name="surveyanswerslist")
#XmlElement(name="surveyanswer")
public List<String> getSurveyanswers() {
return surveyanswers;
}
public void setSurveyanswers(List<String> surveyanswers) {
this.surveyanswers = surveyanswers;
}
}
I've tried several thinks from refactoring to use XmlAccessType.PUBLIC_MEMBER, XmlAccessType.FIELD, XmlAccessType.PROPERTY but no success there.
I'd really like to understand why this error is generated. If more info is need I'll add it as per asked for, thanks.

Spring Data REST: projection representation of single resource

I have a simple UserRepository which exposed using Spring Data REST.
Here is the User entity class:
#Document(collection = User.COLLECTION_NAME)
#Setter
#Getter
public class User extends Entity {
public static final String COLLECTION_NAME = "users";
private String name;
private String email;
private String password;
private Set<UserRole> roles = new HashSet<>(0);
}
I've created a UserProjection class which looks the following way:
#JsonInclude(JsonInclude.Include.NON_NULL)
#Projection(types = User.class)
public interface UserProjection {
String getId();
String getName();
String getEmail();
Set<UserRole> getRoles();
}
Here is the repository class:
#RepositoryRestResource(collectionResourceRel = User.COLLECTION_NAME, path = RestPath.Users.ROOT,
excerptProjection = UserProjection.class)
public interface RestUserRepository extends MongoRepository<User, String> {
// Not exported operations
#RestResource(exported = false)
#Override
<S extends User> S insert(S entity);
#RestResource(exported = false)
#Override
<S extends User> S save(S entity);
#RestResource(exported = false)
#Override
<S extends User> List<S> save(Iterable<S> entites);
}
I've also specified user projection in configuration to make sure it will be used.
config.getProjectionConfiguration().addProjection(UserProjection.class, User.class);
So, when I do GET on /users path, I get the following response (projection is applied):
{
"_embedded" : {
"users" : [ {
"name" : "Yuriy Yunikov",
"id" : "5812193156aee116256a33d4",
"roles" : [ "USER", "ADMIN" ],
"email" : "yyunikov#gmail.com",
"points" : 0,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4"
},
"user" : {
"href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4{?projection}",
"templated" : true
}
}
} ]
},
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8080/users"
},
"profile" : {
"href" : "http://127.0.0.1:8080/profile/users"
}
},
"page" : {
"size" : 20,
"totalElements" : 1,
"totalPages" : 1,
"number" : 0
}
}
However, when I try to make a GET call for single resource, e.g. /users/5812193156aee116256a33d4, I get the following response:
{
"name" : "Yuriy Yunikov",
"email" : "yyunikov#gmail.com",
"password" : "123456",
"roles" : [ "USER", "ADMIN" ],
"_links" : {
"self" : {
"href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4"
},
"user" : {
"href" : "http://127.0.0.1:8080/users/5812193156aee116256a33d4{?projection}",
"templated" : true
}
}
}
As you may see, the password field is getting returned and projection is not applied. I know there is #JsonIgnore annotation which can be used to hide sensitive data of resource. However, my User object is located in different application module which does not know about API or JSON representation, so it does not make sense to mark fields with #JsonIgnore annotation there.
I've seen a post by #Oliver Gierke here about why excerpt projections are not applied to single resource automatically. However, it's still very inconvenient in my case and I would like to return the same UserProjection when I get a single resource. Is it somehow possible to do it without creating a custom controller or marking fields with #JsonIgnore?
I was able to create a ResourceProcessor class which applies projections on any resource as suggested in DATAREST-428. It works the following way: if projection parameter is specified in URL - the specified projection will be applied, if not - projection with name default will be returned, applied first found projection will be applied. Also, I had to add custom ProjectingResource which ignores the links, otherwise there are two _links keys in the returning JSON.
/**
* Projecting resource used for {#link ProjectingProcessor}. Does not include empty links in JSON, otherwise two
* _links keys are present in returning JSON.
*
* #param <T>
*/
#JsonInclude(JsonInclude.Include.NON_EMPTY)
class ProjectingResource<T> extends Resource<T> {
ProjectingResource(final T content) {
super(content);
}
}
/**
* Resource processor for all resources which applies projection for single resource. By default, projections
* are not
* applied when working with single resource, e.g. http://127.0.0.1:8080/users/580793f642d54436e921f6ca. See
* related issue DATAREST-428
*/
#Component
public class ProjectingProcessor implements ResourceProcessor<Resource<Object>> {
private static final String PROJECTION_PARAMETER = "projection";
private final ProjectionFactory projectionFactory;
private final RepositoryRestConfiguration repositoryRestConfiguration;
private final HttpServletRequest request;
public ProjectingProcessor(#Autowired final RepositoryRestConfiguration repositoryRestConfiguration,
#Autowired final ProjectionFactory projectionFactory,
#Autowired final HttpServletRequest request) {
this.repositoryRestConfiguration = repositoryRestConfiguration;
this.projectionFactory = projectionFactory;
this.request = request;
}
#Override
public Resource<Object> process(final Resource<Object> resource) {
if (AopUtils.isAopProxy(resource.getContent())) {
return resource;
}
final Optional<Class<?>> projectionType = findProjectionType(resource.getContent());
if (projectionType.isPresent()) {
final Object projection = projectionFactory.createProjection(projectionType.get(), resource
.getContent());
return new ProjectingResource<>(projection);
}
return resource;
}
private Optional<Class<?>> findProjectionType(final Object content) {
final String projectionParameter = request.getParameter(PROJECTION_PARAMETER);
final Map<String, Class<?>> projectionsForType = repositoryRestConfiguration.getProjectionConfiguration()
.getProjectionsFor(content.getClass());
if (!projectionsForType.isEmpty()) {
if (!StringUtils.isEmpty(projectionParameter)) {
// projection parameter specified
final Class<?> projectionClass = projectionsForType.get(projectionParameter);
if (projectionClass != null) {
return Optional.of(projectionClass);
}
} else if (projectionsForType.containsKey(ProjectionName.DEFAULT)) {
// default projection exists
return Optional.of(projectionsForType.get(ProjectionName.DEFAULT));
}
// no projection parameter specified
return Optional.of(projectionsForType.values().iterator().next());
}
return Optional.empty();
}
}
I was looking at something similar recently and ended up going round in circles when trying to approach it from the Spring Data /Jackson side of things.
An alternative, and very simple solution, then is to approach it from a different angle and ensure the Projection parameter in the HTTP request is always present. This can be done by using a Servlet Filter to modify the parameters of the incoming request.
This would look something like the below:
public class ProjectionResolverFilter extends GenericFilterBean {
private static final String REQUEST_PARAM_PROJECTION_KEY = "projection";
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (shouldApply(request)) {
chain.doFilter(new ResourceRequestWrapper(request), res);
} else {
chain.doFilter(req, res);
}
}
/**
*
* #param request
* #return True if this filter should be applied for this request, otherwise
* false.
*/
protected boolean shouldApply(HttpServletRequest request) {
return request.getServletPath().matches("some-path");
}
/**
* HttpServletRequestWrapper implementation which allows us to wrap and
* modify the incoming request.
*
*/
public class ResourceRequestWrapper extends HttpServletRequestWrapper {
public ResourceRequestWrapper(HttpServletRequest request) {
super(request);
}
#Override
public String getParameter(final String name) {
if (name.equals(REQUEST_PARAM_PROJECTION_KEY)) {
return "nameOfDefaultProjection";
}
return super.getParameter(name);
}
}
}

Resources