30 September 2016

Written by Christo Mastoroudes

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.

My helpful screenshot



blog comments powered by Disqus