A single cohesive framework consisting of the following parts:

Questions via email to dev@juneau.apache.org are always welcome.

Juneau is packed with features that may not be obvious at first. Users are encouraged to ask for code reviews by providing links to specific source files such as through GitHub. Not only can we help you with feedback, but it helps us understand usage patterns to further improve the product.


We've strived to keep prerequisites to an absolute minimum.


The library consists of 4 components/jars, and a single uber-jar that contains everything.

Juneau Core

Core library includes easy-to-use and customizable serializers and parsers. The examples here are only a small taste of what's possible.

The default serializers can often be used to serialize POJOs in a single line of code:

// A simple bean public class Person { public String name = "John Smith"; public int age = 21; } // Serialize a bean to JSON, XML, or HTML Person p = new Person(); // Produces: // "{name:'John Smith',age:21}" String laxJson = JsonSerializer.DEFAULT_LAX.serialize(p); // Produces: // "{"name":"John Smith","age":21}" String strictJson = JsonSerializer.DEFAULT.serialize(p); // Produces: // <object> // <name>John Smith</name> // <age>21</age> // </object> String xml = XmlSerializer.DEFAULT_SIMPLE.serialize(p); // Produces: // <table> // <tr><td>name</td><td>John Smith</td></tr> // <tr><td>age</td><td>21</td></tr> // </table> String html = HtmlSerializer.DEFAULT.serialize(p); // Same as Html, but wraps it in HTML and BODY elements with page title/description/links: String htmlDoc = HtmlDocSerializer.DEFAULT.serialize(p); // Produces: // name='John+Smith'&age=21 String urlEncoding = UrlEncodingSerializer.DEFAULT.serialize(p); // Produces: // (name='John Smith',age=21) String uon = UonSerializer.DEFAULT.serialize(p); // Produces: // 82 A4 name AA 4A John Smith 68 A3 age 15 byte[] messagePack = MsgPackSerializer.DEFAULT.serialize(p); // Produces: // <rdf:RDF // xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" // xmlns:jp="http://www.apache.org/juneaubp/" // xmlns:j="http://www.apache.org/juneau/"> // <rdf:Description> // <jp:name>John Smith</jp:name> // <jp:age>21</jp:age> // </rdf:Description> // </rdf:RDF> String rdfXml = RdfSerializer.DEFAULT_XMLABBREV.serialize(p); // Produces: // @prefix jp: <http://www.apache.org/juneaubp/> . // @prefix j: <http://www.apache.org/juneau/> . // [] jp:age "21" ; // jp:name "John Smith" . String rdfN3 = RdfSerializer.DEFAULT_N3.serialize(p); // Produces: // _:A3bf53c85X3aX157cf407e2dX3aXX2dX7ffd <http://www.apache.org/juneaubp/name> "John Smith" . // _:A3bf53c85X3aX157cf407e2dX3aXX2dX7ffd <http://www.apache.org/juneaubp/age> "21" . String rdfNTriple = RdfSerializer.DEFAULT_NTRIPLE.serialize(p);

Parsing back into POJOs is equally simple for any of the supported languages shown above (JSON shown here):

// Use one of the predefined parsers. ReaderParser parser = JsonParser.DEFAULT; // Parse a JSON object (creates a generic ObjectMap). String json = "{name:'John Smith',age:21}"; Map m1 = parser.parse(json, Map.class); // Parse a JSON string. json = "'foobar'"; String s2 = parser.parse(json, String.class); // Parse a JSON number as a Long or Float. json = "123"; Long l3 = parser.parse(json, Long.class); Float f3 = parser.parse(json, Float.class); // Parse a JSON object as a bean. json = "{name:'John Smith',age:21}"; Person p4 = parser.parse(json, Person.class); // Parse a JSON object as a HashMap<String,Person>. json = "{a:{name:'John Smith',age:21},b:{name:'Joe Smith',age:42}}"; Map<String,Person> m5 = parser.parse(json, HashMap.class, String.class, Person.class); // Parse a JSON object as a HashMap<String,LinkedList<Person>>. json = "{a:[{name:'John Smith',age:21},{name:'Joe Smith',age:42}]}"; Map<String,List<Person>> m6 = parser.parse(json, HashMap.class, String.class, LinkedList.class, Person.class); // Parse a JSON array of integers as a Collection of Integers or int[] array. json = "[1,2,3]"; List<Integer> l7 = parser.parse(json, LinkedList.class, Integer.class); int[] i7 = parser.parse(json, int[].class); // Parse arbitrary input into ObjectMap or ObjectList objects // (similar to JSONObject/JSONArray but generalized for all languages). json = "{name:'John Smith',age:21}"; ObjectMap m8a = parser.parse(json, ObjectMap.class); int age = m8a.getInt("age"); ObjectMap m8b = (ObjectMap)parser.parse(json, Object.class); // Equivalent. json = "[1,true,null]"; ObjectList l9a = parser.parse(json, ObjectList.class); boolean b = l9a.getBoolean(1); ObjectList l9b = (ObjectList)parser.parse(json, Object.class); // Equivalent.

