Guava EventBus

15 March 2017

Guava is Google’s utilities, collections and helpers library for Java. Similar in spirit to the Apache Commons set of libraries, but possibly a bit more pragmatic. Google’s engineers use the Guava components in their daily work, and have fine-tuned the APIs to boost their own productivity. The current release is version 21.

In this article, we’ll look at the Guava EventBus class, for publishing and subscribing to application wide events of all kinds. It strikes a good balance between convenience and type-safety, and is much more lightweight than rolling your own Event, Handler and Fire classes. See also the Guava documentations for further details.

To have an example to work with, the following Player skeleton class is implemented. It comes with three separate Event classes, which are very minimal in this contrived example. In a real application, they might inherit from a super-Event class, and possibly carry some content, at least the source of the event or similar. Alternatively, they could have been made simpler, by being elements of an enumeration, however, then we’d have to do manual matching and routing on the receiving end.

Notice, that the receiving part of the EventBus events is already in place, in the form of the @Subscribe annotations.

  class Player {
    boolean playing;
    boolean paused;

    @Subscribe
    void play(PlayEvent e) {
      System.out.println("play");
      playing = true;
      paused = false;
    }

    @Subscribe
    void pause(PauseEvent e) {
      System.out.println("pause");
      playing = false;
      paused = true;
    }

    @Subscribe
    void stop(StopEvent e) {
      System.out.println("stop");
      playing = false;
      paused = false;
    }
  }

  class PlayEvent {}

  class PauseEvent {}

  class StopEvent {}

In the following code, the EventBus is created, and the Player is registered as a receiver. To publish events, all that is required is to pass objects of the same types to the EventBus.post() method. The internals of the EventBus will invoke all methods which are marked with the @Subscribe annotation and match the exact type. Event types are not inherited, so a catch-all subscriber is not possible, which is probably a good design choice.

The EventBus also makes unit testing easier, since sender and receiver can be decoupled without extensive mocking. In fact, we now have a choice of testing the methods of the Player class through the Eventbus, or by invoking its methods directly. Either might be fine when writing unit tests. Although, for component level test, sticking with the EventBus is probably better, as it will be closer to the live application.

  private Player player;
  private EventBus bus;

  @Before
  public void setup() {
    player = new Player();

    bus = new EventBus();
    bus.register(player);
  }

  @Test
  public void testPlay() {
    bus.post(new PlayEvent());

    assertTrue(player.playing);
    assertFalse(player.paused);
  }

To include the Guava library from the Maven Gradle repository, this will suffice.

repositories {
  mavenCentral()
}

dependencies {
  compile 'com.google.guava:guava:21.0'
}

Here is the full code listing.

EventBusTest.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.guava;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Before;
import org.junit.Test;

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;

public class EventBusTest {

  class Player {
    boolean playing;
    boolean paused;

    @Subscribe
    void play(PlayEvent e) {
      System.out.println("play");
      playing = true;
      paused = false;
    }

    @Subscribe
    void pause(PauseEvent e) {
      System.out.println("pause");
      playing = false;
      paused = true;
    }

    @Subscribe
    void stop(StopEvent e) {
      System.out.println("stop");
      playing = false;
      paused = false;
    }
  }

  class PlayEvent {}

  class PauseEvent {}

  class StopEvent {}

  private Player player;
  private EventBus bus;

  @Before
  public void setup() {
    player = new Player();

    bus = new EventBus();
    bus.register(player);
  }

  @Test
  public void testPlay() {
    bus.post(new PlayEvent());

    assertTrue(player.playing);
    assertFalse(player.paused);
  }

  @Test
  public void testAll() {
    bus.post(new Object());

    assertFalse(player.playing);
    assertFalse(player.paused);
  }
}

Cantor circles and recursion

14 March 2017

This article draws inspiration from an old post dubbed “Cantor Circles”, but which turned out to render bisecting circles rather than Cantor sets. Nevertheless, it gained some interest, and the exact image below, from the 2002 post, can now be easily found on Google Image search. In this post, the old code is revived, and animation and colors are added for nice patterns and fun. Cantor’s ternary set is also implemented.

I suspect the origin of the bisecting circles was a simple example for recursive code, and simply dividing by two makes the code and rendering easy to understand. In the block below, the function drawCircles() is recursive, calling itself twice with parameters to half the next circles. A recursive function needs a stopping condition, and in this case it is the parameter times which stops further recursions when it reaches 0. The helper function drawMidCircle() makes it more convenient to set the coordinates and size of the circle.

  private void drawCircles(int x, int y, int radius, int times, Graphics g) {
    System.out.printf("x=%d, y=%d, r=%d, times=%d\n", x, y, radius, times);

    if (times > 0) {
      drawMidCircle(x, y, radius, g);
      drawCircles(x + radius / 2, y, radius / 2, times - 1, g);
      drawCircles(x - radius / 2, y, radius / 2, times - 1, g);
    }
  }

  private void drawMidCircle(int x, int y, int radius, Graphics g) {
    g.drawOval(x - radius, y - radius, 2 * radius, 2 * radius);
  }

The next example adds animation. The recursion is the same, but adds an angle parameter to rotate the circles. Also notice that the angle a is negated, which leads to the alternating clockwise and counter-clockwise rotation within each small circle.

Also of interest here, is the Swing double buffering (actually, triple buffering is used in this example) using the BufferStrategy class. Notice that the paint() method of the Frame is no longer overridden to render the graphics, but rather the Graphics object is provided by the BufferStrategy and passed to the custom render() function.

  private void drawCircles(int x, int y, int r, double a, int times, Graphics g) {
    if (times <= 0) {
      return;
    }

    drawMidCircle(x, y, r, g);

    int x1 = (int) (r / 2 * cos(a));
    int y1 = (int) (r / 2 * sin(a));
    drawCircles(x + x1, y + y1, r / 2, -a, times - 1, g);
    drawCircles(x - x1, y - y1, r / 2, -a, times - 1, g);
  }
    createBufferStrategy(3);
    BufferStrategy strategy = getBufferStrategy();

    new Thread(() -> {
      while (true) {
        Graphics g = strategy.getDrawGraphics();
        render(g);
        g.dispose();
        strategy.show();
      }
    }).start();

