Friday, 29 June 2012

HTML5 Video Pseudostreaming with Java 7

Adapted from the byte range request servlet code from the The BalusC Code, here is a Java 7 pseudostreaming servlet for playing video using the HTML5 video tag.

The servlet takes the name of a video file to play as a request parameter and the byte range of the video to stream in the Range header field.

PseudostreamingServlet.java

package org.adrianwalker.pseudostreaming;

import java.io.IOException;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.nio.file.StandardOpenOption.READ;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public final class PseudostreamingServlet extends HttpServlet {

  private static final int BUFFER_LENGTH = 1024 * 16;
  private static final long EXPIRE_TIME = 1000 * 60 * 60 * 24;
  private static final Pattern RANGE_PATTERN = Pattern.compile("bytes=(?<start>\\d*)-(?<end>\\d*)");
  private String videoPath;

  @Override
  public void init() throws ServletException {
    videoPath = getInitParameter("videoPath");
  }

  @Override
  protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
    processRequest(request, response);
  }

  private void processRequest(final HttpServletRequest request, final HttpServletResponse response) throws IOException {

    String videoFilename = URLDecoder.decode(request.getParameter("video"), "UTF-8");
    Path video = Paths.get(videoPath, videoFilename);

    int length = (int) Files.size(video);
    int start = 0;
    int end = length - 1;

    String range = request.getHeader("Range");
    Matcher matcher = RANGE_PATTERN.matcher(range);
    
    if (matcher.matches()) {
      String startGroup = matcher.group("start");
      start = startGroup.isEmpty() ? start : Integer.valueOf(startGroup);
      start = start < 0 ? 0 : start;

      String endGroup = matcher.group("end");
      end = endGroup.isEmpty() ? end : Integer.valueOf(endGroup);
      end = end > length - 1 ? length - 1 : end;
    }

    int contentLength = end - start + 1;

    response.reset();
    response.setBufferSize(BUFFER_LENGTH);
    response.setHeader("Content-Disposition", String.format("inline;filename=\"%s\"", videoFilename));
    response.setHeader("Accept-Ranges", "bytes");
    response.setDateHeader("Last-Modified", Files.getLastModifiedTime(video).toMillis());
    response.setDateHeader("Expires", System.currentTimeMillis() + EXPIRE_TIME);
    response.setContentType(Files.probeContentType(video));
    response.setHeader("Content-Range", String.format("bytes %s-%s/%s", start, end, length));
    response.setHeader("Content-Length", String.format("%s", contentLength));
    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

    int bytesRead;
    int bytesLeft = contentLength;
    ByteBuffer buffer = ByteBuffer.allocate(BUFFER_LENGTH);

    try (SeekableByteChannel input = Files.newByteChannel(video, READ);
            OutputStream output = response.getOutputStream()) {

      input.position(start);

      while ((bytesRead = input.read(buffer)) != -1 && bytesLeft > 0) {
        buffer.clear();
        output.write(buffer.array(), 0, bytesLeft < bytesRead ? bytesLeft : bytesRead);
        bytesLeft -= bytesRead;
      }
    }
  }
}

The location of the videos on the file system is configurable in the web.xml via the videoPath parameter for the stream servlet.

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <servlet>
        <servlet-name>stream</servlet-name>
        <servlet-class>org.adrianwalker.pseudostreaming.PseudostreamingServlet</servlet-class>
        <init-param>
            <param-name>videoPath</param-name>
            <param-value>/tmp/videos</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>stream</servlet-name>
        <url-pattern>/stream</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
</web-app>

Some simple HTML5 uses the servlet as the source for the video tag. The name of the video files is provided by the video URL parameter.

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>HTML5 Video Pseudostreaming</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  </head>
  <body>
    <div>
      <video id="video" controls>  
        <source src="stream?video=video.webm" />  
      </video>
    </div>
  </body>
</html>

You're going to need a video to play, so download a trailer or something:

http://trailers.apple.com/movies/sony_pictures/theamazingspiderman/amazingspiderman-tlr2b-USA_h480p.mov