Serializers can send output directly to Writers, OutputStreams, Files, Strings, or byte arrays.
Parsers can receive input directly from Readers, InputStreams, Files, Strings, or byte arrays.

Serializers and parsers are builder-based. Build from scratch or clone existing instances. Lots of configuration options available for all the languages.

// Create a serializer from scratch using a builder JsonSerializer serializer = new JsonSerializerBuilder() .simple() // Simple mode .sq() // Use single quotes .pojoSwaps( // Swap unserializable classes with surrogate POJOs IteratorSwap.class, ByteArrayBase64Swap.class, CalendarSwap.ISO8601DT.class, ) .beanFilters(MyBeanFilter.class) // Control how bean properties are handled .timeZone(TimeZone.GMT) // For serializing Calendars .locale(Locale.JAPAN) // For timezone-specific serialization .sortCollections(true) // For locale-specific serialization .sortProperties(true) // Various behavior settings .trimNullProperties(true) .trimStrings(true) .methodVisibility(PROTECTED) // Control which fields/methods are serialized .beanDictionary( // Adds type variables for resolution during parsing MyBeanA.class, MyBeanB.class ) .debug(true) // Add debug output .build(); // Clone an existing serializer and modify it to use single-quotes JsonSerializer serializer = JsonSerializer.DEFAULT.builder() .sq() .build();

Many POJOs such as primitives, beans, collections, arrays, and classes with various known constructors and methods are serializable out-of-the-box. For other objects, "swaps" allow you to swap-in serializable replacement objects during serialization and vis-versa during parsing. Beans can also be tailored in various ways such as customizing property names, ordering, and visibility.

Additional Information

UON (URL-Encoded Object Notation) allows JSON-like data structures (OBJECT, ARRAY, NUMBER, BOOLEAN, STRING, NULL) in HTTP constructs (query parameters, form parameters, headers, URL parts) without violating RFC2396. This allows POJOs to be converted directly into HTTP constructs.