The last class in this article adds colors, a few options and a spartan UI to tune them. The images and animation below give a quick impression of what is possible by adjusting the sliders. Also note, the color palette can be easily changed during runtime by pasting in hex based color string found on pages like colourlovers.com.

The GIF animations were created using ImageMagick, with commands like these:

convert -delay 2 -loop 0 cantor*png cantor.gif

convert -delay 2 -loop 0 -resize 300x300 -layers Optimize -duplicate 1,-2-1 cantor0{0000..1200}.png cantor.gif
Cantor.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.graphics;

import java.awt.Color;
import java.awt.Graphics;

import javax.swing.JFrame;

/**
 * Draws recursive circles
 */
@SuppressWarnings("serial")
class Cantor extends JFrame {

  public static void main(String args[]) {
    new Cantor();
  }

  Cantor() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(900, 900);
    setVisible(true);
  }

  @Override
  public void paint(Graphics g) {
    g.setColor(Color.white);
    g.fillRect(0, 0, getWidth(), getHeight());
    g.setColor(Color.black);

    drawCircles(450, 450, 400, 7, g);
  }

  private void drawCircles(int x, int y, int radius, int times, Graphics g) {
    System.out.printf("x=%d, y=%d, r=%d, times=%d\n", x, y, radius, times);

    if (times > 0) {
      drawMidCircle(x, y, radius, g);
      drawCircles(x + radius / 2, y, radius / 2, times - 1, g);
      drawCircles(x - radius / 2, y, radius / 2, times - 1, g);
    }
  }

  private void drawMidCircle(int x, int y, int radius, Graphics g) {
    g.drawOval(x - radius, y - radius, 2 * radius, 2 * radius);
  }
}
CantorSpin.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.graphics;

import static java.lang.Math.*;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;

import javax.swing.JFrame;

/**
 * Draws recursive spinning circles.
 * 
 * Uses Double Buffering for smooth rendering. See:
 * http://docs.oracle.com/javase/tutorial/extra/fullscreen/bufferstrategy.html
 */
@SuppressWarnings("serial")
class CantorSpin extends JFrame {

  private final int size;

  private double angle;

  public static void main(String args[]) {
    new CantorSpin(800);
  }

  CantorSpin(int size) {
    this.size = size;

    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(size, size);
    setVisible(true);

    createBufferStrategy(3);
    BufferStrategy strategy = getBufferStrategy();

    new Thread(() -> {
      while (true) {
        Graphics g = strategy.getDrawGraphics();
        render(g);
        g.dispose();
        strategy.show();
      }
    }).start();
  }

  private void render(Graphics g) {
    clear(g);

    drawCircles(size / 2, size / 2, (int) (size * 0.4), angle, 7, g);
    angle += 0.01;
  }

  private void clear(Graphics g) {
    g.setColor(Color.white);
    g.fillRect(0, 0, getWidth(), getHeight());
    g.setColor(Color.black);
  }

  private void drawCircles(int x, int y, int r, double a, int times, Graphics g) {
    if (times <= 0) {
      return;
    }

    drawMidCircle(x, y, r, g);

    int x1 = (int) (r / 2 * cos(a));
    int y1 = (int) (r / 2 * sin(a));
    drawCircles(x + x1, y + y1, r / 2, -a, times - 1, g);
    drawCircles(x - x1, y - y1, r / 2, -a, times - 1, g);
  }

  private void drawMidCircle(int x, int y, int r, Graphics g) {
    g.drawOval(x - r, y - r, 2 * r, 2 * r);
  }
}
CantorColors.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.graphics;

import static java.lang.Math.*;

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;

import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.JToggleButton;

/**
 * Draws recursive spinning circles with colors.
 */
@SuppressWarnings("serial")
class CantorColors extends JFrame {

  private Canvas canvas;

  private JPanel colorOut;

  private String colorsList =
      "#000fff,#18A3AC,#F48024,#178CCB,#052049,#FBAF3F,#000fff";
  // "#18A3AC,#052049,#178CCB ,#FBAF3F,#F48024,#18A3AC,#052049,#178CCB";
  // "#722059,#A92268,#E02477,#DB7580,#D6C689,#722059,#A92268,#E02477";
  // "#F28E98,#FF9985,#E1CBA6,#DFCFB8,#E7E4D3,#F28E98,#FF9985,#E1CBA6";

  private Color[] colors;

  private double angle;
  private double angleStepSize;
  private double alternateSign;

  private int sleepMs;

  private float transparency;

  private int recursions;

  private boolean cantor;

  private boolean clearScreen;

  private boolean save;

  private int index;

  private BufferedImage bufImg;

  private Graphics imgG;

  public static void main(String args[]) {
    CantorColors cc = new CantorColors();
    cc.initUi();
    cc.start();
  }

  private void initUi() {
    setLayout(new BorderLayout());

    canvas = new Canvas();
    getContentPane().add(canvas, BorderLayout.CENTER);

    JPanel controls = new JPanel();
    controls.setLayout(new BoxLayout(controls, BoxLayout.Y_AXIS));
    getContentPane().add(controls, BorderLayout.NORTH);

    addSlider(controls, "Rotation step size", 1, 100, 5, slider -> {
      angleStepSize = log(slider.getValue());
    });

    addSlider(controls, "Frame delay", 0, 100, 0, slider -> {
      sleepMs = slider.getValue();
    });

    addSlider(controls, "Recursions", 1, 20, 7, slider -> {
      recursions = slider.getValue();
    });

    addSlider(controls, "Transparency", 1, 1000, 1000, slider -> {
      transparency = (float) slider.getValue();
      colors = decodeColors(colorsList);
      showColors();
    });

    JTextField colorIn = new JTextField(colorsList);
    colorIn.addKeyListener(new KeyAdapter() {
      public void keyTyped(KeyEvent e) {
        colorsList = colorIn.getText();
        colors = decodeColors(colorsList);
        showColors();
      }
    });
    controls.add(colorIn);

    colorOut = new JPanel();
    colorOut.setLayout(new BoxLayout(colorOut, BoxLayout.X_AXIS));
    colors = decodeColors(colorsList);
    showColors();
    controls.add(colorOut);

    JPanel buttons = new JPanel();
    buttons.setLayout(new BoxLayout(buttons, BoxLayout.X_AXIS));
    controls.add(buttons);

    addToggleButton(buttons, "Bisect vs. Cantor", false, button -> {
      cantor = button.isSelected();
    });

    addToggleButton(buttons, "Alternate rotation", true, button -> {
      alternateSign = button.isSelected() ? -1 : 1;
    });

    addToggleButton(buttons, "Save PNGs", false, button -> {
      save = button.isSelected();
      index = 0;
    });

    JButton clearButton = new JButton("Clear");
    clearButton.addActionListener(l -> {
      clearScreen = true;
    });
    buttons.add(clearButton);

    canvas.addComponentListener(new ComponentAdapter() {
      @Override
      public void componentResized(ComponentEvent e) {
        createRenderImage();
      }
    });

    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(600, 738);
    setVisible(true);
  }