The QuickTime trailer video file isn't supported by my target browser, so I need to convert it to another format, for this example WebM. ffmpeg can be used for the conversion:

ffmpeg -i amazingspiderman-tlr2b-USA_h480p.mov /tmp/videos/video.webm

Source Code

Usage

Run the project with 'mvn clean install jetty:run-war' and point your brower at http://localhost:8080/pseudostreaming.

C Legs

Every now and again I vow to learn C, and now seems like a good time to have another go.

My plan for learning C is always the same:

  1. Read K&R
  2. ?
  3. Profit

Where step 2 might as well be 'Don't use C and forget everything in K&R'. The problem is that I'm not a systems programmer, so everything I want to program is just easier to do in Java.

This time round I'm having a go with Learn C The Hard Way. It takes a way more modern and pragmatic approach than K&R, and introduces tools like make and valgrind early on.

I'm going to try and not fall back on more familiar languages, and see if I can knock out interesting things in C where appropriate, and blog about them to help me learn.

First off a simple Sudoku solver:

main.c

#include <stdio.h>
#include <stdlib.h>

#define _ 0
#define GRID_SIZE 9
#define SQUARE_SIZE 3
#define FALSE 0
#define TRUE !FALSE

void print(int grid[GRID_SIZE][GRID_SIZE]);
int complete(int grid[GRID_SIZE][GRID_SIZE]);
int contains(int array[GRID_SIZE], int value);
void row(int grid[GRID_SIZE][GRID_SIZE], int rowIndex, int row[GRID_SIZE]);
void column(int grid[GRID_SIZE][GRID_SIZE], int columnIndex, int column[GRID_SIZE]);
void square(int grid[GRID_SIZE][GRID_SIZE], int rowIndex, int columnIndex, int square[GRID_SIZE]);

int main(int argc, char** argv) {

  int grid[GRID_SIZE][GRID_SIZE] ={
    {_, 3, 6, _, 5, _, _, 8, 7},
    {9, _, 1, 7, 8, _, _, 6, 2},
    {_, _, 7, _, _, _, 1, 4, _},
    {_, _, 5, _, 9, _, _, _, 3},
    {_, _, _, _, 1, _, _, _, _},
    {8, _, _, _, 7, _, 6, _, _},
    {_, 2, 9, _, _, _, 5, _, _},
    {5, 7, _, _, 2, 9, 8, _, 6},
    {6, 1, _, _, 3, _, 2, 9, _}
  };

  int rowIndex;
  int columnIndex;
  int value;
  int possibleValue;
  int possibleValueCount;
  int inRow;
  int inColumn;
  int inSquare;
  int tmp[GRID_SIZE];

  while (!complete(grid)) {
    for (rowIndex = 0; rowIndex < GRID_SIZE; rowIndex++) {
      for (columnIndex = 0; columnIndex < GRID_SIZE; columnIndex++) {

        if (grid[rowIndex][columnIndex] != _) {
          continue;
        }

        possibleValue = _;
        possibleValueCount = 0;

        for (value = 1; value <= GRID_SIZE; value++) {

          row(grid, rowIndex, tmp);
          inRow = contains(tmp, value);
          if (inRow) {
            continue;
          }

          column(grid, columnIndex, tmp);
          inColumn = contains(tmp, value);
          if (inColumn) {
            continue;
          }

          square(grid, rowIndex, columnIndex, tmp);
          inSquare = contains(tmp, value);
          if (inSquare) {
            continue;
          }

          possibleValue = value;
          possibleValueCount++;
        }

        if (possibleValueCount == 1) {
          grid[rowIndex][columnIndex] = possibleValue;
        }
      }
    }
  }

  print(grid);

  return (EXIT_SUCCESS);
}

void print(int grid[GRID_SIZE][GRID_SIZE]) {

  int rowIndex;
  int columnIndex;

  for (rowIndex = 0; rowIndex < GRID_SIZE; rowIndex++) {
    for (columnIndex = 0; columnIndex < GRID_SIZE; columnIndex++) {
      if (columnIndex != 0) {
        printf(", ");
      }
      printf("%d", grid[rowIndex][columnIndex]);
    }
    printf("\n");
  }
}

