Fortune in Java

Wherein you build a fortune teller service and a client to talk to it.

Prerequisites:
All Java tutorials require Java and Gradle.

Introduction

In this tutorial, we will create a fortune teller server and a client to talk to it. The server will have two methods:

Setting up the project

In this tutorial, we will use the Gradle build tool to build the project. There is no requirement that Vanadium Java projects use Gradle, but it's the easiest way to get started. See the installation instructions for details. The remainder of the tutorial will assume that you have the gradle program in your PATH.

Build file

The first step to defining a Gradle project is to create a build.gradle file in the project root directory.

First, create a new project directory.

mkdir fortuneJava
cd fortuneJava

Now, create a build.gradle file:

cat <<EOF > build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        // Our project is going to use VDL, so we need to depend on the Vanadium
        // Gradle plugin.
        classpath 'io.v:gradle-plugin:0.5'
    }
}

// We're going to be building an application to run
apply plugin: 'application'

// It's going to use VDL
apply plugin: 'io.v.vdl'

// And it's going to be written in Java
apply plugin: 'java'

// This class will contain our server's entry point
mainClassName = 'io.v.tutorial.FortuneTutorial'

repositories {
    mavenCentral()
}

dependencies {
    // We need the Vanadium Java libraries.
    compile 'io.v:vanadium:0.1'
}

vdl {
    // This is where the VDL tool will look for VDL definitions.
    inputPaths += 'src/main/java'
}

EOF

Defining the Fortune service

In Java, we must use VDL (Vanadium Definition Language) to define our server interface. We will reuse the same interface definition from the Client/Server Basics tutorial. For this tutorial, the fortune teller server definition lives in src/main/java/io/v/tutorial/fortune.vdl. Let's create this file now:

mkdir -p src/main/java/io/v/tutorial
cat <<EOF > src/main/java/io/v/tutorial/fortune.vdl
package fortune

type Fortune interface {
  // Returns a random fortune.
  Get() (wisdom string | error)
  // Adds a fortune to the set used by Get().
  Add(wisdom string) error
}
EOF

As you can see, we provide 'Get' and 'Add' methods to get and add fortunes. We can now test our VDL file by asking Gradle to generate the corresponding Java files. Do this by running

gradle vdl

You should see output like the following:

:prepareVdl
:extractVdl
:generateVdl
signature
time
vdltool
io/v/tutorial
:removeVdlRoot
:vdl

BUILD SUCCESSFUL

The io/v/tutorial line indicates that VDL tool has processed your input file. If you now look inside the generated-src directory, you'll find the following entries:

generated-src/vdl/io/v/tutorial/FortuneServerWrapper.java
generated-src/vdl/io/v/tutorial/FortuneClient.java
generated-src/vdl/io/v/tutorial/FortuneServer.java
generated-src/vdl/io/v/tutorial/FortuneClientImpl.java
generated-src/vdl/io/v/tutorial/FortuneClientFactory.java

Implementation

Now we must provide an implementation for the FortuneServer.

Create src/main/java/io/v/tutorial/InMemoryFortuneServer.java:

cat <<EOF > src/main/java/io/v/tutorial/InMemoryFortuneServer.java
package io.v.tutorial;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import io.v.v23.V;
import io.v.v23.context.VContext;
import io.v.v23.naming.Endpoint;
import io.v.v23.rpc.Server;
import io.v.v23.rpc.ServerCall;
import io.v.v23.security.VSecurity;
import io.v.v23.verror.VException;

public class InMemoryFortuneServer implements FortuneServer {
    private final List<String> fortunes = new ArrayList<>();
    private final Random random = new Random(System.currentTimeMillis());

    @Override
    public String get(VContext ctx, ServerCall call) throws VException {
        if (fortunes.isEmpty()) {
            throw new VException("There are no fortunes available, try Add()ing one!");
        } else {
            return fortunes.get(random.nextInt(fortunes.size()));
        }
    }

    @Override
    public void add(VContext ctx, ServerCall call, String wisdom) throws VException {
        fortunes.add(wisdom);
    }

    public static Endpoint[] startServer() throws VException {
        // Initialize the Vanadium runtime and load its native shared library
        // implementation. This is required before we can do anything involving
        // Vanadium.
        VContext context = V.init();

        // Serve a new InMemoryFortuneServer with an allow-everyone authorizer.
        // This call will return immediately, serving is done in a separate
        // thread.
        Server fortuneServer = V.getServer(V.withNewServer(context, "",
            new InMemoryFortuneServer(),
            VSecurity.newAllowEveryoneAuthorizer()));

        return fortuneServer.getStatus().getEndpoints();
    }
}
EOF

And now let's create our entry point at src/main/java/io/v/tutorial/FortuneTutorial.java:

cat <<EOF > src/main/java/io/v/tutorial/FortuneTutorial.java
package io.v.tutorial;

import java.io.IOException;
import java.util.Arrays;

import io.v.v23.naming.Endpoint;
import io.v.v23.verror.VException;

public class FortuneTutorial {
    public static void main(String[] args) throws IOException, VException {
        Endpoint[] endpoints = InMemoryFortuneServer.startServer();
        System.out.println("FortuneServer available at the following endpoints: " +
                Arrays.toString(endpoints));
        System.out.println("Listening for connections, press enter to quit.");
        System.in.read();
        System.out.println("Exiting...");
    }
}
EOF

Running the server

Now you are ready to build and run the server.

gradle installDist

This will leave an executable script in build/install/fortuneJava/bin/fortuneJava. When we run it:

