Frogr - RESTful Graphdatabase Java framework

Frogr is a Java framework for developing high-performance RESTful web services.

With Frogr you can get a service up and running in minutes, but there’s no limit in complexity. Frogr uses a repository pattern for seperating data, business logic and external interfaces. This approach makes it easy to test and extend your code.

Frogr packs multiple stable libraries into an easy to use, light-weight framework, so you won’t have tons of additional dependencies, you probably never will need.

Java:

PersonRepository repository = service().repository(Person.class);
List<Person> persons = personRepository.search()
  .query("*Smith"))
  .fields("name", "children.name")
  .list()

REST:

GET localhost:8282/persons?q=*Smith&fields=name,children.name
{
  "success": true,
  "data": [
    {
      "uuid": "99eaeddc012811e883fda165f97f8411",
      "type": "Person",
      "name": "Beth Smith",
      "children": [
        {
          "uuid": "99eb630e012811e883fd97343769a63e",
          "type": "Person",
          "name": "Morty Smith"
        },
        {
          "uuid": "99ebb12f012811e883fd2583ad59c919",
          "type": "Person",
          "name": "Summer Smith"
        }
      ]
    },
    {
      "uuid": "99eb3bfd012811e883fdd551cdefd5ed",
      "type": "Person",
      "name": "Jerry Smith",
      "children": [
        {
          "uuid": "99eb630e012811e883fd97343769a63e",
          "type": "Person",
          "name": "Morty Smith"
        },
        {
          "uuid": "99ebb12f012811e883fd2583ad59c919",
          "type": "Person",
          "name": "Summer Smith"
        }
      ]
    },
    {
      "uuid": "99eb630e012811e883fd97343769a63e",
      "type": "Person",
      "name": "Morty Smith",
      "children": []
    },
    {
      "uuid": "99ebb12f012811e883fd2583ad59c919",
      "type": "Person",
      "name": "Summer Smith",
      "children": []
    }
  ]
}

Quickstart

First, add the dependency to your project.

Maven

<dependency>
    <groupId>de.whitefrog</groupId>
    <artifactId>frogr-base</artifactId>
    <version>0.2.2</version>
</dependency>

Application

Next we will create the main entry point for the service.

public class MyApplication extends Application<Configuration> {
  private MyApplication() {
    // register the rest classes
    register("de.whitefrog.frogr.example.basic.rest");
    // register repositories and models
    serviceInjector().service().register("de.whitefrog.frogr.example.basic");
    // use common config instead of the default config/neo4j.properties
    serviceInjector().service().setConfig("../config/neo4j.properties");
  }
  
  @Override
  public String getName() {
    return "frogr-base-example";
  }

  public static void main(String[] args) throws Exception {
    new MyApplication().run("server", "../config/example.yml");
  }
}

As you can see there are two registry calls in the application’s constructor. register(...) let’s the application know in which package to look for rest classes. serviceInjector().service().register(...) tells the application where to look for models and repositories. More information about the Application entry point: Application

Configs

You may also have noticed there’s a config file used in the main method. This is required to setup our Dropwizard instance, so we have to create that one now. There’s a second config file needed, which configures our embedded Neo4j instance. By default these configs should be in your project in a directory ‘config’.

config/example.yml

server:
    applicationConnectors:
      - type: http
        port: 8282
    adminConnectors:
      - type: http
        port: 8286
logging:
    level: WARN
    loggers:
        de.whitefrog.frogr: INFO
        io.dropwizard.jersey.DropwizardResourceConfig: INFO
        io.dropwizard.jersey.jackson.JsonProcessingExceptionMapper: DEBUG
    appenders:
      # console logging
      - type: console
        logFormat: '[%d] [%-5level] %logger{36} - %msg%n'

Reference: Dropwizard Configuration

config/neo4j.properties

graph.location=target/graph.db

This file is not required, by default the graph.location is “graph.db” inside your working directory. Reference: Neo4j Configuration

RelationshipTypes

