Forget NoSQL – we’re going NoDB in this post!

Stormpath’s hosted Identity Management solution includes a feature called CustomData . It’s basically an unstructured JSON datastore tied to each of your Stormpath Entities, such as Accounts and Groups.

The other day, a bunch of us at Stormpath were talking about our favorite books. We thought it would be cool to both have a list of these books and order them by popularity. So we set up a private Subreddit to do just that.

Only, some of us here at Stormpath don’t have Reddit accounts. Some of us, don’t even like Reddit (not naming names, here).

So I thought: “I wonder how hard it would be to implement this book voting system in Stormpath using CustomData instead of a database?”

Well, the answer is it took a couple of days to create, coding it outside of my normal working hours (What does that even mean in a startup?)

Before we get to building the code sample, let’s examine some pros and cons of this approach:

Cons:

Must make API calls to access CustomData (see Pros) Limit of 10MB per Stormpath Entity (pretty generous, actually)

Pros:

No database for you to worry about Easy to optimize API calls to make as few as possible CustomData implements Java Map interface for easy conversion to your own entities

Learn more about Custom Data Search with Stormpath in our upcoming webinar!

Building The Spring Boot Voting App Sample

The code that goes along with this post is here.

If you have a Stormpath account set up and you want to see this in action right away, click the Deploy button below.

What Is Stormpath?

Stormpath is a complete customer identity API. It gives you powerful authentication, authorization, and user management for any application.

You can check out the docs for a variety of platforms here.

Follow the Java Quickstart to create a Stormpath account. That’s all you need to get going!

What the Heck is CustomData?

Stormpath has up to 10MB of text based CustomData attached to just about every Stormpath Entity. For example, Every Stormpath Directory, Group and Account can have

CustomData attached to it.

The actual data is schema-less JSON. Ordinarily, this would be data would have something to do with your identity management needs. But, there’s no rule book and for this sample, we’ll be storing book data.

Setup your Stormpath Application and Groups

To exercise this app, you’ll need to create a Stormpath Application, as well as an Admin Group and a User Group. Make note of the href of each of these.

We’ll use them later.

Anyone – even someone who is not logged in – can see the list of books ordered by upvote.

Anyone can create an account.

Only members of the User Group you specify can add new books. For the sake of making the app as hands-off as possible, authenticated users can add themselves to the User Group.

Only members of the Admin Group you specify can rebuild the book list. More on that later.

Here’s what this looks like through the view of the Stormpath Admin Console:

Data Structure For Your CustomData

We’re going to use a particular key so as not to interfere with any other CustomData you might want to have. Here’s a sample of what it looks like:

{ "books": [ { "author": "Dan Suarez", "title": "Daemon", "url": "http://www.amazon.com/DAEMON-Daniel-Suarez/dp/0451228731/ref=sr_1_1?ie=UTF8&qid=1447719382&sr=8-1&keywords=daemon", "votes": 2 }, { "author": "Dan Suarez", "title": "Freedom (TM)", "url": "http://www.amazon.com/Freedom-TM-Daniel-Suarez/dp/0451231899/ref=sr_1_1?ie=UTF8&qid=1447795097&sr=8-1&keywords=freedom%28tm%29", "votes": 3 }, { "author": "Neal Stephenson", "title": "Snow Crash", "url": "http://www.amazon.com/Snow-Crash-Neal-Stephenson/dp/0553380958/ref=sr_1_1?ie=UTF8&qid=1447677686&sr=8-1&keywords=snow+crash", "votes": 3 } ] } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "books" : [ { "author" : "Dan Suarez" , "title" : "Daemon" , "url" : "http://www.amazon.com/DAEMON-Daniel-Suarez/dp/0451228731/ref=sr_1_1?ie=UTF8&qid=1447719382&sr=8-1&keywords=daemon" , "votes" : 2 } , { "author" : "Dan Suarez" , "title" : "Freedom (TM)" , "url" : "http://www.amazon.com/Freedom-TM-Daniel-Suarez/dp/0451231899/ref=sr_1_1?ie=UTF8&qid=1447795097&sr=8-1&keywords=freedom%28tm%29" , "votes" : 3 } , { "author" : "Neal Stephenson" , "title" : "Snow Crash" , "url" : "http://www.amazon.com/Snow-Crash-Neal-Stephenson/dp/0553380958/ref=sr_1_1?ie=UTF8&qid=1447677686&sr=8-1&keywords=snow+crash" , "votes" : 3 } ] }

The top level key is books . It’s an array of book data including the number of votes a book has.

The books you submit will be stored in the CustomData attached to your Stormpath Account.

Now, here’s the tricky bit: The CustomData attached to the the User Group you set up earlier will store the aggregated book data. Why? So that the app is responsive in returning the book data to you. Otherwise, it would need to gather up all the books CustomData from

each member of the User Group and build the book list on the fly.

Here’s how the CustomData looks in the Stormpath Admin Console:

Note: This is not a very scalable app. You would not want to store many thousands of books in Stormpath CustomData . But it is functional and fun to play with!

Build and Run Your Spring Boot Application

If you want to see this in action right away, use the Deploy button below.

To build and run locally, do the following:

