Monday, 20 June 2011

Java Sleep Sort - For The Lulz

Strictly for the lulz, here is an implementation of Sleep Sort, first seen on 4chan.

SleepSort.java

package sleepsort;

import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class SleepSort {

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

    int[] numbers = {90, 80, 70, 60, 50, 40, 30, 20, 10};
    int[] sortedNumbers = sort(numbers);
    System.out.println(Arrays.toString(sortedNumbers));
  }

  private static int[] sort(final int[] numbers) throws InterruptedException, ExecutionException {

    ExecutorService executor = Executors.newFixedThreadPool(numbers.length);
    ExecutorCompletionService<Integer> ecs = new ExecutorCompletionService<Integer>(executor);
    for (int number : numbers) {
      ecs.submit(new SleepSortCallable((number)));
    }

    int[] sortedNumbers = new int[numbers.length];
    for (int i = 0; i < sortedNumbers.length; i++) {
      sortedNumbers[i] = ecs.take().get();
    }

    executor.shutdown();

    return sortedNumbers;
  }

  private static class SleepSortCallable implements Callable<Integer> {

    private final int number;

    public SleepSortCallable(final int number) {
      this.number = number;
    }

    @Override
    public Integer call() throws Exception {
      Thread.sleep(number);
      return number;
    }
  }
}

Tuesday, 8 March 2011

GWT File Upload With Event-Based Progress Bar

File upload with progress bar, and paged file downloads.

Adapting the event-based code from Chapter 9 of the excellent book Google Web Toolkit Applications, I coded up a file upload application with a progress bar, which is updated using server pseudo-push events.

The application also provides a download servlet, and layout using GWT UiBinder.

Full source for the webapp is available at the bottom of the page. Not all of the code is listed below, but some of the most important classes are explained here.

The UploadProgressService interface defines the server side functions available, the most important being getEvents(). This function will be called recursively on the client side to simulate a main loop.

UploadProgressService.java

package org.adrianwalker.gwt.uploadprogress.client;

import java.util.List;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
import org.adrianwalker.gwt.uploadprogress.common.dto.FileDto;
import org.adrianwalker.gwt.uploadprogress.common.event.Event;

@RemoteServiceRelativePath("uploadprogress")
public interface UploadProgressService extends RemoteService {

  void initialise();

  int countFiles();

  List<FileDto> readFiles(int page, int pageSize);

  List<Event> getEvents();
}

The only event in the application is UploadProgressChangeEvent. It is concerned with reporting changes in file upload percentage.

UploadProgressChangeEvent.java

package org.adrianwalker.gwt.uploadprogress.common.event;

import java.io.Serializable;

public final class UploadProgressChangeEvent implements Event, Serializable {

  private String filename;
  private Integer percentage;

  public UploadProgressChangeEvent() {
  }

  public String getFilename() {
    return filename;
  }

  public void setFilename(final String filename) {
    this.filename = filename;
  }

  public Integer getPercentage() {
    return percentage;
  }

  public void setPercentage(final Integer percentage) {
    this.percentage = percentage;
  }

  @Override
  public String toString() {
    return filename + " - " + percentage;
  }
}

The UploadProgressServlet implements the methods defined by UploadProgressService. The getEvents() method returns the events stored in the servlets session scope. If the event list is empty, the thread waits for 30 seconds. If the list contained events, they are retuned to the client, and then the event list is cleared.

UploadProgressServlet.java

package org.adrianwalker.gwt.uploadprogress.server;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpSession;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import org.adrianwalker.gwt.uploadprogress.client.UploadProgressService;
import org.adrianwalker.gwt.uploadprogress.common.dto.FileDto;
import org.adrianwalker.gwt.uploadprogress.common.event.Event;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class UploadProgressServlet extends RemoteServiceServlet implements UploadProgressService {

  private static final int EVENT_WAIT = 30 * 1000;
  private static final String PROPERTIES_FILE = "WEB-INF/classes/uploadprogress.properties";
  private static final Logger LOGGER = LoggerFactory.getLogger(UploadProgressServlet.class);
  private String uploadDirectory;

  @Override
  public void init() throws ServletException {
    Properties properties = new Properties();
    try {
      properties.load(getServletContext().getResourceAsStream(PROPERTIES_FILE));
    } catch (IOException ioe) {
      throw new ServletException(ioe);
    }

    uploadDirectory = properties.getProperty("upload.directory", "target");
  }

  @Override
  public void initialise() {
    getThreadLocalRequest().getSession(true);
  }

  @Override
  public List<FileDto> readFiles(final int page, final int pageSize) {

    File[] listFiles = readFiles(this.uploadDirectory);
    sortFiles(listFiles);

    int firstFile = pageSize * (page - 1);
    int lastFile = firstFile + pageSize;

    int fileCount = listFiles.length;
    if (fileCount < lastFile) {
      lastFile = fileCount;
    }

    if (firstFile < fileCount) {
      List<FileDto> files = new ArrayList<FileDto>();

      for (int i = firstFile; i < lastFile; i++) {

        File file = listFiles[i];
        FileDto fileDto = new FileDto();
        fileDto.setFilename(file.getName());
        fileDto.setDateUploaded(new Date(file.lastModified()));
        files.add(fileDto);
      }
      return files;
    } else {
      return Collections.EMPTY_LIST;
    }
  }

  @Override
  public List<Event> getEvents() {

    HttpSession session = getThreadLocalRequest().getSession();
    UploadProgress uploadProgress = UploadProgress.getUploadProgress(session);

    List<Event> events = null;
    if (null != uploadProgress) {
      if (uploadProgress.isEmpty()) {
        try {
          synchronized (uploadProgress) {
            LOGGER.debug("waiting...");
            uploadProgress.wait(EVENT_WAIT);
          }
        } catch (final InterruptedException ie) {
          LOGGER.debug("interrupted...");
        }
      }

      synchronized (uploadProgress) {
        events = uploadProgress.getEvents();
        uploadProgress.clear();
      }
    }

    return events;
  }

  @Override
  public int countFiles() {
    return readFiles(this.uploadDirectory).length;
  }

  private File[] readFiles(final String directory) {
    File uploadDirectory = new File(directory);
    return uploadDirectory.listFiles(new FileFilter() {

      @Override
      public boolean accept(final File file) {
        return null == file ? false : file.isFile();
      }
    });
  }

  private void sortFiles(final File[] listFiles) {
    Arrays.sort(listFiles, new Comparator<File>() {

      @Override
      public int compare(final File f1, final File f2) {
        return Long.valueOf(f2.lastModified()).compareTo(f1.lastModified());
      }
    });
  }
}

