We are using the following Api in our plugin development via dependency injection in order to get a content property of key" docid" of defined pages:
ContentPropertyService
This service is working well except if it is inside a Thread. It will be null in that case.
Below is the method I’m using:
public JsonContentProperty getDocIdJsonContentProperty(Page page) {
Optional<JsonContentProperty> jsonProperty = contentPropertyService
.find()
.withContentId(ContentId.of(page.getId()))
.withPropertyKey("docid")
.fetch();
return jsonProperty.orElse(null);
}
Its call is in the following:
Page page = pageManager.getPage(123);
if (page != null) {
JsonContentProperty jsonContentProperty = getDocIdJsonContentProperty(page);
if (jsonContentProperty != null) {
System.out.println("jsonContentProperty: " + jsonContentProperty);
}
}
This code is working perfectly and I"m able to get the value of jsonContentProperty.
If it is inside a Thread as in below, jsonContentProperty returns NULL:
new Thread(() -> {
// above code
}).start();
Can you please help as I’m only using this service provided by Confluence in order to get content properties and I don’t have any extra custom code.
Thanks,
Rosy
Hello @rosy.salame
This is totally expected behaviour when using the ContentPropertyService, and really any service in our confluence-java-api
These services perform a permission check against the “remote” user available on the thread; the user must be authenticated, or anonymous if anonymous access is enabled on the site.
This means that when you create a custom/new thread, you have to set the user on the ThreadLocal for the ContentPropertyService (and friends) to successfully perform their permission checks and return valid data if any.
So I have created an example rest endpoint to explain what I mean.
@Path("/content-property")
public class ContentPropertyResource {
private final ContentPropertyService contentPropertyService;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Autowired
public ContentPropertyResource(@ComponentImport ContentPropertyService contentPropertyService) {
this.contentPropertyService = contentPropertyService;
}
// we query the contentPermissionService directly, without wrapping into a new thread
// this being a get authenticated get request, the user is already available on the ThreadLocal
@GET
@Path("/{id}/docid")
public Response getDocId(@PathParam("id") long contentId) throws ServiceException {
return contentPropertyService.find()
.withContentId(ContentId.of(contentId))
.withPropertyKey("docid")
.fetch()
.map(property -> Response.ok(property).build())
.orElseThrow(notFound("not found for content : " + contentId));
}
// we query the contentPermissionService by wrapping the call in a new thread
// however, we do not set the user on the ThreadLocal, this will return not found
// (which in your case is null)
@GET
@Path("/thread/{id}/docid")
public Response threadGetDocId(@PathParam("id") long contentId) throws ServiceException {
Future<Response> responseFuture = executor.submit(() -> getDocId(contentId));
try {
return responseFuture.get();
} catch (InterruptedException | ExecutionException exception) {
return Response.
status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(exception.getMessage())
.build();
}
}
// here we query the contentPermissionService, by wrapping the call in a new thread
// however we capture the current user , and set it in the new thread prior to
// calling on the contentPermissionService
// unlike the previous endpoint, this one will return data
@GET
@Path("/auth-thread/{id}/docid")
public Response authThreadGetDocId(@PathParam("id") long contentId) {
ConfluenceUser currentUser = AuthenticatedUserThreadLocal.get();
Future<Response> responseFuture = executor.submit(() -> {
AuthenticatedUserThreadLocal.set(currentUser);
Response response = getDocId(contentId);
AuthenticatedUserThreadLocal.reset();
return response;
});
try {
return responseFuture.get();
} catch (InterruptedException | ExecutionException exception) {
return Response.
status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(exception.getMessage())
.build();
}
}
// just an endpoint to create the property, so I can test the different examples above
@POST
@Path("/{id}/docid")
public Response setDocId(@PathParam("id") long contentId) {
JsonContentProperty property = contentPropertyService.create(
JsonContentProperty.builder()
.content(Content.builder().id(ContentId.of(contentId)).build())
.key("docid")
.value(new JsonString("docid-" + contentId))
.build()
);
return Response.status(Response.Status.CREATED).entity(property).build();
}
}
This is the endpoints above in action.
Also you may find the example code above in here https://github.com/viqueen/devbox/blob/master/confluence-devbox/src/main/java/org/viqueen/devbox/resources/ContentPropertyResource.java
I hope this explains it all, and you can modify your code accordingly.
That said, I am actually quite curious to hear about your use case for using a new thread !
PS: I also noticed that you are using the PageManager
, we do recommend using the ContentService
instead as much as possible
Cheers
Hasnae R.
1 Like
Hi @viqueen
Thank you for your help and all your details, but, the git project is not opening. Can you please share its code?
Many thanks,
Rosy
Hi @rosy.salame,
the repository was just reorganized.
1 Like
Thanks @dennis.fischer for having my back !!
Yup I shuffle things around that repo quite often
I get the same issue, not in a different thread but for specific users, in lower permission level.
Every API which contains ContentService.find - failing due to null results, while for the higher permitted users - it returns the results successfully.
Any help??
1 Like