( id=1, name='John+Smith', uri=http://sample/addressBook/person/1, addressBookUri=http://sample/addressBook, birthDate=1946-08-12T00:00:00Z, addresses=@( ( uri=http://sample/addressBook/address/1, personUri=http://sample/addressBook/person/1, id=1, street='100+Main+Street', city=Anywhereville, state=NY, zip=12345, isCurrent=true ) ) )

Lots of shortcuts are provided throughout the API to simplify tasks, and the APIs are often useful for debugging and logging purposes as well...

// Create JSON strings from scratch using fluent-style code. String jsonObject = new ObjectMap().append("foo","bar").toString(); String jsonArray = new ObjectList().append("foo").append(123).append(null).toString(); // Create maps and beans directly from JSON. Map<String,Object> myMap = new ObjectMap("{foo:'bar'}"); List<Object> myList = new ObjectList("['foo',123,null]"); // Load a POJO from a JSON file. MyPojo myPojo = JsonParser.DEFAULT.parse(new File("myPojo.json")); // Serialize POJOs and ignore exceptions (great for logging) String json = JsonSerializer.DEFAULT_LAX.toString(myPojo); // Dump a POJO to the console. JsonSerializer.DEFAULT_LAX.println(myPojo); // Delayed serialization. // (e.g. don't serialize an object if it's not going to be logged). logger.log(FINE, "My POJO was: {0}", new StringObject(myPojo)); logger.log(FINE, "My POJO in XML was: {0}", new StringObject(XmlSerializer.DEFAULT, myPojo)); String message = new StringMessage("My POJO in {0}: {1}", "JSON", new StringObject(myPojo)).toString(); // Create a 'REST' wrapper around a POJO. PojoRest pojoRest = new PojoRest(myPojo); pojoRest.get(String.class, "addressBook/0/name"); pojoRest.put("addressBook/0/name", "John Smith");

SerializerGroup and ParserGroup classes allow serializers and parsers to be retrieved by W3C-compliant HTTP Accept and Content-Type values:

// Construct a new serializer group with configuration parameters that get applied to all serializers. SerializerGroup sg = new SerializerGroupBuilder() .append(JsonSerializer.class, UrlEncodingSerializer.class); .ws() // or .setUseWhitespace(true) .pojoSwaps(CalendarSwap.ISO8601DT.class) .build(); // Find the appropriate serializer by Accept type and serialize our POJO to the specified writer. sg.getSerializer("text/invalid, text/json;q=0.8, text/*;q:0.6, *\/*;q=0.0") .serialize(myPersonObject, myWriter); // Construct a new parser group with configuration parameters that get applied to all parsers. ParserGroup pg = new ParserGroupBuilder() .append(JsonParser.class, UrlEncodingParser.class); .pojoSwaps(CalendarSwap.ISO8601DT.class) .build(); Person p = pg.getParser("text/json").parse(myReader, Person.class);

Additional Information

DTO libraries

Data Transfer Object libraries are provided for a variety of languages that allow you to serialize commonly-used documents.

HTML5 documents and fragments can be constructed using the HTML5 DTOs and HTML or XML serializers:

import static org.apache.juneau.dto.html5.HtmlBuilder.*; Object myform = form().action("/submit").method("POST") .children( "Position (1-10000): ", input("number").name("pos").value(1), br(), "Limit (1-10000): ", input("number").name("limit").value(100), br(), button("submit", "Submit"), button("reset", "Reset") ); String html = HtmlSerializer.DEFAULT.serialize(myform);

<form action='/submit' method='POST'> Position (1-10000): <input name='pos' type='number' value='1'/><br/> Limit (1-10000): <input name='pos' type='number' value='100'/><br/> <button type='submit'>Submit</button> <button type='reset'>Reset</button> </form>

ATOM feeds can be constructed using the ATOM DTOs and XML serializer:

import static org.apache.juneau.dto.atom.AtomBuilder.*; Feed feed = feed("tag:juneau.apache.org", "Juneau ATOM specification", "2016-01-02T03:04:05Z") .subtitle(text("html").text("Describes <em>stuff</em> about Juneau")) .links( link("alternate", "text/html", "http://juneau.apache.org/").hreflang("en"), link("self", "application/atom+xml", "http://juneau.apache.org/feed.atom") ) .rights("Copyright (c) 2016, Apache Foundation") .entries( entry("tag:juneau.sample.com,2013:1.2345", "Juneau ATOM specification snapshot", "2016-01-02T03:04:05Z") .published("2016-01-02T03:04:05Z") .content( content("xhtml") .lang("en") .base("http://www.apache.org/") .text("<div><p><i>[Update: Juneau supports ATOM.]</i></p></div>") ) ); // Serialize to ATOM/XML String atomXml = XmlSerializer.DEFAULT.serialize(feed);

<feed> <id> tag:juneau.apache.org </id> <link href='http://juneau.apache.org/' rel='alternate' type='text/html' hreflang='en'/> <link href='http://juneau.apache.org/feed.atom' rel='self' type='application/atom+xml'/> <rights> Copyright (c) 2016, Apache Foundation </rights> <title type='text'> Juneau ATOM specification </title> <updated>2016-01-02T03:04:05Z</updated> <subtitle type='html'> Describes <em>stuff</em> about Juneau </subtitle> <entry> <id> tag:juneau.apache.org </id> <title> Juneau ATOM specification snapshot </title> <updated>2016-01-02T03:04:05Z</updated> <content base='http://www.apache.org/' lang='en' type='xhtml'> <div xmlns="http://www.w3.org/1999/xhtml"><p><i>[Update: Juneau supports ATOM.]</i></p></div> </content> <published>2016-01-02T03:04:05Z</published> </entry> </feed>

Swagger documents can be constructed using the Swagger DTOs and JSON serializer:

import static org.apache.juneau.dto.swagger.SwaggerBuilder.*; Swagger swagger = swagger() .swagger("2.0") .info( info("Swagger Petstore", "1.0.0") .description("This is a sample server Petstore server.") .termsOfService("http://swagger.io/terms/") .contact( contact().email("apiteam@swagger.io") ) .license( license("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html") ) ) .path("/pet", "post", operation() .tags("pet") .summary("Add a new pet to the store") .description("") .operationId("addPet") .consumes(MediaType.JSON, MediaType.XML) .produces(MediaType.JSON, MediaType.XML) .parameters( parameterInfo("body", "body") .description("Pet object that needs to be added to the store") .required(true) ) .response(405, responseInfo("Invalid input")) ); // Serialize to Swagger/JSON String swaggerJson = JsonSerializer.DEFAULT_READABLE.serialize(swagger);

{ "swagger": "2.0", "info": { "title": "Swagger Petstore", "description": "This is a sample server Petstore server.", "version": "1.0.0", "termsOfService": "http://swagger.io/terms/", "contact": { "email": "apiteam@swagger.io" }, "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" } }, "paths": { "/pet": { "post": { "tags": [ "pet" ], "summary": "Add a new pet to the store", "description": "", "operationId": "addPet", "consumes": [ "application/json", "text/xml" ], "produces": [ "application/json", "text/xml" ], "parameters": [ { "in": "body", "name": "body", "description": "Pet object that needs to be added to the store", "required": true } ], "responses": { "405": { "description": "Invalid input" } } } } }, }