int complete(int grid[GRID_SIZE][GRID_SIZE]) {
  int tmp[GRID_SIZE];
  int rowIndex;
  int inRow;

  for (rowIndex = 0; rowIndex < GRID_SIZE; rowIndex++) {
    row(grid, rowIndex, tmp);
    inRow = contains(tmp, _);
    if (inRow) {
      return FALSE;
    }
  }
  return TRUE;
}

int contains(int array[GRID_SIZE], int value) {

  int i;

  for (i = 0; i < GRID_SIZE; i++) {
    if (array[i] == value) {
      return TRUE;
    }
  }
  return FALSE;
}

void row(int grid[GRID_SIZE][GRID_SIZE], int rowIndex, int row[GRID_SIZE]) {

  int columnIndex;

  for (columnIndex = 0; columnIndex < GRID_SIZE; columnIndex++) {
    row[columnIndex] = grid[rowIndex][columnIndex];
  }
}

void column(int grid[GRID_SIZE][GRID_SIZE], int columnIndex, int column[GRID_SIZE]) {

  int rowIndex;

  for (rowIndex = 0; rowIndex < GRID_SIZE; rowIndex++) {
    column[rowIndex] = grid[rowIndex][columnIndex];
  }
}

void square(int grid[GRID_SIZE][GRID_SIZE], int rowIndex, int columnIndex, int square[GRID_SIZE]) {

  int i;
  int j;

  int x = SQUARE_SIZE * (rowIndex / SQUARE_SIZE);
  int y = SQUARE_SIZE * (columnIndex / SQUARE_SIZE);

  for (i = 0; i < SQUARE_SIZE; i++) {
    for (j = 0; j < SQUARE_SIZE; j++) {
      square[(i * SQUARE_SIZE) + j] = grid[x + i][y + j];
    }
  }
}

Saturday, 28 April 2012

Filtered JTree

The solutions (1, 2) out there for filtering a JTree's nodes weren't suitable for me, so I rolled by own FilteredTreeModel class which wraps a JTree's underlying TreeModel and applies a string filter to the node names.

The tree model recurses over the tree nodes, from the root to the leaf nodes, checking if the nodes toString() value contains the filter value:

FilteredTreeModel.java

package org.adrianwalker.filteredjtree;

import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

public final class FilteredTreeModel implements TreeModel {

  private TreeModel treeModel;
  private String filter;

  public FilteredTreeModel(final TreeModel treeModel) {
    this.treeModel = treeModel;
    this.filter = "";
  }

  public TreeModel getTreeModel() {
    return treeModel;
  }

  public void setFilter(final String filter) {
    this.filter = filter;
  }

  private boolean recursiveMatch(final Object node, final String filter) {

    boolean matches = node.toString().contains(filter);

    int childCount = treeModel.getChildCount(node);
    for (int i = 0; i < childCount; i++) {
      Object child = treeModel.getChild(node, i);
      matches |= recursiveMatch(child, filter);
    }
    
    return matches;
  }

  @Override
  public Object getRoot() {
    return treeModel.getRoot();
  }

  @Override
  public Object getChild(final Object parent, final int index) {
    int count = 0;
    int childCount = treeModel.getChildCount(parent);
    for (int i = 0; i < childCount; i++) {
      Object child = treeModel.getChild(parent, i);
      if (recursiveMatch(child, filter)) {
        if (count == index) {
          return child;
        }
        count++;
      }
    }
    return null;
  }

  @Override
  public int getChildCount(final Object parent) {
    int count = 0;
    int childCount = treeModel.getChildCount(parent);
    for (int i = 0; i < childCount; i++) {
      Object child = treeModel.getChild(parent, i);
      if (recursiveMatch(child, filter)) {
        count++;
      }
    }
    return count;
  }

  @Override
  public boolean isLeaf(final Object node) {
    return treeModel.isLeaf(node);
  }

  @Override
  public void valueForPathChanged(final TreePath path, final Object newValue) {
    treeModel.valueForPathChanged(path, newValue);
  }