We should add a class that holds our relationship types, so that we have consistent and convienient access. This is not a requirement but I highly recommend it. Doing so we don’t have to deal with strings in Java code, which is never a good choice, right?

object RelationshipTypes {
  const val ChildOf = "ChildOf"
  const val MarriedWith = "MarriedWith"
}

Model

Now, let’s create a model. I recommend using Kotlin for that. All models have to extend the Entity class or implement the Model interface at least.

class Person() : Entity() {
  constructor(name: String) : this() {
    this.name = name
  }
  
  // Unique and required property
  @Unique
  @Indexed(type = IndexType.LowerCase)
  @Required
  var name: String? = null

  // Relationship to another single model
  @RelatedTo(type = RelationshipTypes.MarriedWith, direction = Direction.BOTH)
  var marriedWith: MarriedWith? = null
  // Relationship to a collection of models
  @Lazy
  @RelatedTo(type = RelationshipTypes.ChildOf, direction = Direction.OUTGOING)
  var parents: List<Person> = ArrayList()
  @Lazy
  @RelatedTo(type = RelationshipTypes.ChildOf, direction = Direction.INCOMING)
  var children: List<Person> = ArrayList()
  
  companion object {
    @JvmField val Name = "name"
    @JvmField val MarriedWith = "marriedWith"
    @JvmField val Children = "children"
  }
}

As you can see, we used the relationship types created before, to declare our relationships to other models.

Repository

Normally we would create a repository for persons. But we won’t need extra methods for this tutorial and frogr will create a default repository if it can’t find one. If you need more information visit Repositories.

Service

Next we’ll have to create the REST service layer. There’s a base class, that provides basic CRUD operations, so you only have to add methods for special cases. Of course you can also use any other JAX-RS annotated class.

@Path("person")
public class Persons extends CRUDService<PersonRepository, Person> {
  @GET
  @Path("init")
  public void init() {
    // insert some data
    try(Transaction tx = service().beginTx()) {
      if(repository().search().count() == 0) {
        repository().init();
        tx.success();
      }
    }
  }
}

Examples

You can find the code used in this guide and more examples at Github

Documentation

Application

In our main Application class, we can implement additional features, such as a custom Service implementation, metrics, or authorization. See the chapter on Authorization and Security for further information.

public class MyApplication extends Application<Configuration> {
  private MyServiceInjector serviceInjector;
  
  private MyApplication() {
    // register the rest classes
    register("de.whitefrog.frogr.example.customservice.rest");
    // register repositories and models
    serviceInjector().service().register("de.whitefrog.frogr.example.customservice");
  }

  // override to pass our own ServiceInjector implementation
  @Override
  public ServiceInjector serviceInjector() {
    if(serviceInjector == null) {
      serviceInjector = new MyServiceInjector();
    }
    return serviceInjector;
  }

  @Override
  public void run(Configuration configuration, Environment environment) throws Exception {
    super.run(configuration, environment);

    // bind the custom ServiceInjector to our Service implementation, described below
    environment.jersey().register(new AbstractBinder() {
      @Override
      protected void configure() {
        bindFactory(serviceInjector()).to(MyService.class);
      }
    });

    // register metrics
    environment.jersey().register(new InstrumentedResourceMethodApplicationListener(RestService.metrics));

    // add a console reporter for the metrics
    final ConsoleReporter reporter = ConsoleReporter.forRegistry(RestService.metrics)
      .convertRatesTo(TimeUnit.SECONDS)
      .convertDurationsTo(TimeUnit.MILLISECONDS)
      .build();
    // report every 30 minutes
    reporter.start(30, TimeUnit.MINUTES);

    // add a logger reporter for the metrics
    final Slf4jReporter slf4j = Slf4jReporter.forRegistry(RestService.metrics)
      .outputTo(LoggerFactory.getLogger("com.example.metrics"))
      .convertRatesTo(TimeUnit.SECONDS)
      .convertDurationsTo(TimeUnit.MILLISECONDS)
      .build();
    // report every 30 minutes
    slf4j.start(30, TimeUnit.MINUTES);
  }

