Saturday, October 30, 2010

Formatting XML quickly

There are many times when I have come across badly formatted XML and need to prettify it instantly in order to aid readability or simply to paste into another document, like a blog post. There are plugins available (for example, Textpad's XMLTidy) which tidy up xml, but they involve pasting XML into a file and running a macro to clean it up, which can be slow, especially if you don't have an editor open.

So, I decided to write my own Java utility to format XML instantly. All you have to do is select some XML text, hit CTRL+C to save it to your clipboard, hit CTRL+ALT+F to invoke my formatting utility and finally hit CTRL+V to paste the nicely formatted XML somewhere else. This has made working with XML so much easier!

This is how you can set it up too:

The Java Source Code
Save the following source code to a file called XMLTidy.java and compile it using javac.

import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.sun.org.apache.xml.internal.serialize.OutputFormat;
import com.sun.org.apache.xml.internal.serialize.XMLSerializer;

/**
 * A useful utility for formatting xml.
 * Retrieves xml text from the system clipboard, formats it
 * and resaves it to the clipboard.
 */
public class XMLTidy {


  /**
   * Formats the specified xml string
   *
   * @param src the xml text to format
   * @return formatted xml
   * @throws ParserConfigurationException
   * @throws SAXException
   * @throws IOException
   */
  private static String tidyXml(String src)
      throws ParserConfigurationException, SAXException, IOException {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    DocumentBuilder db = dbf.newDocumentBuilder();
    InputSource is = new InputSource(new StringReader(src));
    Document document = db.parse(is);
    OutputFormat format = new OutputFormat(document);
    format.setLineWidth(65);
    format.setIndenting(true);
    format.setIndent(2);
    Writer out = new StringWriter();
    XMLSerializer serializer = new XMLSerializer(out, format);
    serializer.serialize(document);
    return out.toString();
  }

  /**
   * @return the text in the clipboard
   */
  private static String getClipboard() {
    Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard()
        .getContents(null);
    try {
      if (t != null &&
          t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
        String text = (String) t.getTransferData(DataFlavor.stringFlavor);
        return text;
      }
    } catch (UnsupportedFlavorException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return "";
  }

  /**
   * @param str the text to set in the clipboard
   */
  private static void setClipboard(String str) {
    StringSelection ss = new StringSelection(str);
    Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
  }


  /**
   * Formats the xml supplied as an argument.
   * If no arguments are specified, formats the xml
   * in the clipboard.
   * @param args
   * @throws Exception
   */
  public static void main(String[] args) throws Exception {
    String in = args.length > 0 ? args[0] : getClipboard();
    if (in != null) {
      in = in.trim();
      if (in.charAt(0) == '<') {
        setClipboard(tidyXml(in));
      }
    }
  }
}
The Launcher Script
Create a bat file to launch the java program:
@echo off
%JAVA_HOME%\bin\java -cp \path\to\XMLTidy\classes XMLTidy %1
The Keyboard Shortcut
Finally create a keyboard shortcut to the launcher script as follows:
  • First, create a shortcut to the launcher script, by right-clicking the bat file and selecting "Create a shortcut".
  • Right-click the shortcut file and select "Properties".
  • Enter a "Shortcut key" on the Shortcut tab. For example, the shortcut key I use is CTRL+ALT+F
Try it out!
  • Select some badly formatted XML and copy it (using CTRL+C, for example).
  • Invoke XMLTidy by using the keyboard shortcut, CTRL+ALT+F.
  • Paste the XML (using CTRL+V, for example). The XML will be nicely formatted!

Sunday, October 17, 2010

Redirect stdout to logger

Recently, whilst using an external jar in my project, I found that it was doing a lot of logging to stdout using System.out.print statements. Even exceptions were being printed using e.printStackTrace(). We all know that this is bad practice and we should always consider using a logger instead. It was really annoying, seeing these messages come up on my console. Since my application already uses SLF4J for logging, I decided to redirect stdout and stderr to my logger. The following code snippet shows how:
private static final Logger LOGGER = Logger.getLogger(MyClass.class);

/**
 * Redirects stdout and stderr to logger
 */
public static void redirect(){
 System.setOut(new PrintStream(System.out){
  public void print(String s){
   LOGGER.info(s);
  }
 });
 System.setErr(new PrintStream(System.err){
  public void print(String s){
   LOGGER.error(s);
  }
 });
}

Saturday, October 16, 2010

Logback: Change root logger level programmatically

A couple of years ago, I wrote about how it is possible to change log4j logging levels using JMX. I'm now using logback, which is intended to be the successor of log4j and provides several advantages over it.

The following code snippet can be used to change the root logger's logging level in logback:

import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;

Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.setLevel(Level.DEBUG); //change to debug

Sunday, October 10, 2010

Creating tables with PDFBox

Apache PDFBox is a useful Java library for working with PDF documents. It allows you to create new PDF documents and extract data from existing documents.

However, the library doesn't provide an API for creating tables within PDF documents. So I wrote my own method which uses basic operations like drawLine to draw the table cells and drawString to fill in the content. The code is shown below. Note, that this code could be improved to handle longer strings of text properly by wrapping text within cells, which it doesn't do at present.

/**
 * @param page
 * @param contentStream
 * @param y the y-coordinate of the first row
 * @param margin the padding on left and right of table
 * @param content a 2d array containing the table data
 * @throws IOException
 */
public static void drawTable(PDPage page, PDPageContentStream contentStream,
                            float y, float margin,
                            String[][] content) throws IOException {
    final int rows = content.length;
    final int cols = content[0].length;
    final float rowHeight = 20f;
    final float tableWidth = page.findMediaBox().getWidth()-(2*margin);
    final float tableHeight = rowHeight * rows;
    final float colWidth = tableWidth/(float)cols;
    final float cellMargin=5f;

    //draw the rows
    float nexty = y ;
    for (int i = 0; i <= rows; i++) {
        contentStream.drawLine(margin,nexty,margin+tableWidth,nexty);
        nexty-= rowHeight;
    }

    //draw the columns
    float nextx = margin;
    for (int i = 0; i <= cols; i++) {
        contentStream.drawLine(nextx,y,nextx,y-tableHeight);
        nextx += colWidth;
    }

    //now add the text
    contentStream.setFont(PDType1Font.HELVETICA_BOLD,12);

    float textx = margin+cellMargin;
    float texty = y-15;
    for(int i = 0; i < content.length; i++){
        for(int j = 0 ; j < content[i].length; j++){
            String text = content[i][j];
            contentStream.beginText();
            contentStream.moveTextPositionByAmount(textx,texty);
            contentStream.drawString(text);
            contentStream.endText();
            textx += colWidth;
        }
        texty-=rowHeight;
        textx = margin+cellMargin;
    }
}

public static void main(String[] args){
    PDDocument doc = new PDDocument();
    PDPage page = new PDPage();
    doc.addPage( page );

    PDPageContentStream contentStream =
                    new PDPageContentStream(doc, page);

    String[][] content = {{"a","b", "1"},
                          {"c","d", "2"},
                          {"e","f", "3"},
                          {"g","h", "4"},
                          {"i","j", "5"}} ;

    drawTable(page, contentStream, 700, 100, content);
    contentStream.close();
    doc.save("test.pdf" );
    }