Note that these DTOs can also be serialized to any of the other supported languages such as JSON or MessagePack! And they can be parsed back into their original objects!

Additional Information

Juneau Server

The REST server API builds upon the SerializerGroup and ParserGroup classes to provide annotated REST servlets that automatically negotiate the HTTP media types and allow the developer to work with requests, responses, headers, path variables, query parameters, and form data as POJOs.

The end goal is to provide simple and flexible yet sophisticated REST interfaces that allow POJOs to be automatically represented as different content types depending on whatever the particular need:

A simple example that supports all languages:

@RestResource( path="/systemProperties", title="System properties resource" ) public class SystemPropertiesResource extends RestServletDefault { @RestMethod(name="GET", path="/") public Map getSystemProperties(@Query("sort") boolean sort) throws Throwable { if (sort) return new TreeMap(System.getProperties()); return System.getProperties(); } @RestMethod(name="GET", path="/{propertyName}") public String getSystemProperty(@Path String propertyName) throws Throwable { return System.getProperty(propertyName); } @RestMethod(name="PUT", path="/{propertyName}", guards=AdminGuard.class) public String setSystemProperty(@Path String propertyName, @Body String value) { System.setProperty(propertyName, value); return "OK"; } @RestMethod(name="POST", path="/", guards=AdminGuard.class) public String setSystemProperties(@Body java.util.Properties newProperties) { System.setProperties(newProperties); return "OK"; } @RestMethod(name="DELETE", path="/{propertyName}", guards=AdminGuard.class) public String deleteSystemProperty(@Path String propertyName) { System.clearProperty(propertyName); return "OK"; } }

A more sophisticated example of the same resource using various features, including information for fully populating the Swagger documentation, guards for restricting access to particular methods, customizing supported content types and serialization options, adding g-zip compression, and adding customized branding for the HTML views.