  @Override
  public String getName() {
    return "example-rest";
  }

  public static void main(String[] args) throws Exception {
    new MyApplication().run("server", "config/example.yml");
  }
}

Service Injector

We can also write our own ServiceInjector, in case we want to override the base Service.

public class MyServiceInjector extends ServiceInjector {
  private MyService service;

  public MyServiceInjector() {
    service = new MyService();
  }

  @Override
  public Service service() {
    return service;
  }

  @Override
  public Service provide() {
    if(!service.isConnected()) service.connect();
    return service;
  }

  @Override
  public void dispose(de.whitefrog.frogr.Service service) {
    service.shutdown();
  }
}

In that case, Service is our own implementation and should extend de.whitefrog.frogr.Service.

Service

For instance if we want to provide a common configuration accessible from any Repository or Service:

public class MyService extends Service {
  private static final Logger logger = LoggerFactory.getLogger(MyService.class);
  private static final String appConfig = "config/myapp.properties";

  private Configuration config;

  @Override
  public void connect() {
    try {
      config = new PropertiesConfiguration(appConfig);
      super.connect();
    } catch(ConfigurationException e) {
      logger.error(e.getMessage(), e);
      throw new RuntimeException(e);
    }
  }

  public Configuration config() { return config; }
}

Models

Models define the data structure, used for our project. Each model has at least extend the Model interface.

Entities

For entities there’s a base class Entity that implements all needed methods for us.

Entity models primarily consists of field and relationship definitions that define how it is used inside our project. I recommend using Kotlin for models, because I’m lazy and hate to write getter and setter methods, but thats up to you ;)

Relationships

For relationships we can use the BaseRelationship<From, To> class.

We can use every annotated field entities are using, except @RelatedTo and @RelationshipCount.

Fields

Model fields should always be initialized with null, so that the persistence layer can properly decide if the value is relevant for storing to database. Also we should not use primitive types here.

Fields can be annotated with hibernate annotations extended by a set of unique ones. These are:

@Fetch
Indicates that a field should be automatically fetched.
@Indexed(type=IndexType.Default)
Indicates that a field should be handled by an index.
type can be either IndexType.Default or IndexType.LowerCase for case-insensitive matches.
@Lazy
Indicator for lists to fetch them lazily on demand, not every list item at once. Using this will NOT delete relationships when one is missing in save operations. We have to delete them manually, when needed.
@NotPersistant
The field should not be persisted.
@NullRemove
Remove a property if set to null. Note that this will also delete the property if we save a model where the @NullRemove property is not fetched.
@RelatedTo(type=None, direction=Direction.OUTGOING, multiple=false, restrictType=false)
The field represents a relation to another or the same model.
type has to be set to the relationship type name.
direction defaults to an outgoing relationship, but can be also incoming or even both.
multiple allows multiple relationships to the same model.
restrictType restricts the type. Used when the same relationship type is used for multiple model relationships.

@RelationshipCount(type=None, direction=Direction.OUTGOING, otherModel=Model.class)
The field should contain the relationship count for a specified relationship type when fetched. Will not be persisted.
type has to be set to the relationship type name.
direction defaults to an outgoing relationship, but can be also incoming or even both.
otherModel is used to only query for specific models.

@Required
The field is required upon storing to database. If it is missing an exception will be thrown.
@Unique
The field is unique across all models of the same type. Will be indexed as well. If a duplicate value is passed an exception will be thrown.
@Uuid
Auto-generated uuid field. This should not be required as theres always an uuid field on each model.

Repositories

Repositories are used to communicate with the underlying database. We won’t need to create a repository for each model. There’s a default implementation, that will be used if no appropriate repository was found which provides basic functionality.

The naming of the repository is important, so that Frogr can find it. Names should start with the model name and end with “Repository” (case-sensitive) and it should extend BaseModelRepository or BaseRelationshipRepository.

If, for example, we want a repository for the Person model, we would create a repository called PersonRepository:

