ContentPropertyService is null in Thread

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