@RestResource( path="/systemProperties", title="System properties resource", description="REST interface for performing CRUD operations on system properties.", // Add links to HTML view of page. // "request:/..." URIs are relative to the request URI // "servlet:/..." URIs are relative to the servlet URI pageLinks="{up:'request:/..',options:'servlet:/?method=OPTIONS'}", // Specify your own sets of serializers and parsers for this resource. serializers={ HtmlDocSerializer.class, JsonSerializer.class, JsonSerializer.Simple.class, XmlDocSerializer.class, MsgPackSerializer.class }, parsers={ JsonParser.class, XmlParser.class, HtmlParser.class, MsgPackParser.class }, // Set serializer, parser, and REST context properties. properties={ @Property(name=SERIALIZER_quoteChar, value="'") }, // Set your own look-and-feel for the HTML view. stylesheet="styles/devops.css", // Add compression support. encoders=GzipEncoder.class, // Augment Swagger information. contact="{name:'John Smith',email:'john@smith.com'}", license="{name:'Apache 2.0',url:'http://www.apache.org/licenses/LICENSE-2.0.html'}", version="2.0", termsOfService="You're on your own.", tags="[{name:'Java',description:'Java utility',externalDocs:{description:'Home page',url:'http://juneau.apache.org'}}]", externalDocs="{description:'Home page',url:'http://juneau.apache.org'}" ) public class SystemPropertiesResource extends RestServlet { @RestMethod( name="GET", path="/", summary="Show all system properties", description="Returns all system properties defined in the JVM.", // Augment Swagger information. parameters={ @Parameter(in="query", name="sort", description="Sort results alphabetically.", _default="false") }, responses={ @Response(value=200, description="Returns a map of key/value pairs.") } ) public Map getSystemProperties(@Query("sort") boolean sort) throws Throwable { if (sort) return new TreeMap(System.getProperties()); return System.getProperties(); } ... }

When combined with the support for HTML5 beans, simple HTML forms can be constructed for easy input and output using nothing more than Java:

import static org.apache.juneau.dto.html5.HtmlBuilder.*; @RestMethod( name="GET", path="/formPage", summary="Form entry page", description="A form post page for setting a single system property value.", guards=AdminGuard.class ) public Form getFormPage() { return form().method("POST").action("formPagePost").children( h4("Set system property"), "Name: ", input("text").name("name"), br(), "Value: ", input("text").name("value"), br(), br(), button("submit","Click me!").style("float:right") ); } @RestMethod( name="POST", path="/formPagePost", description="Accepts a simple form post of a system property name/value pair.", guards=AdminGuard.class ) public Redirect formPagePost(@FormData("name") String name, @FormData("value") String value) { System.setProperty(name, value); return new Redirect(); // Redirect to the servlet top page. }

The REST API is built on top of Servlets, making them easy to deploy in any JEE environment.

REST Java methods can return any of the following objects:
POJOs, Readers, InputStreams, ZipFiles, Redirects, Streamables, and Writables.

Or add your own handlers for other types.

REST Java methods can be passed any of the following objects in any order:

It's up to you how you want to define your REST methods. As a general rule, there are 3 broad approaches typically used:

Methodology #1 - Annoted parameters

This approach uses annotated parameters for retrieving input from the request.

@RestMethod(name="GET", path="/example1/{p1}/{p2}/{p3}/*") public String example1( @Method String method, // HTTP method. @Path String p1, // Path variables. @Path int p2, @Path UUID p3, @Query("q1") int q1, // Query parameters. @Query("q2") String q2, @Query("q3") UUID q3, @PathRemainder String remainder, // Path remainder after pattern match. @Header("Accept-Language") String lang, // Headers. @Header("Accept") String accept, @Header("DNT") int doNotTrack ) { // Send back a simple String response String output = String.format( "method=%s, p1=%s, p2=%d, p3=%s, remainder=%s, q1=%d, q2=%s, q3=%s, lang=%s, accept=%s, dnt=%d", method, p1, p2, p3, remainder, q1, q2, q3, lang, accept, doNotTrack); return output; }

Methodology #2 - Low-level request/response objects

This approach uses low-level request/response objects to perform the same as above.

@RestMethod(name="GET", path="/example2/{p1}/{p2}/{p3}/*") public String example2( RestRequest req, // A direct subclass of HttpServletRequest. RestResponse res // A direct subclass of HttpServletResponse. ) { // HTTP method. String method = req.getMethod(); // Path variables. RequestPathMatch path = req.getPathMatch(); String p1 = path.get("p1", String.class); int p2 = path.get("p2", int.class); UUID p3 = path.get("p3", UUID.class); // Query parameters. RequestQuery query = req.getQuery(); int q1 = query.get("q1", 0, int.class); String q2 = query.get("q2", String.class); UUID q3 = query.get("q3", UUID.class); // Path remainder after pattern match. String remainder = req.getPathMatch().getRemainder(); // Headers. String lang = req.getHeader("Accept-Language"); String accept = req.getHeader("Accept"); int doNotTrack = req.getHeaders().get("DNT", int.class); // Send back a simple String response String output = String.format( "method=%s, p1=%s, p2=%d, p3=%s, remainder=%s, q1=%d, q2=%s, q3=%s, lang=%s, accept=%s, dnt=%d", method, p1, p2, p3, remainder, q1, q2, q3, lang, accept, doNotTrack); res.setOutput(output); // Or use getWriter(). }

Methodology #3 - Intermediate-level API objects

This approach is sort of the middle ground where you get access functional area APIs.

@RestMethod(name="GET", path="/example3/{p1}/{p2}/{p3}/*") public String example3( HttpMethod method, // HTTP method. RequestPathMatch path, // Path variables. RequestQuery query, // Query parameters. RequestHeaders headers, // Headers. AcceptLanguage lang, // Specific header classes. Accept accept ) { // Path variables. String p1 = path.get("p1", String.class); int p2 = path.get("p2", int.class); UUID p3 = path.get("p3", UUID.class); // Query parameters. int q1 = query.get("q1", 0, int.class); String q2 = query.get("q2", String.class); UUID q3 = query.get("q3", UUID.class); // Path remainder after pattern match. String remainder = path.getRemainder(); // Headers. int doNotTrack = headers.get("DNT", int.class); // Send back a simple String response String output = String.format( "method=%s, p1=%s, p2=%d, p3=%s, remainder=%s, q1=%d, q2=%s, q3=%s, lang=%s, accept=%s, dnt=%d", method, p1, p2, p3, remainder, q1, q2, q3, lang, accept, doNotTrack); res.setOutput(output); }

All three are completely equivalent. It's up to your own coding preferences which methodology you use.

Auto-generated OPTIONS pages are constructed from Swagger DTO beans, here shown serialized as HTML:

Swagger documentation can be populated from annotations (as above), resource bundles, or Swagger JSON files.

The page shown above is implemented on the RestServletDefault class in the method below which shows that it's doing nothing more than serializing a Swagger bean which is constructed in the RestRequest object:

@RestMethod(name="OPTIONS", path="/*") public Swagger getOptions(RestRequest req) { return req.getSwagger(); }

Navigatable hierarchies of REST resources are easy to set up either programmatically or through annotations:

@RestResource( path="/", title="Root resources", description="This is an example of a router resource that is used to access other resources.", pageLinks="{options:'?method=OPTIONS',source:'$C{Source/gitHub}/org/apache/juneau/rest/example/RootResources.java'}", children={ HelloWorldResource.class, SystemPropertiesResource.class, MethodExampleResource.class, RequestEchoResource.class, TempDirResource.class, AddressBookResource.class, SampleRemoteableServlet.class, PhotosResource.class, AtomFeedResource.class, JsonSchemaResource.class, SqlQueryResource.class, TumblrParserResource.class, CodeFormatterResource.class, UrlEncodedFormResource.class, ConfigResource.class, LogsResource.class, DockerRegistryResource.class, ShutdownResource.class } ) public class RootResources extends RestServletGroupDefault { /* No code needed! */ }

The above resource when rendered in HTML shows how easy it is to discover and navigate to child resources using a browser:

Resources can be nested arbitrarily deep. The @RestResource and @RestMethod annotations can be applied to any classes, not just servlets. The only requirement is that the top-level resource be a subclass of RestServlet as a hook into the servlet container.

Automatic error handling is provided for a variety of conditions:

Other features include:

Additional Information

Juneau Client

The REST client API allows you to access REST interfaces using POJOs as well:

// Create a reusable JSON client. RestClient client = new RestClientBuilder().build(); // The address of the root resource. String url = "http://localhost:10000/systemProperties"; // Do a REST GET against a remote REST interface and convert // the response to an unstructured ObjectMap object. Map m1 = client.doGet(url).getResponse(TreeMap.class); // Add some new system properties. // Use XML as the transport medium. client = new RestClientBuilder(XmlSerializer.class, XmlParser.class).build(); Properties p = new Properties(); p.load(reader); int returnCode = client.doPost(url + "/systemProperties", p).execute();

The client API uses the same serializers and parsers (and subsequently their flexibilty and configurability) as the server side to marshall POJOs back and forth.

The remote proxy interface API allows you to invoke server-side POJO methods on the client side using REST (i.e. RPC over REST):

// Get an interface proxy. IAddressBook ab = restClient.getRemoteableProxy(IAddressBook.class); // Invoke a method on the server side and get the returned result. Person p = ab.createPerson( new Person( "John Smith", "Aug 1, 1999", new Address("My street", "My city", "My state", 12345, true) ) );

There are two ways to expose remoteable proxies on the server side:

  1. Extending from RemoteableServlet.
  2. Using a @RestMethod(name="PROXY") annotation on a Java method.

The RemoteableServlet class is a simple specialized servlet with an abstract getServiceMap() method to define the server-side POJOs:

@RestResource( path="/remote" ) public class SampleRemoteableServlet extends RemoteableServlet { // Our server-side POJO. AddressBook addressBook = new AddressBook(); @Override /* RemoteableServlet */ protected Map<Class<?>,Object> getServiceMap() throws Exception { Map<Class<?>,Object> m = new LinkedHashMap<Class<?>,Object>(); // In this simplified example, we expose the same POJO service under two different interfaces. // One is IAddressBook which only exposes methods defined on that interface, and // the other is AddressBook itself which exposes all methods defined on the class itself (dangerous!). m.put(IAddressBook.class, addressBook); m.put(AddressBook.class, addressBook); return m; } }

The @RestMethod(name="PROXY") approach is easier if you only have a single interface you want to expose. You simply define a Java method whose return type is an interface, and return the implementation of that interface:

// Our exposed proxy object. @RestMethod(name="PROXY", path="/addressbookproxy/*") public IAddressBook getProxy() { return addressBook; }

In either case, the proxy communications layer is pure REST. Parameters passed in on the client side are serialized as an HTTP POST, parsed on the server side, and then passed to the invocation method. The returned POJO is then marshalled back as an HTTP response.

In most cases, you'll want to use JSON or MessagePack as your communications layer since these are the most efficent. Although remoteable proxies work perfectly well for any of the other supported languages. For example, RPC over Turtle!

The parameters and return types of the Java methods can be any of the supported serializable and parsable types in POJO Categories. This ends up being WAY more flexible than other proxy interfaces since Juneau can handle so may POJO types out-of-the-box. Most of the time you don't even need to modify your existing Java implementation code.

The RemoteableServlet class itself shows how sophisticated REST interfaces can be built on the Juneau RestServlet API using very little code. The RemoteableServlet class itself consists of only 53 lines of code, yet is a sophisticated discoverable and self-documenting REST interface. And since the remote proxy API is built on top of REST, it can be debugged using just a browser.

Remoteable proxies can also be used to define interface proxies against 3rd-party REST interfaces. This is an extremely powerful feature that allows you to quickly define easy-to-use interfaces against virtually any REST interface.

Similar in concept to remoteable services defined above, but in this case we simply define our interface with special annotations that tell us how to convert input and output to HTTP headers, query paramters, form post parameters, or request/response bodies.

@Remoteable public interface MyProxyInterface { @RemoteMethod(httpMethod="POST", path="/method") String doMethod(@Header("E-Tag") UUID etag, @Query("debug") boolean debug, @Body MyPojo pojo); } RestClient client = new RestClientBuilder().build(); MyProxyInterface p = client.getRemoteableProxy(MyProxyInterface.class, "http://hostname/some/rest/interface"); String response = p.doMethod(UUID.generate(), true, new MyPojo());

Additional Information


The config file API allows you to interact with INI files using POJOs. A sophisticated variable language is provided for referencing environment variables, system properties, other config file entries, and a host of other types.

#-------------------------- # My section #-------------------------- [MySection] # An integer anInt = 1 # A boolean aBoolean = true # An int array anIntArray = [1,2,3] # A POJO that can be converted from a String aURL = http://foo # A POJO that can be converted from JSON aBean = {foo:'bar',baz:123} # A system property locale = $S{java.locale, en_US} # An environment variable path = $E{PATH, unknown} # A manifest file entry mainClass = $MF{Main-Class} # Another value in this config file sameAsAnInt = $C{MySection/anInt} # A command-line argument in the form "myarg=foo" myArg = $ARG{myarg} # The first command-line argument firstArg = $ARG{0} # Look for system property, or env var if that doesn't exist, or command-line arg if that doesn't exist. nested = $S{mySystemProperty,$E{MY_ENV_VAR,$ARG{0}}} # A POJO with embedded variables aBean2 = {foo:'$ARG{0}',baz:$C{MySection/anInt}}

You're probably wondering "why INI files?" The beauty of these INI files is that they're easy to read and modify, yet sophisticated enough to allow you to store arbitrary-complex data structures and retrieve them as simple values or complex POJOs:

// Load our config file ConfigFile f = new ConfigFileBuilder().build("MyIniFile.cfg"); int anInt = cf.getInt("MySection/anInt"); boolean aBoolean = cf.getBoolean("MySection/aBoolean"); int[] anIntArray = cf.getObject(int[].class, "MySection/anIntArray"); URL aURL = cf.getObject(URL.class, "MySection/aURL"); MyBean aBean = cf.getObject(MyBean.class, "MySection/aBean"); Locale locale = cf.getObject(Locale.class, "MySection/locale"); String path = cf.getString("MySection/path"); String mainClass = cf.getString("MySection/mainClass"); int sameAsAnInt = cf.getInt("MySection/sameAsAnInt"); String myArg = cf.getString("MySection/myArg"); String firstArg = cf.getString("MySection/firstArg");

Values are LAX JSON (i.e. unquoted attributes, single quotes) except for top-level strings which are left unquoted. Any parsable object types are supported as values (e.g. arrays, collections, beans, swappable objects, enums, etc...).

One of the more powerful aspects of the REST servlets is that you can pull values directly from config files by using the "$C" variable in annotations.
For example, the HTML stylesheet for your REST servlet can be defined in a config file like so:

@RestResource( path="/myResource", config="$S{my.config.file}", // Path to config file (here pulled from a system property) stylesheet="$C{MyResourceSettings/myStylesheet}" // Stylesheet location pulled from config file. ) public class MyResource extends RestServlet {

Other features:

Additional Information

Juneau Microservice

The microservice API combines all the features above with a built-in Jetty server to produce a lightweight REST service packaged as two simple files:

The microservice API was originally designed for and particularly suited for use in Docker containers.

REST microservices can also be started programmatically in existing code:

RestMicroservice myRestService = new RestMicroservice().setConfig("microservice.cfg", false); myRestService.start(); URI uri = myRestService.getURI();

The provided microservice.cfg template file gives you a starting point for defining your microservice:

#================================================================================ # Basic configuration file for SaaS microservices # Subprojects can use this as a starting point. #================================================================================ #================================================================================ # REST settings #================================================================================ [REST] # The HTTP port number to use. # Can be a comma-delimited list of ports to try. # 0 means try a random port. # Default is Rest-Port setting in manifest file, or 8000. # In this case, try port 10000, then try 3 random ports. port = [10000, 0, 0, 0] # A JSON map of servlet paths to servlet classes. # Example: # resourceMap = {'/*':'com.foo.MyServlet'} # Either resourceMap or resources must be specified. resourceMap = # A comma-delimited list of names of classes that extend from Servlet. # Resource paths are pulled from @RestResource.path() annotation, or # "/*" if annotation not specified. # Example: # resources = com.foo.MyServlet # Default is Rest-Resources in manifest file. # Either resourceMap or resources must be specified. resources = # The context root of the Jetty server. # Default is Rest-ContextPath in manifest file, or "/". contextPath = # Authentication: NONE, BASIC. authType = NONE # The BASIC auth username. # Default is Rest-LoginUser in manifest file. loginUser = # The BASIC auth password. # Default is Rest-LoginPassword in manifest file. loginPassword = # The BASIC auth realm. # Default is Rest-AuthRealm in manifest file. authRealm = # Stylesheet to use for HTML views. # The default options are: # - styles/juneau.css # - styles/devops.css # Other stylesheets can be referenced relative to the servlet package or working # directory. stylesheet = styles/devops.css # What to do when the config file is saved. # Possible values: # NOTHING - Don't do anything. # RESTART_SERVER - Restart the Jetty server. # RESTART_SERVICE - Shutdown and exit with code '3'. saveConfigAction = RESTART_SERVER # Enable SSL support. useSsl = true #================================================================================ # Bean properties on the org.eclipse.jetty.util.ssl.SslSocketFactory class #-------------------------------------------------------------------------------- # Ignored if REST/useSsl is false. #================================================================================ [REST-SslContextFactory] keyStorePath = client_keystore.jks keyStorePassword* = {HRAaRQoT} excludeCipherSuites = TLS_DHE.*, TLS_EDH.* excludeProtocols = SSLv3 allowRenegotiate = false #================================================================================ # Logger settings # See FileHandler Java class for details. #================================================================================ [Logging] # The directory where to create the log file. # Default is "." logDir = logs # The name of the log file to create for the main logger. # The logDir and logFile make up the pattern that's passed to the FileHandler # constructor. # If value is not specified, then logging to a file will not be set up. logFile = microservice.%g.log # Whether to append to the existing log file or create a new one. # Default is false. append = # The SimpleDateFormat format to use for dates. # Default is "yyyy.MM.dd hh:mm:ss". dateFormat = # The log message format. # The value can contain any of the following variables: # {date} - The date, formatted per dateFormat. # {class} - The class name. # {method} - The method name. # {logger} - The logger name. # {level} - The log level name. # {msg} - The log message. # {threadid} - The thread ID. # {exception} - The localized exception message. # Default is "[{date} {level}] {msg}%n". format = # The maximum log file size. # Suffixes available for numbers. # See ConfigFile.getInt(String,int) for details. # Default is 1M. limit = 10M # Max number of log files. # Default is 1. count = 5 # Default log levels. # Keys are logger names. # Values are serialized Level POJOs. levels = { org.apache.juneau:'INFO' } # Only print unique stack traces once and then refer to them by a simple 8 character hash identifier. # Useful for preventing log files from filling up with duplicate stack traces. # Default is false. useStackTraceHashes = true # The default level for the console logger. # Default is WARNING. consoleLevel = #================================================================================ # System properties #-------------------------------------------------------------------------------- # These are arbitrary system properties that are set during startup. #================================================================================ [SystemProperties] # Configure Jetty for StdErrLog Logging org.eclipse.jetty.util.log.class = org.eclipse.jetty.util.log.StrErrLog # Jetty logging level org.eclipse.jetty.LEVEL = WARN

Various predefined reusable REST resource classes are provided for accessing log files, viewing and editing the config file, etc... These allow you to quickly put together sophisticated REST microservices.

Additional Information

More information

*ALL* user documentation for this project is maintained in its Javadocs. If it's not in the Javadocs, it doesn't exist.

The top-level Overview document is a good starting point for developers wishing to get up-to-speed quickly.

*** fín ***