This is part 3 of my Train Tracker project. Code from this post can be found in the scaffolding branch on Github. Previous post can be found here.

Logging

Log messages written to stdout and stderr are captured by systemd for services it manages. We can then access these logs using journalctl.

In order to keep external dependencies low, I will be using built-in Java logging infrastructure. Default ConsoleHandler, logs everything to stderr with no option to separate the stream based on log level. I want log levels Warning and above to go to stderr and the rest to stdout. StdConsoleHandler will format using SimpleFormatter.

public class StdConsoleHandler extends Handler {

    private final PrintStream out;
    private final PrintStream err;

    public StdConsoleHandler() {
        this(new SimpleFormatter(), System.out, System.err);
    }

    public StdConsoleHandler(Formatter formatter, PrintStream out, PrintStream err) {
        super();
        setFormatter(formatter);
        this.out = out;
        this.err = err;
    }

    @Override
    public void publish(LogRecord record) {
        try {
            String message = getFormatter().format(record);
            if (record.getLevel().intValue() >= Level.WARNING.intValue()) {
                err.write(message.getBytes());
            } else {
                out.write(message.getBytes());
            }
        } catch (Exception e) {
            reportError(e.getMessage(), e, ErrorManager.FORMAT_FAILURE);
        }
    }

    @Override
    public void flush() {
    }

    @Override
    public void close() throws SecurityException {
    }
}

Also lets add a Utils helper method to instantiate a logger and configure it to suit our needs. We have call logger.setUseParentHandle(false) to ensure that other default handlers are not invoked.

public static Logger getFineLogger(String className) {
    Logger logger = Logger.getLogger(className);
    logger.setLevel(Level.FINE);
    logger.setUseParentHandlers(false);
    logger.addHandler(new StdConsoleHandler());
    return logger;
}

With the Console log handler and utility method defined, we can now update our Main method to use the logger.

+import blog.devrandom.utils.Utils;
+
+import java.util.logging.Logger;
+
 public class Main {
+
+    private static final Logger logger = Utils.getFineLogger(Main.class.getName());
+
     public static void main(String[] args) {
-        System.out.println("Train Tracker version: 1.0.0");
+        logger.info("Train Tracker version: 1.0.0");
     }
 }

Now when we build and run the application we get formatted logs

$ java -jar build/libs/train-tracker-service-1.0.0.jar
Jan 1, 1970 0:00:00 AM blog.devrandom.Main main
INFO: Train Tracker version: 1.0.0

Gson TypeAdapters

Gson out of the box does not support any of the Java 8 features like Optional and LocalDateTime. We have to define TypeAdapters for these and all the other Enums that we will need to effectively parse the response from Arrival and Departure Board.

LocalDateTime Type Adapter

public class LocalDateTimeAdapter implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;

    @Override
    public LocalDateTime deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
        return LocalDateTime.parse(jsonElement.getAsJsonPrimitive().getAsString(), formatter);
    }

    @Override
    public JsonElement serialize(LocalDateTime localDateTime, Type type, JsonSerializationContext jsonSerializationContext) {
        return new JsonPrimitive(localDateTime.format(formatter));
    }
}

Optional Type Adapter

public class OptionalTypeAdapter<E> implements JsonSerializer<Optional<E>>, JsonDeserializer<Optional<E>> {

    @Override
    public Optional<E> deserialize(JsonElement json, Type type, JsonDeserializationContext context)
            throws JsonParseException {
        if (json.isJsonNull()) {
            return Optional.empty();
        }
        E value = context.deserialize(json, ((ParameterizedType) type).getActualTypeArguments()[0]);
        return Optional.ofNullable(value);
    }

    @Override
    public JsonElement serialize(Optional<E> src, Type type, JsonSerializationContext context) {
        if (src.isPresent()) {
            return context.serialize(src.get());
        }
        return JsonNull.INSTANCE;
    }
}

A utility method to initialize Gson with type adapters.

public static Gson getGson() {
    return new GsonBuilder()
            .registerTypeAdapter(Optional.class, new OptionalTypeAdapter<>())
            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
            .create();
}

We are not using Gson yet within the application yet, unit tests will do for now. All this leg work is required because in the next part we will be using swagger to auto generate JSON response record classes.