Everybody's doing it, so it must be good…'it' in this case being Google's AngularJS, the "Superheroic JavaScript MVW Framework."
Who am I to disagree?
To keep up with Mr & Mrs Everywhere I set myself the task of making a simple AJAX/REST-based cascading select form. And to ensure that my coolness factor doesn't shoot way off
the scale, I also set myself the task of building the REST componentry with JEE7, rather than something else/better.
Why this task? Because I found very little true guidance on how to do it "out there." There exists a fair bit of "oh, it's easy" handwaving but no concrete example. Time to rectify that.
Along the way, I'll throw in a few toys like MireDot and MongoDB. Because I can, that's why!
To get us started, here's the form in all it's Bootstrap-py glory:
It's easy to see what is going on, I hope: the form uses AJAX (via AngularJS' data binding and services facilities) to hit a REST resource that serves up a list of Australia's capital cities, and bind that list to the origin select form
control. Use the selection from that control to go back to the resource to get another list of destination cities (I'm assuming that our shipping company is pretty small and doesn't cover all of Australia). Once one has the origin and
destination, and a given quantity of 'things' to be shipped, hit a second REST resource to do the calculation.
I guess that I'm not really that cool, because I'm going to take a traditional 3-tier view of the application: Database, Server-side and Client-side. Works for me, anyway.
Database Tier
MongoDB is the storage engine for this application.
The database is a collection of documents like:
[
{_id: 1, "origin": "Brisbane", "destination": [
{"city": "Brisbane", "cost": 0.00},
{"city": "Hobart", "cost":88.88 },
{"city": "Canberra", "cost":22.22},
{"city": "Darwin", "cost":44.44},
{"city": "Sydney", "cost":111.11}
]},
{_id: 2, "origin": "Hobart", "destination": [
{"city": "Brisbane", "cost": 88.88},
...
]},
...
]
In one of those nasty old-fashioned uncool SQL databases, one would model this sort of master/detail structure and implicit 'contains' constraint with a one-to-many relationship. I could do that in MongoDB as well, but MongoDB makes it
possible to use an embedded sub-model, as shown above. This is somewhat cleaner and might well be more performant for certain queries.
For this example, I desire to load the data into a collection called 'cities' in a database called 'cities.'
MongoDB naturally supplies a loader tool to make this happen:
mongoimport.exe --host localhost --port 30000 --db cities --collection cities --file "cities.mongo" --jsonArray
For completeness' sake, here's a few queries and useful miscellaneous commands that can be issued to the database:
> use cities
switched to db cities
> show databases
cats 0.203125GB
cities 0.203125GB
local 0.078125GB
> show collections
cities
system.indexes
>
> var x = db.cities.findOne({"destination.city": "Sydney"}, {"destination.$": 1})
> x
{
"_id" : ObjectId("52a908d43780d39488948586"),
"destination" : [
{
"city" : "Sydney",
"cost" : 111.11
}
]
}
> x.destination[0].cost
111.11
>
> db.cities.find().forEach(printjson)
{
"_id" : 1,
"origin" : "Brisbane",
"destination" : [
{
"city" : "Brisbane",
"cost" : 0
},
{
"city" : "Hobart",
"cost" : 88.88
},
{
"city" : "Canberra",
"cost" : 22.22
},
{
"city" : "Darwin",
"cost" : 44.44
},
{
"city" : "Sydney",
"cost" : 111.11
}
]
}
...
>
> db.getCollection('cities').drop();
true
>
At this trivial level of use, MongoDB is pretty straightforward.
The coding is, as expected, low-level.
// https://raw.github.com/jvmisc22/mongo-jndi/master/src/main/java/com/mongodb/MongoCitiesDatabase.java
package mongodb;
import com.mongodb.*;
import rest.InvalidDataException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
public class MongoCitiesDatabase {
public static Set<String> findAllCities() throws InvalidDataException {
DBObject fields = new BasicDBObject();
fields.put("origin", 1);
fields.put("_id", 0);
DBObject project = new BasicDBObject("$project", fields);
final Set<String> ss = new HashSet<>();
MongoClient mc = MongoSingleton.INSTANCE.getMongoClient();
DB db = null;
try {
db = mc.getDB("cities");
// http://docs.mongodb.org/ecosystem/drivers/java-concurrency/
db.requestStart();
db.requestEnsureConnection();
DBCollection cities = db.getCollection("cities");
AggregationOutput output = cities.aggregate(project);
if (!output.getCommandResult().ok())
throw new InvalidDataException("No Cities found.");
for (DBObject r : output.results()) {
String s = (String) r.get("origin");
ss.add(s);
}
} finally {
try {
db.requestDone();
} catch (NullPointerException e) {
/* SQUELCH!*/
}
}
return ss;
}
public static Set<String> findDestinationsBySource(String source) throws InvalidDataException {
DBObject match = new BasicDBObject("$match", new BasicDBObject("origin", source));
DBObject fields = new BasicDBObject();
fields.put("destination.city", 1);
fields.put("_id", 0);
DBObject project = new BasicDBObject("$project", fields);
final Set<String> destinations = new HashSet<>();
MongoClient mc = MongoSingleton.INSTANCE.getMongoClient();
DB db = null;
try {
db = mc.getDB("cities");
db.requestStart();
db.requestEnsureConnection();
DBCollection cities = db.getCollection("cities");
AggregationOutput output = cities.aggregate(match, project);
if (!output.getCommandResult().ok())
throw new InvalidDataException(String.format("/{source: %1$s}/destinations. Source not found.", source));
for (DBObject r : output.results()) {
BasicDBList destination = (BasicDBList) r.get("destination");
for (Object d : destination) {
BasicDBObject o = (BasicDBObject) d;
String c = (String) o.get("city");
destinations.add(c);
}
}
} finally {
try {
db.requestDone();
} catch (NullPointerException e) {
/* SQUELCH!*/
}
}
return destinations;
}
// http://www.mkyong.com/mongodb/java-mongodb-query-document/
public static BigDecimal findCostBySourceAndDestination(String source, String dest) throws InvalidDataException {
DBObject unwind = new BasicDBObject("$unwind", "$destination");
BasicDBObject andQuery = new BasicDBObject();
List<basicdbobject> obj = new ArrayList<>();
obj.add(new BasicDBObject("origin", source));
obj.add(new BasicDBObject("destination.city", dest));
andQuery.put("$and", obj);
DBObject match = new BasicDBObject("$match", andQuery);
DBObject fields = new BasicDBObject();
fields.put("destination.cost", 1);
fields.put("_id", 0);
DBObject project = new BasicDBObject("$project", fields);
MongoClient mc = MongoSingleton.INSTANCE.getMongoClient();
DB db = null;
try {
db = mc.getDB("cities");
db.requestStart();
db.requestEnsureConnection();
DBCollection cities = db.getCollection("cities");
AggregationOutput output = cities.aggregate(unwind, match, project);
if (!output.getCommandResult().ok())
throw new InvalidDataException(String.format("{source: %1$s} or {destination: %2$s} not found.", source, dest));
for (DBObject r : output.results()) {
BasicDBObject destination = (BasicDBObject) r.get("destination");
return new BigDecimal((Double) destination.get("cost")).setScale(2, RoundingMode.CEILING);
}
} finally {
try {
db.requestDone();
} catch (NullPointerException e) {
/* SQUELCH!*/
}
}
// should not happen!
throw new InvalidDataException(String.format("Given ({source: %1$s}, {destination: %2$s}); no data.", source, dest));
}
// Joshua Bloch's Java 1.5+ Singleton Pattern
// http://books.google.com.au/books?id=ka2VUBqHiWkC&pg=PA17&lpg=PA17&dq=singleton+bloch&source=bl&ots=yYKmLgv1R-&sig=fRzDz11i4NnvspHOlooCHimjh2g&hl=en&sa=X&ei=xvOwUsLVAuSOiAeVyYHoAQ&ved=0CDgQ6AEwAg#v=onepage&q=singleton%20bloch&f=false
private enum MongoSingleton {
INSTANCE;
private static final Logger log = Logger.getLogger(MongoSingleton.class.getName());
private MongoClient mongoClient = null;
MongoClient getMongoClient() {
MongoClientOptions options = MongoClientOptions.builder()
.connectionsPerHost(25)
.build();
if (mongoClient == null)
try {
ServerAddress serverAddress = new ServerAddress("localhost", 30000);
mongoClient = new MongoClient(serverAddress, options);
} catch (UnknownHostException uhe) {
String msg = "getMongoClient(); configuration issue. UnknownHostException: " + uhe.getMessage();
log.severe(msg);
throw new RuntimeException(msg, uhe);
}
return mongoClient;
}
}
}
This is quite reminiscent of straight JDBC coding. Assembly language coding for the database. Shudder. If I had to do this again, I would have gone for something like Jongo to make life somewhat
easier.
There are a few points of interest here:
- The use of the db.request{Start,EnsureConnection,End} sequence to ensure that the MongoClient Java driver handles concurrency in a manner more compatible with server-side requirements than normal.
- The use of $unwind, which "Peels off the elements of an array individually, and returns a stream of documents." and allows for search within the
embedded 'destinations' array.
- (A standard Java trick) the use of Enum to provide a singleton. Yes, I do know that "singletons are evil." In this case I just couldn't be bothered messing with Wildfly's innards
to get an objectfactory asserted into the JNDI, etc. You should! See here and here.
- RuntimeException-based exception handling makes life easy (and see later)
Server Tier
I am 'exploring' JAX-RS in JEE7. No doubt as the result of a long exposure, JBoss seems to be somewhere down "in my DNA" and thus I will eschew Glassfish 4 (which now has a decidedly uncertain future, anyway) and go with Wildfly 8 CR1.
I'm aiming to build a couple of REST resources. Nothing too fancy, level 2 of the Richardson REST Maturity Model seems a happy place to be. The resources will
be: Cities and Shipping. No prizes for guessing what each is for, so without further ado…
Cities
package rest;
import mongodb.MongoCitiesDatabase;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
@Path("/cities")
@Produces(MediaType.APPLICATION_JSON)
public class Cities {
private static final Logger log = Logger.getLogger(Cities.class.getName());
@GET
public Map<String, Set<String>> getDefault() {
log.info("getDefault()");
return sources();
}
@GET
@Path("/sources")
public Map<String, Set<String>> sources() {
log.info("sources()");
return new HashMap<String, Set<String>>() {{
put("sources", MongoCitiesDatabase.findAllCities());
}};
}
@GET
@Path("/{source}/destinations")
public Map<String, Set<String>> destination(@PathParam("source") final String source) {
log.info(String.format("/{source: %1$s}/destinations", source));
return new HashMap<String, Set<String>>() {{
put("destinations", MongoCitiesDatabase.findDestinationsBySource(source));
}};
}
}
A pretty straightforward, read-only resource.
While not strictly necessary, I tend to ensure that my JSON objects all encapsulated as a single entry in a top-level container map. Maybe it's the XML-induced OCD surfacing through my psyche ("thou shalt have but a single root to a
document") but I believe that this makes life a teeny-weeny bit nicer. Most examples over at JSON.org are like this, too. There's another reason to encapsulate a response like this: it is
ridiculous that JEE7 (via JAX-RS) makes it less-than-straightforward (ie: hard) to just return plain old List<String>. Don't believe me? LMGTFY.
Shipping
Shipping is an "Algorithmic Resource." To a degree, referring to this as a resource represents REST doublethink. To us oldies, it is clearly a plain old
service.
package rest;
import mongodb.MongoCitiesDatabase;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
/**
* An "Algorithmic REST Resource" that determines the cost of shipping items around the capital cities of Australia.
*
* @servicetag Cities
*
* @author Bob
*/
@Path("/shipping")
public class Shipping {
private static final Logger log = Logger.getLogger(Shipping.class.getName());
/**
* Determines the cost of shipping items around the capital cities of Australia.
*
* @param origin Where to ship from
* @param destination Where to ship to
* @param quantity how many to ship
* @return The cost of shipping items
* @throws CityNotFoundException leading to a 404 return status.
* @statuscode 404 If any of the origin or destination parameters can't be found.
*/
@Produces(MediaType.APPLICATION_JSON)
@GET
@Path("/calculate")
public Map<String, Map<String, BigDecimal>> calculate(@QueryParam("origin") String origin, @QueryParam("destination") String destination,
@DefaultValue("1") @QueryParam("quantity") Integer quantity) {
log.info(String.format("calculate():: Origin: %1$s; Destination: %2$s; Quantity: %3$d", origin, destination, quantity));
BigDecimal costPer = MongoCitiesDatabase.findCostBySourceAndDestination(origin, destination);
final BigDecimal res = costPer.multiply(BigDecimal.valueOf(quantity).setScale(2, RoundingMode.CEILING));
final Map<String , BigDecimal> resMap = new HashMap<String, BigDecimal>() {{
put("result", res);
}};
return new HashMap<String, Map<String, BigDecimal>>() {{
put("calculate", resMap);
}};
}
}
There's a bit of Javadoc here, which I'll talk about later on, but otherwise, there's nothing special to see here.
Exception Handling
If you refer back to the MongoCitiesDatabase class, you will note that application exceptions are signalled via the InvalidDataException class.
You should also note that none of the client code explicitly handles such exceptions (a benefit of making the class a descendent of the unchecked RuntimeException, rather than plain old-and some would say evil-compiler-checked
Exception). This is possible because we use an "exception mapper" to ensure that exceptions are handled and mutated into acceptable REST behaviour:
package rest;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@Provider
public class InvalidDataMapper implements ExceptionMapper<InvalidDataException> {
@Override
public Response toResponse(InvalidDataException ide) {
return Response.status(Response.Status.BAD_REQUEST).entity(ide.getMessage()).type(MediaType.TEXT_PLAIN_TYPE).build();
}
}
This mapper ensures that a BAD_REQUEST (404) response is issued to the client, bundled with a plain-text message.
Application
JAX-RS likes to know how all the various resources are linked together. The simplest way to let it know what's what is to provide an Application class. For this project, the requisite class is:
package rest;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@ApplicationPath("/rest")
@SuppressWarnings("unused")
public class ApplicationConfig extends Application {
public Set<Class<?>> getClasses() {
return new HashSet<Class<?>>(Arrays.asList(Shipping.class, Cities.class, InvalidDataMapper.class));
}
}
JSON Vulnerability Protection
AngularJS has a built-in mechanism for consuming JSON-formatted data that is protected against misuse. The servlet filter given below shows how to create protected data suitable for AngularJS' use by prepending all JSON data with a
syntactically illegally fragment of JSON. While angularJS knows to automatically remove this standard 'nasty' prefix from any data given to it, the assumption is that a "baddie's" application won't know that it has to do this and so will
encounter JSON parse errors, rendering the protected data inaccessible. Google (they use a slightly different/nastier
approach), Facebook and many other big sites do this sort of thing, so it must be A Good Thing to do, right…
package rest;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
@WebFilter(filterName = "AngularJSJSONVulnerabilityProtectionFilter",
urlPatterns = {"/rest/*"})
public class AngularJSJSONVulnerabilityProtectionFilter implements Filter {
// JSON Vulnerability protection in angular
// see: http://docs.angularjs.org/api/ng.$http
private static final String AngularJSJSONVulnerabilityProtectionString = ")]}',\n";
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
StringResponseWrapper responseWrapper = new StringResponseWrapper(response);
chain.doFilter(req, responseWrapper);
String content = responseWrapper.toString();
// turn on AngularJS' JSON vulnerability protection feature
// ignore mapped exceptions and anything else not 'Kosher'
if (MediaType.APPLICATION_JSON.equals(responseWrapper.getHeader("Content-Type")))
content = String.format("%1$s%2$s", AngularJSJSONVulnerabilityProtectionString, content);
byte[] bytes = content.getBytes();
response.setContentLength(bytes.length);
response.getOutputStream().write(bytes);
}
@Override
public void init(FilterConfig config) throws ServletException {
}
@Override
public void destroy() {
}
// http://stackoverflow.com/questions/1302072/how-can-i-get-the-http-status-code-out-of-a-servletresponse-in-a-servletfilter/1302165#1302165
private class StringResponseWrapper
extends HttpServletResponseWrapper {
private StringWriter writer;
public StringResponseWrapper(HttpServletResponse response) {
super(response);
writer = new StringWriter();
}
@Override
public PrintWriter getWriter() {
return new PrintWriter(writer);
}
@Override
public ServletOutputStream getOutputStream() {
return new StringOutputStream(writer);
}
@Override
public String toString() {
return writer.toString();
}
}
private class StringOutputStream extends ServletOutputStream {
private StringWriter stringWriter;
public StringOutputStream(StringWriter stringWriter) {
this.stringWriter = stringWriter;
}
public void write(int c) {
stringWriter.write(c);
}
public boolean isReady() {
return true;
}
public void setWriteListener(WriteListener writeListener) {
}
}
}
This is all much more cumbersome than I would like to see. Sadly, JEE7 continues to be a rather verbose beastie.
Client Side Tier
This is a simple AngularJS application. Rather than build from scratch, I am using Angular Seed.
I am also using Twitter bootstrap to give me some basic prettiness. I'm not using any of Bootstrap's Javascript-based componentry. It is apparently too much to expect these two big lumps of
Javascript to play well together. There are a couple of projects aiming to "reimplement" the missing components: this and this. Both
are quite low fidelity attempts and so neither are truly Bootstrap. This may or may not matter to you and rather than rant about how imperfect the world is, I'll just swallow my bile and just get on with life…
There are four basic Points Of Interest, so let's take a look.
app.js
This is where angular creates routes, assigns controllers and does the rest of its housekeeping. For this app, it is fairly simple:
'use strict';
// Declare app level module which depends on filters, and services
angular.module('cascadeSelects', [
'ngRoute',
'cascadeSelects.filters',
'cascadeSelects.services',
'cascadeSelects.directives',
'cascadeSelects.controllers'
]).
config(['$routeProvider', '$httpProvider', function($routeProvider, $httpProvider) {
$routeProvider.when('/', {templateUrl: 'partials/cascadeSelectsPartial.html', controller: 'CascadeSelectsController'});
$routeProvider.otherwise({redirectTo: '/'});
}]);
I am following the angular-seed convention of using a template along with views/partials for content even though for this application there is only 1 view. So sue me, I am too lazy to hack anything more specific!
HTML
Leaving aside the very basics of AngularJS, as embodied in the template index.html file, it is worth taking a quick look at the file 'partials/cascadeSelectsPartial.html.'
This embodies an HTML form, with added AngularJS goodness:
<form class="form-horizontal" role="form">
<fieldset>
<legend>Calculator:</legend>
<div class="form-group">
<label for="originSelect" class="col-sm-2 control-label">Origin</label>
<div class="col-sm-6">
<select id="originSelect" class="form-control" ng-model="cities.chosen"
ng-options="src for src in cities.sources">
</select>
</div>
</div>
<div class="form-group">
<label for="destinationSelect" class="col-sm-2 control-label">Destination</label>
<div class="col-sm-6">
<select id="destinationSelect" class="form-control" ng-model="destinations.chosen"
ng-options="dest for dest in destinations.destinations">
</select>
</div>
</div>
<div class="form-group">
<label for="quantityInput" class="col-sm-2 control-label">Quantity</label>
<div class="col-sm-6">
<input type="number" name="input" id="quantityInput" ng-model="shipping.quantity" min="1" required>
</div>
</div>
<div class="form-group">
<label for="shippingButton" class="col-sm-2 control-label"> </label>
<div class="col-sm-6">
<button id="shippingButton" ng-click="shipping.calculateShipping()" class="btn1">Calculate</button>
</div>
</div>
<div class="form-group">
<label for="shippingButton" class="col-sm-2 control-label"> </label>
<div class="col-sm-8" ng-if="shipping.calculateShippingResponse" ng-animate="'example'">
{{shipping.quantity}} item(s), shipping from {{cities.chosen}} to {{destinations.chosen}}:
${{shipping.calculateShippingResponse}}
</div>
<div class="col-sm-8" ng-if="error" ng-animate="'example'">
{{error}}
</div>
</div>
</fieldset>
</form>
The salient points:
- The two select controls show AngularJS data binding to collections originating from REST. The first collection is 'cities.sources' and the second cascades from the chosen city ('cities.chosen') into 'destinations.destinations.'
- The 'Calculate' button shows binding to the 'shipping.calculateShipping' function in the controller.
- There exist two conditionally rendered response divs that show how binding to various elements can drive the page content model.
controllers.js
The embodyment of the application flow.
'use strict';
/* Controllers */
angular.module('cascadeSelects.controllers', []).
controller('CascadeSelectsController', ['$scope', 'Cities', function($scope, Cities) {
$scope.error = undefined;
// TODO: something better needed here
function error(r) {
$scope.error = "Error: " + r.status + ". Message: " + r.data;
}
// Instantiate an object to store your scope data in (Best Practices)
// http://coder1.com/articles/consuming-rest-services-angularjs
$scope.cities = {
sources: null,
chosen: null
};
$scope.destinations = {
destinations: null,
chosen: null
};
$scope.shipping = {
calculateShipping: function() {
Cities.calculate.get({origin: $scope.cities.chosen, destination: $scope.destinations.chosen,
quantity: $scope.shipping.quantity}, function(response) {
$scope.shipping.calculateShippingResponse = response.calculate.result;
}, function(httpResponse) {
console.log(error(httpResponse));
})
},
calculateShippingResponse: undefined,
quantity: 1
};
Cities.cities.query(function(response) {
$scope.cities.sources = response.sources;
$scope.cities.chosen = $scope.cities.sources[0]
}, function(httpResponse) {
console.log(error(httpResponse));
});
$scope.$watch("[shipping.quantity, destinations.chosen, cities.chosen]", function(newValue, oldValue, scope) {
$scope.shipping.calculateShippingResponse = undefined;
$scope.error = undefined;
}, true);
$scope.$watch("cities.chosen", function(newValue, oldValue, scope) {
if (newValue === null)
return;
Cities.destinations.query({source: newValue}, function(response) {
$scope.destinations.destinations = response.destinations;
$scope.destinations.chosen = $scope.destinations.destinations[0]
}, function(httpResponse) {
console.log(error(httpResponse));
}
)
}, true);
}]);
Notable points:
- Nothing is bound directly to $scope; everything is handled using a "poor man's namespace" mechanism, instead. This is easier to read and also less likely to suffer conflict with other parts in a large application.
- Note how $watch allows for cascading the selects.
- Note that 'cities.chosen' is watched more than once. This allows a slightly more structured approach to having multiple tasks operate in response to a single stimulus.
- Where the HTML defines data binding to various elements, the controller is the entity that populates $scope with these necessary elements.
- There is a little, very simple, error handling in play here.
As a structural nicety, the controller calls on a related service to do the REST heavy lifting.
services.js
The service uses AngularJS' $resource service to handle the low-level details of driving HTTP and interacting with a restful resource.
'use strict';
/* Services */
angular.module('cascadeSelects.services', ['ngResource']).
factory("Cities", function($resource){
var context = '/web_war';
return {
cities: $resource(context + '/rest/cities', {}, {
query: {method: 'GET', params: {}, isArray: false}
}),
destinations: $resource(context + '/rest/cities/:source/destinations', {}, {
query: {method: 'GET', params: {source: '@source'}, isArray: false}
}),
calculate: $resource(context + '/rest/shipping/calculate', {}, {
get: {method: 'GET', params: {origin: '@origin', destination: '@destination', quantity: '@quantity'}, isArray: false}
})
};
})
.value('version', '1.0');
Noteworthy here are:
- The application's '/web_war' context is hard coded. Yuk! Bad, bad Bob! It is possible to get it
from incoming request but I didn't bother for this application. Put it down to laziness, again.
- The use of $resource for REST interactions.
- This service handles multiple URLs. This is a nice feature that's not really documented anywhere
'official', as far as I can see.
- The convention whereby the URL definition ':var' is fulfilled using the parameter '@var'. Something else not actually documented as far as I could see, so thank you stackoverflow.
So there you are: a working application showing how to do REST-driven cascading selects using AngularJS. Hopefully in the future, the various search engines will be able to point any needy developer this way.
Toy Time
But wait! There's more!
Given that there is no schema equivalent for JSON/REST, the need for good documentation is paramount. Given, too, that devs (including yours truly) are dreadful at writing doco, there is a need for a good toy to help us document our
beautiful APIs.
"MireDot is a REST API documentation generator for Java that generates beautiful interactive documentation with minimal configuration effort."
How does MireDot go for this application? Like this:
Refer back to the Shipping resource. You can see how Miredot reads the Javadoc comments and incorporates them into the generated doco. Miredoc also supplies a few helpful Javadoc annotations (@servicetag/@statuscode) and also some
'real' annotations to guide the generation process. Miredot also has its own set of configuration options, and this can alter what is generated as well.
Not too shabby!
I'm not sure I like the abstract way that the JSON payload is shown in this case but it is tweakable and it's early days for the product.
I found Miredot easy to get and setup and when I corresponded with the developers (sending a bug report and a few opinions), they were courteous and responsive. What's not to like!
There's a free and a paid version. I played with the free version. Check it out!
(NB: I'm not affiliated in any way.)
You can download the IntelliJ project (sans Miredot configuration, please note), should you so desire.