  @Override
  public int getIndexOfChild(final Object parent, final Object childToFind) {
    int childCount = treeModel.getChildCount(parent);
    for (int i = 0; i < childCount; i++) {
      Object child = treeModel.getChild(parent, i);
      if (recursiveMatch(child, filter)) {
        if (childToFind.equals(child)) {
          return i;
        }
      }
    }
    return -1;
  }

  @Override
  public void addTreeModelListener(final TreeModelListener l) {
    treeModel.addTreeModelListener(l);
  }

  @Override
  public void removeTreeModelListener(final TreeModelListener l) {
    treeModel.removeTreeModelListener(l);
  }
}

Example Application

JTree before and after filtering

Below is a simple example of how the FilteredTreeModel can be used. The tree is filtered using the value of the text field as characters are typed.

FilteredJTreeExample.java

package org.adrianwalker.filteredjtree;

import java.awt.Container;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;

public final class FilteredJTreeExample {

  public static void main(final String[] args) {
    javax.swing.SwingUtilities.invokeLater(new Runnable() {

      @Override
      public void run() {
        createAndShowGUI();
      }
    });
  }

  private static void createAndShowGUI() {
    JFrame frame = new JFrame("Filtered JTree Demo");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    addComponentsToPane(frame.getContentPane());

    frame.pack();
    frame.setVisible(true);
  }

  private static void addComponentsToPane(final Container pane) {
    pane.setLayout(new GridBagLayout());

    JTree tree = createTree(pane);
    JTextField filter = createFilterField(pane);

    filter.getDocument().addDocumentListener(createDocumentListener(tree, filter));
  }

  private static JTree createTree(final Container pane) {
    DefaultMutableTreeNode root = new DefaultMutableTreeNode("JTree");
    FilteredTreeModel model = new FilteredTreeModel(new DefaultTreeModel(root));
    JTree tree = new JTree(model);
    JScrollPane scrollPane = new JScrollPane(tree);
    GridBagConstraints c = new GridBagConstraints();
    c.weightx = 1;
    c.weighty = 1;
    c.fill = GridBagConstraints.BOTH;
    c.gridx = 0;
    c.gridy = 1;
    pane.add(scrollPane, c);
    createTreeNodes(root);
    expandTree(tree);

    return tree;
  }

  private static JTextField createFilterField(final Container pane) {
    JTextField filter = new JTextField();
    GridBagConstraints c = new GridBagConstraints();
    c.weightx = 0;
    c.weighty = 0;
    c.fill = GridBagConstraints.HORIZONTAL;
    c.gridx = 0;
    c.gridy = 0;
    pane.add(filter, c);

    return filter;
  }
  
  private static DocumentListener createDocumentListener(final JTree tree, final JTextField filter) {
    return new DocumentListener() {

      @Override
      public void insertUpdate(final DocumentEvent e) {
        applyFilter();
      }

      @Override
      public void removeUpdate(final DocumentEvent e) {
        applyFilter();
      }

      @Override
      public void changedUpdate(final DocumentEvent e) {
        applyFilter();
      }

      public void applyFilter() {
        FilteredTreeModel filteredModel = (FilteredTreeModel) tree.getModel();
        filteredModel.setFilter(filter.getText());
        
        DefaultTreeModel treeModel = (DefaultTreeModel) filteredModel.getTreeModel();
        treeModel.reload();
        
        expandTree(tree);
      }
    };
  }

  private static void expandTree(final JTree tree) {
    for (int i = 0; i < tree.getRowCount(); i++) {
      tree.expandRow(i);
    }
  }

