Introduction
I recently had my first opportunity to use the Java Reflection API to solve a problem.
The following sections will provide a problem overview, how the problem was solved using reflection, and the result.
The Problem
Our Content Formatting for Confluence plugin uses macros to define tables. These include the following tags:
- table
- thead
- th
- tr
- td
- tbody
Confluence now has its own table implementation in the editor, but the Content Formatting tables still provide some added functionality.
From Confluence v5.9 Atlasssian introduced a macro wrapper that breaks macro generated html that is not valid. Unfortunately it also introduced a bug.
Consider the following example:
<table>
<tr>
<th>
<p>text</p>
</th>
<th>
<p>text</p>
</th>
</tr>
<table>
Above we have a perfectly valid table, however this does not work. To be specific, the th tags were being stripped from the table causing the table to render like this:
<p>text</p>
<p>text</p>
<table>
<tr>
</tr>
<table>
After some digging it became clear why this was happening.
The following is a snippet from the Confluence class ViewMacroMarshaller
public class ViewMacroMarshaller implements Marshaller<MacroDefinition> {
private static final Logger log = LoggerFactory.getLogger(ViewMacroMarshaller.class);
private static final String DEFAULT_MACRO_PLACEHOLDER = "<div class='default-macro-spinner spinner'>\n</div>";
private static final String WRAP_MACRO_KEY = "confluence.wrap.macro";
private static final String DISABLE_WRAP_MACRO_KEY = "confluence.wrap.macro.disable";
private static final Set<String> EXCLUDED_MACROS = new HashSet<String>() {
add("html");
add("html-xhtml");
add("td");
add("tr");
add("thead");
add("tbody");
add("tfoot");
};
Here we can see that some tags are allowed, but mysteriously the th tag is not present in the Set "EXCLUDE_MACROS". The next bit of code is the condition which uses the EXCLUDE_MACROS Set.
try {
boolean wrapped = darkFeaturesManager.getDarkFeatures().isFeatureEnabled(WRAP_MACRO_KEY)
&& !darkFeaturesManager.getDarkFeatures().isFeatureEnabled(DISABLE_WRAP_MACRO_KEY)
&& RenderContext.DISPLAY.equals(context.getOutputType())
&& !EXCLUDED_MACROS.contains(macroDefinition.getName());
The Solution
At first I was sure it could not be fixed, however a colleague recommended using reflection.
I have not used reflection before so it was a bit of a learning curve. Thankfully in the end it was not hard to achieve.
First there are a few basic things to understand about the Reflection API. It really just means that we are inspecting and changing a class at runtime. This is only recommended as a last resort because it is much slower than direct code. It can add complexities and create maintenance issues. It also violates Java access modifier constraints.
The solution is to get the ViewMacroMarshaller class at run time, and then retrieve the Set containing the tags to be excluded. If the Set does not contain the th tag, then we add it.
First we specify a component in the atlassian-plugin.xml file. This points to the class that will be responsible for the modification.
<component key="modifyViewMacroMarshaller" name="Modify ViewMacroMarshaller"
class="com.adaptavist.confl....Initialization.ModifyViewMacroMarshaller">
<description>Using reflection to modify a Confluence class to add the "th" tag,
workaround for a bug
</description>
<interface>org.springframework.beans.factory.InitializingBean</interface>
</component>
Then we create the class and implement the InitializingBean. We then implement the methods imposed by the interface, and write a few lines of code. Some consideration needs to be taken where we encounter an earlier or later version of the ViewMacroMarshaller class. This is because our plugin can be run with earlier or later versions of Confluence.
@Override
public void afterPropertiesSet() throws Exception {
try {
final Field field = ViewMacroMarshaller.class.getDeclaredField(EXCLUDED_MACROS);
field.setAccessible(true);
if (field.get(null) != null) {
if (!((Set<String>) field.get(null)).contains("th")) {
((Set<String>) field.get(null)).add("th");
}
}
} catch (NoSuchFieldException e) {
LOGGER.info("Unable to find EXCLUDE_MACROS field in ViewMacroMarshaller," +
" this field does not exist in pre Confluence 5.9 " + e);
}
}
Here we are getting the class and Set, and then performing some action on the Set if required. You might have noticed something odd "field.get(null)". When the field we are trying to access is static then we must use null as the argument.
The Result
The result is a properly formatted table which contains th elements.
- 09 Oct 2018 » A strange bug on AWS Lambda
- 17 Jan 2018 » How to run Karma tests in browsers in Docker
- 07 Dec 2017 » Switching from Javascript to Typescript
- 30 Oct 2017 » Fun with React event handlers
- 17 Jul 2017 » Switching from Groovy to Java
- 24 May 2017 » Useful Git Aliases
- 27 Mar 2017 » Practical Ratpack Promises
- 03 Nov 2016 » Custom Content in Forms for Confluence Connect
- 04 Oct 2016 » Checking user permissions from REST calls
- 30 Sep 2016 » Using the reflection API in Confluence
- 28 Sep 2016 » Creating a custom Confluence Blueprint
- 06 Sep 2016 » ReactJS in Forms for Confluence Connect
- 25 Apr 2016 » Migrating to ES6 in Atlassian Add-ons
- 17 Mar 2016 » All kinds of things I learnt trying to performance test against Fisheye/Crucible
- 24 Dec 2015 » Adaptavist’s Holiday Gift of Atlassian Deployment Automation
- 17 Dec 2015 » Getting a Custom Field value safely
- 07 Dec 2015 » Putting Google Analytics to work with plugins for Confluence
- 02 Dec 2015 » Devoxx Voting, A retrospective
- 25 Nov 2015 » Some things I've learnt about SingleSelect
- 15 Oct 2015 » Using SOY for JIRA actions
- 26 Sep 2015 » Object Reflection in Groovy
- 22 Sep 2015 » Introducing Adaptavist Labs