public class PersonRepository extends BaseModelRepository<Person> {
  public void init() {
    Person rick = new Person("Rick Sanchez");
    Person beth = new Person("Beth Smith");
    Person jerry = new Person("Jerry Smith");
    Person morty = new Person("Morty Smith");
    Person summer = new Person("Summer Smith");
    // we need to save the people first, before we can create relationships
    save(rick, beth, jerry, morty, summer);

    rick.setChildren(Arrays.asList(beth));
    beth.setChildren(Arrays.asList(morty, summer));
    jerry.setChildren(Arrays.asList(morty, summer));
    save(rick, beth, jerry, morty, summer);
    MarriedWith marriedWith = new MarriedWith(jerry, beth);
    marriedWith.setYears(10L);
    service().repository(MarriedWith.class).save(marriedWith);
  }
}

We can access our repository easily by calling the .repository(..) method of the Service instance. The method takes either the model class or the name as string. There’s access to it in any REST service class and inside any repository:

@Path("person")
public class Persons extends CRUDService<PersonRepository, Person> {
  @GET
  @Path("init")
  public void init() {
    // insert some data
    try(Transaction tx = service().beginTx()) {
      if(repository().search().count() == 0) {
        repository().init();
        tx.success();
      }
    }
  }
  
  @GET
  @Path("custom-search")
  public List<Person> customSearch(@SearchParam SearchParameter params) {
    try(Transaction ignored = service().beginTx()) {
      return repository().search().params(params).list();
    }
  }
}

Services

Services define the REST entry points of our application. They should only have minimal logic and communicate with the repositories instead.

Services can extend the CRUDService class, which provides basic CRUD methods for creating, reading, updating and deleting models. They also can extend the RestService class, which only provides some minimal convenience methods. But we can also create a service from scratch.

Here’s a basic example of a service, used for the Person models:

@Path("persons")
public class Persons extends CRUDService<PersonRepository, Person> {
  @GET
  @Path("init")
  public void init() {
    // insert some data
    try(Transaction tx = service().beginTx()) {
      if(repository().search().count() == 0) {
        repository().init();
        tx.success();
      }
    }
  }
}

And here’s the repository implementation:

public class PersonRepository extends BaseModelRepository<Person> {
  public void init() {
    Person rick = new Person("Rick Sanchez");
    Person beth = new Person("Beth Smith");
    Person jerry = new Person("Jerry Smith");
    Person morty = new Person("Morty Smith");
    Person summer = new Person("Summer Smith");
    // we need to save the people first, before we can create relationships
    save(rick, beth, jerry, morty, summer);

    rick.setChildren(Arrays.asList(beth));
    beth.setChildren(Arrays.asList(morty, summer));
    beth.setMarriedWith(jerry);
    jerry.setChildren(Arrays.asList(morty, summer));
    jerry.setMarriedWith(beth);
    save(rick, beth, jerry, morty, summer);
  }
}

This would create these REST paths:

POST    /persons (de.whitefrog.frogr.example.rest.Persons)
GET     /persons (de.whitefrog.frogr.example.rest.Persons)
GET     /persons/{uuid: [a-zA-Z0-9]+} (de.whitefrog.frogr.example.rest.Persons)
POST    /persons/search (de.whitefrog.frogr.example.rest.Persons)
PUT     /persons (de.whitefrog.frogr.example.rest.Persons)
DELETE  /persons/{uuid: [a-zA-Z0-9]+} (de.whitefrog.frogr.example.rest.Persons)
GET     /persons/init (de.whitefrog.frogr.example.rest.Persons)

The POST and PUT methods both take a json object, representing the model to create or update.

The GET method takes url parameters used for a search operation. See Searches for further details. Additionally there’s also a GET /{uuid} method for convenience, which searches for a particular person.

We can also use POST /search path, which takes the search parameters as a json object, but I strongly recommend using GET for that purpose, as only GET can be cached correctly.

DELETE has the UUID inside its path and will delete the model with that UUID.