  private void addSlider(JPanel controls, String text, int min, int max, int value,
      Consumer<JSlider> changeListener) {
    JPanel row = new JPanel();
    row.setLayout(new BoxLayout(row, BoxLayout.X_AXIS));
    controls.add(row);

    JLabel label = new JLabel(text);
    label.setPreferredSize(new Dimension(150, (int) label.getPreferredSize().getHeight()));
    row.add(label);

    JSlider slider = new JSlider(min, max, value);
    row.add(slider);

    slider.addChangeListener(e -> {
      changeListener.accept(slider);
    });
  }

  private void addToggleButton(JPanel buttons, String text, boolean selected,
      Consumer<JToggleButton> actionListener) {
    JToggleButton button = new JToggleButton(text);
    button.setSelected(selected);
    buttons.add(button);

    button.addActionListener(e -> {
      actionListener.accept(button);
    });
  }

  private void showColors() {
    colorOut.removeAll();
    colorOut.setBackground(Color.WHITE);
    for (Color c : colors) {
      JPanel l = new JPanel();
      l.setPreferredSize(
          new Dimension(getWidth() / colors.length, (int) l.getPreferredSize().getHeight()));
      l.setBackground(c);
      colorOut.add(l);
    }
    colorOut.revalidate();
    colorOut.repaint();
  }

  private void start() {
    transparency = 1000f;
    angleStepSize = 0.005;
    alternateSign = -1;
    recursions = 7;
    colors = decodeColors(colorsList);
    showColors();

    createRenderImage();
    Graphics g = canvas.getGraphics();

    new Thread(() -> {
      while (true) {
        render(imgG);

        if (save && index++ % 3 == 0) {
          saveImage(bufImg, index);
        }

        g.drawImage(bufImg, 0, 0, this);

        sleep(sleepMs);
      }
    }).start();
  }

  private void createRenderImage() {
    bufImg = new BufferedImage(canvas.getWidth(), canvas.getHeight(), BufferedImage.TYPE_INT_ARGB);
    imgG = bufImg.createGraphics();
    clear(imgG);
  }

