Aeonics Quick Tutorials
Introduction
This documentation is intended for developers, devops, fullstack engineers or whomever has sufficient background knowledge to understand it. It shows how to use certain concepts of the Aeonics SDK and quickly start with a first implementaton.
The content of this guide is protected by intellectual property rights and is subject to the Aeonics Commercial License Agreement. The information contained in this documentation is provided as-is without any guarantee of correctness nor any other sort, it is provided in good faith and is regularly checked and updated. If some content is not clear, outdated or misleading, please let us know and we will try to fix it. Some fine internal details are voluntarily omitted from this guide, if you need more information about a specific aspect, please contact us.
The form and language used in this guide is non-formal and intended for trained professionals, therefore some technical details may be omitted and/or the examples provided may not work or compile as-is. This guide is thought to be used along the official javadoc that include the specifics of every class and method of the Aeonics SDK. If you see ways to improve this guide or if you believe some notions would benefit from more details, please let us know.
Conventions
In this document, you will encounter different notations. Words highlighted in blue or orange are special keywords
that are important. Other words with a grey background
means that it references a technical term or a code-specific keyword.
There are also some code samples or configuration bits that are displayed in separate blocks. The color and header of the block provides some context on how to read the content.
This block type contains Java code
This block type contains other unspecified information
This documentation is constantly evolving, so sections or paragraphs highlighted in yellow means that some additional content should be delivered soon...
Next steps
Additional documentation and next steps are :
- Github: Some code samples and other material are publicly published on Github.
- Javadoc: The official Aeonics SDK javadoc.
- Technological Overview: The overview of core principles and terminology.
- Developer Guide: The main documentation about the context and the global system principles.
- Modules: Aeonics is a modular system and each module provides its own functionalities. This documentation lists the specific configuration parameters and detailed behavior of all officially supported module.
- Aeonics Frontend Framework (AFF): Aeonics uses its own lightweight frontend single page application framework. You can reuse this framework to build your own applications.
Creating Rest Endpoints
The RestEndpoint
class provides a simple and flexible way to define and handle RESTful endpoints in your application.
With RestEndpoint
, you can easily map URLs to methods and handle HTTP requests.
The inline-code approach of the RestEndpoint
offers several advantages over systems that rely on runtime annotations or other complex frameworks:
- Simplicity and Readability: developers define their endpoints and their corresponding logic directly in the code, making it easier to understand and maintain.
- Predictability: inline-coding require less configuration and boilerplate code compared to frameworks that heavily rely on runtime annotations. In the code sample below, the endpoints are defined using concise and readable syntax, without the need for additional annotations or complex configuration files. The behavior of the code is predictable and can be understood directly from a few lines of code.
- Flexibility and Extensibility: developers have more control over the logic and can easily extend the functionality as needed. In contrast, runtime annotation-based systems or restrictive frameworks may have limitations on the available features and customization options.
- Ease of Testing: Since the logic is directly written in the code, it becomes easier to test the endpoints and their associated functionality in isolation. Developers can write unit tests that directly call the handler methods and verify the expected behavior, without relying on complex setup or mocking of dependent services.
Creating Endpoints
To create an endpoint, you can instantiate the RestEndpoint
class and define the URL path and supported HTTP
methods in the constructor. Here's an example:
public Endpoint a = new RestEndpoint("/path/a", "GET") { public Data handle(Data parameters) { return Data.map().put("hello", "world"); } };
In this example, the endpoint /path/a is mapped to the HTTP GET method, and the handle() method is overridden to return a Data object with a key-value pair. (See working with Data).
Handling Requests
The handle() method is where you define the logic for processing incoming requests and generating the response.
It takes a Data
object as a parameter, which contains the request parameters.
You can access and manipulate these parameters as needed. Here's an example:
public Data handle(Data parameters) { return Data.map().put("hello", parameters.get("name")); }
In this case, the handle() method retrieves the value of the "name" parameter from the request parameters and includes it in the response.
Adding Parameters
You can add parameters to your endpoint to define specific requirements or constraints. The add() method allows you to add parameters to the endpoint, which are automatically validated. It does not matter if the parameter commes from the query string or from the request body, it will be provided directly to the handle() method. Here's an example:
public Endpoint c = new RestEndpoint("/path/c", "GET") { public Data handle(Data parameters) { return Data.map().put("hello", parameters.get("name")); } }.add(new Parameter("name").optional(false).max(50));
In this example, a required parameter named "name" is added to the endpoint. The parameter is set to be non-optional and has a maximum length of 50 characters.
Mapping Parameters in the URL
If you want to include parameters in the URL itself, you can use curly braces {}
to specify the parameter name.
The RestEndpoint
class will automatically extract the values from the URL and make them available in the handle() method.
Here's an example:
public Endpoint d = new RestEndpoint("/path/d/{name}", "GET") { public Data handle(Data parameters) { return Data.map().put("hello", parameters.get("name")); } }.add(new Parameter("name").optional(false).max(50));
In this example, the parameter "name" is specified in the URL path. When a request is made to /path/d/somevalue, the value "somevalue" will be available as the "name" parameter in the handle() method.
Error Handling
If an error occurs while processing a request, you can throw a RestException
with the desired HTTP response code.
Uncaught exceptions will be returned as a code 500 error. Here's an example:
public Endpoint e = new RestEndpoint("/path/e", "GET") { public Data handle(Data parameters) throws Exception { throw new RestException(400, "Some error happened"); } };
In this example, a RestException
with a response code of 400 and an error message is thrown when handling the request.
Documentation
You can provide documentation for your endpoints and parameters to make it easier for other developers to understand and use them.
The RestEndpoint
class provides methods to set the summary and description for the endpoint. Here's an example:
public Endpoint d = new RestEndpoint("/path/d/{name}", "GET") { public Data handle(Data parameters) { return Data.map().put("hello", parameters.get("name")); } }.add(new Parameter("name", "This is the description for the name parameter").optional(false).max(50)) .summary("Simple hello world") .description("This endpoint will greet you in JSON format using the provided name.");
In this example, the summary() method sets a brief summary for the endpoint,
while the description() method provides a detailed description. Additionally, the Parameter
constructor accepts a description parameter to document the purpose or usage of the parameter.
Security and Filtering
If you need to validate the user and filter the request or response, you can use the TriggerRestEndpoint
class instead of RestEndpoint
. The TriggerRestEndpoint
allows you to add security checks
and apply filters to the request and response in the form of triggers. Here's an example:
public Endpoint f = new TriggerRestEndpoint("/path/f/{name}", "GET") { public Data handle(Data parameters, User user) { return Data.map() .put("hello", parameters.get("name")) .put("you are", parameters.get("login")); } } .security((request, user) -> { if (user == User.ANONYMOUS) throw new SecurityException(); }) .before((request, user) -> { request.put("login", user.name()); }) .after((request, response, user) -> { response.put("modified", true); }) .add(new Parameter("name").optional(false).max(50));
In this example, the security() method sets a security check that throws a SecurityException
if the user is anonymous. The before() method is used to modify the request by adding the user's login name
to the request parameters. The after() method allows you to modify the response by adding or altering its content.
Each of these methods is optional.
In the before() method, you can manipulate the request parameters in place, or return an entire new set. Here is an example:
.before((request, user) -> { return Data.map() .put("key", "value"); })
In the after() method, you can manipulate the endpoint response, or override it entirely with a new one. Here is an example:
.after((request, response, user) -> { return Data.map() .put("new", "response"); })
Conclusion
You've now learned the basics of using the RestEndpoint
class to define and handle RESTful endpoints in your application.
With RestEndpoint
, you can easily map URLs to methods, handle requests, add parameters, handle errors, and provide documentation.
Additionally, the TriggerRestEndpoint
class extends the functionality by allowing user validation and request/response filtering.
Feel free to explore the various methods and options available in the RestEndpoint
class to customize and extend your RESTful API
implementation according to your application's needs. Happy coding!
Working with Data
The Data
class provides a flexible and convenient way to store and manipulate data in your Java applications.
It allows you to store various types of data and easily coerce them into different types when needed.
The Data
class works in parallel with the Encodable
, Decodable
, and Updatable
interfaces
provided by the aeonics.data
package.
When data is subject to frequent changes, using the Data class can offer advantages over an Object-Relational Mapping (ORM) approach.
- Flexibility: The Data class allows you to store data of different types dynamically. It provides a convenient way to handle data that doesn't adhere to a fixed schema. In contrast, other techniques typically require predefined mappings between objects and database tables, making it more challenging to handle flexible or evolving data structures.
- Prototyping: When working on prototypes or proof-of-concept projects, the Data class offers a quick and flexible way to handle data without the overhead of setting up and configuring lots of other components. It allows you to focus on experimenting with data structures and logic without being constrained by rigid mappings.
- Unflat Data Structures: If you're working with non-relational databases or data stores that don't fit the traditional table-based structure, using the Data class can be more suitable. It allows you to model and manipulate data in a way that aligns with the specific requirements of the data store. Moreover, it provides a convenient API for data manipulation, including methods for adding, accessing, modifying, and removing data.
Creating a Data object
There are different ways to create a Data
object from scratch: you can create a Data
object using the
Data.map() method, which creates a new empty map-like data structure, using Data.list() which creates
a new array-like structure, or using Data.wrap(value) which will encapsulate an axisting value in a Data
object.
Data data_map = Data.map(); Data data_list = Data.list(); Data data_value = Data.wrap("some value");
Adding Values
Once you have a Data
object, you can add values to it using the put() method if it is a map, or the add()
method if it is a list. You can store values of any type, such as strings, booleans, numbers, or even nested Data
objects:
data.put("foo", "bar"); data.put("test", true); data.put("answer", 42); data.put("array", Data.list().add(1).add("a").add(null));
Accessing and Manipulating Values
You can access the stored values in the Data
object using various methods. Here are some examples:
String fooValue = data.asString("foo"); int answerValue = data.asInt("answer"); boolean testValue = data.asBool("test");
If you do not know the exact native Java type, you can perform type coercion to convert values from one type to another automatically:
int intValue = Data.of("42").asInt(); // Coerces the string "42" to an integer (42) boolean boolValue = Data.of("true").asBool(); // Coerces the string "true" to a boolean (true) int boolToInt = Data.of(false).asInt(); // Coerces the boolean false to an integer (0) String boolToString = Data.of(true).asString(); // Coerces the boolean true to a string ("true")
Removing Values
If you need to remove a value from the Data
object, you can use the remove() method.
If you specify a String key, it will remove it from the map. If you provide a numerical index, it will remove it from the list:
data.remove("answer"); data.get("array").remove(2);
Merging Data
To combine two Data
objects, you can use the merge() method.
This method merges the contents of the specified Data
object into the current Data
object:
Data data2 = Data.map().put("hero", "batman"); data.merge(data2); // Merges the contents of data2 into the current data object
Checking Values and Types
The Data
class provides several methods to check the existence and type of values:
boolean containsFoo = data.containsKey("foo"); // Checks if the key "foo" exists boolean isFooNull = data.isNull("foo"); // Checks if the value associated with "foo" is null boolean isFooString = data.isString("foo"); // Checks if the value associated with "foo" is a string boolean isFooBool = data.isBool("foo"); // Checks if the value associated with "foo" is a boolean boolean isFooNumber = data.isNumber("foo"); // Checks if the value associated with "foo" is a number boolean isFooAnswer = data.is("foo", Answer.class); // Checks if the value associated with "foo" is of type Answer boolean isFooEmpty = data.isEmpty("foo"); // Checks if the value associated with "foo" is empty or null
Encoding and Decoding to Custom Classes
You can also encode and decode data to custom classes by implementing the Decodable
interface. Here's an example:
public static class Answer implements Decodable { int value; public void decode(Data data) { value = data.asInt(); } }
You can decode a Data
object into an instance of the Answer class using the as() method:
Answer answer = data.get("answer").as(Answer.class);
JSON Representation
The Data
class provides a convenient way to represent data in JSON format.
You can obtain the JSON representation of a Data
object using the toString() method:
String json = data.toString(); // Returns the JSON representation of the data object
You can also decode a JSON string into a Data
object using the Json.decode() method:
Data decodedData = Json.decode(json); // Decodes the JSON string into a Data object
Conclusion
The Data
class offers a convenient and flexible way to store and manipulate data in your Java applications.
Its ease of use and versatility make it a powerful tool for handling various data types and performing type coercion.
Whether you need to store simple values or complex nested structures, the Data
class provides a convenient
API to make your data handling tasks easier and more efficient.
Item, Registry, Factory
tutorial about the registry and factory