Application Development with BeAPI
The Beapi Springboot Starter has several built-in tools to help you get started that will automate most of the development/build for you. Below is an explaination of those tools and how to use them.
Prior to running the application, make sure you follow the configuration instructions.
build.gradle
When setting up a new project, your build.gradle file needs to include the following at a 'bare minimum'.
You can use the following as a template or just copy the file from beapi-java-demo:
repositories {
mavenLocal()
mavenCentral()
maven {
url 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
//url 'https://s01.oss.sonatype.org/content/repositories/releases'
}
}
...
dependencies {
...
implementation 'io.beapi:spring-boot-starter-beapi:0.7.0-SNAPSHOT'
implementation "org.springframework.boot:spring-boot-autoconfigure:2.6.2"
implementation "org.springframework.data:spring-data-jpa:2.6.2"
implementation "org.springframework.boot:spring-boot:2.6.2"
implementation("jakarta.persistence:jakarta.persistence-api:2.2.3")
implementation("org.apache.tomcat.embed:tomcat-embed-core:9.0.56")
implementation("org.slf4j:slf4j-api:1.7.32")
implementation("org.springframework:spring-beans:5.3.14")
implementation("org.springframework:spring-context:5.3.14")
implementation("org.springframework:spring-web:5.3.14")
...
}
ext.javaMainClass = "demo.application.Application"
springBoot {
buildInfo()
}
test {
useJUnitPlatform()
}
configurations {
commonWebResources
}
jar {
manifest {
attributes 'Main-Class': javaMainClass
}
}
NOTE: You will want to replace ext.javaMainClass with the path and name of your 'Application' class
Application Class
The Application class needs to be directly copied and moved into src/main/java or src/main/groovy of the project (depending on the language you are working in). See beapi-java-demo for an example
ALSO... prior to running the application, make sure you follow the configuration instructions
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.repository.config.BootstrapMode;
import org.springframework.boot.ApplicationArguments;
import demo.application.init.BootStrap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.slf4j.LoggerFactory;
import java.util.*;
import io.beapi.api.service.CliService;
import org.springframework.beans.factory.annotation.Value;
@ComponentScan({"${beapi.components}","${application.components}"})
@EnableJpaRepositories(basePackages = {"${beapi.repository}","${application.repository}"})
@SpringBootApplication(exclude = {HibernateJpaAutoConfiguration.class})
class Application implements ApplicationRunner {
@Value("${spring.profiles.active}")
String profile;
@Autowired
BootStrap bootStrap;
@Autowired
private CliService cliService;
@Autowired
ApplicationContext applicationContext;
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
public void run(ApplicationArguments args) throws Exception {
System.out.println("Running in : "+profile);
bootStrap.init(applicationContext);
cliService.parse();
}
}
The Controllers
When building a controller class, you require only two things : a @Controller annotation and to 'extend'' the BeapiRequestHandler class :
import io.beapi.api.controller.BeapiRequestHandler;
import org.springframework.stereotype.Controller;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Controller("user")
public class UserController extends BeapiRequestHandler {
@Autowired
private UserService userService;
...
}
Notice that the @Controller annotation has a value that matches the NAME in the corresponding I/O state file (which in turn matches how it is called in the url). This is how we map the controller to the I/O state and mapping.
The second thing you will see is an Autowired service. In most controllers you will have some service that enables you to access your repository. This is just good practice.
Controller Methods
Every API controller method MUST get two args: HttpServletRequest and HttpServletResponse. Lets walk through a simple method example:
public User show(HttpServletRequest request, HttpServletResponse response){
Long id = Long.parseLong(this.params.get("id"));
User usr = userService.findById(id);
if (Objects.nonNull(usr)) {
return usr;
}
return null;
}
First, you may notice that we are parsing something called this.params. There are also several variables available to the controller that are inherited through the BeapiRequestHandler:
- this.params - [LinkedHashMap<String,String>] params sent for this endpoint associated with this ROLE
- this.keyList - [Set] param keys sent for this endpoint associated with this ROLE
- this.controller - [String] current controller being called
- this.action - [String]current controller method being called
- this.apiversion - [String] api version sent in request
- this.authority - [String] ROLE of user sent in request
When returning data from your methods, you can return data as an:
- JPA ENTITY - See above controller method for example
- List / ArrayList / HashSet / Set
- Map / HashMap / LinkedHashMap
If you need examples, see the demo-application/src/main/java/demo/application/controller directory
Controller Methods with Multiple ROLES
In IO State files, we can enable endpoint to be called by multiple ROLES in the NETWORKGRP. Through this, we can then establish different REQUEST DATA to send & different RESPONSE DATA to return for each ROLE. This is what is commonly referred to as RBAC/ABAC
Below we have an example from an IO State file showing how it would look if we had BASIC NETWORKGRP PERMISSIONS (permitAll) as well as ROLE_ADMINpermissions:
"REQUEST": {
"permitAll": [],
"ROLE_ADMIN": ["id"]
},
"RESPONSE": {
"permitAll": ["id", "version", "username", "email", "enabled", "accountExpired"],
"ROLE_ADMIN": ["firstName","lastName"]
}
}
Now we can't just return the same data from our controller for both permissions as this would throw an error. We have to parse for each permission level:
@Autowired
protected PrincipleService principle;
...
public User show(HttpServletRequest request, HttpServletResponse response){
if(principle.authorities().contains(this.authority)){
// authority
switch(this.authority){
case 'ROLE_ADMIN':
Long userId = this.params.get("id")
User user = userService.findById(username);
break;
}
}else{
// permitAll
String username = principle.name();
User user = userService.findByUsername(username);
}
if (Objects.nonNull(user)) {
return user;
}
return null;
}
IO State
An IO State File is RULES / DESCRIPTIONS for your API endpoints ASSOCIATED a CONTROLLER; It is a cacheable configuration file that can then be synchronized with all application servers.
This enables all URI's (and their associated data) to be easily cached and this data to be shared with services in the architecture (ie Proxy, MQ, etc).
Examples of IO state files can be found in spring-boot-starter-beapi-config.
Variable Declaration
Lets break apart the pieces of an IOState file to see what they consist of and how they work; the first part of the file declares 'VALUES' for use by the endpoints:
"NAME": "user",
"HANDLER": "demo.application.controller.UserController",
"NETWORKGRP": "public",
"VALUES": {
"id": {
"key": "PKEY",
"type": "Long",
"description": "",
"mockData": "112"
},
"version": {
"type": "Long",
"description": "",
"mockData": "0"
}
...
We start by declaring the NAME of the file and our 'values':
- NAME (REQUIRED): This corresponds directly with the name of the controller(ie UserController); lowercase because that is how you will call it.
- NETWORKGRP (REQUIRED): 'networkGroup' is the group of 'roles' that can access it. See 'networkGroups' and 'networkRoles' in your beapi_api.yaml file for more
- VALUES (REQUIRED): These are the individual variables returned. These help us for automating testing, api docs and defining what needs to be sent/returned for each endpoint.
Underneath values, we then define each individual value to be sent/returned with the following stats:
- key (optional): One of [PKEY, FKEY]; this is used when the value is a ID(PKEY) or a reference to another table(FKEY)
- reference (optional): When key equals FKEY, we then supply the ENTITY name that 'reference'. You can see an example here.
- type (REQUIRED) : variable type. Merely a descriptor for the ApiDocs. Defines scope of variable for people implementing your API; be descriptive and accurate.
- description (REQUIRED) : description of variable, how it is used; mainly for apidocs
- mockData(REQUIRED) : string descriptor; Mock data for tests and apidoc.
Rules Declaration
Following this, we then define the 'rules' for our endpoints like so:
"CURRENTSTABLE": "1",
"VERSION": {
"1": {
"DEFAULTACTION":"list",
"URI": {
"create": {
"METHOD":"POST",
"DESCRIPTION":"Create new User",
"ROLES":{
"BATCH":["ROLE_ADMIN"],
"HOOK":["ROLE_ADMIN"]
},
"REQUEST": {
"permitAll":["username","password","email"]
},
"RESPONSE": {
"permitAll":["id","version"]
}
},
...
The above data has the following definitions:
- CURRENTSTABLE (REQUIRED): This is the current stable version of API's; if you you do not have multiple versions of this file, this number is the same as 'VERSION'
- VERSION (REQUIRED): This is the file version. This allows to API to have several versions of IOState if you are currently supported deprecated versions
- DEFAULTACTION (REQUIRED): This is the default endpoint action if if none given; commonly 'list'
- DEPRECATED (optional): Formated a MM/DD/YYYY, this field represents when this api version goes 'stale' and can no longer be used
- URI (REQUIRED): This is the endpoint and the rules for the endpoint.
Then under 'URI', you define the rules for the endpoint for the controller/method. In this case for the NAME:'user' and URI:'create' point to the controller/method 'person.create', which would be called like so:
http://<yourdomain>:8080/v{applicationVersion}/user/create
We will explain more about where the 'v1.0' comes under 'Versioning' but for right now, lets finish explaining the rest of the IOState:
- METHOD (REQUIRED): One of [GET, PUT, POST,DELETE]. This defines how the endpoint should be called.
- DESCRIPTION (REQUIRED): A description of the endpoint for the API Docs
- ROLES (REQUIRED): A grouping for security rules (SEE 'Security: Network Roles')
- BATCH (REQUIRED if enabled): Security roles for endpoints that have batching enabled
- HOOK (REQUIRED if enabled): Security roles for endpoints that have web hooks enabled
- REQUEST (REQUIRED): Per role data for expected when sending the request; concatenates across roles
- RESPONSE (REQUIRED): Per role data for expected when sending the request; concatenates across roles
Of note on REQUEST/RESPONSE and ROLES: one can set different access levels by concatenating privileges. Their is a default privilege level called 'permitAll' which will allow ALL privileges in the NetworkGrp access to this data. You can then allow more atomic access by setting additional values PER ROLE like so:
"REQUEST": {
"permitAll":["name"],
"ROLE_USER":["id"]
},
Empty PermitAll
If you are sending no values (such as a 'list' call) or returning no values, you will often have an empty 'permitAll' such as the following:
"REQUEST": {
"permitAll":[]
},
An empty 'permitAll' should NEVER be declared with quotes as this sends an empty string. Always leave the brackets empty like above to declare that nothing needs to be sent.
Troubleshooting
- If you are having issues with your build, try running './gradlew --stop;./gradlew clean;./gradlew build --stacktrace --refresh-dependencies'. This will give you a fresh gradle instance, show your errors and refresh all dependencies.