UploadProgress stores the list of events, and has a factory method for creating/retrieving an instance of its self in the session.

UploadProgress.java

package org.adrianwalker.gwt.uploadprogress.server;

import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpSession;
import org.adrianwalker.gwt.uploadprogress.common.event.Event;

public final class UploadProgress {

  private static final String SESSION_KEY = "uploadProgress";
  private List<Event> events = new ArrayList<Event>();

  private UploadProgress() {
  }

  public List<Event> getEvents() {

    return events;
  }

  public void add(final Event event) {
    events.add(event);
  }

  public void clear() {
    events = new ArrayList<Event>();
  }

  public boolean isEmpty() {
    return events.isEmpty();
  }

  public static UploadProgress getUploadProgress(final HttpSession session) {
    Object attribute = session.getAttribute(SESSION_KEY);
    if (null == attribute) {
      attribute = new UploadProgress();
      session.setAttribute(SESSION_KEY, attribute);
    }

    return null == attribute ? null : (UploadProgress) attribute;
  }
}

The UploadServlet uses Apache Commons FileUpload library to parse the multipart post request and stream the file to disk. An UploadProgressListener is added to a custom input stream, UploadProgressInputStream, which together report the status of the upload back to the client.

UploadServlet.java

package org.adrianwalker.gwt.uploadprogress.server;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Properties;
import javax.servlet.http.HttpServlet;

import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileItemIterator;
import org.apache.commons.fileupload.FileItemStream;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.util.Streams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class UploadServlet extends HttpServlet {

  private static final String PROPERTIES_FILE = "WEB-INF/classes/uploadprogress.properties";
  private static final Logger LOGGER = LoggerFactory.getLogger(UploadServlet.class);
  private static final String FILE_SEPERATOR = System.getProperty("file.separator");
  private String uploadDirectory;

  @Override
  public void init() throws ServletException {
    Properties properties = new Properties();
    try {
      properties.load(getServletContext().getResourceAsStream(PROPERTIES_FILE));
    } catch (IOException ioe) {
      throw new ServletException(ioe);
    }

    uploadDirectory = properties.getProperty("upload.directory", "target");
  }

  @Override
  protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
    try {
      uploadFile(request);
    } catch (FileUploadException fue) {
      throw new ServletException(fue);
    }
  }

  private void uploadFile(final HttpServletRequest request) throws FileUploadException, IOException {

    if (!ServletFileUpload.isMultipartContent(request)) {
      throw new FileUploadException("error multipart request not found");
    }

    FileItemFactory fileItemFactory = new DiskFileItemFactory();
    ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);

    FileItemIterator fileItemIterator = servletFileUpload.getItemIterator(request);

    HttpSession session = request.getSession();
    UploadProgress uploadProgress = UploadProgress.getUploadProgress(session);

    while (fileItemIterator.hasNext()) {
      FileItemStream fileItemStream = fileItemIterator.next();

      String filePath = fileItemStream.getName();
      String fileName = filePath.substring(filePath.lastIndexOf(FILE_SEPERATOR) + 1);

      UploadProgressListener uploadProgressListener = new UploadProgressListener(fileName, uploadProgress);

      UploadProgressInputStream inputStream = new UploadProgressInputStream(fileItemStream.openStream(), request.getContentLength());
      inputStream.addListener(uploadProgressListener);

      File file = new File(uploadDirectory, fileName);

      Streams.copy(inputStream, new FileOutputStream(file), true);

      LOGGER.info(String.format("uploaded file %s", file.getAbsolutePath()));
    }
  }
}

UploadProgressInputStream is a FilterInputStream which reports the number of bytes read to attached listeners.

UploadProgressInputStream.java

package org.adrianwalker.gwt.uploadprogress.server;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.fileupload.ProgressListener;

public final class UploadProgressInputStream extends FilterInputStream {

  private List<ProgressListener> listeners;
  private long bytesRead = 0;
  private long totalBytes = 0;

  public UploadProgressInputStream(final InputStream in, final long totalBytes) {
    super(in);

    this.totalBytes = totalBytes;

    listeners = new ArrayList<ProgressListener>();
  }

  public void addListener(final ProgressListener listener) {
    listeners.add(listener);
  }

  @Override
  public int read() throws IOException {
    int b = super.read();

    this.bytesRead++;

    updateListeners(bytesRead, totalBytes);

    return b;
  }

  @Override
  public int read(final byte b[]) throws IOException {
    return read(b, 0, b.length);
  }

  @Override
  public int read(final byte b[], final int off, final int len) throws IOException {
    int bytesRead = in.read(b, off, len);

    this.bytesRead = this.bytesRead + bytesRead;

    updateListeners(this.bytesRead, totalBytes);

    return bytesRead;
  }

  @Override
  public void close() throws IOException {
    super.close();

    updateListeners(totalBytes, totalBytes);
  }

  private void updateListeners(final long bytesRead, final long totalBytes) {

    for (ProgressListener listener : listeners) {

      listener.update(bytesRead, totalBytes, listeners.size());
    }
  }
}

An UploadProgressListener attached to the input stream, adds UploadProgressChangeEvent events to the event list and calls notifyAll() to reply to the waiting client.

UploadProgressListener.java

package org.adrianwalker.gwt.uploadprogress.server;

import org.adrianwalker.gwt.uploadprogress.common.event.UploadProgressChangeEvent;
import org.apache.commons.fileupload.ProgressListener;

public final class UploadProgressListener implements ProgressListener {