We can also see our previously implemented /init path configured.

We use primarily UUIDs to reference models, not Ids as they can be reused by the underlying Neo4j database.

Searching

Java

In Java code there’s a easy to use method in each repository. Here are some examples:

List<Person> results = search()
  .uuids(uuid1, uuid2)
  .fields(Person.Name, Person.MarriedWith)
  .list();

// Get a count of persons, where on of its parents name is "Jerry Smith".
long count = search()
  .filter(new Filter.Equals("parents.name", "Jerry Smith"))
  .count();

// Get a paged result of all persons, with a page size of 10, ordered by the name property.
List<Person> page = search()
  .limit(10)
  .page(1)
  .orderBy(Person.Name)
  .fields(Person.Name)
  .list();

// Get a single person and its children with their names.
Person beth = search()
  .filter(Person.Name, "Beth Smith")
  .fields(FieldList.parseFields("name,children.name"))
  .single();

REST

Over HTTP you would normally use a CRUDService, that provides the neccessary methods, but we can of course write our own ones. Here are some examples, that would return the same results as the Java queries above:

// Filter results by uuids and return the name and the person married with the found person.
http://localhost:8282/persons?uuids=e4633739050611e887032b418598e63f,e4635e4a050611e88703efbc809ff2fd&fields=name,marriedWith
{
  "success": true,
  "data": [
    {
      "uuid": "e4633739050611e887032b418598e63f",
      "type": "Person",
      "name": "Beth Smith",
      "marriedWith": {
        "uuid": "e4635e4a050611e88703efbc809ff2fd",
        "type": "Person"
      }
    },
    {
      "uuid": "e4635e4a050611e88703efbc809ff2fd",
      "type": "Person",
      "name": "Jerry Smith",
      "marriedWith": {
        "uuid": "e4633739050611e887032b418598e63f",
        "type": "Person"
      }
    }
  ]
}
// Get a count of persons, where on of its parents name is "Jerry Smith".
http://localhost:8282/persons?filter=parents.name:=Jerry%20Smith&count
{
  "success": true,
  "total": 2,
  "data": [
    {
      "uuid": "e463d37c050611e887034f42b099b0cd",
      "type": "Person"
    },
    {
      "uuid": "e463ac6b050611e887038de1cbd926c1",
      "type": "Person"
    }
  ]
}
// Get a paged result of all persons, with a page size of 10, ordered by the name property.
http://localhost:8282/persons?limit=10&page=1&order=name&fields=name
{
  "success": true,
  "data": [
    {
      "uuid": "e4633739050611e887032b418598e63f",
      "type": "Person",
      "name": "Beth Smith"
    },
    {
      "uuid": "e4635e4a050611e88703efbc809ff2fd",
      "type": "Person",
      "name": "Jerry Smith"
    },
    {
      "uuid": "e463ac6b050611e887038de1cbd926c1",
      "type": "Person",
      "name": "Morty Smith"
    },
    {
      "uuid": "e4607818050611e8870361190053d169",
      "type": "Person",
      "name": "Rick Sanchez"
    },
    {
      "uuid": "e463d37c050611e887034f42b099b0cd",
      "type": "Person",
      "name": "Summer Smith"
    }
  ]
}
http://localhost:8282/persons?filter=name:=Beth%20Smith&fields=name,children.name
{
  "success": true,
  "data": [
    {
      "uuid": "e4633739050611e887032b418598e63f",
      "type": "Person",
      "name": "Beth Smith",
      "children": [
        {
          "uuid": "e463ac6b050611e887038de1cbd926c1",
          "type": "Person",
          "name": "Morty Smith"
        },
        {
          "uuid": "e463d37c050611e887034f42b099b0cd",
          "type": "Person",
          "name": "Summer Smith"
        }
      ]
    }
  ]
}

Usage in services

If we want to write a method that takes its own search parameters, we can use the @SearchParam annotation along with a SearchParameter argument:

