How to disable the automatic HTML support of JLabel?

Viewed 3073

A Swing JLabel automatically interprets any text as HTML content, if it starts with <html>. If the content of this HTML is an image with invalid URL this will cause the whole GUI to hang since the ImageFetche which should load this image will quit by an NPE.

To reproduce this problem simply create a JLabel as follows

new JLabel("<html><img src='http:\\\\invalid\\url'>")

I know there is a client property to prevent the JLabel from interpreting HTML. But JLabel is the default renderer implementation for many Swing components(like JTree, JTable and so on) which makes this a problem for nearly any Swing application which allows user input. So instead of implementing tons of custom renderer I'm searching for a global solution to disable the HTML interpretation.

4 Answers

Since there is no way of globally setting the html.disable property to true for each created JLabel, one hacky way (I say hacky because I'm not sure of the impact on performance, or whether such a solution could be placed on production) is to do some bytecode interception for every created JLabel instance. A library like ByteBuddy can do this. I've experimented a bit with ByteBuddy and found a way to set a Java agent that intercepts calls to the setText() method for a JLabel. This method is called when creating a JLabel with the provided text.

Agent

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.AgentBuilder.InitializationStrategy;
import net.bytebuddy.agent.builder.AgentBuilder.Listener;
import net.bytebuddy.agent.builder.AgentBuilder.RedefinitionStrategy;
import net.bytebuddy.agent.builder.AgentBuilder.TypeStrategy;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.dynamic.loading.ClassInjector;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.matcher.StringMatcher;

import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.nio.file.Files;

import static java.util.Collections.singletonMap;
import static net.bytebuddy.description.type.TypeDescription.ForLoadedType;
import static net.bytebuddy.dynamic.ClassFileLocator.ForClassLoader.read;
import static net.bytebuddy.dynamic.loading.ClassInjector.UsingInstrumentation.Target.BOOTSTRAP;
import static net.bytebuddy.matcher.ElementMatchers.*;

public class JLabelAgent {

    private static final Class<?> INTERCEPTOR_CLASS = JLabelInterceptor.class;

    private JLabelAgent() {
    }

    public static void premain(String arg, Instrumentation instrumentation) throws Exception {
        injectBootstrapClasses(instrumentation);
        new AgentBuilder.Default()
        .with(RedefinitionStrategy.RETRANSFORMATION)
        .with(InitializationStrategy.NoOp.INSTANCE)
        .with(TypeStrategy.Default.REDEFINE)
        .ignore(new AgentBuilder.RawMatcher.ForElementMatchers(nameStartsWith("net.bytebuddy.").or(isSynthetic()), any(), any()))
        .with(new Listener.Filtering(
                new StringMatcher("javax.swing.JLabel", StringMatcher.Mode.EQUALS_FULLY),
                Listener.StreamWriting.toSystemOut()))
        .type(named("javax.swing.JLabel"))
        .transform((builder, type, classLoader, module) ->
                builder.visit(Advice.to(INTERCEPTOR_CLASS).on(named("setText")))
        )
        .installOn(instrumentation);
    }

    private static void injectBootstrapClasses(Instrumentation instrumentation) throws IOException {
        File temp = Files.createTempDirectory("tmp").toFile();
        temp.deleteOnExit();

        ClassInjector.UsingInstrumentation.of(temp, BOOTSTRAP, instrumentation)
            .inject(singletonMap(new ForLoadedType(INTERCEPTOR_CLASS), read(INTERCEPTOR_CLASS)));
    }
}

Interceptor

import javax.swing.JComponent;

import net.bytebuddy.asm.Advice;
import net.bytebuddy.asm.Advice.Argument;
import net.bytebuddy.asm.Advice.This;

public class JLabelInterceptor {


    @Advice.OnMethodEnter()
    public static void setText(@This Object label, @Argument(0) String text) {
        ((JComponent) label).putClientProperty("html.disable", Boolean.TRUE);
        System.out.println("Label text is " + text);
    }
}

Example

public static void main(String[] args) throws Exception {
    JFrame frame = new JFrame("JList Test");
    frame.setLayout(new FlowLayout());
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    String[] selections = {"<html><img src='http:\\\\invalid\\url'>", "<html><H1>Hello</h1></html>", "orange", "dark blue"};

    JList list = new JList(selections);

    list.setSelectedIndex(1);
    System.out.println(list.getSelectedValue());

    JLabel jLabel = new JLabel("<html><h2>standard Label</h2></html>");
    frame.add(new JScrollPane(list));
    frame.add(jLabel);
    frame.pack();

    frame.setVisible(true);
}

enter image description here

Running the example

Compile the Java agent then run the example:

java -javaagent:agent.jar -jar example.jar

Note: When using Maven to build the agent Jar, I had to put the following configuration in the POM to setup the manifest:

<plugin>
    <artifactId>maven-jar-plugin</artifactId>
    <configuration>
        <archive>
            <manifestEntries>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                <Agent-Class>example.JLabelAgent</Agent-Class>
                <Premain-Class>example.JLabelAgent</Premain-Class>
                <Boot-Class-Path>byte-buddy-1.10.14.jar</Boot-Class-Path>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>
Related