Getting started
This an introductory tutorial for getting confidence with the Jolie language. You will learn to:
- define an interface for a service;
- program and run a service;
- set the execution modality.
As a reference example, here we are creating a service which implements a simple basic calculator. In particular, the service will provide four basic operations for each of the basic arithmetic ones: sum, subtraction, multiplication and division.
Define an interface for a service
Jolie enables the developer to follow a contract first programming approach. This means that, before starting with the development, it is necessary to define the API of the service. In Jolie, this can be done by defining the interface. An interface contains a list of functionalities, called operations, which can be implemented by a service. In the following we report a first draft of an interface for a calculator:
interface CalculatorInterface {
RequestResponse:
sum,
sub,
mul,
div
}
This code can be read as defines an interface called CalculatorInterface which contains four operations of type RequestResponse called sum, sub, mul and div respectively. It is worth noting that there are two possible types for the operations: RequestResponse and OneWay. The former represents a synchronous exchange which involves a request message and a response message, whereas the latter represents an asynchronous exchange where there is only a request message without any response.
Save this code into a specific file called CalculatorInterfaceModule.ol
, we will import it later from the service module.
Define message types
So far, we have just defined an interface as a list of operations without specifying anything about the signatures of the operations. In Jolie it is possible to define message types in order to specify the structure of the messages. In the following we enhance the previous definition of the interface, by adding message types.
type SumRequest: void {
term[1,*]: int
}
type SubRequest: void {
minuend: int
subtraend: int
}
type MulRequest: void {
factor*: double
}
type DivRequest: void {
dividend: double
divisor: double
}
interface CalculatorInterface {
RequestResponse:
sum( SumRequest )( int ),
sub( SubRequest )( int ),
mul( MulRequest )( double ),
div( DivRequest )( double )
}
Some interesting things to note:
- in Jolie there are basic data types as integers, string, double, etc. In the example we exploit
int
(integers) for all the operations with the exception of operations multiplication and division where we use typedouble
. You can check the other basic types here; - the keyword
type
allows for the definition of structured data types; - an operation message type is just a data type associated with it into the definition of the operation. As an example the request message of operation
sum
isSumRequest
whereas the reply is just aint
; - a data type structure in Jolie represents a tree of nodes. As an example, type
DivRequest
contains two subnodes nameddividend
anddivisor
respectively. Both of them aredouble
; - a node in a data structure can be a vector. As an example node
term
of typeSumRequest
is a vector ofint
.[1,*]
stands for: minimum occurrences 1 and maximum occurrences infinite. We readterm[1,*]:int
as an unbounded vector of int with at least one element;
Program and run a service
Once we have defined the interface to implement, we are ready to define the service. Let's call the service CalculatorService
. Edit a new module as follows:
from .CalculatorInterfaceModule import CalculatorInterface
service CalculatorService {
}
This code permits to import the definition of the CalculatorInterface
from module CalculatorInterfaceModule
stored into file CalculatorInterfaceModule.ol
and defines a service called CalculatorService
. The dot prefix tells Jolie that it should find the module in the same directory.
Defining the inputPort
Unfortunately, the code above will raise an error if executed, because the service definition does not contain any listening port nor any behaviour too. Let's start by defining a listening endpoint for this service:
rom .CalculatorInterfaceModule import CalculatorInterface
service CalculatorService {
inputPort CalculatorPort {
location: "socket://localhost:8000"
protocol: http { format = "json" }
interfaces: CalculatorInterface
}
}
Listening endpoints in Jolie are called inputPort
. In this example we defined one inputPort named CalculatorPort
. An inputPort always requires three parameters in order to be properly set:
- location: it specifies where the service is listening for messages. In the example
socket://localhost:8000
wheresocket
defines the medium used for the communication; - protocol: it specifies the protocol do use for interacting with the service. In this example is
http
. In particular, protocol http is parameterized setting propertyformat
tojson
which means that the message body of the http message is a JSON; - interfaces: it specifies the interfaces available at the port. In this case the interface
CalculatorInterface
is defined. Summarizing we can read the inputPort definition of this example as follows: _start to listen on a socket of localhost at port 8000. Use protocol http for interpreting received messages and preparing responses too. Enable all the operations defined into CalculatorInterface`.
Defining the behaviour
Now, the service is ready to receive messages on the operation specified in interface CalculatorInterface
but we did not tell it what to do once a message is received. It is time to finalize the service by specifying the behaviour:
from .CalculatorInterfaceModule import CalculatorInterface
service CalculatorService {
inputPort CalculatorPort {
location: "socket://localhost:8000"
protocol: http { format = "json" }
interfaces: CalculatorInterface
}
main {
[ sum( request )( response ) {
for( t in request.term ) {
response = response + t
}
}]
[ sub( request )( response ) {
response = request.minuend - request.subtraend
}]
[ mul( request )( response ) {
response = 1
for ( f in request.factor ) {
response = response * f
}
}]
[ div( request )( response ) {
response = request.dividend / request.divisor
}]
}
}
Some interesting things to be noticed:
- the behaviour is set within scope
main
; - the list of operations are specified using input choices. This is why you see square brackets around the implementation of each operation. Briefly, when more than one operation is put within an input choice, it means they are all available but only that which receives a message is executed;
- each operation specifies a variable which contains the request message, in the example we named all of them as
request
. they specify the variable which will contain the response, in the example we named all of them asresponse
; - the code specified within curly brackets in an operation, defines the code to be executed after the reception of a request and the final sending of the response;
- once the body code of a request-response is finished, the content of the variable specified as a response will be actually sent as response message. This means that its data structure must correspond to what is defined into the interface;
- we read
for( t in request.term )
as: for each element of vectorrequest.term
do the code within curly brackets. Use tokent
for referring to the current element of the vector.
Running the service
Save the previous code into a module called CalculatorService.ol
within the same folder where you previously saved the interface module CalculatorInterfaceModule.ol
. Run the service using the following command:
jolie CalculatorService.ol
The service will start immediately waiting for a request.
Sending a request to the service
For the sake of this example, we can use curl
as a program for sending a message to the service. Other http clients can be used instead. Running the following clients you can check how the different operations reply:
- sum:
curl 'http://localhost:8000/sum?term=5&term=6&term=20'
{"$":31}
- sub:
curl 'http://localhost:8000/sub?minuend=10&subtraend=5'
{"$":5}
- mul:
curl 'http://localhost:8000/mul?factor=5&factor=2&factor=3'
{"$":30}
- div:
curl 'http://localhost:8000/div?dividend=10.8&divisor=2'
{"$":5.4}
Setting the execution modality
We are quite sure that, if you strictly followed this tutorial, you were able to run only one client and then restart the service because it went down. This is not an error or a malfunction, but it is due to the fact that we did not specify any execution modality for the service CalculatorService
. The execution modality specifies three different way to run a service: concurrent
, sequential
or single
. If nothing is specified, modality single
is set. This modality means that the service executes its behaviour once, then stops. This is why our service just executed one operation and then stopped.
In order to enable the service to continuously serve requests we need to specify the execution modality concurrent
.
So, let's admire our first service in Jolie!
from .CalculatorInterfaceModule import CalculatorInterface
service CalculatorService {
execution: concurrent
inputPort CalculatorPort {
location: "socket://localhost:8000"
protocol: http { format = "json" }
interfaces: CalculatorInterface
}
main {
[ sum( request )( response ) {
for( t in request.term ) {
response = response + t
}
}]
[ sub( request )( response ) {
response = request.minuend - request.subtraend
}]
[ mul( request )( response ) {
for ( f in request.factor ) {
response = response * f
}
}]
[ div( request )( response ) {
response = request.dividend / request.divisor
}]
}
}
The complete example
The complete example of this tutorial can be found at this link
Exiting a service
Jolie provides the exit
instruction to exit the current program by terminating the running Java virtual machine. In the example above, we could extend our service interface and behaviour with the shutdown
operation, which closes the service using the exit
instruction — notice that we use the full syntax of input choices here, which is [ inputOperation ]{ post-operation code }
.
main {
[ sum( request )( response ) {
for( t in request.term ) {
response = response + t
}
}]
// ...
[ shutdown()() ]{
exit
}
}
}