mvn clean package STORMPATH_API_KEY_FILE=<path to your apiKey.properties file> \ STORMPATH_APPLICATION_HREF=<full href to your Stormpath Application> \ STORMPATH_AUTHORIZED_GROUP_USER_HREF=<full href to your Stormpath User Group> \ STORMPATH_AUTHORIZED_GROUP_ADMIN_HREF=<full href to your Stormpath Admin Group> \ java -jar target/*.jar 1 2 3 4 5 6 7 mvn clean package STORMPATH_API_KEY_FILE = < path to your apiKey . properties file > \ STORMPATH_APPLICATION_HREF = < full href to your Stormpath Application > \ STORMPATH_AUTHORIZED_GROUP_USER_HREF = < full href to your Stormpath User Group > \ STORMPATH_AUTHORIZED_GROUP_ADMIN_HREF = < full href to your Stormpath Admin Group > \ java - jar target /* . jar

How the Book Upvoting Application Works

Follow these steps to get going with the app:

Create an Account Verify your Account Login and Join the User Group Now you can post new books and upvote

You may ask yourself, “Why is step 3 even there?”

I wanted to demonstrate some of the awesome power of Spring Security. Only members of the User Group are allowed post new books. You also have to be a member of the group to upvote. This is protected both on the client side and the server side.

On the client side, you will not see the form to post a new book until you are a member of the user group.

All too often, I’ve seen application developers stop at the client side only to have script kiddies perform unauthorized actions on their site because they didn’t properly protect the server side.

Here’s how method level protection works on the server side:

@PreAuthorize("hasRole(@roles.GROUP_USER)") public void newBook(CustomData accountCustomData, CustomData groupCustomData, Book book) { ... } @PreAuthorize("hasRole(@roles.GROUP_USER)") public void upvote(CustomData accountCustomData, CustomData groupCustomData, Book book) { ... } 1 2 3 4 5 6 7 8 9 10 @ PreAuthorize ( "hasRole(@roles.GROUP_USER)" ) public void newBook ( CustomData accountCustomData , CustomData groupCustomData , Book book ) { . . . } @ PreAuthorize ( "hasRole(@roles.GROUP_USER)" ) public void upvote ( CustomData accountCustomData , CustomData groupCustomData , Book book ) { . . . }

The above is an excerpt from BookService.java . The @PreAuthorize lines makes it so that you can’t even enter the method unless you are a member of the Stormpath User Group.

This blog post has more on Stormpath’s Spring Security integration.

The final bit of functionality in this app is the ability to rebuild the master book list if you’re a member of the Admin Group. All this function does is iterate over the Account level CustomData and rebuild the Group level CustomData . This is a little demonstration of how easy it is to efficiently work with Stormpath objects.

This is what you’ll see if you’re a member of the Admin Group:

Here’s the method from BookService.java :

@PreAuthorize("hasRole(@roles.GROUP_ADMIN)") public void rebuildBookData(Application application, CustomData groupCustomData) { List<Book> books = new ArrayList<Book>(); AccountCriteria criteria = Accounts.criteria().withCustomData().withGroups(); AccountList accountList = application.getAccounts(criteria); for (Account account : accountList) { List<Book> accountBooks = getBooksFromCustomData(account.getCustomData()); for (Book book : accountBooks) { int index; if ((index = books.indexOf(book)) >= 0) { Book foundBook = books.get(index); foundBook.setVotes(foundBook.getVotes()+1); } else { book.setVotes(1); books.add(book); } } } groupCustomData.put("books", books); groupCustomData.save(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @ PreAuthorize ( "hasRole(@roles.GROUP_ADMIN)" ) public void rebuildBookData ( Application application , CustomData groupCustomData ) { List < Book > books = new ArrayList < Book > ( ) ; AccountCriteria criteria = Accounts . criteria ( ) . withCustomData ( ) . withGroups ( ) ; AccountList accountList = application . getAccounts ( criteria ) ; for ( Account account : accountList ) { List < Book > accountBooks = getBooksFromCustomData ( account . getCustomData ( ) ) ; for ( Book book : accountBooks ) { int index ; if ( ( index = books . indexOf ( book ) ) > = 0 ) { Book foundBook = books . get ( index ) ; foundBook . setVotes ( foundBook . getVotes ( ) + 1 ) ; } else { book . setVotes ( 1 ) ; books . add ( book ) ; } } } groupCustomData . put ( "books" , books ) ; groupCustomData . save ( ) ; }

Notice the @PreAuthorize on line 1. You can’t even enter this method unless you are a member of the Admin Group.

Line 5 makes all the difference in how efficiently we iterate over our collection of Stormpath Accounts. By having the .withCustomData() as part of the chained method calls, it ensures that every Account we fetch will also have the associated CustomData with it. There will not be an additional request over the wire on line 8 at account.getCustomData() because of this use of link expansion. If we did not have the .withCustomData() , every call to account.getCustomData() would trigger another request to Stormpath.

On Line 7, I’m just iterating over ALL the Accounts. What if there’s a million of them? Well, Stormpath automatically takes care of paging and fetching. The number of Accounts that are fetched at once is configurable and defaults to 50. So, I get to just have a familiar for loop and Stormpath takes care of all the messy pagination behind the scenes. Pretty cool, right?

That’s pretty much it. There’s not a ton of bells and whistles – yet!

Want to join our book club? Click here!

What the Application Is Not

It’s not a SPA (Single Page Application). Making an Angular app out of it would be a fun improvement.

It’s not meant to have tons of data. It should be very responsive with hundreds of books. It may start to become less responsive if you have thousands of books. Maybe I’ll do a little stress testing to see how it performs with a ridiculous number of books. I’ll keep you posted!

Feel free to drop a line over to email anytime.

Like what you see? Follow @goStormpath to keep up with the latest releases.