  private static void createTreeNodes(final DefaultMutableTreeNode node) {
    DefaultMutableTreeNode ab = new DefaultMutableTreeNode("ab");
    DefaultMutableTreeNode cd = new DefaultMutableTreeNode("cd");
    DefaultMutableTreeNode ef = new DefaultMutableTreeNode("ef");
    DefaultMutableTreeNode gh = new DefaultMutableTreeNode("gh");
    DefaultMutableTreeNode ij = new DefaultMutableTreeNode("ij");
    DefaultMutableTreeNode kl = new DefaultMutableTreeNode("kl");
    DefaultMutableTreeNode mn = new DefaultMutableTreeNode("mn");
    DefaultMutableTreeNode op = new DefaultMutableTreeNode("op");
    DefaultMutableTreeNode qr = new DefaultMutableTreeNode("qr");
    DefaultMutableTreeNode st = new DefaultMutableTreeNode("st");
    DefaultMutableTreeNode uv = new DefaultMutableTreeNode("uv");
    DefaultMutableTreeNode wx = new DefaultMutableTreeNode("wx");
    DefaultMutableTreeNode yz = new DefaultMutableTreeNode("yz");

    node.add(ab);
    node.add(cd);
    ab.add(ef);
    ab.add(gh);
    cd.add(ij);
    cd.add(kl);
    ef.add(mn);
    ef.add(op);
    gh.add(qr);
    gh.add(st);
    ij.add(uv);
    ij.add(wx);

    node.add(yz);
  }
}

Source Code

Usage

Build and install the Filtered JTree project, using 'mvn clean install'.

Run the FilteredJTreeExample class, and enter characters in the filter text field.

Monday, 5 March 2012

Java 7 New Features Cookbook Review

Java 7 is the latest major release of the Java Language from Oracle. This release contains a number of language enhancements and new APIs, including better exception handling, new threading mechanisms and additions to the core libraries.

Packt Publishing requested that I review one of their titles about Java 7: Java 7 New Features Cookbook by Richard M. Reese and Jennifer L. Reese, available to buy from Packt's website.

Java 7 New Features Cookbook is an OK, but sometimes inconsistent book. I found parts of it very informative and other parts frustrating, with errors here and there in the example code – overall I'd give it an average score. 

The information in the book is very densely packed, but often repetitive and sometimes very slow reading. The information is presented in recipes - how-to examples focusing on a specific Java 7 feature, put together to form a cookbook.

Each recipe is presented in a formulaic way, with information under the sub-headings: 'Getting ready', 'How to do it...', 'How it works...' and 'There's more'. The code examples sufficiently cover the new feature they are trying to demonstrate, and are quite self contained, so you don't need to read the whole chapter to quickly get going with a new Java 7 feature. The 'There's more' sections expand on the basics, fleshing out the examples and include some good tips.

The book covers the full range of Java 7 new features, without going over old ground with respect to previous versions of Java. The book is definitely aimed at developers who are familiar with writing programs in Java 5 and beyond, and not for developers new to Java and OOP. A previous knowledge of Java file I/O, concurrency and a bit of SQL is required to get the most from this book.

This book was difficult to read from cover to cover, and I wouldn't suggest reading it that way. I would recommend consulting its relevant recipes before implementing new code.

A Java 7 JDK installation and an editor are requirements to reproduce the examples in the book. NetBeans 7 is stated as the IDE used to develop the code, but Eclipse or any other IDE or text editor could be used.

A note on the code examples in the book: the immediate thing you notice about the code examples is that they are badly formatted. This coupled with occasional errors in the code and new language syntax, such as try-with-resources, makes for frustrating debugging. The poor code layout is a problem throughout the book and spoilt my enjoyment of it slightly.

Chapter 1 covers new language features from Project Coin. These are small language enhancements designed to make code more readable, more useful and reduce verbosity. The useful new features to take away from this chapter are the new try-with-resources exception handling code, the use of the new AutoClosable interface, catching multiple exception types and using the diamond operator. 

This chapter is a good introduction to Java 7 and I think most developers will want to get to grips with all the recipes.

Chapter 2 introduces the new Paths and FileSystem objects, which are used extensively in subsequent chapters. The new classes provide utility methods to simplify the manipulation and comparison of file paths, helping to reducing some of the bespoke code you may have had to write in previous Java versions. The most important feature introduced in this chapter is the managing of symbolic links, but you will need to be running an operating system that supports them.

Chapter 3 deals with file and directory attributes. The first recipe demonstrates a useful new method for determining a files MIME type, something which has required 3rd party libraries in the past. The chapter goes on to give examples of managing file attributes, including support for DOS and POSIX file systems, very useful for managing Linux file permissions, which wasn't directly possible before.

