Raviolini is a lightweight framework for building RESTful Web services, in Java 8. You simply provide your domain objects and, out of the box, Raviolini gives you authentication, caching, configuration, logging, persistence, serialization and validation on those objects. Moreover, Raviolini gives you pre- and post-execution hooks that let you extend Raviolini beyond the scope of CRUD (Create, Read, Update, Delete) operations.
Raviolini is built on top of Spark (version 2.2) and ORM Lite (version 4.48). The former provides a lightweight HTTP foundation, whereas the latter provides a database abstraction layer and simple object-relational mapping capability.
There are only two simple steps for you to install Raviolini in your project.
First, add the JitPack repository to your build file (pom.xml):
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
Second, add Raviolini as a dependency:
<dependencies>
<dependency>
<groupId>com.github.otaviofff</groupId>
<artifactId>raviolini</artifactId>
<version>2.0.1/version>
</dependency>
</dependencies>
The above code assumes you use Maven to build your project. However, if you use Gradle, Sbt or Leiningen, then you should grab your code from Raviolini on JitPack.
First, you need to code the resource you want to expose through your API. In this sample, our domain object is named Dog
, and is defined by three simple attributes, namely id
, name
and neutered
. Please note the @JsonIgnore
annotation from Jackson, as well as the @DatabaseTable
and @DatabaseField
annotations from ORM Lite. Also note that Dog
must implement the Entity
interface, provided by Raviolini itself.
package org.muttie.domain;
import org.raviolini.domain.Entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@DatabaseTable(tableName = "dog")
public class Dog implements Entity {
@DatabaseField(generatedId = true)
private Integer id;
@DatabaseField
private String name;
@DatabaseField
private Boolean neutered;
public Dog() {}
@Override
public Integer getId() {
return id;
}
@JsonIgnore
@Override
public Boolean isValid() {
return (neutered != null && name != null && !name.isEmpty());
}
}
Second, you need to code a FrontController
to your API, which will hold your main
function. This controller will instantiate Application
, and add an HTTP router for the domain object you coded in the previous step. This router defines all valid URIs that will compose your final RESTful interface. Also note that Application
can take a boolean parameter through its constructor. When set to true
, this parameter will make your Application
listen to the HTTP port assigned to it by your environment (such as Heroku).
package org.muttie.api;
import org.muttie.domain.Dog;
import org.raviolini.api.Application;
public class FrontController {
public static void main(String[] args) {
Application app = new Application(true);
app.addRouter(Dog.class);
}
}
Finally, you just need to create a configuration file (named application.properties
) in order to setup your application, database, cache, and authentication strategy. Optionally, you may also create another config file (named logging.properties
) to setup you logging preferences. Raviolini comes with sample config files to help you out.
####################
# Application
####################
application.name = ${pom.name}
application.version = ${pom.version}
####################
# Database
####################
raviolini.database.driver = relational
raviolini.database.engine = postgresql
raviolini.database.host = localhost
raviolini.database.port = 15432
raviolini.database.name = db_name
raviolini.database.user = db_user
raviolini.database.pass = db_pass
####################
# Cache
####################
raviolini.cache.driver = redis
raviolini.cache.host = localhost
raviolini.cache.port = 16379
raviolini.cache.pass = cache_pass
####################
# Auth
####################
raviolini.auth.driver = basic
raviolini.auth.realm = secured
raviolini.auth.user = api_user
raviolini.auth.pass = api_pass
raviolini.auth.methods = GET,POST,PUT
The full sample application can be found at repository raviolini-sample.
This is it. Your RESTful API is done. Enjoy your day =)
Request:
POST /dog HTTP/1.1
Host: localhost:4567
Content-Type: application/json
{
"name": "Madalena",
"neutered": true
}
Response:
HTTP/1.1 201 Created
Date: Thu, 05 Nov 2015 12:17:48 GMT
Content-Type: text/plain
Location: http://localhost:4567/dog/1
Note the HTTP response header Location
informing the URL for the newly created object.
Request:
GET /dog/1 HTTP/1.1
Host: localhost:4567
Accept: application/json
Response:
HTTP/1.1 200 OK
Date: Thu, 05 Nov 2015 12:18:01 GMT
Content-Type: application/json
{
"id": 1,
"name": "Madalena",
"neutered": true
}
Request:
PUT /dog/1 HTTP/1.1
Host: localhost:4567
Content-Type: application/json
{
"id": 1,
"name": "Madalena",
"neutered": false
}
Response:
HTTP/1.1 200 OK
Date: Thu, 05 Nov 2015 12:18:01 GMT
Content-Type: text/plain
Request:
DELETE /dog/1 HTTP/1.1
Host: localhost:4567
Response:
HTTP/1.1 200 OK
Date: Thu, 05 Nov 2015 12:18:01 GMT
Content-Type: text/plain
Request:
GET /dog HTTP/1.1
Host: localhost:4567
Accept: application/json
Response:
HTTP/1.1 200 OK
Date: Thu, 05 Nov 2015 12:18:01 GMT
Content-Type: application/json
X-Items-Stored: 3
X-Items-Returned: 3
[
{
"id": 1,
"name": "Madalena",
"neutered": true
},
{
"id": 2,
"name": "Martin",
"neutered": false
},
{
"id": 3,
"name": "Junior",
"neutered": false
}
]
Note the HTTP response headers X-Items-Stored
and X-Items-Returned
, which inform the total number of objects found in the database, and the number of objects returned for this request, respectively.
Request:
GET /dog?orderby=-name&limit=1&offset=0&name=~ma%25 HTTP/1.1
Host: localhost:4567
Accept: application/json
Reponse:
HTTP/1.1 200 OK
Date: Thu, 05 Nov 2015 12:18:01 GMT
Content-Type: application/json
X-Items-Stored: 3
X-Items-Returned: 1
[
{
"id": 2,
"name": "Martin",
"neutered": false
}
]
Note that, in the example above, the resulting list of objects is constrained as follows:
- List sorted on field
name
in descending order, as defined by that dash prefix (-
) - List paginated with
limit
of only 1 object per page, and withoutoffset
, so from the begining - List composed of objects with field
name
starting with"ma"
, but case insensitive, as defined by that tilde prefix (~
)
By default, Raviolini addresses seven major non-functional requirements for your API, namely authentication, caching, configuration, logging, persistence, serialization, and validation. And this doesn't come at the expense of flexibility at all. You may still configure each one of these aspects by swapping out their drivers.
For example, you can define whether authentication will be based on Basic or Digest, whether caching will be powered by Redis or Memcached, whether persistence will be powered by PostgreSQL or MySQL, whether serialization will output JSON or XML, whether configuration will be read from File or Environment, and whether logging will output to File or Memory.
Many other drivers are available. Check them out below.
- Package: org.raviolini.aspects.security.auth
- Drivers: Null, Basic, Digest
- Learn more about Authentication Handling
- Package: org.raviolini.aspects.data.caching
- Drivers: Null, Memcached, Redis
- Package: org.raviolini.aspects.io.configuration
- Drivers: Environment, File
- Package: org.raviolini.aspects.io.logging
- Drivers: DatedFile, java.util.logging.Handler (e.g. Console, File, Memory, Socket, Stream)
- Exceptions logged can also be pushed automatically into Airbrake
- Package: org.raviolini.aspects.data.database
- Drivers: Relational (e.g. PostgreSQL, MySQL)
- Package: org.raviolini.aspects.io.serialization
- Drivers: JSON, XML
As depicted by the following UML diagram, Raviolini is composed of lightweight, loosely-coupled, cohesive packages, with no cycles in its dependency structure.
Now have a closer look inside package org.raviolini.aspects
. Once again, there are no cycles in this dependency structure either, which allows for more reusable building blocks.
- 0 dependencies:
data.validation
io.configuration
io.logging
io.serialization
security.crypt
- 1 dependency:
data.database
depends onio.configuration
- 2 dependencies:
data.caching
depends onio.configuration
andio.serialization
security.auth
deppends onio.configuration
andsecurity.crypt
Raviolini embrances HTTP and its status codes.
Code | Message | Description |
---|---|---|
400 | Bad Request | API client sent an invalid HTTP request. This could be either a malformed URI, or an empty body for a POST/PUT request. |
401 | Unauthorized | API client didn't provide a valid access credential through HTTP header Authorization. API client may retry the request with different credentials. |
403 | Forbidden | API client won't have access to the target resource at all, regardless of its access credentials. No retry should be attempted. |
404 | Not Found | API client sent an HTTP request against a resource that doesn't exist on the server, at this point in time. |
405 | Method Not Allowed | API client sent an HTTP request along with an unsupported HTTP verb. Raviolini handles GET, POST, PUT and DELETE only. |
406 | Not Acceptable | API client sent an HTTP GET request with an unsupported media-type on HTTP header Accept. Only JSON and XML are acceptable. |
415 | Unsupported Media Type | API client sent an HTTP POST/PUT request with an unsupported media-type on HTTP header Content-Type. Only JSON and XML are. |
500 | Internal Server Error | API server experienced an exception with one of its components, such as Cache, Configuration, Database, or Log. |
Less specific HTTP status codes, namely 400 (client-side error) and 500 (server-side error), should be interpreted along with the HTTP reponse message body, which will always contain more information about the exception thrown. This is usually an effective approach to troubleshooting your API, in addition to Raviolini logs, of course.
Raviolini is a stateless engine, so are the authentication drivers it provides. Thus, Raviolini's Digest
implmentation makes use of a signed timestamp nonce
, which is valid within the hour, as opposed to a one-off value that would have to be stored on the server. Hence, after this timestamp nonce
expires, the client will get a 401 response message, with a new nonce
on response header WWW-Authenticate
.
As defined by RFC2617, the contents of the nonce
are implementation dependent. The quality of the implementation depends on a good choice. And the strategy defined here is Raviolini's take on it.
Made in São Paulo, by Otavio Ferreira.
EOF