  private static final double COMPLETE_PERECENTAGE = 100d;
  private int percentage = -1;
  private String fileName;
  private UploadProgress uploadProgress;

  public UploadProgressListener(final String fileName, final UploadProgress uploadProgress) {
    this.fileName = fileName;
    this.uploadProgress = uploadProgress;
  }

  @Override
  public void update(final long bytesRead, final long totalBytes, final int items) {
    int percentage = (int) Math.floor(((double) bytesRead / (double) totalBytes) * COMPLETE_PERECENTAGE);

    if (this.percentage == percentage) {
      return;
    }

    this.percentage = percentage;

    UploadProgressChangeEvent event = new UploadProgressChangeEvent();
    event.setFilename(this.fileName);
    event.setPercentage(percentage);

    synchronized (this.uploadProgress) {
      this.uploadProgress.add(event);
      this.uploadProgress.notifyAll();
    }
  }
}

The client side ProgressController is responsible for calling the getEvents() method on the service servlet, and calling it again after a successful response; creating an event loop.

ProgressController.java

package org.adrianwalker.gwt.uploadprogress.client.controller;

import com.google.gwt.core.client.GWT;
import java.util.List;

import com.google.gwt.user.client.rpc.AsyncCallback;
import org.adrianwalker.gwt.uploadprogress.client.state.UploadProgressState;
import org.adrianwalker.gwt.uploadprogress.common.dto.FileDto;
import org.adrianwalker.gwt.uploadprogress.common.event.Event;
import org.adrianwalker.gwt.uploadprogress.common.event.UploadProgressChangeEvent;

public final class ProgressController extends AbstractController {

  public static final ProgressController INSTANCE = new ProgressController();

  private ProgressController() {
  }

  public void findFiles(final int page, final int pageSize) {
    SERVICE.readFiles(page, pageSize, new AsyncCallback<List<FileDto>>() {

      @Override
      public void onFailure(final Throwable t) {
        GWT.log("error find files", t);
      }

      @Override
      public void onSuccess(final List<FileDto> files) {
        UploadProgressState.INSTANCE.setFiles(files);
      }
    });
  }

  private void getEvents() {

    SERVICE.getEvents(new AsyncCallback<List<Event>>() {

      @Override
      public void onFailure(final Throwable t) {
        GWT.log("error get events", t);
      }

      @Override
      public void onSuccess(final List<Event> events) {

        for (Event event : events) {
          handleEvent(event);
        }
        SERVICE.getEvents(this);
      }

      private void handleEvent(final Event event) {

        if (event instanceof UploadProgressChangeEvent) {
          UploadProgressChangeEvent uploadPercentChangeEvent = (UploadProgressChangeEvent) event;
          String filename = uploadPercentChangeEvent.getFilename();
          Integer percentage = uploadPercentChangeEvent.getPercentage();

          UploadProgressState.INSTANCE.setUploadProgress(filename, percentage);
        }
      }
    });
  }

  public void initialise() {
    SERVICE.initialise(new AsyncCallback<Void>() {

      @Override
      public void onFailure(final Throwable t) {
        GWT.log("error initialise", t);
      }

      @Override
      public void onSuccess(final Void result) {
        getEvents();
      }
    });
  }

  public void countFiles() {
    SERVICE.countFiles(new AsyncCallback<Integer>() {

      @Override
      public void onFailure(final Throwable t) {
        GWT.log("error count files", t);
      }

      @Override
      public void onSuccess(final Integer result) {
        int pageSize = UploadProgressState.INSTANCE.getPageSize();
        int pages = (int) Math.ceil((double) result / (double) pageSize);
        UploadProgressState.INSTANCE.setPages(pages);
      }
    });
  }
}

Changes to the files upload percentage are stored in UploadProgressState which can have listerners attached to be notified of updates to its fields.

UploadProgressState.java

package org.adrianwalker.gwt.uploadprogress.client.state;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.adrianwalker.gwt.uploadprogress.common.dto.FileDto;

public final class UploadProgressState extends PageableState {

  public static final UploadProgressState INSTANCE = new UploadProgressState();
  private Map<String, Integer> uploadProgress;
  private List<FileDto> files;

  private UploadProgressState() {
    uploadProgress = new HashMap<String, Integer>();
  }

  public List<FileDto> getFiles() {
    return files;
  }

  public void setFiles(final List<FileDto> files) {
    List<FileDto> old = this.files;
    this.files = files;
    firePropertyChange("files", old, files);
  }

  public Integer getUploadProgress(final String filename) {
    return uploadProgress.get(filename);
  }

  public void setUploadProgress(final String filename, final Integer percentage) {
    Integer old = this.uploadProgress.get(filename);
    uploadProgress.put(filename, percentage);
    firePropertyChange("uploadProgress", old, uploadProgress);
  }
}

Finally, UploadProgress listens for changes to UploadProgressState and updates the UploadPanel with percentage changes for the file upload, which in turn updates the panels ProgressBar.

UploadProgress.java

package org.adrianwalker.gwt.uploadprogress.client.view;

import com.google.gwt.user.client.Timer;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.VerticalPanel;
import java.util.HashMap;
import java.util.Map;
import org.adrianwalker.gwt.uploadprogress.client.state.UploadProgressState;

public final class UploadProgress extends Composite {

  private Panel panel;
  private Map<String, UploadPanel> uploads;

  public UploadProgress() {

    panel = new VerticalPanel();
    panel.setStyleName("UploadProgress");
    uploads = new HashMap<String, UploadPanel>();

    this.initWidget(panel);

    UploadProgressState.INSTANCE.addPropertyChangeListener("uploadProgress", new UploadProgressListener());
  }

  private final class UploadProgressListener implements PropertyChangeListener {

    private static final int COMPLETE_PERECENTAGE = 100;
    private static final int REMOVE_DELAY = 3000;