@Path("person")
public class Persons extends CRUDService<PersonRepository, Person> {
  @GET
  @Path("custom-search")
  public List<Person> customSearch(@SearchParam SearchParameter params) {
    try(Transaction ignored = service().beginTx()) {
      return repository().search().params(params).list();
    }
  }
}

Parameters

These are the possible querystring parameters, but you can find nearly identical methods in Java too.

uuids
Comma-seperated list of uuids to search.
query
Searches all indexed fields for a query string.
count
Add a total value of found records, useful if the result is limited.
start
Start returning results at a specific position, not required when page is set.
limit
Limit the results.
page
Page to return. Takes the limit parameter and sets the cursor to the needed position.
filter/filters
Filter to apply. Filters start with the field name, followed by a : and the comparator. Valid comparators are:
= Equal
! Not equal
< less than
<= less or equal than
> greater than
>= greater or equal than
({x}-{y}) in a range between x and y

fields
Comma-seperated list of fields to fetch. Can also fetch sub-fields of related models seperated by a ., for example children.name would fetch all childrens and their names. Multiple sub-fields can be fetched inside curly braces, children.{name,age} would fetch all childrens and their names and ages.
return/returns
Returns a related model instead of the service model.
order/orderBy/sort
Comma-seperated list of fields on which the results are sorted. - before the field sorts in descending, + in ascending direction. If bypassed ascending direction is used.

Authorization and Security

Setup

Maven
<dependency>
    <groupId>de.whitefrog</groupId>
    <artifactId>frogr-auth</artifactId>
    <version>0.2.2</version>
</dependency>
User Model

The User model has to extend BaseUser and defines our user, which can be passed in Service methods using the @Auth annotation.

class User : BaseUser() {
  @JsonView(Views.Secure::class)
  @RelatedTo(type = RelationshipTypes.FriendWith)
  var friends: ArrayList<User> = ArrayList()
}

As you can see the annotation @JsonView(Views.Secure::class) is used on the friends field. These views can be used on Service methods too, and describe what can be seen by the user. The default is Views.Public::class, so any field annotated with that @JsonView is visible to everyone. Fields without @JsonView annotation are always visible.

BaseUser provides some commonly used fields describing a user in an authentication environment:

/**
 * Base user class required for Authentication.
 */
open class BaseUser : Entity(), Principal {
  /**
   * Unique and required field.
   */
  @Unique @Fetch @Required 
  open var login: String? = null

  /**
   * The password as string.
   */
  @JsonView(Views.Hidden::class)
  open var password: String? = null

  /**
   * [Role] in which the user is
   */
  @JsonView(Views.Hidden::class)
  open var role: String? = null

  /**
   * Used for oAuth user authentication.
   */
  @Indexed @JsonView(Views.Secure::class)
  var accessToken: String? = null

  override fun getName(): String? {
    return login
  }

  override fun implies(subject: Subject?): Boolean {
    return subject!!.principals.any { p -> (p as BaseUser).id == id }
  }

  override fun equals(other: Any?): Boolean {
    return super<Entity>.equals(other)
  }

  companion object {
    const val Login = "login"
    const val Password = "password"
    const val AccessToken = "accessToken"
    const val Roles = "role"
  }
}

You can write your own User class, but then you’ll have to create your own oAuth implementation.

Warning: Be cautious with Views.Secure on Service methods, as it could reveal sensitive data. So it’s best to have custom methods like findFriendsOfFriends for example to get all friends of the users friends instead of the common search function.

User Repository

Next, we’ll have to define a repository for our users, extending BaseUserRepository:

