1. Introduction
JavaLite is a collection of frameworks for simplifying common tasks that every developer has to deal with when building applications.
In this tutorial, we’re going to take a look at JavaLite features focused on building a simple API.
2. Setup
Throughout this tutorial, we’ll create a simple RESTful CRUD application. In order to do that, we’ll use ActiveWeb and ActiveJDBC – two of the frameworks that JavaLite integrates with.
So, let’s get started and add the first dependency that we need:
<dependency> <groupId>org.javalite</groupId> <artifactId>activeweb</artifactId> <version>1.15</version> </dependency>
ActiveWeb artifact includes ActiveJDBC, so there’s no need to add it separately. Please note that the latest activeweb version can be found in Maven Central.
The second dependency we need is a database connector. For this example, we’re going to use MySQL so we need to add:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.45</version> </dependency>
Again, latest mysql-connector-java dependency can be found over on Maven Central.
The last dependency that we have to add is something specific to JavaLite:
<plugin> <groupId>org.javalite</groupId> <artifactId>activejdbc-instrumentation</artifactId> <version>1.4.13</version> <executions> <execution> <phase>process-classes</phase> <goals> <goal>instrument</goal> </goals> </execution> </executions> </plugin>
The latest activejdbc-instrumentation plugin can also be found in Maven Central.
Having all this in place and before starting with entities, tables, and mappings, we’ll make sure that one of the supported databases is up and running. As we said before, we’ll use MySQL.
Now we’re ready to start with object-relational mapping.
3. Object-Relational Mapping
3.1. Mapping and Instrumentation
Let’s get started by creating a Product class that will be our main entity:
public class Product {}
And, let’s also create the corresponding table for it:
CREATE TABLE PRODUCTS ( id int(11) DEFAULT NULL auto_increment PRIMARY KEY, name VARCHAR(128) );
Finally, we can modify our Product class to do the mapping:
public class Product extends Model {}
We only need to extend org.javalite.activejdbc.Model class. ActiveJDBC infers DB schema parameters from the database. Thanks to this capability, there’s no need to add getters and setters or any annotation.
Furthermore, ActiveJDBC automatically recognizes that Product class needs to be mapped to PRODUCTS table. It makes use of English inflections to convert singular form of a model to a plural form of a table. And yes, it works with exceptions as well.
There’s one final thing that we will need to make our mapping work: instrumentation. Instrumentation is an extra step required by ActiveJDBC that will allow us to play with our Product class as if it had getters, setters, and DAO-like methods.
After running instrumentation, we’ll be able to do things like:
Product p = new Product(); p.set("name","Bread"); p.saveIt();
or:
List<Product> products = Product.findAll();
This is where activejdbc-instrumentation plugin comes in. As we already have the dependency in our pom, we should see classes being instrumented during build:
... [INFO] --- activejdbc-instrumentation:1.4.11:instrument (default) @ javalite --- **************************** START INSTRUMENTATION **************************** Directory: ...\tutorials\java-lite\target\classes Instrumented class: .../tutorials/java-lite/target/classes/app/models/Product.class **************************** END INSTRUMENTATION **************************** ...
Next, we’ll create a simple test to make sure this is working.
3.2. Testing
Finally, to test our mapping, we’ll follow three simple steps: open a connection to the database, save a new product and retrieve it:
@Test public void givenSavedProduct_WhenFindFirst_ThenSavedProductIsReturned() { Base.open( "com.mysql.jdbc.Driver", "jdbc:mysql://localhost/dbname", "user", "password"); Product toSaveProduct = new Product(); toSaveProduct.set("name", "Bread"); toSaveProduct.saveIt(); Product savedProduct = Product.findFirst("name = ?", "Bread"); assertEquals( toSaveProduct.get("name"), savedProduct.get("name")); }
Note that all this (and more) is possible by only having an empty model and instrumentation.
4. Controllers
Now that our mapping is ready, we can start thinking about our application and its CRUD methods.
For that, we’re going to make use of controllers which process HTTP requests.
Let’s create our ProductsController:
@RESTful public class ProductsController extends AppController { public void index() { // ... } }
With this implementation, ActiveWeb will automatically map index() method to the following URI:
http://<host>:<port>/products
Controllers annotated with @RESTful, provide a fixed set of methods automatically mapped to different URIs. Let’s see the ones that will be useful for our CRUD example:
And if we add this set of methods to our ProductsController:
@RESTful public class ProductsController extends AppController { public void index() { // code to get all products } public void create() { // code to create a new product } public void update() { // code to update an existing product } public void show() { // code to find one product } public void destroy() { // code to remove an existing product } }
Before moving on to our logic implementation, we’ll take a quick look at few things that we need to configure.
5. Configuration
ActiveWeb is based mostly on conventions, project structure is an example of that. ActiveWeb projects need to follow a predefined package layout:
src |----main |----java.app | |----config | |----controllers | |----models |----resources |----webapp |----WEB-INF |----views
There’s one specific package that we need to take a look at – app.config.
Inside that package we’re going to create three classes:
public class DbConfig extends AbstractDBConfig { @Override public void init(AppContext appContext) { this.configFile("/database.properties"); } }
This class configures database connections using a properties file in the project’s root directory containing the required parameters:
development.driver=com.mysql.jdbc.Driver development.username=user development.password=password development.url=jdbc:mysql://localhost/dbname
This will create the connection automatically replacing what we did in the first line of our mapping test.
The second class that we need to include inside app.config package is:
public class AppControllerConfig extends AbstractControllerConfig { @Override public void init(AppContext appContext) { add(new DBConnectionFilter()).to(ProductsController.class); } }
This code will bind the connection that we just configured to our controller.
The third class will configure our app’s context:
public class AppBootstrap extends Bootstrap { public void init(AppContext context) {} }
After creating the three classes, the last thing regarding configuration is creating our web.xml file under webapp/WEB-INF directory:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns=...> <filter> <filter-name>dispatcher</filter-name> <filter-class>org.javalite.activeweb.RequestDispatcher</filter-class> <init-param> <param-name>exclusions</param-name> <param-value>css,images,js,ico</param-value> </init-param> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>dispatcher</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
Now that configuration is done, we can go ahead and add our logic.
6. Implementing CRUD Logic
With the DAO-like capabilities provided by our Product class, it’s super simple to add basic CRUD functionality:
@RESTful public class ProductsController extends AppController { private ObjectMapper mapper = new ObjectMapper(); public void index() { List<Product> products = Product.findAll(); // ... } public void create() { Map payload = mapper.readValue(getRequestString(), Map.class); Product p = new Product(); p.fromMap(payload); p.saveIt(); // ... } public void update() { Map payload = mapper.readValue(getRequestString(), Map.class); String id = getId(); Product p = Product.findById(id); p.fromMap(payload); p.saveIt(); // ... } public void show() { String id = getId(); Product p = Product.findById(id); // ... } public void destroy() { String id = getId(); Product p = Product.findById(id); p.delete(); // ... } }
Easy, right? However, this isn’t returning anything yet. In order to do that, we have to create some views.
7. Views
ActiveWeb uses FreeMarker as a templating engine, and all its templates should be located under src/main/webapp/WEB-INF/views.
Inside that directory, we will place our views in a folder called products (same as our controller). Let’s create our first template called _product.ftl:
{ "id" : ${product.id}, "name" : "${product.name}" }
It’s pretty clear at this point that this is a JSON response. Of course, this will only work for one product, so let’s go ahead and create another template called index.ftl:
[<@render partial="product" collection=products/>]
This will basically render a collection named products, with each one formatted by _product.ftl.
Finally, we need to bind the result from our controller to the corresponding view:
@RESTful public class ProductsController extends AppController { public void index() { List<Product> products = Product.findAll(); view("products", products); render(); } public void show() { String id = getId(); Product p = Product.findById(id); view("product", p); render("_product"); } }
In the first case, we’re assigning products list to our template collection named also products.
Then, as we’re not specifying any view, index.ftl will be used.
In the second method, we’re assigning product p to element product in the view and we’re explicitly saying which view to render.
We could also create a view message.ftl:
{ "message" : "${message}", "code" : ${code} }
And then call it form any of our ProductsController‘s method:
view("message", "There was an error.", "code", 200); render("message");
Let’s now see our final ProductsController:
@RESTful public class ProductsController extends AppController { private ObjectMapper mapper = new ObjectMapper(); public void index() { view("products", Product.findAll()); render().contentType("application/json"); } public void create() { Map payload = mapper.readValue(getRequestString(), Map.class); Product p = new Product(); p.fromMap(payload); p.saveIt(); view("message", "Successfully saved product id " + p.get("id"), "code", 200); render("message"); } public void update() { Map payload = mapper.readValue(getRequestString(), Map.class); String id = getId(); Product p = Product.findById(id); if (p == null) { view("message", "Product id " + id + " not found.", "code", 200); render("message"); return; } p.fromMap(payload); p.saveIt(); view("message", "Successfully updated product id " + id, "code", 200); render("message"); } public void show() { String id = getId(); Product p = Product.findById(id); if (p == null) { view("message", "Product id " + id + " not found.", "code", 200); render("message"); return; } view("product", p); render("_product"); } public void destroy() { String id = getId(); Product p = Product.findById(id); if (p == null) { view("message", "Product id " + id + " not found.", "code", 200); render("message"); return; } p.delete(); view("message", "Successfully deleted product id " + id, "code", 200); render("message"); } @Override protected String getContentType() { return "application/json"; } @Override protected String getLayout() { return null; } }
At this point, our application is done and we’re ready to run it.
8. Running the Application
We’ll use Jetty plugin:
<plugin> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>9.4.8.v20171121</version> </plugin>
Find latest jetty-maven-plugin in Maven Central.
And we’re ready, we can run our application:
mvn jetty:run
Let’s create a couple of products:
$ curl -X POST http://localhost:8080/products -H 'content-type: application/json' -d '{"name":"Water"}' { "message" : "Successfully saved product id 1", "code" : 200 }
$ curl -X POST http://localhost:8080/products -H 'content-type: application/json' -d '{"name":"Bread"}' { "message" : "Successfully saved product id 2", "code" : 200 }
.. read them:
$ curl -X GET http://localhost:8080/products [ { "id" : 1, "name" : "Water" }, { "id" : 2, "name" : "Bread" } ]
.. update one of them:
$ curl -X PUT http://localhost:8080/products/1 -H 'content-type: application/json' -d '{"name":"Juice"}' { "message" : "Successfully updated product id 1", "code" : 200 }
… read the one that we just updated:
$ curl -X GET http://localhost:8080/products/1 { "id" : 1, "name" : "Juice" }
Finally, we can delete one:
$ curl -X DELETE http://localhost:8080/products/2 { "message" : "Successfully deleted product id 2", "code" : 200 }
9. Conclusion
JavaLite has a lot of tools to help developers get an application up and running in minutes. However, while basing things on conventions results in a cleaner and simpler code, it takes a while to understand naming and location of classes, packages, and files.
This was only an introduction to ActiveWeb and ActiveJDBC, find more documentation on their website and look for our products application in the Github project.