    @Override
    public void propertyChange(final PropertyChangeEvent event) {

      Map<String, Integer> uploadPercentage = (Map<String, Integer>) event.getNewValue();

      for (Map.Entry<String, Integer> entry : uploadPercentage.entrySet()) {
        String file = entry.getKey();
        Integer percentage = entry.getValue();

        final UploadPanel uploadPanel;
        if (!uploads.containsKey(file)) {
          uploadPanel = new UploadPanel(file);
          uploads.put(file, uploadPanel);
          panel.add(uploadPanel);
        } else {
          uploadPanel = uploads.get(file);
        }

        uploadPanel.update(percentage);

        if (percentage == COMPLETE_PERECENTAGE) {
          Timer timer = new Timer() {

            @Override
            public void run() {
              panel.remove(uploadPanel);
            }
          };
          timer.schedule(REMOVE_DELAY);
        }
      }
    }
  }

  private static final class UploadPanel extends HorizontalPanel {

    private ProgressBar bar;
    private Label label;

    public UploadPanel(final String file) {

      setStyleName("UploadPanel");

      bar = new ProgressBar();
      label = new Label(file);

      add(bar);
      add(label);
    }

    public void update(final int percentage) {
      bar.update(percentage);
    }
  }
}

Source Code

Usage

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

Update - 29/7/2012

If you're seeing the following exception during compilation try this updated pom.xml :

[gwt:compile]
auto discovered modules [org.adrianwalker.gwt.uploadprogress.UploadProgress]
You're project declares dependency on gwt-user 2.1.0. This plugin is designed for at least gwt version 2.5.0-rc1
You're project declares dependency on gwt-user 2.1.0. This plugin is designed for at least gwt version 2.5.0-rc1
Compiling module org.adrianwalker.gwt.uploadprogress.UploadProgress
   Validating units:
      Ignored 50 units with compilation errors in first pass.
Compile with -strict or with -logLevel set to TRACE or DEBUG to see all errors.
   [ERROR] Unexpected internal compiler error
java.lang.NullPointerException
 at com.google.gwt.dev.javac.CompilationProblemReporter.reportErrors(CompilationProblemReporter.java:200)
 at com.google.gwt.dev.jjs.impl.UnifyAst.assimilateUnit(UnifyAst.java:666)
 at com.google.gwt.dev.jjs.impl.UnifyAst.searchForTypeBySource(UnifyAst.java:985)
 at com.google.gwt.dev.jjs.impl.UnifyAst.addRootTypes(UnifyAst.java:530)
 at com.google.gwt.dev.jjs.JavaToJavaScriptCompiler.precompile(JavaToJavaScriptCompiler.java:601)
 at com.google.gwt.dev.jjs.JavaScriptCompiler.precompile(JavaScriptCompiler.java:33)
 at com.google.gwt.dev.Precompile.precompile(Precompile.java:278)
 at com.google.gwt.dev.Precompile.precompile(Precompile.java:229)
 at com.google.gwt.dev.Precompile.precompile(Precompile.java:141)
 at com.google.gwt.dev.Compiler.run(Compiler.java:232)
 at com.google.gwt.dev.Compiler.run(Compiler.java:198)
 at com.google.gwt.dev.Compiler$1.run(Compiler.java:170)
 at com.google.gwt.dev.CompileTaskRunner.doRun(CompileTaskRunner.java:88)
 at com.google.gwt.dev.CompileTaskRunner.runWithAppropriateLogger(CompileTaskRunner.java:82)
 at com.google.gwt.dev.Compiler.main(Compiler.java:177)

Monday, 7 February 2011

Maven Dependency Finder

My friend Mark had an idea for a utility to search a list of your favourite repositories for a given dependency. So I thought I'd code up a version.

Maven Dependency Finder is a command line utility that takes a list of repositories and a dependency group id,artifact id and version, and returns the repositories which contain the JAR you want.

The utility reads a list of repositories from a file, and uses multiple threads to check for the required JARs, outputting the URLs where the JARs are hosted.

MavenDependencyFinder.java

