Modifying page content in a plugin fails with 7.15

Hi,

our plugin has to modify the page content to insert/update macros. The code in question (which no doubt has been written according to older guidelines) looks more or less like this:

Page page = pageManager.getPage(pageId);
Page originalPage = (Page) page.clone();
String content = page.getBodyAsString();
//modify content
page.setBodyAsString(content);
pageManager.saveContentEntity(page, originalPage, null);

which worked fine until 7.15. This all happens in a servlet.

In 7.15, when synchrony is also enabled (we don’t see the problem without synchrony), the hibernation layer dies horribly (optimistic locking exception etc.) thus making save functionality impossible. To make life more fun, the problem only occurs in production environments, not in development instances, so debugging this is close to impossible anyway.

So… we are aware that said code is deprecated anyway, but how should we change it? Is there any working sample out there?

We tried to wrap it into a HibernationTemplate, but couldn’t get Spring to actually load the plugin then, also we tried to rewrite it using ContentService, but we are unsure whether this is the right approach at all.

From customer reports, this seems to affect other plugins as well, and I’d say it’s a severe and uncommunicated incompatible change.

TIA

4 Likes

To answer myself:

We are also running in the issue mentioned here

So we can retrieve the old content and modify it just fine, but we aren’t able to actually update the content with ContentService.update()

I can’t believe there is NO working sample or even samplecode that shows how to reliably modify page content via either the old or the new Java API - seems that’s just something plugin developers aren’t supposed to do.

We are also having similar problems.
https://productsupport.adaptavist.com/browse/SRCONF-2082

We too have yet to find a workaround though typical hibernate methods (like wrapping things in a transaction template).

As @jcarter said, we are also experiencing an incredible number of Hibernate-related issues in version 7.15.0. Similar to you, we have code which modifies pages and simply uses the PageManager to save the entity. When doing so, we’re met with the same errors you’ve described - OptimisticLockingException, StaleObjectException, etc.

We tried to wrap it into a HibernationTemplate, but couldn’t get Spring to actually load the plugin then

We also went down this path, but again ran into similar problems as you have stated here. We are able to get ahold of com.atlassian.sal.api.transaction.TransactionTemplate, but that doesn’t seem to do anything for this particular case. Using com.atlassian.confluence.core.SynchronizationManager helps some, but doesn’t appear to be a consistent and real solution.

In 7.15, when synchrony is also enabled (we don’t see the problem without synchrony)

This is the same determining factor that we have also noted. With Collaborative Editing turned on (and thus Synchrony turned on), we run into a significant number of errors when modifying pages. We don’t seem to see this in other versions Confluence (it appears to work correctly on 7.4.11, atleast).

2 Likes

@JasmineMller and me already put some time into this but to no avail, unfortunately. Eventually, we’ve also managed wrapping it in a TransactionTemplate, but this didn’t make a difference for us.

Then, we tried to switch to the contentService approach, which is the suggested replacement for the deprecated pageManager stuff, although still being @ExperimentalAPI.
After hours of research (thanks to no documentation…), we came up with this:

      Content pageContent = contentService.find(new Expansion(Content.Expansions.BODY, new Expansions(new Expansion("storage"))),
              new Expansion(Content.Expansions.SPACE),
              new Expansion(Content.Expansions.VERSION)).withType(ContentType.PAGE).withId(ContentId.deserialise(pageId)).fetchOrNull();

       ContentBody body = pageContent.getBody().get(ContentRepresentation.STORAGE);

       Version nextVersion = pageContent
                .getVersion()
                .nextBuilder()
                .message("Message to explain the change")
                .by(User.fromUserkey(AuthenticatedUserThreadLocal.get().getKey()))
                .when(new Date())
                .content(body.getContentRef())
                .minorEdit(false)
                .hidden(false)
                .build();

        Content newContent = Content
                .builder(pageContent) // clone content of original page
                .version(nextVersion)
                .body(body.getValue(), body.getRepresentation()) // just rewrite the current body w/o change...
                .build();

        ContentService.Validator validator = contentService.validator();
        ValidationResult validationResult = validator.validateCreate(newContent);

        if (validationResult.isSuccessful()) {
          // Checks for both: validity and authorization.
          contentService.update(newContent);
        }

Unfortunately, the update call throws (running in a dev instance):

com.atlassian.confluence.api.service.exceptions.ServiceException: java.lang.IllegalStateException: Request-local WebResourceAssembler has already been initialised
	at com.atlassian.confluence.api.impl.service.content.ContentBodyConversionManagerImpl.lambda$computeConversionResources$1(ContentBodyConversionManagerImpl.java:110)
	at com.atlassian.confluence.api.impl.ReadOnlyAndReadWriteTransactionConversionTemplate.executeInReadWrite(ReadOnlyAndReadWriteTransactionConversionTemplate.java:65)
	at com.atlassian.confluence.api.impl.service.content.ContentBodyConversionManagerImpl.computeConversionResources(ContentBodyConversionManagerImpl.java:97)
	at com.atlassian.confluence.api.impl.service.content.ContentBodyConversionManagerImpl.convert(ContentBodyConversionManagerImpl.java:90)
...

The approach seemed promising to us, but the exception isn’t helpful at all.

After reducing the code as much as possible we noticed that the initial code snippet

Page page = pageManager.getPage(pageId);
Page originalPage = (Page) page.clone();
String content = page.getBodyAsString();
//modify content
page.setBodyAsString(content);
pageManager.saveContentEntity(page, originalPage, null);

actually works in Confluence 7.15 when not executed with other database interactions.

In our case, immediately before executing the page update with the pageManager we used the attachmentManager to update/save an associated attachment on the same page.

Commenting out the attachmentManager call, made the pageManager working properly (though I assume that other external concurrent database actions could still interfere…).

Therefore, we wrapped both of our database write accesses in a TransactionTemplate and this seems to work in our case in Confluence 7.15

transactionTemplate.execute(() -> {
  // [...]
  attachmentManager.saveAttachment(...);
  // [...]
  Page page = pageManager.getPage(pageId);
  Page originalPage = (Page) page.clone();
  String content = page.getBodyAsString();
  //modify content
  page.setBodyAsString(content);
  pageManager.saveContentEntity(page, originalPage, null);
  // [...]
  return null;
});
3 Likes

Thanks for the code @fabian.schwarzkopf ! I can also confirm that this appears to solve the problem with our scripts as well. :smile:

The problem we still have is that we still have a lot of testing infrastructure that would need to have similar changes made to it, but we are questioning whether this is necessary. It seems like the solution should be implemented on Atlassian’s side, as this only seems to be a recent problem made by changes in the Confluence source. I would like to know whether Atlassian intended for this to occur and/or if they have a more thorough explanation of what the resolution should be.