Chapter 4 builds on the previous chapters and uses the new Files object to create, copy, move and delete files and directories with much more control, and much less code than was previously possible. Useful new features presented are temporary file and directory management and creating symbolic and hard links.

Chapter 5 has loads of useful information for interacting with file systems. Recipes include: accessing root file store information, processing directory contents with DirectoryStream, file filtering, globbing patterns, file event monitoring and more! Lastly in this chapter, the ZIP filesystem provider recipe demonstrates how to treat a ZIP or JAR file as though they were file systems.

Java is often criticised for the amount of code needed to perform simple file reading and writing operations. Chapter 6 shows how simple these operations can now be with Java 7, including new APIs for working with buffered I/O.  The new SeekableByteChannel class for random access I/O, and the new AsynchronousServerSocketChannel class for building client-server applications are recipes also worth visiting.

Thankfully, after half the book, this is the last chapter that deals with paths, files, files systems, file permissions and file I/O.

Chapter 7 showcases additions to swing which can improve the look of your GUI interfaces. Some of the cool looking new features include window opacity, varying gradient window translucency, and custom window shapes. Less exciting, but definitely useful, border type and print dialog box control recipes are also worth taking a look at in this chapter.

Chapter 9 contains a mix of new features, but has some really useful recipes. Firstly the RowSetFactory example shows how to really reduce the amount of JDBC boiler plate code needed to connect to a SQL database, execute queries and iterate over the returned data.

The next recipe demonstrates further database enhancements for reading database schema and pseudo-column information.

Another recipe worth a look include secure client-server communication using the ExtendedSSLSession interface, making use of the keytool, a serverside keystore and a client side trust store.

A new nested class, ProcessBuilder.Redirect, has been introduced to provide redirecting of input and output of external processes – useful for interacting with other executables and scripts.

I think chapter 9 is another chapter all developers will want to read.

Chapter 10's recipes deal with further concurrency enhancements to the Java language, which have been getting more and more powerful since Java 5. The new fork/join framework allows a task to be split into smaller parallelisable chunks, and then combining the results.

Other useful recipes in this chapter demonstrate using the new ThreadLocalRandom class for more efficient random number generation in a threaded environment. And the ConcurrentLinkedDeque example shows how concurrent threads can safely access the same collection data.

Chapter 11 contains bit and bobs which don't fit in other chapters. The Handling null references recipe introduces the new Objects class which provides utility methods for object comparison, null checking and hashing.

The JavaBean enhancements recipe shows how to use the new Expression class to programatically create and execute Java statements. Also the Introspector and BeanInfo class provide a new way of determining a classes' methods and fields without using the Reflection API.

Overall, I think this book is good in places, but repetitive and difficult to work though in others. About half the book is devoted to paths, files, filesystems and I/O, which I'm sure could be slimmed down a bit. I'd recommend using the book as a reference to consult rather than a book to pick up and read cover to cover.  Java 7 New Features Cookbook is definitely worth a read, and I will be using the things I've learned from it in future blog posts.

Downloads From Packt

Friday, 2 December 2011

Java Multiline String

Here is an implementation of multiline string literals in Java, using Javadoc comments, an annotation and an annotation processor.

This method works by annotating a String field with a @Multiline annotation, placing the fields initialisation value in a Javadoc comment, then using an annotation processor to set the fields value to the contents of the Javadoc comment at compile time.

First off, the @Multiline annotation.

Multiline.java

package org.adrianwalker.multilinestring;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Multiline {
}

Next, the annotation processor to insert the value of the Javadoc comment into the String field.

MultilineProcessor.java

package org.adrianwalker.multilinestring;

import com.sun.tools.javac.model.JavacElements;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

@SupportedAnnotationTypes({"org.adrianwalker.multilinestring.Multiline"})
public final class MultilineProcessor extends AbstractProcessor {

  private JavacElements elementUtils;
  private TreeMaker maker;

  @Override
  public void init(final ProcessingEnvironment procEnv) {
    
    super.init(procEnv);

    JavacProcessingEnvironment javacProcessingEnv = (JavacProcessingEnvironment) procEnv;
    this.elementUtils = javacProcessingEnv.getElementUtils();
    this.maker = TreeMaker.instance(javacProcessingEnv.getContext());
  }