  private void saveImage(BufferedImage bi, int i) {
    try {
      String name = String.format("/tmp/cantor%05d.png", i);
      ImageIO.write(bi, "PNG", new File(name));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * Given a string of comma separated hex encoded RGB values, creates the
   * individual Color objects. All spaces are removed and ignored; the hex
   * values can be prefixed by a hash (#) or not. Adds the alpha transparency
   * setting from the UI.
   */
  private Color[] decodeColors(String hexColors) {
    try {
      String[] split = hexColors.replaceAll(" +", "").split(",");
      Color[] result = new Color[split.length];

      for (int i = 0; i < split.length; i++) {
        String str = (split[i].startsWith("#") ? "" : "#") + split[i];
        float[] comp = Color.decode(str).getRGBComponents(null);
        float a = max(0, min(1f, ((float) i) / transparency));
        result[i] = new Color(comp[0], comp[1], comp[2], a);
      }

      return result;
    } catch (Exception e) {
      System.out.println(e);
      return colors;
    }
  }

  private void sleep(long ms) {
    try {
      Thread.sleep(ms);
    } catch (InterruptedException e) {}
  }

  private void render(Graphics g) {
    if (clearScreen) {
      clear(g);
      clearScreen = false;
    }

    int size = min(canvas.getWidth(), canvas.getHeight());
    drawCantor(size / 2, size / 2, (int) (size * 0.45), angle, recursions, g);
    angle += angleStepSize;
  }

  private void drawCantor(int x, int y, int r, double a, int times, Graphics g) {
    if (times <= 0) {
      return;
    }

    g.setColor(colors[times % colors.length]);
    drawMidCircle(x, y, r, g);

    double nextAngel = alternateSign * a;

    if (cantor) {
      // Cantor set
      int x1 = (int) (r / 3 * cos(a));
      int y1 = (int) (r / 3 * sin(a));
      drawCantor(x + 2 * x1, y + 2 * y1, r / 3, nextAngel, times - 1, g);
      drawCantor(x - 2 * x1, y - 2 * y1, r / 3, nextAngel, times - 1, g);
    } else {
      // Bisect
      int x1 = (int) (r / 2 * cos(a));
      int y1 = (int) (r / 2 * sin(a));
      drawCantor(x + x1, y + y1, r / 2, nextAngel, times - 1, g);
      drawCantor(x - x1, y - y1, r / 2, nextAngel, times - 1, g);
    }
  }

  private void drawMidCircle(int x, int y, int r, Graphics g) {
    g.drawOval(x - r, y - r, 2 * r, 2 * r);
  }

  private void clear(Graphics g) {
    g.setColor(Color.white);
    g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
  }
}

Sun Doclet API

10 March 2017

Since Java 1.2, the javadoc command has generated neatly formatted documentation. The tool comes with its own API which allows customised output. The relevant classes are under the com.sun package hierarchy, and located in JRE_HOME/lib/tools.jar, which typically will have to be included manually. E.g. it can be found under /usr/lib/jvm/java-8-openjdk-amd64/lib/tools.jar.

Note that the Sun Doclet API is getting long in th tooth, and is already slated for replacement in Java 9, through the “Simplified Doclet API” in JDK Enhancement Proposal 221. Java 9 is planned for Q3 2017.

Meanwhile, the old Doclet API still does an OK job of parsing JavaDoc in .java source files. If the goal is to parse, rather than to produce the standard formatted JavaDoc, it’s useful to start the process pragmatically. Than can be achieved through its main class, com.sun.tools.javadoc.Main:

    Main.execute("MyName", SunDocletPrinter.class.getName(),
        new String[] { "com/rememberjava/doc/SunDocletPrinter.java" });

The execute() method will invoke the public static method start() in the specified class. In the example below, a few of the main JavaDoc entities are enumerated. The direct output can be see the block below. The class which is parsed is the example class itself, included at the bottom of this article.

  public static boolean start(RootDoc root) {
    System.out.println("--- start");

    for (ClassDoc classDoc : root.classes()) {
      System.out.println("Class: " + classDoc);

      // Class annotations
      for (AnnotationDesc annotation : classDoc.annotations()) {
        System.out.println("  Annotation: " + annotation);
      }

      // Class JavaDoc tags
      for (Tag tag : classDoc.tags()) {
        System.out.println("  Class tag:" + tag.name() + "=" + tag.text());
      }

      // Global constants and fields
      for (FieldDoc fieldDoc : classDoc.fields()) {
        System.out.println("  Field: " + fieldDoc);
      }

      // Methods
      for (MethodDoc methodDoc : classDoc.methods()) {
        System.out.println("  Method: " + methodDoc);

        // Method annotations
        for (AnnotationDesc annotation : methodDoc.annotations()) {
          System.out.println("    Annotation: " + annotation);
        }

        // Method JavaDoc comment (without parameters)
        System.out.println("    Doc: " + methodDoc.commentText());

        // Method JavaDoc (only the first sentence)
        for (Tag tag : methodDoc.firstSentenceTags()) {
          System.out.println("    Tag: " + tag);
        }

        // Method parameters (without return)
        for (ParamTag paramTag : methodDoc.paramTags()) {
          System.out.println("    Param:" + paramTag.parameterName() + "=" + paramTag.parameterComment());
        }

        // The full method JavaDoc text
        System.out.println("    Raw doc:\n" + methodDoc.getRawCommentText());
      }
    }

    System.out.println("--- the end");
    return true;
  }

- start main
Loading source file com/rememberjava/doc/SunDocletPrinter.java...
Constructing Javadoc information...
--- start
Class: com.rememberjava.doc.SunDocletPrinter
  Annotation: @java.lang.Deprecated
  Class tag:@author=Bob
  Class tag:@since=123
  Class tag:@custom=Custom Annotation
  Class tag:@see="http://docs.oracle.com/javase/6/docs/technotes/guides/javadoc/doclet/overview.html"
  Field: com.rememberjava.doc.SunDocletPrinter.SOME_FIELD
  Method: com.rememberjava.doc.SunDocletPrinter.main(java.lang.String[])
    Annotation: @java.lang.SuppressWarnings("Test")
    Doc: 
    Raw doc:
 @see "http://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/standard-doclet.html"

  Method: com.rememberjava.doc.SunDocletPrinter.start(com.sun.javadoc.RootDoc)
    Doc: This method processes everything. And there's more to it.
    Tag: Text:This method processes everything.
    Param:root=the root element
    Raw doc:
 This method processes everything. And there's more to it.
 
 @param root
          the root element
 @return returns true

--- the end
- done execute

Here is full file, which also shows the JavaDoc the example operates on.

SunDocletPrinter.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.doc;

import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;
import com.sun.tools.javadoc.Main;

/**
 * Example self-contained Doclet which prints raw text.
 * 
 * @author Bob
 * @since 123
 * @custom Custom Annotation
 * @see "http://docs.oracle.com/javase/6/docs/technotes/guides/javadoc/doclet/overview.html"
 */
@Deprecated
public class SunDocletPrinter {

  public static String SOME_FIELD;

  /**
   * @see "http://docs.oracle.com/javase/7/docs/technotes/guides/javadoc/standard-doclet.html"
   */
  @SuppressWarnings(value = { "Test" })
  public static void main(String[] args) {
    System.out.println("- start main");
    Main.execute("MyName", SunDocletPrinter.class.getName(),
        new String[] { "com/rememberjava/doc/SunDocletPrinter.java" });
    System.out.println("- done execute");
  }

  /**
   * This method processes everything. And there's more to it.
   * 
   * @param root
   *          the root element
   * @return returns true
   */
  public static boolean start(RootDoc root) {
    System.out.println("--- start");

    for (ClassDoc classDoc : root.classes()) {
      System.out.println("Class: " + classDoc);

      // Class annotations
      for (AnnotationDesc annotation : classDoc.annotations()) {
        System.out.println("  Annotation: " + annotation);
      }

      // Class JavaDoc tags
      for (Tag tag : classDoc.tags()) {
        System.out.println("  Class tag:" + tag.name() + "=" + tag.text());
      }

      // Global constants and fields
      for (FieldDoc fieldDoc : classDoc.fields()) {
        System.out.println("  Field: " + fieldDoc);
      }

      // Methods
      for (MethodDoc methodDoc : classDoc.methods()) {
        System.out.println("  Method: " + methodDoc);

        // Method annotations
        for (AnnotationDesc annotation : methodDoc.annotations()) {
          System.out.println("    Annotation: " + annotation);
        }

        // Method JavaDoc comment (without parameters)
        System.out.println("    Doc: " + methodDoc.commentText());

        // Method JavaDoc (only the first sentence)
        for (Tag tag : methodDoc.firstSentenceTags()) {
          System.out.println("    Tag: " + tag);
        }

        // Method parameters (without return)
        for (ParamTag paramTag : methodDoc.paramTags()) {
          System.out.println("    Param:" + paramTag.parameterName() + "=" + paramTag.parameterComment());
        }

        // The full method JavaDoc text
        System.out.println("    Raw doc:\n" + methodDoc.getRawCommentText());
      }
    }

    System.out.println("--- the end");
    return true;
  }
}

Connecting to Bittorrent's Mainline DHT

01 March 2017

In addition to its fast and efficient file transfer protocol, Bittorrent and other peer-to-peer file sharing networks bring another interesting technology to the masses: the Distributed Hash Table (DHT). In the case of Bittorrent, this is used to look up and download a torrent file based on its magnet link. The DHT network for Bittorrent is called Mainline DHT based the Kademlia DHT, although the network itself is separate from other implementations.

For Mainline DHT, there is an interesting open source client and library, called “mldht”. There are two instances on Github, with moreus/mldht as the original and the fork the8472/mldht. Although, both seems to be somewhat active. Both depend on the EdDSA library which the8472 has also forked. To get started, grab the source, and make sure the “mldht” code depend on or have access to the “ed25519” project. To confirm run the 28 unit tests from “mldht”. They should all pass.

https://github.com/the8472/ed25519-java.git

https://github.com/the8472/mldht.git

The “mldht” project includes a stand-alone DHT server node which can be started through the executable Launcher class. Its main configuration is in “config.xml” which gets written to the current directory if it does not already exist. It’s based on “config-defaults.xml”. To be able to connect to the DHT network, I had to change the option “multihoming” to false in the config file.

Furthermore, in order to use the CLI client utility, the CLI server has to be enabled. I ended up with the following configuration file.

Once started, the activity can be observed in log/dht.log. There will be plenty of noise.

<?xml version="1.0" encoding="UTF-8"?>
<mldht:config
  xmlns:mldht="http://mldht/config/"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="http://mldht/config/config.xsd ">

  <core>
    <logLevel>Info</logLevel>
    <port>49001</port>
    <useBootstrapServers>true</useBootstrapServers>
    <multihoming>false</multihoming>
    <persistID>true</persistID>
  </core>
    
  <components>
    <component>
      <className>the8472.mldht.cli.Server</className>
    </component>
  </components>

</mldht:config>

While the server is running, the CLI Client executable can be used to issue a few commands. Of interest is the “SAMPLE” command which lists random hash keys from random peers. Using that output, the “GETPEERS” can be used to look up specific hashes (make sure to remove the space formatting from the sample output). Finally, a torrent file can be downloaded with the “GETTORRENT” command.

Assuming the Java classpath is set correctly, example use would look like:

java the8472.mldht.cli.Client SAMPLE
java the8472.mldht.cli.Client GETPEERS f61c5a0dfaac58ba943c5d0c115343477196ad91
java the8472.mldht.cli.Client GETTORRENT f61c5a0dfaac58ba943c5d0c115343477196ad91

The hash used above is the Wikileaks “insurance” file posted last December, with the name “2016-12-09_WL-Insurance.aes256”. The “mldht” project does not contain any tools to actually read the torrent, but we can use the Transmission client:

apt-get install transmission-cli

transmission-show f61c5a0dfaac58ba943c5d0c115343477196ad91.torrent

The expected output would look like this:

Name: 2016-12-09_WL-Insurance.aes256
File: F61C5A0DFAAC58BA943C5D0C115343477196AD91.torrent

GENERAL

  Name: 2016-12-09_WL-Insurance.aes256
  Hash: f61c5a0dfaac58ba943c5d0c115343477196ad91
  Created by: 
  Created on: Unknown
  Piece Count: 42979
  Piece Size: 2.00 MiB
  Total Size: 90.13 GB
  Privacy: Public torrent

TRACKERS

FILES

  2016-12-09_WL-Insurance.aes256 (90.13 GB)

Given that this worked fine, I’m thinking it should be trivial to create a custom ML DHT client which performs the steps above. I hope to come back to that in a future article.

Try Catch Exception

28 February 2017

This post looks at the syntax variations of the try/catch/finally blocks. For further details, see the excellent Java tutorial on the topic.

To start off, below is the basic syntax, with code surrounded by a try-block, and a NullPointerException caught by the catch-block. As can bee seen, the code will fail, since the variable “str” is null, leading to a NullPointerException. The Exception variable in the catch-block is by most common conventions simply “e”. It has a few convenience methods, including printStackTrace() which shows the call trace since the Exception was thrown. Although the print-out might look scary, it does provide useful information to the developer. Thus, keeping the full stack trace is helpful, typically in a detailed log-file. That is beyond the scope of this post.

    try {
      String str = null;
      str.toString();
    } catch (NullPointerException e) {
      e.printStackTrace();
    }

Exceptions are typed classes, and in the following example the catch-block will not be reached since the expected Exception is not the same or a sub-type of the one which is thrown: NullPointerException vs. ArithmeticException. Instead, the ArithmeticException will be thrown out of the method.

    try {
      int a = 1 / 0;
    } catch (NullPointerException e) {
      System.out.println("This will not trigger.");
    }
    System.out.println("Will not reach this point.");

To handle multiple exception types, there are three options: Declare multiple catch-blocks with different types, as seen in the first part below; or declare multiple Exceptions within the same catch statement, as in the second part. The latter syntax has been available since Java 7. Finally, it’s possible to catch multiple Exceptions by specifying a type higher up the class hierarchy, e.g. using Exception or Throwable.

    try {
      Integer a = null;
      int b = 1 / a;
    } catch (NullPointerException e) {
      System.out.println("NullPointerException");
    } catch (ArithmeticException e) {
      System.out.println("ArithmeticException");
    }

    try {
      Integer a = null;
      int b = 1 / a;
    } catch (NullPointerException | ArithmeticException e) {
      e.printStackTrace();
    }

In addition to the try and catch blocks, there is also a finally-block. It is executed at the end, regardless of whether there was an Exceptions thrown or not. This is useful for setting state or cleaning up, and a common example is closing an IO stream. However, as seen below, this can get crufty since we have to consider that the IO object might not have been opened in the first place, and that the close() method throws its own checked Exception.

    OutputStream out = null;
    try {
      out = new FileOutputStream("/dev/null");
      out.write(0);
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (out != null) {
        try {
          out.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }

To clean up the code above, the try-with-resources syntax was introduced in Java 7. It allows the try statement to take an extra block which is executed before its content. In addition, variables declared within this block will be closed at the end through the Closeable interface. This significantly reduces the foot-print of the code above.

    try (OutputStream out = new FileOutputStream("/dev/null")) {
      out.write(0);
    } catch (IOException e) {
      e.printStackTrace();
    }

A good example for multiple resources is the Socket example discussed previously. Here the Socket and both IO streams are closable resources handled by the try-block.

    try (Socket s = new Socket("google.com", 80);
         OutputStream out = s.getOutputStream();
         InputStream in = s.getInputStream()) {
      out.write(0);
      in.read();
    } catch (IOException e) {
      e.printStackTrace();
    }

Finally, a word on messaging and wrapping of Exceptions. As mentioned in the tutorial, it’s poor practice to throw RuntimeExceptions or simply wrap checked Exceptions, as seen below. However, regardless of where you stand in that debate, Exceptions can always be made more helpful and useful by clear messaging and relevant context. The wrapped RuntimeException below adds a more specific message and also includes the filename the IO stream operates on, since it might not be included in all types of IOExceptions. Furthermore, in the case of the File object, is is useful to use the getAbsolutePath() method. It forces the full path to the resolved and included. It really helps when debugging issues where the full path can be copy/pasted and confirmed.

    File file = new File("/dev/null");
    try (OutputStream out = new FileOutputStream(file)) {
      out.write(0);
    } catch (IOException e) {
      throw new RuntimeException("Could not open or read file " + file.getAbsolutePath(), e);
    }

Here is the full listing with all examples as tests.

TryCatchTest.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.basics;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import org.junit.Test;

public class TryCatchTest {

  // The second try-block will throw an uncaught ArithmeticException because of
  // the divide by 0.
  @Test(expected = ArithmeticException.class)
  public void basic() {
    try {
      String str = null;
      str.toString();
    } catch (NullPointerException e) {
      e.printStackTrace();
    }

    try {
      int a = 1 / 0;
    } catch (NullPointerException e) {
      System.out.println("This will not trigger.");
    }
    System.out.println("Will not reach this point.");
  }

  @Test
  public void multi() {
    try {
      Integer a = null;
      int b = 1 / a;
    } catch (NullPointerException e) {
      System.out.println("NullPointerException");
    } catch (ArithmeticException e) {
      System.out.println("ArithmeticException");
    }

    try {
      Integer a = null;
      int b = 1 / a;
    } catch (NullPointerException | ArithmeticException e) {
      e.printStackTrace();
    }
  }

  @Test
  public void testFinally() {
    OutputStream out = null;
    try {
      out = new FileOutputStream("/dev/null");
      out.write(0);
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (out != null) {
        try {
          out.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

  @Test
  public void tryWith() {
    try (OutputStream out = new FileOutputStream("/dev/null")) {
      out.write(0);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  @Test
  public void tryWithMulti() {
    try (Socket s = new Socket("google.com", 80);
         OutputStream out = s.getOutputStream();
         InputStream in = s.getInputStream()) {
      out.write(0);
      in.read();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  @Test
  public void message() {
    File file = new File("/dev/null");
    try (OutputStream out = new FileOutputStream(file)) {
      out.write(0);
    } catch (IOException e) {
      throw new RuntimeException("Could not open or read file " + file.getAbsolutePath(), e);
    }
  }
}

Unit test Exceptions

24 February 2017

Testing the “happy path” of the code, when everything goes right, is fine, however error handling and Exceptions is just as much a part of the code under test. In fact, it is possibly a more delicate area, since you want an application which degrades gracefully in the event of error. This post goes through different ways of setting expectations on thrown Exceptions.

In the first method below, the old style pre-Junit 4 way of asserting for an Exception is shown. The idea is that the Exception must be thrown, so if the execution reaches the fail() statement, that did not happen. The expected path is instead that the catch-block engages, with the expected Exception type. If a different type of Exception, which is not a sub-class of the caught Exception is thrown, it propagates out of the method and the test fails. Although this style is a bit clunky and verbose, it has a few advantages over the annotation-style: You have control over exactly what point in the code you expect the Exception to be thrown; you can inspect the Exception and assert its message; you can set a custom error message.

  public void testOldStyle() {
    try {
      Integer.parseInt(INVALID_INTEGER);
      fail("Expected an Exception to be thrown");
    } catch (NumberFormatException e) {
      assertEquals("For input string: \"" + INVALID_INTEGER + "\"", e.getMessage());
    }
  }

Since Java 6 and Junit 4, annotations become available, and the @Test annotation is now the way to declare a test method. It comes with an extra parameter expected which takes a Class type indicating which Exception is expected to be thrown. This approach is clean and even minimalist. If all there is to a test is a one-liner like shown below, this is a perfectly fine way to declaring the expectation. However, it loses the ability to inspect the message or cause. Furthermore, if the test method contains more lines, there is no way to control or verify from where the Exception originated.

  @Test(expected = NumberFormatException.class)
  public void annotation() {
    Integer.parseInt(INVALID_INTEGER);
  }

Enter JUnit 4.7, and the @Rule annotation and ExpectedException rule. It solves the problem with the Test annotation above, but retains a clean way of expressing the assertion. In the example below, the Rule annotation makes sure the thrown field is initialised a-new before every test method. It can then be used right above the method which is expected to throw an Exception, and can assert on its type and message. The expectMessage() method asserts that the message contains rather than equals the expected string.

  @Rule
  public final ExpectedException thrown = ExpectedException.none();

  @Test
  public void testThrown() {
    thrown.expect(NumberFormatException.class);
    thrown.expectMessage(INVALID_INTEGER);

    Integer.parseInt(INVALID_INTEGER);
  }

The ExpectedException class comes with some extra assertion methods which takes the popular Hamcrest matchers. In the code below, the endsWith matcher is used.

import static org.hamcrest.CoreMatchers.endsWith;

  @Test
  public void hamcrest() {
    thrown.expect(NumberFormatException.class);
    thrown.expectMessage(endsWith(INVALID_INTEGER + "\""));

    Integer.parseInt(INVALID_INTEGER);
  }

Finally, the expectCause() method takes another Hamcrest matcher, where for example the type of the contained cause can be asserted. Notice that the outer exception type can be asserted as well. The expectCause assertion only goes one level deep, so if further unnesting is required, a custom Matcher could be implemented. In this example, the wrapping RuntimeException does not alter the message, so it can be asserted directly. If the outer Exception has its own message, another custom Matcher would be needed to assert on the inner message.

import static org.hamcrest.CoreMatchers.isA;

  @Test
  public void cause() {
    thrown.expect(RuntimeException.class);
    thrown.expectMessage(INVALID_INTEGER);
    thrown.expectCause(isA(NumberFormatException.class));

    try {
      Integer.parseInt(INVALID_INTEGER);
    } catch (NumberFormatException e) {
      throw new RuntimeException(e);
    }
  }

Here is the full test case listing.

ExceptionsTest.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.junit;

import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.isA;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class ExceptionsTest {

  private static final String INVALID_INTEGER = "invalid integer";

  @Rule
  public final ExpectedException thrown = ExpectedException.none();

  @Test
  public void testOldStyle() {
    try {
      Integer.parseInt(INVALID_INTEGER);
      fail("Expected an Exception to be thrown");
    } catch (NumberFormatException e) {
      assertEquals("For input string: \"" + INVALID_INTEGER + "\"", e.getMessage());
    }
  }

  @Test(expected = NumberFormatException.class)
  public void annotation() {
    Integer.parseInt(INVALID_INTEGER);
  }

  @Test
  public void testThrown() {
    thrown.expect(NumberFormatException.class);
    thrown.expectMessage(INVALID_INTEGER);

    Integer.parseInt(INVALID_INTEGER);
  }

  @Test
  public void hamcrest() {
    thrown.expect(NumberFormatException.class);
    thrown.expectMessage(endsWith(INVALID_INTEGER + "\""));

    Integer.parseInt(INVALID_INTEGER);
  }

  @Test
  public void cause() {
    thrown.expect(RuntimeException.class);
    thrown.expectMessage(INVALID_INTEGER);
    thrown.expectCause(isA(NumberFormatException.class));

    try {
      Integer.parseInt(INVALID_INTEGER);
    } catch (NumberFormatException e) {
      throw new RuntimeException(e);
    }
  }
}

Socket client / server example

21 February 2017

After posts on several server libraries, including Sun’s HTTP; Simple Framework’s HTTP; and Websocket, it seems appropriate to include a plain TCP socket server / client example. Without any application protocol to adhere to, it’s straight forward to setup and test. There are two essential classes involved: On the server side there’s a ServerSocket, while the client has a Socket. The only difference is that the ServerSocket accepts one or more incoming connections, and provide a Socket handle for each. After that, both client and server are the same, in that the Socket object provides input and output streams once established.

In the code below, a ServerSocket is set to listen to a specific port, and to accept a single incoming connection. It grabs the IO streams, makes these available to the rest of the test, and finally releases a semaphore lock to allow the test to continue. Normally, the server thread would wait for further incoming requests, and would probably spawn or assign pooled threads to handle the request. Here we focus only on the basic IO parts.

...
    ServerSocket server = new ServerSocket(PORT);
    listen(server);
...

  private void listen(ServerSocket server) {
    new Thread(() -> {
      try {
        Socket socket = server.accept();
        System.out.println("Incoming connection: " + socket);

        serverOut = socket.getOutputStream();
        serverIn = socket.getInputStream();

        lock.release();
        System.out.println("Released lock");
      } catch (IOException e) {
        e.printStackTrace();
      }
    }).start();
  }

On the client side, it’s just as simple: Establish a connection to the severer host and port, and get the IO streams.

    Socket client = new Socket("localhost", PORT);
    OutputStream clientOut = client.getOutputStream();
    InputStream clientIn = client.getInputStream();

The rest of the test code below asserts that messages are received correctly both on the client and the server, using the raw streams and wrapped PrinterWriter helpers. Again, he semaphore is used to wait for the server thread to establish its connection before the rest of the test continues. Without it, using the IO streams will results in NullPointerExceptions, since they are not initialized yet.

SocketTest.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.net;

import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Semaphore;

import org.junit.Test;

/**
 * Simple client / server Socket tests, including a Buffered PrintWriter which
 * has to be flushed.
 */
public class SocketTest {

  private static final int PORT = 8887;

  private OutputStream serverOut;
  private InputStream serverIn;

  /**
   * Shared lock between the "client" and "server" code, to make the test case
   * synchronous.
   */
  private Semaphore lock = new Semaphore(0);

  /**
   * Tests server and client side sockets in one flow. A lock object is used for
   * synchronous between the two sides.
   */
  @Test
  public void testClientServer() throws IOException, InterruptedException {
    ServerSocket server = new ServerSocket(PORT);
    listen(server);

    Socket client = new Socket("localhost", PORT);
    OutputStream clientOut = client.getOutputStream();
    InputStream clientIn = client.getInputStream();

    System.out.println("Waiting for lock");
    lock.acquire();
    System.out.println("Acquired lock");

    write(clientOut, "Hi");
    assertRead(serverIn, "Hi");

    write(serverOut, "Hello");
    assertRead(clientIn, "Hello");

    printWrite(clientOut, "Test printWrite");
    assertRead(serverIn, "Test printWrite");

    printWrite(serverOut, "Test printWrite again");
    assertRead(clientIn, "Test printWrite again");

    client.close();
    server.close();
  }

  /**
   * Writes to an OutputStream. Used for both server and client output streams.
   */
  private void write(OutputStream out, String str) throws IOException {
    out.write(str.getBytes());
    out.flush();
  }

  /**
   * Writes to an OutputStream. Used for both server and client output streams.
   */
  private void printWrite(OutputStream out, String str) throws IOException {
    PrintWriter pw = new PrintWriter(out);
    pw.print(str);
    pw.flush();
  }

  /**
   * Reads from an InputStream. Used for both server and client input streams.
   */
  private void assertRead(InputStream in, String expected) throws IOException {
    assertEquals("Too few bytes available for reading: ", expected.length(), in.available());

    byte[] buf = new byte[expected.length()];
    in.read(buf);
    assertEquals(expected, new String(buf));
  }

  /**
   * Listens for and accepts one incoming request server side on a separate
   * thread. When a request is received, grabs its IO streams and "signals" to
   * the client side above through the shared lock object.
   */
  private void listen(ServerSocket server) {
    new Thread(() -> {
      try {
        Socket socket = server.accept();
        System.out.println("Incoming connection: " + socket);

        serverOut = socket.getOutputStream();
        serverIn = socket.getInputStream();

        lock.release();
        System.out.println("Released lock");
      } catch (IOException e) {
        e.printStackTrace();
      }
    }).start();
  }
}

Styles with JTextPane

20 February 2017

The Swing tutorial for the JTextPane and demo code is good and comprehensive, covering multiple ways to interact with the the StyledDocument and add Styles. So, without repeating all of that, here’s a minimal example including only simple style changes on the insert update and selection events.

In the first snippet below, the implementation for the insertUpdate event is shown. For each typed character or pasted text, the entire text of the document will be searched for the word “foo”. For each occurrence, the bold attribute is set for that word. Notice that the update of the attribute happens on a separate AWT thread.

  @Override
  public void insertUpdate(DocumentEvent event) {
    try {
      String text = doc.getText(0, doc.getLength());

      Pattern p = Pattern.compile(FOO);
      Matcher matcher = p.matcher(text);
      while (matcher.find()) {
        updateAttribute(matcher.start(), FOO.length(), Style.BOLD);
      }
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
  }

  private void updateAttribute(int pos, int len, Style style) {
    SwingUtilities.invokeLater(() -> {
      doc.setCharacterAttributes(pos, len, style.get(), true);
    });
  }

The other functionality of this small application is on select. If the selected word is “bar”, it is set to italic. Notice the dot and mark positions, which might be at the start or the end of the selection. Furthermore, notice that here the predefined ItalicAction from the StyledEditorKit is used, since we’re dealing with a selection. We just have to translate the CaretEvent into an ActionEvent, or rather just make sure to forward the source component of the selection. (The alternative would have been to go with the plain update as in the example above).

  @Override
  public void caretUpdate(CaretEvent event) {
    int dot = event.getDot();
    int mark = event.getMark();
    int start;
    int end;

    if (dot == mark) {
      return;
    } else if (dot < mark) {
      start = dot;
      end = mark;
    } else {
      start = mark;
      end = dot;
    }

    System.out.println(start + ", " + end);
    try {
      if (doc.getText(start, BAR.length()).startsWith(BAR)) {
        ItalicAction action = new StyledEditorKit.ItalicAction();
        action.actionPerformed(new ActionEvent(event.getSource(), 0, ""));

        // Alternative custom update:
        // updateAttribute(start, BAR.length(), Style.ITALIC);
      }
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
  }

Here’s the full file listing, as a stand-alone application.

JTextPaneStylesExample.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.ui;

import java.awt.event.ActionEvent;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JFrame;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.StyledEditorKit.ItalicAction;

/**
 * Minimal example with JTextPane and StyledDocument. When the string "foo" is
 * typed or pasted it is made bold. When the string "bar" is selected it is made
 * italic.
 */
@SuppressWarnings("serial")
public class JTextPaneStylesExample extends JFrame implements DocumentListener, CaretListener {

  /**
   * Predefined styles.
   */
  enum Style {
    BOLD(StyleConstants.Bold),
    ITALIC(StyleConstants.Italic);

    private MutableAttributeSet attrib;

    private Style(Object style) {
      attrib = new SimpleAttributeSet();
      attrib.addAttribute(style, true);
    }

    AttributeSet get() {
      return attrib;
    }
  }

  private static final String FOO = "foo";

  private static final String BAR = "bar";

  private final StyledDocument doc;

  public static void main(String[] args) {
    new JTextPaneStylesExample();
  }

  public JTextPaneStylesExample() {
    JTextPane editor = new JTextPane();
    doc = editor.getStyledDocument();
    getContentPane().add(editor);

    setSize(500, 500);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setVisible(true);

    editor.getDocument().addDocumentListener(this);
    editor.addCaretListener(this);
  }

  /**
   * Check of a selection of the string "bar".
   */
  @Override
  public void caretUpdate(CaretEvent event) {
    int dot = event.getDot();
    int mark = event.getMark();
    int start;
    int end;

    if (dot == mark) {
      return;
    } else if (dot < mark) {
      start = dot;
      end = mark;
    } else {
      start = mark;
      end = dot;
    }

    System.out.println(start + ", " + end);
    try {
      if (doc.getText(start, BAR.length()).startsWith(BAR)) {
        ItalicAction action = new StyledEditorKit.ItalicAction();
        action.actionPerformed(new ActionEvent(event.getSource(), 0, ""));

        // Alternative custom update:
        // updateAttribute(start, BAR.length(), Style.ITALIC);
      }
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
  }

  /**
   * Check for the occurrence of the string "foo".
   */
  @Override
  public void insertUpdate(DocumentEvent event) {
    try {
      String text = doc.getText(0, doc.getLength());

      Pattern p = Pattern.compile(FOO);
      Matcher matcher = p.matcher(text);
      while (matcher.find()) {
        updateAttribute(matcher.start(), FOO.length(), Style.BOLD);
      }
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
  }

  /**
   * Update the string at the given position and length with the given style.
   * The update happens on a separate AWT thread, to avoid mutations of the
   * Document model while the event is processing.
   */
  private void updateAttribute(int pos, int len, Style style) {
    SwingUtilities.invokeLater(() -> {
      doc.setCharacterAttributes(pos, len, style.get(), true);
    });
  }

  @Override
  public void removeUpdate(DocumentEvent e) {}

  @Override
  public void changedUpdate(DocumentEvent e) {}
}
Older posts