public class UserRepository extends BaseUserRepository<User> {
  public String init() {
    String token;
    PersonRepository persons = service().repository(Person.class);
    if(search().count() == 0) {
      Person rick = new Person("Rick Sanchez");
      Person beth = new Person("Beth Smith");
      Person jerry = new Person("Jerry Smith");
      Person morty = new Person("Morty Smith");
      Person summer = new Person("Summer Smith");
      // we need to save the people first, before we can create relationships
      persons.save(rick, beth, jerry, morty, summer);

      rick.setChildren(Arrays.asList(beth));
      beth.setChildren(Arrays.asList(morty, summer));
      beth.setMarriedWith(jerry);
      jerry.setChildren(Arrays.asList(morty, summer));
      jerry.setMarriedWith(beth);
      persons.save(rick, beth, jerry, morty, summer);

      User user = new User();
      user.setLogin("justin_roiland");
      user.setPassword("rickandmorty");
      user.setRole(Role.Admin);
      save(user);
      
      // login to create and print an access token - for test purposes only
      user = login("justin_roiland", "rickandmorty");
      token = "access_token=" + user.getAccessToken();
      System.out.println("User created. Authorization: Bearer " + user.getAccessToken());
    } else {
      User user = login("justin_roiland", "rickandmorty");
      token = "access_token=" + user.getAccessToken();
      System.out.println("Authorization: Bearer " + user.getAccessToken());
    }
    return token;
  }
}

The extended class BaseUserRepository provides some basic functionality and security.

register(user)
Registration of a new user, passwords will be encrypted by default.
login(login, password)
Login method, encrypts the password automatically for you.
validateModel(context)
Overridden to ensure a password and a role is set on new users.
Application

In our applications run method, we need to set up some authentication configurations:

public class MyApplication extends Application<Configuration> {
  private MyApplication() {
    // register the rest classes
    register("de.whitefrog.frogr.example.oauth");
    // register repositories and models
    serviceInjector().service().register("de.whitefrog.frogr.example.oauth");
    
  }

  @Override
  @SuppressWarnings("unchecked")
  public void run(Configuration configuration, Environment environment) throws Exception {
    super.run(configuration, environment);

    Authorizer authorizer = new Authorizer(service().repository(User.class));
    AuthFilter oauthFilter = new OAuthCredentialAuthFilter.Builder<User>()
      .setAuthenticator(new Authenticator(service().repository(User.class)))
      .setAuthorizer(authorizer)
      .setPrefix("Bearer")
      .buildAuthFilter();

    environment.jersey().register(RolesAllowedDynamicFeature.class);
    environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
    environment.jersey().register(new AuthDynamicFeature(oauthFilter));
  }

  @Override
  public String getName() {
    return "frogr-auth-example-rest";
  }

  public static void main(String[] args) throws Exception {
    new MyApplication().run("server", "config/example.yml");
  }
}

Inside the run method, we set up RolesAllowedDynamicFeature, which activates the previously used @RolesAllowed annotation. We also set up AuthValueFactoryProvider.Binder which activates the later described @Auth injection annotation and AuthDynamicFeature which activates the actual oAuth authentication.

Services

Here’s a simple service, that can only be called when the user is authenticated. The user will be passed as argument to the method:

@Path("person")
public class Persons extends AuthCRUDService<PersonRepository, Person, User> {
  @GET
  @Path("find-morty")
  @RolesAllowed(Role.User)
  public Person findMorty(@Auth User user) {
    try(Transaction ignored = service().beginTx()) {
      return repository().findMorty();
    }
  }
}

See how the @RolesAllowed(Role.User) annotation is used, to only allow this method to registered users. You can always extend the Role class and use your own roles. Predefined roles are Admin, User and Public.

The first (and only) parameter on findMorty is annotated with @Auth and has the type of our User class created before. This will inject the currently authenticated user and also tells the application that this method is only allowed for authenticated users.

The extended class AuthCRUDService provides some convenient methods for authentication and sets some default @RolesAllowed annotations on the basic CRUD methods. All predefined methods are only allowed for registered and authenticated users.

authorize
Override to implement your access rules for specific models. Is used by default in create and update methods.
authorizeDelete
Override to implement your rules to who can delete specific models. Is used by default in delete method.

License

The MIT License (MIT)

Copyright (c) 2018 Jochen Weis

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Contact

Questions? Please contact jochen@whitefrog.de

Help

If you have any questions, contact jochen@whitefrog.de