  @Override
  public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {

    Set<? extends Element> fields = roundEnv.getElementsAnnotatedWith(Multiline.class);
    for (Element field : fields) {
      String docComment = elementUtils.getDocComment(field);
      if (null != docComment) {
        JCTree.JCVariableDecl fieldNode = (JCTree.JCVariableDecl) elementUtils.getTree(field);
        fieldNode.init = maker.Literal(docComment);
      }
    }

    return true;
  }
}

And finally, a class which demonstrates the usage of the annotation.

MultilineStringUsage.java

package org.adrianwalker.multilinestring;

public final class MultilineStringUsage {

  /**
  <html>
    <head/>
    <body>
      <p>
        Hello<br/>
        Multiline<br/>
        World<br/>
      </p>
    </body>
  </html>
  */
  @Multiline
  private static String html;
   
  public static void main(final String[] args) {
    System.out.println(html);
  }
}

Maven

Remember to include an <annotationProcessor> element when compiling classes which use @Multiline in the compiler plugin configuration when using Maven.

pom.xml

...
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>2.3.2</version>
  <configuration>
    <source>1.6</source>
    <target>1.6</target>
    <annotationProcessors>
      <annotationProcessor>
        org.adrianwalker.multilinestring.MultilineProcessor
      </annotationProcessor>
    </annotationProcessors>
  </configuration>
</plugin>
...

Eclipse

To use in a non Maven Java project with Eclipse, try this: https://github.com/benelog/multiline/wiki/Non-Maven-Java-project-with-Eclipse

Source Code

Usage

Build and install the Multiline String project which contains the @Multiline annotation and annotation processor, using 'mvn clean install'.

Build and install the Multiline String Usage project which contains an example of how to use the @Multiline annotation, using 'mvn clean install'.

Run the MultilineStringUsage class to output the multiline string.

Monday, 11 July 2011

HTTP Proxy

Sometimes you just want a simple HTTP proxy without the hastle.

HttpProxy.java

package org.adrianwalker.httpproxy;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class HttpProxy {

  public static final int MAX_THREADS = 2;
  public static final int LOCAL_PORT = 9090;
  public static final String REMOTE_HOST = "localhost";
  public static final int REMOTE_PORT = 8080;

  public static void main(final String[] args) throws Exception {

    ServerSocket server = new ServerSocket(LOCAL_PORT);

    Socket remoteSocket = new Socket(REMOTE_HOST, REMOTE_PORT);
    while (true) {
      Socket localSocket = server.accept();

      ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS);
      executor.submit(new SocketStreamCopy(remoteSocket.getInputStream(), localSocket.getOutputStream()));
      executor.submit(new SocketStreamCopy(localSocket.getInputStream(), remoteSocket.getOutputStream()));
    }
  }

  public static final class SocketStreamCopy implements Callable<Void> {

    public static final int BUFFER_SIZE = 1024;
    private final BufferedInputStream in;
    private final BufferedOutputStream out;

    SocketStreamCopy(final InputStream in, final OutputStream out) {

      this.in = new BufferedInputStream(in);
      this.out = new BufferedOutputStream(out);
    }

    @Override
    public Void call() throws Exception {
      byte[] b = new byte[BUFFER_SIZE];
      int n;
      try {
        while ((n = in.read(b)) > 0) {
          out.write(b, 0, n);
          out.flush();

          System.out.write(b, 0, n);
          System.out.flush();
        }
      } finally {
        in.close();
        out.close();
      }

      return Void.TYPE.newInstance();
    }
  }
}

jboard - yet another imageboard

This time I've kept it super simple - no SQL databases, no EJB containers, no extra features, just a simple imageboard web app which writes to the file system.

Image resizing is handled by ImageMagick which needs to be installed and configured before you run jboard.

Source Code

Usage

Set the location of ImageMagick's convert binary in: src\main\resources\BoardConfiguration.properties

Run the project with 'mvn clean install tomcat:run-war' and point your brower at http://localhost:8080/jboard.