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:
Add
, which adds a fortune to the list of fortunes, andGet
, which retrieves a random fortune.
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:
no arguments: start a Fortune server just as we did in the first part of the tutorial
one argument: fetch a fortune from the endpoint named by that argument
two or more arguments: add the second and subsequent arguments as fortunes to the server named by the first argument
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:
- Built a fortune client and server in Java.
- Established communication between the Java and Go clients and servers.
- Learned about using Vanadium in Java as compared to Go.