package org.adrianwalker.maven;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public final class MavenDependencyFinder {

  private static final int EXIT_FAILURE = -1;
  private static final int EXIT_SUCCESS = 0;
  private static final String FLAG_START = "-";
  private static final String GROUP_FLAG = FLAG_START + "g";
  private static final String ARTIFACT_FLAG = FLAG_START + "a";
  private static final String VERSION_FLAG = FLAG_START + "v";
  private static final String MAX_THREADS_FLAG = FLAG_START + "t";
  private static final String REPOSITORY_FILE_FLAG = FLAG_START + "r";
  private static final String HELP_FLAG = FLAG_START + "h";
  private static final String DEFAULT_VERSION = "";
  private static final List<String> DEFAULT_REPOSITORIES = Arrays.asList(new String[]{
            "http://mirrors.ibiblio.org/pub/mirrors/maven2/"
          });
  private static final int DEFAULT_MAX_THREADS = 2;
  private static final String ARTIFACT_VERSION_SEPERATOR = "-";
  private static final String EMPTY_STRING = "";
  private static final String HTTP_METHOD = "GET";
  private static final String JAR_FILE_EXTENSION = ".jar";
  private static final String PACKAGE_SEPERATOR = "\\.";
  private static final int TIMEOUT_MILLISECONDS = 10000;
  private static final String URL_SEPERATOR = "/";

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


    Map<String, String> parameters = readParameters(args);

    if (parameters.isEmpty() || parameters.containsKey(HELP_FLAG)) {
      printHelp();
      System.exit(EXIT_SUCCESS);
    }

    if (!parameters.containsKey(GROUP_FLAG)) {
      printError("Required parameter '-g' missing");
      System.exit(EXIT_FAILURE);
    }

    if (!parameters.containsKey(ARTIFACT_FLAG)) {
      printError("Required parameter '-a' missing");
      System.exit(EXIT_FAILURE);
    }

    String groupId = parameters.get(GROUP_FLAG);
    String artifactId = parameters.get(ARTIFACT_FLAG);

    String version = DEFAULT_VERSION;
    if (parameters.containsKey(VERSION_FLAG)) {
      version = parameters.get(VERSION_FLAG);
    }

    int maxThreads = DEFAULT_MAX_THREADS;
    if (parameters.containsKey(MAX_THREADS_FLAG)) {
      maxThreads = Integer.parseInt(parameters.get(MAX_THREADS_FLAG));
    }

    List<String> repositories = DEFAULT_REPOSITORIES;
    if (parameters.containsKey(REPOSITORY_FILE_FLAG)) {
      String fileName = parameters.get(REPOSITORY_FILE_FLAG);
      try {
        repositories = readRepositoriesFile(fileName);
      } catch (Throwable t) {
        printError(String.format("Error reading file '%s'", fileName));
        System.exit(EXIT_FAILURE);
      }
    }

    findDependency(maxThreads, repositories, groupId, artifactId, version);

    System.exit(EXIT_SUCCESS);
  }

  private MavenDependencyFinder() {
  }

  private static void findDependency(final int maxThreads, final List<String> repositories, final String groupId, final String artifactId, final String version) {

    ExecutorService threadPool = Executors.newFixedThreadPool(maxThreads);
    Map<String, Future<Boolean>> results = new HashMap<String, Future<Boolean>>();
    for (String repository : repositories) {
      Future<Boolean> isHostingDependancy = threadPool.submit(new CallableUrlFinder(repository, groupId, artifactId, version));
      results.put(repository, isHostingDependancy);
    }
    Set<String> responses = new HashSet<String>();
    while (results.size() > responses.size()) {

      for(Entry<String, Future<Boolean>> entry : results.entrySet()) {
          
        String repository = entry.getKey();
        Future<Boolean> isHostingDependancy = entry.getValue();
          
        if (!responses.contains(repository) && isHostingDependancy.isDone()) {
          Boolean found;
          try {
            found = isHostingDependancy.get();
            responses.add(repository);
          } catch (Throwable t) {
            found = false;
          }
          if (found) {
            System.out.println(repository);
          }
        }
      }
      Thread.yield();
    }
    threadPool.shutdown();
  }

  private static Map<String, String> readParameters(final String[] args) {
    int argsCount = args.length;

    if (argsCount == 1 && args[0].equals(HELP_FLAG)) {
      printHelp();
      System.exit(EXIT_SUCCESS);
    }else if (argsCount % 2 == 1) {
      printError("Invalid number of arguments");
      System.exit(EXIT_FAILURE);
    }

    Map<String, String> parameters = new HashMap<String, String>();
    for (int i = 0; i < argsCount; i = i + 2) {

      String flag = args[i];

      if (!flag.startsWith(FLAG_START)) {
        printError(String.format("Invalid flag '%s'", flag));
        System.exit(EXIT_FAILURE);
      }

      String value = args[i + 1];
      if (value.startsWith(FLAG_START)) {
        printError(String.format("Invalid value '%s' for flag '%s'", value, flag));
        System.exit(EXIT_FAILURE);
      }


      parameters.put(flag, value);
    }

    return parameters;
  }

  private static List readRepositoriesFile(final String fileName) throws IOException {
    List<String> repositories = new ArrayList<String>();
    BufferedReader reader = null;
    try {
      reader = new BufferedReader(new FileReader(fileName));
      String line;
      while ((line = reader.readLine()) != null) {
        repositories.add(line.trim());
      }
    } finally {
      if (null != reader) {
        reader.close();
      }
    }
    return repositories;
  }

  private static void printHelp() {
    String help = "Usage: mdf -g <group id> -a <artifact id> [-v <version>] [-r <filename>] [-t <threads>] [-h]\n\n"
            + "  -g\tDependency group id\n"
            + "  -a\tDependency artifact id\n"
            + "  -v\tDependency version\n"
            + "  -r\tRepository file\n"
            + "  -t\tMaximum threads\n"
            + "  -h\tHelp";
    System.out.println(help);
  }

  private static void printError(final String message) {
    System.out.println(message);
  }

  private static final class CallableUrlFinder implements Callable<Boolean> {

    private final String repository;
    private final String groupId;
    private final String artifactId;
    private final String version;

    public CallableUrlFinder(final String repository, final String groupId, final String artifactId, final String version) {
      this.repository = repository;
      this.groupId = groupId;
      this.artifactId = artifactId;
      this.version = version;
    }

    @Override
    public Boolean call() throws Exception {
      return isHostingDependency(repository, groupId, artifactId, version);
    }

    private boolean isHostingDependency(final String repository, final String groupId, final String artifactId, final String version) {

      String dependencyUrl = gavToUrl(repository, groupId, artifactId, version);

      HttpURLConnection connection = null;
      try {
        URL url = new URL(dependencyUrl);
        connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod(HTTP_METHOD);
        connection.setReadTimeout(TIMEOUT_MILLISECONDS);
        connection.connect();
        return connection.getResponseCode() == HttpURLConnection.HTTP_OK;
      } catch (Throwable t) {
        return false;
      } finally {
        if (null != connection) {
          connection.disconnect();
        }
      }
    }

    private String gavToUrl(final String repository, final String groupId, final String artifactId, final String version) {
      /*
       * Format:
       *
       * http://mirrors.ibiblio.org/pub/mirrors/maven2/log4j/log4j/1.2.13/log4j-1.2.13.jar
       *
       */

      StringBuilder builder = new StringBuilder();
      builder.append(repository) //
              .append(repository.endsWith(URL_SEPERATOR) ? EMPTY_STRING : URL_SEPERATOR) //
              .append(groupId.replaceAll(PACKAGE_SEPERATOR, URL_SEPERATOR)) //
              .append(URL_SEPERATOR).append(artifactId);
      if (version != null && !version.isEmpty()) {
        builder.append(URL_SEPERATOR) //
                .append(version) //
                .append(URL_SEPERATOR) //
                .append(artifactId) //
                .append(ARTIFACT_VERSION_SEPERATOR) //
                .append(version).append(JAR_FILE_EXTENSION);
      }

      return builder.toString();
    }
  }
}


Build the project with:

mvn clean install
to build a directory and zip assembly containing the executable JAR file, a batch file and shell script to run the utility from the command line.

Running the command with the -h flag shows the help for the utility:

Usage: mdf -g <group id> -a <artifact id> [-v <version>] [-r <filename>] [-t <threads>] [-h]

  -g    Dependency group id
  -a    Dependency artifact id
  -v    Dependency version
  -r    Repository file
  -t    Maximum threads
  -h    Help

To run the utility with a list of repositories and search for log4j:

mdf -g log4j -a log4j -r repositories.txt

outputs:

http://mirrors.ibiblio.org/pub/mirrors/maven2/
http://repository.jboss.org/maven2/

where the repositories list input file contains:

repositories.txt

http://mirrors.ibiblio.org/pub/mirrors/maven2/
http://repository.jboss.org/maven2/
http://download.java.net/maven/2
http://repository.codehaus.org

Source Code

Monday, 6 December 2010

jchannel - a Java imageboard

I love the chans, so I though I'd build my own. jchannel is simple, linearly-directed, forced anonymous imageboard, implemented using enterprise Java, EJB 3.1, JPA/EclipseLink, with a Google Web Toolkit front end, designed to run on GlassFish application server.

Features

  • GWT AJAX powered front end
  • JPA RDBMS agnostic back end
  • Rich text editor message input
  • Role based administration & moderation
  • Catgegory/Topic administration
  • Post reporting & IP ban management

Screen Shots

(click to enlarge)
Auto generated contents home page

Thread view with rich text editor input

Topic administration view

Demo

A demo of jchannel with just one topic (/b/) is running at http://jchannel.org

Source Code

The latest source code can be downloaded from Google Code using svn:

svn checkout http://jchannel.googlecode.com/svn/trunk/ jchannel-read-only

or downloaded here:

Usage

Run the project with 'mvn clean install embedded-glassfish:run' and point your brower at http://127.0.0.1:8080/jchannel/.

Running the project with all the tests will create an embedded Derby database to test against, run an embedded GlassFish server, create some default topics and a default admin user.

To log into the admin interface go to http://127.0.0.1:8080/jchannel/#login

and use the login credentials:

username: admin
password: adminadmin

Update 8/12/10

The http://jchannel.org demo now has a bunch of 4chan style topics.

Thursday, 26 August 2010

Maven, Spring, Hibernate & JPA skeleton project

Today I read that Spring-JPA is the 'dream team' for POJO development. So I thought I'd see what all the hype is about. Here is a skeleton Maven project using Spring, JPA implemented with Hibernate and backed with an embedded Derby database.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.adrianwalker.maven.skeleton.spring.jpa</groupId>
  <artifactId>maven-spring-jpa-skeleton</artifactId>
  <version>0.1.0</version>
  <name>Maven Spring JPA Skeleton</name>

  <description>
    Skeleton project for using Spring, Hibernate and Derby 
    database accessed through JPA.
 
    Usage: mvn clean install
  </description>

  <url>http://www.adrianwalker.org</url>

  <organization>
    <name>adrianwalker.org</name>
    <url>http://www.adrianwalker.org</url>
  </organization>

  <developers>
    <developer>
      <name>Adrian Walker</name>
      <email>ady.walker@gmail.com</email>
      <organization>adrianwalker.org</organization>
      <organizationUrl>http://www.adrianwalker.org</organizationUrl>
    </developer>
  </developers>

  <repositories>
    <repository>
      <id>java.net</id>
      <url>http://download.java.net/maven/2</url>
    </repository>
    
    <repository>
      <id>java.net - legacy</id>
      <url>http://download.java.net/maven/1</url>
      <layout>legacy</layout>
    </repository>    
  </repositories>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <dependencies>
    <dependency>
      <groupId>javax.persistence</groupId>
      <artifactId>persistence-api</artifactId>
      <version>1.0</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring</artifactId>
      <version>2.5.6</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>2.5.6</version>
    </dependency>

    <dependency>
      <groupId>org.apache.derby</groupId>
      <artifactId>derby</artifactId>
      <version>10.6.1.0</version>
    </dependency>

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate</artifactId>
      <version>3.2.1.ga</version>
    </dependency>

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>3.2.1.ga</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.4</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Spring Configuration

context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

  <bean id="dataSource"
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="org.apache.derby.jdbc.EmbeddedDriver" />
    <property name="url"
      value="jdbc:derby:target/database/message;create=true" />
    <property name="username" value="app" />
    <property name="password" value="app" />
  </bean>

  <bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource"></property>
    <property name="jpaVendorAdapter">
      <bean
        class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <property name="databasePlatform" value="org.hibernate.dialect.DerbyDialect" />
      </bean>
    </property>
    <property name="jpaProperties">
      <props>
        <prop key="hibernate.hbm2ddl.auto">create-drop</prop>
      </props>
    </property>
  </bean>

  <bean name="jpaMessageDao"
    class="org.adrianwalker.maven.skeleton.spring.jpa.JpaMessageDao">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>
  
  <bean name="messageJpaController"
    class="org.adrianwalker.maven.skeleton.spring.jpa.MessageJpaController">
  </bean>  

  <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
    <property name="dataSource" ref="dataSource" />
  </bean>

</beans>

Persistence Configuration

persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="
    http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    
  <persistence-unit name="messagePersistenceUnit" transaction-type="RESOURCE_LOCAL">
    <class>org.adrianwalker.maven.skeleton.spring.jpa.MessageEntity</class>
  </persistence-unit>
</persistence>

JPA Entity

A very simple entity which has an id and some message text.

MessageEntity.java

package org.adrianwalker.maven.skeleton.spring.jpa;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;

@Entity
@NamedQueries({
  @NamedQuery(
    name="MessageEntity.find",
    query = "SELECT m FROM MessageEntity m"),
  @NamedQuery(
      name = "MessageEntity.count", 
      query = "SELECT COUNT(m) FROM MessageEntity m")
})
public class MessageEntity implements Serializable {

  private static final long serialVersionUID = 1L;
 
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private long id;

  private String text;

  public MessageEntity() {
  }

  public MessageEntity(final String text) {
    this.text = text;
  }

  public long getId() {
    return id;
  }

  public void setId(final long id) {
    this.id = id;
  }

  public String getText() {
    return text;
  }

  public void setText(final String text) {
    this.text = text;
  }

  @Override
  public String toString() {
    return "MessageEntity [id=" + id + ", text=" + text + "]";
  }
}

Spring DAO vs JPA Controller

Spring provides a JpaDaoSupport class for extension, but this couples your DAO objects to the Spring framework, and you have to do some pretty ugly stuff to execute your named queries:

JpaMessageDao.java

package org.adrianwalker.maven.skeleton.spring.jpa;

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import org.springframework.orm.jpa.JpaCallback;
import org.springframework.orm.jpa.support.JpaDaoSupport;

public final class JpaMessageDao extends JpaDaoSupport {

  public long count() {
    return (Long) getJpaTemplate().execute(new JpaCallback() {

      public Object doInJpa(final EntityManager em) throws PersistenceException {
        Query q = em.createNamedQuery("MessageEntity.count");
        return q.getSingleResult();
      }
    });
  }

  public void create(final MessageEntity message) {
    getJpaTemplate().persist(message);
  }

  public MessageEntity read(final long id) {
    return getJpaTemplate().find(MessageEntity.class, id);
  }

  @SuppressWarnings("unchecked")
  public List<MessageEntity> read(final int firstResult, final int maxResults) {
    return (List<MessageEntity>) getJpaTemplate().execute(new JpaCallback() {

      public Object doInJpa(final EntityManager em) throws PersistenceException {
        Query q = em.createNamedQuery("MessageEntity.find");
        q.setFirstResult(firstResult);
        q.setMaxResults(maxResults);
        return q.getResultList();
      }
    });
  }

  public MessageEntity update(final MessageEntity message) {
    return getJpaTemplate().merge(message);
  }

  public void delete(final MessageEntity message) {
    getJpaTemplate().remove(message);
  }
}

A much neater alternative is to use a JPA style controller which gives you direct access to the EntityManager. The @PersistenceContext annotation is understood by Spring and your entity manager is injected into the controller class.

MessageJpaController.java

package org.adrianwalker.maven.skeleton.spring.jpa;

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.PersistenceContext;

public class MessageJpaController {

  @PersistenceContext(unitName = "messagePersistenceUnit")
  private EntityManager em;

  public long count() {
    Query q = em.createNamedQuery("MessageEntity.count");
    return (Long) q.getSingleResult();
  }

  public void create(MessageEntity messageEntity) {
    em.persist(messageEntity);
  }

  public MessageEntity read(final long id) {
    return em.find(MessageEntity.class, id);
  }

  @SuppressWarnings("unchecked")
  public List<MessageEntity> read(int firstResult, int maxResults) {
    Query q = em.createNamedQuery("MessageEntity.find");
    q.setFirstResult(firstResult);
    q.setMaxResults(maxResults);
    return q.getResultList();
  }

  public MessageEntity update(MessageEntity messageEntity) {
    return em.merge(messageEntity);
  }

  public void delete(final MessageEntity message) {
    em.remove(message);
  }
}

Usage & Tests

And finally here are some JUnit 4 tests for both the DAO and Controller.

MessageTest.java

package org.adrianwalker.maven.skeleton.spring.jpa;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:context.xml" })
@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = false)
@Transactional
public final class MessageTest {

  @Autowired
  private MessageJpaController messageJpaController;
  
  @Autowired
  private JpaMessageDao jpaMessageDao;
   
  @Test
  public void daoCreate() throws Exception {
    
    for (int i = 1; i <= 10; i++) {
      jpaMessageDao.create(new MessageEntity(String.format("Message %s", i)));
    }

    assertEquals(10, jpaMessageDao.count());
  }

  @Test
  public void daoRead() throws Exception {
    List<MessageEntity> messages = jpaMessageDao.read(1, 2);
    assertNotNull(messages);
    assertEquals(2, messages.size());
  }  
  
  @Test
  public void daoUpdate() throws Exception {
    
    MessageEntity message = jpaMessageDao.read(1);

    assertNotNull(message);
    assertEquals("Message 1", message.getText());

    message.setText("Message X");
    message = jpaMessageDao.update(message);

    message = jpaMessageDao.read(1);
    assertEquals("Message X", message.getText());
  }

  @Test
  public void daoDelete() throws Exception {
    long beforeCount = jpaMessageDao.count();
    assertTrue(beforeCount == 10);

    for (int i = 1; i <= 10; i++) {
      MessageEntity message = jpaMessageDao.read(i);
      jpaMessageDao.delete(message);
    }

    long afterCount = jpaMessageDao.count();
    assertTrue(afterCount == 0);
  }
  
  @Test
  public void controllerCreate() throws Exception {    

    for (int i = 11; i <= 20; i++) {
      messageJpaController.create(new MessageEntity(String.format("Message %s", i)));
    }

    assertEquals(10, messageJpaController.count());
  }

  @Test
  public void controllerRead() throws Exception {
    List<MessageEntity> messages = messageJpaController.read(1, 2);
    
    assertNotNull(messages);
    assertEquals(2, messages.size());
  }    
  
  @Test
  public void controllerUpdate() throws Exception {
    MessageEntity message = messageJpaController.read(11);

    assertNotNull(message);
    assertEquals("Message 11", message.getText());

    message.setText("Message X");
    message = messageJpaController.update(message);

    message = messageJpaController.read(11);
    assertEquals("Message X", message.getText());
  }

  @Test
  public void controllerDelete() throws Exception {
    long beforeCount = messageJpaController.count();
    assertTrue(beforeCount == 10);

    for (int i = 11; i <= 20; i++) {
      MessageEntity message = messageJpaController.read(i);
      messageJpaController.delete(message);
    }

    long afterCount = messageJpaController.count();
    assertTrue(afterCount == 0);
  }  
}

Don't Believe The Hype

So basically, for me anyway, using Spring in conjunction with JPA gives you nothing above and beyond using JPA directly. By the the time you decide not to bother with JpaDaoSupport, all that has been achieved is moving connection properties form persistence.xml to context.xml, which just creates more Spring configuration XML clutter.

Spring recognising the @PersistenceContext is a nice touch for testing, but using constructor/setter injection to inject an EntityManager into a JPA controller class is no big deal if you don't want to use spring.

Source Code

Wednesday, 18 August 2010

Using Ordnance Survey OpenData Street View Rasters With GeoServer

Getting the data

The Ordnance Survey OpenData can be downloaded or ordered on DVD from here. I thought I'd order the lot on DVD since its free and it would save my bandwidth and time burning gigs of data to disc.

The data comes on EcoDisc DVDs, packaged in cardboard sleeves:

For this post I'll only be using data on disc 1/6 and 3/6 on the OS Street View discs.

Getting GeoServer

GeoServer stable release can be downloaded here. I punted for the binary download format, because I want to run GeoServer on Linux. After unzipping and starting GeoServer, you should be able to browse to http://localhost:8080/geoserver/web/ and log in using the default username/password:

admin/geoserver

Using the Street View Rasters

First we need to decide what set of tiles we want to use. I only want maps for York and the surrounding area, so using the grid map:

I can see I only want to use the SE data.

Disc 3 of the OS Street View set contains the tif image files I want, located in the directory:

data/se
The files need to be coppied to a location in GeoServers data directory tree. I coppied the images to:
geoserver-2.0.2/data_dir/data/OpenData/StreetView/se

The image files aren't GeoTIFFs, so need geo-referencing data not contained in the image file. This information is held on the disc 1 of the OS Street View set, in the directory:

data/georeferencing files/TFW
We only need the files which start with se, and these need to be copied along side the images in the data directory:
geoserver-2.0.2/data_dir/data/OpenData/StreetView/se
We also need to make sure that the file extension of the tfw is lower cased, using a Linux shell:
rename .TFW .tfw *.TFW
We should now have a directory containing .tif image files and .tfw world files. Each of the tif/tfw pairs needs a file containing projection information before it can be loaded into GeoServer. The files aren't supplied with the raster data, but they are simple to create. The name of the file must match the name of the tif/tfw, and each of the files must contain the same data:

PROJCS["OSGB 1936 / British National Grid", GEOGCS["OSGB 1936", DATUM["OSGB_1936", SPHEROID["Airy 1830",6377563.396,299.3249646, AUTHORITY["EPSG","7001"]], AUTHORITY["EPSG","6277"]], PRIMEM["Greenwich",0, AUTHORITY["EPSG","8901"]], UNIT["degree",0.01745329251994328, AUTHORITY["EPSG","9122"]], AUTHORITY["EPSG","4277"]], UNIT["metre",1, AUTHORITY["EPSG","9001"]], PROJECTION["Transverse_Mercator"], PARAMETER["latitude_of_origin",49], PARAMETER["central_meridian",-2], PARAMETER["scale_factor",0.9996012717], PARAMETER["false_easting",400000], PARAMETER["false_northing",-100000], AUTHORITY["EPSG","27700"], AXIS["Easting",EAST], AXIS["Northing",NORTH]]

You can create the files by hand, or running the following Python script in the directory containing the .tif files will do it for you:

import glob

content = 'PROJCS["OSGB 1936 / British National Grid", GEOGCS["OSGB 1936", DATUM["OSGB_1936", SPHEROID["Airy 1830",6377563.396,299.3249646, AUTHORITY["EPSG","7001"]], AUTHORITY["EPSG","6277"]], PRIMEM["Greenwich",0, AUTHORITY["EPSG","8901"]], UNIT["degree",0.01745329251994328, AUTHORITY["EPSG","9122"]], AUTHORITY["EPSG","4277"]], UNIT["metre",1, AUTHORITY["EPSG","9001"]], PROJECTION["Transverse_Mercator"], PARAMETER["latitude_of_origin",49], PARAMETER["central_meridian",-2], PARAMETER["scale_factor",0.9996012717], PARAMETER["false_easting",400000], PARAMETER["false_northing",-100000], AUTHORITY["EPSG","27700"], AXIS["Easting",EAST], AXIS["Northing",NORTH]]'

tifs = glob.glob('*.tif')

for tif in tifs:
  prj = tif.split('.')[0] + '.prj'
  file = open(prj,'w')
  file.writelines(content)
  file.close()


If you have a data directory with files that look a bit like this, then you're ready to load them into GeoServer:

  1. Load GeoServer up in a browser http://localhost:8080/geoserver/web/ and login. Click the 'Workspaces' link in the 'Data' section of the left hand navigation bar. Now click the 'Add new workspace' link.

  2. Name the workspace OpenData and give it the URI https://www.ordnancesurvey.co.uk/, and click save.

  3. Click the 'Stores' link in the 'Data' section of the left hand navigation bar. Now click the 'Add new Store' link. Under the 'Raster Data Sources' heading, click the 'ImageMosaic' link.

  4. Select the OpenData workspace, name the data source OS Street View SE and point the URL connection parameter to the data directory:
    file:data/OpenData/StreetView/se
    and then click save.

  5. You should be forwarded to the 'New Layer chooser page', click the 'Publish' link next to the 'se' layer in the table.

  6. About halfway down the page there should be inputs for 'Native SRS' and 'Declared SRS', make sure they both contain:
    EPSG:27700
    and click the 'Compute from data' and 'Compute from native bounds' links.

  7. Finally click save.

If all went well you should be able to use GeoServers 'Layer Preview' built in OpenLayers client to view the map:

Source Code

Friday, 13 August 2010

ColdFusion Is Dead (To Me)

So as from today I never have to program any ColdFusion again, it feels pretty good. ColdFusion just doesn't knock my frock off, it never did, and it probably never will.

This isn't one of those ColdFusion is dead, or ColdFusion sucks rants, those things aren't really my call, and besides, I can't be bothered to deal with ColdTard butthurt on my blog.

A Helmet

All I really know is that programming in CF made me feel a lot like this picture, and you secretly know it makes you feel like this too: