SALAD: Slick Argonaut Lift Angular Derby
The scala-salad project is a complete step-by-step walkthrough that defines a functionally complete set of components for a lightweight multi-tier web application.
The server side exposes services using REST/JSON. Lift is the web framework, used for the stack. DerbyDB is selected as a database back-end. The code uses two JSON processing frameworks: Lift JSON and Argonaut. My experience is that lift-json is a solid piece of engineering, but Argonaut implementation is more elegant. Slick is selected as an FRM framework.
The client side is written in Angular2. The important intentional decision was to use TypeScript for the client implementation, as TypeScript makes the usage of functional idioms so much easier and supports many FP constructs on a language level. An ability to write type-safe code, even with existing limitations of the type system, facilitates a better quality of the code.
Toolkit | |
---|---|
sbt | simple build tool |
ScalaIDE | |
VSCode | TypeScript Editor, as well as markdown and many other formats. |
Postman | REST Client Chrome application |
npm | javascript shell |
tsc | TypeScript compiler |
liveserver | light http server with automatic update of changes files |
Servers | |
Derby | Database |
Jetty | Http server |
Languages and Frameworks | |
Scala | The Scala |
Lift | |
Argonaut | |
Angular2 |
- Create SALAD directory
mkdir ~/SALAD
- Define
SALAD_HOME
environment variable
export SALAD_HOME=~/SALAD
- Create
$SALAD/saladenv.sh
file that will contain environment configuration. Add previous command to it. - Create
_downloads
directory
mkdir $SALAD_HOME/_downloads
- Download .zip file from http://download.eclipse.org/jetty/ into
$SALAD_HOME/_downloads
. - Unzip its contents into
$SALAD_HOME
unzip $SALAD_HOME/_downloads/jetty-distribution-9.3.5.v20151012.zip -d $SALAD_HOME
- Export JETTY_HOME environment variable
export JETTY_HOME=$SALAD_HOME/jetty-distribution-9.3.5.v20151012
- Append
JETTY_HOME/bin
to thePATH
variablePATH=$PATH:$JETTY_HOME/bin
Note: Add JETTY_HOME definition to the saladenv.sh
file
- Run
java -jar $JETTY_HOME/start.jar --module=http jetty.http.port=8080
command. - In a browser navigate to
http://localhost:8080/
. Observe response from the server. - To exit the jetty, press
Ctrl+C
NOTE: Open separate Terminal window for Derby DB so you can leave its instance running.
- Download latest Derby DB distribution
db-derby-*-bin.zip
file (i.e.,10.12.1.1
) from http://db.apache.org/derby/releases/ .cgi link into$SALAD_HOME/_downloads
- Unzip its contents into
$SALAD_HOME
unzip $SALAD_HOME/_downloads/db-derby-10.12.1.1-bin.zip -d $SALAD_HOME
- Export DERBY_HOME environment variable
export DERBY_HOME=$SALAD_HOME/db-derby-10.12.1.1-bin
- Append
$DERBY_HOME/bin
to thePATH
variablePATH=$PATH:$DERBY_HOME/bin
Note: Add DERBY_HOME, DERBY_OPTS, and DERBY_HOME/bin definitions to the saladenv.sh
file
-
Create a directory for database files
mkdir $SALAD_HOME/db-derby-home
-
Creave Derby DB configuration with db-derby-home directory, assuming default default port
1527
export DERBY_OPTS=-Dderby.system.home=$SALAD_HOME/db-derby-home
-
Start the database in the server mode
startNetworkServer
Upon successful start you will see diagnostic output
startNetworkServer Wed Oct 21 23:26:39 BST 2015 : Security manager installed using the Basic server security policy. Wed Oct 21 23:26:40 BST 2015 : Apache Derby Network Server - 10.12.1.1 - (1704137) started and ready to accept connections on port 1527
-
Open new Terminal window. Source the environment
source ~/SALAD/saladenv.sh
-
Launch derby interative scripting tool utility ij
ij
-
Type in
connect
ij command with ;create=true prefix to create test-db databaseconnect 'jdbc:derby://localhost:1527/test-db;create=true';
The database should be created in $SALAD_HOME/db-derby-home directory
$ ls -ls $SALAD_HOME/db-derby-home total 8 8 -rw-r--r-- 1 nz staff 187 21 Oct 23:29 derby.log 0 drwxr-xr-x 9 nz staff 306 21 Oct 23:28 test-db
-
Output contents of
sys.systables
tableij> select tablename from sys.systables; TABLENAME --------------------------- SYSALIASES SYSCHECKS SYSCOLPERMS SYSCOLUMNS ... SYSTABLES SYSTRIGGERS SYSUSERS SYSVIEWS 23 rows selected ij>
-
Enter
exit;
to quit ij.
-
Go to
$SALAD_HOME
directorycd $SALAD_HOME
. -
Create scala-salad directory
mkdir scala-salad
. -
Go to the
scala-salad
directorycd scala-salad
. -
In the project directory create file
build.sbt
name := "salad-intro" version := "1.0" scalaVersion := "2.11.6" libraryDependencies ++= Seq( "com.typesafe.slick" %% "slick" % "3.0.0", "org.slf4j" % "slf4j-nop" % "1.6.4", "org.apache.derby" % "derbyclient" % "10.11.1.1" )
-
Create file projects/plugins.sbt
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0")
-
In the project folder run
sbt
command. -
After the sbt prompt appear run
eclipse
sbt command. The.classpath
,.project
and project folder structure will be created. -
Launch ScalaIDE and import project
File\Import ...\Existing Project
Choose $SALAD_HOME/scala-salad folder, click
Open
.Confirm project import defaults by selecting
Finish
.
-
Right-click on a
src/main/scala
node and selectCreate package
-
Enter salad.intro as a package name under
src/main/scala
. -
In the
src/main/resources
createapplication.conf
filetest-db = { url = "jdbc:derby://localhost:1527/test-db" driver = "org.apache.derby.jdbc.ClientDriver" connectionPool = disabled keepAliveConnection = true }
-
In the
salad.intro
package create Scala ObjectSlickDerby.scala
package salad.intro import slick.driver.DerbyDriver.api._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Await import scala.concurrent.duration._ case class Users(id: Int, name: String, age: Int, role: String) class UsersTable (tag:Tag) extends Table[Users](tag, "USERS") { def id = column[Int]("ID", O.PrimaryKey, O.AutoInc) def name = column[String]("NAME") def age = column[Int]("AGE") def role = column[String]("ROLE") def * = (id,name, age, role) <> ( (Users.apply _).tupled, Users.unapply) } object SlickDerbyGenerateDDL extends App{ Database.forConfig("test-db") val users = TableQuery[UsersTable] users.ddl.createStatements.foreach(println) }
Copy generated create table statement
create table "USERS" ( "ID" INTEGER NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "NAME" VARCHAR(254) NOT NULL, "AGE" INTEGER NOT NULL, "ROLE" VARCHAR(254) NOT NULL )
-
In the Terminal window with sourced environment, start ij
-
Execute
connect 'jdbc:derby://localhost:1527/test-db;';
-
Execute create table statement. Don't forget ; at the end.
-
Run
describe users;
to display newly created table.ij> describe users;
COLUMN_NAME TYPE_NAME DEC& NUM& COLUM& COLUMN_DEF CHAR_OCTE& IS_NULL& ID INTEGER 0 10 10 GENERATED& NULL NO NAME VARCHAR NULL NULL 254 NULL 508 NO AGE INTEGER 0 10 10 NULL NULL NO ROLE VARCHAR NULL NULL 254 NULL 508 NO 4 rows selected
-
Execute
insert table
statementsinsert into users values (1, 'Jones', 25, 'Developer'); insert into users values (2, 'Watson', 27, 'Manager');
In the
SickDerby.scala
addSlickDerbySelectRecords
objectobject SlickDerbySelectRecords extends App { val db = Database.forConfig("test-db") val users = TableQuery[UsersTable] val actions = for { all <- users.result } yield all println( "ResultSet:") val future = db.run(actions).map { _ foreach println } Await.result( future, 2 seconds) }
The output should look like
ResultSet: Users(1,Jones,25,Developer) Users(2,Watson,27,Manager)
-
Add dependency to the
build.sbt
"io.argonaut" %% "argonaut" % "6.0.4"
-
On sbt prompt execute
reload
andeclipse
commands. Refresh Eclipse workspace. -
Refactor users case class from
SlickDerby.scala
into separate file Users.scala. Add implicit casecodec for the JSON generator.Users.scala
:package salad.intro import scalaz._, Scalaz._ import argonaut._, Argonaut._ case class Users(id: Int, name: String, age: Int, role: String) object Users { implicit def UsersCodecJson: CodecJson[Users] = casecodec4(Users.apply, Users.unapply)("id","name","age","role") }
-
In the
salad.intro
package, createArgonautUser.scala
filepackage salad.intro import scalaz._, Scalaz._ import argonaut._, Argonaut._ object ArgonautUsers extends App{ val users = Vector( Users(1, "Janes", 25, "Developer"), Users(1, "Watson", 25, "Manager") ) val usersJson = users.asJson println( usersJson ) // parse the Users Vector as a Json string val usersJsonString = usersJson.toString val parsedUsers = usersJsonString.decodeOption[Vector[Users]].getOrElse(Nil) println( parsedUsers ) }
-
Execute ArgonautUsers object. The output should be
[{"id":1,"name":"Janes","age":25,"role":"Developer"},{"id":1,"name":"Watson","age":25,"role":"Manager"}]
Vector(Users(1,Janes,25,Developer), Users(1,Watson,25,Manager)) ```
-
Add plugin dependency to the project/plugins.sbt
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "1.1.0")
-
Add scala-logging dependency
"com.typesafe.scala-logging" %% "scala-logging" % "3.1.0"
-
Add libraryDependencies section to the
build.sbt
libraryDependencies ++= { val liftVersion = "2.6-RC1" Seq( "net.liftweb" %% "lift-webkit" % liftVersion % "compile", "net.liftweb" %% "lift-json" % liftVersion % "compile" ) } jetty()
-
On the sbt prompt execute
reload
andeclipse
commands. Refresh Eclipse project to load new dependencies.
- Open src and then main nodes in the Package Explorer. Create new folders
webapp
insrc/main
and folder WEB-INF in it. - In the
WEB-INF
Createweb.xml
with following contents<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5"> <filter> <filter-name>LiftFilter</filter-name> <display-name>List Filter</display-name> <description>the filter that intercepts lift calls</description> <filter-class>net.liftweb.http.LiftFilter</filter-class> </filter> <filter-mapping> <filter-name>LiftFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <static-files> <include path="/favicon.ico"/> </static-files> </web-app>
- In the
src/main/scala
create new packagebootstap.liftweb
- Create new Lift Scala Boot class
Boot.scala
package bootstrap.liftweb import net.liftweb._ import http._ import provider.HTTPParam import salad.intro.server.LiftRest class Boot { def boot{ // Allow Cross-Origin Resource Sharing LiftRules.supplimentalHeaders = s => s.addHeaders( List(HTTPParam("X-Lift-Version", LiftRules.liftVersion), HTTPParam("Access-Control-Allow-Origin", "*"), HTTPParam("Access-Control-Allow-Credentials", "true"), HTTPParam("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS"), HTTPParam("Access-Control-Allow-Headers", "WWW-Authenticate,Keep-Alive,User-Agent,X- Requested-With,Cache-Control,Content-Type") )) LiftRest.init() } }
- Create
LiftRest.scala
in thesrc/main/scala/salad.intro.server
packagepackage salad.intro.server import com.typesafe.scalalogging._ import net.liftweb.http.rest.RestHelper import net.liftweb.http.LiftRules import net.liftweb.http.OkResponse import net.liftweb.http.PlainTextResponse import net.liftweb.http.JsonResponse import net.liftweb.json.JsonAST._ import net.liftweb.json.JsonDSL._ import net.liftweb.json.Extraction._ import net.liftweb.json.Printer._ import net.liftweb.json.DefaultFormats import salad.intro.Users object LiftRest extends RestHelper with LazyLogging{ serve ( "api" / "v1" prefix { case "ping" :: Nil JsonGet req => OkResponse() case "list" :: Nil JsonGet req => JsonResponse( ("xx"-> "A xx" ) ~ ("yy" -> "A yy" ) ) case "users" :: Nil JsonGet req => JsonResponse( decompose( Users(1,"J", 15, "D") )) }) def init(): Unit = { LiftRules.statelessDispatch.append(LiftRest) } }
-
To generate .ware file in the sbt prompt execut
package
-
On the sbt prompt run jetty container
container:start
-
In a browser navigate to
http://localhost:8080/api/v1/list
. You should see a test JSON hard-coded datagram. -
Navigate to
http://localhost:8080/api/v1/users
. The Lift json has generated a JSON datagram from Scala Users Object. -
Stop the jetty server by pressing Enter in the sbt window and entering
container:stop
NOTE:* To reload automatically when files change, use following sbt command
~;container:start; container: reload /
Argonaut doesn't work out of the box with the Lift. To generate http response from JSON datagram, let's create the ArgonautResponse class and object that complies with the Lift response framework.
-
Int the
argonaut.http
package, createArgonautResponse.scala
package argonaut.http import net.liftweb.http.LiftResponse import net.liftweb.http.InMemoryResponse import net.liftweb.http.provider.HTTPCookie import net.liftweb.http.S import argonaut._, Argonaut._ case class ArgonautResponse( json: Json, headers: List[(String, String)], cookies: List[HTTPCookie], code: Int ) extends LiftResponse{ def toResponse = { val bytes = json.toString.getBytes("UTF-8") InMemoryResponse(bytes, ("Content-Length", bytes.length.toString) :: ("Content-Type", "application/json; charset=utf-8") :: headers, cookies, code) } } object ArgonautResponse { def headers: List[(String, String)] = S.getResponseHeaders(Nil) def cookies: List[HTTPCookie] = S.responseCookies def apply(json: Json): LiftResponse = new ArgonautResponse(json, headers, cookies, 200) }
-
In the
salad.intro.server
createArgonautRest.scala
so that the Argonaut responses are returned.package salad.intro.server import com.typesafe.scalalogging._ import net.liftweb.http.rest.RestHelper import net.liftweb.http.LiftRules import net.liftweb.http.OkResponse import net.liftweb.http.PlainTextResponse import salad.intro.Users import argonaut.http.ArgonautResponse import scalaz._, Scalaz._ import argonaut._, Argonaut._ object ArgonautRest extends RestHelper with LazyLogging{ serve ( "api" / "v1" prefix { case "ping" :: Nil JsonGet req => OkResponse() case "usersecho" :: Nil Post req => { ///// // the request feeds back to the client a Vector of Users // unmarchalled and marchalled by Argonaut // ArgonautRequest() proto-hack // json body is assumed and forced // TODO: infer text charset from req.contentType // body is Array[Byte] val json = new String(req.body.get) val users: Vector[Users] = json.decodeOption[Vector[Users]].getOrElse(Vector.empty) //// // I.e.: // [{"id":1,"name":"Janes","age":25,"role":"Developer"},{"id":1,"name":"Watson","age":25,"role":"Manager"}] // converts to: // val users = Vector( // Users(1, "Janes", 25, "Developer"), // Users(1, "Watson", 25, "Manager") // ) ArgonautResponse( users.asJson ) } case "users" :: Nil Get req => ArgonautResponse( Vector( Users(1, "Janes", 25, "Developer"), Users(1, "Watson", 25, "Manager") ).asJson ) }) def init(): Unit = { LiftRules.statelessDispatch.append(ArgonautRest) } }
-
Change
bootstrap.liftweb.Boot.scala
class to use ArgonautRest. ReplaceLiftRest
byArgonautRest
-
On sbt prompt run
package
, thencontainer:start
. -
In the browser navigate to
http://localhost:8080/api/v1/users
. You should see as a result the JSON array of Users objects.[{"id":1,"name":"Janes","age":25,"role":"Developer"},{"id":1,"name":"Watson","age":25,"role":"Manager"}]
The Postman Google Chrome application can be used to interact with our server by sending and receiving JSON requests. We are going to use usersecho
request to POST a user object, then use Argonaut for round-trip communication: to unmarchall it into Scala Users object and marchall it back into a JSON response.
The /api/v1/userecho
service accepts JSON datagram, converts it into scala object using Argonaut, then sends the object, packaging it into an ArgonautResponse.
-
Install Postman REST Client application from the Chrome Web Store.
-
Open the Postman. Configure the POST operatin with
localhost:8080/api/v1/usersecho
URL. -
Select Row JSON request and enter
[{"id":1,"name":"Janes","age":25,"role":"Developer"}]
as a request datagram.
-
Press
Send
button. You should obtain JSON response:[ { "id": 1, "name": "Janes", "age": 25, "role": "Developer" } ]
???
NOTE: while working on a client application and developing angular-only code, we want to optimise edit-run cycle by using live-server.
-
Launch VSCode and use
Open Folder
button to opensrc/main/webapp
folder of the scala-salad project. -
Create
package.json
file{ "name": "userscomponent", "version": "0.0.1", "dependencies": { "angular2": "2.0.0-alpha.42", "systemjs": "0.19.2" }, "devDependencies": { "live-server": "latest" }, "scripts": { "tsc": "tsc -p src/webapp -w", "liveserver": "live-server --no-browser --port=9090 --open=src/webapp" } }
-
Execute
npm install
to load dependencies and live-server. -
Create index.html file with
Hello, World!
contents. -
Execute
npm run liveserver
command to start local server on port 9090. -
In a browser, navigate to localhost:9090/ URL. The
Hello, World!
page should be displayed.
-
In the
webapp
folder createapp
folder -
In the
app
folder create tsconfig.json file{ "compilerOptions": { "module": "commonjs", "target": "es5", "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "noImplicitAny": false } }
-
In a separate window launch
npm run tsc
command. -
Create
app/usersapp.ts
TypeScript fileimport {bootstrap,Component} from 'angular2/angular2'; @Component({ selector: 'users-app', template: ` <h2>Hello, World</h2> ` }) export class UsersApp{ constructor(){} } bootstrap(UsersApp, []);
-
Edit
index.html
file<html> <head> <title>Users Component</title> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="node_modules/angular2/bundles/angular2.js"></script> <script> System.config({ packages: {'app': {defaultExtension: 'js'}} }); System.import('app/usersapp').catch(console.log.bind(console)); </script> </head> <body> <users-app></users-app> </body> </html>
-
In brower refresh
localhost:9090
page. You will seeHello World!
, but don't be fooled: this is a full-fetured Angular2 application.
-
In the
app
folder, create usermodel.ts file.export class User{ id: number; name: String; age: number; role: string; }
-
Create
app/service.ts
fileimport {Http} from 'angular2/http'; import {Injectable} from 'angular2/angular2'; import {User} from './usermodel'; @Injectable() export class UsersService { users: User[] = []; constructor( private _http: Http) {} getUsers(): Promise<User[]> { this.users.length = 0; let promise = this._http.get('http://localhost:8080/api/v1/users') .map((response: any) => response.json()).toPromise() .then((users: User[])=> { this.users.push(...users); return this.users; }) .then((_: any) => _, (e: any) => this._fetchFailed(e)); return promise; } private _fetchFailed(error:any) { console.error(error); return Promise.reject(error); } }
-
Refactor app.ts class to define
AppComponent
import {bootstrap,Component,CORE_DIRECTIVES} from 'angular2/angular2' import {HTTP_BINDINGS} from 'angular2/http'; import {UsersService} from './service'; import {User} from './usermodel'; @Component({ selector: 'users-app', template: `<h1>Hello</h1> <ul> <li *ng-for="#user of users"> <span>{{user.id}}</span> {{user.name}} | {{user.age}}> </li> </ul> `, directives: [CORE_DIRECTIVES] }) class UsersApp { public users: User[]; constructor(private _usersService: UsersService){ this.getUsers(); } getUsers(){ this.users = []; this._usersService.getUsers() .then(users => this.users = users ); } } bootstrap(UsersApp, [UsersService,HTTP_BINDINGS])
-
Add http.dev.js link to the index.html
<script src="node_modules/angular2/bundles/http.dev.js"></script>
-
Look at the browser window. The page should automatically re-draw contents as you save your changes. The final output should look like:
Hello 1 Janes | 25> 1 Watson | 25>
- At the sbt prompt, start container and in the browser navigate to http://localhost:8080. You should see same output, but this time it is generated by whole end-to-end processing.