FortuneServer available at the following endpoints: [@5@wsh@127.0.0.1:54221@50704f409f1fc0bf20f01020b02f2030@s@sjr@example.com-15180@@, @5@wsh@192.168.2.4:54221@50704f409f1fc0bf20f01020b02f2030@s@sjr@example.com-15180@@]
Listening for connections, press enter to quit.

Excellent, the server is now running. Let's write a client to talk to it.

Defining a Fortune client

The VDL step has generated a client stub for us to use to call methods on a server. We obtain stub instances from the generated FortuneClientFactory class. The only other piece of information we need is the name of the endpoint to which to talk.

Let's create src/main/java/io/v/tutorial/FancyFortuneClient.java:

cat <<EOF > src/main/java/io/v/tutorial/FancyFortuneClient.java
package io.v.tutorial;

import io.v.v23.OptionDefs;
import io.v.v23.Options;
import io.v.v23.context.VContext;
import io.v.v23.verror.VException;

public class FancyFortuneClient {
    private final VContext context;
    private final FortuneClient client;
    private final Options options;

    public FancyFortuneClient(VContext context, String endpointName) {
        this.context = context;
        this.client = FortuneClientFactory.getFortuneClient(endpointName);

        // The SKIP_SERVER_ENDPOINT_AUTHORIZATION is necessary because this
        // tutorial does not deal with trust. If we omit this option, our client
        // will not trust the server. Your production code should not set this
        // option because it makes the client vulnerable to man-in-the-middle
        // attacks.
        this.options = new Options().set(OptionDefs.SKIP_SERVER_ENDPOINT_AUTHORIZATION,
                true);
    }

    public String get() throws VException {
        return client.get(context, options);
    }

    public void add(String wisdom) throws VException {
        client.add(context, wisdom, options);
    }
}
EOF

Let's modify the main FortuneTutorial class a little. It will now have three modes depending on how many arguments we pass in:

For example:

build/install/fortuneJava/bin/fortuneJava  # run a server
build/install/fortuneJava/bin/fortuneJava @5@...@@  # fetch a fortune
build/install/fortuneJava/bin/fortuneJava @5@...@@ "Hello world!"  # add a fortune

Here is the new FortuneTutorial:

cat <<EOF > src/main/java/io/v/tutorial/FortuneTutorial.java
package io.v.tutorial;

import java.io.IOException;
import java.util.Arrays;

import io.v.v23.V;
import io.v.v23.naming.Endpoint;
import io.v.v23.verror.VException;

public class FortuneTutorial {
    public static void main(String[] args) throws IOException, VException {
        if (args.length > 0) {
            FancyFortuneClient client = new FancyFortuneClient(V.init(), args[0]);
            if (args.length >= 2) {
                for (int i = 1; i < args.length; i++) {
                    client.add(args[i]);
                }
            } else {
                System.out.println(client.get());
            }
        } else {
            Endpoint[] endpoints = InMemoryFortuneServer.startServer();
            System.out.println("FortuneServer available at the following endpoints: " +
                    Arrays.toString(endpoints));
            System.out.println("Listening for connections, press enter to quit.");
            System.in.read();
            System.out.println("Exiting...");
        }
    }
}
EOF

Build the tutorial again:

gradle installDist

Here's an example session:

$ FortuneServer available at the following endpoints: [@5@wsh@127.0.0.1:36191@c00090af3ff050d020ef3f4f4f40ffff@s@sjr@example.com-28934@@, @5@wsh@192.168.2.4:36191@c00090af3ff050d020ef3f4f4f40ffff@s@sjr@example.com-28934@@]
Listening for connections, press enter to quit.

In a separate terminal:

$ export ENDPOINT=/@5@wsh@192.168.2.4:36191@c00090af3ff050d020ef3f4f4f40ffff@s@sjr@example.com-28934@@
$ build/install/fortuneJava/bin/fortuneJava $ENDPOINT
Exception in thread "main" io.v.v23.verror.VException: sjr:"/@5@wsh@192.168.2.4:36191@c00090af3ff050d020ef3f4f4f40ffff@s@sjr@example.com-28934@@".Get: Error: There are no fortunes available, try Add()ing one!
        at io.v.v23.verror.VExceptionVdlConverter.nativeFromVdlValue(VExceptionVdlConverter.java:70)
        at io.v.v23.verror.VExceptionVdlConverter.nativeFromVdlValue(VExceptionVdlConverter.java:22)
        at io.v.v23.vom.BinaryDecoder.readValue(BinaryDecoder.java:190)
        at io.v.v23.vom.BinaryDecoder.readValueMessage(BinaryDecoder.java:118)
        at io.v.v23.vom.BinaryDecoder.decodeValue(BinaryDecoder.java:85)
        at io.v.v23.vom.VomUtil.decode(VomUtil.java:104)
        at io.v.impl.google.rpc.ClientCallImpl.nativeFinish(Native Method)
        at io.v.impl.google.rpc.ClientCallImpl.finish(ClientCallImpl.java:35)
        at io.v.tutorial.FortuneClientImpl.get(FortuneClientImpl.java:62)
        at io.v.tutorial.FancyFortuneClient.get(FancyFortuneClient.java:21)
        at io.v.tutorial.FortuneTutorial.main(FortuneTutorial.java:19)
$ build/install/fortuneJava/bin/fortuneJava $ENDPOINT "Hello, world!"
$ build/install/fortuneJava/bin/fortuneJava $ENDPOINT
Hello, world!

Summary

Congratulations! You have successfully run the Java fortune example.

You have: