Skip to content
Aleksey Zhidkov edited this page Feb 5, 2016 · 1 revision

Restler ToDo

In this tutorial, we will create a simple RESTful Web serivce for todo list management, and a Restler-based console client for that service.

You can check full source code here

Architecture

The project consists of 3 parts: API, server and client. The server will implement the API, and the client will depend on it.

Restler ToDo Architecture

Restler Todo Architecture

Restler ToDo Project Structure

Restler Todo Project Structure

Build

The root build file contains only a subprojects section that adds the java plugin, maven repositories and sets the source and target versions to Java 1.8.

subprojects {

	apply plugin: 'java'

	repositories {
    	mavenCentral()
    	maven { url "http://repo.maven.apache.org/maven2" }
	}

	sourceCompatibility = 1.8
	targetCompatibility = 1.8
}

Let's now consider each of the three project parts in detail.

API

Code

The API consists of two classes: Todo and Todos. Todo is a simple DTO that is transferred between a client and a server, Todos describes operations provided by a server.

public class Todo implements Serializable {

    public final String id;
    public final String name;
    public final String description;
    public final boolean done;

    @JsonCreator
    public Todo(String id, String name, String description, boolean done) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.done = done;
    }

    public Todo(String name, String description, boolean done) {
        this(null, name, description, done);
    }

    public Todo withId(String id) {
        return new Todo(id, name, description, done);
    }

    @Override
    public String toString() {
        return "Todo{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", description='" + description + '\'' +
                ", done=" + done +
                '}';
    }

}

The Todo class doesn't stand out in any way, except that it is immutable, and getters are dropped out in favor of public fields, because any logic requiring a getter may be placed in the constructor.

@RestController("todo")
public interface Todos {

	@RequestMapping(value = "", method = RequestMethod.POST)
	Todo create(@RequestBody Todo todo);

	@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
	Todo update(@PathVariable String id, @RequestBody Todo todo);

	@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
	Todo delete(@PathVariable String id);

	@RequestMapping(value = "/{id}", method = RequestMethod.GET)
	Todo get(@PathVariable String id);

	@RequestMapping(value = "", method = RequestMethod.GET)
	Todo[] list();

}

Todos is an interface that will be implemented by the server module. But that class specifies not only a Java interface, but also a REST interface. Specifically, it describes which HTTP requests may be executed on a Todo resource. For Restler, this solution is of particular importance. because a client doesn’t depend on the server implementation, it only depends on its interface.

But, as everything in the software world, this solution has a flaw: names of interface method parameters are not present in Java class files by default. That means that parameter names cannot be retrieved at run time. There are two ways to avoid that limitation:

  1. Define parameter names in annotations (e.g. @PathVariable("id"))
  2. Add the "-parameters" flag to javac arguments to enforce writing of interface parameter names into class files.

In this tutorial we picked the second option.

Build

In the API build file, we add the required dependencies and the compiler flag -parameters (see the Code section above).

dependencies {
	compile 'org.springframework:spring-webmvc:4.1.7.RELEASE',
        	'com.fasterxml.jackson.core:jackson-databind:2.5.4'
}

  compileJava {
	options.compilerArgs << '-parameters'
}

Server

Code

The server will be based on Spring Boot, so it can be defined in just two classes:

@EnableAutoConfiguration
@Configuration
@EnableWebMvc
public class Starter extends WebMvcConfigurerAdapter {

    @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        ParanamerModule module = new ParanamerModule();
        converters.stream().
                filter(c -> c instanceof MappingJackson2HttpMessageConverter).
                forEach(c -> ((MappingJackson2HttpMessageConverter) c).getObjectMapper().registerModule(module));
    }


    @Bean Todos todos() {
        return new TodosController();
    }

    public static void main(String[] args) {
        SpringApplication.run(Starter.class, args);
    }

}
public class TodosController implements Todos {

	private ConcurrentHashMap<String, Todo> todos = new ConcurrentHashMap<>();

	@Override
	public Todo create(Todo todo) {
    	String id = UUID.randomUUID().toString();
    	Todo storedTodo = todo.withId(id);
    	todos.put(id, storedTodo);
    	return storedTodo;
	}

	@Override
	public Todo update(String id, Todo todo) {
    	return todos.put(id, todo.withId(id));
	}

	@Override
	public Todo delete(String id) {
    	return todos.remove(id);
	}

	@Override
	public Todo get(String id) {
    	return todos.get(id);
	}

	@Override
	public Todo[] list() {
    	return todos.values().toArray(new Todo[todos.size()]);
	}
}

The Starter class is the configuration and the server entry point at the same time. TodosController is a trivial server implementation that uses an in-memory map to store todos.

Build

The required dependencies are specified in the server build file, and the application plugin is applied, so that the server can be packaged and run as an application.

apply plugin: 'application'

mainClassName = 'org.restler.todo.Starter'

dependencies {
    compile project(':api'),
            'org.springframework.boot:spring-boot-starter-web:1.2.5.RELEASE',
            'com.fasterxml.jackson.module:jackson-module-paranamer:2.6.1'
}

Client

Code

Manual interface implementation

Without Restler, we would have to implement the Todos interface manually, as in the following class:

public class ManualTodosImpl implements Todos {

    private static final String baseUrl = "http://localhost:8080/";
    private RestTemplate restTemplate = new RestTemplate();

    public ManualTodosImpl() {
        ParanamerModule module = new ParanamerModule();
        restTemplate.getMessageConverters().stream().
                filter(c -> c instanceof MappingJackson2HttpMessageConverter).
                forEach(c -> ((MappingJackson2HttpMessageConverter) c).getObjectMapper().registerModule(module));
    }

    @Override
    public Todo create(Todo todo) {
        return restTemplate.postForObject(baseUrl, todo, Todo.class);
    }

    @Override
    public Todo update(String id, @RequestBody Todo todo) {
        try {
            return restTemplate.exchange(
                      new RequestEntity<>(todo, null, HttpMethod.PUT, 
                                          new URI(baseUrl + "/" + id)), Todo.class).getBody();
        } catch (URISyntaxException e) {
            throw new RuntimeException("Unexpected exception", e);
        }
    }

    @Override
    public Todo delete(@PathVariable String id) {
        try {
            return restTemplate.exchange(
                       new RequestEntity<>(null, null, HttpMethod.DELETE, 
                                           new URI(baseUrl + "/" + id)), Todo.class).getBody();
        } catch (URISyntaxException e) {
            throw new RuntimeException("Unexpected exception", e);
        }
    }

    @Override
    public Todo get(@PathVariable String id) {
        return restTemplate.getForObject(baseUrl + "/" + id, Todo.class);
    }

    @Override
    public Todo[] list() {
        return restTemplate.getForObject(baseUrl, Todo[].class);
    }

}

Automatic interface implementation using Restler

With Restler, the entire client consists of just one class:

public class TodoClient {

	private HashMap<String, Function<String[], Result>> handlers;
	private Todos todos;

	public TodoClient() {
    	handlers = new HashMap<>();
    	handlers.put("e", this::exitHandler);
    	handlers.put("l", this::listHandler);
    	handlers.put("c", this::createHandler);
    	handlers.put("u", this::updateHandler);
    	handlers.put("d", this::deleteHandler);
    	handlers.put("g", this::getHandler);
	}

	public static void main(String[] args) throws IOException {
    	new TodoClient().run();
	}

	private void run() throws IOException {
        // THIS 5 LINES ARE EQUIVALENT TO CLASS FROM PREVIOUS SECTION
        SpringMvcSupport springMvcSupport = new SpringMvcSupport();
        builder.addJacksonModule(new ParanamerModule());		+        springMvcSupport.addJacksonModule(new ParanamerModule());
        Restler builder = new Restler("http://localhost:8080", springMvcSupport);
    	Service todosService = builder.build();
    	todos = todosService.produceClient(Todos.class);

    	try (BufferedReader reader = new BufferedReader(
               new InputStreamReader(System.in))) {
        	reader.lines().
                	map(command -> {
                    	String[] cmdParts = command.split(" ");
                    	return handlers.
                                 getOrDefault(cmdParts[0], this::defaultHandler).
                                 apply(tail(cmdParts));
                	}).
                	filter(Result.BREAK::equals).findAny();
    	}
	}

	private Result createHandler(String[] args) {
    	todos.create(todo(args));
    	return Result.CONTINUE;
	}

	private Result updateHandler(String[] args) {
    	todos.update(args[0], todo(tail(args)));
    	return Result.CONTINUE;
	}

	private Result deleteHandler(String[] args) {
    	todos.delete(args[0]);
    	return Result.CONTINUE;
	}

	private Result listHandler(String[] args) {
    	Arrays.stream(todos.list()).
            	map(todo -> todo.id).
            	forEach(System.out::println);
    	return Result.CONTINUE;
	}

	private Result getHandler(String[] args) {
    	System.out.println(todos.get(args[0]));
    	return Result.CONTINUE;
	}

	private Result exitHandler(String[] args) {
    	return Result.BREAK;
	}

	private Result defaultHandler(String[] args) {
    	return Result.CONTINUE;
	}

	private Todo todo(String[] args) {
    	return new Todo(args[0], args[1], Boolean.valueOf(args[2]));
	}

	private String[] tail(String[] cmdParts) {
    	return Arrays.copyOfRange(cmdParts, 1, cmdParts.length);
	}

	private enum Result {CONTINUE, BREAK}
}

The following three lines in the run() method are of interest to us:

ServiceBuilder builder = new ServiceBuilder("http://localhost:8080");
Service todosService = builder.build();
todos = todosService.produceClient(Todos.class);

In these lines we create an instance of a class that implements the Todos interface that we can use later to perform todos list operations.

Build

In the client build file, the required dependencies are specified and the application plugin is applied, so thet the client can be packaged and run as application.

apply plugin: 'application'

mainClassName = 'org.restler.todo.client.TodoClient'

dependencies {
	compile 'org.restler:restler-spring-mvc:0.4.0',
                'com.fasterxml.jackson.module:jackson-module-paranamer:2.6.1',
                 project(':api')
}

Running the app

First, we need to build the project:

./gradlew build

Then we need to start the server:

./gradlew :server:run

Running the client is a little bit more complicated, because when Gradle starts an application, there is no console present, and we have to run it manually. But thanks to the application plugin we only need to unzip the created archive and run a script:

unzip client/build/distributions/client-0.1.0-SNAPSHOT.zip
client-0.1.0-SNAPSHOT/bin/client